JuliusBrussee/caveman · error
public_key conflicts with public_keys entry ${current.info.k
Error message
public_key conflicts with public_keys entry ${current.info.key_id} What it means
If the top-level public_key shares a key_id with an entry in public_keys, the two raw 32-byte Ed25519 keys must be identical. Same id with different bytes means a key_id was reused for different key material, which breaks rotation accounting and signature attribution.
Source
Thrown at packages/cli/src/index.ts:17419
const keys = new Map<string, DecodedReceiptKey>();
for (const [index, info] of infos.entries()) {
const decoded = decodeReceiptKey(info, `${label}[${index}]`);
if (keys.has(decoded.info.key_id)) throw new Error(`${label} contains duplicate key_id ${decoded.info.key_id}`);
keys.set(decoded.info.key_id, decoded);
}
return keys;
}
function embeddedReceiptKeys(bundle: ReceiptBundle): { current: DecodedReceiptKey; keys: Map<string, DecodedReceiptKey> } {
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);View on GitHub (pinned to 5184b3d11a)
Solutions
- Give the new key a fresh key_id and re-export the bundle with consistent entries
- Audit the rotation pipeline so every new key gets a new id
- If the top-level public_key is stale, re-export so it matches the keyring entry
Example fix
// before
"public_key": { "key_id": "k1", "key": "NEWBYTES..." }
"public_keys": [ { "key_id": "k1", "key": "OLDBYTES..." } ]
// after
"public_key": { "key_id": "k2", "key": "NEWBYTES..." }
"public_keys": [ { "key_id": "k1", "key": "OLDBYTES..." }, { "key_id": "k2", "key": "NEWBYTES..." } ] Defensive patterns
Strategy: validation
Validate before calling
const ringKey = (bundle.public_keys ?? []).find((k) => k.key_id === bundle.public_key.key_id);
if (ringKey && Buffer.from(ringKey.key, "base64").toString("base64") !== bundle.public_key.key) {
throw new Error(`key_id ${bundle.public_key.key_id} maps to two different keys`);
} Type guard
function keyIdIsUnambiguous(b: { public_key: { key_id: string; key: string }; public_keys?: { key_id: string; key: string }[] }): boolean {
const hit = (b.public_keys ?? []).find((k) => k.key_id === b.public_key.key_id);
return !hit || hit.key === b.public_key.key;
} Try / catch
try { execSync(`caveman receipts verify ${bundle}`); }
catch (e) {
if (/conflicts with public_keys entry/.test(String((e as Error).message))) fail("key_id reuse detected — re-export with fresh ids");
throw e;
} Prevention
- Treat key_id as immutable per key: one id, one key, forever
- Store (key_id -> key) pairs in one source of truth instead of two fields that can drift
- Assert public_key matches its keyring entry in producer tests
When it happens
Trigger: A rotation minted a new key but kept the old key_id, so the bundle carries key_id X in public_key with new bytes and key_id X in public_keys with old bytes (or vice versa).
Common situations: Rotation tooling generates keys but not ids; restoring an old key under its previous id; partial manual rotation of one field without the other.
Related errors
- ${label} contains duplicate key_id ${decoded.info.key_id}
- trusted --pubkey keyring does not contain the bundle public
- ed25519 public key must be 32 bytes, got ${raw.length}
- ${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/9673f9adb839b9fd.
Report an issue: GitHub.