Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
159 lines
5.7 KiB
JavaScript
159 lines
5.7 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const crypto = require("node:crypto");
|
|
const { utils: sshUtils } = require("ssh2");
|
|
|
|
const {
|
|
normalizePrivateKeyForSsh2,
|
|
PrivateKeyPassphraseError,
|
|
UnsupportedPrivateKeyError,
|
|
hasPrivateKeyMaterial,
|
|
} = require("./privateKeyNormalizer.cjs");
|
|
|
|
function genRsa(encoding) {
|
|
return crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
privateKeyEncoding: encoding,
|
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
}).privateKey;
|
|
}
|
|
|
|
const rsaPkcs8 = () => genRsa({ type: "pkcs8", format: "pem" });
|
|
const rsaPkcs1 = () => genRsa({ type: "pkcs1", format: "pem" });
|
|
const encryptedRsaPkcs8 = (passphrase) =>
|
|
genRsa({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase });
|
|
const ecPkcs8 = () =>
|
|
crypto.generateKeyPairSync("ec", {
|
|
namedCurve: "prime256v1",
|
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
}).privateKey;
|
|
const ed25519Pkcs8 = () =>
|
|
crypto.generateKeyPairSync("ed25519", {
|
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
}).privateKey;
|
|
|
|
function parseOk(key) {
|
|
const r = sshUtils.parseKey(key);
|
|
return r && !(r instanceof Error) ? r : null;
|
|
}
|
|
|
|
test("checks private material on ssh2 parser objects and arrays", () => {
|
|
const privateKey = {
|
|
isPrivateKey: () => true,
|
|
getPrivatePEM: () => "private",
|
|
};
|
|
const publicKey = {
|
|
isPrivateKey: () => false,
|
|
getPrivatePEM: () => null,
|
|
};
|
|
|
|
assert.equal(hasPrivateKeyMaterial(privateKey), true);
|
|
assert.equal(hasPrivateKeyMaterial(publicKey), false);
|
|
assert.equal(hasPrivateKeyMaterial([publicKey, privateKey]), true);
|
|
assert.equal(hasPrivateKeyMaterial([publicKey]), false);
|
|
});
|
|
|
|
test("converts unencrypted RSA PKCS#8 into an ssh2-parseable key", () => {
|
|
const result = normalizePrivateKeyForSsh2(rsaPkcs8());
|
|
assert.equal(result.converted, true);
|
|
const parsed = parseOk(result.privateKey);
|
|
assert.ok(parsed, "converted key should be parseable by ssh2");
|
|
assert.equal(parsed.type, "ssh-rsa");
|
|
});
|
|
|
|
test("converts unencrypted EC PKCS#8 into an ssh2-parseable key", () => {
|
|
const result = normalizePrivateKeyForSsh2(ecPkcs8());
|
|
assert.equal(result.converted, true);
|
|
const parsed = parseOk(result.privateKey);
|
|
assert.ok(parsed);
|
|
assert.equal(parsed.type, "ecdsa-sha2-nistp256");
|
|
});
|
|
|
|
test("leaves an already-supported PKCS#1 key untouched", () => {
|
|
const key = rsaPkcs1();
|
|
const result = normalizePrivateKeyForSsh2(key);
|
|
assert.equal(result.converted, false);
|
|
assert.equal(result.privateKey, key);
|
|
});
|
|
|
|
test("decrypts and converts an encrypted RSA PKCS#8 key with the correct passphrase", () => {
|
|
const result = normalizePrivateKeyForSsh2(encryptedRsaPkcs8("secret"), "secret");
|
|
assert.equal(result.converted, true);
|
|
assert.equal(result.passphrase, undefined);
|
|
const parsed = parseOk(result.privateKey);
|
|
assert.ok(parsed);
|
|
assert.equal(parsed.type, "ssh-rsa");
|
|
});
|
|
|
|
test("throws PrivateKeyPassphraseError for encrypted PKCS#8 with a wrong passphrase", () => {
|
|
assert.throws(
|
|
() => normalizePrivateKeyForSsh2(encryptedRsaPkcs8("secret"), "wrong"),
|
|
(err) => err instanceof PrivateKeyPassphraseError,
|
|
);
|
|
});
|
|
|
|
test("throws UnsupportedPrivateKeyError for Ed25519 PKCS#8 with a conversion hint", () => {
|
|
assert.throws(
|
|
() => normalizePrivateKeyForSsh2(ed25519Pkcs8()),
|
|
(err) => err instanceof UnsupportedPrivateKeyError && /ssh-keygen/.test(err.message),
|
|
);
|
|
});
|
|
|
|
test("passes through content that is not a PKCS#8 key", () => {
|
|
const junk = "not a private key";
|
|
const result = normalizePrivateKeyForSsh2(junk);
|
|
assert.equal(result.converted, false);
|
|
assert.equal(result.privateKey, junk);
|
|
});
|
|
|
|
const indentLines = (key) =>
|
|
key.split("\n").map((line) => (line ? " " + line : line)).join("\n");
|
|
const dropEndLine = (key) => key.split("\n").slice(0, -2).join("\n");
|
|
const legacyEncryptedRsa = (passphrase) =>
|
|
crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
privateKeyEncoding: { type: "pkcs1", format: "pem", cipher: "aes-128-cbc", passphrase },
|
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
}).privateKey;
|
|
|
|
test("repairs an RSA key whose newlines were collapsed to spaces", () => {
|
|
const result = normalizePrivateKeyForSsh2(rsaPkcs1().replace(/\n/g, " "));
|
|
assert.equal(result.converted, true);
|
|
assert.ok(parseOk(result.privateKey), "repaired key should be parseable by ssh2");
|
|
});
|
|
|
|
test("repairs an RSA key whose newlines became literal backslash-n", () => {
|
|
const result = normalizePrivateKeyForSsh2(rsaPkcs1().replace(/\n/g, "\\n"));
|
|
assert.equal(result.converted, true);
|
|
assert.ok(parseOk(result.privateKey));
|
|
});
|
|
|
|
test("repairs an RSA key whose lines are indented", () => {
|
|
const result = normalizePrivateKeyForSsh2(indentLines(rsaPkcs1()));
|
|
assert.equal(result.converted, true);
|
|
assert.ok(parseOk(result.privateKey));
|
|
});
|
|
|
|
test("repairs and converts a collapsed PKCS#8 key", () => {
|
|
const result = normalizePrivateKeyForSsh2(rsaPkcs8().replace(/\n/g, " "));
|
|
assert.equal(result.converted, true);
|
|
const parsed = parseOk(result.privateKey);
|
|
assert.ok(parsed);
|
|
assert.equal(parsed.type, "ssh-rsa");
|
|
});
|
|
|
|
test("cannot repair a truncated key and leaves it unchanged", () => {
|
|
const truncated = dropEndLine(rsaPkcs1());
|
|
const result = normalizePrivateKeyForSsh2(truncated);
|
|
assert.equal(result.converted, false);
|
|
assert.equal(result.privateKey, truncated);
|
|
});
|
|
|
|
test("does not attempt to repair an encrypted legacy PEM (DEK-Info)", () => {
|
|
const collapsed = legacyEncryptedRsa("secret").replace(/\n/g, " ");
|
|
const result = normalizePrivateKeyForSsh2(collapsed, "secret");
|
|
assert.equal(result.converted, false);
|
|
});
|