mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
558525b59f |
No files matched your search
@@ -201,6 +201,8 @@ const requestHostKeyVerification = (sender, info) => new Promise((resolve) => {
|
||||
}
|
||||
});
|
||||
|
||||
const toKeyBuffer = (rawKey) => (Buffer.isBuffer(rawKey) ? rawKey : Buffer.from(rawKey || ""));
|
||||
|
||||
const createHostVerifier = ({
|
||||
sender,
|
||||
sessionId,
|
||||
@@ -213,64 +215,79 @@ const createHostVerifier = ({
|
||||
// TTL leaves the upload on "Waiting..." with no dialog and no error.
|
||||
unattended = false,
|
||||
onAccepted,
|
||||
}) => (rawKey, callback) => {
|
||||
const keyInfo = describeHostKey(rawKey);
|
||||
const noteAccepted = () => {
|
||||
try {
|
||||
onAccepted?.({
|
||||
hostname,
|
||||
port,
|
||||
keyType: keyInfo.keyType,
|
||||
fingerprint: keyInfo.fingerprint,
|
||||
publicKey: keyInfo.publicKey,
|
||||
});
|
||||
} catch {
|
||||
// Recording the accepted key must not change the verifier result.
|
||||
}) => {
|
||||
// ssh2 calls the verifier again on every rekey. A prompt there is async,
|
||||
// and while it waits ssh2 keeps sending channel data ahead of its NEWKEYS,
|
||||
// so the server drops the connection mid-transfer ("Bad packet length").
|
||||
// Like OpenSSH, a rekey only has to present the key already accepted for
|
||||
// this connection: answer it synchronously and never prompt again.
|
||||
let acceptedKey = null;
|
||||
|
||||
return (rawKey, callback) => {
|
||||
if (verifyHostKeys !== false && acceptedKey) {
|
||||
callback(acceptedKey.equals(toKeyBuffer(rawKey)));
|
||||
return;
|
||||
}
|
||||
|
||||
const keyInfo = describeHostKey(rawKey);
|
||||
const noteAccepted = () => {
|
||||
acceptedKey = Buffer.from(toKeyBuffer(rawKey));
|
||||
try {
|
||||
onAccepted?.({
|
||||
hostname,
|
||||
port,
|
||||
keyType: keyInfo.keyType,
|
||||
fingerprint: keyInfo.fingerprint,
|
||||
publicKey: keyInfo.publicKey,
|
||||
});
|
||||
} catch {
|
||||
// Recording the accepted key must not change the verifier result.
|
||||
}
|
||||
};
|
||||
|
||||
if (verifyHostKeys === false) {
|
||||
noteAccepted();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const decision = classifyHostKey({
|
||||
knownHosts,
|
||||
hostname,
|
||||
port,
|
||||
keyType: keyInfo.keyType,
|
||||
fingerprint: keyInfo.fingerprint,
|
||||
});
|
||||
|
||||
if (decision.status === "trusted") {
|
||||
noteAccepted();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (unattended) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
void requestHostKeyVerification(sender, {
|
||||
sessionId,
|
||||
hostname,
|
||||
port,
|
||||
status: decision.status,
|
||||
keyType: keyInfo.keyType,
|
||||
fingerprint: keyInfo.fingerprint,
|
||||
publicKey: keyInfo.publicKey,
|
||||
knownHostId: decision.knownHost?.id,
|
||||
knownFingerprint: decision.expectedFingerprint,
|
||||
...(Number.isFinite(bootEpoch) ? { bootEpoch: Number(bootEpoch) } : {}),
|
||||
}).then((response) => {
|
||||
if (response?.accept) noteAccepted();
|
||||
callback(Boolean(response?.accept));
|
||||
}).catch(() => {
|
||||
callback(false);
|
||||
});
|
||||
};
|
||||
|
||||
if (verifyHostKeys === false) {
|
||||
noteAccepted();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const decision = classifyHostKey({
|
||||
knownHosts,
|
||||
hostname,
|
||||
port,
|
||||
keyType: keyInfo.keyType,
|
||||
fingerprint: keyInfo.fingerprint,
|
||||
});
|
||||
|
||||
if (decision.status === "trusted") {
|
||||
noteAccepted();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (unattended) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
void requestHostKeyVerification(sender, {
|
||||
sessionId,
|
||||
hostname,
|
||||
port,
|
||||
status: decision.status,
|
||||
keyType: keyInfo.keyType,
|
||||
fingerprint: keyInfo.fingerprint,
|
||||
publicKey: keyInfo.publicKey,
|
||||
knownHostId: decision.knownHost?.id,
|
||||
knownFingerprint: decision.expectedFingerprint,
|
||||
...(Number.isFinite(bootEpoch) ? { bootEpoch: Number(bootEpoch) } : {}),
|
||||
}).then((response) => {
|
||||
if (response?.accept) noteAccepted();
|
||||
callback(Boolean(response?.accept));
|
||||
}).catch(() => {
|
||||
callback(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleResponse = (_event, payload) => {
|
||||
|
||||
@@ -356,6 +356,100 @@ test("createHostVerifier prompts for unknown host keys and waits for user respon
|
||||
assert.equal(await acceptedPromise, true);
|
||||
});
|
||||
|
||||
test("createHostVerifier answers rekeys synchronously with the key accepted at connect", async () => {
|
||||
const rawKey = Buffer.from("new server key");
|
||||
const sent = [];
|
||||
const sender = {
|
||||
id: 1,
|
||||
isDestroyed: () => false,
|
||||
send: (channel, payload) => sent.push({ channel, payload }),
|
||||
};
|
||||
// The knownHosts snapshot stays empty for the life of the connection, as it
|
||||
// does when the user picks "Continue" or "Add and continue" on first connect.
|
||||
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);
|
||||
handleResponse({ sender: { id: sender.id } }, {
|
||||
requestId: sent[0].payload.requestId,
|
||||
accept: true,
|
||||
addToKnownHosts: false,
|
||||
});
|
||||
assert.equal(await acceptedPromise, true);
|
||||
|
||||
// ssh2 re-runs the verifier on every rekey. It must answer before returning
|
||||
// (ssh2 keeps sending channel data while an async answer is pending) and
|
||||
// must not show the host-key dialog again.
|
||||
const rekeyResults = [];
|
||||
verifier(Buffer.from(rawKey), (ok) => rekeyResults.push(ok));
|
||||
assert.deepEqual(rekeyResults, [true]);
|
||||
assert.equal(sent.length, 1);
|
||||
});
|
||||
|
||||
test("createHostVerifier rejects a different host key on rekey 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,
|
||||
}],
|
||||
});
|
||||
|
||||
assert.equal(await new Promise((resolve) => verifier(rawKey, resolve)), true);
|
||||
|
||||
const rekeyResults = [];
|
||||
verifier(Buffer.from("other server key"), (ok) => rekeyResults.push(ok));
|
||||
assert.deepEqual(rekeyResults, [false]);
|
||||
assert.deepEqual(sent, []);
|
||||
});
|
||||
|
||||
test("createHostVerifier still prompts on the next connect after a rejected key", 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 first = new Promise((resolve) => verifier(rawKey, resolve));
|
||||
handleResponse({ sender: { id: sender.id } }, { requestId: sent[0].payload.requestId, accept: false });
|
||||
assert.equal(await first, false);
|
||||
|
||||
const second = new Promise((resolve) => verifier(rawKey, resolve));
|
||||
assert.equal(sent.length, 2);
|
||||
handleResponse({ sender: { id: sender.id } }, { requestId: sent[1].payload.requestId, accept: true });
|
||||
assert.equal(await second, true);
|
||||
});
|
||||
|
||||
test("createHostVerifier forwards bootEpoch on host-key prompts", async () => {
|
||||
const rawKey = Buffer.from("new server key");
|
||||
const sent = [];
|
||||
|
||||
Reference in new issue
Block a user