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

  1. Fetch the publisher's current published key and update the --pubkey file
  2. 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
  3. 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

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


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