Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.11.7 - Unreleased

### Maintenance

- Notarize every signed macOS release binary and require Apple's online ticket verification before publication.

## 0.11.6 - 2026-07-17

### Highlights
Expand Down
26 changes: 18 additions & 8 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Assumptions:
- GitHub CLI authenticated
- CI green on `main`
- OpenClaw Foundation Developer ID Application identity available through the managed release keychain
- A `NOTARYTOOL_KEYCHAIN_PROFILE` runtime value whose credentials are stored in the operator's login keychain

## 1) Verify build + tests

Expand Down Expand Up @@ -56,7 +57,7 @@ git push origin main
git push origin vX.Y.Z
```

## 4) Publish signed release artifacts
## 4) Publish signed and notarized release artifacts

From the clean checkout whose `HEAD` exactly matches the signed tag, publish all
GoReleaser artifacts through the shared secret-safe keychain helper:
Expand All @@ -66,16 +67,25 @@ GoReleaser artifacts through the shared secret-safe keychain helper:
```

The script uses an existing GitHub token environment variable or the
authenticated GitHub CLI without writing credentials to disk.

The GoReleaser hook signs only the two macOS binaries. Snapshot builds and all
non-macOS targets remain credential-free. Production builds fail closed unless
the macOS artifacts use identifier `org.openclaw.discrawl` and OpenClaw
Foundation Team ID `FWJYW4S8P8`.
authenticated GitHub CLI without writing credentials to disk. Set
`NOTARYTOOL_KEYCHAIN_PROFILE` only in the private release environment; never
commit a real profile name or its credentials.

The GoReleaser hook signs and notarizes both thin macOS binaries. Each binary is
signed into a temporary candidate with hardened runtime and a trusted timestamp,
submitted to Apple as an ephemeral ZIP with `notarytool --wait`, and accepted
only after the response reports `Accepted` and an online ticket check passes.
Failed signing or notarization leaves the GoReleaser output untouched. Snapshot
builds and all non-macOS targets remain credential-free. Production builds fail
closed unless the notary profile is set and the macOS artifacts use identifier
`org.openclaw.discrawl` and OpenClaw Foundation Team ID `FWJYW4S8P8`.

Publishing the GitHub Release triggers `.github/workflows/release.yml`, which
downloads both macOS archives on native runners, verifies their checksums and
Developer ID signatures, then dispatches the Homebrew update.
Developer ID signatures, hardened-runtime metadata, stable requirement, and
Apple notarization tickets, then dispatches the Homebrew update. Raw CLI
binaries cannot carry stapled tickets, so this verification requires network
access to Apple's ticket service.

## 5) Verify GitHub Release

Expand Down
59 changes: 55 additions & 4 deletions scripts/codesign-release-binary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ EXPECTED_TEAM_ID=${DISCRAWL_CODESIGN_TEAM_ID:-FWJYW4S8P8}
REQUIREMENT="identifier \"$IDENTIFIER\" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = \"$EXPECTED_TEAM_ID\""

case "$TARGET" in
darwin_*) ;;
darwin_arm64*) EXPECTED_ARCH=arm64 ;;
darwin_amd64*) EXPECTED_ARCH=x86_64 ;;
darwin_*)
echo "unsupported macOS release target: $TARGET" >&2
exit 1
;;
*) exit 0 ;;
esac

Expand All @@ -26,16 +31,62 @@ fi
echo "CODESIGN_IDENTITY is required; run the release through mac-release codesign-run" >&2
exit 1
}
[[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || {
echo "NOTARYTOOL_KEYCHAIN_PROFILE is required for official macOS release notarization" >&2
exit 1
}
[[ -f "$BINARY" ]] || {
echo "release binary not found: $BINARY" >&2
exit 1
}
for tool in codesign ditto lipo mktemp mv plutil xcrun; do
command -v "$tool" >/dev/null 2>&1 || {
echo "missing required release command: $tool" >&2
exit 1
}
done

binary_dir=$(cd "$(dirname "$BINARY")" && pwd)
binary_name=$(basename "$BINARY")
work_dir=$(mktemp -d "$binary_dir/.discrawl-notary.XXXXXX")
candidate="$work_dir/$binary_name"
submission="$work_dir/$binary_name.zip"
trap 'rm -rf "$work_dir"' EXIT

# Keep the GoReleaser output untouched unless signing, notarization, and every
# verification step succeeds.
cp -p "$BINARY" "$candidate"
codesign --force --options runtime --timestamp \
--identifier "$IDENTIFIER" --sign "$CODESIGN_IDENTITY" "$BINARY"
codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$BINARY"
--identifier "$IDENTIFIER" --sign "$CODESIGN_IDENTITY" "$candidate"

ditto -c -k --sequesterRsrc --keepParent "$candidate" "$submission"
notary_result=$(xcrun notarytool submit "$submission" \
--keychain-profile "$NOTARYTOOL_KEYCHAIN_PROFILE" \
--no-s3-acceleration \
--wait \
--output-format json)
notary_status=$(plutil -extract status raw -o - - <<<"$notary_result")
notary_id=$(plutil -extract id raw -o - - <<<"$notary_result")
[[ "$notary_status" == Accepted ]] || {
echo "macOS notarization status is ${notary_status:-missing}, expected Accepted" >&2
exit 1
}
[[ "$notary_id" =~ ^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}$ ]] || {
echo "macOS notarization response has an invalid submission id" >&2
exit 1
}

codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$candidate"
codesign --verify --strict --check-notarization -R=notarized --verbose=2 "$candidate"

signature=$(codesign -dvvv "$BINARY" 2>&1)
signature=$(codesign -dvvv "$candidate" 2>&1)
grep -Fx "Identifier=$IDENTIFIER" <<<"$signature" >/dev/null
grep -Fx "TeamIdentifier=$EXPECTED_TEAM_ID" <<<"$signature" >/dev/null
grep -F "Authority=Developer ID Application:" <<<"$signature" >/dev/null
grep -F '(runtime)' <<<"$signature" >/dev/null
[[ "$(lipo -archs "$candidate")" == "$EXPECTED_ARCH" ]] || {
echo "release binary must contain exactly the $EXPECTED_ARCH architecture" >&2
exit 1
}

mv -f "$candidate" "$BINARY"
4 changes: 4 additions & 0 deletions scripts/release-signed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ HELPER=${MAC_RELEASE_HELPER:-$HOME/Projects/agent-scripts/skills/release-mac-app
echo "signed releases must run on macOS" >&2
exit 1
}
[[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || {
echo "NOTARYTOOL_KEYCHAIN_PROFILE is required for official macOS release notarization" >&2
exit 1
}
[[ -x "$HELPER" ]] || {
echo "managed-keychain release helper not found: $HELPER" >&2
exit 1
Expand Down
Loading