docs/mac-app-store-code-signing-guide.md
Related macOS docs:
- build-and-publish-notes.md -- Build/publish workflow (screenshots, iOS, Windows signing)
- update-mac-certificates.md -- Annual certificate renewal
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.
The Mac App Store build requires precise matching between:
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.
We had two distribution certificates in the keychain:
Apple Distribution: Johannes Millan (363FAFK383) - fingerprint 968086... (modern)3rd Party Mac Developer Application: Johannes Millan (363FAFK383) - fingerprint 3731BEC0... (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.
Create a new provisioning profile that uses the modern "Apple Distribution" certificate to match what electron-builder wants to use.
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) - fingerprint 968086...3rd Party Mac Developer Application: Johannes Millan (363FAFK383) - fingerprint 3731BEC0...Go to Apple Developer Portal - Profiles:
Click ➕ to create a new profile
Select: Distribution → Mac App Store Connect
CRITICAL: When selecting the certificate, choose:
Why this matters:
Select App ID: com.super-productivity.app
Download as mas.provisionprofile
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
# Copy provisioning profile to the correct location
cp ~/Downloads/mas.provisionprofile tools/mac-profiles/mas.provisionprofile
# 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:
mas_provision_profile# 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)
# 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
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 worknotarize: true - Only for Developer ID builds, not MAS- name: Build Electron app
run: npm run dist:mac:mas:buildOnly
Do NOT add:
CSC_NAME environment variable - Causes errors with modern certificatesCSC_FINGERPRINT environment variable - Ignored by electron-builderCause: 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.
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).
Common causes:
Still processing - Apple takes 5-30 minutes to process builds
Missing export compliance - Required for apps with encryption
Version/build number already used
package.json version matches the built appnpm version patchPlatform mismatch - Build doesn't match the selected platform
| 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.
When your certificates expire (annually), follow these steps:
Create new certificates in Apple Developer Portal
Create new provisioning profile with the new certificates
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
# 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"
docs/update-mac-certificates.md - Detailed certificate management guide✅ 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.