Files
hash-wallet/scripts/macos/build_signed.sh
T
jwinterm d0f4e20b29 ci(macos): sign without get-task-allow, and show the notary log on refusal
The first notarized submission came back Invalid: xcodebuild build injects
com.apple.security.get-task-allow into the signature, which archive would
have stripped and which notarization and App Store review both reject.
build_signed.sh now passes CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO and
refuses an app still signed with it, before any upload.

notarytool exits 0 on Invalid, and the step learned the result only when
stapling failed with an unrelated-looking CloudKit error. It now reads the
status, and on anything but Accepted prints Apple's log and names the
submission.
2026-10-27 16:10:23 -04:00

84 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build and sign Hash Bags for macOS against one provisioning profile.
#
# Usage: build_signed.sh <profile.provisionprofile> <keychain> <derived-data-dir> [setting=value ...]
# Run from the repository root after `flutter build macos --release --config-only`.
# Prints the built .app's path as the last line of stdout; build output goes to stderr.
#
# Signing happens inside xcodebuild, never afterwards. Xcode then expands the
# entitlements' $(AppIdentifierPrefix), embeds the profile, and signs every nested
# dylib and framework with the same identity. Re-signing a finished .app by hand
# did none of that: the keychain group shipped as a literal "$(AppIdentifierPrefix)"
# string and the app had no profile to authorise it.
#
# The profile decides the identity. check_macos_profile.py reads the certificates it
# was issued for and picks the one the keychain holds, so a Developer ID profile
# cannot be paired with the App Store certificate or the other way round.
set -euo pipefail
[ $# -ge 3 ] || { echo "usage: $0 <profile> <keychain> <derived-data-dir> [setting=value ...]" >&2; exit 64; }
profile=$1 keychain=$2 derived=$3
shift 3
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
security cms -D -i "$profile" > "$work/profile.plist"
name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$work/profile.plist")"
uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$work/profile.plist")"
team="$(/usr/libexec/PlistBuddy -c 'Print :TeamIdentifier:0' "$work/profile.plist")"
echo "profile: $name ($uuid), team $team" >&2
# Xcode 16 reads UserData; older Xcode reads MobileDevice. Both, by UUID, which is
# what Xcode matches on after resolving the specifier.
for dir in "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" \
"$HOME/Library/MobileDevice/Provisioning Profiles"; do
mkdir -p "$dir"
cp "$profile" "$dir/$uuid.provisionprofile"
done
security find-identity -v -p codesigning "$keychain" > "$work/identities.txt"
identity="$(python3 scripts/macos/check_macos_profile.py \
--profile "$work/profile.plist" --identities "$work/identities.txt" --expect-name "$name")"
if [ -z "$identity" ]; then
# The checker declines only when the profile lists no readable certificate. With
# two lanes in one keychain, guessing could sign with the other lane's identity.
echo "FATAL: profile '$name' names no certificate this build can match; refusing to guess." >&2
exit 1
fi
echo "signing identity: $identity" >&2
# The profile specifier is a Runner-only setting (project.pbxproj reads it from
# HASH_BAGS_MACOS_PROFILE), so the pods never see a profile they would refuse.
# `xcodebuild build`, unlike `archive`, injects com.apple.security.get-task-allow
# into the signature; the notary service rejects any executable carrying it.
# CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO keeps the entitlements to our own file.
xcodebuild -workspace macos/Runner.xcworkspace -scheme Runner \
-configuration Release -derivedDataPath "$derived" \
CODE_SIGN_IDENTITY="$identity" \
DEVELOPMENT_TEAM="$team" \
HASH_BAGS_MACOS_PROFILE="$name" \
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \
OTHER_CODE_SIGN_FLAGS="--keychain $keychain --timestamp" \
"$@" build >&2
app="$(find "$derived/Build/Products/Release" -maxdepth 1 -name '*.app' | head -1)"
[ -n "$app" ] || { echo "FATAL: xcodebuild produced no .app under $derived" >&2; exit 1; }
codesign --verify --deep --strict --verbose=2 "$app" >&2
codesign -d --entitlements - --xml "$app" > "$work/entitlements.plist" 2>/dev/null
if grep -q '\$(' "$work/entitlements.plist"; then
echo "FATAL: the signed entitlements still hold an unexpanded build variable:" >&2
grep '\$(' "$work/entitlements.plist" >&2
exit 1
fi
if grep -q 'get-task-allow' "$work/entitlements.plist"; then
# Notarization and App Store review both reject it, minutes later and remotely.
echo "FATAL: $app is signed with com.apple.security.get-task-allow, a debug-only entitlement" >&2
exit 1
fi
[ -f "$app/Contents/embedded.provisionprofile" ] || {
echo "FATAL: $app carries no embedded provisioning profile" >&2; exit 1; }
echo "$app"