chore(#68): rotate signing keys, key-rotation re-attestation mode, harden show-seed-b64 #71
Reference in New Issue
Block a user
Delete Branch "chore/68-key-rotation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the remaining follow-up from #68 finding 5 (the catalog/release keys were split but the content of the split — actually rotating the exposed key — hadn't landed yet), plus the CLI-prompt ambiguity from #62's
show-seed-b64that led to a private key being pasted into a chat.Task 1 — install the new keys
Both values below are public keys — safe to commit, nothing private is generated, requested, or handled by this PR.
bcc_core.py:CATALOG_PUBKEYS→ new catalog pubkey0s24PmkZcTT5yxNDdyTPHl5fyxArrHNPJKBjnXoQd8k=(signsdata/catalog.json, Console-only, offline)..github/workflows/ci.yml:EXPECTED_CATALOG_PUBKEY_B64→ same new catalog pubkey (the CI trust anchor from #68 finding 4).scripts/sign_checksums.py:RELEASE_PUBKEYS→ new release pubkey6BnPgJEHJFyVltFoLTCNadIsehjy00iiW8IRlC1TfhA=(signsSHA256SUMSonly, CI-resident).README.md: "Verifying your download" / "Signing keys" sections filled in with both pubkeys, explicit about which is which.The old catalog key
082NOwVB7uURkvfyS3+knJ+40Fk6C9unsF47+2uPKo4=is retired — it was exposed to CI.CATALOG_PUBKEYSdeliberately does not keep it: retaining a burned key in the trust list would defeat the point of rotating it.Task 2 — key-rotation re-attestation mode (Catalog Console)
Problem: post-rotation,
data/catalog.json.sig(signed with the OLD key) fails to verify under the NEWCATALOG_PUBKEYS. Content is unchanged, sodiff_catalogs(last_signed, current)is empty — andcan_sign()'s empty-diff guard (load-bearing, #68 finding 1) correctly refuses to sign an empty changeset. Without a rotation-aware path the Console could never re-sign and CI stays red forever.Fix — rotation is a full re-attestation, not a diff:
catalog_review.py:ReviewSession/start_reviewgainreattest: bool = False. When set,changesis built viadiff_catalogs(None, new_catalog)— every entry presented as if newly added, requiring a fresh acknowledgement — instead of diffing againstold_catalog.can_sign()itself is unchanged: it still refuses a genuinely-empty changeset and still enforces the TOCTOU blob-SHA pin and the blocking-risk check. Reattest sessions simply never produce an empty changeset (unless the catalog itself is empty), so gate 0 passes for the right reason.catalog_console.py: newcatalog_signature_valid_at(repo_dir, commit, raw)callsbcc_core.verify_catalog_signaturedirectly (not reimplemented) to detect whether the committed.sigverifies under the currentCATALOG_PUBKEYS.ReviewWindow._on_load'ssource=mainpath uses this to decidereattest=True/Falseand shows a loud, explicit red banner ("KEY ROTATION IN PROGRESS…") whenever reattest mode is entered — never silently. Status text and Sign-button gating flow through the samecan_sign()/all_entries_acknowledged()path as normal review; no separate code path to drift.tests/test_catalog_review.py: 6 new tests — reattest mode yields one change-entry per server and gates on all being acknowledged before permitting; normal mode with unchanged content still refuses (rotation is not a general bypass of the empty-diff guard); reattest mode still enforces the blocking-risk check; reattest mode still enforces the TOCTOU pin;reattestdefaults toFalse.Task 3 — harden
show-seed-b64The maintainer ran
show-seed-b64 --release, saw the ambiguous prompt "Release signing key passphrase:", and pasted the printed private seed into a chat believing it was public.cmd_show_seed_b64: passphrase prompt is now explicit ("Passphrase for the release signing key (the one YOU chose when generating it):"). A loud 3-line banner prints to stderr immediately before the seed:| pbcopy/ piping into a CI secret field still works cleanly.cmd_keygen: labels for both key kinds now say PUBLIC/PRIVATE explicitly and state the safety property inline (safe to commit vs. never commit), so the printed output can't be mistaken for the other key's.Verification
ruff check ./ruff format --check .: clean.pytest: full suite green — 360 passed, 1 skipped.can_sign()'s empty-diff guard (gate 0) →test_can_sign_false_on_empty_changesetandtest_normal_mode_with_unchanged_content_still_refuses_empty_difffailed.test_can_sign_false_with_outstanding_blocking_risk_even_if_acknowledgedandtest_reattest_mode_still_enforces_blocking_risk_checkfailed.test_can_sign_refuses_on_blob_mismatch_even_if_acknowledged,test_sign_precondition_refuses_when_loaded_ref_blob_moved, andtest_reattest_mode_still_enforces_toctou_pinfailed.test_no_acknowledge_all_shortcut_and_gate_is_real,test_can_sign_false_when_not_all_acknowledged, andtest_reattest_mode_can_sign_refuses_until_all_acknowledged_then_permitsfailed.reattestbranch inReviewSession.__post_init__entirely → all 4 reattest-specific tests failed.Maintainer follow-up: re-signing the catalog under the new key
python catalog_console.py keygen. Confirm the printed public key matches0s24PmkZcTT5yxNDdyTPHl5fyxArrHNPJKBjnXoQd8k=(this PR already assumes that key; if it doesn't match,bcc_core.CATALOG_PUBKEYSneeds a follow-up correction).main.python catalog_console.py gui --repo <checkout>, select main (current tip) as the source, click Load selected source. The Console will detect thatdata/catalog.json.signo longer verifies under the new key and enter re-attestation mode (red banner, all 19 entries shown as needing acknowledgement).data/catalog.jsonand a freshly-produceddata/catalog.json.sigin one commit.catalog-signatureCI job goes green on the resulting commit.Do NOT / not touched
data/catalog.jsonitself is untouched — re-signing is the maintainer's job via the Console, per step above.bcc.pyuntouched (open PR #67).can_sign()'s empty-diff, acknowledge, blocking-risk, and TOCTOU guards are unchanged in strictness — only a new, explicitly-flagged input mode was added upstream of them.keysstatus command, and complete rotation without a red main (#62, #68)View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.