From 6e896a06bb3689a3991aa952797ad6e8c67338a2 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Fri, 16 Jan 2026 14:44:22 -0800 Subject: [PATCH] fix: ensure SSH signing key has trailing newline (#834) ssh-keygen requires a trailing newline to parse private keys correctly. Without it, git signing fails with the confusing error: 'Couldn't load public key: No such file or directory?' This normalizes the key to always end with a newline before writing. --- src/github/operations/git-config.ts | 7 ++++- test/ssh-signing.test.ts | 41 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/github/operations/git-config.ts b/src/github/operations/git-config.ts index 733744f..97e02be 100644 --- a/src/github/operations/git-config.ts +++ b/src/github/operations/git-config.ts @@ -82,8 +82,13 @@ export async function setupSshSigning(sshSigningKey: string): Promise { const sshDir = join(homedir(), ".ssh"); await mkdir(sshDir, { recursive: true, mode: 0o700 }); + // Ensure key ends with newline (required for ssh-keygen to parse it) + const normalizedKey = sshSigningKey.endsWith("\n") + ? sshSigningKey + : sshSigningKey + "\n"; + // Write the signing key atomically with secure permissions (600) - await writeFile(SSH_SIGNING_KEY_PATH, sshSigningKey, { mode: 0o600 }); + await writeFile(SSH_SIGNING_KEY_PATH, normalizedKey, { mode: 0o600 }); console.log(`✓ SSH signing key written to ${SSH_SIGNING_KEY_PATH}`); // Configure git to use SSH signing diff --git a/test/ssh-signing.test.ts b/test/ssh-signing.test.ts index ffb02ae..eb4b24c 100644 --- a/test/ssh-signing.test.ts +++ b/test/ssh-signing.test.ts @@ -55,6 +55,47 @@ describe("SSH Signing", () => { expect(permissions).toBe(0o600); }); + test("should normalize key to have trailing newline", async () => { + // ssh-keygen requires a trailing newline to parse the key + const keyWithoutNewline = + "-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key-content\n-----END OPENSSH PRIVATE KEY-----"; + const keyWithNewline = keyWithoutNewline + "\n"; + + // Create directory + await mkdir(testSshDir, { recursive: true, mode: 0o700 }); + + // Normalize the key (same logic as setupSshSigning) + const normalizedKey = keyWithoutNewline.endsWith("\n") + ? keyWithoutNewline + : keyWithoutNewline + "\n"; + + await writeFile(testKeyPath, normalizedKey, { mode: 0o600 }); + + // Verify the written key ends with newline + const keyContent = await readFile(testKeyPath, "utf-8"); + expect(keyContent).toBe(keyWithNewline); + expect(keyContent.endsWith("\n")).toBe(true); + }); + + test("should not add extra newline if key already has one", async () => { + const keyWithNewline = + "-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key-content\n-----END OPENSSH PRIVATE KEY-----\n"; + + await mkdir(testSshDir, { recursive: true, mode: 0o700 }); + + // Normalize the key (same logic as setupSshSigning) + const normalizedKey = keyWithNewline.endsWith("\n") + ? keyWithNewline + : keyWithNewline + "\n"; + + await writeFile(testKeyPath, normalizedKey, { mode: 0o600 }); + + // Verify no double newline + const keyContent = await readFile(testKeyPath, "utf-8"); + expect(keyContent).toBe(keyWithNewline); + expect(keyContent.endsWith("\n\n")).toBe(false); + }); + test("should create .ssh directory with secure permissions", async () => { // Clean up first await rm(testSshDir, { recursive: true, force: true });