{"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":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":17264,"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":17246,"sourceCodeEnd":17282,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L17246-L17282","documentation":"ed25519PublicKey wraps a raw 32-byte Ed25519 public key in a DER SPKI prefix (hex 302a300506032b6570032100) before handing it to node:crypto. A buffer of any other length cannot be an Ed25519 public key, so it is rejected immediately with the observed byte count.","triggerScenarios":"Feeding a hex-decoded key (64 bytes from 64 hex chars); passing the 64-byte Ed25519 secret key/seed material instead of the 32-byte public half; base64 decoded twice; a truncated key buffer.","commonSituations":"Copy-pasting keys between formats (hex vs raw vs base64); mixing up public and secret key material; keys generated for other curves that decode to different lengths.","solutions":["Ensure the buffer is the raw 32-byte public key","From base64: Buffer.from(key, 'base64') must decode to exactly 32 bytes","From hex: Buffer.from(key, 'hex') on a 64-character hex string","Never pass the secret/seed — export the public key first"],"exampleFix":"// before\nconst key = ed25519PublicKey(Buffer.from(hexSeed, 'hex')); // decodes to 64 bytes\n// after\nconst key = ed25519PublicKey(Buffer.from(hexPublicKey, 'hex')); // decodes to 32 bytes","handlingStrategy":"validation","validationCode":"const raw = Buffer.from(keyMaterial, 'base64');\nif (raw.length !== 32) throw new Error(`expected a 32-byte Ed25519 public key, got ${raw.length} — check the encoding (hex vs base64) and use the public half`);","typeGuard":"const isRawEd25519Key = (b: Buffer): boolean => b.length === 32;","tryCatchPattern":null,"preventionTips":["Store keys with an explicit encoding label (raw-base64 vs hex)","Validate length at ingestion, not at verification time","Unit-test key parsing with fixture keys in each supported format"],"tags":["crypto","ed25519","key-validation","receipts"],"backgroundTag":"invalid-public-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"}