Skip to content

Commit 6e896a0

Browse files
authored
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.
1 parent a017b83 commit 6e896a0

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

src/github/operations/git-config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,13 @@ export async function setupSshSigning(sshSigningKey: string): Promise<void> {
8282
const sshDir = join(homedir(), ".ssh");
8383
await mkdir(sshDir, { recursive: true, mode: 0o700 });
8484

85+
// Ensure key ends with newline (required for ssh-keygen to parse it)
86+
const normalizedKey = sshSigningKey.endsWith("\n")
87+
? sshSigningKey
88+
: sshSigningKey + "\n";
89+
8590
// Write the signing key atomically with secure permissions (600)
86-
await writeFile(SSH_SIGNING_KEY_PATH, sshSigningKey, { mode: 0o600 });
91+
await writeFile(SSH_SIGNING_KEY_PATH, normalizedKey, { mode: 0o600 });
8792
console.log(`✓ SSH signing key written to ${SSH_SIGNING_KEY_PATH}`);
8893

8994
// Configure git to use SSH signing

test/ssh-signing.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,47 @@ describe("SSH Signing", () => {
5555
expect(permissions).toBe(0o600);
5656
});
5757

58+
test("should normalize key to have trailing newline", async () => {
59+
// ssh-keygen requires a trailing newline to parse the key
60+
const keyWithoutNewline =
61+
"-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key-content\n-----END OPENSSH PRIVATE KEY-----";
62+
const keyWithNewline = keyWithoutNewline + "\n";
63+
64+
// Create directory
65+
await mkdir(testSshDir, { recursive: true, mode: 0o700 });
66+
67+
// Normalize the key (same logic as setupSshSigning)
68+
const normalizedKey = keyWithoutNewline.endsWith("\n")
69+
? keyWithoutNewline
70+
: keyWithoutNewline + "\n";
71+
72+
await writeFile(testKeyPath, normalizedKey, { mode: 0o600 });
73+
74+
// Verify the written key ends with newline
75+
const keyContent = await readFile(testKeyPath, "utf-8");
76+
expect(keyContent).toBe(keyWithNewline);
77+
expect(keyContent.endsWith("\n")).toBe(true);
78+
});
79+
80+
test("should not add extra newline if key already has one", async () => {
81+
const keyWithNewline =
82+
"-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key-content\n-----END OPENSSH PRIVATE KEY-----\n";
83+
84+
await mkdir(testSshDir, { recursive: true, mode: 0o700 });
85+
86+
// Normalize the key (same logic as setupSshSigning)
87+
const normalizedKey = keyWithNewline.endsWith("\n")
88+
? keyWithNewline
89+
: keyWithNewline + "\n";
90+
91+
await writeFile(testKeyPath, normalizedKey, { mode: 0o600 });
92+
93+
// Verify no double newline
94+
const keyContent = await readFile(testKeyPath, "utf-8");
95+
expect(keyContent).toBe(keyWithNewline);
96+
expect(keyContent.endsWith("\n\n")).toBe(false);
97+
});
98+
5899
test("should create .ssh directory with secure permissions", async () => {
59100
// Clean up first
60101
await rm(testSshDir, { recursive: true, force: true });

0 commit comments

Comments
 (0)