Skip to content

Commit 2fef566

Browse files
authored
fix(release): switch macOS sign+notarize to rcodesign hooks (#20)
goreleaser's built-in `notarize.macos` block fails on Apple Developer ID certificates with: failed to verify certificate chain: x509: unhandled critical extension because Go's stdlib x509 verifier doesn't recognise Apple's critical extension OIDs (e.g. 1.2.840.113635.100.6.1.13 for Developer ID Application). Replace it with `rcodesign` (Rust) which handles them correctly. Pipeline shape: 1. release.yaml downloads + installs the rcodesign binary on the ubuntu-latest runner before goreleaser runs. 2. .goreleaser.yaml drops the broken `notarize:` block. Instead, `builds.hooks.post` invokes scripts/sign-macos.sh once per built binary, which signs darwin binaries with rcodesign + --code-signature-flags runtime. Non-darwin builds and missing secret = silent no-op. 3. After goreleaser finishes (archives + checksums + cosign + sbom), release.yaml runs scripts/notarize-macos.sh against dist/, which re-zips each darwin binary, submits to Apple's notary API via the App Store Connect key, and waits for approval. We don't staple — stapling only works on bundles/.pkg/.dmg, and a plain Mach-O CLI binary can't carry a ticket. Apple records notarization server-side and Gatekeeper online-queries at first launch, which is sufficient for a CLI. Both scripts are env-gated: forks and snapshot builds without the secrets pass through unchanged. Co-authored-by: gok03 <gok03@users.noreply.github.com>
1 parent 8ea8f4b commit 2fef566

4 files changed

Lines changed: 143 additions & 25 deletions

File tree

.github/workflows/release.yaml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ jobs:
2727
- uses: sigstore/cosign-installer@v3
2828
- uses: anchore/sbom-action/download-syft@v0
2929

30+
- name: Install rcodesign (Apple code-sign + notarize CLI)
31+
run: |
32+
version=0.29.0
33+
archive=apple-codesign-${version}-x86_64-unknown-linux-musl.tar.gz
34+
curl -fsSL "https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F${version}/${archive}" \
35+
| tar -xz -C /tmp
36+
sudo install /tmp/apple-codesign-${version}-x86_64-unknown-linux-musl/rcodesign /usr/local/bin/rcodesign
37+
rcodesign --version
38+
3039
- uses: goreleaser/goreleaser-action@v7
3140
id: goreleaser
3241
with:
@@ -41,13 +50,15 @@ jobs:
4150
# `.goreleaser.yaml`'s `homebrew_casks.repository.token` template
4251
# reads this env var.
4352
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
44-
# Apple Developer ID signing + notarization. The MACOS_SIGN_P12
45-
# and MACOS_NOTARY_KEY secrets hold base64-encoded blobs of the
46-
# .p12 cert and .p8 notary API key. goreleaser's `notarize.macos`
47-
# block decodes them. Absent secrets → block is `enabled: false`
48-
# and the step becomes a no-op.
53+
# Read by scripts/sign-macos.sh, invoked from goreleaser's
54+
# builds.hooks.post. Absent → script no-ops.
4955
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
5056
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
57+
58+
- name: Notarize darwin binaries
59+
if: env.MACOS_NOTARY_KEY != ''
60+
run: ./scripts/notarize-macos.sh dist
61+
env:
5162
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
5263
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
5364
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}

.goreleaser.yaml

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ builds:
4040
# 32-bit ARM windows is not a supported Go target.
4141
- goos: windows
4242
goarch: arm
43+
hooks:
44+
post:
45+
# Apple Developer ID code-signing for darwin binaries via
46+
# `rcodesign` (Rust). Goreleaser's native notarize uses Go's
47+
# stdlib x509 which can't parse Apple's critical extension OIDs,
48+
# so we shell out to rcodesign instead.
49+
# Skips silently for non-darwin and when MACOS_SIGN_P12 is unset.
50+
- cmd: scripts/sign-macos.sh "{{ .Path }}" "{{ .Os }}"
4351

4452
archives:
4553
- formats: [tar.gz]
@@ -76,26 +84,6 @@ signs:
7684
artifacts: checksum
7785
output: true
7886

79-
# Apple Developer ID code-sign + notarize the macOS binaries so users
80-
# don't hit Gatekeeper ("Apple could not verify…") on first launch.
81-
# Gated on MACOS_SIGN_P12 being set so forks without the secrets get a
82-
# clean no-op rather than an error.
83-
#
84-
# Implementation note: goreleaser's `notarize.macos` shells out to
85-
# rcodesign (built into goreleaser), so this works from a Linux runner —
86-
# no macOS runner needed.
87-
notarize:
88-
macos:
89-
- enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
90-
sign:
91-
certificate: "{{ .Env.MACOS_SIGN_P12 }}"
92-
password: "{{ .Env.MACOS_SIGN_PASSWORD }}"
93-
notarize:
94-
issuer_id: "{{ .Env.MACOS_NOTARY_ISSUER_ID }}"
95-
key_id: "{{ .Env.MACOS_NOTARY_KEY_ID }}"
96-
key: "{{ .Env.MACOS_NOTARY_KEY }}"
97-
wait: true
98-
9987
# SBOMs for every built binary.
10088
sboms:
10189
- artifacts: archive

