JuliusBrussee/caveman · error
trusted --pubkey keyring does not contain the bundle public
Error message
trusted --pubkey keyring does not contain the bundle public key
What it means
The --pubkey JSON keyring parsed successfully, but it does not contain the bundle's current key: either no entry carries the bundle's key_id, or the entry with that id has different raw key bytes. The bundle was signed by a key your trust anchor does not know, so the pinned keyring cannot establish authenticity.
Source
Thrown at packages/cli/src/index.ts:17438
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.
// caveman receipts verify <bundle.json> [--pubkey <file>]
async function receiptsVerify(argv: string[]) {
const file = positionalAfterOptions(argv.slice(1), new Set(["--pubkey"]));
if (!file) throw new Error(`usage: ${invokedCommand("receipts")} verify <bundle.json> [--pubkey <file>]`);
let bundle: ReceiptBundle;
try { bundle = JSON.parse(await readFile(file, "utf8")) as ReceiptBundle; } catch (e) { return fail(`invalid bundle JSON: ${(e as Error).message}`); }
if (!Array.isArray(bundle.receipts)) return fail("bundle receipts must be an array");
try {View on GitHub (pinned to 5184b3d11a)
Solutions
- Refresh the pinned keyring from the publisher so it contains the new key (keep old keys for older bundles)
- Or pin the new current key directly as a raw base64 --pubkey file
- If the new key is unexpected, treat the bundle as untrusted and investigate before trusting any new key material
Example fix
// before: pinned keyring only has the pre-rotation key
{ "public_keys": [ { "key_id": "k1", "alg": "Ed25519", "key": "OLD..." } ] }
// after: rotation-aware keyring
{ "public_keys": [ { "key_id": "k1", "alg": "Ed25519", "key": "OLD..." }, { "key_id": "k2", "alg": "Ed25519", "key": "NEW..." } ] } Defensive patterns
Strategy: validation
Validate before calling
const doc = JSON.parse(await readFile(pubkeyFile, "utf8"));
const infos = Array.isArray(doc.public_keys) ? doc.public_keys : doc.public_key ? [doc.public_key] : [];
const current = infos.find((k) => k.key_id === bundle.public_key.key_id);
if (!current || current.key !== bundle.public_key.key) {
throw new Error("pinned keyring does not cover the bundle's signing key — refresh after rotation");
} Type guard
function keyringCoversKey(infos: { key_id: string; key: string }[], keyId: string, keyB64: string): boolean {
const hit = infos.find((k) => k.key_id === keyId);
return !!hit && hit.key === keyB64;
} Try / catch
try { execSync(`caveman receipts verify ${bundle} --pubkey ${pubkey}`); }
catch (e) {
if (/keyring does not contain the bundle public key/.test(String((e as Error).message))) {
fail("unknown signing key: refresh the pinned keyring or treat bundle as untrusted");
}
throw e;
} Prevention
- Subscribe to publisher key rotations and refresh pinned keyrings promptly
- Keep historical keys in the pinned keyring so older bundles still verify
- Treat an unknown signing key as a security event, not a routine failure
When it happens
Trigger: The publisher rotated to a new key that is not yet in your pinned keyring file; or you are verifying a bundle from a different publisher than the one your keyring belongs to.
Common situations: Rotation happens between when you pinned the keyring and when the bundle was produced; teams pin a keyring snapshot and forget to refresh it after rotation; cross-publisher mix-ups in CI caches.
Related errors
- ${label} contains duplicate key_id ${decoded.info.key_id}
- public_key conflicts with public_keys entry ${current.info.k
- bundle public key does not match the published --pubkey
- ${label} key_id is required
- ${label} has unsupported algorithm ${String(info.alg)}
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18).
Data as JSON: /api/errors/15004f0f0fc575af.
Report an issue: GitHub.