Skip to main content

max / goingson

9.3 KB · 192 lines History Blame Raw
1 #!/usr/bin/env bash
2 # build-keychain.sh: ephemeral Developer ID build keychain for headless signing.
3 #
4 # Why this exists: over SSH the Mac's login.keychain is locked
5 # ("User interaction is not allowed"), so `security find-identity` reports
6 # "0 valid identities found" and codesign silently produces an unsigned bundle.
7 # This creates a throwaway keychain, imports a password-protected .p12, and runs
8 # `set-key-partition-list` so codesign can use the key WITHOUT the GUI auth
9 # prompt that otherwise hangs a non-interactive session forever.
10 #
11 # Team-agnostic on purpose: it signs with whatever identity the .p12 contains.
12 # That means remote signing works BEFORE the Apple Developer org transfer to the
13 # LLC is finished; the team only matters at the notarization step, which is a
14 # separate concern (see release-macos.sh --sign-only).
15 #
16 # This is the first concrete piece of Bento (the app-release orchestrator).
17 # Bento's macOS recipe will `source` this; for now release-macos.sh does.
18 #
19 # --- Usage (sourced, the normal path) ---
20 # source "$(dirname "$0")/build-keychain.sh"
21 # bk_setup # create + import + unlock + put on search list
22 # trap bk_teardown EXIT # ALWAYS tear down, even on failure
23 # id="$(bk_identity_name)" # the "Developer ID Application: ... (TEAM)" string
24 # ... codesign -s "$id" ... / ... APPLE_SIGNING_IDENTITY="$id" cargo tauri build ...
25 # # teardown runs on exit
26 #
27 # --- Usage (standalone, for inspection/debugging) ---
28 # # setup + teardown run as SEPARATE processes, so pin the keychain path so
29 # # both agree (the default is unique per process, see BUILD_KEYCHAIN below):
30 # export BUILD_KEYCHAIN="${TMPDIR:-/tmp}/bento-build.debug.keychain-db"
31 # ./dist/build-keychain.sh setup # leaves the keychain in place
32 # ./dist/build-keychain.sh identity # print the Developer ID identity it holds
33 # ./dist/build-keychain.sh teardown # remove it + restore the search list
34 #
35 # --- Required env (live in ~/.tauri/passwords.env on mbp; never in git) ---
36 # BUILD_P12_PATH path to the exported Developer ID Application .p12
37 # BUILD_P12_PASSWORD password protecting that .p12
38 # BUILD_KEYCHAIN_PASSWORD password for the temp keychain (ephemeral; any value)
39 #
40 # One-time prerequisite (cannot be done over SSH, needs a GUI Terminal on the
41 # Mac with login.keychain unlocked): export the cert + private key to a .p12:
42 # security find-identity -v -p codesigning | grep "Developer ID Application"
43 # # then, in Keychain Access: right-click the "Developer ID Application" identity
44 # # -> Export -> .p12 -> set a password -> save to ~/Code/_private/developer-id.p12
45 # # (or CLI: security export -t identities -f pkcs12 \
46 # # -P "<p12pw>" -o ~/Code/_private/developer-id.p12 )
47 # # NOTE: omit -k. On modern macOS the Developer ID key lives in the
48 # # data-protection keychain, not login.keychain-db; passing
49 # # `-k login.keychain` finds nothing. Default search list works.
50 # Then add BUILD_P12_PATH / BUILD_P12_PASSWORD / BUILD_KEYCHAIN_PASSWORD to
51 # ~/.tauri/passwords.env.
52
53 set -euo pipefail
54
55 # A FULL PATH, not a bare name. Over SSH `security create-keychain` with a bare
56 # name tries ~/Library/Keychains/ and fails "SecKeychainCreate ... Permission
57 # denied"; a full path in $TMPDIR works headlessly (the CI-standard approach).
58 #
59 # UNIQUE PER RUN. A fixed path (`bento-build.keychain-db`) let two macOS builds
60 # on the same Mac collide: the second's `security delete-keychain` (bk_setup
61 # below) wiped the first's keychain mid-codesign, and both stomped one shared
62 # search-list state file, so teardown restored a polluted list. `mktemp -u`
63 # mints an unused name once, at source time, so it is stable across this
64 # process's bk_setup + bk_teardown yet distinct from any concurrent run.
65 # Override with BUILD_KEYCHAIN to pin a location (e.g. to pair a standalone
66 # `setup` and `teardown` that run as separate processes).
67 : "${BUILD_KEYCHAIN:=$(mktemp -u "${TMPDIR:-/tmp}/bento-build.XXXXXX").keychain-db}"
68
69 # The original keychain search list is stashed here across setup/teardown.
70 # Derived from BUILD_KEYCHAIN so it is unique-and-paired with it: a concurrent
71 # run saves/restores its own list rather than sharing one file.
72 _BK_STATE_FILE="${BUILD_KEYCHAIN%.keychain-db}.searchlist"
73
74 _bk_require_env() {
75 local missing=0
76 for v in BUILD_P12_PATH BUILD_P12_PASSWORD BUILD_KEYCHAIN_PASSWORD; do
77 if [ -z "${!v:-}" ]; then
78 echo "build-keychain: required env $v is unset (source ~/.tauri/passwords.env)" >&2
79 missing=1
80 fi
81 done
82 if [ -n "${BUILD_P12_PATH:-}" ] && [ ! -f "$BUILD_P12_PATH" ]; then
83 echo "build-keychain: BUILD_P12_PATH not found: $BUILD_P12_PATH" >&2
84 echo " Run the one-time GUI .p12 export first (see header of this script)." >&2
85 missing=1
86 fi
87 [ "$missing" -eq 0 ] || return 1
88 }
89
90 bk_setup() {
91 _bk_require_env || return 1
92
93 # If a stale keychain from a crashed run exists, remove it first.
94 security delete-keychain "$BUILD_KEYCHAIN" 2>/dev/null || true
95
96 echo "build-keychain: creating $BUILD_KEYCHAIN" >&2
97 if ! security create-keychain -p "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN"; then
98 echo "build-keychain: create-keychain FAILED. Over SSH this needs a full" >&2
99 echo " keychain PATH (BUILD_KEYCHAIN=$BUILD_KEYCHAIN) and an active console" >&2
100 echo " login session on the Mac." >&2
101 return 1
102 fi
103 # No idle auto-lock during a long build; lock-on-sleep stays on.
104 security set-keychain-settings -lut 21600 "$BUILD_KEYCHAIN"
105 security unlock-keychain -p "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN"
106
107 echo "build-keychain: importing $BUILD_P12_PATH" >&2
108 security import "$BUILD_P12_PATH" -k "$BUILD_KEYCHAIN" -P "$BUILD_P12_PASSWORD" \
109 -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productsign
110
111 # Import the Apple intermediate(s). An isolated keychain holds only the leaf
112 # cert; codesign rejects an identity whose chain it can't build ("no identity
113 # found"). Match the leaf's issuer: this team's Developer ID Application cert is
114 # issued by the ORIGINAL "Developer ID Certification Authority"
115 # (OU=Apple Certification Authority) -> DeveloperIDCA.cer; G2 (OU=G2) is for
116 # newer certs and will NOT chain it. Both are public; we fetch (cached) and
117 # import both so either generation works. Apple Root CA is in the System trust
118 # store, so System.keychain must stay on the search list (below).
119 _bk_import_apple_intermediates
120
121 # Pre-authorize the tools to use the imported key so macOS doesn't pop a GUI
122 # prompt (apple-tool:/apple: is the Apple-standard partition set).
123 security set-key-partition-list -S apple-tool:,apple: \
124 -s -k "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN" >/dev/null
125
126 # Prepend our keychain to the user search list, KEEPING the existing ones
127 # (login + System). Dropping System breaks root anchoring/lookups.
128 local orig
129 orig="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
130 printf '%s\n' "$orig" > "$_BK_STATE_FILE"
131 # shellcheck disable=SC2086 # word-splitting the list is intentional
132 security list-keychains -d user -s "$BUILD_KEYCHAIN" $orig
133
134 echo "build-keychain: ready; identity: $(bk_identity_name || echo '??')" >&2
135 # NOTE: keychain setup is now correct, but codesign STILL needs to run in the
136 # console GUI (Aqua) security session to USE the private key. A pure SSH session
137 # can enumerate the identity but not sign with it ("no identity found"). Drive
138 # the build from a GUI Terminal, a user LaunchAgent, or `launchctl asuser <uid>`
139 # (needs root). See bento/design.md "THE WALL" for the full analysis.
140 }
141
142 # Fetch (cached under the keychain dir) + import the public Apple Developer ID
143 # intermediates so codesign can build the cert chain.
144 _bk_import_apple_intermediates() {
145 local dir cer url
146 dir="$(dirname "$BUILD_KEYCHAIN")"
147 for cer in DeveloperIDCA.cer DeveloperIDG2CA.cer; do
148 url="https://www.apple.com/certificateauthority/$cer"
149 if [ ! -f "$dir/$cer" ]; then
150 curl -fsS "$url" -o "$dir/$cer" 2>/dev/null \
151 || { echo "build-keychain: warn: could not fetch $cer" >&2; continue; }
152 fi
153 security import "$dir/$cer" -k "$BUILD_KEYCHAIN" >/dev/null 2>&1 || true
154 done
155 }
156
157 bk_identity_name() {
158 security find-identity -v -p codesigning "$BUILD_KEYCHAIN" \
159 | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -1
160 }
161
162 bk_identity_hash() {
163 security find-identity -v -p codesigning "$BUILD_KEYCHAIN" \
164 | awk '/Developer ID Application/ {print $2; exit}'
165 }
166
167 bk_teardown() {
168 # Restore the original search list if we saved one.
169 if [ -f "$_BK_STATE_FILE" ]; then
170 local orig
171 orig="$(cat "$_BK_STATE_FILE")"
172 if [ -n "$orig" ]; then
173 # shellcheck disable=SC2086
174 security list-keychains -d user -s $orig 2>/dev/null || true
175 fi
176 rm -f "$_BK_STATE_FILE"
177 fi
178 security delete-keychain "$BUILD_KEYCHAIN" 2>/dev/null || true
179 echo "build-keychain: torn down $BUILD_KEYCHAIN" >&2
180 }
181
182 # When run directly (not sourced), act as a small CLI.
183 if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
184 case "${1:-}" in
185 setup) bk_setup ;;
186 identity) bk_identity_name ;;
187 hash) bk_identity_hash ;;
188 teardown) bk_teardown ;;
189 *) echo "usage: $0 {setup|identity|hash|teardown}" >&2; exit 2 ;;
190 esac
191 fi
192