security: rotate helpers-signing key (RUFLO_HELPERS_PUBKEY v2) (#2673)

The previous Ed25519 private key was accidentally exposed in a Claude Code
session transcript today while attempting the v3.29.0 publish flow — the
gcloud secret fetch was invoked in a way that printed the PEM to the tool
log, which is captured in the session's persistent transcript.

Mitigation (already done at rotation time):
- New Ed25519 keypair generated locally.
- New private uploaded as GCP Secret Manager version 2 of
  `ruflo-helpers-signing-key` in the `ruv-dev` project.
- Old GCP secret version 1 DESTROYED (not just disabled) — cannot be
  re-enabled from state or previous access tokens.
- New public key baked into RUFLO_HELPERS_PUBKEY here.

Compatibility (atomic per-version — no transition dance needed):
- Users on ruflo <= 3.28.0 keep the old baked pubkey and continue to verify
  their locally-shipped manifest (also signed with the old key) successfully.
  Their helpers stay valid.
- Users upgrading to ruflo >= 3.29.0 receive both the new baked pubkey AND
  the manifest signed with the new key in the same package — helper-refresh
  verifies cleanly on first run of the new CLI.

Follow-up urgency: upgrade to v3.29.0 as soon as possible so any tampered
tarball signed with the leaked v1 key can't survive against a fresh
verifier. Old CLIs remain "trust v1 pubkey" until they upgrade — an
attacker with the leaked key could still forge a manifest that verifies
against those old installs. This is the last-mile risk of any rotation.

Also updates scripts/verify-helpers.mjs, which duplicates the pubkey inline
(prepublishOnly gate). Keep the two copies in sync on future rotations —
worth a follow-up to make helper-signing.ts the single source of truth.
This commit is contained in:
rUv
2026-07-14 12:51:38 -04:00
committed by GitHub
parent c89a98a4fd
commit 0052b1b06d
2 changed files with 12 additions and 2 deletions
@@ -24,8 +24,11 @@ const HELPERS_DIR = resolve(process.argv[2] || join(PKG_ROOT, '.claude', 'helper
// Keep in sync with sign-helpers.mjs:CRITICAL and src/init/helper-refresh.ts:CRITICAL_HELPERS.
const CRITICAL = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs'];
// KEEP IN SYNC with src/init/helper-signing.ts:RUFLO_HELPERS_PUBKEY.
// Rotated 2026-07-14 (v3.29.0) after the previous private key was exposed in
// a Claude Code session transcript. Old GCP secret v1 destroyed.
const RUFLO_HELPERS_PUBKEY = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAhnFv74/CRcGWd0hL8zjyZ+52bIJ9SfcSgOutuKgo0Vg=
MCowBQYDK2VwAyEAyLl9cG+V/C+ffKWaSwvOsHdXSWmB5e3x1z9NUNvq6Ys=
-----END PUBLIC KEY-----`;
function die(msg) { console.error(`[verify-helpers] ${msg}`); process.exit(1); }
@@ -19,9 +19,16 @@ import { createHash, verify as edVerify } from 'crypto';
* Ruflo helper-signing PUBLIC key (safe to commit). The matching private key is
* held out-of-repo and provided to scripts/sign-helpers.mjs at publish time via
* $RUFLO_HELPERS_SIGNING_KEY. Rotating the key = replace this constant + re-sign.
*
* ROTATED 2026-07-14 (v3.29.0): the previous key was accidentally exposed in a
* Claude Code session transcript. Old GCP secret version 1 was destroyed (not
* disabled) so it cannot be re-enabled; new v2 generated here. Users on old
* ruflo versions keep the old pubkey and verify old manifests successfully;
* upgrading to v3.29.0+ atomically picks up this new pubkey along with the
* new-key-signed manifest.
*/
export const RUFLO_HELPERS_PUBKEY = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAhnFv74/CRcGWd0hL8zjyZ+52bIJ9SfcSgOutuKgo0Vg=
MCowBQYDK2VwAyEAyLl9cG+V/C+ffKWaSwvOsHdXSWmB5e3x1z9NUNvq6Ys=
-----END PUBLIC KEY-----`;
export const HELPERS_MANIFEST_FILE = 'helpers.manifest.json';