{"record":{"id":"55367c47551a155e","repo":"JuliusBrussee/caveman","slug":"ed25519-public-key-must-be-32-bytes-got-raw-len","errorCode":null,"errorMessage":"ed25519 public key must be 32 bytes, got ${raw.length}","messagePattern":"ed25519 public key must be 32 bytes, got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":17720,"sourceCode":"    previous = method;\n  }\n  return true;\n}\n\n// canonicalize reproduces cloud/metering's canonical form byte-for-byte: compact\n// JSON with object keys sorted lexicographically and ES6 (shortest) numbers,\n// which JSON.stringify and Go's encoding/json both emit identically.\nfunction canonicalize(value: unknown): string {\n  if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n  if (Array.isArray(value)) return \"[\" + value.map(canonicalize).join(\",\") + \"]\";\n  const obj = value as Record<string, unknown>;\n  return \"{\" + Object.keys(obj).sort().map((k) => JSON.stringify(k) + \":\" + canonicalize(obj[k])).join(\",\") + \"}\";\n}\n\n// ed25519PublicKey wraps a raw 32-byte key in DER SPKI so node:crypto can verify\n// with it (matching Go's raw ed25519 public key).\nfunction ed25519PublicKey(raw: Buffer): KeyObject {\n  if (raw.length !== 32) throw new Error(`ed25519 public key must be 32 bytes, got ${raw.length}`);\n  const der = Buffer.concat([Buffer.from(\"302a300506032b6570032100\", \"hex\"), raw]);\n  return createPublicKey({ key: der, format: \"der\", type: \"spki\" });\n}\n\n// verifyReceipt recomputes the hash over the canonical core (every field except\n// receipt_hash and signature) and verifies the signature over receipt_hash.\nfunction validateReceiptContent(r: Receipt): string | null {\n  if (r.schema !== RECEIPT_V1 && r.schema !== RECEIPT_V2 && r.schema !== RECEIPT_V3 && r.schema !== RECEIPT_V4) return `seq ${r.seq}: unsupported receipt schema ${String(r.schema)}`;\n  if (!r.scope || !SHA256_VALUE.test(r.scope.org_hash) || !SHA256_VALUE.test(r.scope.project_hash)) return `seq ${r.seq}: invalid receipt scope hash`;\n  if (r.schema === RECEIPT_V4) {\n    if (r.scope_completeness !== INCLUDED_RECEIPTS_ONLY) return `seq ${r.seq}: scope_completeness must be ${INCLUDED_RECEIPTS_ONLY}`;\n    if (!Array.isArray(r.methods)) return `seq ${r.seq}: methods must be an array`;\n    if (r.formula_version === RECEIPT_FORMULA && !validBillableReceiptMethods(r.methods)) return `seq ${r.seq}: unsupported billable methods`;\n    if (r.formula_version === CAVEBENCH_RECEIPT_FORMULA && r.methods.length !== 0) return `seq ${r.seq}: CaveBench receipts cannot claim verified methods`;\n  } else if (r.scope_completeness !== undefined || r.methods !== undefined) {\n    return `seq ${r.seq}: signed scope and methods require receipt v4`;\n  }\n  if (!validReceiptDay(r.day)) return `seq ${r.seq}: invalid receipt day ${String(r.day)}`;","sourceCodeStart":17702,"sourceCodeEnd":17738,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L17702-L17738","documentation":"Thrown by the internal ed25519PublicKey helper, which wraps a raw 32-byte Ed25519 public key in a DER SPKI structure so node:crypto's createPublicKey can verify receipt signatures. It fires when the decoded key buffer is not exactly 32 bytes, i.e. the supplied key material is truncated, padded, or not a raw Ed25519 public key at all.","triggerScenarios":"Calling receipt verification with a receipt bundle whose public key decodes (from base64) to a buffer whose length !== 32 — e.g. a corrupted, hand-edited, or truncated key field, or a PEM/DER key pasted into a field that expects raw key bytes.","commonSituations":"Developers pasting a full DER/PEM key where only the raw 32 bytes belong; keys damaged by copy/paste (whitespace stripped or line breaks removed changing base64 decode); exporting from a tool that emits a different key format.","solutions":["Re-export the receipt bundle / key from the producing system so the raw 32-byte Ed25519 key is used.","Check that the base64 string decodes to exactly 32 bytes: Buffer.from(key, 'base64').length === 32.","If you have a PEM/SPKI key, extract the raw key: createPublicKey(pem).export({format:'der',type:'spki'}).subarray(12) and base64 that.","Confirm no encoding mangling (whitespace, quotes, URL-encoding) corrupted the key string."],"exampleFix":"// before\nconst key = Buffer.from(pemString, 'base64'); // length 44, not 32 -> throws\n// after\nconst raw = createPublicKey(pemString).export({ format: 'der', type: 'spki' }).subarray(12);\nverifyReceipt(receipt, Buffer.from(raw)); // 32 bytes","handlingStrategy":"validation","validationCode":"const raw = Buffer.from(keyB64, 'base64');\nif (raw.length !== 32) throw new Error(`expected 32-byte ed25519 key, got ${raw.length}`);","typeGuard":"function isRawEd25519Key(key: unknown): key is string {\n  return typeof key === 'string' && Buffer.from(key, 'base64').length === 32;\n}","tryCatchPattern":"try {\n  verifyReceipt(receipt, bundle);\n} catch (e) {\n  if ((e as Error).message.includes('must be 32 bytes')) console.error('bad key material:', e.message);\n  throw e;\n}","preventionTips":["Never paste PEM/DER key text into raw-key fields; export raw bytes first.","Assert key length === 32 at bundle-build time.","Round-trip check: Buffer.from(key, 'base64').toString('base64') === key."],"tags":["crypto","ed25519","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"}