JuliusBrussee/caveman · error
${label} must be a canonical base64 Ed25519 public key
Error message
${label} must be a canonical base64 Ed25519 public key What it means
The strictest key-encoding check in decodeReceiptKey: the key string must be canonical standard base64 that decodes to exactly 32 bytes AND re-encodes to the identical string. One guard rejects hex, URL-safe base64, non-canonical padding, embedded whitespace, and wrong-length keys.
Source
Thrown at packages/cli/src/index.ts:17396
if (!decoded) return `seq ${r.seq}: no trusted public key for key_id ${String(r.signature?.key_id)}`;
const err = verifyReceipt(r, decoded.key, decoded.info.key_id);
if (err) return err;
if (prev) {
if (r.seq !== prev.seq + 1) return `seq ${r.seq}: not strictly after ${prev.seq}`;
if (r.prev_receipt_hash !== prev.receipt_hash) return `seq ${r.seq}: prev_receipt_hash does not link to seq ${prev.seq}`;
if (r.day <= prev.day) return `seq ${r.seq}: day ${r.day} does not follow ${prev.day}`;
}
prev = r;
}
return null;
}
function decodeReceiptKey(info: ReceiptPublicKey, label: string): DecodedReceiptKey {
if (!info || typeof info.key_id !== "string" || !info.key_id.trim()) throw new Error(`${label} key_id is required`);
if (info.alg !== "Ed25519") throw new Error(`${label} has unsupported algorithm ${String(info.alg)}`);
if (typeof info.key !== "string" || !info.key.trim()) throw new Error(`${label} key is required`);
const raw = Buffer.from(info.key, "base64");
if (raw.length !== 32 || raw.toString("base64") !== info.key) throw new Error(`${label} must be a canonical base64 Ed25519 public key`);
return { info, raw, key: ed25519PublicKey(raw) };
}
function decodeUniqueKeyring(infos: ReceiptPublicKey[], label: string): Map<string, DecodedReceiptKey> {
const keys = new Map<string, DecodedReceiptKey>();
for (const [index, info] of infos.entries()) {
const decoded = decodeReceiptKey(info, `${label}[${index}]`);
if (keys.has(decoded.info.key_id)) throw new Error(`${label} contains duplicate key_id ${decoded.info.key_id}`);
keys.set(decoded.info.key_id, decoded);
}
return keys;
}
function embeddedReceiptKeys(bundle: ReceiptBundle): { current: DecodedReceiptKey; keys: Map<string, DecodedReceiptKey> } {
if (bundle.schema !== RECEIPT_BUNDLE_V1 && bundle.schema !== RECEIPT_BUNDLE_V2) throw new Error(`unsupported bundle schema ${String(bundle.schema)}`);
if (bundle.verification_coverage !== undefined && bundle.verification_coverage !== INCLUDED_RECEIPTS_ONLY) throw new Error(`unsupported unsigned verification coverage ${String(bundle.verification_coverage)}`);
if (bundle.completeness_attested === true) throw new Error("bundle completeness cannot be attested by unsigned export metadata");
const current = decodeReceiptKey(bundle.public_key, "public_key");View on GitHub (pinned to 5184b3d11a)
Solutions
- Encode the raw 32 bytes with standard base64: Buffer.from(raw).toString('base64')
- Convert base64url: replace '-' with '+' and '_' with '/', then re-encode canonically
- Convert hex: Buffer.from(hex, 'hex').toString('base64')
- Verify the round-trip: decode then encode must reproduce the string exactly
Example fix
// before
{ "key_id": "k1", "alg": "Ed25519", "key": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" }
// after
{ "key_id": "k1", "alg": "Ed25519", "key": "11qYAYKxCrfVS/_TyWQHOg7hcvPapiMlrwIaaPcHURo=" } Defensive patterns
Strategy: validation
Validate before calling
const raw = Buffer.from(info.key, 'base64');
const canonical = raw.length === 32 && raw.toString('base64') === info.key;
if (!canonical) throw new Error('key must be canonical standard base64 of the 32 raw bytes'); Type guard
const isCanonicalBase64Key = (s: unknown): s is string =>
typeof s === 'string' &&
/^[A-Za-z0-9+/]{43}=$/.test(s) &&
Buffer.from(s, 'base64').toString('base64') === s; Prevention
- Produce keys only via Buffer.from(raw32).toString('base64')
- Never base64url-encode receipt keys
- Add a fixture test that the published keyring parses cleanly
When it happens
Trigger: Hex-encoded keys (64 hex characters); base64url using '-'/'_' instead of '+'/'/'; base64 with unusual padding or line breaks; base64 of a value that is not 32 bytes; double-encoded base64.
Common situations: Keys copied from Go/Rust tooling that prints hex or base64url; keys passed through systems that re-encode them; JSON serialization escaping or trimming characters.
Related errors
- ${label} key_id is required
- ${label} has unsupported algorithm ${String(info.alg)}
- ${label} key is required
- ed25519 public key must be 32 bytes, got ${raw.length}
- ${label} contains duplicate key_id ${decoded.info.key_id}
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18).
Data as JSON: /api/errors/dd95b544db9ec58d.
Report an issue: GitHub.