{"record":{"id":"72861bad15a96348","repo":"Hmbown/CodeWhale","slug":"public-key-must-decode-to-32-bytes","errorCode":null,"errorMessage":"public key must decode to 32 bytes","messagePattern":"public key must decode to 32 bytes","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":78,"sourceCode":"    if (v === undefined) continue;\n    parts.push(`${JSON.stringify(key)}:${canonicalize(v)}`);\n  }\n  return `{${parts.join(\",\")}}`;\n}\n\nexport function signingMessage(keyId, payloadBytes) {\n  return Buffer.concat([Buffer.from(DOMAIN, \"utf8\"), Buffer.from(keyId, \"utf8\"), Buffer.from([0]), payloadBytes]);\n}\n\nexport function rawPublicKeyFromKeyObject(keyObject) {\n  const spki = keyObject.export({ type: \"spki\", format: \"der\" });\n  // Ed25519 SPKI DER is a fixed 12-byte prefix followed by the 32-byte key.\n  return spki.subarray(spki.length - 32);\n}\n\nexport function publicKeyObjectFromRaw(rawB64) {\n  const raw = strictBase64(rawB64, 32);\n  if (raw.length !== 32) throw new Error(\"public key must decode to 32 bytes\");\n  const prefix = Buffer.from(\"302a300506032b6570032100\", \"hex\");\n  return createPublicKey({ key: Buffer.concat([prefix, raw]), type: \"spki\", format: \"der\" });\n}\n\nexport function signPayload(privateKey, keyId, payloadBytes) {\n  return sign(null, signingMessage(keyId, payloadBytes), privateKey);\n}\n\n/** Canonical base64 is checked before decoding to bound allocation. */\nexport function strictBase64(value, maxBytes) {\n  if (typeof value !== \"string\" || !value.length || value.length > 4 * Math.ceil(maxBytes / 3) ||\n      (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))) throw new Error(\"invalid base64\");\n  const bytes = Buffer.from(value, \"base64\");\n  if (bytes.length > maxBytes || bytes.toString(\"base64\") !== value) throw new Error(\"invalid base64\");\n  return bytes;\n}\n\nexport function utcTime(value) {","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L60-L96","documentation":"publicKeyObjectFromRaw builds an Ed25519 public key from a raw base64-encoded 32-byte key by wrapping it in an SPKI DER prefix. After strictBase64 decoding it re-checks the length is exactly 32 bytes; any other length throws this error because only 32-byte Ed25519 raw keys can be wrapped.","triggerScenarios":"Calling publicKeyObjectFromRaw with a base64 string that decodes to fewer or more than 32 bytes (e.g. a truncated key, an Ed448 key, or a PEM body pasted in).","commonSituations":"Rotating signing keys and publishing a malformed public key file, copying the key from a hex encoding instead of base64, or accidentally publishing the private key's longer material.","solutions":["Regenerate or re-export the public key and re-encode exactly 32 raw bytes as standard base64: `openssl pkey -pubin -in pub.pem -outform DER | tail -c 32 | base64`","Verify the decoded length: `echo <b64> | base64 -d | wc -c` must print 32","Ensure you are not pasting a hex-encoded key or a full SPKI PEM where raw base64 is expected"],"exampleFix":"// before\npublicKeyObjectFromRaw(truncatedKeyB64); // decodes to 16 bytes\n// after\nconst raw = Buffer.from(spkiDer.subarray(-32));\npublicKeyObjectFromRaw(raw.toString('base64'));","handlingStrategy":"validation","validationCode":"const raw = Buffer.from(keyB64, 'base64');\nif (raw.length !== 32) throw new Error(`public key must be 32 raw bytes, got ${raw.length}`);","typeGuard":"const isRaw32B64 = (v) => typeof v === 'string' && Buffer.from(v, 'base64').length === 32;","tryCatchPattern":"try { publicKeyObjectFromRaw(keyB64); } catch (e) { if (e.message.includes('32 bytes')) throw new Error('re-export the Ed25519 public key as raw 32-byte base64'); throw e; }","preventionTips":["Export keys with: openssl pkey -pubin -outform DER | tail -c 32 | base64","Never paste PEM or hex where raw base64 is expected","Check decoded length before use"],"tags":["crypto","ed25519","base64"],"backgroundTag":"invalid-argument-format","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}