515 lines
17 KiB
JavaScript
515 lines
17 KiB
JavaScript
|
|
const test = require("node:test");
|
||
|
|
const assert = require("node:assert/strict");
|
||
|
|
const fs = require("node:fs");
|
||
|
|
const os = require("node:os");
|
||
|
|
const path = require("node:path");
|
||
|
|
const crypto = require("node:crypto");
|
||
|
|
|
||
|
|
const {
|
||
|
|
buildAuthoritativeKnownHostsContent,
|
||
|
|
buildExternalHostKeyConfigLines,
|
||
|
|
buildExternalHostKeySshOptions,
|
||
|
|
buildVaultKnownHostsContent,
|
||
|
|
filterKnownHostsContentExcludingVaultHosts,
|
||
|
|
formatVaultKnownHostLine,
|
||
|
|
getDefaultGlobalKnownHostsPaths,
|
||
|
|
quoteOpenSshOptionValue,
|
||
|
|
resolveExternalStrictHostKeyChecking,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
|
||
|
|
test("formatVaultKnownHostLine builds OpenSSH known_hosts lines", () => {
|
||
|
|
const validBlob = "AAAAC3NzaC1lZDI1NTE5AAAAIAcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH";
|
||
|
|
assert.equal(
|
||
|
|
formatVaultKnownHostLine({
|
||
|
|
hostname: "host.example",
|
||
|
|
port: 22,
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: `ssh-ed25519 ${validBlob}`,
|
||
|
|
}),
|
||
|
|
`host.example ssh-ed25519 ${validBlob}`,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
formatVaultKnownHostLine({
|
||
|
|
hostname: "host.example",
|
||
|
|
port: 2222,
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: validBlob,
|
||
|
|
}),
|
||
|
|
`[host.example]:2222 ssh-ed25519 ${validBlob}`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("formatVaultKnownHostLine skips fingerprint-only vault entries", () => {
|
||
|
|
assert.equal(
|
||
|
|
formatVaultKnownHostLine({
|
||
|
|
hostname: "host.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "SHA256:abcdef",
|
||
|
|
fingerprint: "abcdef",
|
||
|
|
}),
|
||
|
|
null,
|
||
|
|
);
|
||
|
|
// Unprefixed fingerprint-like token must not be treated as a key blob.
|
||
|
|
assert.equal(
|
||
|
|
formatVaultKnownHostLine({
|
||
|
|
hostname: "host.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "not-a-real-key-blob",
|
||
|
|
}),
|
||
|
|
null,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildVaultKnownHostsContent joins usable vault entries", () => {
|
||
|
|
const content = buildVaultKnownHostsContent([
|
||
|
|
{
|
||
|
|
hostname: "a.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAAa",
|
||
|
|
},
|
||
|
|
{ hostname: "skip.example", fingerprint: "only" },
|
||
|
|
{
|
||
|
|
hostname: "b.example",
|
||
|
|
port: 2200,
|
||
|
|
keyType: "ssh-rsa",
|
||
|
|
publicKey: "ssh-rsa AAAAb",
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
assert.equal(
|
||
|
|
content,
|
||
|
|
"a.example ssh-ed25519 AAAAa\n[b.example]:2200 ssh-rsa AAAAb\n",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("getDefaultGlobalKnownHostsPaths covers Unix and Windows defaults", () => {
|
||
|
|
assert.deepEqual(getDefaultGlobalKnownHostsPaths({ platform: "linux" }), [
|
||
|
|
"/etc/ssh/ssh_known_hosts",
|
||
|
|
"/etc/ssh/ssh_known_hosts2",
|
||
|
|
]);
|
||
|
|
assert.deepEqual(
|
||
|
|
getDefaultGlobalKnownHostsPaths({ platform: "win32", programData: "C:\\ProgramData" }),
|
||
|
|
[
|
||
|
|
path.join("C:\\ProgramData", "ssh", "ssh_known_hosts"),
|
||
|
|
path.join("C:\\ProgramData", "ssh", "ssh_known_hosts2"),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("filterKnownHostsContentExcludingVaultHosts drops conflicting system pins", () => {
|
||
|
|
const content = [
|
||
|
|
"host.example ssh-ed25519 AAASYSTEM",
|
||
|
|
"other.example ssh-rsa AAAOTHER",
|
||
|
|
"[host.example]:2222 ssh-ed25519 AAAPORT",
|
||
|
|
"# comment kept",
|
||
|
|
].join("\n");
|
||
|
|
const filtered = filterKnownHostsContentExcludingVaultHosts(content, [
|
||
|
|
{ hostname: "host.example", port: 22 },
|
||
|
|
]);
|
||
|
|
assert.doesNotMatch(filtered, /AAASYSTEM/);
|
||
|
|
assert.match(filtered, /other\.example ssh-rsa AAAOTHER/);
|
||
|
|
assert.match(filtered, /\[host\.example\]:2222/);
|
||
|
|
assert.match(filtered, /# comment kept/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("filterKnownHostsContentExcludingVaultHosts rewrites multi-host lines", () => {
|
||
|
|
const content = "jump.example,target.example ssh-ed25519 AAASHARED\n";
|
||
|
|
const filtered = filterKnownHostsContentExcludingVaultHosts(content, [
|
||
|
|
{ hostname: "jump.example", port: 22 },
|
||
|
|
]);
|
||
|
|
// Jump pattern removed; target pattern kept so unpinned target stays trusted.
|
||
|
|
assert.match(filtered, /^target\.example ssh-ed25519 AAASHARED$/m);
|
||
|
|
assert.doesNotMatch(filtered, /jump\.example/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("filterKnownHostsContentExcludingVaultHosts drops wildcards covering vault hosts", () => {
|
||
|
|
const {
|
||
|
|
filterKnownHostsContentExcludingVaultHosts: filter,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const content = [
|
||
|
|
"*.example.com ssh-ed25519 AAAWILD",
|
||
|
|
"db.example.com ssh-rsa AAADB",
|
||
|
|
"unrelated.example.org ssh-ed25519 AAAOK",
|
||
|
|
].join("\n");
|
||
|
|
const filtered = filter(content, [{ hostname: "host.example.com", port: 22 }]);
|
||
|
|
assert.doesNotMatch(filtered, /AAAWILD/);
|
||
|
|
assert.match(filtered, /db\.example\.com/);
|
||
|
|
assert.match(filtered, /AAAOK/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("filterKnownHostsContentExcludingVaultHosts respects negated patterns", () => {
|
||
|
|
const {
|
||
|
|
filterKnownHostsContentExcludingVaultHosts: filter,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
// OpenSSH: target.example.com is excluded by !target; jump still matches.
|
||
|
|
const content = "*.example.com,!target.example.com ssh-ed25519 AAAJUMP\n";
|
||
|
|
const filteredForTarget = filter(content, [{ hostname: "target.example.com", port: 22 }]);
|
||
|
|
// Negation means this line does not pin target → keep it for the jump host.
|
||
|
|
assert.match(filteredForTarget, /AAAJUMP/);
|
||
|
|
const filteredForJump = filter(content, [{ hostname: "jump.example.com", port: 22 }]);
|
||
|
|
// Jump matches the positive wildcard → drop so vault remains authoritative.
|
||
|
|
assert.doesNotMatch(filteredForJump, /AAAJUMP/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("filterKnownHostsContentExcludingVaultHosts keeps @revoked for vault hosts", () => {
|
||
|
|
const {
|
||
|
|
filterKnownHostsContentExcludingVaultHosts: filter,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const content = [
|
||
|
|
"@revoked host.example ssh-ed25519 AAAREVOKED",
|
||
|
|
"host.example ssh-ed25519 AAASYSTEM",
|
||
|
|
"other.example ssh-rsa AAAOTHER",
|
||
|
|
].join("\n");
|
||
|
|
const filtered = filter(content, [{ hostname: "host.example", port: 22 }]);
|
||
|
|
assert.match(filtered, /@revoked host\.example ssh-ed25519 AAAREVOKED/);
|
||
|
|
assert.doesNotMatch(filtered, /AAASYSTEM/);
|
||
|
|
assert.match(filtered, /other\.example/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("vaultPinsConnectionHosts only matches the active connection", () => {
|
||
|
|
const { vaultPinsConnectionHosts } = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const knownHosts = [{
|
||
|
|
hostname: "other.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAA",
|
||
|
|
}];
|
||
|
|
assert.equal(
|
||
|
|
vaultPinsConnectionHosts(knownHosts, [{ hostname: "target.example", port: 22 }]),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
vaultPinsConnectionHosts(knownHosts, [{ hostname: "other.example", port: 22 }]),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("filterKnownHostsContentExcludingVaultHosts matches hashed host entries", () => {
|
||
|
|
const hostname = "hashed.example";
|
||
|
|
const salt = crypto.randomBytes(20);
|
||
|
|
const digest = crypto.createHmac("sha1", salt).update(hostname).digest("base64");
|
||
|
|
const hostField = `|1|${salt.toString("base64")}|${digest}`;
|
||
|
|
const content = `${hostField} ssh-ed25519 AAAHASHED\nother.example ssh-rsa AAAOTHER\n`;
|
||
|
|
const filtered = filterKnownHostsContentExcludingVaultHosts(content, [
|
||
|
|
{ hostname, port: 22 },
|
||
|
|
]);
|
||
|
|
assert.doesNotMatch(filtered, /AAAHASHED/);
|
||
|
|
assert.match(filtered, /other\.example/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildAuthoritativeKnownHostsContent makes vault authoritative over system pins", () => {
|
||
|
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-kh-auth-"));
|
||
|
|
try {
|
||
|
|
const global1 = path.join(base, "ssh_known_hosts");
|
||
|
|
const user1 = path.join(base, "user_known_hosts");
|
||
|
|
fs.writeFileSync(global1, "host.example ssh-ed25519 AAAGLOBAL\nadmin.example ssh-rsa AAAADMIN\n");
|
||
|
|
fs.writeFileSync(user1, "host.example ssh-ed25519 AAAUSER\nother.example ssh-ed25519 AAAOTHER\n");
|
||
|
|
|
||
|
|
const content = buildAuthoritativeKnownHostsContent({
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "host.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAVAULT",
|
||
|
|
}],
|
||
|
|
fs,
|
||
|
|
globalPaths: [global1],
|
||
|
|
userPaths: [user1],
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.match(content, /host\.example ssh-ed25519 AAAVAULT/);
|
||
|
|
assert.doesNotMatch(content, /AAAGLOBAL/);
|
||
|
|
assert.doesNotMatch(content, /AAAUSER/);
|
||
|
|
assert.match(content, /admin\.example ssh-rsa AAAADMIN/);
|
||
|
|
assert.match(content, /other\.example ssh-ed25519 AAAOTHER/);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(base, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildAuthoritativeKnownHostsContent merges ssh -G global known_hosts paths", () => {
|
||
|
|
const {
|
||
|
|
buildAuthoritativeKnownHostsContent: build,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-kh-sshg-"));
|
||
|
|
try {
|
||
|
|
const customGlobal = path.join(base, "custom_global_known_hosts");
|
||
|
|
fs.writeFileSync(customGlobal, "admin.custom ssh-ed25519 AAAADMINCUSTOM\n");
|
||
|
|
const content = build({
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "host.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAVAULT",
|
||
|
|
}],
|
||
|
|
fs,
|
||
|
|
hostname: "host.example",
|
||
|
|
// Force discovery path (no explicit globalPaths).
|
||
|
|
globalPaths: undefined,
|
||
|
|
userPaths: [],
|
||
|
|
execFileSyncFn: () => `globalknownhostsfile ${customGlobal}\n`,
|
||
|
|
});
|
||
|
|
assert.match(content, /host\.example ssh-ed25519 AAAVAULT/);
|
||
|
|
assert.match(content, /admin\.custom ssh-ed25519 AAAADMINCUSTOM/);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(base, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildAuthoritativeKnownHostsContent honors HostKeyAlias for vault pins", () => {
|
||
|
|
const {
|
||
|
|
buildAuthoritativeKnownHostsContent: build,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const content = build({
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "host.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAVAULT",
|
||
|
|
}],
|
||
|
|
fs,
|
||
|
|
hostname: "host.example",
|
||
|
|
port: 22,
|
||
|
|
globalPaths: [],
|
||
|
|
userPaths: [],
|
||
|
|
execFileSyncFn: () => "hostkeyalias shared-key\nhostname host.example\n",
|
||
|
|
});
|
||
|
|
assert.match(content, /^shared-key ssh-ed25519 AAAVAULT$/m);
|
||
|
|
assert.doesNotMatch(content, /^host\.example ssh-ed25519 AAAVAULT$/m);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildAuthoritativeKnownHostsContent rewrites pins under resolved HostName", () => {
|
||
|
|
const {
|
||
|
|
buildAuthoritativeKnownHostsContent: build,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const content = build({
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "prod",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAVAULT",
|
||
|
|
}],
|
||
|
|
fs,
|
||
|
|
hostname: "prod",
|
||
|
|
port: 22,
|
||
|
|
globalPaths: [],
|
||
|
|
userPaths: [],
|
||
|
|
execFileSyncFn: () => "hostname 10.0.0.5\n",
|
||
|
|
});
|
||
|
|
assert.match(content, /^10\.0\.0\.5 ssh-ed25519 AAAVAULT$/m);
|
||
|
|
assert.doesNotMatch(content, /^prod ssh-ed25519 AAAVAULT$/m);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildAuthoritativeKnownHostsContent keeps port form for resolved HostName", () => {
|
||
|
|
const {
|
||
|
|
buildAuthoritativeKnownHostsContent: build,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const content = build({
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "prod",
|
||
|
|
port: 2222,
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAVAULT",
|
||
|
|
}],
|
||
|
|
fs,
|
||
|
|
hostname: "prod",
|
||
|
|
port: 2222,
|
||
|
|
globalPaths: [],
|
||
|
|
userPaths: [],
|
||
|
|
execFileSyncFn: () => "hostname 10.0.0.5\n",
|
||
|
|
});
|
||
|
|
assert.match(content, /^\[10\.0\.0\.5\]:2222 ssh-ed25519 AAAVAULT$/m);
|
||
|
|
assert.doesNotMatch(content, /^10\.0\.0\.5 ssh-ed25519 AAAVAULT$/m);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("runSshG discovery receives port and username", () => {
|
||
|
|
const { buildAuthoritativeKnownHostsContent: build } = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
let capturedArgs = null;
|
||
|
|
build({
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "host.example",
|
||
|
|
port: 2222,
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAVAULT",
|
||
|
|
}],
|
||
|
|
fs,
|
||
|
|
hostname: "host.example",
|
||
|
|
port: 2222,
|
||
|
|
username: "bob",
|
||
|
|
globalPaths: [],
|
||
|
|
userPaths: [],
|
||
|
|
execFileSyncFn: (_cmd, args) => {
|
||
|
|
capturedArgs = args;
|
||
|
|
return "hostkeyalias none\n";
|
||
|
|
},
|
||
|
|
});
|
||
|
|
assert.deepEqual(capturedArgs, ["-G", "-p", "2222", "-l", "bob", "host.example"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("parseSshGKnownHostsPaths reads multi-path directives", () => {
|
||
|
|
const { parseSshGKnownHostsPaths } = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
assert.deepEqual(
|
||
|
|
parseSshGKnownHostsPaths(
|
||
|
|
"globalknownhostsfile /etc/ssh/ssh_known_hosts /etc/custom/known_hosts\nuserknownhostsfile ~/.ssh/known_hosts\n",
|
||
|
|
"globalknownhostsfile",
|
||
|
|
{ homedir: "/home/me", pathModule: path },
|
||
|
|
),
|
||
|
|
["/etc/ssh/ssh_known_hosts", "/etc/custom/known_hosts"],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
parseSshGKnownHostsPaths(
|
||
|
|
"userknownhostsfile ~/.ssh/known_hosts ~/.ssh/known_hosts2\n",
|
||
|
|
"userknownhostsfile",
|
||
|
|
{ homedir: "/home/me", pathModule: path },
|
||
|
|
),
|
||
|
|
[
|
||
|
|
path.join("/home/me", ".ssh", "known_hosts"),
|
||
|
|
path.join("/home/me", ".ssh", "known_hosts2"),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("parseSshGKnownHostsPaths preserves paths that contain spaces", () => {
|
||
|
|
const { parseSshGKnownHostsPaths } = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-kh-space-"));
|
||
|
|
try {
|
||
|
|
const spaced = path.join(base, "dir with space", "global kh");
|
||
|
|
fs.mkdirSync(path.dirname(spaced), { recursive: true });
|
||
|
|
fs.writeFileSync(spaced, "ok\n");
|
||
|
|
assert.deepEqual(
|
||
|
|
parseSshGKnownHostsPaths(
|
||
|
|
`globalknownhostsfile ${spaced}\n`,
|
||
|
|
"globalknownhostsfile",
|
||
|
|
{ fs, pathModule: path },
|
||
|
|
),
|
||
|
|
[spaced],
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(base, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildAuthoritativeKnownHostsContent merges effective user known_hosts paths", () => {
|
||
|
|
const {
|
||
|
|
buildAuthoritativeKnownHostsContent: build,
|
||
|
|
} = require("./externalSshHostKeyPolicy.cjs");
|
||
|
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-kh-user-"));
|
||
|
|
try {
|
||
|
|
const customUser = path.join(base, "custom_user_known_hosts");
|
||
|
|
fs.writeFileSync(customUser, "admin.user ssh-ed25519 AAAUSERCUSTOM\n");
|
||
|
|
const content = build({
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "host.example",
|
||
|
|
keyType: "ssh-ed25519",
|
||
|
|
publicKey: "ssh-ed25519 AAAVAULT",
|
||
|
|
}],
|
||
|
|
fs,
|
||
|
|
hostname: "host.example",
|
||
|
|
globalPaths: [],
|
||
|
|
userPaths: undefined,
|
||
|
|
execFileSyncFn: () => `userknownhostsfile ${customUser}\n`,
|
||
|
|
});
|
||
|
|
assert.match(content, /host\.example ssh-ed25519 AAAVAULT/);
|
||
|
|
assert.match(content, /admin\.user ssh-ed25519 AAAUSERCUSTOM/);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(base, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildAuthoritativeKnownHostsContent is empty when vault has no usable pins", () => {
|
||
|
|
assert.equal(
|
||
|
|
buildAuthoritativeKnownHostsContent({
|
||
|
|
knownHosts: [{ hostname: "x", fingerprint: "only" }],
|
||
|
|
fs,
|
||
|
|
globalPaths: [],
|
||
|
|
userPaths: [],
|
||
|
|
}),
|
||
|
|
"",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveExternalStrictHostKeyChecking matches protocol constraints", () => {
|
||
|
|
assert.equal(resolveExternalStrictHostKeyChecking({ protocol: "et" }), "accept-new");
|
||
|
|
assert.equal(resolveExternalStrictHostKeyChecking({ protocol: "mosh" }), "ask");
|
||
|
|
assert.equal(
|
||
|
|
resolveExternalStrictHostKeyChecking({ protocol: "et", verifyHostKeys: false }),
|
||
|
|
"no",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveExternalStrictHostKeyChecking({ protocol: "mosh", verifyHostKeys: false }),
|
||
|
|
"no",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("quoteOpenSshOptionValue quotes paths with whitespace", () => {
|
||
|
|
assert.equal(quoteOpenSshOptionValue("/tmp/plain"), "/tmp/plain");
|
||
|
|
assert.equal(
|
||
|
|
quoteOpenSshOptionValue("/Users/Foo Bar/known_hosts"),
|
||
|
|
'"/Users/Foo Bar/known_hosts"',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildExternalHostKeySshOptions uses authoritative trust for both slots", () => {
|
||
|
|
const values = buildExternalHostKeySshOptions({
|
||
|
|
authoritativeKnownHostsPath: "/tmp/auth-kh",
|
||
|
|
protocol: "et",
|
||
|
|
style: "values",
|
||
|
|
});
|
||
|
|
assert.deepEqual(values, [
|
||
|
|
"UserKnownHostsFile=/tmp/auth-kh",
|
||
|
|
"GlobalKnownHostsFile=/tmp/auth-kh",
|
||
|
|
"KnownHostsCommand=none",
|
||
|
|
"StrictHostKeyChecking=accept-new",
|
||
|
|
]);
|
||
|
|
|
||
|
|
const moshArgs = buildExternalHostKeySshOptions({
|
||
|
|
authoritativeKnownHostsPath: "/tmp/auth-kh",
|
||
|
|
protocol: "mosh",
|
||
|
|
style: "args",
|
||
|
|
});
|
||
|
|
assert.deepEqual(moshArgs, [
|
||
|
|
"-o", "UserKnownHostsFile=/tmp/auth-kh",
|
||
|
|
"-o", "GlobalKnownHostsFile=/tmp/auth-kh",
|
||
|
|
"-o", "KnownHostsCommand=none",
|
||
|
|
"-o", "StrictHostKeyChecking=ask",
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildExternalHostKeySshOptions quotes whitespace paths", () => {
|
||
|
|
const values = buildExternalHostKeySshOptions({
|
||
|
|
authoritativeKnownHostsPath: "/tmp/user name/auth-kh",
|
||
|
|
protocol: "et",
|
||
|
|
style: "values",
|
||
|
|
});
|
||
|
|
assert.deepEqual(values, [
|
||
|
|
'UserKnownHostsFile="/tmp/user name/auth-kh"',
|
||
|
|
'GlobalKnownHostsFile="/tmp/user name/auth-kh"',
|
||
|
|
"KnownHostsCommand=none",
|
||
|
|
"StrictHostKeyChecking=accept-new",
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildExternalHostKeySshOptions neutralizes trust when verification is disabled", () => {
|
||
|
|
const disabled = buildExternalHostKeySshOptions({
|
||
|
|
authoritativeKnownHostsPath: "/tmp/auth-kh",
|
||
|
|
emptyKnownHostsPath: "/tmp/empty-kh",
|
||
|
|
verifyHostKeys: false,
|
||
|
|
protocol: "mosh",
|
||
|
|
style: "args",
|
||
|
|
});
|
||
|
|
assert.deepEqual(disabled, [
|
||
|
|
"-o", "UserKnownHostsFile=/tmp/empty-kh",
|
||
|
|
"-o", "GlobalKnownHostsFile=/tmp/empty-kh",
|
||
|
|
"-o", "KnownHostsCommand=none",
|
||
|
|
"-o", "StrictHostKeyChecking=no",
|
||
|
|
]);
|
||
|
|
assert.equal(disabled.some((part) => String(part).includes("/tmp/auth-kh")), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildExternalHostKeyConfigLines formats indented jump-host stanzas", () => {
|
||
|
|
const lines = buildExternalHostKeyConfigLines({
|
||
|
|
authoritativeKnownHostsPath: "/tmp/auth-kh",
|
||
|
|
protocol: "et",
|
||
|
|
});
|
||
|
|
assert.deepEqual(lines, [
|
||
|
|
" UserKnownHostsFile /tmp/auth-kh",
|
||
|
|
" GlobalKnownHostsFile /tmp/auth-kh",
|
||
|
|
" KnownHostsCommand none",
|
||
|
|
" StrictHostKeyChecking accept-new",
|
||
|
|
]);
|
||
|
|
});
|