diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 6f822cf..614c0f4 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -25,6 +25,9 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Verify signing key consistency (repo + docs) + run: ./scripts/ci/verify_signing_key_consistency.sh + - name: Auto-discover skills from releases env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -274,6 +277,18 @@ jobs: input_file: public/checksums.json signature_file: public/checksums.sig + - name: Verify generated public signing key matches canonical key + run: | + set -euo pipefail + CANONICAL_FPR=$(openssl pkey -pubin -in clawsec-signing-public.pem -outform DER | sha256sum | awk '{print $1}') + GENERATED_FPR=$(openssl pkey -pubin -in public/signing-public.pem -outform DER | sha256sum | awk '{print $1}') + echo "Canonical key fingerprint: $CANONICAL_FPR" + echo "Generated key fingerprint: $GENERATED_FPR" + if [ "$CANONICAL_FPR" != "$GENERATED_FPR" ]; then + echo "::error::public/signing-public.pem fingerprint mismatch vs clawsec-signing-public.pem" + exit 1 + fi + - name: Copy public key to advisory directory run: | # Clients expect the public key at advisories/feed-signing-public.pem diff --git a/.github/workflows/skill-release.yml b/.github/workflows/skill-release.yml index 9644a0a..1befd4c 100644 --- a/.github/workflows/skill-release.yml +++ b/.github/workflows/skill-release.yml @@ -36,6 +36,9 @@ jobs: with: fetch-depth: 0 + - name: Verify signing key consistency (repo + docs) + run: ./scripts/ci/verify_signing_key_consistency.sh + - name: Validate version parity for bumped skills env: BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -526,6 +529,9 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Verify signing key consistency (repo + docs) + run: ./scripts/ci/verify_signing_key_consistency.sh + - name: Validate skill exists run: | SKILL_PATH="${{ steps.parse.outputs.skill_path }}" @@ -782,6 +788,18 @@ jobs: signature_file: release-assets/checksums.sig public_key_output: release-assets/signing-public.pem + - name: Verify generated release signing key matches canonical key + run: | + set -euo pipefail + CANONICAL_FPR=$(openssl pkey -pubin -in clawsec-signing-public.pem -outform DER | sha256sum | awk '{print $1}') + GENERATED_FPR=$(openssl pkey -pubin -in release-assets/signing-public.pem -outform DER | sha256sum | awk '{print $1}') + echo "Canonical key fingerprint: $CANONICAL_FPR" + echo "Generated key fingerprint: $GENERATED_FPR" + if [ "$CANONICAL_FPR" != "$GENERATED_FPR" ]; then + echo "::error::release-assets/signing-public.pem fingerprint mismatch vs clawsec-signing-public.pem" + exit 1 + fi + - name: Show signed release assets run: | echo "Signed and verified release-assets/checksums.json" diff --git a/scripts/ci/verify_signing_key_consistency.sh b/scripts/ci/verify_signing_key_consistency.sh new file mode 100755 index 0000000..541bf66 --- /dev/null +++ b/scripts/ci/verify_signing_key_consistency.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +SKILL_MD="skills/clawsec-suite/SKILL.md" +CANONICAL_KEYS=( + "clawsec-signing-public.pem" + "advisories/feed-signing-public.pem" + "skills/clawsec-suite/advisories/feed-signing-public.pem" +) + +fingerprint_for_pem() { + local pem_file="$1" + openssl pkey -pubin -in "$pem_file" -outform DER | shasum -a 256 | awk '{print $1}' +} + +if [[ ! -f "$SKILL_MD" ]]; then + echo "ERROR: missing $SKILL_MD" >&2 + exit 1 +fi + +DOC_EXPECTED_FPR="$(awk -F'"' '/RELEASE_PUBKEY_SHA256=/{print $2; exit}' "$SKILL_MD")" +if [[ -z "$DOC_EXPECTED_FPR" ]]; then + echo "ERROR: could not parse RELEASE_PUBKEY_SHA256 from $SKILL_MD" >&2 + exit 1 +fi + +TMP_DOC_KEY="$(mktemp)" +trap 'rm -f "$TMP_DOC_KEY"' EXIT +awk ' + /-----BEGIN PUBLIC KEY-----/ {in_key=1} + in_key {print} + /-----END PUBLIC KEY-----/ {exit} +' "$SKILL_MD" > "$TMP_DOC_KEY" + +if ! grep -q "BEGIN PUBLIC KEY" "$TMP_DOC_KEY"; then + echo "ERROR: could not extract inline public key from $SKILL_MD" >&2 + exit 1 +fi + +DOC_INLINE_FPR="$(fingerprint_for_pem "$TMP_DOC_KEY")" + +if [[ "$DOC_INLINE_FPR" != "$DOC_EXPECTED_FPR" ]]; then + echo "ERROR: SKILL.md mismatch: inline key fingerprint ($DOC_INLINE_FPR) != RELEASE_PUBKEY_SHA256 ($DOC_EXPECTED_FPR)" >&2 + exit 1 +fi + +echo "SKILL.md inline key fingerprint matches RELEASE_PUBKEY_SHA256: $DOC_EXPECTED_FPR" + +CANONICAL_FPR="" +for key_file in "${CANONICAL_KEYS[@]}"; do + if [[ ! -f "$key_file" ]]; then + echo "ERROR: missing canonical key file: $key_file" >&2 + exit 1 + fi + fpr="$(fingerprint_for_pem "$key_file")" + echo "$key_file -> $fpr" + if [[ -z "$CANONICAL_FPR" ]]; then + CANONICAL_FPR="$fpr" + elif [[ "$fpr" != "$CANONICAL_FPR" ]]; then + echo "ERROR: key fingerprint mismatch among canonical pem files" >&2 + exit 1 + fi +done + +if [[ "$CANONICAL_FPR" != "$DOC_EXPECTED_FPR" ]]; then + echo "ERROR: canonical pem fingerprint ($CANONICAL_FPR) != SKILL.md RELEASE_PUBKEY_SHA256 ($DOC_EXPECTED_FPR)" >&2 + exit 1 +fi + +echo "All signing key references are consistent: $CANONICAL_FPR"