JuliusBrussee/caveman · error

--pubkey JSON must contain public_key or public_keys

Error message

--pubkey JSON must contain public_key or public_keys

What it means

A --pubkey JSON file must contain either public_key (an object) or public_keys (an array). If public_keys is not an array and public_key is falsy, no key candidates exist and the CLI throws "--pubkey JSON must contain public_key or public_keys".

Source

Thrown at packages/cli/src/index.ts:17435

  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.
//   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}`); }

View on GitHub (pinned to 5184b3d11a)

Solutions

  1. Pass a document that has public_key or public_keys at the top level
  2. Check field names exactly: public_key (object) or public_keys (array of key objects)
  3. Do not point --pubkey at the bundle; it needs the separately published key material

Example fix

# before
caveman receipts verify bundle.json --pubkey bundle.json

# after
caveman receipts verify bundle.json --pubkey published-keyring.json
# published-keyring.json: { "public_keys": [ { "key_id": "k1", "alg": "Ed25519", "key": "AAA..." } ] }
Defensive patterns

Strategy: validation

Validate before calling

const parsed = JSON.parse(await readFile(pubkeyFile, "utf8"));
if (!Array.isArray(parsed.public_keys) && !parsed.public_key) {
  throw new Error("--pubkey document has neither public_key nor public_keys");
}

Type guard

function isPubkeyDocument(v: unknown): v is { public_key?: unknown; public_keys?: unknown[] } {
  return !!v && typeof v === "object" &&
    (Array.isArray((v as any).public_keys) || !!(v as any).public_key);
}

Try / catch

try { execSync(`caveman receipts verify ${bundle} --pubkey ${pubkey}`); }
catch (e) {
  if (/must contain public_key or public_keys/.test(String((e as Error).message))) fail("wrong file passed to --pubkey");
  throw e;
}

Prevention

When it happens

Trigger: Passing the wrong JSON file to --pubkey — e.g. the receipt bundle itself, a credentials/config file, or a key document using different field names (e.g. "keys" or "jwk").

Common situations: Reusing the bundle path for both arguments; a key file produced by another tool whose schema names fields differently; typos in field names when hand-writing the file.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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