Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Notarization Requirements

Notarization is Apple’s automated security scanning service for software distributed outside the Mac App Store. When you notarize your software, Apple scans it for malware and known security issues, then issues a “ticket” that Gatekeeper recognizes. Since macOS 10.15 Catalina, notarization is required for all Developer ID signed software to pass Gatekeeper without warnings.

What Notarization Does

┌─────────────────────────────────────────────────────────────────┐
│                    Notarization Process                          │
├─────────────────────────────────────────────────────────────────┤
│  1. Developer uploads signed app to Apple                        │
│                         ↓                                        │
│  2. Apple performs automated security scans:                     │
│     - Malware detection                                          │
│     - Hardened runtime verification                              │
│     - Signature validation                                       │
│     - Unsafe API usage detection                                 │
│                         ↓                                        │
│  3. Apple issues notarization ticket                             │
│                         ↓                                        │
│  4. Developer staples ticket to software (optional)              │
│                         ↓                                        │
│  5. User downloads software                                      │
│                         ↓                                        │
│  6. Gatekeeper verifies:                                         │
│     - Valid Developer ID signature                               │
│     - Notarization ticket (from staple or Apple servers)         │
└─────────────────────────────────────────────────────────────────┘

Notarization Requirements

To successfully notarize software, it must meet these requirements:

Code Signing Requirements

  1. Signed with Developer ID certificate (not Apple Development)
  2. Hardened runtime enabled (--options runtime)
  3. Secure timestamp included (--timestamp)
  4. All nested code must be signed (frameworks, helpers, plugins)

Technical Requirements

# The app must be signed with these minimum options:
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  --options runtime \
  MyApp.app

# Verify hardened runtime is enabled
$ codesign -dv MyApp.app 2>&1 | grep flags
CodeDirectory v=20500 size=1234 flags=0x10000(runtime) hashes=42+7 location=embedded

Common Notarization Blockers

IssueSolution
No hardened runtimeAdd --options runtime to codesign
Missing timestampAdd --timestamp to codesign
Unsigned nested codeSign all frameworks and helpers
Invalid signatureRe-sign with valid Developer ID
Forbidden entitlementsRemove or get Apple approval
Insecure library loadingUse @rpath or hardcoded paths

The notarytool Command

Apple’s current notarization tool is notarytool, included with Xcode command-line tools. It replaced the older altool starting with Xcode 13.

Storing Credentials

Create an app-specific password at appleid.apple.com, then store credentials:

# Store credentials in keychain (recommended for scripts)
$ xcrun notarytool store-credentials "notarization-profile" \
  --apple-id "your@email.com" \
  --team-id "TEAMID123" \
  --password "app-specific-password"

# Credentials are stored in keychain
# Profile name can be anything meaningful to you

# Verify stored credentials
$ security find-generic-password -l "notarization-profile" -w 2>/dev/null && \
  echo "Credentials stored successfully"

Submitting for Notarization

Notarization works with zip archives, disk images, or installer packages:

# Create a zip archive of your app
$ ditto -c -k --keepParent MyApp.app MyApp.zip

# Submit for notarization using stored credentials
$ xcrun notarytool submit MyApp.zip \
  --keychain-profile "notarization-profile" \
  --wait

# Output shows submission progress:
# Conducting pre-submission checks for MyApp.zip and initiating connection to the Apple notary service...
# Submission ID received
#   id: 12345678-1234-1234-1234-123456789012
# Successfully uploaded file
# Waiting for processing to complete.
# ....
# Processing complete
#   id: 12345678-1234-1234-1234-123456789012
#   status: Accepted

# Submit without waiting (returns immediately)
$ xcrun notarytool submit MyApp.zip \
  --keychain-profile "notarization-profile"
# Save the submission ID to check status later

Checking Submission Status

# Check status of a submission
$ xcrun notarytool info 12345678-1234-1234-1234-123456789012 \
  --keychain-profile "notarization-profile"

# Get detailed log (useful when submission fails)
$ xcrun notarytool log 12345678-1234-1234-1234-123456789012 \
  --keychain-profile "notarization-profile"

# Save log to file for analysis
$ xcrun notarytool log 12345678-1234-1234-1234-123456789012 \
  --keychain-profile "notarization-profile" \
  developer_log.json

# View submission history
$ xcrun notarytool history --keychain-profile "notarization-profile"

Notarization Log Analysis

When notarization fails, the log provides details:

# Download and examine the log
$ xcrun notarytool log SUBMISSION_ID \
  --keychain-profile "notarization-profile" \
  log.json

# View formatted log
$ cat log.json | python3 -m json.tool

# Common issues in logs:
# - "The signature does not include a secure timestamp"
# - "The executable does not have the hardened runtime enabled"
# - "The binary is not signed with a valid Developer ID certificate"
# - "The signature of the binary is invalid"

