[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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,428 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const {
classifyHostKey,
createHostVerifier,
describeHostKey,
handleResponse,
normalizeFingerprint,
} = require("./hostKeyVerifier.cjs");
const makeRawPublicKey = (keyType, body = "trusted imported host key") => {
const type = Buffer.from(keyType);
const length = Buffer.alloc(4);
length.writeUInt32BE(type.length, 0);
return Buffer.concat([length, type, Buffer.from(body)]);
};
test("classifyHostKey prompts for unknown hosts", () => {
const result = classifyHostKey({
knownHosts: [],
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
fingerprint: "new-key",
});
assert.equal(result.status, "unknown");
});
test("classifyHostKey trusts a matching known host fingerprint", () => {
const result = classifyHostKey({
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
publicKey: "SHA256:trusted-key",
discoveredAt: 1,
}],
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
fingerprint: "trusted-key",
});
assert.equal(result.status, "trusted");
});
test("classifyHostKey trusts a matching full known_hosts public key", () => {
const rawKey = makeRawPublicKey("ssh-ed25519");
const fingerprint = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
const result = classifyHostKey({
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
publicKey: `ssh-ed25519 ${rawKey.toString("base64")}`,
discoveredAt: 1,
}],
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
fingerprint,
});
assert.equal(result.status, "trusted");
});
test("describeHostKey preserves the full public key from raw SSH key blobs", () => {
const rawKey = makeRawPublicKey("ssh-ed25519");
const result = describeHostKey(rawKey);
assert.equal(result.keyType, "ssh-ed25519");
assert.equal(result.publicKey, `ssh-ed25519 ${rawKey.toString("base64")}`);
});
test("classifyHostKey warns when a known host fingerprint changes", () => {
const result = classifyHostKey({
knownHosts: [{
id: "kh-1",
hostname: "192.0.2.10",
port: 22,
keyType: "ssh-ed25519",
publicKey: "SHA256:old-key",
discoveredAt: 1,
}],
hostname: "192.0.2.10",
port: 22,
keyType: "ssh-ed25519",
fingerprint: "new-key",
});
assert.equal(result.status, "changed");
assert.equal(result.knownHost?.id, "kh-1");
assert.equal(result.expectedFingerprint, "old-key");
});
test("classifyHostKey treats the same hostname on a different port as unknown", () => {
const result = classifyHostKey({
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 2222,
keyType: "ssh-ed25519",
publicKey: "SHA256:trusted-key",
discoveredAt: 1,
}],
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
fingerprint: "trusted-key",
});
assert.equal(result.status, "unknown");
});
test("classifyHostKey reports unknown when only the incoming key type is unknown", () => {
// Without a confident key type from ssh2 we cannot tell whether this is a
// rotation of the stored key or a brand-new algorithm; force the user back
// through the first-time-trust path rather than scaring them with a
// "fingerprint changed" warning (#972).
const result = classifyHostKey({
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
publicKey: "SHA256:trusted-key",
discoveredAt: 1,
}],
hostname: "switch.local",
port: 22,
keyType: "unknown",
fingerprint: "new-key",
});
assert.equal(result.status, "unknown");
});
test("classifyHostKey reports unknown when the stored record has no key type", () => {
// Legacy / imported records sometimes have an empty or "unknown" keyType.
// Promoting those to "changed" on every connect was the root cause of #972;
// treat them as not-comparable so the user re-confirms cleanly.
const result = classifyHostKey({
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 22,
keyType: "unknown",
publicKey: "SHA256:trusted-key",
discoveredAt: 1,
}],
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
fingerprint: "new-key",
});
assert.equal(result.status, "unknown");
});
test("classifyHostKey reports unknown when the server presents a different key type than any stored record", () => {
// Server with ssh-rsa stored; presents ssh-ed25519 this time. OpenSSH treats
// this as a new key offering, not a rotation; we match that behavior so a
// host with multiple algorithms doesn't spam mismatch warnings on every
// algorithm renegotiation.
const result = classifyHostKey({
knownHosts: [{
id: "kh-rsa",
hostname: "switch.local",
port: 22,
keyType: "ssh-rsa",
publicKey: "SHA256:rsa-key",
discoveredAt: 1,
}],
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
fingerprint: "new-key",
});
assert.equal(result.status, "unknown");
});
test("classifyHostKey prefers exact key type mismatches when a host has multiple keys", () => {
const result = classifyHostKey({
knownHosts: [{
id: "kh-rsa",
hostname: "switch.local",
port: 22,
keyType: "ssh-rsa",
publicKey: "SHA256:rsa-key",
discoveredAt: 1,
}, {
id: "kh-ed25519",
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
publicKey: "SHA256:ed25519-key",
discoveredAt: 2,
}],
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
fingerprint: "new-key",
});
assert.equal(result.status, "changed");
assert.equal(result.knownHost?.id, "kh-ed25519");
});
test("classifyHostKey does not pick an arbitrary known host when incoming key type is unknown", () => {
const result = classifyHostKey({
knownHosts: [{
id: "kh-rsa",
hostname: "switch.local",
port: 22,
keyType: "ssh-rsa",
publicKey: "SHA256:rsa-key",
discoveredAt: 1,
}, {
id: "kh-ed25519",
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
publicKey: "SHA256:ed25519-key",
discoveredAt: 2,
}],
hostname: "switch.local",
port: 22,
keyType: "unknown",
fingerprint: "new-key",
});
assert.equal(result.status, "unknown");
});
test("normalizeFingerprint accepts SHA256-prefixed values", () => {
assert.equal(normalizeFingerprint("SHA256:abc123==="), "abc123");
});
test("createHostVerifier accepts trusted host keys without prompting", async () => {
const rawKey = Buffer.from("trusted server key");
const fingerprint = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
const sent = [];
const sender = {
id: 1,
isDestroyed: () => false,
send: (channel, payload) => sent.push({ channel, payload }),
};
const verifier = createHostVerifier({
sender,
sessionId: "session-1",
hostname: "switch.local",
port: 22,
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 22,
keyType: "unknown",
publicKey: `SHA256:${fingerprint}`,
discoveredAt: 1,
}],
});
const accepted = await new Promise((resolve) => verifier(rawKey, resolve));
assert.equal(accepted, true);
assert.deepEqual(sent, []);
});
test("createHostVerifier skips prompts when host key verification is disabled", async () => {
const rawKey = Buffer.from("untrusted server key");
const sent = [];
const sender = {
id: 1,
isDestroyed: () => false,
send: (channel, payload) => sent.push({ channel, payload }),
};
const verifier = createHostVerifier({
sender,
sessionId: "session-1",
hostname: "switch.local",
port: 22,
knownHosts: [],
verifyHostKeys: false,
});
const accepted = await new Promise((resolve) => verifier(rawKey, resolve));
assert.equal(accepted, true);
assert.deepEqual(sent, []);
});
test("createHostVerifier accepts imported full known_hosts public keys without prompting", async () => {
const rawKey = makeRawPublicKey("ssh-ed25519");
const sent = [];
const sender = {
id: 1,
isDestroyed: () => false,
send: (channel, payload) => sent.push({ channel, payload }),
};
const verifier = createHostVerifier({
sender,
sessionId: "session-1",
hostname: "switch.local",
port: 22,
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
publicKey: `ssh-ed25519 ${rawKey.toString("base64")}`,
discoveredAt: 1,
}],
});
const accepted = await new Promise((resolve) => verifier(rawKey, resolve));
assert.equal(accepted, true);
assert.deepEqual(sent, []);
});
test("createHostVerifier prompts for unknown host keys and waits for user response", async () => {
const rawKey = Buffer.from("new server key");
const sent = [];
const sender = {
id: 1,
isDestroyed: () => false,
send: (channel, payload) => sent.push({ channel, payload }),
};
const verifier = createHostVerifier({
sender,
sessionId: "session-1",
hostname: "switch.local",
port: 22,
knownHosts: [],
});
const acceptedPromise = new Promise((resolve) => verifier(rawKey, resolve));
assert.equal(sent.length, 1);
assert.equal(sent[0].channel, "netcatty:host-key:verify");
assert.equal(sent[0].payload.hostname, "switch.local");
assert.equal(sent[0].payload.status, "unknown");
handleResponse({ sender: { id: sender.id } }, {
requestId: sent[0].payload.requestId,
accept: true,
addToKnownHosts: true,
});
assert.equal(await acceptedPromise, true);
});
test("createHostVerifier forwards bootEpoch on host-key prompts", async () => {
const rawKey = Buffer.from("new server key");
const sent = [];
const sender = {
id: 1,
isDestroyed: () => false,
send: (channel, payload) => sent.push({ channel, payload }),
};
const verifier = createHostVerifier({
sender,
sessionId: "session-1",
hostname: "switch.local",
port: 22,
knownHosts: [],
bootEpoch: 7,
});
const acceptedPromise = new Promise((resolve) => verifier(rawKey, resolve));
assert.equal(sent[0].payload.bootEpoch, 7);
handleResponse({ sender: { id: sender.id } }, {
requestId: sent[0].payload.requestId,
accept: false,
});
assert.equal(await acceptedPromise, false);
});
test("createHostVerifier includes existing known host details when a key changes", async () => {
// A well-formed wire blob so `describeHostKey` can recover keyType =
// "ssh-ed25519"; that triggers the strict (host, port, type) mismatch
// branch with a stored record of the same type but different fingerprint.
const rawKey = makeRawPublicKey("ssh-ed25519", "changed server key");
const sent = [];
const sender = {
id: 1,
isDestroyed: () => false,
send: (channel, payload) => sent.push({ channel, payload }),
};
const verifier = createHostVerifier({
sender,
sessionId: "session-1",
hostname: "switch.local",
port: 22,
knownHosts: [{
id: "kh-1",
hostname: "switch.local",
port: 22,
keyType: "ssh-ed25519",
publicKey: "SHA256:old-key",
fingerprint: "old-key",
discoveredAt: 1,
}],
});
const acceptedPromise = new Promise((resolve) => verifier(rawKey, resolve));
assert.equal(sent.length, 1);
assert.equal(sent[0].channel, "netcatty:host-key:verify");
assert.equal(sent[0].payload.status, "changed");
assert.equal(sent[0].payload.knownHostId, "kh-1");
assert.equal(sent[0].payload.knownFingerprint, "old-key");
handleResponse({ sender: { id: sender.id } }, {
requestId: sent[0].payload.requestId,
accept: true,
addToKnownHosts: true,
});
assert.equal(await acceptedPromise, true);
});