docs: add comprehensive Mac App Store code signing guide

Documents the complete solution for certificate/provisioning profile matching issues with electron-builder and Apple's MAS validation.
This commit is contained in:
johannesjo 2025-11-10 15:59:24 +01:00
parent 73ea2bcae3
commit 348dfeac82
7 changed files with 333 additions and 6 deletions

View file

@ -1,3 +1,5 @@
## [16.3.2](https://github.com/johannesjo/super-productivity/compare/v16.3.1...v16.3.2) (2025-11-10)
## [16.3.1](https://github.com/johannesjo/super-productivity/compare/v16.3.0...v16.3.1) (2025-11-10)
### Reverts

View file

@ -20,8 +20,8 @@ android {
minSdkVersion 24
targetSdkVersion 35
compileSdk 35
versionCode 16_03_01_0000
versionName "16.3.1"
versionCode 16_03_02_0000
versionName "16.3.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
hostName : "app.super-productivity.com",

View file

@ -0,0 +1,323 @@
# 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:
1. The **code signing certificate** used by electron-builder
2. 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:
1. `Apple Distribution: Johannes Millan (363FAFK383)` - fingerprint `968086...` (modern)
2. `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.
### 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:
```bash
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...`
### 2. Create New Provisioning Profile
Go to [Apple Developer Portal - Profiles](https://developer.apple.com/account/resources/profiles/list):
1. Click **** to create a new profile
2. Select: **Distribution** → **Mac App Store Connect**
3. **Important**: Choose certificate: **"Johannes Millan (Distribution)"** (NOT "Mac App Distribution")
- "Distribution" = modern "Apple Distribution" certificate ✅
- "Mac App Distribution" = legacy "3rd Party Mac Developer Application" ❌
4. Select App ID: `com.super-productivity.app`
5. Download as `mas.provisionprofile`
### 3. Verify the Provisioning Profile
Check which certificate is in the provisioning profile:
```bash
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
```bash
# Copy provisioning profile to the correct location
cp ~/Downloads/mas.provisionprofile tools/mac-profiles/mas.provisionprofile
```
### 5. Update GitHub Actions Secret
```bash
# 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:
1. Go to: https://github.com/johannesjo/super-productivity/settings/secrets/actions
2. Find `mas_provision_profile`
3. Click **Update**
4. Paste the base64-encoded content
5. Click **Save**
### 6. Test Locally
```bash
# 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
```bash
# 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
```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
```yaml
- name: Build Electron app
run: npm run dist:mac:mas:buildOnly
```
**Do NOT add:**
- ❌ `CSC_NAME` environment variable - Causes errors with modern certificates
- ❌ `CSC_FINGERPRINT` environment 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:**
```bash
# 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:**
1. **Still processing** - Apple takes 5-30 minutes to process builds
- Wait and refresh the page periodically
2. **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
3. **Version/build number already used**
- Check `package.json` version matches the built app
- Increment version if needed: `npm version patch`
4. **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 | Internal Name | Electron-builder Preference | Use Case |
| ---------------------------------------- | ----------------------------------- | --------------------------- | ------------------- |
| "Johannes Millan (Distribution)" | Apple Distribution | ✅ Preferred | MAS builds (modern) |
| "Johannes Millan (Mac App Distribution)" | 3rd Party Mac Developer Application | ❌ Legacy | MAS builds (old) |
| "Developer ID Application" | Developer ID Application | N/A | Direct download DMG |
**Key Insight:** Even though both "Distribution" and "Mac App Distribution" are for Mac App Store, electron-builder prefers the modern "Apple Distribution" certificate.
## Annual Certificate Renewal
When your certificates expire (annually), follow these steps:
1. **Create new certificates** in Apple Developer Portal
- Apple Distribution (for MAS)
- Mac Installer Distribution (for PKG signing)
2. **Create new provisioning profile** with the new certificates
- Type: Mac App Store Connect
- Certificate: "Johannes Millan (Distribution)" ← Use the new one
3. **Export all certificates** to PKCS#12
```bash
# 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
```
4. **Update GitHub Actions secrets**
```bash
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
```
5. **Test locally** before pushing
## Quick Diagnostic Commands
```bash
# 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](https://developer.apple.com/account/resources/certificates/list)
- [Electron Builder MAS Config](https://www.electron.build/configuration/mas)
- [Apple Code Signing Guide](https://developer.apple.com/support/code-signing/)
- 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.

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "superProductivity",
"version": "16.3.1",
"version": "16.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "superProductivity",
"version": "16.3.1",
"version": "16.3.2",
"license": "MIT",
"workspaces": [
"packages/*"

View file

@ -1,6 +1,6 @@
{
"name": "superProductivity",
"version": "16.3.1",
"version": "16.3.2",
"description": "ToDo list and Time Tracking",
"keywords": [
"ToDo",

View file

@ -1,6 +1,6 @@
// this file is automatically generated by git.version.ts script
export const versions = {
version: '16.3.1',
version: '16.3.2',
revision: 'NO_REV',
branch: 'NO_BRANCH',
};