ci: gate the catalog signature, smoke-test the release key (#61, #63)
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 10s
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.12 / windows-latest) (push) Successful in 23s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
CI / Catalog signature (push) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 10s
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.12 / windows-latest) (push) Successful in 23s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
CI / Catalog signature (push) Successful in 6s
Two gaps closed now that a real key exists. 1. CI 'Catalog signature' job (the #61 gate): every push/PR verifies data/catalog.json against data/catalog.json.sig using the public key in bcc_core, and runs validate_catalog. The threat model here is not an outsider pushing to the repo -- it is merging a friendly-looking PR without really reading it. A contributor can change catalog.json but cannot produce a matching signature, so a blindly-merged PR now lands as a red build within a minute instead of quietly riding into the next release. Public-key only; no secret involved. 2. release.yml 'Signing key smoke test' (workflow_dispatch only): the Publish job is gated on a tag, so a manual run never exercised signing -- a wrong or missing RELEASE_SIGNING_KEY would first surface during a real release. This signs a throwaway manifest with the secret and verifies it against the public key compiled into bcc_core, proving the two halves of the keypair actually match. Publishes nothing.
This commit is contained in:
@@ -68,3 +68,61 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: python -m pytest -v
|
||||
|
||||
# ── Catalog signature gate (#61) ─────────────────────────────────────────
|
||||
#
|
||||
# data/catalog.json is a list of command+args entries that BCC writes into
|
||||
# the user's Claude config, which Claude then EXECUTES. The catalog is only
|
||||
# trusted if it carries a valid Ed25519 signature from the maintainer key.
|
||||
#
|
||||
# The threat this gate exists for is NOT an outsider pushing to the repo —
|
||||
# it is the maintainer merging a friendly-looking PR without really reading
|
||||
# it. A contributor can change catalog.json but cannot produce a matching
|
||||
# signature, so a blindly-merged PR lands here as a RED BUILD within a
|
||||
# minute, instead of quietly riding into the next release.
|
||||
#
|
||||
# Public-key verification only. No secret is used or needed.
|
||||
catalog-signature:
|
||||
name: Catalog signature
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install cryptography
|
||||
|
||||
- name: Verify data/catalog.json.sig
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import pathlib, sys
|
||||
import bcc_core as c
|
||||
|
||||
raw = pathlib.Path("data/catalog.json").read_bytes()
|
||||
sig_path = pathlib.Path("data/catalog.json.sig")
|
||||
|
||||
if not sig_path.exists():
|
||||
sys.exit("FAIL: data/catalog.json.sig is missing. The catalog must be "
|
||||
"signed via the Catalog Console (#62) before it can land.")
|
||||
|
||||
if b"\x00" * 32 in c.CATALOG_PUBKEYS:
|
||||
sys.exit("FAIL: CATALOG_PUBKEYS still holds the placeholder key.")
|
||||
|
||||
if not c.verify_catalog_signature(raw, sig_path.read_bytes(), c.CATALOG_PUBKEYS):
|
||||
sys.exit(
|
||||
"FAIL: data/catalog.json does NOT match its signature.\n"
|
||||
"\n"
|
||||
"The catalog changed without being re-signed. Either someone edited\n"
|
||||
"it directly (a PR you merged?), or a signing pass was forgotten.\n"
|
||||
"Re-review and re-sign with the Catalog Console — do not bypass this."
|
||||
)
|
||||
|
||||
problems = c.validate_catalog(c.load_catalog(raw))
|
||||
if problems:
|
||||
sys.exit("FAIL: catalog failed validation:\n " + "\n ".join(problems))
|
||||
|
||||
print("OK: catalog signature verifies and the catalog validates clean.")
|
||||
PY
|
||||
|
||||
Reference in New Issue
Block a user