{"record":{"id":"dd95b544db9ec58d","repo":"JuliusBrussee/caveman","slug":"label-must-be-a-canonical-base64-ed25519-public","errorCode":null,"errorMessage":"${label} must be a canonical base64 Ed25519 public key","messagePattern":"(.+?) must be a canonical base64 Ed25519 public key","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":17396,"sourceCode":"    if (!decoded) return `seq ${r.seq}: no trusted public key for key_id ${String(r.signature?.key_id)}`;\n    const err = verifyReceipt(r, decoded.key, decoded.info.key_id);\n    if (err) return err;\n    if (prev) {\n      if (r.seq !== prev.seq + 1) return `seq ${r.seq}: not strictly after ${prev.seq}`;\n      if (r.prev_receipt_hash !== prev.receipt_hash) return `seq ${r.seq}: prev_receipt_hash does not link to seq ${prev.seq}`;\n      if (r.day <= prev.day) return `seq ${r.seq}: day ${r.day} does not follow ${prev.day}`;\n    }\n    prev = r;\n  }\n  return null;\n}\n\nfunction decodeReceiptKey(info: ReceiptPublicKey, label: string): DecodedReceiptKey {\n  if (!info || typeof info.key_id !== \"string\" || !info.key_id.trim()) throw new Error(`${label} key_id is required`);\n  if (info.alg !== \"Ed25519\") throw new Error(`${label} has unsupported algorithm ${String(info.alg)}`);\n  if (typeof info.key !== \"string\" || !info.key.trim()) throw new Error(`${label} key is required`);\n  const raw = Buffer.from(info.key, \"base64\");\n  if (raw.length !== 32 || raw.toString(\"base64\") !== info.key) throw new Error(`${label} must be a canonical base64 Ed25519 public key`);\n  return { info, raw, key: ed25519PublicKey(raw) };\n}\n\nfunction decodeUniqueKeyring(infos: ReceiptPublicKey[], label: string): Map<string, DecodedReceiptKey> {\n  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\");","sourceCodeStart":17378,"sourceCodeEnd":17414,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L17378-L17414","documentation":"The strictest key-encoding check in decodeReceiptKey: the key string must be canonical standard base64 that decodes to exactly 32 bytes AND re-encodes to the identical string. One guard rejects hex, URL-safe base64, non-canonical padding, embedded whitespace, and wrong-length keys.","triggerScenarios":"Hex-encoded keys (64 hex characters); base64url using '-'/'_' instead of '+'/'/'; base64 with unusual padding or line breaks; base64 of a value that is not 32 bytes; double-encoded base64.","commonSituations":"Keys copied from Go/Rust tooling that prints hex or base64url; keys passed through systems that re-encode them; JSON serialization escaping or trimming characters.","solutions":["Encode the raw 32 bytes with standard base64: Buffer.from(raw).toString('base64')","Convert base64url: replace '-' with '+' and '_' with '/', then re-encode canonically","Convert hex: Buffer.from(hex, 'hex').toString('base64')","Verify the round-trip: decode then encode must reproduce the string exactly"],"exampleFix":"// before\n{ \"key_id\": \"k1\", \"alg\": \"Ed25519\", \"key\": \"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a\" }\n// after\n{ \"key_id\": \"k1\", \"alg\": \"Ed25519\", \"key\": \"11qYAYKxCrfVS/_TyWQHOg7hcvPapiMlrwIaaPcHURo=\" }","handlingStrategy":"validation","validationCode":"const raw = Buffer.from(info.key, 'base64');\nconst canonical = raw.length === 32 && raw.toString('base64') === info.key;\nif (!canonical) throw new Error('key must be canonical standard base64 of the 32 raw bytes');","typeGuard":"const isCanonicalBase64Key = (s: unknown): s is string =>\n  typeof s === 'string' &&\n  /^[A-Za-z0-9+/]{43}=$/.test(s) &&\n  Buffer.from(s, 'base64').toString('base64') === s;","tryCatchPattern":null,"preventionTips":["Produce keys only via Buffer.from(raw32).toString('base64')","Never base64url-encode receipt keys","Add a fixture test that the published keyring parses cleanly"],"tags":["crypto","keyring","base64","encoding","receipts"],"backgroundTag":"invalid-base64-key","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"}