Stapling

Stapling attaches the notarization ticket directly to your software, allowing offline verification:

# Staple ticket to an app
$ xcrun stapler staple MyApp.app
Processing: /path/to/MyApp.app
Processing: /path/to/MyApp.app
The staple and validate action worked!

# Staple to a disk image
$ xcrun stapler staple MyApp.dmg

# Staple to an installer package
$ xcrun stapler staple MyInstaller.pkg

# Verify stapling
$ xcrun stapler validate MyApp.app
Processing: /path/to/MyApp.app
The validate action worked!

# Check if an app has a stapled ticket
$ spctl -a -vvv MyApp.app 2>&1 | grep -i "ticket"

Why Stapling Matters

Without stapling:

  • Gatekeeper must contact Apple’s servers on first launch
  • Won’t work if user is offline
  • Slower first-launch experience

With stapling:

  • Offline verification possible
  • Faster first launch
  • Better user experience

Complete Notarization Workflow

Here’s a complete script for signing, notarizing, and stapling:

#!/bin/bash
# notarize.sh - Complete notarization workflow

set -e

APP_NAME="MyApp"
APP_PATH="./build/${APP_NAME}.app"
DEVELOPER_ID="Developer ID Application: Your Name (TEAMID)"
KEYCHAIN_PROFILE="notarization-profile"
BUNDLE_ID="com.example.myapp"

echo "=== Step 1: Sign the application ==="
# Sign all nested components first
find "$APP_PATH" -name "*.framework" -exec \
  codesign -s "$DEVELOPER_ID" --timestamp --options runtime {} \;

find "$APP_PATH" -name "*.dylib" -exec \
  codesign -s "$DEVELOPER_ID" --timestamp --options runtime {} \;

# Sign helper tools
if [[ -d "$APP_PATH/Contents/Library/LoginItems" ]]; then
  find "$APP_PATH/Contents/Library/LoginItems" -name "*.app" -exec \
    codesign -s "$DEVELOPER_ID" --timestamp --options runtime {} \;
fi

# Sign the main app
codesign -s "$DEVELOPER_ID" \
  --timestamp \
  --options runtime \
  --entitlements entitlements.plist \
  "$APP_PATH"

echo "=== Step 2: Verify signature ==="
codesign -vvv --deep --strict "$APP_PATH"

echo "=== Step 3: Create ZIP for notarization ==="
ZIP_PATH="./build/${APP_NAME}.zip"
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"

echo "=== Step 4: Submit for notarization ==="
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
  --keychain-profile "$KEYCHAIN_PROFILE" \
  --wait 2>&1)

echo "$SUBMIT_OUTPUT"

# Check if successful
if echo "$SUBMIT_OUTPUT" | grep -q "status: Accepted"; then
  echo "=== Step 5: Staple the ticket ==="
  xcrun stapler staple "$APP_PATH"

  echo "=== Step 6: Verify final product ==="
  spctl -a -vvv "$APP_PATH"

  echo "=== Notarization complete! ==="
else
  echo "=== Notarization failed ==="
  # Extract submission ID and get log
  SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
  if [[ -n "$SUBMISSION_ID" ]]; then
    echo "Fetching detailed log..."
    xcrun notarytool log "$SUBMISSION_ID" \
      --keychain-profile "$KEYCHAIN_PROFILE"
  fi
  exit 1
fi

Notarizing Disk Images

For distributing via DMG:

# Create the DMG
$ hdiutil create -volname "MyApp" \
  -srcfolder MyApp.app \
  -ov -format UDZO \
  MyApp.dmg

# Sign the DMG
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  MyApp.dmg

# Submit for notarization
$ xcrun notarytool submit MyApp.dmg \
  --keychain-profile "notarization-profile" \
  --wait

# Staple the ticket
$ xcrun stapler staple MyApp.dmg

Notarizing Installer Packages

# Build the package with pkgbuild
$ pkgbuild --root ./payload \
  --identifier com.example.myapp \
  --version 1.0 \
  --install-location /Applications \
  MyApp-unsigned.pkg

# Sign with Developer ID Installer certificate
$ productsign --sign "Developer ID Installer: Your Name (TEAMID)" \
  MyApp-unsigned.pkg \
  MyApp.pkg

# Submit for notarization
$ xcrun notarytool submit MyApp.pkg \
  --keychain-profile "notarization-profile" \
  --wait

# Staple
$ xcrun stapler staple MyApp.pkg

Notarizing Command-Line Tools

Command-line tools can also be notarized:

# Sign the binary
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  --options runtime \
  mytool

# Create a zip for submission
$ zip mytool.zip mytool

# Submit
$ xcrun notarytool submit mytool.zip \
  --keychain-profile "notarization-profile" \
  --wait

# Note: Can't staple directly to a binary
# Distribute as signed zip or embed in pkg/dmg

