Hmbown/CodeWhale · error · Error
signing key must be Ed25519
Error message
signing key must be Ed25519
What it means
loadPrivateKeyFromEnv parses the PEM with createPrivateKey and then asserts the key type is Ed25519; any other algorithm (RSA, EC, etc.) throws this error. The signing scheme is fixed to Ed25519, so verifiers will only accept Ed25519 signatures.
Solutions
- Generate an Ed25519 key: openssl genpkey -algorithm ed25519 -out key.pem
- Point CODEWHALE_FACTS_SIGNING_KEY_FILE at the new private key
- Verify the type: openssl pkey -in key.pem -text -noout | head -1
- Publish/rotate the corresponding public key into the trusted-keys pin list
Example fix
// before export CODEWHALE_FACTS_SIGNING_KEY="$(cat rsa-key.pem)" # RSA // after openssl genpkey -algorithm ed25519 -out ed25519.pem export CODEWHALE_FACTS_SIGNING_KEY="$(cat ed25519.pem)"
Defensive patterns
Strategy: validation
Validate before calling
import { createPrivateKey } from 'node:crypto';
const key = createPrivateKey({ key: pem, format: 'pem' });
if (key.asymmetricKeyType !== 'ed25519') throw new Error(`need Ed25519 private key, got ${key.asymmetricKeyType}`); Type guard
const isEd25519Pem = (s) => { try { return createPrivateKey({ key: s, format: 'pem' }).asymmetricKeyType === 'ed25519'; } catch { return false; } }; Try / catch
try { key = loadPrivateKeyFromEnv(); } catch (e) { if (e.message === 'signing key must be Ed25519') { console.error('Generate an Ed25519 key: openssl genpkey -algorithm ed25519'); process.exit(2); } throw e; } Prevention
- Generate keys explicitly with -algorithm ed25519
- Never point the env/file at certificates or public keys
- Verify key type after any rotation before publishing
- Standardize on a key-generation script so algorithm drift cannot happen
When it happens
Trigger: Setting CODEWHALE_FACTS_SIGNING_KEY(_FILE) to an RSA, EC, or PKCS8-encrypted PEM; the file/env containing a public key or certificate instead of a private key where asymmetricKeyType resolves to something else.
Common situations: Reusing an existing RSA/EC key pair instead of generating an Ed25519 one; pointing at a .pub file or a cert chain; older automation generating RSA keys by default.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- set CODEWHALE_FACTS_SIGNING_KEY (PEM) or…
- signing key exceeds size limit
- 127
- A pinned task provider requires an explicit model
- agent profile reasoning_effort must be one of: inherit…
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/8d7e3b8d239d8748.
Report an issue: GitHub.
Appendix: source
Thrown at web/scripts/facts-publish.mjs:369
while (size <= maxBytes) {
const count = readSync(fd, bytes, size, maxBytes + 1 - size, null);
if (!count) break;
size += count;
}
if (size > maxBytes) throw new Error("file exceeds size limit");
return bytes.subarray(0, size);
} 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];View on GitHub (pinned to 433685b202)