Skip to main content

max / goingson

8.5 KB · 180 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 # ./dist/build-keychain.sh setup # leaves the keychain in place
29 # ./dist/build-keychain.sh identity # print the Developer ID identity it holds
30 # ./dist/build-keychain.sh teardown # remove it + restore the search list
31 #
32 # --- Required env (live in ~/.tauri/passwords.env on mbp; never in git) ---
33 # BUILD_P12_PATH path to the exported Developer ID Application .p12
34 # BUILD_P12_PASSWORD password protecting that .p12
35 # BUILD_KEYCHAIN_PASSWORD password for the temp keychain (ephemeral; any value)
36 #
37 # One-time prerequisite (cannot be done over SSH -- needs a GUI Terminal on the
38 # Mac with login.keychain unlocked) -- export the cert + private key to a .p12:
39 # security find-identity -v -p codesigning | grep "Developer ID Application"
40 # # then, in Keychain Access: right-click the "Developer ID Application" identity
41 # # -> Export -> .p12 -> set a password -> save to ~/Code/_private/developer-id.p12
42 # # (or CLI: security export -t identities -f pkcs12 \
43 # # -P "<p12pw>" -o ~/Code/_private/developer-id.p12 )
44 # # NOTE: omit -k. On modern macOS the Developer ID key lives in the
45 # # data-protection keychain, not login.keychain-db; passing
46 # # `-k login.keychain` finds nothing. Default search list works.
47 # Then add BUILD_P12_PATH / BUILD_P12_PASSWORD / BUILD_KEYCHAIN_PASSWORD to
48 # ~/.tauri/passwords.env.
49
50 set -euo pipefail
51
52 # A FULL PATH, not a bare name. Over SSH `security create-keychain` with a bare
53 # name tries ~/Library/Keychains/ and fails "SecKeychainCreate ... Permission
54 # denied"; a full path in $TMPDIR works headlessly (the CI-standard approach).
55 # Override with BUILD_KEYCHAIN if you need a different location.
56 : "${BUILD_KEYCHAIN:=${TMPDIR:-/tmp}/bento-build.keychain-db}"
57
58 # Path used to stash the original keychain search list across setup/teardown so a
59 # standalone teardown (separate process) can still restore it.
60 _BK_STATE_FILE="${TMPDIR:-/tmp}/.bento-build-keychain.searchlist"
61
62 _bk_require_env() {
63 local missing=0
64 for v in BUILD_P12_PATH BUILD_P12_PASSWORD BUILD_KEYCHAIN_PASSWORD; do
65 if [ -z "${!v:-}" ]; then
66 echo "build-keychain: required env $v is unset (source ~/.tauri/passwords.env)" >&2
67 missing=1
68 fi
69 done
70 if [ -n "${BUILD_P12_PATH:-}" ] && [ ! -f "$BUILD_P12_PATH" ]; then
71 echo "build-keychain: BUILD_P12_PATH not found: $BUILD_P12_PATH" >&2
72 echo " Run the one-time GUI .p12 export first (see header of this script)." >&2
73 missing=1
74 fi
75 [ "$missing" -eq 0 ] || return 1
76 }
77
78 bk_setup() {
79 _bk_require_env || return 1
80
81 # If a stale keychain from a crashed run exists, remove it first.
82 security delete-keychain "$BUILD_KEYCHAIN" 2>/dev/null || true
83
84 echo "build-keychain: creating $BUILD_KEYCHAIN" >&2
85 if ! security create-keychain -p "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN"; then
86 echo "build-keychain: create-keychain FAILED. Over SSH this needs a full" >&2
87 echo " keychain PATH (BUILD_KEYCHAIN=$BUILD_KEYCHAIN) and an active console" >&2
88 echo " login session on the Mac." >&2
89 return 1
90 fi
91 # No idle auto-lock during a long build; lock-on-sleep stays on.
92 security set-keychain-settings -lut 21600 "$BUILD_KEYCHAIN"
93 security unlock-keychain -p "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN"
94
95 echo "build-keychain: importing $BUILD_P12_PATH" >&2
96 security import "$BUILD_P12_PATH" -k "$BUILD_KEYCHAIN" -P "$BUILD_P12_PASSWORD" \
97 -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productsign
98
99 # Import the Apple intermediate(s). An isolated keychain holds only the leaf
100 # cert; codesign rejects an identity whose chain it can't build ("no identity
101 # found"). Match the leaf's issuer: this team's Developer ID Application cert is
102 # issued by the ORIGINAL "Developer ID Certification Authority"
103 # (OU=Apple Certification Authority) -> DeveloperIDCA.cer; G2 (OU=G2) is for
104 # newer certs and will NOT chain it. Both are public; we fetch (cached) and
105 # import both so either generation works. Apple Root CA is in the System trust
106 # store, so System.keychain must stay on the search list (below).
107 _bk_import_apple_intermediates
108
109 # Pre-authorize the tools to use the imported key so macOS doesn't pop a GUI
110 # prompt (apple-tool:/apple: is the Apple-standard partition set).
111 security set-key-partition-list -S apple-tool:,apple: \
112 -s -k "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN" >/dev/null
113
114 # Prepend our keychain to the user search list, KEEPING the existing ones
115 # (login + System). Dropping System breaks root anchoring/lookups.
116 local orig
117 orig="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
118 printf '%s\n' "$orig" > "$_BK_STATE_FILE"
119 # shellcheck disable=SC2086 # word-splitting the list is intentional
120 security list-keychains -d user -s "$BUILD_KEYCHAIN" $orig
121
122 echo "build-keychain: ready -- identity: $(bk_identity_name || echo '??')" >&2
123 # NOTE: keychain setup is now correct, but codesign STILL needs to run in the
124 # console GUI (Aqua) security session to USE the private key. A pure SSH session
125 # can enumerate the identity but not sign with it ("no identity found"). Drive
126 # the build from a GUI Terminal, a user LaunchAgent, or `launchctl asuser <uid>`
127 # (needs root). See bento/design.md "THE WALL" for the full analysis.
128 }
129
130 # Fetch (cached under the keychain dir) + import the public Apple Developer ID
131 # intermediates so codesign can build the cert chain.
132 _bk_import_apple_intermediates() {
133 local dir cer url
134 dir="$(dirname "$BUILD_KEYCHAIN")"
135 for cer in DeveloperIDCA.cer DeveloperIDG2CA.cer; do
136 url="https://www.apple.com/certificateauthority/$cer"
137 if [ ! -f "$dir/$cer" ]; then
138 curl -fsS "$url" -o "$dir/$cer" 2>/dev/null \
139 || { echo "build-keychain: warn: could not fetch $cer" >&2; continue; }
140 fi
141 security import "$dir/$cer" -k "$BUILD_KEYCHAIN" >/dev/null 2>&1 || true
142 done
143 }
144
145 bk_identity_name() {
146 security find-identity -v -p codesigning "$BUILD_KEYCHAIN" \
147 | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -1
148 }
149
150 bk_identity_hash() {
151 security find-identity -v -p codesigning "$BUILD_KEYCHAIN" \
152 | awk '/Developer ID Application/ {print $2; exit}'
153 }
154
155 bk_teardown() {
156 # Restore the original search list if we saved one.
157 if [ -f "$_BK_STATE_FILE" ]; then
158 local orig
159 orig="$(cat "$_BK_STATE_FILE")"
160 if [ -n "$orig" ]; then
161 # shellcheck disable=SC2086
162 security list-keychains -d user -s $orig 2>/dev/null || true
163 fi
164 rm -f "$_BK_STATE_FILE"
165 fi
166 security delete-keychain "$BUILD_KEYCHAIN" 2>/dev/null || true
167 echo "build-keychain: torn down $BUILD_KEYCHAIN" >&2
168 }
169
170 # When run directly (not sourced), act as a small CLI.
171 if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
172 case "${1:-}" in
173 setup) bk_setup ;;
174 identity) bk_identity_name ;;
175 hash) bk_identity_hash ;;
176 teardown) bk_teardown ;;
177 *) echo "usage: $0 {setup|identity|hash|teardown}" >&2; exit 2 ;;
178 esac
179 fi
180