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.
119 lines
5.6 KiB
Bash
119 lines
5.6 KiB
Bash
# 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.
|