From e091bb3fd1b30fcd1ef536526014c063d7b0a46f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 14:40:07 +0200 Subject: [PATCH] Signing key path: absolute only, and no error carries the path Soul S3: GC_ACCESS_SIGNING_PRIVATE_KEY_PATH is now the only way the signing key gets in, and its errors printed the path: the missing-file message interpolated it, and fs errors from the read and the bootstrap mkdir carried it in their message and properties. An operator moving off the deleted inline PEM input who binds the key text to _PATH would have the private key written to the journal. The path now takes the secret reader's rules. loadConfig refuses a relative value, which PEM text always is. Every signing-key failure is a fresh error naming only the variable and a code, with no cause and no properties. Co-Authored-By: Claude Opus 5.5 --- src/config.ts | 12 ++++++++++-- src/signing.ts | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/config.ts b/src/config.ts index c31e0e8..39ab96f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -332,8 +332,16 @@ export function loadConfig( config.odinCultMeshUri = odinCultMeshUri; } - if (env.GC_ACCESS_SIGNING_PRIVATE_KEY_PATH) { - config.signingPrivateKeyPath = env.GC_ACCESS_SIGNING_PRIVATE_KEY_PATH; + // The signing key path follows the secret reader's rules: it must be + // absolute, and no error repeats it. An operator moving off the deleted + // inline PEM input who binds the key text here would otherwise have the + // private key printed back into the journal. + const signingPrivateKeyPath = env.GC_ACCESS_SIGNING_PRIVATE_KEY_PATH; + if (signingPrivateKeyPath) { + if (!path.isAbsolute(signingPrivateKeyPath)) { + throw new Error("GC_ACCESS_SIGNING_PRIVATE_KEY_PATH is not an absolute path."); + } + config.signingPrivateKeyPath = signingPrivateKeyPath; } if (env.GC_ACCESS_SIGNING_KEY_ID) { diff --git a/src/signing.ts b/src/signing.ts index 1a41a97..b7d72aa 100644 --- a/src/signing.ts +++ b/src/signing.ts @@ -56,8 +56,30 @@ function exportPrivateKeyPem(privateKey: KeyObject): string { return typeof exported === "string" ? exported : exported.toString("utf8"); } +const SIGNING_KEY_VARIABLE = "GC_ACCESS_SIGNING_PRIVATE_KEY_PATH"; + +/** + * Every signing-key failure names only the variable and an error code. It + * carries no path, no file contents and no cause, because Node prints an + * error's cause and properties along with its message. + */ +function signingKeyError(failure: string, error: unknown): Error { + const code = (error as NodeJS.ErrnoException | undefined)?.code ?? "error"; + return new Error(`${SIGNING_KEY_VARIABLE} ${failure} (${code}).`); +} + function loadPrivateKeyFromPath(path: string): KeyObject { - return createPrivateKey(readFileSync(path, "utf8")); + let pem: string; + try { + pem = readFileSync(path, "utf8"); + } catch (error) { + throw signingKeyError("could not be read", error); + } + try { + return createPrivateKey(pem); + } catch (error) { + throw signingKeyError("does not hold a usable private key", error); + } } function createOrLoadPrivateKeyFromPath(config: HeimdallConfig): { @@ -81,12 +103,16 @@ function createOrLoadPrivateKeyFromPath(config: HeimdallConfig): { if (!config.bootstrapSigningPrivateKeyOnMissing) { throw new Error( - `Signing key file '${path}' does not exist. Set GC_ACCESS_SIGNING_PRIVATE_KEY_BOOTSTRAP=1 to create it on first boot.` + `${SIGNING_KEY_VARIABLE} names no file (ENOENT). Set GC_ACCESS_SIGNING_PRIVATE_KEY_BOOTSTRAP=1 to create it on first boot.` ); } const privateKey = generateKeyPairSync("ed25519").privateKey; - mkdirSync(dirname(path), { recursive: true }); + try { + mkdirSync(dirname(path), { recursive: true }); + } catch (error) { + throw signingKeyError("could not be created", error); + } try { writeFileSync(path, exportPrivateKeyPem(privateKey), { encoding: "utf8", @@ -99,7 +125,7 @@ function createOrLoadPrivateKeyFromPath(config: HeimdallConfig): { }; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") { - throw error; + throw signingKeyError("could not be created", error); } return {