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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.8.1 - Unreleased

- Require every official macOS release binary to pass hardened-runtime signing, Apple notarization, and independent Foundation and notarized-requirement verification before packaging.
- Route embedding and semantic-search requests through a configurable embedding-only OpenAI-compatible endpoint while preserving the shared endpoint fallback. Thanks @larroy.

## 0.8.0 - 2026-07-17
Expand Down
8 changes: 5 additions & 3 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ permalink: /releasing/
Official releases are assembled locally on an authorized maintainer Mac. GitHub Actions only validates credential-free snapshots and never publishes release artifacts.

1. Prepare and sign the release commit and tag on `main`, then ensure the checkout is clean and `HEAD` exactly matches that tag.
2. Configure the shared `release-mac-app` helper at runtime for the passwordless managed keychain. The identity must be `Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)`. Keep keychain and 1Password routing in the ignored `.mac-release.env` or another approved private environment, never in Git.
3. Build all release archives locally; Darwin binaries are signed as `org.openclaw.gitcrawl`, while Linux and Windows builds remain ordinary cross-compiles:
2. Configure the shared `release-mac-app` helper at runtime for the passwordless managed keychain. The identity must be `Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)`. Supply `NOTARYTOOL_KEYCHAIN_PROFILE` through the approved private runtime environment; never commit its value or add it to GitHub Actions. Keep keychain and 1Password routing in the ignored `.mac-release.env` or another approved private environment, never in Git.
3. Build all release archives locally; each thin Darwin binary is signed as `org.openclaw.gitcrawl` with the hardened runtime and a trusted timestamp, submitted to Apple in an ephemeral ZIP, and required to pass the notarized code requirement before packaging. Linux and Windows builds remain ordinary cross-compiles:

