mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
fix(ssh): normalize PKCS8 private keys before connecting (#1570)
This commit is contained in:
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();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createPrivateKey } from "node:crypto";
|
||||
import { parsePrivateKey } from "sshpk";
|
||||
import { getErrorMessage } from "./error-message.js";
|
||||
import ssh2Pkg from "ssh2";
|
||||
const ssh2Utils = ssh2Pkg.utils;
|
||||
@@ -246,7 +248,26 @@ export function preparePrivateKeyForSSH2(
|
||||
privateKeyData: string,
|
||||
passphrase?: string,
|
||||
): Buffer {
|
||||
const cleanKey = normalizePrivateKeyText(privateKeyData);
|
||||
let cleanKey = normalizePrivateKeyText(privateKeyData);
|
||||
if (
|
||||
/^-----BEGIN (?:ENCRYPTED )?PRIVATE KEY-----/.test(cleanKey) &&
|
||||
ssh2Utils.parseKey(cleanKey, passphrase) instanceof Error
|
||||
) {
|
||||
try {
|
||||
const pem = createPrivateKey({ key: cleanKey, format: "pem", passphrase })
|
||||
.export({ type: "pkcs8", format: "pem" })
|
||||
.toString();
|
||||
cleanKey = parsePrivateKey(pem, "pkcs8").toString("openssh");
|
||||
} catch {
|
||||
throw new Error(
|
||||
cleanKey.startsWith("-----BEGIN ENCRYPTED PRIVATE KEY-----")
|
||||
? "Could not decrypt PKCS#8 private key; check the passphrase"
|
||||
: "Invalid or unsupported PKCS#8 private key",
|
||||
);
|
||||
}
|
||||
const parsed = ssh2Utils.parseKey(cleanKey);
|
||||
if (parsed instanceof Error) throw parsed;
|
||||
}
|
||||
const keyInfo = parseSSHKey(cleanKey, passphrase);
|
||||
|
||||
if (!keyInfo.success) {
|
||||
|
||||
Reference in new issue
Block a user