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

  1. Refresh the pinned keyring from the publisher so it contains the new key (keep old keys for older bundles)
  2. Or pin the new current key directly as a raw base64 --pubkey file
  3. 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

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


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