Skip to content

Sign and notarize the macOS binary - #5

Open
las7 wants to merge 1 commit into
mainfrom
sign-macos-release
Open

Sign and notarize the macOS binary#5
las7 wants to merge 1 commit into
mainfrom
sign-macos-release

Conversation

@las7

@las7 las7 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes the Gatekeeper refusal on a browser download of the macOS tarball.

Why it happens

The macOS build already runs on a macos-14 runner, so the linker applies an ad-hoc signature. That is why curl | sh works — curl sets no com.apple.quarantine attribute, so Gatekeeper never runs on that path.

A browser download does set it, and an ad-hoc signature is not a Developer ID. The same artifact fetched from the releases page is refused with "the developer cannot be verified".

The change

The macOS binary is signed with a Developer ID Application certificate under the hardened runtime, then notarized with --wait, so a rejection fails the release rather than being discovered by whoever downloads it. --options runtime is required — notarization rejects a submission without it, with an error that does not say so.

Signing runs before packaging, and has to. Signing rewrites the binary, so a tarball built first would ship the unsigned one and SHA256SUMS would still verify: install.sh would confirm the checksum of the wrong artifact, and the failure would name the download rather than the ordering.

Only the aarch64-apple-darwin matrix leg is touched. The Linux musl legs need none of this and are unchanged.

Two things recorded in the README rather than left to be rediscovered

  • notarytool will not take a .tar.gz (only .zip, .pkg, .dmg), so the binary is zipped purely to be submitted. What ships is still the tarball containing that same signed binary — the ticket binds to the code signature, not to the container it was submitted in.
  • A bare executable cannot be stapled (stapling needs a bundle, .dmg or .pkg), so Gatekeeper resolves the ticket online. A first run on a machine with no route to Apple fails the way an unnotarized binary does. A stapled .pkg alongside the tarball is the fix if that ever becomes a real complaint; it did not seem worth the second artifact today.

Before this can run — six secrets, none of which I can create

Documented in README → Signed macOS builds. A missing one fails the release loudly rather than shipping something Gatekeeper will refuse, matching how RELEASE_REPO_TOKEN already behaves.

APPLE_CERT_P12_BASE64, APPLE_CERT_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_API_KEY_P8_BASE64, APPLE_API_KEY_ID, APPLE_API_ISSUER_ID.

Two notes on obtaining them: the certificate must be Developer ID Application, not "Mac App Distribution" (App Store only, will not satisfy Gatekeeper here); and prefer an App Store Connect API key over an Apple ID with an app-specific password, which breaks whenever someone's password or 2FA changes.

Verification

The workflow YAML parses. Nothing here has been executed — it cannot be until the secrets exist, and it only runs on a cli-v* tag. The first tagged release after the secrets land is the real test; the check to make afterwards is a browser download of reachpad-aarch64-apple-darwin.tar.gz, extracted and run on a Mac that has never seen the binary, plus spctl -a -vv -t install against the extracted binary.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V1dcKuVdxyArHpXvHLkiGx

A browser download of the macOS tarball is refused by Gatekeeper: "the
developer cannot be verified". Building on a macos-14 runner already gets
an ad-hoc signature from the linker, which is why `curl | sh` works --
curl sets no quarantine attribute, so Gatekeeper never runs -- but an
ad-hoc signature is not a Developer ID, and the same artifact fetched from
the releases page is quarantined and refused.

The binary is now signed with a Developer ID certificate under the
hardened runtime and notarized before the release is cut. Notarization
rejects a submission without `--options runtime`, with an error that does
not say so.

Signing runs BEFORE packaging, and has to: it rewrites the binary, so a
tarball built first would ship the unsigned one and SHA256SUMS would still
verify -- install.sh would confirm the checksum of the wrong artifact and
the failure would name the download.

A bare executable cannot be stapled, so the ticket is resolved online. A
first run with no route to Apple fails the way an unnotarized binary does;
a stapled .pkg is the fix if that is ever a real complaint. Recorded in
the README rather than left for someone to rediscover.

Six secrets, documented in the README, and a missing one fails the release
loudly rather than shipping something Gatekeeper will refuse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1dcKuVdxyArHpXvHLkiGx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant