{"record":{"id":"731972183eec1e8c","repo":"Hmbown/CodeWhale","slug":"invalid-or-duplicated-pinned-key","errorCode":null,"errorMessage":"invalid or duplicated pinned key","messagePattern":"invalid or duplicated pinned key","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":376,"sourceCode":"  } finally { closeSync(fd); }\n}\n\nfunction loadPrivateKeyFromEnv() {\n  refuseUnderCi();\n  let pem = process.env.CODEWHALE_FACTS_SIGNING_KEY;\n  const file = process.env.CODEWHALE_FACTS_SIGNING_KEY_FILE;\n  if (!pem && file) pem = readBoundedFile(file, 16 * 1024).toString(\"utf8\");\n  if (!pem) throw new Error(\"set CODEWHALE_FACTS_SIGNING_KEY (PEM) or CODEWHALE_FACTS_SIGNING_KEY_FILE\");\n  if (Buffer.byteLength(pem) > 16 * 1024) throw new Error(\"signing key exceeds size limit\");\n  const key = createPrivateKey({ key: pem, format: \"pem\" });\n  if (key.asymmetricKeyType !== \"ed25519\") throw new Error(\"signing key must be Ed25519\");\n  return key;\n}\n\nexport function validateTrustedKeys(keys) {\n  const seen = new Set();\n  for (const key of keys) {\n    if (!KEY_ID_RE.test(key.keyId) || seen.has(key.keyId) || ![\"active\", \"retired\"].includes(key.status) || strictBase64(key.publicKey, 32).length !== 32) throw new Error(\"invalid or duplicated pinned key\");\n    seen.add(key.keyId);\n  }\n  return keys;\n}\n\n/** Deliberately narrow syntax: a changed/unparseable table must fail the gate. */\nexport function parseTsKeys(text) {\n  const source = text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\").replace(/^\\s*\\/\\/.*$/gm, \"\");\n  const tables = [...source.matchAll(/^\\s*export\\s+const\\s+TRUSTED_KEYS\\s*:\\s*readonly\\s+TrustedKey\\[\\]\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*;/gm)];\n  if (tables.length !== 1) throw new Error(\"cannot parse exactly one TypeScript TRUSTED_KEYS table\");\n  const table = tables[0];\n  const body = table[1].replace(/^\\s*\\/\\/.*$/gm, \"\");\n  const keys = [];\n  const remainder = body.replace(/\\{\\s*keyId:\\s*\"([^\"]+)\",\\s*publicKey:\\s*\"([^\"]+)\",\\s*status:\\s*\"([^\"]+)\"\\s*,?\\s*\\}/g, (_, keyId, publicKey, status) => {\n    keys.push({ keyId, publicKey, status });\n    return \"\";\n  });\n  if (remainder.replace(/[\\s,]/g, \"\")) throw new Error(\"unparsed TypeScript TRUSTED_KEYS entry\");","sourceCodeStart":358,"sourceCodeEnd":394,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L358-L394","documentation":"validateTrustedKeys checks each pinned key entry: keyId must match KEY_ID_RE, be unique (no duplicates), status must be 'active' or 'retired', and publicKey must be strict base64 decoding to exactly 32 bytes. Any violation throws this single generic error describing the first offending entry's rule failure.","triggerScenarios":"Calling validateTrustedKeys(keys) where an entry has a malformed/duplicate keyId, a status outside ['active','retired'], or a publicKey that is not strict base64 of length 32 (e.g. a PEM, hex string, or truncated key).","commonSituations":"Hand-editing the trusted-keys table and mistyping an id or status; pasting an RSA public key or hex-encoded Ed25519 key; duplicate rows after merging key-rotation branches; whitespace in base64 from copy-paste.","solutions":["Validate each entry against the same rules: KEY_ID_RE.test(keyId), unique keyId, status in ['active','retired'], strictBase64(publicKey, 32).length === 32","Convert the public key to raw 32-byte base64: openssl pkey -pubin -in pub.pem -outform DER | tail -c 32 | base64","Remove duplicate keyId rows, keeping the intended rotation state","Normalize status to exactly 'active' or 'retired' (lowercase)"],"exampleFix":"// before\ntrustedKeys: [{ keyId: 'Key 1', status: 'ACTIVE', publicKey: 'AABBCC...' }]\n// after\ntrustedKeys: [{ keyId: 'facts-2024-01', status: 'active', publicKey: base64(opensslRaw32) }]\nvalidateTrustedKeys(trustedKeys);","handlingStrategy":"validation","validationCode":"import { KEY_ID_RE, strictBase64 } from './facts-publish.mjs';\nconst ids = new Set();\nfor (const k of keys) {\n  if (!KEY_ID_RE.test(k.keyId) || ids.has(k.keyId) || !['active','retired'].includes(k.status) || strictBase64(k.publicKey, 32).length !== 32) throw new Error(`bad pinned key: ${JSON.stringify(k.keyId)}`);\n  ids.add(k.keyId);\n}","typeGuard":"const isValidPinnedKey = (k) => KEY_ID_RE.test(k.keyId) && ['active','retired'].includes(k.status) && strictBase64(k.publicKey, 32).length === 32;","tryCatchPattern":"try { validateTrustedKeys(keys); } catch (e) { if (e.message === 'invalid or duplicated pinned key') { console.error('Check each pinned key: id pattern, uniqueness, status, 32-byte base64 public key'); process.exit(2); } throw e; }","preventionTips":["Convert public keys to raw 32-byte base64 via openssl before pinning","Keep statuses lowercase: 'active' or 'retired' only","Deduplicate key ids when merging rotation branches","Run validateTrustedKeys in CI against the checked-in key table"],"tags":["validation","pinned-keys","trust"],"backgroundTag":"schema-validation-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}