macOS Distribution: Gatekeeper, Signing, and Notarization¶
Status: decision record — records what was measured, what was changed, and what a maintainer must provide to remove the macOS security prompt entirely. Last updated: 2026-08-06.
1. The symptom¶
A user who downloads sysml-darwin-arm64.tar.gz (or the Intel/sysml-lsp equivalents)
from GitHub Releases in a browser, extracts it, and runs it gets:
"sysml-darwin-arm64" cannot be opened because the developer cannot be verified.
and has to go to System Settings → Privacy & Security → Open Anyway.
2. Root cause: quarantine, not a missing signature¶
There are two distinct macOS mechanisms that can block a downloaded binary. Only one of them is biting us.
2.1 Executability (code signature) — already satisfied¶
macOS on Apple silicon refuses to execute arm64 code with no valid signature attached (Apple Platform Security, "Rosetta 2 on a Mac with Apple silicon"). An ad-hoc signature is enough to satisfy this.
Go's linker emits that ad-hoc signature itself, including when cross-compiling from Linux
(see golang/go#42684 and
cmd/internal/codesign).
Measured on a Linux host with Go 1.25.0, cross-compiling this repository's cmd/sysml
exactly as the build-release CircleCI job does, then parsing the Mach-O load commands:
| Target | LC_CODE_SIGNATURE |
CodeDirectory flags |
|---|---|---|
GOOS=darwin GOARCH=arm64 |
present (57 KB blob, 0xfade0cc0 superblob) |
0x20002 = adhoc, linker-signed, identifier a.out |
GOOS=darwin GOARCH=amd64 |
absent | n/a (x86-64 does not require one) |
Those are the same flags codesign -dv reports as flags=0x20002(adhoc,linker-signed) in
the Go issue above. Untested claim (no macOS host available here): that the binary
therefore launches on Apple silicon once quarantine is cleared — this follows from the
Apple/Go documentation cited, but it was not executed on a Mac as part of this work.
Consequence: adding our own ad-hoc signing step (codesign -s -) would change nothing. It
does not, and cannot, satisfy Gatekeeper for a quarantined download — an ad-hoc signature
carries no Developer ID and no notarization ticket.
2.2 Gatekeeper / quarantine — this is the prompt users see¶
Programs that download files on behalf of a user (Safari, Chrome, Mail, and anything else
that opts into LSFileQuarantineEnabled) tag the downloaded file with the
com.apple.quarantine extended attribute. Archive utilities propagate that attribute to
the files they extract, so the extracted sysml-darwin-arm64 is quarantined too. On first
launch, Gatekeeper evaluates a quarantined binary and blocks anything that is not signed
with a Developer ID and notarized — which is exactly the prompt in §1
(Apple: "Safely open apps on your Mac",
"Notarizing macOS software before distribution").
Two consequences worth knowing:
- The attribute is set by the downloader, not by the file.
curl,wget,git, andgo installdo not set it, so binaries obtained those ways never trigger the prompt. This is whybrew installis prompt-free (Homebrew applies quarantine only to casks — hence the cask-only--no-quarantineflag — not to formulae). xattr -d com.apple.quarantine <file>removes it, which is why that workaround "works".
3. Options considered¶
| Option | Removes the prompt for a browser download? | Cost | Requires credentials we don't have |
|---|---|---|---|
Developer ID signing + notarization (stapling only if we ship .pkg/.dmg, see §5.3) |
Yes — the only option that does | $99/yr Apple Developer Program + macOS CI minutes | Yes |
| Homebrew tap/formula (accepted stopgap) | Yes, for users who install via brew (curl download, no quarantine) |
Free (a tap repo) | No, but needs a new repo |
go install .../cmd/sysml@latest |
Yes (built locally, never quarantined) | Free; user needs a Go toolchain | No |
Ad-hoc signing in CI (codesign -s -) |
No — see §2.1; also already effectively done by the Go linker | Needs a macOS runner | No |
Documented xattr -d com.apple.quarantine |
Yes, but by having the user disable a security check | Free | No |
Notes on the last row: telling users to strip quarantine is a poor default because it
trains them to bypass Gatekeeper on any download, and it gives them no way to tell our
archive from a tampered one. It is documented in docs/guide/01-install.md as an escape hatch
next to the checksum verification step, not as the recommended path.
4. What this change actually lands¶
- Docs (
README.md,docs/guide/01-install.md): a macOS section that recommendsbrew install Open-MBEE/tap/opensysml, withcurl/go installand the quarantine-clearing + checksum-verification commands as fallbacks. - Release artifacts (
.circleci/config.yml): in addition to the existing per-binary archives (unchanged, so existing links keep working), thebuild-releasejob now publishesopensysml-<os>-<arch>.tar.gz/.zipbundles containingsysmlandsysml-lspunder their plain names, plus aSHA256SUMS.txtcovering every archive. Both are prerequisites for a Homebrew formula and for users who want to verify a download. - Homebrew formula (
packaging/homebrew/Formula/opensysml.rb) plusscripts/render-homebrew-formula.sh, which renders it for a tag fromSHA256SUMS.txt. Homebrew is the accepted macOS install path until notarization exists. The tap repositoryOpen-MBEE/homebrew-tapnow exists and carries the rendered 0.0.4 formula; per-release maintenance steps are inpackaging/homebrew/README.md.
5. Decision record: notarization¶
Decision: not implemented in this change, because it cannot be done without credentials the maintainer must obtain. Homebrew is the accepted stopgap in the meantime (maintainer decision, 2026-08-06). Nothing about the change above conflicts with adding it later; the notarization step would run after the existing build step and re-tar the signed binaries.
5.1 What the maintainer must provide¶
| # | Item | How to obtain | Where it goes |
|---|---|---|---|
| 1 | Apple Developer Program membership | https://developer.apple.com/programs/ — $99/year, organization enrollment needs a D-U-N-S number and takes days-to-weeks | prerequisite for 2–4 |
| 2 | Developer ID Application certificate (not "Apple Development", not "Mac App Distribution") | Certificates, Identifiers & Profiles → create a CSR on a Mac → download .cer → export as .p12 with a password |
CI secret MACOS_CERT_P12 (base64) + MACOS_CERT_PASSWORD |
| 3 | App Store Connect API key for notarytool (preferred over an Apple ID + app-specific password: no 2FA interaction, revocable) |
App Store Connect → Users and Access → Integrations → Keys → download the .p8 once |
CI secrets APPLE_API_KEY_P8 (base64), APPLE_API_KEY_ID, APPLE_API_ISSUER_ID |
| 4 | Team ID | Apple Developer account membership page | CI secret APPLE_TEAM_ID |
| 5 | A decision on where it runs (see §5.2) | — | — |
Without item 2 specifically, nothing works: only a Developer ID certificate produces a signature Gatekeeper accepts for software distributed outside the App Store.
5.2 Which CI provider it would run on¶
codesign, notarytool, and stapler are Xcode command-line tools and only run on macOS.
The current release pipeline is CircleCI (.circleci/config.yml, release workflow on
v* tags), which cross-compiles on the Linux cimg/go:1.25 executor; GitHub Actions is
used only for PR checks (.github/workflows/pr.yml), which explicitly does not mirror the
release workflow.
- CircleCI macOS VM:
macos: {xcode: ...}withresource_class: m4pro.mediumcosts 200 credits/minute, and macOS VMs are not available on the Free plan; CircleCI's 400,000 free open-source credits/month apply to Linux, Arm, and Docker only, so macOS jobs draw on the 30,000-credit free allowance (CircleCI price list, pricing). - GitHub Actions
macos-latest: a standard GitHub-hosted runner, and standard runners are free for public repositories (GitHub Actions billing). (macos-*-large/-xlargeare larger runners and are always billed — do not use them.)
Recommendation: add a tag-triggered GitHub Actions release workflow on macos-latest
for the darwin artifacts only, and keep CircleCI for everything else — or move the whole
release there. Doing it on CircleCI is functionally fine but costs credits and, on the Free
plan, is not available at all. This only stays free if the repository is public.
5.3 What the pipeline steps would look like¶
Sketch, not tested (no macOS host available here); step order and flags follow Apple's "Customizing the notarization workflow":
# .github/workflows/release-macos.yml (sketch), on: push: tags: ['v*']
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version-file: go.mod }
# 1. Import the Developer ID certificate into a throwaway keychain.
- run: |
echo "$MACOS_CERT_P12" | base64 -d > cert.p12
security create-keychain -p "$KEYCHAIN_PW" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PW" build.keychain
security import cert.p12 -k build.keychain -P "$MACOS_CERT_PASSWORD" \
-T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PW" build.keychain
rm cert.p12
# 2. Build both darwin architectures.
- run: |
GOOS=darwin GOARCH=arm64 make build-sysml VERSION="$GITHUB_REF_NAME" ...
GOOS=darwin GOARCH=amd64 make build-sysml VERSION="$GITHUB_REF_NAME" ...
# ...and build-lsp for both
# 3. Sign with the hardened runtime (notarization requires --options runtime).
- run: |
codesign --force --timestamp --options runtime \
--sign "Developer ID Application: <NAME> ($APPLE_TEAM_ID)" \
dist/sysml-darwin-arm64 dist/sysml-darwin-amd64 \
dist/sysml-lsp-darwin-arm64 dist/sysml-lsp-darwin-amd64
codesign --verify --strict --verbose=2 dist/sysml-darwin-arm64
# 4. Notarize. notarytool accepts .zip/.pkg/.dmg, not .tar.gz, so zip for submission.
- run: |
echo "$APPLE_API_KEY_P8" | base64 -d > key.p8
ditto -c -k --keepParent dist/sysml-darwin-arm64 notarize.zip
xcrun notarytool submit notarize.zip --wait \
--key key.p8 --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER_ID"
rm key.p8
# 5. Tar the *signed* binaries and publish. Do not modify a binary after signing —
# any edit (including strip) invalidates the signature.
Two constraints that shape the above:
- Stapling does not apply to bare executables. Apple: "Although tickets are created for
standalone binaries, it's not currently possible to staple tickets to them"
("Customizing the notarization workflow").
Gatekeeper then fetches the ticket online at first launch. Getting a stapled artifact
would mean shipping a
.pkgor.dmginstead of a tarball — a larger packaging change, worth doing only if offline first launch matters. - Signing must happen after the last modification of the binary, and the tarball must be built from the signed files.
5.4 Recurring cost¶
$99/year (Apple Developer Program) plus CI time: on GitHub Actions macos-latest for a
public repository, $0; on CircleCI, ~200 credits/minute of macOS VM time per release.
6. Windows¶
Out of scope for this change, and deliberately not fixed here: the Windows artifacts are
unsigned too, so SmartScreen shows "Windows protected your PC" for downloaded
sysml-windows-amd64.exe. The remedy (an Authenticode / EV code-signing certificate from a
commercial CA, typically a few hundred USD per year) is a separate decision with a separate
cost, and SmartScreen reputation also builds over download volume.