Hmbown/CodeWhale · error · Error
invalid or duplicated pinned key
Error message
invalid or duplicated pinned key
What it means
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.
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)
Example fix
// before
trustedKeys: [{ keyId: 'Key 1', status: 'ACTIVE', publicKey: 'AABBCC...' }]
// after
trustedKeys: [{ keyId: 'facts-2024-01', status: 'active', publicKey: base64(opensslRaw32) }]
validateTrustedKeys(trustedKeys); Defensive patterns
Strategy: validation
Validate before calling
import { KEY_ID_RE, strictBase64 } from './facts-publish.mjs';
const ids = new Set();
for (const k of keys) {
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)}`);
ids.add(k.keyId);
} Type guard
const isValidPinnedKey = (k) => KEY_ID_RE.test(k.keyId) && ['active','retired'].includes(k.status) && strictBase64(k.publicKey, 32).length === 32;
Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- only Codewhale managed skills can be trusted
- Source is disabled or its workspace is untrusted
- 1
- A pinned task provider requires an explicit model
- A positive pull request number is required
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/731972183eec1e8c.
Report an issue: GitHub.
Appendix: source
Thrown at web/scripts/facts-publish.mjs:376
} finally { closeSync(fd); }
}
function loadPrivateKeyFromEnv() {
refuseUnderCi();
let pem = process.env.CODEWHALE_FACTS_SIGNING_KEY;
const file = process.env.CODEWHALE_FACTS_SIGNING_KEY_FILE;
if (!pem && file) pem = readBoundedFile(file, 16 * 1024).toString("utf8");
if (!pem) throw new Error("set CODEWHALE_FACTS_SIGNING_KEY (PEM) or CODEWHALE_FACTS_SIGNING_KEY_FILE");
if (Buffer.byteLength(pem) > 16 * 1024) throw new Error("signing key exceeds size limit");
const key = createPrivateKey({ key: pem, format: "pem" });
if (key.asymmetricKeyType !== "ed25519") throw new Error("signing key must be Ed25519");
return key;
}
export function validateTrustedKeys(keys) {
const seen = new Set();
for (const key of keys) {
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");
seen.add(key.keyId);
}
return keys;
}
/** Deliberately narrow syntax: a changed/unparseable table must fail the gate. */
export function parseTsKeys(text) {
const source = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
const tables = [...source.matchAll(/^\s*export\s+const\s+TRUSTED_KEYS\s*:\s*readonly\s+TrustedKey\[\]\s*=\s*\[([\s\S]*?)\]\s*;/gm)];
if (tables.length !== 1) throw new Error("cannot parse exactly one TypeScript TRUSTED_KEYS table");
const table = tables[0];
const body = table[1].replace(/^\s*\/\/.*$/gm, "");
const keys = [];
const remainder = body.replace(/\{\s*keyId:\s*"([^"]+)",\s*publicKey:\s*"([^"]+)",\s*status:\s*"([^"]+)"\s*,?\s*\}/g, (_, keyId, publicKey, status) => {
keys.push({ keyId, publicKey, status });
return "";
});
if (remainder.replace(/[\s,]/g, "")) throw new Error("unparsed TypeScript TRUSTED_KEYS entry");View on GitHub (pinned to 433685b202)