12 KiB
Mac App Store Code Signing Guide
This document explains the Mac App Store (MAS) code signing setup and troubleshooting for Super Productivity. It covers the complete solution to certificate/provisioning profile mismatches.
Overview
The Mac App Store build requires precise matching between:
- The code signing certificate used by electron-builder
- The certificate embedded in the provisioning profile
Any mismatch will cause Apple's validation to fail with errors like:
Invalid Code Signing. The executable '...' must be signed with the certificate
that is contained in the provisioning profile.
The Root Problem (and Solution)
What Was Happening
We had two distribution certificates in the keychain:
Apple Distribution: Johannes Millan (363FAFK383)- fingerprint968086...(modern)3rd Party Mac Developer Application: Johannes Millan (363FAFK383)- fingerprint3731BEC0...(legacy)
Electron-builder always prefers "Apple Distribution" over "3rd Party Mac Developer Application" when both are present, regardless of environment variables or config settings.
Our provisioning profile contained the legacy "3rd Party Mac Developer Application" certificate, but electron-builder was signing with "Apple Distribution" → mismatch → validation failure.
The Solution
Create a new provisioning profile that uses the modern "Apple Distribution" certificate to match what electron-builder wants to use.
Step-by-Step Setup
1. Verify Your Certificates
Check which certificates are in your keychain:
security find-identity -v -p codesigning | grep -E "Apple Distribution|3rd Party"
You should see:
Apple Distribution: Johannes Millan (363FAFK383)- fingerprint968086...3rd Party Mac Developer Application: Johannes Millan (363FAFK383)- fingerprint3731BEC0...
2. Create New Provisioning Profile
Go to Apple Developer Portal - Profiles:
-
Click ➕ to create a new profile
-
Select: Distribution → Mac App Store Connect
-
CRITICAL: When selecting the certificate, choose:
- ✅ "Johannes Millan (Distribution)" - Shows "For use in Xcode 11 or later"
- ❌ NOT "Johannes Millan (Mac App Distribution)"
Why this matters:
- "Johannes Millan (Distribution)" = modern "Apple Distribution" certificate → What electron-builder will use ✅
- "Johannes Millan (Mac App Distribution)" = legacy "3rd Party Mac Developer Application" → Will cause mismatch ❌
-
Select App ID:
com.super-productivity.app -
Download as
mas.provisionprofile
3. Verify the Provisioning Profile
Check which certificate is in the provisioning profile:
python3 << 'EOF'
import plistlib
import subprocess
import hashlib
result = subprocess.run(['security', 'cms', '-D', '-i', 'tools/mac-profiles/mas.provisionprofile'],
capture_output=True)
plist_data = plistlib.loads(result.stdout)
cert_data = plist_data['DeveloperCertificates'][0]
fingerprint = hashlib.sha1(cert_data).hexdigest().upper()
with open('/tmp/cert.der', 'wb') as f:
f.write(cert_data)
result = subprocess.run(['openssl', 'x509', '-in', '/tmp/cert.der', '-inform', 'DER',
'-noout', '-subject'],
capture_output=True, text=True)
print("Certificate in provisioning profile:")
print(result.stdout)
print(f"Fingerprint: {fingerprint}")
print(f"\nExpected: 968086560EC4643B4192E7755CBF7D6E009334F4 (Apple Distribution)")
EOF
Expected output:
Certificate in provisioning profile:
subject=UID=363FAFK383, CN=Apple Distribution: Johannes Millan (363FAFK383), ...
Fingerprint: 968086560EC4643B4192E7755CBF7D6E009334F4
4. Update Local Files
# Copy provisioning profile to the correct location
cp ~/Downloads/mas.provisionprofile tools/mac-profiles/mas.provisionprofile
5. Update GitHub Actions Secret
# Base64 encode the provisioning profile
base64 -i tools/mac-profiles/mas.provisionprofile -o /tmp/mas-profile.b64
# Copy to clipboard
cat /tmp/mas-profile.b64 | pbcopy
Then update the GitHub Actions secret:
- Go to: https://github.com/johannesjo/super-productivity/settings/secrets/actions
- Find
mas_provision_profile - Click Update
- Paste the base64-encoded content
- Click Save
6. Test Locally
# Copy provisioning profile to root
cp tools/mac-profiles/mas.provisionprofile embedded.provisionprofile
# Build
npm run dist:mac:mas:buildOnly 2>&1 | grep -E "signing.*platform=mas"
Expected output:
• signing file=.tmp/app-builds/mas-universal/Super Productivity.app
platform=mas type=distribution
identityName=Apple Distribution: Johannes Millan (363FAFK383)
identityHash=968086560EC4643B4192E7755CBF7D6E009334F4
provisioningProfile=embedded.provisionprofile
✅ identityHash should be 968086... (Apple Distribution)
7. Verify the Built Package
# Check package signature
pkgutil --check-signature .tmp/app-builds/mas-universal/superProductivity-universal.pkg
Should show: Status: signed by a developer certificate issued by Apple
Configuration Files
build/electron-builder.mas.yaml
mas:
type: distribution # Important: explicitly set type
appId: com.super-productivity.app
category: public.app-category.productivity
icon: build/icon-mac.icns
gatekeeperAssess: false
darkModeSupport: true
hardenedRuntime: false
entitlements: build/entitlements.mas.plist
entitlementsInherit: build/entitlements.mas.inherit.plist
provisioningProfile: embedded.provisionprofile
Do NOT add:
- ❌
identity: '...'- Causes errors and doesn't work - ❌
notarize: true- Only for Developer ID builds, not MAS
GitHub Actions Workflow
- name: Build Electron app
run: npm run dist:mac:mas:buildOnly
Do NOT add:
- ❌
CSC_NAMEenvironment variable - Causes errors with modern certificates - ❌
CSC_FINGERPRINTenvironment variable - Ignored by electron-builder
Troubleshooting
Error: "Please remove prefix '3rd Party Mac Developer Application:' from the specified name"
Cause: You set CSC_NAME or identity with the full certificate type prefix.
Solution: Remove the CSC_NAME environment variable or identity config field entirely. Let electron-builder auto-discover the certificate.
Error: "Invalid Code Signing. The executable '...' must be signed with the certificate that is contained in the provisioning profile"
Cause: Certificate mismatch between what electron-builder is using and what's in the provisioning profile.
Diagnosis:
# 1. Check which certificate electron-builder is using
npm run dist:mac:mas:buildOnly 2>&1 | grep "signing.*platform=mas"
# 2. Check which certificate is in the provisioning profile
python3 << 'EOF'
import plistlib, subprocess, hashlib
result = subprocess.run(['security', 'cms', '-D', '-i', 'tools/mac-profiles/mas.provisionprofile'], capture_output=True)
plist_data = plistlib.loads(result.stdout)
cert_data = plist_data['DeveloperCertificates'][0]
print(f"Profile certificate fingerprint: {hashlib.sha1(cert_data).hexdigest().upper()}")
EOF
# 3. Compare the fingerprints - they must match!
Solution: Create a new provisioning profile with the certificate that electron-builder is using (usually Apple Distribution).
Build Uploads Successfully But Can't Select in App Store Connect
Common causes:
-
Still processing - Apple takes 5-30 minutes to process builds
- Wait and refresh the page periodically
-
Missing export compliance - Required for apps with encryption
- Go to App Store Connect → Your App → TestFlight
- Click on the build, answer the export compliance questions
-
Version/build number already used
- Check
package.jsonversion matches the built app - Increment version if needed:
npm version patch
- Check
-
Platform mismatch - Build doesn't match the selected platform
- Ensure you're selecting from the correct platform tab (macOS)
Certificate Types Reference
| Apple Portal Name | Portal Description | Internal Name | Electron-builder Preference | Use Case |
|---|---|---|---|---|
| "Johannes Millan (Distribution)" | "For use in Xcode 11 or later" | Apple Distribution | ✅ Preferred | MAS builds (modern) |
| "Johannes Millan (Mac App Distribution)" | No special description | 3rd Party Mac Developer Application | ❌ Legacy | MAS builds (old) |
| "Developer ID Application" | N/A | Developer ID Application | N/A | Direct download DMG |
Key Insight: In the Apple Developer Portal, look for the certificate that says "For use in Xcode 11 or later" - this is the modern "Apple Distribution" certificate that electron-builder will use. The one without this description is the legacy certificate.
Annual Certificate Renewal
When your certificates expire (annually), follow these steps:
-
Create new certificates in Apple Developer Portal
- Apple Distribution (for MAS)
- Mac Installer Distribution (for PKG signing)
-
Create new provisioning profile with the new certificates
- Type: Mac App Store Connect
- Certificate: "Johannes Millan (Distribution)" ← Use the new one
-
Export all certificates to PKCS#12
# Export from Keychain Access or use security command security export -k ~/Library/Keychains/login.keychain-db \ -t identities -f pkcs12 -P "$PASSWORD" \ -o all-certs.p12 -
Update GitHub Actions secrets
base64 -i all-certs.p12 -o all-certs.b64 # Update MAC_CERTS secret with contents of all-certs.b64 base64 -i tools/mac-profiles/mas.provisionprofile -o mas-profile.b64 # Update mas_provision_profile secret with contents of mas-profile.b64 -
Test locally before pushing
Quick Diagnostic Commands
# Check available certificates
security find-identity -v -p codesigning
# Check what electron-builder will use
npm run dist:mac:mas:buildOnly 2>&1 | grep "signing.*platform=mas"
# Check provisioning profile certificate
security cms -D -i tools/mac-profiles/mas.provisionprofile | \
plutil -p - | grep -A 5 "DeveloperCertificates"
# Verify built package
pkgutil --check-signature .tmp/app-builds/mas-universal/*.pkg
# Check app version/build
plutil -p ".tmp/app-builds/mas-universal/Super Productivity.app/Contents/Info.plist" | \
grep -E "CFBundleVersion|CFBundleShortVersionString"
Related Documentation
- Apple Developer Portal
- Electron Builder MAS Config
- Apple Code Signing Guide
- Internal:
docs/update-mac-certificates.md- Detailed certificate management guide
Summary
✅ The key to success: Ensure your provisioning profile uses the "Apple Distribution" certificate (modern "Distribution" type), not the legacy "3rd Party Mac Developer Application" certificate, because electron-builder always prefers Apple Distribution when both are available.
❌ What doesn't work: Trying to force electron-builder to use the legacy certificate through environment variables or config options - it will ignore them and use Apple Distribution anyway.