JuliusBrussee/caveman · error
ed25519 public key must be 32 bytes, got
Error message
ed25519 public key must be 32 bytes, got ${raw.length} What it means
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.
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.
Example fix
// before
const key = Buffer.from(pemString, 'base64'); // length 44, not 32 -> throws
// after
const raw = createPublicKey(pemString).export({ format: 'der', type: 'spki' }).subarray(12);
verifyReceipt(receipt, Buffer.from(raw)); // 32 bytes Defensive patterns
Strategy: validation
Validate before calling
const raw = Buffer.from(keyB64, 'base64');
if (raw.length !== 32) throw new Error(`expected 32-byte ed25519 key, got ${raw.length}`); Type guard
function isRawEd25519Key(key: unknown): key is string {
return typeof key === 'string' && Buffer.from(key, 'base64').length === 32;
} Try / catch
try {
verifyReceipt(receipt, bundle);
} catch (e) {
if ((e as Error).message.includes('must be 32 bytes')) console.error('bad key material:', e.message);
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- runtimePolicy publicKey must be a base64-encoded 32-byte…
- bundle completeness cannot be attested by unsigned export…
- envelope: object kind is required
- envelope: organization scope is required
- contains duplicate key_id
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/55367c47551a155e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:17720
previous = method;
}
return true;
}
// canonicalize reproduces cloud/metering's canonical form byte-for-byte: compact
// JSON with object keys sorted lexicographically and ES6 (shortest) numbers,
// which JSON.stringify and Go's encoding/json both emit identically.
function canonicalize(value: unknown): string {
if (value === null || typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
const obj = value as Record<string, unknown>;
return "{" + Object.keys(obj).sort().map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
}
// ed25519PublicKey wraps a raw 32-byte key in DER SPKI so node:crypto can verify
// with it (matching Go's raw ed25519 public key).
function ed25519PublicKey(raw: Buffer): KeyObject {
if (raw.length !== 32) throw new Error(`ed25519 public key must be 32 bytes, got ${raw.length}`);
const der = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
return createPublicKey({ key: der, format: "der", type: "spki" });
}
// verifyReceipt recomputes the hash over the canonical core (every field except
// receipt_hash and signature) and verifies the signature over receipt_hash.
function validateReceiptContent(r: Receipt): string | null {
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)}`;
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`;
if (r.schema === RECEIPT_V4) {
if (r.scope_completeness !== INCLUDED_RECEIPTS_ONLY) return `seq ${r.seq}: scope_completeness must be ${INCLUDED_RECEIPTS_ONLY}`;
if (!Array.isArray(r.methods)) return `seq ${r.seq}: methods must be an array`;
if (r.formula_version === RECEIPT_FORMULA && !validBillableReceiptMethods(r.methods)) return `seq ${r.seq}: unsupported billable methods`;
if (r.formula_version === CAVEBENCH_RECEIPT_FORMULA && r.methods.length !== 0) return `seq ${r.seq}: CaveBench receipts cannot claim verified methods`;
} else if (r.scope_completeness !== undefined || r.methods !== undefined) {
return `seq ${r.seq}: signed scope and methods require receipt v4`;
}
if (!validReceiptDay(r.day)) return `seq ${r.seq}: invalid receipt day ${String(r.day)}`;View on GitHub (pinned to 3ee70a1026)