fix(ssh): normalize PKCS8 private keys before connecting (#1570)

This commit is contained in:
ZacharyZcR authored and GitHub committed 2026-10-07 16:37:50 -05:00
1 parent f4164a74b2
commit 9c31dd41c7
4 files changed
+182 -1

No files matched your search

@@ -1,3 +1,6 @@
import { generateKeyPairSync } from "node:crypto";
import ssh2 from "ssh2";
import { parsePrivateKey } from "sshpk";
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import {
@@ -158,3 +161,64 @@ describe("validateKeyPair", () => {
expect(result.error).toMatch(/private key/i);
});
});
describe("PKCS#8 runtime key normalization", () => {
it.each(["ed25519", "rsa", "ec"] as const)(
"converts %s without changing its public key",
(type) => {
const { privateKey, publicKey } =
type === "rsa"
? generateKeyPairSync("rsa", { modulusLength: 2048 })
: type === "ec"
? generateKeyPairSync("ec", { namedCurve: "prime256v1" })
: generateKeyPairSync("ed25519");
const pem = privateKey
.export({ type: "pkcs8", format: "pem" })
.toString();
const prepared = preparePrivateKeyForSSH2(pem);
const parsed = ssh2.utils.parseKey(prepared);
expect(parsed).not.toBeInstanceOf(Error);
if (parsed instanceof Error) throw parsed;
expect(
parsePrivateKey(prepared).toPublic().toBuffer("pkcs8").toString(),
).toBe(publicKey.export({ type: "spki", format: "pem" }).toString());
},
);
it("decrypts encrypted PKCS#8 only with the correct passphrase", () => {
const { privateKey } = generateKeyPairSync("ed25519");
const pem = privateKey
.export({
type: "pkcs8",
format: "pem",
cipher: "aes-256-cbc",
passphrase: " key passphrase ",
})
.toString();
expect(
ssh2.utils.parseKey(preparePrivateKeyForSSH2(pem, " key passphrase ")),
).not.toBeInstanceOf(Error);
expect(() => preparePrivateKeyForSSH2(pem)).toThrow();
expect(() => preparePrivateKeyForSSH2(pem, "wrong")).toThrow();
});
it("keeps an already usable OpenSSH key unchanged", () => {
expect(preparePrivateKeyForSSH2(ED25519_PRIVATE).toString()).toBe(
ED25519_PRIVATE,
);
});
it("rejects unsupported X25519 keys", () => {
const { privateKey } = generateKeyPairSync("x25519");
const pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
expect(() => preparePrivateKeyForSSH2(pem)).toThrow();
});
it("rejects malformed PKCS#8 instead of guessing from its header", () => {
expect(() =>
preparePrivateKeyForSSH2(
"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----",
),
).toThrow();
});
});