{"record":{"id":"ff2c52e67693cd41","repo":"JuliusBrussee/caveman","slug":"bundle-public-key-does-not-match-the-published-p","errorCode":null,"errorMessage":"bundle public key does not match the published --pubkey","messagePattern":"bundle public key does not match the published --pubkey","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":17429,"sourceCode":"  if (bundle.schema !== RECEIPT_BUNDLE_V1 && bundle.schema !== RECEIPT_BUNDLE_V2) throw new Error(`unsupported bundle schema ${String(bundle.schema)}`);\n  if (bundle.verification_coverage !== undefined && bundle.verification_coverage !== INCLUDED_RECEIPTS_ONLY) throw new Error(`unsupported unsigned verification coverage ${String(bundle.verification_coverage)}`);\n  if (bundle.completeness_attested === true) throw new Error(\"bundle completeness cannot be attested by unsigned export metadata\");\n  const current = decodeReceiptKey(bundle.public_key, \"public_key\");\n  if (bundle.public_keys !== undefined && !Array.isArray(bundle.public_keys)) throw new Error(\"public_keys must be an array\");\n  if (bundle.schema === RECEIPT_BUNDLE_V2 && (!Array.isArray(bundle.public_keys) || bundle.public_keys.length === 0)) throw new Error(\"v2 bundle requires public_keys\");\n  const keys = decodeUniqueKeyring(bundle.public_keys ?? [], \"public_keys\");\n  const currentInRing = keys.get(current.info.key_id);\n  if (currentInRing && !currentInRing.raw.equals(current.raw)) throw new Error(`public_key conflicts with public_keys entry ${current.info.key_id}`);\n  if (bundle.schema === RECEIPT_BUNDLE_V2 && !currentInRing) throw new Error(\"v2 public_keys must include public_key\");\n  if (!currentInRing) keys.set(current.info.key_id, current);\n  return { current, keys };\n}\n\nasync function pinnedReceiptKeys(file: string, current: DecodedReceiptKey): Promise<{ keys: Map<string, DecodedReceiptKey>; trust: string }> {\n  const source = (await readFile(file, \"utf8\")).trim();\n  if (!source.startsWith(\"{\")) {\n    const pinned = decodeReceiptKey({ ...current.info, key: source }, \"--pubkey\");\n    if (!pinned.raw.equals(current.raw)) throw new Error(\"bundle public key does not match the published --pubkey\");\n    return { keys: new Map([[current.info.key_id, pinned]]), trust: \"pinned_public_key\" };\n  }\n  let parsed: { public_key?: ReceiptPublicKey; public_keys?: ReceiptPublicKey[] };\n  try { parsed = JSON.parse(source); } catch { throw new Error(\"--pubkey JSON is malformed\"); }\n  const infos = Array.isArray(parsed.public_keys) ? parsed.public_keys : parsed.public_key ? [parsed.public_key] : [];\n  if (infos.length === 0) throw new Error(\"--pubkey JSON must contain public_key or public_keys\");\n  const keys = decodeUniqueKeyring(infos, \"--pubkey public_keys\");\n  const pinnedCurrent = keys.get(current.info.key_id);\n  if (!pinnedCurrent || !pinnedCurrent.raw.equals(current.raw)) throw new Error(\"trusted --pubkey keyring does not contain the bundle public key\");\n  return { keys, trust: \"pinned_keyring\" };\n}\n\n// receiptsVerify validates a signed receipt bundle offline (no network). A raw\n// --pubkey pins the current key; JSON may independently pin a full rotation\n// keyring. Without either, embedded keys prove self-consistency, not publisher\n// authenticity. Exits non-zero on any included content, signature, or\n// scope-chain break. Tail/scope omission needs separately trusted head manifest;\n// bundle output states completeness is not attested.","sourceCodeStart":17411,"sourceCodeEnd":17447,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L17411-L17447","documentation":"When --pubkey points at a raw (non-JSON) file, its trimmed content is treated as the base64 Ed25519 public key and must equal the bundle's public_key byte-for-byte. A mismatch means the bundle was signed by a different key than the one you pinned, so authenticity cannot be established.","triggerScenarios":"`caveman receipts verify bundle.json --pubkey key.b64` where key.b64 decodes to a different 32-byte key than bundle.public_key — e.g. the publisher rotated keys after key.b64 was captured.","commonSituations":"A stale pinned key checked into the repo; key rotation without updating the published key file; verifying a bundle from publisher B with publisher A's key file.","solutions":["Fetch the publisher's current published key and update the --pubkey file","If the rotation is legitimate, have the publisher publish a JSON keyring (old + new keys) and pass that file instead, so rotation verifies without repinning","Confirm you are verifying the intended artifact from the intended publisher before trusting any key update"],"exampleFix":"# before: pinned file holds the pre-rotation key\ncaveman receipts verify bundle.json --pubkey old-key.b64  # -> mismatch\n\n# after: pin the rotated keyring (JSON, contains k1 and k2)\ncaveman receipts verify bundle.json --pubkey keyring.json","handlingStrategy":"validation","validationCode":"const pinned = (await readFile(pubkeyFile, \"utf8\")).trim();\nconst bundleKey = Buffer.from(bundle.public_key.key, \"base64\").toString(\"base64\");\nif (!pubkeyFile.trim().startsWith(\"{\") && pinned !== bundleKey) {\n  throw new Error(\"pinned raw key does not match bundle public_key — refresh the published key or use a keyring file\");\n}","typeGuard":"function rawKeyMatches(pinnedB64: string, bundleKeyB64: string): boolean {\n  return Buffer.from(pinnedB64, \"base64\").toString(\"base64\") === bundleKeyB64;\n}","tryCatchPattern":"try { execSync(`caveman receipts verify ${bundle} --pubkey ${pubkey}`); }\ncatch (e) {\n  if (/does not match the published --pubkey/.test(String((e as Error).message))) {\n    fail(\"key rotation: fetch the updated key or pin a JSON keyring with old+new keys\");\n  }\n  throw e;\n}","preventionTips":["Pin a rotation-aware JSON keyring (old + new keys) instead of a single raw key","Automate refreshing pinned keys whenever the publisher announces rotation","Fail CI on key mismatch rather than silently falling back to embedded keys"],"tags":["receipts","public-key","pinning","key-rotation","verification"],"backgroundTag":"public-key-mismatch","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}