{"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":17852,"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":17834,"sourceCodeEnd":17870,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L17834-L17870","documentation":"Thrown by decodeReceiptKey when the key field is base64-decodable but is not a canonical base64 encoding of exactly 32 bytes — i.e. either the decoded length differs from 32, or re-encoding the decoded bytes does not reproduce the original string (non-canonical padding, extra whitespace, or trailing bits set). This canonicality check prevents ambiguous key encodings.","triggerScenarios":"Verifying a bundle with a key string containing line breaks or whitespace, base64 with missing/incorrect padding, base64url encoding (- and _ instead of + and /), or key material longer/shorter than 32 bytes when decoded.","commonSituations":"Copy/paste wrapping a key across lines; a PEM body pasted including headers; base64url from a JWT-style tool; a hex string mistakenly base64-encoded.","solutions":["Re-encode the 32-byte raw key with standard Buffer.from(raw).toString('base64') so padding and alphabet are canonical.","Strip all whitespace/newlines from the key string before embedding it.","Check decoded length is exactly 32 bytes: Buffer.from(key, 'base64').length === 32.","Convert base64url to standard base64 if the source used -/_ characters."],"exampleFix":"// before\nkey: \"MCowBQYDK2VwAyEAmV-4_zQ\" // base64url, non-canonical\n// after\nkey: Buffer.from(rawKeyBytes).toString(\"base64\") // canonical std base64, 44 chars","handlingStrategy":"validation","validationCode":"const raw = Buffer.from(key, 'base64');\nif (raw.length !== 32 || raw.toString('base64') !== key) throw new Error('key must be canonical base64 of 32 bytes');","typeGuard":"function isCanonicalB64Key(key: unknown): key is string {\n  if (typeof key !== 'string') return false;\n  const raw = Buffer.from(key, 'base64');\n  return raw.length === 32 && raw.toString('base64') === key;\n}","tryCatchPattern":"try {\n  verifyBundle(bundle);\n} catch (e) {\n  if ((e as Error).message.includes('canonical base64')) console.error('re-encode key with Buffer.from(raw).toString(\"base64\")');\n  throw e;\n}","preventionTips":["Always produce key strings via Buffer.from(raw).toString('base64').","Strip whitespace/newlines before embedding keys in JSON.","Convert base64url to standard base64 at export time."],"tags":["crypto","encoding","validation"],"backgroundTag":"invalid-argument-format","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}