Sign and notarize the macOS bundle - #70
Open
landsman wants to merge 4 commits into
Open
Conversation
The macOS bundle is about to be signed with the hardened runtime, which notarization requires and which refuses PCRE's executable-memory allocation. The lost optimisation is not the problem -- PCRE falls back and matches fine. The warning it prints is: display_errors puts it on the page, so headers are sent before adminer reaches session_start(), the session never starts, and the app dies on "Undefined constant Adminer\SID" before drawing anything. The alternative is a com.apple.security.cs.allow-jit entitlement, which buys back a micro-optimisation nothing here notices -- every regex in a request costs less than the database round trip it precedes -- by letting the whole process map executable memory. Not a trade worth making. Set everywhere rather than on macOS alone: one ini is loaded on all three platforms and a per-OS override is more machinery than the setting is worth.
… unsigned A newly installed Adminer Desktop.app did not open on macOS and left no log -- frankenphp never started, so there was nothing to write one. The bundle carried the go linker's ad-hoc signature on its executable but had no _CodeSignature of its own, so that signature claimed sealed resources that did not exist. Apple's own checker calls it a fatal codesign error, and Gatekeeper acts on it: a damaged app is killed outright, where an unsigned one would at least have been offered as right-click > Open. So `bundle` now signs. Ad-hoc when no identity is passed, which is still a sealed and therefore valid app -- it just names no developer -- so a local build and a secretless CI run keep working and differ from a release only in which identity got attached. ditto rather than zip(1) for the archive: it is what notarytool documents and it preserves the symlinks and extended attributes the signature was computed over. Named once because notarize has to build the same archive again after stapling.
Signing alone leaves the app unidentified: still a warning, still right-click > Open. Apple's notary service is what removes it, and stapling the ticket into the bundle is what makes a first launch work offline -- without it Gatekeeper asks Apple, and a machine behind a filter refuses the app. codesign, notarytool, stapler and ditto all ship with the Xcode command line tools already on the runner, so this adds no action and no install. Only a certificate, which has to come from a paid membership and so lives in secrets. Both steps are guarded on the secret rather than switched on in the matrix, so a fork -- and this repo before the certificate exists -- still builds and uploads an ad-hoc signed zip instead of dying on a missing credential. The cost is that the phony bundle > zip chain runs twice; fold notarize into matrix.package once the secrets are permanent. The secrets belong on the per-platform environment and nowhere else. This repo is public and claude-mentions.yml triggers on issue_comment, which fires for a comment from anyone; an environment secret is readable only by a job declaring that environment, so it cannot reach them. A repo-level secret could.
…ff CI macOS runners bill at 10x and the three-platform build is manual dispatch precisely so it is not iterated against -- but the signing path was shell in a YAML `run:` block, which is the one place it could neither be run nor linted before a dispatch found out. `make keychain` is the same code on a laptop, and `qa` globs cli/*.sh into shellcheck so it is checked like everything else. That is only worth having if it is safe to run here, and the recipe everyone copies is not: `security default-keychain -s` points the default away from login and leaves it there if the script exits early. On an ephemeral runner that costs nothing. codesign searches the whole keychain list, so appending to the list was always enough, and now login stays both present and default. Two things the inline version could not do. It takes the .p12 as a path or as base64, so neither side has to shape its input for the other. And it fails when `find-identity` reports no Developer ID Application -- an expired certificate, or a Mac Development one, imports perfectly happily and is only discovered minutes later inside `make bundle`, where codesign has far less to say about why. A trap unwinds the keychain on any failure so a wrong password leaves nothing behind.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A newly installed
Adminer Desktop.appdid not open, and left no log — the lastline predates the install, because frankenphp never started. Apple's own checker
says why:
make bundleassembledContents/and stopped. The executable carried the Golinker's ad-hoc signature, which claims sealed resources — but nothing ever
signed the bundle, so there was no
_CodeSignatureto hold them. Gatekeeperreads that as damaged rather than unsigned, and a damaged app is killed outright
where an unsigned one is at least offered as right-click → Open.
Codesign Error"no resources…"Adhoc Signed AppNotary Ticket MissingWhat this does
Makefile—bundlesigns, nested code first and no--deep. Ad-hocwhen no identity is passed, so a local build and a secretless CI run keep
working and differ from a release only in which identity got attached.
zipmoves to
ditto(what notarytool documents; it preserves the symlinks andxattrs the seal covers). New
notarizetarget submits, staples and re-zips.cli/keychain.sh— the certificate import, out of the workflow so it canbe run and linted off CI.
qaglobscli/*.shinto shellcheck..github/workflows/build.yml— calls that script and notarizes, bothguarded on their secret being present.
app/php/desktop.ini—pcre.jit = 0. Hardened runtime is mandatory fornotarization and it refuses PCRE's executable-memory allocation. The lost
optimisation is not the problem; the warning it prints is —
display_errorsputs it on the page, headers go out before
session_start(), and the app dieson
Undefined constant Adminer\SIDbefore drawing anything. Caught by bootingthe signed bundle, not by reading the diff.
Before this can notarize: six secrets
Put them on the
macos-arm64environment (Settings → Environments), notat repository level. This repo is public and
claude-mentions.ymltriggers onissue_comment, which fires for a comment from anyone — an environment secretis readable only by a job that declares that environment, so it cannot reach
them. A repository secret could.
Needs an Apple Developer Program membership ($99/yr); there is no free tier.
MACOS_CERT_P12base64 -i DeveloperID.p12 | pbcopyMACOS_CERT_PASSWORD.p12MACOS_SIGN_IDDeveloper ID Application: NAME (TEAMID), verbatim fromsecurity find-identity -v -p codesigningNOTARY_KEYbase64 -i AuthKey_XXXXXXXXXX.p8 | pbcopyNOTARY_KEY_IDXXXXXXXXXXfrom that filenameNOTARY_ISSUERWhere the two files come from:
Certificate Authority → save to disk.
Development", which notarization rejects → upload the CSR → download the
.cer→ double-click to install.its private key →
.p12, set a password. That pair isMACOS_CERT_P12andMACOS_CERT_PASSWORD.Developer ID role. The
.p8downloads exactly once.Also worth doing first: all three environments currently have no protection
rules (
gh api repos/{owner}/{repo}/environments→protection: []). Add adeployment branch rule limiting
macos-arm64tomainand tags, so a dispatchfrom an arbitrary branch carrying a modified workflow cannot read the
certificate.
Doing it locally
For your own machine you need none of
cli/keychain.sh— double-click the.ceronce and codesign finds it in your login keychain forever:The script is for rehearsing the CI path before spending a runner minute on it,
or on a machine the certificate is not installed on. It takes the
.p12as apath as well as base64, so locally that is just:
MACOS_CERT_P12=DeveloperID.p12 MACOS_CERT_PASSWORD=... make keychain make keychain-clean # afterwardsIt appends to the keychain search list rather than using
security default-keychain -s, which every CI recipe for this reaches for andwhich would point your default away from
loginand leave it there if thescript exited early. codesign searches the whole list, so appending was always
enough — that is what makes one script correct in both places.
Checked
make qagreen (shellcheck now coverscli/*.sh),make securityclean.make ziplocally, then booting the signed bundle headless — which is whatcaught the PCRE fatal that reading the diff would not have. A copy extracted from
the zip with a quarantine xattr applied now passes
codesign --verify --strict,and the fatal codesign error is gone from
syspolicy_check.cli/keychain.shexercised with a throwaway self-signed.p12, both inputshapes: the
loginkeychain stays present and default, an unusable certificatefails with a message rather than at
codesigntime minutes later, and the trapleaves nothing in the search list when it does.
Not verifiable here: notarization, and the script's success path, both of which
need the real certificate. Once the secrets exist, dispatch with
build: true,then on a machine that has never seen the app —
spctl -a -vvvshould saysource=Notarized Developer ID, andstapler validateshould find the ticket.Double-click with no dialog is the pass.
Out of scope
Windows SmartScreen — same problem, dearer certificate, nobody has asked.