mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 21:26:33 +00:00
fix(firmware): tolerate mangled PEM in BF_FIRMWARE_SIGNING_KEY env
Coolify / docker compose env injection routinely strips real newlines or wraps in quotes, causing createPrivateKey to throw ERR_OSSL_UNSUPPORTED and crashing the server before it can even start. tryParsePrivateKey now attempts: literal, \n→LF, CRLF→LF, quote-stripped, base64-decoded, and single-line PEM re-wrapped to 64-col. On total failure, logs a clear warning and falls back to on-disk / generated key instead of crashing.
This commit is contained in:
parent
d242f0eb12
commit
8082571b03
1 changed files with 58 additions and 4 deletions
|
|
@ -139,12 +139,23 @@ function loadOrCreateKeyPair(
|
||||||
log: FirmwareLog,
|
log: FirmwareLog,
|
||||||
): FirmwareKeyPair {
|
): FirmwareKeyPair {
|
||||||
// Env override for cloud / k8s — full private key PEM in a single var.
|
// Env override for cloud / k8s — full private key PEM in a single var.
|
||||||
|
// Coolify / shell env vars frequently mangle newlines (escaped `\n` instead
|
||||||
|
// of real LF, CRLF, or wrapping quotes). Try multiple normalisations before
|
||||||
|
// giving up and falling through to the on-disk / generated path.
|
||||||
const envKey = process.env["BF_FIRMWARE_SIGNING_KEY"];
|
const envKey = process.env["BF_FIRMWARE_SIGNING_KEY"];
|
||||||
if (envKey && envKey.trim().length > 0) {
|
if (envKey && envKey.trim().length > 0) {
|
||||||
const priv = createPrivateKey({ key: envKey, format: "pem" });
|
const parsed = tryParsePrivateKey(envKey);
|
||||||
const pub = createPublicKey(priv).export({ format: "pem", type: "spki" });
|
if (parsed) {
|
||||||
log.info("firmware: signing key loaded from BF_FIRMWARE_SIGNING_KEY env");
|
const pub = createPublicKey(parsed).export({ format: "pem", type: "spki" });
|
||||||
return { privateKey: priv, publicKeyPem: String(pub) };
|
log.info("firmware: signing key loaded from BF_FIRMWARE_SIGNING_KEY env");
|
||||||
|
return { privateKey: parsed, publicKeyPem: String(pub) };
|
||||||
|
}
|
||||||
|
log.warn(
|
||||||
|
"firmware: BF_FIRMWARE_SIGNING_KEY set but could not be parsed as PEM " +
|
||||||
|
"(checked literal, unescaped \\n, CRLF→LF, base64-wrapped). Falling " +
|
||||||
|
"back to on-disk key / fresh generation. Existing kiosks will need a " +
|
||||||
|
"re-pair if the public key changes.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsSync(privPath) && existsSync(pubPath)) {
|
if (existsSync(privPath) && existsSync(pubPath)) {
|
||||||
|
|
@ -163,6 +174,49 @@ function loadOrCreateKeyPair(
|
||||||
return { privateKey, publicKeyPem: pubPem };
|
return { privateKey, publicKeyPem: pubPem };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try several normalisations of an env-supplied PEM string. Coolify / docker
|
||||||
|
* compose env passing routinely strips real newlines or wraps in quotes.
|
||||||
|
*/
|
||||||
|
function tryParsePrivateKey(raw: string): KeyObject | null {
|
||||||
|
const candidates: string[] = [];
|
||||||
|
candidates.push(raw);
|
||||||
|
// \n / \r\n escape sequences → real newlines.
|
||||||
|
if (raw.includes("\\n")) {
|
||||||
|
candidates.push(raw.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n"));
|
||||||
|
}
|
||||||
|
// CRLF → LF
|
||||||
|
if (raw.includes("\r")) candidates.push(raw.replace(/\r\n?/g, "\n"));
|
||||||
|
// Strip surrounding single / double quotes.
|
||||||
|
const m = raw.match(/^["'](.*)["']$/s);
|
||||||
|
if (m && m[1]) candidates.push(m[1]);
|
||||||
|
// Base64-encoded entire PEM (some platforms recommend this for safety).
|
||||||
|
if (/^[A-Za-z0-9+/=\s]+$/.test(raw.trim())) {
|
||||||
|
try {
|
||||||
|
const decoded = Buffer.from(raw.trim(), "base64").toString("utf8");
|
||||||
|
if (decoded.includes("BEGIN")) candidates.push(decoded);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
// Recover from "BEGIN PRIVATE KEY-----<body>-----END PRIVATE KEY" with no
|
||||||
|
// internal line breaks: re-inject 64-char-wide line breaks around the body.
|
||||||
|
if (/-----BEGIN [^-]+-----.*-----END [^-]+-----/.test(raw) && !raw.includes("\n")) {
|
||||||
|
const pemMatch = raw.match(/-----BEGIN ([^-]+)-----(.*)-----END \1-----/s);
|
||||||
|
if (pemMatch) {
|
||||||
|
const header = pemMatch[1]!;
|
||||||
|
const body = pemMatch[2]!.replace(/\s+/g, "");
|
||||||
|
const wrapped = body.match(/.{1,64}/g)?.join("\n") ?? body;
|
||||||
|
candidates.push(`-----BEGIN ${header}-----\n${wrapped}\n-----END ${header}-----\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of candidates) {
|
||||||
|
try {
|
||||||
|
return createPrivateKey({ key: c, format: "pem" });
|
||||||
|
} catch { /* try next */ }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standalone verifier used by anyone with just the public key (kiosk-side
|
* Standalone verifier used by anyone with just the public key (kiosk-side
|
||||||
* equivalent of `verifyBlob` lives in Rust — this is for server-side checks
|
* equivalent of `verifyBlob` lives in Rust — this is for server-side checks
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue