Skip to main content

Overview

macOS notarization is a security process required by Apple for all software distributed outside the Mac App Store. Starting with macOS 10.15 (Catalina), all software must be notarized to run without warnings.
Notarization is automatic in CI/CD when credentials are configured. This guide covers setup and troubleshooting.

Why Notarization is Required

Without notarization, users see this warning when opening your app:
“MQTT Explorer.app” cannot be opened because the developer cannot be verified.
Notarized apps:
  • Run without security warnings
  • Meet Apple’s security requirements
  • Pass Gatekeeper validation
  • Can be distributed via direct download or DMG
Notarization requires an active Apple Developer Program membership ($99/year).

Prerequisites

1

Apple Developer Account

Enroll in the Apple Developer Program:
  • Individual or Organization account
  • Active membership ($99 USD/year)
2

Developer ID Certificate

Install Developer ID Application certificate:
  1. Log in to Apple Developer
  2. Navigate to Certificates, IDs & Profiles
  3. Create “Developer ID Application” certificate
  4. Download and install in Keychain Access
Verify installation:
security find-identity -v -p codesigning
# Should show: "Developer ID Application: Your Name (TEAM_ID)"
3

Xcode Command Line Tools

Required for notarization on build machines:
xcode-select --install

Setup for GitHub Actions

1. Create App-Specific Password

1

Generate password

  1. Sign in to appleid.apple.com
  2. Navigate to Sign-In and Security
  3. Under App-Specific Passwords, click Generate an app-specific password
  4. Enter a descriptive name: MQTT Explorer Notarization
  5. Copy the generated password (format: xxxx-xxxx-xxxx-xxxx)
Save this password immediately - you cannot view it again!

2. Find Your Team ID

1

Locate Team ID

  1. Sign in to developer.apple.com/account
  2. Navigate to Membership Details
  3. Copy your Team ID (10-character alphanumeric string)
Example: A1B2C3D4E5

3. Configure GitHub Secrets

Add three secrets to your GitHub repository:
1

Navigate to repository settings

  1. Go to your repository on GitHub
  2. Click SettingsSecrets and variablesActions
  3. Click New repository secret
2

Add APPLE_ID

Name: APPLE_IDValue: Your Apple ID email (e.g., your.email@example.com)
3

Add APPLE_APP_SPECIFIC_PASSWORD

Name: APPLE_APP_SPECIFIC_PASSWORDValue: The app-specific password from step 1 (e.g., xxxx-xxxx-xxxx-xxxx)
4

Add APPLE_TEAM_ID

Name: APPLE_TEAM_IDValue: Your Team ID from step 2 (e.g., A1B2C3D4E5)

How Notarization Works

Build Configuration

Notarization is configured in package.json:
package.json
{
  "build": {
    "mac": {
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "entitlements": "res/entitlements.mac.plist",
      "entitlementsInherit": "res/entitlements.mac.inherit.plist"
    },
    "afterSign": "./dist/scripts/notarize.js"
  }
}
Key settings:
  • hardenedRuntime: true - Enables hardened runtime (required for notarization)
  • gatekeeperAssess: false - Skips Gatekeeper assessment during build
  • afterSign - Runs notarization script after code signing

Notarization Script

The scripts/notarize.ts script handles the notarization process:
scripts/notarize.ts
import { notarize } from '@electron/notarize'

export default async function notarizing(context: Context) {
  const { electronPlatformName, appOutDir } = context
  
  // Only notarize macOS builds
  if (electronPlatformName !== 'darwin') {
    return
  }
  
  // Check for required environment variables
  const appleId = process.env.APPLE_ID
  const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD
  const teamId = process.env.APPLE_TEAM_ID
  
  if (!appleId || !appleIdPassword || !teamId) {
    console.warn('Skipping notarization: credentials not set')
    return
  }
  
  const appPath = path.join(appOutDir, `${appName}.app`)
  
  console.log(`Notarizing ${appPath}...`)
  
  await notarize({
    appPath,
    appleId,
    appleIdPassword,
    teamId,
  })
  
  console.log('Notarization successful!')
}

Process Flow

1

Code Signing

electron-builder signs the app with your Developer ID certificate:
codesign --sign "Developer ID Application: ..." \
  --options runtime \
  --entitlements res/entitlements.mac.plist \
  MQTT\ Explorer.app
2

Submit to Apple

The notarization script uploads the app to Apple’s servers:
xcrun notarytool submit MQTT\ Explorer.app \
  --apple-id your.email@example.com \
  --password xxxx-xxxx-xxxx-xxxx \
  --team-id A1B2C3D4E5 \
  --wait
Apple scans for malware and validates code signing.
3

Staple Ticket

After approval, the notarization ticket is stapled to the app:
xcrun stapler staple MQTT\ Explorer.app
This embeds the notarization ticket so users can open the app offline.
4

Package DMG

electron-builder creates the final DMG with the notarized app.
Notarization typically takes 1-5 minutes. The --wait flag blocks until Apple’s servers respond.

Entitlements

Entitlements define the permissions your app requires.

DMG Build Entitlements

Parent App: res/entitlements.mac.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.network.client</key>
  <true/>
  <key>com.apple.security.network.server</key>
  <true/>
</dict>
</plist>
Child Processes: res/entitlements.mac.inherit.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
</dict>
</plist>

Mac App Store Entitlements

MAS Build: res/entitlements.mas.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.app-sandbox</key>
  <true/>
  <key>com.apple.security.network.client</key>
  <true/>
  <key>com.apple.security.network.server</key>
  <true/>
  <key>com.apple.security.files.user-selected.read-write</key>
  <true/>
</dict>
</plist>
Mac App Store builds require full sandboxing (com.apple.security.app-sandbox).

Entitlement Selection

