JuliusBrussee/caveman · error · Error
ed25519 public key must be 32 bytes, got ${raw.length}
Error message
ed25519 public key must be 32 bytes, got ${raw.length} What it means
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.
Source
Thrown at packages/cli/src/index.ts:17264
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 5184b3d11a)
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
Example fix
// before const key = ed25519PublicKey(Buffer.from(hexSeed, 'hex')); // decodes to 64 bytes // after const key = ed25519PublicKey(Buffer.from(hexPublicKey, 'hex')); // decodes to 32 bytes
Defensive patterns
Strategy: validation
Validate before calling
const raw = Buffer.from(keyMaterial, 'base64');
if (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`); Type guard
const isRawEd25519Key = (b: Buffer): boolean => b.length === 32;
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- runtimePolicy publicKey must be a base64-encoded 32-byte Ed2
- runtime_policy public_key must be a base64-encoded 32-byte E
- ${label} key_id is required
- ${label} has unsupported algorithm ${String(info.alg)}
- ${label} key is required
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18).
Data as JSON: /api/errors/55367c47551a155e.
Report an issue: GitHub.