P3-10: macOS .app, Developer ID signing, notarisation #39

Open
opened 2026-09-10 17:45:38 +00:00 by Cordy · 0 comments
Owner

Depends on P3-9, P0-2 (Apple account and certificates) and P0-4 (Mac runner).

Files

  • Create: .forgejo/workflows/release-macos.yml, build/macos/Info.plist, build/macos/entitlements.plist

Universal binary

Build for both arm64 and amd64 and combine with lipo into a universal binary. Intel Macs are still in use, and shipping two downloads confuses people.

The exact signing and notarisation chain

# 1. sign with hardened runtime — REQUIRED for notarisation
codesign --force --options runtime --timestamp \
  --sign "Developer ID Application: Swiss Cairn GmbH (<TEAMID>)" Cairn.app

# 2. store notary credentials once, in the keychain (done in P0-4)
xcrun notarytool store-credentials "cairn-notary" \
  --apple-id "<apple-id>" --team-id "<TEAMID>" --password "<app-specific-password>"

# 3. package — notary accepts .zip, .dmg or .pkg only
ditto -c -k --keepParent Cairn.app Cairn.zip

# 4. submit and block until the verdict
xcrun notarytool submit Cairn.zip --keychain-profile "cairn-notary" --wait

# 5. staple the ticket
xcrun stapler staple Cairn.app

# 6. verify exactly as Gatekeeper will
spctl -a -vvv -t install Cairn.app

Five things that trip people up

  1. --options runtime is mandatory. Without the hardened runtime, notarisation is rejected outright.
  2. You cannot staple a bare Mach-O binary. Stapling works on .app, .dmg and .pkg only. Staple the bundle, then wrap it in the .dmg you ship.
  3. Sign inside-out. Nested frameworks, helpers and the Sparkle framework must each be signed before the outer bundle.
  4. Never use the plain Apple ID password for notarytool — use an app-specific password or an App Store Connect API key.
  5. Notarisation is not signing. Signing proves who built it; notarisation is Apple scanning it. You need both, in that order.

Ship a DMG

A drag-to-Applications DMG is the convention macOS users expect. Sign and staple the DMG too, so Gatekeeper is satisfied before the app is even copied.

Sparkle

Sparkle (P3-7) is a framework inside the bundle — it must be signed as part of the inside-out pass, and its EdDSA public key goes in Info.plist.

Steps

  • Build a universal binary and assemble the .app.
  • Implement inside-out signing, including the Sparkle framework.
  • Wire notarisation and stapling into the release workflow on the Mac runner.
  • Build, sign and staple the DMG.
  • Download the DMG on a clean Mac — one that has never seen the app or the certificate — and confirm it opens with no Gatekeeper warning. Testing on the build machine proves nothing; that machine trusts your own certificate.
  • Commit: git commit -s -m "build: macOS universal app, signing and notarisation"

Acceptance criteria

  • spctl -a -vvv -t install reports accepted.
  • A clean Mac opens the app with no warning dialog.
  • The whole chain runs unattended in CI.
  • Sparkle updates work on a signed, notarised build.
Depends on P3-9, P0-2 (Apple account and certificates) and P0-4 (Mac runner). ## Files - Create: `.forgejo/workflows/release-macos.yml`, `build/macos/Info.plist`, `build/macos/entitlements.plist` ## Universal binary Build for **both** `arm64` and `amd64` and combine with `lipo` into a universal binary. Intel Macs are still in use, and shipping two downloads confuses people. ## The exact signing and notarisation chain ```bash # 1. sign with hardened runtime — REQUIRED for notarisation codesign --force --options runtime --timestamp \ --sign "Developer ID Application: Swiss Cairn GmbH (<TEAMID>)" Cairn.app # 2. store notary credentials once, in the keychain (done in P0-4) xcrun notarytool store-credentials "cairn-notary" \ --apple-id "<apple-id>" --team-id "<TEAMID>" --password "<app-specific-password>" # 3. package — notary accepts .zip, .dmg or .pkg only ditto -c -k --keepParent Cairn.app Cairn.zip # 4. submit and block until the verdict xcrun notarytool submit Cairn.zip --keychain-profile "cairn-notary" --wait # 5. staple the ticket xcrun stapler staple Cairn.app # 6. verify exactly as Gatekeeper will spctl -a -vvv -t install Cairn.app ``` ## Five things that trip people up 1. **`--options runtime` is mandatory.** Without the hardened runtime, notarisation is rejected outright. 2. **You cannot staple a bare Mach-O binary.** Stapling works on `.app`, `.dmg` and `.pkg` only. Staple the bundle, then wrap it in the `.dmg` you ship. 3. **Sign inside-out.** Nested frameworks, helpers and the Sparkle framework must each be signed **before** the outer bundle. 4. **Never use the plain Apple ID password** for `notarytool` — use an app-specific password or an App Store Connect API key. 5. **Notarisation is not signing.** Signing proves who built it; notarisation is Apple scanning it. You need both, in that order. ## Ship a DMG A drag-to-Applications DMG is the convention macOS users expect. Sign and staple the DMG too, so Gatekeeper is satisfied before the app is even copied. ## Sparkle Sparkle (P3-7) is a framework inside the bundle — it must be signed as part of the inside-out pass, and its EdDSA public key goes in `Info.plist`. ## Steps - [ ] Build a universal binary and assemble the `.app`. - [ ] Implement inside-out signing, including the Sparkle framework. - [ ] Wire notarisation and stapling into the release workflow on the Mac runner. - [ ] Build, sign and staple the DMG. - [ ] **Download the DMG on a clean Mac** — one that has never seen the app or the certificate — and confirm it opens with **no Gatekeeper warning**. Testing on the build machine proves nothing; that machine trusts your own certificate. - [ ] Commit: `git commit -s -m "build: macOS universal app, signing and notarisation"` ## Acceptance criteria - `spctl -a -vvv -t install` reports accepted. - A clean Mac opens the app with no warning dialog. - The whole chain runs unattended in CI. - Sparkle updates work on a signed, notarised build.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-desktop#39
No description provided.