JuliusBrussee/caveman · error

${label} key is required

Error message

${label} key is required

What it means

A keyring entry passed the key_id and algorithm checks but its key field is missing, empty, or whitespace-only, so there is no key material to verify receipts with. decodeReceiptKey throws with the entry's label so the offending entry can be located directly.

Source

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

  for (const r of sorted) {
    const decoded = keys.get(r.signature?.key_id);
    if (!decoded) return `seq ${r.seq}: no trusted public key for key_id ${String(r.signature?.key_id)}`;
    const err = verifyReceipt(r, decoded.key, decoded.info.key_id);
    if (err) return err;
    if (prev) {
      if (r.seq !== prev.seq + 1) return `seq ${r.seq}: not strictly after ${prev.seq}`;
      if (r.prev_receipt_hash !== prev.receipt_hash) return `seq ${r.seq}: prev_receipt_hash does not link to seq ${prev.seq}`;
      if (r.day <= prev.day) return `seq ${r.seq}: day ${r.day} does not follow ${prev.day}`;
    }
    prev = r;
  }
  return null;
}

function decodeReceiptKey(info: ReceiptPublicKey, label: string): DecodedReceiptKey {
  if (!info || typeof info.key_id !== "string" || !info.key_id.trim()) throw new Error(`${label} key_id is required`);
  if (info.alg !== "Ed25519") throw new Error(`${label} has unsupported algorithm ${String(info.alg)}`);
  if (typeof info.key !== "string" || !info.key.trim()) throw new Error(`${label} key is required`);
  const raw = Buffer.from(info.key, "base64");
  if (raw.length !== 32 || raw.toString("base64") !== info.key) throw new Error(`${label} must be a canonical base64 Ed25519 public key`);
  return { info, raw, key: ed25519PublicKey(raw) };
}

function decodeUniqueKeyring(infos: ReceiptPublicKey[], label: string): Map<string, DecodedReceiptKey> {
  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)}`);

View on GitHub (pinned to 5184b3d11a)

Solutions

  1. Fill in the base64 Ed25519 public key for the entry named in the message
  2. Ensure the variable feeding the template is non-empty before rendering
  3. Re-fetch the published keyring from the issuer

Example fix

// before
{ "key_id": "k1", "alg": "Ed25519", "key": "" }
// after
{ "key_id": "k1", "alg": "Ed25519", "key": "11qYAYKxCrfVS/_TyWQHOg7hcvPapiMlrwIaaPcHURo=" }
Defensive patterns

Strategy: validation

Validate before calling

for (const [i, k] of keyring.entries()) {
  if (typeof k?.key !== 'string' || !k.key.trim()) throw new Error(`keyring[${i}].key is missing`);
}

Prevention

When it happens

Trigger: Keyring entries where the base64 key was dropped during serialization; templates with a key placeholder that was never filled; whitespace-padded keys from copy-paste (the check trims).

Common situations: Hand-authored keyring JSON; CI templates interpolating an unset key variable; sanitization steps stripping long opaque strings.

Related errors


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