{"record":{"id":"9673f9adb839b9fd","repo":"JuliusBrussee/caveman","slug":"public-key-conflicts-with-public-keys-entry-curr","errorCode":null,"errorMessage":"public_key conflicts with public_keys entry ${current.info.key_id}","messagePattern":"public_key conflicts with public_keys entry (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":17419,"sourceCode":"  const keys = new Map<string, DecodedReceiptKey>();\n  for (const [index, info] of infos.entries()) {\n    const decoded = decodeReceiptKey(info, `${label}[${index}]`);\n    if (keys.has(decoded.info.key_id)) throw new Error(`${label} contains duplicate key_id ${decoded.info.key_id}`);\n    keys.set(decoded.info.key_id, decoded);\n  }\n  return keys;\n}\n\nfunction embeddedReceiptKeys(bundle: ReceiptBundle): { current: DecodedReceiptKey; keys: Map<string, DecodedReceiptKey> } {\n  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);","sourceCodeStart":17401,"sourceCodeEnd":17437,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L17401-L17437","documentation":"If the top-level public_key shares a key_id with an entry in public_keys, the two raw 32-byte Ed25519 keys must be identical. Same id with different bytes means a key_id was reused for different key material, which breaks rotation accounting and signature attribution.","triggerScenarios":"A rotation minted a new key but kept the old key_id, so the bundle carries key_id X in public_key with new bytes and key_id X in public_keys with old bytes (or vice versa).","commonSituations":"Rotation tooling generates keys but not ids; restoring an old key under its previous id; partial manual rotation of one field without the other.","solutions":["Give the new key a fresh key_id and re-export the bundle with consistent entries","Audit the rotation pipeline so every new key gets a new id","If the top-level public_key is stale, re-export so it matches the keyring entry"],"exampleFix":"// before\n\"public_key\": { \"key_id\": \"k1\", \"key\": \"NEWBYTES...\" }\n\"public_keys\": [ { \"key_id\": \"k1\", \"key\": \"OLDBYTES...\" } ]\n\n// after\n\"public_key\": { \"key_id\": \"k2\", \"key\": \"NEWBYTES...\" }\n\"public_keys\": [ { \"key_id\": \"k1\", \"key\": \"OLDBYTES...\" }, { \"key_id\": \"k2\", \"key\": \"NEWBYTES...\" } ]","handlingStrategy":"validation","validationCode":"const ringKey = (bundle.public_keys ?? []).find((k) => k.key_id === bundle.public_key.key_id);\nif (ringKey && Buffer.from(ringKey.key, \"base64\").toString(\"base64\") !== bundle.public_key.key) {\n  throw new Error(`key_id ${bundle.public_key.key_id} maps to two different keys`);\n}","typeGuard":"function keyIdIsUnambiguous(b: { public_key: { key_id: string; key: string }; public_keys?: { key_id: string; key: string }[] }): boolean {\n  const hit = (b.public_keys ?? []).find((k) => k.key_id === b.public_key.key_id);\n  return !hit || hit.key === b.public_key.key;\n}","tryCatchPattern":"try { execSync(`caveman receipts verify ${bundle}`); }\ncatch (e) {\n  if (/conflicts with public_keys entry/.test(String((e as Error).message))) fail(\"key_id reuse detected — re-export with fresh ids\");\n  throw e;\n}","preventionTips":["Treat key_id as immutable per key: one id, one key, forever","Store (key_id -> key) pairs in one source of truth instead of two fields that can drift","Assert public_key matches its keyring entry in producer tests"],"tags":["receipts","keyring","ed25519","key-rotation","conflict"],"backgroundTag":"key-id-collision","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}