You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sign and notarize macOS release binaries using a paid Apple Developer ID Individual account (99 USD/yr). Alternatives considered and rejected:
Ad-hoc sign (codesign -s -): leaves the Gatekeeper "unidentified developer" prompt; user still needs to right-click → Open or run xattr -d com.apple.quarantine on first launch. Half-measure.
Document the xattr -d workaround in README: shifts friction onto every first-time user — terrible first impression for a pre-1.0 project.
No signing at all (status quo): macOS 14+ blocks browser-downloaded tarballs outright; brew install via the tap hits the same block.
release.yml builds darwin/arm64 and darwin/amd64 binaries on macos-15 runners (lines 40-41, 134-135) but performs no codesigning or notarization. A user downloading the release tarball or installing via the Homebrew tap (#123) is blocked by Gatekeeper on first run. Target milestone is 0.3 — 0.1 and 0.2 already shipped unsigned, but the signing gap should close before broader distribution effort ramps up.
Acceptance criteria
Active Apple Developer ID Individual enrollment; Developer ID Application certificate + private key exported as .p12, base64-encoded, stored as repo secret APPLE_DEVELOPER_ID_CERT_P12_B64.
Additional secrets stored on laradji/deadzone:
APPLE_DEVELOPER_ID_CERT_PASSWORD — the .p12 export password
APPLE_ID — the Apple ID email
APPLE_APP_PASSWORD — app-specific password generated at appleid.apple.com
APPLE_TEAM_ID — 10-char team identifier from developer.apple.com/account
.github/apple/entitlements.plist checked in, minimal set necessary for the hardened runtime to coexist with the runtime dlopen of libonnxruntime in internal/ort.Bootstrap. Start from the sketch below, but trim empirically: each entitlement is an attack-surface admission — keep only those that are actually required for the scrape/consolidate/server smoke tests to pass on a signed+notarized binary.
.github/workflows/release.yml has a new sign-notarize step that runs only on darwin matrix entries, after go build and before archiving, and performs (in order):
Import cert into an ephemeral keychain via security create-keychain / security import / security set-key-partition-list.
Temporary keychain is deleted in an if: always() cleanup step so a mid-job failure doesn't leak keys on the runner.
CI fails loudly (not silently) if any of the five Apple secrets is missing — no skip-when-empty passthrough.
Smoke test on a clean macOS machine (or via curl download on macOS): after tar xzf, running ./deadzone -version does not show a Gatekeeper prompt and does not require xattr -d com.apple.quarantine.
CLAUDE.md → ## Release operator secrets section gains an entry for each new Apple secret, following the same format as the existing HOMEBREW_TAP_TOKEN entry: what it does, which workflow consumes it, how failures surface, and the renewal procedure (Apple cert is 1-year validity, Apple sends a reminder email ~30 days before expiry).
Implementation notes
Entitlements sketch (.github/apple/entitlements.plist) — start here, then minimize:
Rationale: internal/ort.Bootstrap downloads libonnxruntime at first launch from a SHA256-pinned URL and dlopens it. The hardened runtime (mandatory for notarization) will block that dlopen without disable-library-validation (the dylib is not signed by the same Team ID as the deadzone binary). allow-dyld-environment-variables is needed because hugot sometimes reads DYLD_LIBRARY_PATH. allow-unsigned-executable-memory may also be required for ORT's tensor arena — determine empirically by checking whether ./deadzone scrape against a single-URL fixture completes without a crash under the hardened runtime. Do not add entitlements speculatively.
release.yml skeleton (sketch, not prescriptive — finalize in implementation; currently matrix entries macos-15 / goos: darwin at lines 40-41 and 134-135):
Operator runbook — document in CLAUDE.md the one-time setup steps (enroll, create Developer ID Application cert in Keychain Access, export .p12, encode base64 on macOS with base64 -i cert.p12 -o cert.b64, paste into GH secret) and the yearly renewal path.
Out of scope
Linux signing — no universal equivalent; separate concern if ever needed.
Signing or re-signing libonnxruntime — the auto-downloaded dylib is already integrity-checked by SHA256 in internal/ort.Bootstrap; codesign isn't the trust root there.
Changing release cadence, tag format, tarball naming, or checksum manifest layout.
just build && codesign --force --options runtime --sign - ./deadzone && codesign --verify --strict --verbose=2 ./deadzone — validates that the entitlements plist parses and the binary survives codesign with the hardened runtime flag.
Post-tag verification against published asset:
curl -L -o dz.tar.gz <release-asset-url> && tar xzf dz.tar.gz
codesign --verify --deep --strict --verbose=2 deadzone → exits 0, prints "deadzone: valid on disk" and "satisfies its Designated Requirement"
Decision (locked 2026-04-17)
Sign and notarize macOS release binaries using a paid Apple Developer ID Individual account (99 USD/yr). Alternatives considered and rejected:
codesign -s -): leaves the Gatekeeper "unidentified developer" prompt; user still needs to right-click → Open or runxattr -d com.apple.quarantineon first launch. Half-measure.xattr -dworkaround in README: shifts friction onto every first-time user — terrible first impression for a pre-1.0 project.brew installvia the tap hits the same block.Paid Developer ID + notarize = zero-friction install; correct long-term choice.
Why
release.ymlbuilds darwin/arm64 and darwin/amd64 binaries onmacos-15runners (lines 40-41, 134-135) but performs no codesigning or notarization. A user downloading the release tarball or installing via the Homebrew tap (#123) is blocked by Gatekeeper on first run. Target milestone is 0.3 — 0.1 and 0.2 already shipped unsigned, but the signing gap should close before broader distribution effort ramps up.Acceptance criteria
.p12, base64-encoded, stored as repo secretAPPLE_DEVELOPER_ID_CERT_P12_B64.laradji/deadzone:APPLE_DEVELOPER_ID_CERT_PASSWORD— the.p12export passwordAPPLE_ID— the Apple ID emailAPPLE_APP_PASSWORD— app-specific password generated at appleid.apple.comAPPLE_TEAM_ID— 10-char team identifier from developer.apple.com/account.github/apple/entitlements.plistchecked in, minimal set necessary for the hardened runtime to coexist with the runtimedlopenoflibonnxruntimeininternal/ort.Bootstrap. Start from the sketch below, but trim empirically: each entitlement is an attack-surface admission — keep only those that are actually required for the scrape/consolidate/server smoke tests to pass on a signed+notarized binary..github/workflows/release.ymlhas a new sign-notarize step that runs only on darwin matrix entries, aftergo buildand before archiving, and performs (in order):security create-keychain/security import/security set-key-partition-list.codesign --force --options runtime --timestamp --entitlements .github/apple/entitlements.plist --sign "Developer ID Application: <team>" deadzone.ditto -c -k --keepParent deadzone deadzone.zip(required fornotarytool submit).xcrun notarytool submit deadzone.zip --apple-id <secret> --team-id <secret> --password <secret> --wait.xcrun stapler staple deadzone(embeds the notarization ticket so it verifies offline).codesign --verify --deep --strict --verbose=2 deadzone→ exits 0.spctl --assess --type execute --verbose deadzone→ printssource=Notarized Developer ID.if: always()cleanup step so a mid-job failure doesn't leak keys on the runner.curldownload on macOS): aftertar xzf, running./deadzone -versiondoes not show a Gatekeeper prompt and does not requirexattr -d com.apple.quarantine.CLAUDE.md→## Release operator secretssection gains an entry for each new Apple secret, following the same format as the existingHOMEBREW_TAP_TOKENentry: what it does, which workflow consumes it, how failures surface, and the renewal procedure (Apple cert is 1-year validity, Apple sends a reminder email ~30 days before expiry).Implementation notes
Entitlements sketch (
.github/apple/entitlements.plist) — start here, then minimize:Rationale:
internal/ort.Bootstrapdownloadslibonnxruntimeat first launch from a SHA256-pinned URL anddlopens it. The hardened runtime (mandatory for notarization) will block thatdlopenwithoutdisable-library-validation(the dylib is not signed by the same Team ID as the deadzone binary).allow-dyld-environment-variablesis needed because hugot sometimes readsDYLD_LIBRARY_PATH.allow-unsigned-executable-memorymay also be required for ORT's tensor arena — determine empirically by checking whether./deadzone scrapeagainst a single-URL fixture completes without a crash under the hardened runtime. Do not add entitlements speculatively.release.yml skeleton (sketch, not prescriptive — finalize in implementation; currently matrix entries
macos-15/goos: darwinat lines 40-41 and 134-135):Operator runbook — document in
CLAUDE.mdthe one-time setup steps (enroll, create Developer ID Application cert in Keychain Access, export .p12, encode base64 on macOS withbase64 -i cert.p12 -o cert.b64, paste into GH secret) and the yearly renewal path.Out of scope
libonnxruntime— the auto-downloaded dylib is already integrity-checked by SHA256 ininternal/ort.Bootstrap; codesign isn't the trust root there.update-package-channels.ymlor thehomebrew-deadzonetap formula — feat: add Homebrew tap distribution for macOS (#105) #123 is orthogonal; this issue only alters what's inside the tarball the tap pulls.Test commands
Local dry-run (ad-hoc sign, skips notarize):
just build && codesign --force --options runtime --sign - ./deadzone && codesign --verify --strict --verbose=2 ./deadzone— validates that the entitlements plist parses and the binary survives codesign with the hardened runtime flag.Post-tag verification against published asset:
curl -L -o dz.tar.gz <release-asset-url> && tar xzf dz.tar.gzcodesign --verify --deep --strict --verbose=2 deadzone→ exits 0, prints "deadzone: valid on disk" and "satisfies its Designated Requirement"spctl --assess --type execute --verbose deadzone→ exits 0, prints "source=Notarized Developer ID"./deadzone -version→ runs immediately, no Gatekeeper prompt, no quarantine attributeDependencies