Sign builds and releases with a stable code identity
swiftc linker-signs only the inner binary, leaving the bundle unsigned and its codesign identifier as "launcher" rather than the bundle id. More importantly it leaves the app ad-hoc signed, and macOS pins an ad-hoc app's Accessibility grant to its exact cdhash instead of to a designated requirement. Every build mints a new cdhash, so each rebuild-and-replace silently revoked the permission while the app stayed listed and ticked under Privacy & Security — and every release did the same to everyone who updated. That was the root cause of the window tags never appearing. - build.sh signs the bundle with its real identifier, honours SHANNONCOAT_SIGN_IDENTITY, and fails outright rather than falling back to ad-hoc when an identity was asked for explicitly. - The identity is read from a gitignored .env, so it doesn't have to be retyped every build. Parsed rather than sourced, so a stray command in the file can't execute as a side effect of building, and so an existing environment variable still wins. .env.example carries the full one-time setup. - release.yml imports the certificate into a throwaway keychain, builds, verifies, and deletes the keychain on if: always(). It stays inert until the three secrets exist, and fails the release rather than shipping ad-hoc. - Guards the empty-password case explicitly: macOS cannot import an OpenSSL-produced PKCS#12 with an empty password, and reports it as "MAC verification failed (wrong password?)", which sends you hunting for a wrong password rather than a missing one. Nothing local catches this, since the login keychain imports the PEM pair and needs no password. - Ignores *.p12 and *.pem as a backstop; the certificate belongs outside the working tree entirely. Verified end-to-end: two from-scratch builds produce byte-identical designated requirements where ad-hoc differs every time, and six rebuild-reinstall cycles under a real certificate kept the Accessibility grant with no System Settings interaction. This buys permission persistence, not Gatekeeper approval — a self-signed certificate isn't notarized, so downloads still need System Settings -> Privacy & Security -> Open Anyway.
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
# Local build configuration. Copy to `.env` (gitignored) and fill in.
|
||||
#
|
||||
# cp .env.example .env
|
||||
#
|
||||
# build.sh reads `.env` automatically if it exists. Anything already set in
|
||||
# the environment wins over this file, so a one-off
|
||||
#
|
||||
# SHANNONCOAT_SIGN_IDENTITY= ./build.sh
|
||||
#
|
||||
# still forces an ad-hoc build without editing anything.
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Code-signing identity
|
||||
# ---------------------------------------------------------------------------
|
||||
# The common name of the code-signing certificate in your keychain. Not a
|
||||
# secret — it's a label, not a credential — but it lives here so every build
|
||||
# picks it up without you having to remember the variable.
|
||||
#
|
||||
# Why bother signing at all: macOS pins an *ad-hoc* signed app's
|
||||
# Accessibility grant to its exact cdhash, which changes on every build. So
|
||||
# an unsigned local build silently loses the permission each time you
|
||||
# rebuild and reinstall, while still appearing listed and ticked under
|
||||
# Privacy & Security → Accessibility. Signing with a certificate gives the
|
||||
# app a stable designated requirement, and the grant survives.
|
||||
#
|
||||
# Leave empty (or delete the line) to build ad-hoc.
|
||||
SHANNONCOAT_SIGN_IDENTITY="shannoncoat Signing"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Certificate material — only needed to publish the GitHub Actions secrets
|
||||
# ---------------------------------------------------------------------------
|
||||
# The build itself never reads these: once the certificate is imported, the
|
||||
# private key lives in your keychain and codesign finds it by the identity
|
||||
# name above. They're recorded here purely so the `gh secret set` commands
|
||||
# at the bottom have somewhere to read from.
|
||||
#
|
||||
# Keep the .p12 OUTSIDE the working tree. A gitignored file is still one
|
||||
# `git add -f`, one editor-indexed backup, or one shared folder away from
|
||||
# leaking, and this one holds a private key.
|
||||
#
|
||||
# The $HOME below is expanded by the shell when you `source .env` (which is
|
||||
# how the gh commands at the bottom read it). build.sh parses rather than
|
||||
# sources this file, so it takes values literally — which costs nothing
|
||||
# here, as the only variable it actually reads is the identity above.
|
||||
SHANNONCOAT_P12_PATH="$HOME/.shannoncoat-signing/shannoncoat-signing.p12"
|
||||
|
||||
# The .p12 export password. Storing it in plaintext here is weaker than
|
||||
# leaving it in your password manager and typing it when prompted — prefer
|
||||
# leaving this field empty and letting `gh secret set` ask for it
|
||||
# interactively.
|
||||
#
|
||||
# Leaving the *field* empty is fine. Exporting the .p12 itself with an
|
||||
# empty password is not: macOS cannot import one, so CI would fail even
|
||||
# though local builds carry on working (they import the PEM pair, which
|
||||
# needs no password). Give the export a real password.
|
||||
SHANNONCOAT_P12_PASSWORD=""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# One-time setup
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generate a ten-year self-signed code-signing certificate (codesign refuses
|
||||
# an expired one, and replacing it later resets every user's Accessibility
|
||||
# permission again):
|
||||
#
|
||||
# mkdir -p ~/.shannoncoat-signing && chmod 700 ~/.shannoncoat-signing
|
||||
# openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||
# -keyout ~/.shannoncoat-signing/key.pem \
|
||||
# -out ~/.shannoncoat-signing/cert.pem \
|
||||
# -subj "/CN=shannoncoat Signing" \
|
||||
# -addext "basicConstraints=critical,CA:false" \
|
||||
# -addext "keyUsage=critical,digitalSignature" \
|
||||
# -addext "extendedKeyUsage=critical,codeSigning"
|
||||
#
|
||||
# Bundle it as a .p12. `-legacy` is required: OpenSSL 3's default encoding
|
||||
# is one macOS cannot read, and `security import` then fails with a
|
||||
# misleading "MAC verification failed (wrong password?)".
|
||||
#
|
||||
# openssl pkcs12 -export -legacy \
|
||||
# -inkey ~/.shannoncoat-signing/key.pem \
|
||||
# -in ~/.shannoncoat-signing/cert.pem \
|
||||
# -name "shannoncoat Signing" \
|
||||
# -out ~/.shannoncoat-signing/shannoncoat-signing.p12
|
||||
#
|
||||
# Import for local builds, then delete the now-redundant loose private key
|
||||
# (the .p12 remains your only backup, so keep that):
|
||||
#
|
||||
# security import ~/.shannoncoat-signing/shannoncoat-signing.p12 \
|
||||
# -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign
|
||||
# rm ~/.shannoncoat-signing/key.pem
|
||||
#
|
||||
# The first signed build raises a "codesign wants to use key…" dialog —
|
||||
# choose Always Allow. Then re-grant Accessibility one final time; from
|
||||
# then on it persists across rebuilds.
|
||||
#
|
||||
# Note that `security find-identity -v -p codesigning` will report "0 valid
|
||||
# identities": -v filters to *trusted* certificates and a self-signed one
|
||||
# reads as CSSMERR_TP_NOT_TRUSTED. codesign uses it regardless. Drop the -v
|
||||
# to see it.
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Publishing the same certificate to GitHub Actions
|
||||
# ---------------------------------------------------------------------------
|
||||
# Releases need this too — an ad-hoc release revokes the Accessibility
|
||||
# permission of everyone who updates. .github/workflows/release.yml stays
|
||||
# inert until all three secrets exist, so nothing breaks in the meantime.
|
||||
#
|
||||
# source .env
|
||||
# base64 -i "$SHANNONCOAT_P12_PATH" | gh secret set SIGNING_CERTIFICATE_P12
|
||||
# gh secret set SIGNING_IDENTITY --body "$SHANNONCOAT_SIGN_IDENTITY"
|
||||
# gh secret set SIGNING_CERTIFICATE_PASSWORD # prompts, so it stays out of shell history
|
||||
#
|
||||
# This buys permission persistence, not Gatekeeper approval: a self-signed
|
||||
# certificate isn't notarized, so downloads are still blocked on first open
|
||||
# and need System Settings → Privacy & Security → Open Anyway.
|
||||
Reference in New Issue
Block a user