Files
horizon-airdef/packaging/macos/keychain.sh
T
Ludovic Marie 3ffbb1cb9f Sign and notarize the macOS builds with Developer ID
With the repository variable MACOS_SIGN_IDENTITY set, the launcher's Horizon.app and .dmg and the
game's three programs are signed with the hardened runtime and a timestamp, notarized and (app,
dmg) stapled, from a keychain of the Mac mini's own that keychain.sh unlocks with its password from
1Password. Unset, everything is signed ad hoc as before. The launcher checks an updated bundle's
signature before and after swapping it in, and puts the old one back when it doesn't hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:20:34 +02:00

100 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# The keychain the macOS builds are signed and notarized from: a keychain of its own (not the
# login keychain, which a runner's job can't unlock), holding only the "Developer ID
# Application" identity, its intermediate certificate and the notarytool profile. Its password
# is in 1Password (the Agents vault, read with the fleet's service account), so a job unlocks
# it without anyone typing anything. See packaging/macos/README.md.
#
# packaging/macos/keychain.sh unlock # a job's first macOS step: unlock, check, export the env
# packaging/macos/keychain.sh lock # its last
# packaging/macos/keychain.sh create # once: the keychain, with the password from 1Password
# packaging/macos/keychain.sh import P12 # once, from the Mac's own Terminal: the identity
# packaging/macos/keychain.sh notary # once: the notarytool profile from the App Store Connect key
#
# MACOS_SIGN_IDENTITY names the identity (empty: nothing is signed, see package.sh);
# MACOS_SIGN_KEYCHAIN the keychain; MACOS_NOTARY_PROFILE the profile in it; MACOS_KEYCHAIN_PASSWORD,
# when set, is the keychain's password instead of 1Password's.
set -euo pipefail
kc=${MACOS_SIGN_KEYCHAIN:-$HOME/Library/Keychains/horizon-signing.keychain-db}
profile=${MACOS_NOTARY_PROFILE:-horizon-notary}
identity=${MACOS_SIGN_IDENTITY:-Developer ID Application: Ludovic Marie (YYJU63HSD4)}
# The 1Password CLI, signed in as the fleet's agents (`op` elsewhere), and the items it reads.
op=${OP:-$HOME/fleet/op}
[[ -x "$op" ]] || op=op
password_ref=${MACOS_KEYCHAIN_PASSWORD_REF:-op://Agents/Horizon signing keychain/password}
# "App Store Connect API - GitHub CI (DISPO)": a team key; by id, as the name's parentheses
# can't be in a secret reference.
notary_item=${MACOS_NOTARY_ITEM:-wwweyy32dwt6oylm6ypcfivsxe}
# The keychain's password: $MACOS_KEYCHAIN_PASSWORD (a CI secret elsewhere), or from 1Password.
password() {
if [[ -n "${MACOS_KEYCHAIN_PASSWORD:-}" ]]; then
printf '%s' "$MACOS_KEYCHAIN_PASSWORD"
else
"$op" read "$password_ref" < /dev/null
fi
}
case "${1:-}" in
unlock)
security unlock-keychain -p "$(password)" "$kc"
security find-identity -v -p codesigning "$kc" | grep -F "\"$identity\"" > /dev/null ||
{ echo "::error::$kc has no valid \"$identity\" (packaging/macos/keychain.sh import)"; exit 1; }
xcrun notarytool history --keychain-profile "$profile" --keychain "$kc" > /dev/null ||
{ echo "::error::no notarytool profile $profile in $kc (packaging/macos/keychain.sh notary)"; exit 1; }
echo "Unlocked $kc: \"$identity\" and the notarytool profile $profile"
if [[ -n "${GITHUB_ENV:-}" ]]; then
{ echo "MACOS_SIGN_KEYCHAIN=$kc"; echo "MACOS_NOTARY_PROFILE=$profile"; } >> "$GITHUB_ENV"
fi
;;
lock)
security lock-keychain "$kc"
;;
create)
[[ -e "$kc" ]] && { echo "$kc already exists"; exit 1; }
security create-keychain -p "$(password)" "$kc"
# Locks itself after two hours idle and when the Mac sleeps.
security set-keychain-settings -t 7200 -l "$kc"
echo "Created $kc"
;;
import)
p12=${2:?usage: keychain.sh import IDENTITY.p12}
pass=$(password)
security unlock-keychain -p "$pass" "$kc"
read -rsp "The password you gave the .p12 when exporting it: " p12pass && echo
security import "$p12" -k "$kc" -f pkcs12 -P "$p12pass" -T /usr/bin/codesign
unset p12pass
# The intermediate, so codesign builds the chain without the login keychain.
issuer=$(security find-certificate -c "$identity" -p "$kc" | openssl x509 -noout -issuer |
sed -n 's/.*CN *= *\([^,]*\).*/\1/p')
if [[ -n "$issuer" ]] && ! security find-certificate -c "$issuer" "$kc" > /dev/null 2>&1 &&
security find-certificate -c "$issuer" -p "$HOME/Library/Keychains/login.keychain-db" > "$kc.ca.pem"; then
security import "$kc.ca.pem" -k "$kc"
fi
rm -f "$kc.ca.pem"
# Only the one identity: anything else the .p12 held goes.
security find-identity -v -p codesigning "$kc" | sed -n 's/^ *[0-9]*) \([0-9A-F]*\) "\(.*\)"$/\1 \2/p' |
while read -r hash name; do
[[ "$name" == "$identity" ]] || security delete-identity -Z "$hash" "$kc"
done
# codesign may use the key without asking.
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pass" "$kc" > /dev/null
rm -f "$p12"
security find-identity -v -p codesigning "$kc"
echo "Imported; $p12 is deleted."
;;
notary)
security unlock-keychain -p "$(password)" "$kc"
key=$(mktemp) && chmod 600 "$key"
trap 'rm -f "$key"' EXIT
"$op" read "op://Agents/$notary_item/private_key_p8" < /dev/null > "$key"
xcrun notarytool store-credentials "$profile" --key "$key" --keychain "$kc" \
--key-id "$("$op" read "op://Agents/$notary_item/key_id" < /dev/null)" \
--issuer "$("$op" read "op://Agents/$notary_item/issuer_id" < /dev/null)"
;;
*)
sed -n '/^set -e/q; 2,$s/^# \{0,1\}//p' "$0"
exit 1
;;
esac