{"record":{"id":"f2d98dda638010d8","repo":"JuliusBrussee/caveman","slug":"binary-signing-private-key-does-not-match-committe","errorCode":null,"errorMessage":"binary signing private key does not match committed public key","messagePattern":"binary signing private key does not match committed public key","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"scripts/sign-binary-checksums.mjs","lineNumber":49,"sourceCode":"    );\n}\n\nfunction normalizePublicKey(value) {\n  return createPublicKey(value).export({ type: \"spki\", format: \"pem\" }).toString();\n}\n\nif (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {\n  try {\n    const [checksumsPath, outputPath, publicKeyPath] = process.argv.slice(2);\n    const privateKeyPEM = process.env.CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM;\n    if (!checksumsPath || !outputPath || !publicKeyPath) {\n      throw new Error(\"usage: sign-binary-checksums.mjs <checksums.txt> <output.keysig> <public-key.pem>\");\n    }\n    if (!privateKeyPEM) throw new Error(\"CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM is required\");\n    const checksums = readFileSync(checksumsPath);\n    const publicKeyPEM = readFileSync(publicKeyPath, \"utf8\");\n    if (normalizePublicKey(privateKeyPEM) !== normalizePublicKey(publicKeyPEM)) {\n      throw new Error(\"binary signing private key does not match committed public key\");\n    }\n    const bundle = checksumSignatureBundle(checksums, privateKeyPEM);\n    if (!verifyChecksumSignatureBundle(checksums, bundle, publicKeyPEM)) {\n      throw new Error(\"generated checksum signature failed local verification\");\n    }\n    writeFileSync(outputPath, `${JSON.stringify(bundle)}\\n`, { mode: 0o600 });\n  } catch (error) {\n    process.stderr.write(`${error.message}\\n`);\n    process.exit(1);\n  }\n}\n","sourceCodeStart":31,"sourceCodeEnd":61,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/scripts/sign-binary-checksums.mjs#L31-L61","documentation":"Thrown by scripts/sign-binary-checksums.mjs when the private key from CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM derives a different public key than the committed public-key.pem passed on the command line. Both are normalized to canonical SPKI PEM before comparing, so formatting differences cannot cause a false mismatch — a hit means the key pair genuinely differs. This prevents signing binaries with a key the published verification flow does not trust.","triggerScenarios":"Using a newly generated or wrong private key while the repo still carries the old committed public key; rotating the key pair but updating only one side; pointing the third argument at a stale copy of the public key.","commonSituations":"Post-rotation release where the new private key is in the environment but the old public-key.pem is still committed; multiple signing keys in an org and the wrong one configured; CI secret updated ahead of the repo.","solutions":["Export the private key that corresponds to the committed public-key.pem and re-run.","If the rotation is intentional, generate the new key pair, commit the new public-key.pem, and update the CI secret to match, then re-run.","Verify the pair locally: `diff <(openssl pkey -in priv.pem -pubout) keys/binary-public.pem`."],"exampleFix":"# before: env holds rotated private key, repo holds old public key\nCAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM=\"$(cat new-key.pem)\" node scripts/sign-binary-checksums.mjs ...\n# after: commit the matching public key first\ngit add keys/binary-public.pem && git commit -m 'rotate binary signing key'\nCAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM=\"$(cat new-key.pem)\" node scripts/sign-binary-checksums.mjs dist/checksums.txt dist/checksums.keysig keys/binary-public.pem","handlingStrategy":"validation","validationCode":"# Verify the key pair matches BEFORE running the signer.\nif [ \"$(openssl pkey -in \"$PRIV_PEM\" -pubout | openssl pkey -pubin -outform DER | sha256sum)\" != \\\n     \"$(openssl pkey -pubin -in \"$PUB_PEM\" -outform DER | sha256sum)\" ]; then\n  echo \"signing key does not match committed public key\" >&2; exit 1\nfi","typeGuard":null,"tryCatchPattern":"try { signRelease(); } catch (e) { if (/does not match committed public key/.test(e.message)) haltRotation(); else throw e; }","preventionTips":["Rotate keys as an atomic change: new private key secret + committed public key in the same release.","Keep a local diff check between openssl-derived and committed public key in the release runbook."],"tags":["signing","key-management","release","security"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}