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
- Pass a document that has public_key or public_keys at the top level
- Check field names exactly: public_key (object) or public_keys (array of key objects)
- 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
- Use distinct, descriptive filenames for bundles vs key documents
- Validate the --pubkey document shape in pipeline preflight
- Keep key documents in one canonical schema ({public_keys: [...]})
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
- --pubkey JSON is malformed
- cave response was not valid JSON
- cave response must be a JSON object
- ${label} key_id is required
- ${label} key is required
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18).
Data as JSON: /api/errors/a2bbfb1856bfa431.
Report an issue: GitHub.