scripts/notarize-macos.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bash
2+
# notarize-macos.sh — submit darwin binaries from a goreleaser dist/ dir
3+
# to Apple's notary service via rcodesign.
4+
#
5+
# Called as a post-goreleaser step in release.yaml. For each
6+
# refuse_darwin_*.tar.gz in dist/, extract the binary, zip it on its
7+
# own (Apple's notary service accepts .zip, not .tar.gz), submit, and
8+
# wait for approval. We don't staple the ticket — stapling only works
9+
# on bundles/.pkg/.dmg, not plain Mach-O binaries — but the
10+
# notarization is recorded server-side and Gatekeeper online-queries
11+
# Apple at first launch.
12+
#
13+
# Requires (set as workflow env from secrets):
14+
# MACOS_NOTARY_KEY — base64 of the App Store Connect .p8 key
15+
# MACOS_NOTARY_KEY_ID — the 10-char Key ID
16+
# MACOS_NOTARY_ISSUER_ID — the UUID Issuer ID
17+
#
18+
# Exits 0 (silently) when secrets aren't set. Fails loudly on Apple's
19+
# rejection so the release workflow surfaces the problem.
20+
21+
set -euo pipefail
22+
23+
DIST="${1:-dist}"
24+
25+
if [ -z "${MACOS_NOTARY_KEY:-}" ]; then
26+
echo "notarize-macos: MACOS_NOTARY_KEY unset — skipping" >&2
27+
exit 0
28+
fi
29+
30+
tmp=$(mktemp -d)
31+
trap 'rm -rf "$tmp"' EXIT
32+
33+
# Build an rcodesign-compatible API key JSON from the three secrets.
34+
p8="$tmp/AuthKey.p8"
35+
printf '%s' "$MACOS_NOTARY_KEY" | base64 -d > "$p8"
36+
37+
api_key_json="$tmp/api-key.json"
38+
rcodesign encode-app-store-connect-api-key \
39+
-o "$api_key_json" \
40+
"$MACOS_NOTARY_ISSUER_ID" \
41+
"$MACOS_NOTARY_KEY_ID" \
42+
"$p8"
43+
44+
shopt -s nullglob
45+
archives=( "$DIST"/refuse_darwin_*.tar.gz )
46+
if [ ${#archives[@]} -eq 0 ]; then
47+
echo "notarize-macos: no darwin archives in $DIST — nothing to do" >&2
48+
exit 0
49+
fi
50+
51+
for archive in "${archives[@]}"; do
52+
name=$(basename "$archive" .tar.gz)
53+
work="$tmp/$name"
54+
mkdir -p "$work"
55+
56+
echo "notarize-macos: preparing $name"
57+
tar -xzf "$archive" -C "$work"
58+
59+
zip="$tmp/$name.zip"
60+
( cd "$work" && zip -q "$zip" refuse )
61+
62+
echo "notarize-macos: submitting $name.zip to Apple notary"
63+
rcodesign notary-submit \
64+
--api-key-file "$api_key_json" \
65+
--wait \
66+
"$zip"
67+
done
68+
69+
echo "notarize-macos: all darwin archives notarized"

scripts/sign-macos.sh

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
# sign-macos.sh — code-sign a single darwin Go binary with rcodesign.
3+
#
4+
# Called by goreleaser's `builds.hooks.post` once per built binary. We
5+
# can't use goreleaser's native `notarize.macos` block because its Go
6+
# x509 verifier chokes on Apple's critical extension OIDs (e.g.
7+
# 1.2.840.113635.100.6.1.13 — Developer ID Application). rcodesign
8+
# (Rust) handles them correctly.
9+
#
10+
# Usage: sign-macos.sh BINARY_PATH GOOS
11+
#
12+
# Requires (set as workflow env from secrets):
13+
# MACOS_SIGN_P12 — base64 of the Developer ID .p12
14+
# MACOS_SIGN_PASSWORD — password the .p12 was exported with
15+
#
16+
# Exits 0 on success, 0 (silently) when GOOS != darwin or when the
17+
# signing secret isn't set (forks / local snapshot builds).
18+
19+
set -euo pipefail
20+
21+
BINARY="${1:?missing BINARY arg}"
22+
OS="${2:?missing GOOS arg}"
23+
24+
if [ "$OS" != "darwin" ]; then
25+
exit 0
26+
fi
27+
if [ -z "${MACOS_SIGN_P12:-}" ]; then
28+
echo "sign-macos: MACOS_SIGN_P12 unset — leaving $BINARY unsigned" >&2
29+
exit 0
30+
fi
31+
32+
tmp=$(mktemp -d)
33+
trap 'rm -rf "$tmp"' EXIT
34+
35+
p12="$tmp/cert.p12"
36+
printf '%s' "$MACOS_SIGN_P12" | base64 -d > "$p12"
37+
38+
echo "sign-macos: signing $BINARY"
39+
rcodesign sign \
40+
--p12-file "$p12" \
41+
--p12-password "$MACOS_SIGN_PASSWORD" \
42+
--code-signature-flags runtime \
43+
"$BINARY"
44+
45+
# Best-effort sanity check.
46+
if rcodesign verify "$BINARY" >/dev/null 2>&1; then
47+
echo "sign-macos: $BINARY verified"
48+
else
49+
echo "sign-macos: WARNING — $BINARY verify failed (continuing)" >&2
50+
fi

0 commit comments

Comments
 (0)