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

  1. Ensure the buffer is the raw 32-byte public key
  2. From base64: Buffer.from(key, 'base64') must decode to exactly 32 bytes
  3. From hex: Buffer.from(key, 'hex') on a 64-character hex string
  4. 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

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


AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18). Data as JSON: /api/errors/55367c47551a155e. Report an issue: GitHub.