History

build(release): notarize macOS binaries (#118)

Peter Steinberger committed 4 months ago

Commit afaefcb1 parent 2c26a606 files · +130 −10Browse files

Showing the changes to scripts (4 of 6 files). Show all

Unified · Split

4 files
  1. scripts/codesign-macos.sh +44 −3
  2. scripts/package-release.sh +5 −1
  3. scripts/test-release.sh +74 −3
  4. scripts/verify-release.sh +1 −0
scripts/codesign-macos.sh+44−3
@@ -31,16 +31,57 @@ identity=${CODESIGN_IDENTITY:-} · 30 unmodified lines
3131 echo "codesign: official macOS releases require $expected_authority" >&2
3232 exit 1
3333 }
34+[[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || {
35+ echo "codesign: NOTARYTOOL_KEYCHAIN_PROFILE is required at runtime" >&2
36+ exit 1
37+}
38+
39+for tool in codesign ditto mktemp mv plutil xcrun; do
40+ command -v "$tool" >/dev/null 2>&1 || {
41+ echo "codesign: missing required command: $tool" >&2
42+ exit 1
43+ }
44+done
3445
46+binary_dir=$(cd "$(dirname "$binary")" && pwd)
47+binary_name=$(basename "$binary")
48+work_dir=$(mktemp -d "$binary_dir/.gitcrawl-notary.XXXXXX")
49+candidate="$work_dir/$binary_name"
50+submission="$work_dir/$binary_name.zip"
51+trap 'rm -rf "$work_dir"' EXIT
52+
53+# Keep the GoReleaser output unchanged unless the complete signing and
54+# notarization contract succeeds.
55+cp -p "$binary" "$candidate"
3556 codesign --force \
3657 --options runtime \
3758 --timestamp \
3859 --identifier "$identifier" \
3960 --sign "$identity" \
40− "$binary"
41−codesign --verify --strict -R="$requirement" --verbose=2 "$binary"
61+ "$candidate"
62+codesign --verify --strict -R="$requirement" --verbose=2 "$candidate"
4263
43−signature=$(codesign -dvvv "$binary" 2>&1)
64+signature=$(codesign -dvvv "$candidate" 2>&1)
4465 grep -Fx "Identifier=$identifier" <<<"$signature" >/dev/null
4566 grep -Fx "TeamIdentifier=$expected_team_id" <<<"$signature" >/dev/null
4667 grep -Fx "Authority=$expected_authority" <<<"$signature" >/dev/null
68+
69+ditto -c -k --sequesterRsrc --keepParent "$candidate" "$submission"
70+notary_result=$(xcrun notarytool submit "$submission" \
71+ --keychain-profile "$NOTARYTOOL_KEYCHAIN_PROFILE" \
72+ --no-s3-acceleration \
73+ --wait \
74+ --output-format json)
75+notary_status=$(plutil -extract status raw -o - - <<<"$notary_result")
76+notary_id=$(plutil -extract id raw -o - - <<<"$notary_result")
77+[[ "$notary_status" == Accepted ]] || {
78+ echo "codesign: notarization status is ${notary_status:-missing}, expected Accepted" >&2
79+ exit 1
80+}
81+[[ "$notary_id" =~ ^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}$ ]] || {
82+ echo "codesign: notarization response has an invalid submission id" >&2
83+ exit 1
84+}
85+
86+codesign --verify --strict --check-notarization -R=notarized "$candidate"
87+mv -f "$candidate" "$binary"
scripts/package-release.sh+5−1
@@ -21,8 +21,12 @@ fi · 20 unmodified lines
2121 echo "official releases require $EXPECTED_AUTHORITY" >&2
2222 exit 1
2323 }
24+[[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || {
25+ echo "official releases require NOTARYTOOL_KEYCHAIN_PROFILE at runtime" >&2
26+ exit 1
27+}
2428
25−for tool in codesign git go goreleaser lipo shasum tar; do
29+for tool in codesign ditto git go goreleaser lipo plutil shasum tar xcrun; do
2630 command -v "$tool" >/dev/null || {
2731 echo "missing required tool: $tool" >&2
2832 exit 1
scripts/test-release.sh+74−3
@@ -25,6 +25,9 @@ EOF · 24 unmodified lines
2525 cat > "$FAKE_BIN/codesign" <<'EOF'
2626 #!/usr/bin/env bash
2727 printf '%s\n' "$*" >> "${MOCK_CODESIGN_LOG:?}"
28+if [[ " $* " == *' --check-notarization '* && "${MOCK_NOTARIZATION_REJECT:-0}" == 1 ]]; then
29+ exit 1
30+fi
2831 case " $* " in
2932 *' -dvvv '*)
3033 {
@@ -36,6 +39,32 @@ case " $* " in · 5 unmodified lines
3639 esac
3740 EOF
3841
42+cat > "$FAKE_BIN/ditto" <<'EOF'
43+#!/usr/bin/env bash
44+previous=
45+for arg in "$@"; do
46+ source=$previous
47+ previous=$arg
48+done
49+cp "$source" "$previous"
50+printf 'ditto %s\n' "$*" >> "${MOCK_CODESIGN_LOG:?}"
51+EOF
52+
53+cat > "$FAKE_BIN/xcrun" <<'EOF'
54+#!/usr/bin/env bash
55+printf 'xcrun %s\n' "$*" >> "${MOCK_CODESIGN_LOG:?}"
56+printf '{"id":"12345678-1234-1234-1234-123456789abc","status":"%s"}\n' "${MOCK_NOTARY_STATUS:-Accepted}"
57+EOF
58+
59+cat > "$FAKE_BIN/plutil" <<'EOF'
60+#!/usr/bin/env bash
61+case "${2:-}" in
62+ status) printf '%s\n' "${MOCK_NOTARY_STATUS:-Accepted}" ;;
63+ id) printf '%s\n' '12345678-1234-1234-1234-123456789abc' ;;
64+ *) exit 1 ;;
65+esac
66+EOF
67+
3968 cat > "$FAKE_BIN/lipo" <<'EOF'
4069 #!/usr/bin/env bash
4170 case "${2:-}" in
@@ -47,6 +76,7 @@ EOF · 5 unmodified lines
4776 chmod 0755 "$FAKE_BIN"/*
4877 export PATH="$FAKE_BIN:$PATH"
4978 export MOCK_CODESIGN_LOG="$WORK_DIR/codesign.log"
79+unset NOTARYTOOL_KEYCHAIN_PROFILE
5080
5181 test_binary="$WORK_DIR/gitcrawl"
5282 cat > "$test_binary" <<'EOF'
@@ -62,15 +92,48 @@ if CODESIGN_IDENTITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' · 9 unmodified lines
6292 echo "package script accepted personal signing identity" >&2
6393 exit 1
6494 fi
95+if CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
96+ "$ROOT/scripts/package-release.sh" v0.7.1 >/dev/null 2>&1; then
97+ echo "package script accepted a missing notary profile" >&2
98+ exit 1
99+fi
65100 if GITCRAWL_REQUIRE_CODESIGN=1 \
66101 CODESIGN_IDENTITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \
67102 "$ROOT/scripts/codesign-macos.sh" "$test_binary" >/dev/null 2>&1; then
68103 echo "personal signing identity was accepted" >&2
69104 exit 1
70105 fi
106+if GITCRAWL_REQUIRE_CODESIGN=1 \
107+ CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
108+ "$ROOT/scripts/codesign-macos.sh" "$test_binary" >/dev/null 2>&1; then
109+ echo "official signing accepted a missing notary profile" >&2
110+ exit 1
111+fi
71112 GITCRAWL_REQUIRE_CODESIGN=1 \
72113 CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
114+ NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \
73115 "$ROOT/scripts/codesign-macos.sh" "$test_binary"
116+grep -F -- '--identifier org.openclaw.gitcrawl' "$MOCK_CODESIGN_LOG" >/dev/null
117+grep -F 'FWJYW4S8P8' "$MOCK_CODESIGN_LOG" >/dev/null
118+grep -F -- 'notarytool submit' "$MOCK_CODESIGN_LOG" >/dev/null
119+grep -F -- '--keychain-profile test-profile --no-s3-acceleration --wait --output-format json' "$MOCK_CODESIGN_LOG" >/dev/null
120+signed_hash=$(shasum -a 256 "$test_binary" | awk '{ print $1 }')
121+if GITCRAWL_REQUIRE_CODESIGN=1 \
122+ CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
123+ NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \
124+ MOCK_NOTARY_STATUS=Invalid \
125+ "$ROOT/scripts/codesign-macos.sh" "$test_binary" >/dev/null 2>&1; then
126+ echo "invalid notarization status was accepted" >&2
127+ exit 1
128+fi
129+[[ "$(shasum -a 256 "$test_binary" | awk '{ print $1 }')" == "$signed_hash" ]] || {
130+ echo "failed notarization mutated the release binary" >&2
131+ exit 1
132+}
133+if find "$WORK_DIR" -maxdepth 1 -name '.gitcrawl-notary.*' | grep -q .; then
134+ echo "ephemeral notarization files were not removed" >&2
135+ exit 1
136+fi
74137
75138 ARTIFACTS="$WORK_DIR/artifacts"
76139 mkdir -p "$ARTIFACTS"
@@ -84,7 +147,14 @@ for arch in amd64 arm64; do · 7 unmodified lines
84147 shasum -a 256 "$ARTIFACTS/$archive" | awk -v name="$archive" '{ print $1 " " name }' >> "$ARTIFACTS/checksums.txt"
85148 done
86149
150+: > "$MOCK_CODESIGN_LOG"
87151 "$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS"
152+[[ "$(grep -F -c -- '--verify --strict --check-notarization -R=notarized' "$MOCK_CODESIGN_LOG")" == 2 ]]
153+if MOCK_NOTARIZATION_REJECT=1 \
154+ "$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS" >/dev/null 2>&1; then
155+ echo "release verifier accepted a missing notarization ticket" >&2
156+ exit 1
157+fi
88158 if MOCK_CODESIGN_AUTHORITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \
89159 "$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS" >/dev/null 2>&1; then
90160 echo "personal signature was accepted" >&2
@@ -104,9 +174,6 @@ if "$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS" >/dev/null 2>&1; then · 13 unmodified lines
104174 exit 1
105175 fi
106176
107−grep -F -- '--identifier org.openclaw.gitcrawl' "$MOCK_CODESIGN_LOG" >/dev/null
108−grep -F 'FWJYW4S8P8' "$MOCK_CODESIGN_LOG" >/dev/null
109−
110177 release_workflow="$ROOT/.github/workflows/release-assets.yml"
111178 grep -F 'contents: write' "$release_workflow" >/dev/null
112179 grep -F "github.ref == format('refs/heads/{0}', github.event.repository.default_branch)" "$release_workflow" >/dev/null
@@ -121,6 +188,10 @@ grep -F 'persist-credentials: false' "$release_workflow" >/dev/null · 8 unmodified lines
121188 grep -F 'tag_name == $tag and (.draft == ($draft == "true"))' "$release_workflow" >/dev/null
122189 grep -F 'Accept: application/octet-stream' "$release_workflow" >/dev/null
123190 grep -F 'unset GH_TOKEN GITHUB_TOKEN' "$release_workflow" >/dev/null
191+if grep -R -F 'NOTARYTOOL_KEYCHAIN_PROFILE' "$ROOT/.github/workflows" >/dev/null; then
192+ echo "notary profile must not be configured in GitHub Actions" >&2
193+ exit 1
194+fi
124195 if grep -F 'gh release download' "$release_workflow" >/dev/null; then
125196 echo "release workflow cannot resolve draft assets through gh release download" >&2
126197 exit 1
scripts/verify-release.sh+1−0
@@ -73,6 +73,7 @@ for goarch in "${architectures[@]}"; do · 72 unmodified lines
7373 }
7474
7575 codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$binary"
76+ codesign --verify --strict --check-notarization -R=notarized "$binary"
7677 signature=$(codesign -dvvv "$binary" 2>&1)
7778 grep -Fx "Identifier=$IDENTIFIER" <<<"$signature" >/dev/null
7879 grep -Fx "TeamIdentifier=$EXPECTED_TEAM_ID" <<<"$signature" >/dev/null

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t