Checking Notarization Status

Verify that distributed software is properly notarized:

# Check if an app is notarized
$ spctl -a -vvv /Applications/SomeApp.app
/Applications/SomeApp.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: Company Name (TEAMID)

# If not notarized, you'll see:
# source=Developer ID
# (without "Notarized" prefix)

# Check for stapled ticket
$ stapler validate /Applications/SomeApp.app
Processing: /Applications/SomeApp.app
The validate action worked!

# If no ticket stapled:
# The validate action worked!
# (but without the Processing line showing the ticket)

Troubleshooting Notarization

Common Issues and Solutions

“The signature does not include a secure timestamp”

# Always use --timestamp when signing
$ codesign -s "Developer ID Application" --timestamp MyApp.app

“The executable does not have the hardened runtime enabled”

# Enable hardened runtime
$ codesign -s "Developer ID Application" \
  --timestamp \
  --options runtime \
  MyApp.app

“The binary uses an SDK older than the 10.9 SDK”

# Rebuild with a newer SDK
# In Xcode, set Deployment Target to 10.9 or later

“Found an unsigned library”

# Find unsigned components
$ codesign -vvv --deep --strict MyApp.app 2>&1 | grep "not signed"

# Sign each component
$ codesign -s "Developer ID Application" --timestamp path/to/unsigned.dylib

Debugging Script

#!/bin/bash
# check-notarization-ready.sh - Verify app is ready for notarization

APP="$1"

if [[ -z "$APP" ]]; then
    echo "Usage: $0 /path/to/app"
    exit 1
fi

echo "=== Checking $APP for notarization readiness ==="

# Check signature exists
echo -n "Signature: "
if codesign -v "$APP" 2>/dev/null; then
    echo "Valid"
else
    echo "INVALID or MISSING"
    codesign -vvv "$APP"
    exit 1
fi

# Check hardened runtime
echo -n "Hardened Runtime: "
FLAGS=$(codesign -dv "$APP" 2>&1 | grep flags | grep -o "0x[0-9a-f]*")
if [[ "$FLAGS" == *"10000"* ]] || [[ "$FLAGS" == *"runtime"* ]]; then
    echo "Enabled"
else
    echo "DISABLED"
    echo "  Add --options runtime to codesign"
fi

# Check timestamp
echo -n "Secure Timestamp: "
if codesign -dv "$APP" 2>&1 | grep -q "Timestamp="; then
    echo "Present"
else
    echo "MISSING"
    echo "  Add --timestamp to codesign"
fi

# Check Developer ID
echo -n "Developer ID: "
AUTHORITY=$(codesign -dv "$APP" 2>&1 | grep "Authority=Developer ID")
if [[ -n "$AUTHORITY" ]]; then
    echo "Yes"
else
    echo "NO"
    echo "  Must be signed with Developer ID certificate"
fi

# Check for nested unsigned code
echo "Checking nested code..."
UNSIGNED=$(codesign -vvv --deep --strict "$APP" 2>&1 | grep -i "not signed")
if [[ -n "$UNSIGNED" ]]; then
    echo "WARNING: Unsigned components found:"
    echo "$UNSIGNED"
fi

# Test Gatekeeper assessment
echo -n "Gatekeeper Assessment: "
spctl -a -vvv "$APP" 2>&1 | head -2

echo "=== Check complete ==="

Entitlements for Notarization

Some entitlements require special handling for notarization:

<?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>
    <!-- Required for JIT (like JavaScriptCore) -->
    <key>com.apple.security.cs.allow-jit</key>
    <true/>

    <!-- Allow unsigned executable memory (avoid if possible) -->
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>

    <!-- Disable library validation (for plugins) -->
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>

    <!-- Allow DYLD environment variables -->
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
</dict>
</plist>

Certain entitlements (like com.apple.security.cs.allow-unsigned-executable-memory) may cause notarization to fail or require additional review.

Summary

Notarization is mandatory for Developer ID signed software on modern macOS:

ComponentPurpose
notarytool submitUpload software for scanning
notarytool infoCheck submission status
notarytool logGet detailed scan results
stapler stapleAttach ticket to software
spctl -a -vvvVerify notarization

Key requirements:

  • Developer ID certificate (not Apple Development)
  • Hardened runtime enabled (--options runtime)
  • Secure timestamp (--timestamp)
  • All nested code signed
  • No blocked entitlements
# Quick notarization workflow
$ codesign -s "Developer ID Application" --timestamp --options runtime MyApp.app
$ ditto -c -k --keepParent MyApp.app MyApp.zip
$ xcrun notarytool submit MyApp.zip --keychain-profile "profile" --wait
$ xcrun stapler staple MyApp.app

Without notarization, your users will see scary Gatekeeper warnings or be unable to run your software at all. Proper notarization ensures a smooth, trusted experience.