JuliusBrussee/caveman · error
bundle public key does not match the published --pubkey
Error message
bundle public key does not match the published --pubkey
What it means
When --pubkey points at a raw (non-JSON) file, its trimmed content is treated as the base64 Ed25519 public key and must equal the bundle's public_key byte-for-byte. A mismatch means the bundle was signed by a different key than the one you pinned, so authenticity cannot be established.
Source
Thrown at packages/cli/src/index.ts:17429
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");
if (bundle.public_keys !== undefined && !Array.isArray(bundle.public_keys)) throw new Error("public_keys must be an array");
if (bundle.schema === RECEIPT_BUNDLE_V2 && (!Array.isArray(bundle.public_keys) || bundle.public_keys.length === 0)) throw new Error("v2 bundle requires public_keys");
const keys = decodeUniqueKeyring(bundle.public_keys ?? [], "public_keys");
const currentInRing = keys.get(current.info.key_id);
if (currentInRing && !currentInRing.raw.equals(current.raw)) throw new Error(`public_key conflicts with public_keys entry ${current.info.key_id}`);
if (bundle.schema === RECEIPT_BUNDLE_V2 && !currentInRing) throw new Error("v2 public_keys must include public_key");
if (!currentInRing) keys.set(current.info.key_id, current);
return { current, keys };
}
async function pinnedReceiptKeys(file: string, current: DecodedReceiptKey): Promise<{ keys: Map<string, DecodedReceiptKey>; trust: string }> {
const source = (await readFile(file, "utf8")).trim();
if (!source.startsWith("{")) {
const pinned = decodeReceiptKey({ ...current.info, key: source }, "--pubkey");
if (!pinned.raw.equals(current.raw)) throw new Error("bundle public key does not match the published --pubkey");
return { keys: new Map([[current.info.key_id, pinned]]), trust: "pinned_public_key" };
}
let parsed: { public_key?: ReceiptPublicKey; public_keys?: ReceiptPublicKey[] };
try { parsed = JSON.parse(source); } catch { throw new Error("--pubkey JSON is malformed"); }
const infos = Array.isArray(parsed.public_keys) ? parsed.public_keys : parsed.public_key ? [parsed.public_key] : [];
if (infos.length === 0) throw new Error("--pubkey JSON must contain public_key or public_keys");
const keys = decodeUniqueKeyring(infos, "--pubkey public_keys");
const pinnedCurrent = keys.get(current.info.key_id);
if (!pinnedCurrent || !pinnedCurrent.raw.equals(current.raw)) throw new Error("trusted --pubkey keyring does not contain the bundle public key");
return { keys, trust: "pinned_keyring" };
}
// receiptsVerify validates a signed receipt bundle offline (no network). A raw
// --pubkey pins the current key; JSON may independently pin a full rotation
// keyring. Without either, embedded keys prove self-consistency, not publisher
// authenticity. Exits non-zero on any included content, signature, or
// scope-chain break. Tail/scope omission needs separately trusted head manifest;
// bundle output states completeness is not attested.View on GitHub (pinned to 5184b3d11a)
Solutions
- Fetch the publisher's current published key and update the --pubkey file
- If the rotation is legitimate, have the publisher publish a JSON keyring (old + new keys) and pass that file instead, so rotation verifies without repinning
- Confirm you are verifying the intended artifact from the intended publisher before trusting any key update
Example fix
# before: pinned file holds the pre-rotation key caveman receipts verify bundle.json --pubkey old-key.b64 # -> mismatch # after: pin the rotated keyring (JSON, contains k1 and k2) caveman receipts verify bundle.json --pubkey keyring.json
Defensive patterns
Strategy: validation
Validate before calling
const pinned = (await readFile(pubkeyFile, "utf8")).trim();
const bundleKey = Buffer.from(bundle.public_key.key, "base64").toString("base64");
if (!pubkeyFile.trim().startsWith("{") && pinned !== bundleKey) {
throw new Error("pinned raw key does not match bundle public_key — refresh the published key or use a keyring file");
} Type guard
function rawKeyMatches(pinnedB64: string, bundleKeyB64: string): boolean {
return Buffer.from(pinnedB64, "base64").toString("base64") === bundleKeyB64;
} Try / catch
try { execSync(`caveman receipts verify ${bundle} --pubkey ${pubkey}`); }
catch (e) {
if (/does not match the published --pubkey/.test(String((e as Error).message))) {
fail("key rotation: fetch the updated key or pin a JSON keyring with old+new keys");
}
throw e;
} Prevention
- Pin a rotation-aware JSON keyring (old + new keys) instead of a single raw key
- Automate refreshing pinned keys whenever the publisher announces rotation
- Fail CI on key mismatch rather than silently falling back to embedded keys
When it happens
Trigger: `caveman receipts verify bundle.json --pubkey key.b64` where key.b64 decodes to a different 32-byte key than bundle.public_key — e.g. the publisher rotated keys after key.b64 was captured.
Common situations: A stale pinned key checked into the repo; key rotation without updating the published key file; verifying a bundle from publisher B with publisher A's key file.
Related errors
- trusted --pubkey keyring does not contain the bundle public
- ${label} contains duplicate key_id ${decoded.info.key_id}
- public_key conflicts with public_keys entry ${current.info.k
- runtime_policy public_key must be a base64-encoded 32-byte E
- ed25519 public key must be 32 bytes, got ${raw.length}
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18).
Data as JSON: /api/errors/ff2c52e67693cd41.
Report an issue: GitHub.