[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
514
electron/bridges/externalSshHostKeyPolicy.test.cjs
Normal file
514
electron/bridges/externalSshHostKeyPolicy.test.cjs
Normal file
@@ -0,0 +1,514 @@
|
||||
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",
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user