The package.ts script selects entitlements based on build type:
package.ts
if (buildInfo.package === 'mas') {
  // MAS builds use sandboxed entitlements
  dotProp.set(packageJson, 'build.mac.entitlements', 'res/entitlements.mas.plist')
  dotProp.set(packageJson, 'build.mac.entitlementsInherit', 'res/entitlements.mas.plist')
} else {
  // DMG builds use different entitlements for parent and child
  dotProp.set(packageJson, 'build.mac.entitlements', 'res/entitlements.mac.plist')
  dotProp.set(packageJson, 'build.mac.entitlementsInherit', 'res/entitlements.mac.inherit.plist')
}

Local Testing

Test notarization locally before committing to CI/CD:
1

Set environment variables

export APPLE_ID="your.email@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="A1B2C3D4E5"
2

Build and package

yarn build
yarn prepare-release
yarn package mac
Watch for notarization output:
Notarizing /path/to/MQTT Explorer.app...
Submitting to Apple...
Waiting for response...
Notarization successful!
Stapling ticket...
Done.
3

Verify notarization

Check if the app is notarized:
spctl -a -vv "build/mac/MQTT Explorer.app"
Expected output:
build/mac/MQTT Explorer.app: accepted
source=Notarized Developer ID
4

Test stapling

Verify the ticket is stapled:
stapler validate "build/mac/MQTT Explorer.app"
Expected output:
The validate action worked!

CI/CD Integration

Notarization runs automatically in .github/workflows/platform-builds.yml:
.github/workflows/platform-builds.yml
jobs:
  build:
    strategy:
      matrix:
        build:
          - os: macos-latest
            task: mac
    runs-on: ${{ matrix.build.os }}
    steps:
      - run: yarn package ${{ matrix.build.task }}
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
The macOS runner:
  1. Builds the app with electron-builder
  2. Signs with Developer ID certificate (auto-detected from Keychain)
  3. Runs scripts/notarize.ts after signing
  4. Uploads notarized DMG to GitHub Releases
GitHub-hosted macOS runners include Xcode and can access certificates in the Keychain.

Checking Notarization Status

Check Notarization History

View recent notarization submissions:
xcrun notarytool history \
  --apple-id your.email@example.com \
  --team-id A1B2C3D4E5
Output:
ID: abc-123-def
Date: 2024-03-05 10:30:00 UTC
Name: MQTT Explorer.app
Status: Accepted

Check Specific Submission

Get detailed logs for a submission:
xcrun notarytool log abc-123-def \
  --apple-id your.email@example.com \
  --team-id A1B2C3D4E5

Verify Installed App

Check if an installed app is notarized:
# Check notarization status
spctl -a -vv "/Applications/MQTT Explorer.app"

# Check code signature
codesign -dvv "/Applications/MQTT Explorer.app"

Troubleshooting

Symptom: Notarization fails with “Invalid” statusCheck logs:
xcrun notarytool log <submission-id> \
  --apple-id your.email@example.com \
  --team-id A1B2C3D4E5
Common issues:
  • Missing hardened runtime: Set hardenedRuntime: true in package.json
  • Invalid entitlements: Verify plist files are well-formed XML
  • Unsigned frameworks: Ensure all dependencies are code-signed
  • Malware detected: Scan your code for suspicious patterns
Symptom: Error: Unable to authenticateVerify credentials:
echo $APPLE_ID
echo $APPLE_TEAM_ID
# Don't echo password (security risk)
Solutions:
  • Regenerate app-specific password at appleid.apple.com
  • Update GitHub secrets with new password
  • Verify Team ID matches your developer account
Symptom: No identity found for signingCheck certificates:
security find-identity -v -p codesigning
Solutions:
  • Install Developer ID Application certificate
  • Import certificate to Keychain Access
  • Verify certificate is not expired
  • On CI: Ensure certificate is uploaded to runner secrets
Symptom: Notarization hangs or times outApple’s servers are slow: This is normal. Notarization can take 1-10 minutes.Workaround:
  • Increase timeout in GitHub Actions (default: 60 minutes)
  • Run xcrun notarytool wait <submission-id> to resume
Symptom: User sees “cannot be opened because the developer cannot be verified”Verify notarization:
spctl -a -vv "/Applications/MQTT Explorer.app"
Solutions:
  • Re-notarize the app
  • Ensure ticket is stapled: stapler validate app.app
  • User workaround: Right-click app, select “Open”, click “Open”

Security Best Practices

Protect Credentials

  • Never commit Apple credentials to git
  • Use app-specific passwords, not main password
  • Rotate passwords periodically
  • Limit GitHub secret access to admins

Validate Before Release

  • Test notarized builds locally
  • Verify app launches without warnings
  • Check Gatekeeper status with spctl
  • Install and test on clean macOS VM

Monitor Apple's Requirements

  • Apple updates notarization requirements annually
  • Subscribe to Apple Developer News
  • Update Xcode and notarytool regularly

Keep Certificates Valid

  • Developer ID certificates expire in 5 years
  • Renew before expiration to avoid interruptions
  • Update GitHub CI/CD with renewed certificate

Mac App Store Distribution

For Mac App Store submission (separate from notarization):
1

Use MAS entitlements

yarn package mac
# package.ts automatically selects entitlements.mas.plist
2

Upload to App Store Connect

xcrun altool --upload-app \
  --type macos \
  --file "build/MQTT Explorer.pkg" \
  --username your.email@example.com \
  --password xxxx-xxxx-xxxx-xxxx
3

Submit for review

Complete submission in App Store Connect:
  • Add screenshots
  • Write app description
  • Set pricing
  • Submit for review
Mac App Store apps undergo a separate review process in addition to notarization.

References

Next Steps

Releases

Automate notarized releases with semantic versioning

Packaging

Learn about packaging for all platforms