```bash
make release-artifacts VERSION=vX.Y.Z
Expand All @@ -19,4 +19,6 @@ Official releases are assembled locally on an authorized maintainer Mac. GitHub
4. Create a draft GitHub release from the signed tag. Attach the archives and `checksums.txt` from `dist/`, then manually run the `Release Assets` workflow for that tag. Its ephemeral token has `contents: write` only because GitHub otherwise hides drafts; the token is scoped to read-only asset downloads and is removed before verification. Publish only after both macOS verification jobs pass.
5. After publication, verify the release notes and assets, then dispatch the `openclaw/homebrew-tap` formula update for `gitcrawl` and verify the installed binary.

Local `go build`, `make build`, tests, and GoReleaser snapshots never require release credentials. `scripts/package-release.sh` fails closed unless it runs from the exact trusted signed tag with the Foundation identity supplied by `release-mac-app codesign-run`.
Local `go build`, `make build`, tests, and credential-free GoReleaser snapshots never require release credentials. Official snapshots and releases set `GITCRAWL_REQUIRE_CODESIGN=1`; the signing hook then requires `NOTARYTOOL_KEYCHAIN_PROFILE`, waits for an accepted notarization response, and replaces the GoReleaser output only after online notarization verification succeeds. `scripts/verify-release.sh` independently checks every extracted Darwin binary for the Foundation designated requirement and the notarized requirement. Raw executables cannot carry a stapled ticket, so verification requires network access to Apple.

`scripts/package-release.sh` fails closed unless it runs from the exact trusted signed tag with the Foundation identity supplied by `release-mac-app codesign-run` and a runtime notary profile.
47 changes: 44 additions & 3 deletions scripts/codesign-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,57 @@ identity=${CODESIGN_IDENTITY:-}
echo "codesign: official macOS releases require $expected_authority" >&2
exit 1
}
[[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || {
echo "codesign: NOTARYTOOL_KEYCHAIN_PROFILE is required at runtime" >&2
exit 1
}

for tool in codesign ditto mktemp mv plutil xcrun; do
command -v "$tool" >/dev/null 2>&1 || {
echo "codesign: missing required command: $tool" >&2
exit 1
}
done

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

# Keep the GoReleaser output unchanged unless the complete signing and
# notarization contract succeeds.
cp -p "$binary" "$candidate"
codesign --force \
--options runtime \
--timestamp \
--identifier "$identifier" \
--sign "$identity" \
"$binary"
codesign --verify --strict -R="$requirement" --verbose=2 "$binary"
"$candidate"
codesign --verify --strict -R="$requirement" --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 -Fx "Authority=$expected_authority" <<<"$signature" >/dev/null

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 "codesign: notarization status is ${notary_status:-missing}, expected Accepted" >&2
exit 1
}
[[ "$notary_id" =~ ^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}$ ]] || {
echo "codesign: notarization response has an invalid submission id" >&2
exit 1
}

codesign --verify --strict --check-notarization -R=notarized "$candidate"
mv -f "$candidate" "$binary"
6 changes: 5 additions & 1 deletion scripts/package-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ fi
echo "official releases require $EXPECTED_AUTHORITY" >&2
exit 1
}
[[ -n "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]] || {
echo "official releases require NOTARYTOOL_KEYCHAIN_PROFILE at runtime" >&2
exit 1
}

for tool in codesign git go goreleaser lipo shasum tar; do
for tool in codesign ditto git go goreleaser lipo plutil shasum tar xcrun; do
command -v "$tool" >/dev/null || {
echo "missing required tool: $tool" >&2
exit 1
Expand Down
77 changes: 74 additions & 3 deletions scripts/test-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ EOF
cat > "$FAKE_BIN/codesign" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "${MOCK_CODESIGN_LOG:?}"
if [[ " $* " == *' --check-notarization '* && "${MOCK_NOTARIZATION_REJECT:-0}" == 1 ]]; then
exit 1
fi
case " $* " in
*' -dvvv '*)
{
Expand All @@ -36,6 +39,32 @@ case " $* " in
esac
EOF

cat > "$FAKE_BIN/ditto" <<'EOF'
#!/usr/bin/env bash
previous=
for arg in "$@"; do
source=$previous
previous=$arg
done
cp "$source" "$previous"
printf 'ditto %s\n' "$*" >> "${MOCK_CODESIGN_LOG:?}"
EOF

cat > "$FAKE_BIN/xcrun" <<'EOF'
#!/usr/bin/env bash
printf 'xcrun %s\n' "$*" >> "${MOCK_CODESIGN_LOG:?}"
printf '{"id":"12345678-1234-1234-1234-123456789abc","status":"%s"}\n' "${MOCK_NOTARY_STATUS:-Accepted}"
EOF

cat > "$FAKE_BIN/plutil" <<'EOF'
#!/usr/bin/env bash
case "${2:-}" in
status) printf '%s\n' "${MOCK_NOTARY_STATUS:-Accepted}" ;;
id) printf '%s\n' '12345678-1234-1234-1234-123456789abc' ;;
*) exit 1 ;;
esac
EOF

cat > "$FAKE_BIN/lipo" <<'EOF'
#!/usr/bin/env bash
case "${2:-}" in
Expand All @@ -47,6 +76,7 @@ EOF
chmod 0755 "$FAKE_BIN"/*
export PATH="$FAKE_BIN:$PATH"
export MOCK_CODESIGN_LOG="$WORK_DIR/codesign.log"
unset NOTARYTOOL_KEYCHAIN_PROFILE

test_binary="$WORK_DIR/gitcrawl"
cat > "$test_binary" <<'EOF'
Expand All @@ -62,15 +92,48 @@ if CODESIGN_IDENTITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)'
echo "package script accepted personal signing identity" >&2
exit 1
fi
if CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
"$ROOT/scripts/package-release.sh" v0.7.1 >/dev/null 2>&1; then
echo "package script accepted a missing notary profile" >&2
exit 1
fi
if GITCRAWL_REQUIRE_CODESIGN=1 \
CODESIGN_IDENTITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \
"$ROOT/scripts/codesign-macos.sh" "$test_binary" >/dev/null 2>&1; then
echo "personal signing identity was accepted" >&2
exit 1
fi
if GITCRAWL_REQUIRE_CODESIGN=1 \
CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
"$ROOT/scripts/codesign-macos.sh" "$test_binary" >/dev/null 2>&1; then
echo "official signing accepted a missing notary profile" >&2
exit 1
fi
GITCRAWL_REQUIRE_CODESIGN=1 \
CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \
"$ROOT/scripts/codesign-macos.sh" "$test_binary"
grep -F -- '--identifier org.openclaw.gitcrawl' "$MOCK_CODESIGN_LOG" >/dev/null
grep -F 'FWJYW4S8P8' "$MOCK_CODESIGN_LOG" >/dev/null
grep -F -- 'notarytool submit' "$MOCK_CODESIGN_LOG" >/dev/null
grep -F -- '--keychain-profile test-profile --no-s3-acceleration --wait --output-format json' "$MOCK_CODESIGN_LOG" >/dev/null
signed_hash=$(shasum -a 256 "$test_binary" | awk '{ print $1 }')
if GITCRAWL_REQUIRE_CODESIGN=1 \
CODESIGN_IDENTITY="$EXPECTED_AUTHORITY" \
NOTARYTOOL_KEYCHAIN_PROFILE=test-profile \
MOCK_NOTARY_STATUS=Invalid \
"$ROOT/scripts/codesign-macos.sh" "$test_binary" >/dev/null 2>&1; then
echo "invalid notarization status was accepted" >&2
exit 1
fi
[[ "$(shasum -a 256 "$test_binary" | awk '{ print $1 }')" == "$signed_hash" ]] || {
echo "failed notarization mutated the release binary" >&2
exit 1
}
if find "$WORK_DIR" -maxdepth 1 -name '.gitcrawl-notary.*' | grep -q .; then
echo "ephemeral notarization files were not removed" >&2
exit 1
fi

ARTIFACTS="$WORK_DIR/artifacts"
mkdir -p "$ARTIFACTS"
Expand All @@ -84,7 +147,14 @@ for arch in amd64 arm64; do
shasum -a 256 "$ARTIFACTS/$archive" | awk -v name="$archive" '{ print $1 " " name }' >> "$ARTIFACTS/checksums.txt"
done

: > "$MOCK_CODESIGN_LOG"
"$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS"
[[ "$(grep -F -c -- '--verify --strict --check-notarization -R=notarized' "$MOCK_CODESIGN_LOG")" == 2 ]]
if MOCK_NOTARIZATION_REJECT=1 \
"$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS" >/dev/null 2>&1; then
echo "release verifier accepted a missing notarization ticket" >&2
exit 1
fi
if MOCK_CODESIGN_AUTHORITY='Developer ID Application: Peter Steinberger (Y5PE65HELJ)' \
"$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS" >/dev/null 2>&1; then
echo "personal signature was accepted" >&2
Expand All @@ -104,9 +174,6 @@ if "$ROOT/scripts/verify-release.sh" v0.7.1 "$ARTIFACTS" >/dev/null 2>&1; then
exit 1
fi

grep -F -- '--identifier org.openclaw.gitcrawl' "$MOCK_CODESIGN_LOG" >/dev/null
grep -F 'FWJYW4S8P8' "$MOCK_CODESIGN_LOG" >/dev/null

release_workflow="$ROOT/.github/workflows/release-assets.yml"
grep -F 'contents: write' "$release_workflow" >/dev/null
grep -F "github.ref == format('refs/heads/{0}', github.event.repository.default_branch)" "$release_workflow" >/dev/null
Expand All @@ -121,6 +188,10 @@ grep -F 'persist-credentials: false' "$release_workflow" >/dev/null
grep -F 'tag_name == $tag and (.draft == ($draft == "true"))' "$release_workflow" >/dev/null
grep -F 'Accept: application/octet-stream' "$release_workflow" >/dev/null
grep -F 'unset GH_TOKEN GITHUB_TOKEN' "$release_workflow" >/dev/null
if grep -R -F 'NOTARYTOOL_KEYCHAIN_PROFILE' "$ROOT/.github/workflows" >/dev/null; then
echo "notary profile must not be configured in GitHub Actions" >&2
exit 1
fi
if grep -F 'gh release download' "$release_workflow" >/dev/null; then
echo "release workflow cannot resolve draft assets through gh release download" >&2
exit 1
Expand Down
1 change: 1 addition & 0 deletions scripts/verify-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ for goarch in "${architectures[@]}"; do
}

codesign --verify --strict -R="$REQUIREMENT" --verbose=2 "$binary"
codesign --verify --strict --check-notarization -R=notarized "$binary"
signature=$(codesign -dvvv "$binary" 2>&1)
grep -Fx "Identifier=$IDENTIFIER" <<<"$signature" >/dev/null
grep -Fx "TeamIdentifier=$EXPECTED_TEAM_ID" <<<"$signature" >/dev/null
Expand Down