mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1723024860 | ||
|
|
17f89ca6ef | ||
|
|
08355fa5cb | ||
|
|
f1d135f5af |
No files matched your search
@@ -47,16 +47,6 @@ test("effectiveDefaultAlgorithms (modern) includes chacha20-poly1305", () => {
|
||||
assert.ok(result.cipher.includes("chacha20-poly1305@openssh.com"));
|
||||
});
|
||||
|
||||
test("effectiveDefaultAlgorithms offers OpenSSH host certificates before plain host keys", () => {
|
||||
const cert = "ssh-ed25519-cert-v01@openssh.com";
|
||||
const modern = effectiveDefaultAlgorithms(false);
|
||||
assert.ok(modern.serverHostKey.includes(cert));
|
||||
assert.ok(modern.serverHostKey.indexOf(cert) < modern.serverHostKey.indexOf("ssh-ed25519"));
|
||||
const legacy = effectiveDefaultAlgorithms(true);
|
||||
assert.ok(legacy.serverHostKey.includes(cert));
|
||||
assert.ok(legacy.serverHostKey.includes("ssh-dss"));
|
||||
});
|
||||
|
||||
test("effectiveDefaultAlgorithms (legacy) appends sha1 KEX, CBC, and ssh-dss", () => {
|
||||
const modern = effectiveDefaultAlgorithms(false);
|
||||
const legacy = effectiveDefaultAlgorithms(true);
|
||||
|
||||
@@ -77,13 +77,6 @@ export const SUPPORTED_HMAC_ALGORITHMS: readonly string[] = [
|
||||
];
|
||||
|
||||
export const SUPPORTED_SERVER_HOST_KEY_ALGORITHMS: readonly string[] = [
|
||||
"ssh-ed25519-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com",
|
||||
"rsa-sha2-512-cert-v01@openssh.com",
|
||||
"rsa-sha2-256-cert-v01@openssh.com",
|
||||
"ssh-rsa-cert-v01@openssh.com",
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"ecdsa-sha2-nistp384",
|
||||
@@ -155,13 +148,6 @@ const MODERN_DEFAULT_ALGORITHMS: Readonly<Record<SSHAlgorithmCategory, readonly
|
||||
"hmac-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com",
|
||||
"rsa-sha2-512-cert-v01@openssh.com",
|
||||
"rsa-sha2-256-cert-v01@openssh.com",
|
||||
"ssh-rsa-cert-v01@openssh.com",
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"ecdsa-sha2-nistp384",
|
||||
|
||||
@@ -195,9 +195,6 @@ module.exports = {
|
||||
'node_modules/@cursor/sdk-*/**/*',
|
||||
'node_modules/sqlite3/**/*',
|
||||
'node_modules/@modelcontextprotocol/sdk/**/*',
|
||||
// External MCP scripts run from asarUnpack via ELECTRON_RUN_AS_NODE and
|
||||
// must resolve re2js from a real directory (issue #3910).
|
||||
'node_modules/re2js/**/*',
|
||||
'lib/**/*.cjs',
|
||||
'lib/**/*.json',
|
||||
'node_modules/zod/**/*',
|
||||
|
||||
@@ -61,13 +61,10 @@ const POWERSHELL_PROMPT_PATTERN = /^PS(?:\s+\S.*)?>$/;
|
||||
// PowerShell's `PS C:\...>` (handled by POWERSHELL_PROMPT_PATTERN first).
|
||||
const CMD_PROMPT_PATTERN = /^[A-Za-z]:(?:\\[^<>"|]*)?>$/;
|
||||
|
||||
// Default login prompts, kept narrow so custom / fish / starship prompts do
|
||||
// not flip a Windows DefaultShell soft hint:
|
||||
// - bash/zsh `user@host[:path][#$]` (colon before the path; `root@host ~#` stays rejected)
|
||||
// - RHEL-family `[\u@\h \W]\$` -> `[user@host path][#$]`, or `[user@host][#$]`
|
||||
// with no path. `\W` may contain whitespace or `]` (`[user@host My [Project]]$`);
|
||||
// the final `]` before `$`/`#` is the delimiter.
|
||||
const POSIX_PROMPT_PATTERN = /^(?:[^\s@]+@[^\s:]+(?::[^\n\r]*)?|\[[^\s@\]]+@[^\s\]:]+(?:\s+[^\n\r]+)?\])[#$]$/;
|
||||
// Classic `user@host:...$` / `user@host:...#` login prompt (bash/zsh in WSL,
|
||||
// remote Linux, etc.). Intentionally narrow so custom / fish prompts do not
|
||||
// flip a Windows DefaultShell soft hint.
|
||||
const POSIX_PROMPT_PATTERN = /^[^\s@]+@[^\s:]+(?::[^\n\r]*)?[#$]$/;
|
||||
|
||||
function isDefaultPowerShellPromptLine(line) {
|
||||
return POWERSHELL_PROMPT_PATTERN.test(String(line || ""));
|
||||
@@ -152,7 +149,7 @@ function trackSessionIdlePrompt(session, chunk) {
|
||||
// Return `session.lastIdlePrompt` only if the PTY's recent rolling tail
|
||||
// still ends with it. The cached prompt is updated only when
|
||||
// extractTrailingIdlePrompt recognizes a known shape (PowerShell, cmd.exe,
|
||||
// `user@host[:path][#$]`, or RHEL `[user@host path][#$]`); a remote shell switch into another shell, an
|
||||
// or `user@host[:path][#$]`); a remote shell switch into another shell, an
|
||||
// oh-my-posh / starship / custom PS1, or any unrecognized prompt would
|
||||
// otherwise leave a stale value behind, which `resolveEffectiveShellKind`
|
||||
// would then keep using to coerce future commands into a PowerShell
|
||||
|
||||
@@ -133,50 +133,14 @@ test("isDefaultCmdPromptLine matches drive-letter cmd prompts only", () => {
|
||||
|
||||
test("isDefaultPosixPromptLine matches classic user@host prompts", () => {
|
||||
assert.equal(isDefaultPosixPromptLine("alice@host:~$"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("user@host:~$"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("alice@wsl:/mnt/c$"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("root@box:/#"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[opc@instance-20260905-1821 ~]$"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[opc@instance-20260905-1821 ~]$ "), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[root@host ~]#"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[user@host]$"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[user@host My Project]$"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[user@host My Project]$ "), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[user@host My [Project]]$"), true);
|
||||
assert.equal(isDefaultPosixPromptLine("[user@host My [Project]]$ "), true);
|
||||
assert.equal(isDefaultPosixPromptLine("root@host ~#"), false);
|
||||
assert.equal(isDefaultPosixPromptLine("PS C:\\Users\\alice>"), false);
|
||||
assert.equal(isDefaultPosixPromptLine("C:\\Users\\alice>"), false);
|
||||
assert.equal(isDefaultPosixPromptLine(""), false);
|
||||
});
|
||||
|
||||
test("extracts a trailing RHEL-family bracketed idle prompt", () => {
|
||||
// Oracle Linux / RHEL / CentOS / Fedora default PS1 is `[\u@\h \W]\$ `.
|
||||
// Trailing space must round-trip, same as the PowerShell prompt capture.
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("[opc@instance-20260905-1821 ~]$ "),
|
||||
"[opc@instance-20260905-1821 ~]$ ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("Last login: Fri Oct 24\r\n[root@host ~]#"),
|
||||
"[root@host ~]#",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("welcome\r\nuser@host:~$ "),
|
||||
"user@host:~$ ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("welcome\r\n[user@host My Project]$ "),
|
||||
"[user@host My Project]$ ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("welcome\r\n[user@host My [Project]]$ "),
|
||||
"[user@host My [Project]]$ ",
|
||||
);
|
||||
// Unbracketed space form is still not a recognized default prompt.
|
||||
assert.equal(extractTrailingIdlePrompt("welcome\r\nroot@host ~#"), "");
|
||||
});
|
||||
|
||||
test("isPlausibleCliVersionOutput rejects stack traces and file URLs", () => {
|
||||
assert.equal(isPlausibleCliVersionOutput("2.1.123 (Claude Code)"), true);
|
||||
assert.equal(isPlausibleCliVersionOutput("codex-cli 0.125.0"), true);
|
||||
@@ -710,35 +674,6 @@ test("getFreshIdlePrompt returns empty string when the session has no cached pro
|
||||
);
|
||||
});
|
||||
|
||||
test("trackSessionIdlePrompt and getFreshIdlePrompt accept an Oracle/RHEL bracketed bash prompt", () => {
|
||||
const session = {};
|
||||
trackSessionIdlePrompt(session, "Last login...\r\n[opc@instance-20260905-1821 ~]$ ");
|
||||
assert.equal(session.lastIdlePrompt, "[opc@instance-20260905-1821 ~]$ ");
|
||||
assert.equal(getFreshIdlePrompt(session), "[opc@instance-20260905-1821 ~]$ ");
|
||||
|
||||
const rootSession = {};
|
||||
trackSessionIdlePrompt(rootSession, "[root@host ~]#");
|
||||
assert.equal(getFreshIdlePrompt(rootSession), "[root@host ~]#");
|
||||
|
||||
const classic = {};
|
||||
trackSessionIdlePrompt(classic, "user@host:~$ ");
|
||||
assert.equal(getFreshIdlePrompt(classic), "user@host:~$ ");
|
||||
|
||||
const unbracketed = {};
|
||||
assert.equal(trackSessionIdlePrompt(unbracketed, "root@host ~#"), "");
|
||||
assert.equal(getFreshIdlePrompt(unbracketed), "");
|
||||
|
||||
const spacedPath = {};
|
||||
trackSessionIdlePrompt(spacedPath, "Last login...\r\n[user@host My Project]$ ");
|
||||
assert.equal(spacedPath.lastIdlePrompt, "[user@host My Project]$ ");
|
||||
assert.equal(getFreshIdlePrompt(spacedPath), "[user@host My Project]$ ");
|
||||
|
||||
const bracketedPath = {};
|
||||
trackSessionIdlePrompt(bracketedPath, "Last login...\r\n[user@host My [Project]]$ ");
|
||||
assert.equal(bracketedPath.lastIdlePrompt, "[user@host My [Project]]$ ");
|
||||
assert.equal(getFreshIdlePrompt(bracketedPath), "[user@host My [Project]]$ ");
|
||||
});
|
||||
|
||||
test("getFreshIdlePrompt and trackSessionIdlePrompt round-trip through a real PTY-like flow", () => {
|
||||
// (1) Remote PowerShell prompt arrives — lastIdlePrompt is captured.
|
||||
const session = {};
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
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 { execFileSync } = require("node:child_process");
|
||||
const {
|
||||
DEFAULT_SERVER_HOST_KEY,
|
||||
SUPPORTED_SERVER_HOST_KEY,
|
||||
} = require("ssh2/lib/protocol/constants.js");
|
||||
const {
|
||||
parseDERKey,
|
||||
parseKey,
|
||||
} = require("ssh2/lib/protocol/keyParser.js");
|
||||
const {
|
||||
hostKeyBlobTypeAccepted,
|
||||
hostKeySignatureTypeAccepted,
|
||||
} = require("ssh2/lib/protocol/kex.js");
|
||||
|
||||
// Manual live check (optional; needs network and real Instabox credentials):
|
||||
// ssh -vv -p 2222 USER@ssh.instabox.sh
|
||||
// Instabox advertises only ssh-ed25519-cert-v01@openssh.com and signs the
|
||||
// exchange hash as ssh-ed25519. After this patch, host-key negotiation should
|
||||
// succeed; authentication still depends on the real certificate credentials.
|
||||
|
||||
const HOST_CERT = "ssh-ed25519-cert-v01@openssh.com";
|
||||
const HOST_CERT_ALGORITHMS = [
|
||||
HOST_CERT,
|
||||
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com",
|
||||
"rsa-sha2-512-cert-v01@openssh.com",
|
||||
"rsa-sha2-256-cert-v01@openssh.com",
|
||||
"ssh-rsa-cert-v01@openssh.com",
|
||||
];
|
||||
|
||||
function sshKeygenSkipReason(error) {
|
||||
if (error && error.code === "ENOENT")
|
||||
return "ssh-keygen is not installed";
|
||||
return false;
|
||||
}
|
||||
|
||||
function detectSshKeygenSkip() {
|
||||
try {
|
||||
execFileSync("ssh-keygen", ["-h"], { stdio: "ignore" });
|
||||
return false;
|
||||
} catch (error) {
|
||||
return sshKeygenSkipReason(error);
|
||||
}
|
||||
}
|
||||
|
||||
const sshKeygenSkip = detectSshKeygenSkip();
|
||||
|
||||
function mintEd25519HostCert(dir) {
|
||||
const ca = path.join(dir, "ca");
|
||||
const host = path.join(dir, "host");
|
||||
execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-f", ca]);
|
||||
execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-f", host]);
|
||||
execFileSync("ssh-keygen", [
|
||||
"-q",
|
||||
"-s",
|
||||
ca,
|
||||
"-I",
|
||||
"netcatty-hostcert",
|
||||
"-h",
|
||||
"-n",
|
||||
"127.0.0.1",
|
||||
"-V",
|
||||
"+52w",
|
||||
`${host}.pub`,
|
||||
]);
|
||||
const certLine = fs.readFileSync(`${host}-cert.pub`, "utf8").trim();
|
||||
const [type, b64] = certLine.split(/\s+/);
|
||||
const privateKey = parseKey(fs.readFileSync(host));
|
||||
return {
|
||||
type,
|
||||
certBlob: Buffer.from(b64, "base64"),
|
||||
privateKeyPem: fs.readFileSync(host),
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
test("ssh2 default and supported host key lists offer OpenSSH host certificates", () => {
|
||||
for (const algo of HOST_CERT_ALGORITHMS) {
|
||||
assert.equal(DEFAULT_SERVER_HOST_KEY.includes(algo), true, `${algo} missing from defaults`);
|
||||
assert.equal(SUPPORTED_SERVER_HOST_KEY.includes(algo), true, `${algo} missing from supported`);
|
||||
}
|
||||
assert.ok(
|
||||
DEFAULT_SERVER_HOST_KEY.indexOf(HOST_CERT) < DEFAULT_SERVER_HOST_KEY.indexOf("ssh-ed25519"),
|
||||
"host certificates should be offered before the matching plain key",
|
||||
);
|
||||
assert.equal(DEFAULT_SERVER_HOST_KEY.includes("ssh-ed25519"), true);
|
||||
assert.equal(DEFAULT_SERVER_HOST_KEY.includes("ssh-rsa"), true);
|
||||
assert.equal(
|
||||
SUPPORTED_SERVER_HOST_KEY.includes("sk-ssh-ed25519-cert-v01@openssh.com"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
SUPPORTED_SERVER_HOST_KEY.includes("sk-ecdsa-sha2-nistp256-cert-v01@openssh.com"),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("missing ssh-keygen skips host-certificate minting instead of failing", () => {
|
||||
const missing = Object.assign(new Error("spawn ssh-keygen ENOENT"), { code: "ENOENT" });
|
||||
assert.equal(sshKeygenSkipReason(missing), "ssh-keygen is not installed");
|
||||
const present = Object.assign(new Error("usage"), { status: 1 });
|
||||
assert.equal(sshKeygenSkipReason(present), false);
|
||||
assert.equal(sshKeygenSkipReason(null), false);
|
||||
});
|
||||
|
||||
test("parseDERKey reads an OpenSSH ed25519 host certificate the way KEX does", { skip: sshKeygenSkip }, () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-hostcert-"));
|
||||
try {
|
||||
const minted = mintEd25519HostCert(dir);
|
||||
assert.equal(minted.type, HOST_CERT);
|
||||
assert.equal(minted.privateKey instanceof Error, false, minted.privateKey.message);
|
||||
const typeLen = minted.certBlob.readUInt32BE(0);
|
||||
const type = minted.certBlob.subarray(4, 4 + typeLen).toString();
|
||||
const keyMaterial = minted.certBlob.subarray(4 + typeLen);
|
||||
const parsed = parseDERKey(keyMaterial, type);
|
||||
assert.equal(parsed instanceof Error, false, parsed instanceof Error ? parsed.message : "");
|
||||
assert.equal(parsed.type, HOST_CERT);
|
||||
const message = Buffer.from("kex-exchange-hash");
|
||||
const signature = minted.privateKey.sign(message);
|
||||
assert.equal(signature instanceof Error, false, signature instanceof Error ? signature.message : "");
|
||||
assert.equal(parsed.verify(message, signature), true);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("host key signature type accepts the underlying algorithm for host certificates", () => {
|
||||
assert.equal(typeof hostKeySignatureTypeAccepted, "function");
|
||||
assert.equal(hostKeySignatureTypeAccepted(HOST_CERT, "ssh-ed25519"), true);
|
||||
assert.equal(hostKeySignatureTypeAccepted(HOST_CERT, HOST_CERT), true);
|
||||
assert.equal(hostKeySignatureTypeAccepted(HOST_CERT, "ssh-rsa"), false);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("ecdsa-sha2-nistp256-cert-v01@openssh.com", "ecdsa-sha2-nistp256"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("rsa-sha2-256-cert-v01@openssh.com", "rsa-sha2-256"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("rsa-sha2-256-cert-v01@openssh.com", "rsa-sha2-512"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("rsa-sha2-256-cert-v01@openssh.com", "ssh-rsa"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("rsa-sha2-256-cert-v00@openssh.com", "ssh-rsa"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("rsa-sha2-512-cert-v01@openssh.com", "rsa-sha2-512"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("rsa-sha2-512-cert-v01@openssh.com", "rsa-sha2-256"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("rsa-sha2-512-cert-v01@openssh.com", "ssh-rsa"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("ssh-rsa-cert-v01@openssh.com", "rsa-sha2-256"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("ssh-rsa-cert-v01@openssh.com", "rsa-sha2-512"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("ssh-rsa-cert-v01@openssh.com", "ssh-rsa"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeySignatureTypeAccepted("ssh-rsa-cert-v00@openssh.com", "ssh-rsa"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeyBlobTypeAccepted("rsa-sha2-256-cert-v01@openssh.com", "ssh-rsa-cert-v01@openssh.com"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeyBlobTypeAccepted("rsa-sha2-512-cert-v01@openssh.com", "ssh-rsa-cert-v01@openssh.com"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeyBlobTypeAccepted("rsa-sha2-256-cert-v01@openssh.com", "ssh-rsa"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
hostKeyBlobTypeAccepted("rsa-sha2-512-cert-v01@openssh.com", "ssh-rsa"),
|
||||
false,
|
||||
);
|
||||
assert.equal(hostKeyBlobTypeAccepted("rsa-sha2-256", "ssh-rsa"), true);
|
||||
assert.equal(hostKeyBlobTypeAccepted("rsa-sha2-512", "ssh-rsa"), true);
|
||||
assert.equal(hostKeySignatureTypeAccepted("ssh-ed25519", "ssh-ed25519"), true);
|
||||
assert.equal(hostKeySignatureTypeAccepted("ssh-ed25519", HOST_CERT), false);
|
||||
assert.equal(hostKeySignatureTypeAccepted("rsa-sha2-256", "ssh-rsa"), false);
|
||||
assert.equal(hostKeySignatureTypeAccepted("rsa-sha2-512", "rsa-sha2-256"), false);
|
||||
});
|
||||
|
||||
function loadSsh2WithCertHostKey(pem, certBlob) {
|
||||
// ssh2.Server only accepts PEM text and then advertises privateKey.type.
|
||||
// Reload it after wrapping parseKey so this one key is presented as an
|
||||
// OpenSSH host certificate while signing still uses the embedded key.
|
||||
const keyParser = require("ssh2/lib/protocol/keyParser.js");
|
||||
const originalParseKey = keyParser.parseKey;
|
||||
keyParser.parseKey = function parseCertHostKey(data, passphrase) {
|
||||
const parsed = originalParseKey(data, passphrase);
|
||||
if (!(parsed instanceof Error) && data === pem) {
|
||||
parsed.type = HOST_CERT;
|
||||
parsed.getPublicSSH = () => certBlob;
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
const reloadIds = ["ssh2/lib/server.js", "ssh2/lib/index.js", "ssh2"];
|
||||
const resolved = reloadIds.map((id) => require.resolve(id));
|
||||
const previous = resolved.map((filename) => require.cache[filename]);
|
||||
for (const filename of resolved) delete require.cache[filename];
|
||||
const ssh2 = require("ssh2");
|
||||
for (let i = 0; i < resolved.length; i++) {
|
||||
if (previous[i]) require.cache[resolved[i]] = previous[i];
|
||||
else delete require.cache[resolved[i]];
|
||||
}
|
||||
keyParser.parseKey = originalParseKey;
|
||||
return ssh2;
|
||||
}
|
||||
|
||||
test("ssh2 client completes KEX against a cert-only ed25519 host key", { skip: sshKeygenSkip }, async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-hostcert-kex-"));
|
||||
const minted = mintEd25519HostCert(dir);
|
||||
assert.equal(minted.privateKey instanceof Error, false, minted.privateKey.message);
|
||||
const { Client, Server } = loadSsh2WithCertHostKey(minted.privateKeyPem, minted.certBlob);
|
||||
|
||||
const server = new Server({ hostKeys: [minted.privateKeyPem] }, (client) => {
|
||||
client.on("error", () => {});
|
||||
client.on("authentication", (ctx) => ctx.reject());
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
|
||||
try {
|
||||
const authError = await new Promise((resolve, reject) => {
|
||||
const client = new Client();
|
||||
const timer = setTimeout(() => {
|
||||
client.end();
|
||||
reject(new Error("timed out waiting for KEX/auth"));
|
||||
}, 10000);
|
||||
client.on("ready", () => {
|
||||
clearTimeout(timer);
|
||||
client.end();
|
||||
reject(new Error("authentication unexpectedly succeeded"));
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
clearTimeout(timer);
|
||||
client.end();
|
||||
resolve(err);
|
||||
});
|
||||
client.connect({
|
||||
host: "127.0.0.1",
|
||||
port: server.address().port,
|
||||
username: "netcatty",
|
||||
password: "not-the-password",
|
||||
readyTimeout: 8000,
|
||||
});
|
||||
});
|
||||
assert.match(authError.message, /authentication/i);
|
||||
assert.doesNotMatch(
|
||||
authError.message,
|
||||
/Handshake failed|no matching host key|Wrong signature type|Unsupported OpenSSH|signature verification failed|Host key does not match/,
|
||||
);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { createHash, generateKeyPairSync, sign } = require("node:crypto");
|
||||
const {
|
||||
parseKey,
|
||||
getOpenSSHCertSignatureAlgorithm,
|
||||
} = require("ssh2/lib/protocol/keyParser.js");
|
||||
const { convertSignature } = require("ssh2/lib/protocol/utils.js");
|
||||
|
||||
function buildSkEd25519PublicLine() {
|
||||
const type = Buffer.from("sk-ssh-ed25519@openssh.com");
|
||||
@@ -148,89 +146,3 @@ test("ssh2 agent protocol preserves SK signature framing, flags and counter", as
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function sshString(value) {
|
||||
const body = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
||||
const out = Buffer.alloc(4 + body.length);
|
||||
out.writeUInt32BE(body.length, 0);
|
||||
body.copy(out, 4);
|
||||
return out;
|
||||
}
|
||||
|
||||
function opensshPublicLine(typeName, fields) {
|
||||
const blob = Buffer.concat([
|
||||
sshString(typeName),
|
||||
...fields.map((field) => sshString(field)),
|
||||
]);
|
||||
return `${typeName} ${blob.toString("base64")} netcatty@sk`;
|
||||
}
|
||||
|
||||
function skEcdsaSignature(rawSig, flags, counter) {
|
||||
const out = Buffer.alloc(4 + rawSig.length + 5);
|
||||
out.writeUInt32BE(rawSig.length, 0);
|
||||
rawSig.copy(out, 4);
|
||||
const pos = 4 + rawSig.length;
|
||||
out[pos] = flags;
|
||||
out.writeUInt32BE(counter, pos + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
test("ssh2 verifies an SK ECDSA certificate with the same P-256 signature as the plain key", () => {
|
||||
const { publicKey, privateKey } = generateKeyPairSync("ec", {
|
||||
namedCurve: "prime256v1",
|
||||
});
|
||||
const jwk = publicKey.export({ format: "jwk" });
|
||||
const point = Buffer.concat([
|
||||
Buffer.from([0x04]),
|
||||
Buffer.from(jwk.x, "base64url"),
|
||||
Buffer.from(jwk.y, "base64url"),
|
||||
]);
|
||||
const application = "ssh:";
|
||||
const plain = parseKey(opensshPublicLine("sk-ecdsa-sha2-nistp256@openssh.com", [
|
||||
"nistp256",
|
||||
point,
|
||||
application,
|
||||
]));
|
||||
const cert = parseKey(opensshPublicLine("sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", [
|
||||
Buffer.alloc(16, 3),
|
||||
"nistp256",
|
||||
point,
|
||||
application,
|
||||
]));
|
||||
assert.equal(plain instanceof Error, false, plain instanceof Error ? plain.message : "");
|
||||
assert.equal(cert instanceof Error, false, cert instanceof Error ? cert.message : "");
|
||||
assert.equal(cert.type, "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com");
|
||||
assert.equal(cert.getApplication(), application);
|
||||
|
||||
const message = Buffer.from("kex-exchange-hash");
|
||||
const flags = 0x01;
|
||||
const counter = 0x12345678;
|
||||
const appHash = createHash("sha256").update(application).digest();
|
||||
const dataHash = createHash("sha256").update(message).digest();
|
||||
const signedData = Buffer.alloc(appHash.length + 1 + 4 + dataHash.length);
|
||||
let offset = 0;
|
||||
appHash.copy(signedData, offset);
|
||||
offset += appHash.length;
|
||||
signedData[offset++] = flags;
|
||||
signedData.writeUInt32BE(counter, offset);
|
||||
offset += 4;
|
||||
dataHash.copy(signedData, offset);
|
||||
|
||||
const der = sign("sha256", signedData, privateKey);
|
||||
const rawSig = convertSignature(der, "ecdsa-sha2-nistp256");
|
||||
assert.ok(Buffer.isBuffer(rawSig));
|
||||
const signature = skEcdsaSignature(rawSig, flags, counter);
|
||||
|
||||
const plainResult = plain.verify(message, signature);
|
||||
assert.equal(
|
||||
plainResult,
|
||||
true,
|
||||
plainResult instanceof Error ? plainResult.message : String(plainResult),
|
||||
);
|
||||
const certResult = cert.verify(message, signature);
|
||||
assert.equal(
|
||||
certResult,
|
||||
true,
|
||||
certResult instanceof Error ? certResult.message : String(certResult),
|
||||
);
|
||||
});
|
||||
@@ -114,13 +114,6 @@ function applyLegacyAlgorithms(algorithms) {
|
||||
]),
|
||||
);
|
||||
algorithms.serverHostKey = [
|
||||
"ssh-ed25519-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com",
|
||||
"rsa-sha2-512-cert-v01@openssh.com",
|
||||
"rsa-sha2-256-cert-v01@openssh.com",
|
||||
"ssh-rsa-cert-v01@openssh.com",
|
||||
"ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521",
|
||||
"rsa-sha2-512", "rsa-sha2-256",
|
||||
"ssh-rsa", "ssh-dss",
|
||||
@@ -157,16 +150,10 @@ function applyLegacyHmacAlgorithms(algorithms) {
|
||||
}
|
||||
|
||||
// Mirror of ssh2's DEFAULT_SERVER_HOST_KEY (see ssh2/lib/protocol/constants.js)
|
||||
// minus every ecdsa-sha2-* (plain keys and host certificates). Used when
|
||||
// skipEcdsaHostKey is on but legacy mode is off, so the field would otherwise
|
||||
// be undefined and ssh2 would fall back to its built-in defaults (which still
|
||||
// include ecdsa-sha2-*). Non-ECDSA host certificates stay so a cert-only
|
||||
// bastion can still be reached.
|
||||
// minus every ecdsa-sha2-*. Used when skipEcdsaHostKey is on but legacy mode
|
||||
// is off, so the field would otherwise be undefined and ssh2 would fall back
|
||||
// to its built-in defaults (which still include ecdsa-sha2-*).
|
||||
const MODERN_SERVER_HOST_KEY_WITHOUT_ECDSA = Object.freeze([
|
||||
"ssh-ed25519-cert-v01@openssh.com",
|
||||
"rsa-sha2-512-cert-v01@openssh.com",
|
||||
"rsa-sha2-256-cert-v01@openssh.com",
|
||||
"ssh-rsa-cert-v01@openssh.com",
|
||||
"ssh-ed25519",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
|
||||
@@ -269,19 +269,6 @@ for (const [label, buildAlgorithms] of [
|
||||
["SSH", sshBridge.buildAlgorithms],
|
||||
["SFTP", sftpBridge.buildSftpAlgorithms],
|
||||
]) {
|
||||
test(`${label} legacy serverHostKey offers OpenSSH host certificates before plain keys`, () => {
|
||||
withAlgorithmRuntime({}, () => {
|
||||
const keys = buildAlgorithms(true).serverHostKey;
|
||||
assert.ok(keys.includes("ssh-ed25519-cert-v01@openssh.com"));
|
||||
assert.ok(keys.indexOf("ssh-ed25519-cert-v01@openssh.com") < keys.indexOf("ssh-ed25519"));
|
||||
assert.ok(keys.includes("ecdsa-sha2-nistp256-cert-v01@openssh.com"));
|
||||
assert.ok(keys.includes("rsa-sha2-512-cert-v01@openssh.com"));
|
||||
assert.ok(keys.includes("rsa-sha2-256-cert-v01@openssh.com"));
|
||||
assert.ok(keys.includes("ssh-rsa-cert-v01@openssh.com"));
|
||||
assert.ok(keys.includes("ssh-dss"));
|
||||
});
|
||||
});
|
||||
|
||||
test(`${label} skipEcdsaHostKey removes every ecdsa-sha2-* from serverHostKey (legacy on)`, () => {
|
||||
withAlgorithmRuntime({}, () => {
|
||||
const algorithms = buildAlgorithms(true, { skipEcdsaHostKey: true });
|
||||
@@ -293,9 +280,6 @@ for (const [label, buildAlgorithms] of [
|
||||
assert.ok(algorithms.serverHostKey.includes("ssh-rsa"));
|
||||
assert.ok(algorithms.serverHostKey.includes("ssh-dss"));
|
||||
assert.ok(algorithms.serverHostKey.includes("ssh-ed25519"));
|
||||
assert.ok(algorithms.serverHostKey.includes("ssh-ed25519-cert-v01@openssh.com"));
|
||||
assert.ok(algorithms.serverHostKey.includes("rsa-sha2-512-cert-v01@openssh.com"));
|
||||
assert.ok(algorithms.serverHostKey.includes("ssh-rsa-cert-v01@openssh.com"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -309,10 +293,6 @@ for (const [label, buildAlgorithms] of [
|
||||
for (const algo of algorithms.serverHostKey) {
|
||||
assert.ok(!algo.startsWith("ecdsa-sha2-"), `${algo} should be filtered out`);
|
||||
}
|
||||
assert.ok(algorithms.serverHostKey.includes("ssh-ed25519-cert-v01@openssh.com"));
|
||||
assert.ok(algorithms.serverHostKey.includes("rsa-sha2-512-cert-v01@openssh.com"));
|
||||
assert.ok(algorithms.serverHostKey.includes("rsa-sha2-256-cert-v01@openssh.com"));
|
||||
assert.ok(algorithms.serverHostKey.includes("ssh-rsa-cert-v01@openssh.com"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -73,11 +73,6 @@ function normalizePassword(password) {
|
||||
return typeof password === "string" ? password : "";
|
||||
}
|
||||
|
||||
// Bare CR/LF used to wake a silent serial console before "login:" appears.
|
||||
function isAutoLoginWakeInput(data) {
|
||||
return typeof data === "string" && /^[\r\n]+$/.test(data);
|
||||
}
|
||||
|
||||
function createTelnetAutoLogin(options = {}) {
|
||||
const username = normalizeUsername(options.username);
|
||||
const password = normalizePassword(options.password);
|
||||
@@ -212,26 +207,7 @@ function createTelnetAutoLogin(options = {}) {
|
||||
notifyIncomplete();
|
||||
}
|
||||
},
|
||||
// Silent serial consoles need a bare CR/LF to wake getty before "login:"
|
||||
// appears. While the detector is still active that wake is not a
|
||||
// takeover: keep autofill armed, leave the caller's write on the port,
|
||||
// and do not consume the one-shot continue-prompt response. A later
|
||||
// "Press RETURN to continue" must still be answered. Once the detector
|
||||
// is disabled (expiry, completion, or a prior cancel), the same Enter
|
||||
// notifies like any other user input so callers can drop a deferred
|
||||
// startup command. Any other user input still cancels.
|
||||
handleUserInput(data) {
|
||||
const wakeOnly = isAutoLoginWakeInput(data);
|
||||
if (
|
||||
wakeOnly
|
||||
&& !disabled
|
||||
&& !sentUsername
|
||||
&& !sentPassword
|
||||
&& !isUsernamePrompt(tail)
|
||||
&& !isPasswordPrompt(tail)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
handleUserInput() {
|
||||
disabled = true;
|
||||
clearExpiryTimer();
|
||||
notifyUserInput();
|
||||
@@ -248,7 +224,6 @@ function createTelnetAutoLogin(options = {}) {
|
||||
|
||||
module.exports = {
|
||||
createTelnetAutoLogin,
|
||||
isAutoLoginWakeInput,
|
||||
isCommandPrompt,
|
||||
isContinuePrompt,
|
||||
isPasswordPrompt,
|
||||
|
||||
@@ -224,155 +224,6 @@ test("telnet auto-login stops when the user starts typing manually", () => {
|
||||
assert.equal(cancelCount, 1);
|
||||
});
|
||||
|
||||
test("telnet auto-login stays armed when a wake-only enter arrives before any login prompt", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
// Silent serial consoles need a bare CR to wake getty before "login:" appears.
|
||||
autoLogin.handleUserInput("\r");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 0);
|
||||
assert.deepEqual(writes, ["admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login treats a bare newline as a wake and still autofills", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
autoLogin.handleUserInput("\n");
|
||||
autoLogin.handleUserInput("\r\n");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 0);
|
||||
assert.deepEqual(writes, ["admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login notifies when a wake enter arrives after expiry", () => {
|
||||
let cancelCount = 0;
|
||||
let timerFn = null;
|
||||
const writes = [];
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
timeoutMs: 60_000,
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
setTimeout: (fn) => {
|
||||
timerFn = fn;
|
||||
return { unref() {} };
|
||||
},
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
|
||||
// Quiet device: the 60s window elapses with no prompt, which disables the
|
||||
// detector without a user-input notification. Serial startup still has its
|
||||
// 65s fallback armed until this cancellation arrives.
|
||||
timerFn();
|
||||
assert.equal(cancelCount, 0);
|
||||
autoLogin.handleUserInput("\r");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
autoLogin.handleText("Press RETURN to continue");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test("telnet auto-login still answers a continue prompt after a blind wake enter", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
// The user's Enter woke a silent console. It must not consume the
|
||||
// detector's one-shot response when the device then asks to continue.
|
||||
autoLogin.handleUserInput("\r");
|
||||
autoLogin.handleText("Press RETURN to continue");
|
||||
autoLogin.handleText("\r\nlogin: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 0);
|
||||
assert.deepEqual(writes, ["\r", "admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login still cancels when a normal character arrives before login", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
autoLogin.handleUserInput("x");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test("telnet auto-login still cancels an enter after credentials have started", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleUserInput("\r");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, ["admin\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login still cancels an enter when a login prompt is already in the tail", () => {
|
||||
let clock = 0;
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
now: () => clock,
|
||||
timeoutMs: 60_000,
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
// After the window, the prompt is recorded but credentials are not sent.
|
||||
clock = 60_001;
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleUserInput("\r");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test("telnet auto-login avoids common non-prompt login text", () => {
|
||||
const writes = [];
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
|
||||
@@ -63,7 +63,7 @@ const { discoverShells } = require("./shellDiscovery.cjs");
|
||||
const { isWindowsAppExecutionAliasPath } = require("../../lib/localShell.cjs");
|
||||
const moshHandshake = require("./moshHandshake.cjs");
|
||||
const tempDirBridge = require("./tempDirBridge.cjs");
|
||||
const { createTelnetAutoLogin, isAutoLoginWakeInput } = require("./telnetAutoLogin.cjs");
|
||||
const { createTelnetAutoLogin } = require("./telnetAutoLogin.cjs");
|
||||
const telnetProtocol = require("./telnetProtocol.cjs");
|
||||
const { createPtyOutputBuffer } = require("./ptyOutputBuffer.cjs");
|
||||
const { enableTcpNoDelay } = require("./tcpNoDelay.cjs");
|
||||
@@ -1713,9 +1713,8 @@ function writeToSessionNow(payload, data, logRewrite = payload.logRewrite) {
|
||||
(session.type === 'telnet-native' || session.type === 'serial')
|
||||
&& !payload.automated
|
||||
&& !isTerminalReportSequence(data)
|
||||
&& !payload.autoLoginInputHandled
|
||||
) {
|
||||
session.autoLogin?.handleUserInput(data);
|
||||
session.autoLogin?.handleUserInput();
|
||||
}
|
||||
|
||||
// Encode keystrokes with the SAME charset the output path decodes with so
|
||||
@@ -1806,8 +1805,6 @@ function writeToSessionWithInterception(
|
||||
paste = null,
|
||||
index = 0,
|
||||
) {
|
||||
// Stays the original payload until ingress classification marks this write.
|
||||
let sessionWritePayload = payload;
|
||||
const writeWithReceipt = (nextData) => {
|
||||
if (paste && !paste.active) return;
|
||||
const current = sessions.get(payload.sessionId);
|
||||
@@ -1820,7 +1817,7 @@ function writeToSessionWithInterception(
|
||||
paste.finish(index, true);
|
||||
return;
|
||||
}
|
||||
const written = writeToSessionNow(sessionWritePayload, nextData, logRewrite);
|
||||
const written = writeToSessionNow(payload, nextData, logRewrite);
|
||||
if (paste) paste.finish(written ? index : undefined);
|
||||
};
|
||||
const bypass = payload?.sensitive === true || isTerminalReportSequence(data);
|
||||
@@ -1833,24 +1830,20 @@ function writeToSessionWithInterception(
|
||||
writeWithReceipt(data);
|
||||
return;
|
||||
}
|
||||
// Classify auto-login at input ingress, before the asynchronous
|
||||
// Cancel the auto-login detector at input ingress, before the asynchronous
|
||||
// interception pipeline: a slow interceptor would otherwise leave the
|
||||
// detector armed while the user's keystrokes are queued, letting a login
|
||||
// prompt that arrives during that wait trigger a saved-credential
|
||||
// transmission after the user has already taken over. Same guard
|
||||
// conditions as writeToSessionNow. Wake-only CR/LF is not idempotent once
|
||||
// credentials start, so mark this write and do not classify those bytes
|
||||
// again when they reach writeToSessionNow. That preserved classification
|
||||
// describes the queued bytes only while they stay wake-only; writeIfCurrent
|
||||
// classifies an interceptor rewrite that is no longer wake-only.
|
||||
// conditions as writeToSessionNow; handleUserInput is idempotent, so the
|
||||
// later call there stays a no-op.
|
||||
if (
|
||||
expectedSession
|
||||
&& (expectedSession.type === 'telnet-native' || expectedSession.type === 'serial')
|
||||
&& !payload.automated
|
||||
&& !isTerminalReportSequence(data)
|
||||
) {
|
||||
expectedSession.autoLogin?.handleUserInput(data);
|
||||
sessionWritePayload = { ...payload, autoLoginInputHandled: true };
|
||||
expectedSession.autoLogin?.handleUserInput();
|
||||
}
|
||||
const writeIfCurrent = (nextData) => {
|
||||
const current = sessions.get(payload.sessionId);
|
||||
@@ -1858,19 +1851,6 @@ function writeToSessionWithInterception(
|
||||
paste?.finish();
|
||||
return;
|
||||
}
|
||||
// An Enter-to-command macro can turn the wake CR/LF classified above
|
||||
// into ordinary text. Classify that transformed value before it is
|
||||
// written. autoLoginInputHandled stays set so writeToSessionNow does not
|
||||
// classify the same write again. A rewrite that is still only CR/LF keeps
|
||||
// the ingress classification: those bytes are not idempotent once
|
||||
// credentials have started.
|
||||
if (
|
||||
sessionWritePayload.autoLoginInputHandled
|
||||
&& !isAutoLoginWakeInput(nextData)
|
||||
&& !isTerminalReportSequence(nextData)
|
||||
) {
|
||||
current.autoLogin?.handleUserInput(nextData);
|
||||
}
|
||||
writeWithReceipt(nextData);
|
||||
};
|
||||
const write = async () => {
|
||||
|
||||
@@ -1,32 +1,6 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const terminalBridge = require("./terminalBridge.cjs");
|
||||
const { createTelnetAutoLogin } = require("./telnetAutoLogin.cjs");
|
||||
|
||||
function createSerialAutoLoginSession() {
|
||||
const writes = [];
|
||||
const events = [];
|
||||
const session = {
|
||||
type: "serial",
|
||||
protocol: "serial",
|
||||
encoding: "utf-8",
|
||||
serialPort: {
|
||||
write: (data) => {
|
||||
writes.push(typeof data === "string" ? data : Buffer.from(data).toString());
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
session.autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => {
|
||||
session.serialPort.write(data);
|
||||
},
|
||||
onUserInput: () => { events.push("cancelled"); },
|
||||
});
|
||||
return { session, writes, events };
|
||||
}
|
||||
|
||||
test("serial auto-login cancels on interactive user input but not automated writes", () => {
|
||||
let userInputs = 0;
|
||||
@@ -87,221 +61,6 @@ test("serial line-mode buffered input notification cancels auto-login", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("serial wake-only enter keeps auto-login armed and still reaches the port", () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
terminalBridge.init({ sessions: new Map([["s", session]]), electronModule: {} });
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["\r"]);
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["\r", "admin\r", "secret\r"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial character input before login still cancels auto-login", () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
terminalBridge.init({ sessions: new Map([["s", session]]), electronModule: {} });
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "x" });
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, ["x"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial wake-only enter stays armed when an input interceptor is pending", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let interceptorResolved = false;
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (sessionId, data) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
interceptorResolved = true;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\n" });
|
||||
assert.deepEqual(events, []);
|
||||
assert.equal(interceptorResolved, false);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 40));
|
||||
assert.equal(interceptorResolved, true);
|
||||
assert.deepEqual(writes, ["\n"]);
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["\n", "admin\r", "secret\r"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial wake enter is not cancelled when login arrives during interceptor delay", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let interceptorStarted = false;
|
||||
let releaseInterceptor = () => {};
|
||||
const interceptorGate = new Promise((resolve) => {
|
||||
releaseInterceptor = resolve;
|
||||
});
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (_sessionId, data) => {
|
||||
interceptorStarted = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
await interceptorGate;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
await Promise.resolve();
|
||||
assert.equal(interceptorStarted, true);
|
||||
|
||||
// Prompt arrives while the wake byte is still queued in the interceptor.
|
||||
session.autoLogin.handleText("login: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r"]);
|
||||
|
||||
releaseInterceptor();
|
||||
const deadline = Date.now() + 500;
|
||||
while (writes.length < 2 && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r"]);
|
||||
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r", "secret\r"]);
|
||||
} finally {
|
||||
releaseInterceptor();
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial interceptor that rewrites a wake enter into text cancels auto-login", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let interceptorStarted = false;
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (_sessionId, data) => {
|
||||
interceptorStarted = true;
|
||||
assert.equal(data, "\r");
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
// Enter-to-command macro: the bare wake CR is no longer what is written.
|
||||
return "uptime\r";
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
await Promise.resolve();
|
||||
assert.equal(interceptorStarted, true);
|
||||
// Ingress still sees a wake, so autofill stays armed until the rewrite lands.
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, []);
|
||||
|
||||
const deadline = Date.now() + 500;
|
||||
while (writes.length < 1 && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, ["uptime\r"]);
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, ["uptime\r"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial interceptor that rewrites a wake enter into another wake still autofills", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let releaseInterceptor = () => {};
|
||||
const interceptorGate = new Promise((resolve) => {
|
||||
releaseInterceptor = resolve;
|
||||
});
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (_sessionId, data) => {
|
||||
assert.equal(data, "\r");
|
||||
await interceptorGate;
|
||||
return "\r\n";
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
await Promise.resolve();
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r"]);
|
||||
|
||||
releaseInterceptor();
|
||||
const deadline = Date.now() + 500;
|
||||
while (writes.length < 2 && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r\n"]);
|
||||
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r\n", "secret\r"]);
|
||||
} finally {
|
||||
releaseInterceptor();
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial line-mode notification still cancels auto-login without keystroke data", () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
terminalBridge.init({ sessions: new Map([["s", session]]), electronModule: {} });
|
||||
try {
|
||||
terminalBridge.notifySessionUserInput({}, { sessionId: "s" });
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, []);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial sessions without auto-login credentials accept input normally", () => {
|
||||
const writes = [];
|
||||
const session = {
|
||||
|
||||
+84
-308
@@ -1,8 +1,7 @@
|
||||
diff --git a/node_modules/ssh2/lib/agent.js b/node_modules/ssh2/lib/agent.js
|
||||
index bb495d1..32c48a0 100644
|
||||
--- a/node_modules/ssh2/lib/agent.js
|
||||
+++ b/node_modules/ssh2/lib/agent.js
|
||||
@@ -6,7 +6,7 @@ const { resolve } = require('path');
|
||||
@@ -6,7 +6,7 @@
|
||||
const { readFile } = require('fs');
|
||||
const { execFile, spawn } = require('child_process');
|
||||
|
||||
@@ -11,7 +10,7 @@ index bb495d1..32c48a0 100644
|
||||
|
||||
const {
|
||||
makeBufferParser,
|
||||
@@ -655,8 +655,12 @@ const AgentProtocol = (() => {
|
||||
@@ -655,8 +655,12 @@
|
||||
// TODO: verify signature type based on key and options used
|
||||
// during initial sign request
|
||||
binaryParser.init(signature, 0);
|
||||
@@ -26,7 +25,7 @@ index bb495d1..32c48a0 100644
|
||||
binaryParser.clear();
|
||||
|
||||
if (signature === undefined)
|
||||
@@ -711,7 +715,10 @@ const AgentProtocol = (() => {
|
||||
@@ -711,7 +715,10 @@
|
||||
hash: undefined,
|
||||
};
|
||||
let ctx;
|
||||
@@ -38,7 +37,7 @@ index bb495d1..32c48a0 100644
|
||||
if (flagsVal & SSH_AGENT_RSA_SHA2_256) {
|
||||
ctx = 'rsa-sha2-256';
|
||||
flags.hash = 'sha256';
|
||||
@@ -721,7 +728,7 @@ const AgentProtocol = (() => {
|
||||
@@ -721,7 +728,7 @@
|
||||
}
|
||||
}
|
||||
if (ctx === undefined)
|
||||
@@ -47,7 +46,7 @@ index bb495d1..32c48a0 100644
|
||||
|
||||
const req = new AgentInboundRequest(msgType, ctx);
|
||||
this[SYM_REQS].push(req);
|
||||
@@ -732,6 +739,9 @@ const AgentProtocol = (() => {
|
||||
@@ -732,6 +739,9 @@
|
||||
default: {
|
||||
const req = new AgentInboundRequest(msgType);
|
||||
this[SYM_REQS].push(req);
|
||||
@@ -57,7 +56,7 @@ index bb495d1..32c48a0 100644
|
||||
this.failureReply(req);
|
||||
}
|
||||
}
|
||||
@@ -781,7 +791,11 @@ const AgentProtocol = (() => {
|
||||
@@ -781,7 +791,11 @@
|
||||
if (pubKey instanceof Error)
|
||||
throw new Error('Invalid public key argument');
|
||||
|
||||
@@ -70,7 +69,7 @@ index bb495d1..32c48a0 100644
|
||||
switch (options.hash) {
|
||||
case 'sha256':
|
||||
flags = SSH_AGENT_RSA_SHA2_256;
|
||||
@@ -992,19 +1006,24 @@ const AgentProtocol = (() => {
|
||||
@@ -992,19 +1006,24 @@
|
||||
let p = 0;
|
||||
const sigFormat = req.getContext();
|
||||
const sigFormatLen = Buffer.byteLength(sigFormat);
|
||||
@@ -100,10 +99,9 @@ index bb495d1..32c48a0 100644
|
||||
return respond(this, req, buf);
|
||||
}
|
||||
diff --git a/node_modules/ssh2/lib/client.js b/node_modules/ssh2/lib/client.js
|
||||
index 7291c2c..8943c9a 100644
|
||||
--- a/node_modules/ssh2/lib/client.js
|
||||
+++ b/node_modules/ssh2/lib/client.js
|
||||
@@ -2170,6 +2170,29 @@ function getKeyAlgos(client, key, serverSigAlgs) {
|
||||
@@ -2170,6 +2170,29 @@
|
||||
return [];
|
||||
}
|
||||
return [['ssh-rsa', 'sha1']];
|
||||
@@ -134,10 +132,9 @@ index 7291c2c..8943c9a 100644
|
||||
}
|
||||
|
||||
diff --git a/node_modules/ssh2/lib/protocol/Protocol.js b/node_modules/ssh2/lib/protocol/Protocol.js
|
||||
index 7302488..df0c9b3 100644
|
||||
--- a/node_modules/ssh2/lib/protocol/Protocol.js
|
||||
+++ b/node_modules/ssh2/lib/protocol/Protocol.js
|
||||
@@ -58,6 +58,7 @@ const {
|
||||
@@ -58,6 +58,7 @@
|
||||
} = require('./kex.js');
|
||||
const {
|
||||
parseKey,
|
||||
@@ -145,7 +142,7 @@ index 7302488..df0c9b3 100644
|
||||
} = require('./keyParser.js');
|
||||
const MESSAGE_HANDLERS = require('./handlers.js');
|
||||
const {
|
||||
@@ -701,11 +702,19 @@ class Protocol {
|
||||
@@ -701,11 +702,19 @@
|
||||
if (signature === false)
|
||||
throw new Error('Error while converting handshake signature');
|
||||
|
||||
@@ -166,7 +163,7 @@ index 7302488..df0c9b3 100644
|
||||
);
|
||||
|
||||
// TODO: simply copy from original "packet" to new `packet` to avoid
|
||||
@@ -729,13 +738,17 @@ class Protocol {
|
||||
@@ -729,13 +738,17 @@
|
||||
writeUInt32BE(packet, pubKeyLen, p += algoLen);
|
||||
packet.set(pubKey, p += 4);
|
||||
|
||||
@@ -189,7 +186,7 @@ index 7302488..df0c9b3 100644
|
||||
|
||||
// Servers shouldn't send packet type 60 in response to signed publickey
|
||||
// attempts, but if they do, interpret as type 60.
|
||||
@@ -810,20 +823,32 @@ class Protocol {
|
||||
@@ -810,20 +823,32 @@
|
||||
if (!signature)
|
||||
throw new Error('Error while converting handshake signature');
|
||||
|
||||
@@ -228,7 +225,7 @@ index 7302488..df0c9b3 100644
|
||||
|
||||
this._authsQueue.push('hostbased');
|
||||
|
||||
@@ -1916,7 +1941,10 @@ class Protocol {
|
||||
@@ -1916,7 +1941,10 @@
|
||||
}
|
||||
|
||||
// SSH-protoversion-softwareversion (SP comments) CR LF
|
||||
@@ -241,10 +238,9 @@ index 7302488..df0c9b3 100644
|
||||
// TODO: optimize this by starting n bytes from the end of this._buffer instead
|
||||
// of the beginning
|
||||
diff --git a/node_modules/ssh2/lib/protocol/SFTP.js b/node_modules/ssh2/lib/protocol/SFTP.js
|
||||
index 9f33c02..9751164 100644
|
||||
--- a/node_modules/ssh2/lib/protocol/SFTP.js
|
||||
+++ b/node_modules/ssh2/lib/protocol/SFTP.js
|
||||
@@ -117,6 +117,20 @@ const OPENSSH_MAX_PKT_LEN = 256 * 1024;
|
||||
@@ -117,6 +117,20 @@
|
||||
|
||||
const bufferParser = makeBufferParser();
|
||||
|
||||
@@ -265,7 +261,7 @@ index 9f33c02..9751164 100644
|
||||
const fakeStderr = {
|
||||
readable: false,
|
||||
writable: false,
|
||||
@@ -155,6 +169,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -155,6 +169,8 @@
|
||||
this._writeReqid = -1;
|
||||
this._requests = {};
|
||||
this._maxInPktLen = OPENSSH_MAX_PKT_LEN;
|
||||
@@ -274,7 +270,7 @@ index 9f33c02..9751164 100644
|
||||
this._maxOutPktLen = 34000;
|
||||
this._maxReadLen =
|
||||
(this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD;
|
||||
@@ -196,6 +212,53 @@ class SFTP extends EventEmitter {
|
||||
@@ -196,6 +212,53 @@
|
||||
this.emit('end');
|
||||
return;
|
||||
}
|
||||
@@ -328,7 +324,7 @@ index 9f33c02..9751164 100644
|
||||
/*
|
||||
uint32 length
|
||||
byte type
|
||||
@@ -339,7 +402,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -339,7 +402,7 @@
|
||||
uint32 pflags
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -337,7 +333,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen);
|
||||
|
||||
@@ -349,7 +412,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -349,7 +412,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -346,7 +342,7 @@ index 9f33c02..9751164 100644
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
writeUInt32BE(buf, attrsFlags, p += 4);
|
||||
if (attrsLen) {
|
||||
@@ -734,7 +797,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -734,7 +797,7 @@
|
||||
uint32 id
|
||||
string filename
|
||||
*/
|
||||
@@ -355,7 +351,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + fnameLen);
|
||||
|
||||
@@ -744,7 +807,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -744,7 +807,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, fnameLen, p);
|
||||
@@ -364,7 +360,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -762,8 +825,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -762,8 +825,8 @@
|
||||
string oldpath
|
||||
string newpath
|
||||
*/
|
||||
@@ -375,7 +371,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + oldLen + 4 + newLen);
|
||||
|
||||
@@ -773,9 +836,9 @@ class SFTP extends EventEmitter {
|
||||
@@ -773,9 +836,9 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, oldLen, p);
|
||||
@@ -387,7 +383,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -806,7 +869,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -806,7 +869,7 @@
|
||||
string path
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -396,7 +392,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
|
||||
|
||||
@@ -816,7 +879,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -816,7 +879,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -405,7 +401,7 @@ index 9f33c02..9751164 100644
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
if (attrsLen) {
|
||||
p += 4;
|
||||
@@ -844,7 +907,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -844,7 +907,7 @@
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -414,7 +410,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -854,7 +917,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -854,7 +917,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -423,7 +419,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -987,7 +1050,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -987,7 +1050,7 @@
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -432,7 +428,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -997,7 +1060,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -997,7 +1060,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -441,7 +437,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1014,7 +1077,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1014,7 +1077,7 @@
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -450,7 +446,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1024,7 +1087,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1024,7 +1087,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -459,7 +455,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1041,7 +1104,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1041,7 +1104,7 @@
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -468,7 +464,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1051,7 +1114,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1051,7 +1114,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -477,7 +473,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1080,7 +1143,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1080,7 +1143,7 @@
|
||||
string path
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -486,7 +482,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
|
||||
|
||||
@@ -1090,7 +1153,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1090,7 +1153,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -495,7 +491,7 @@ index 9f33c02..9751164 100644
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
if (attrsLen) {
|
||||
p += 4;
|
||||
@@ -1205,7 +1268,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1205,7 +1268,7 @@
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -504,7 +500,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1215,7 +1278,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1215,7 +1278,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -513,7 +509,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = {
|
||||
cb: (err, names) => {
|
||||
@@ -1243,8 +1306,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -1243,8 +1306,8 @@
|
||||
string linkpath
|
||||
string targetpath
|
||||
*/
|
||||
@@ -524,7 +520,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + linkLen + 4 + targetLen);
|
||||
|
||||
@@ -1256,14 +1319,14 @@ class SFTP extends EventEmitter {
|
||||
@@ -1256,14 +1319,14 @@
|
||||
if (this._isOpenSSH) {
|
||||
// OpenSSH has linkpath and targetpath positions switched
|
||||
writeUInt32BE(buf, targetLen, p);
|
||||
@@ -543,7 +539,7 @@ index 9f33c02..9751164 100644
|
||||
}
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
@@ -1281,7 +1344,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1281,7 +1344,7 @@
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -552,7 +548,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1291,7 +1354,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1291,7 +1354,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -561,7 +557,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = {
|
||||
cb: (err, names) => {
|
||||
@@ -1325,8 +1388,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -1325,8 +1388,8 @@
|
||||
string oldpath
|
||||
string newpath
|
||||
*/
|
||||
@@ -572,7 +568,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf =
|
||||
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 24 + 4 + oldLen + 4 + newLen);
|
||||
@@ -1337,11 +1400,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -1337,11 +1400,11 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 24, p);
|
||||
@@ -587,7 +583,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1364,7 +1427,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1364,7 +1427,7 @@
|
||||
string "statvfs@openssh.com"
|
||||
string path
|
||||
*/
|
||||
@@ -596,7 +592,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen);
|
||||
|
||||
@@ -1374,9 +1437,9 @@ class SFTP extends EventEmitter {
|
||||
@@ -1374,9 +1437,9 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 19, p);
|
||||
@@ -608,7 +604,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { extended: 'statvfs@openssh.com', cb };
|
||||
|
||||
@@ -1411,7 +1474,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1411,7 +1474,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 20, p);
|
||||
@@ -617,7 +613,7 @@ index 9f33c02..9751164 100644
|
||||
writeUInt32BE(buf, handleLen, p += 20);
|
||||
buf.set(handle, p += 4);
|
||||
|
||||
@@ -1437,8 +1500,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -1437,8 +1500,8 @@
|
||||
string oldpath
|
||||
string newpath
|
||||
*/
|
||||
@@ -628,7 +624,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf =
|
||||
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + oldLen + 4 + newLen);
|
||||
@@ -1449,11 +1512,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -1449,11 +1512,11 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 20, p);
|
||||
@@ -643,7 +639,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1488,7 +1551,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1488,7 +1551,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 17, p);
|
||||
@@ -652,7 +648,7 @@ index 9f33c02..9751164 100644
|
||||
writeUInt32BE(buf, handleLen, p += 17);
|
||||
buf.set(handle, p += 4);
|
||||
|
||||
@@ -1524,7 +1587,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1524,7 +1587,7 @@
|
||||
string path
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -661,7 +657,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf =
|
||||
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen);
|
||||
@@ -1535,10 +1598,10 @@ class SFTP extends EventEmitter {
|
||||
@@ -1535,10 +1598,10 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 20, p);
|
||||
@@ -674,7 +670,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
if (attrsLen) {
|
||||
@@ -1573,7 +1636,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1573,7 +1636,7 @@
|
||||
string "expand-path@openssh.com"
|
||||
string path
|
||||
*/
|
||||
@@ -683,7 +679,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen);
|
||||
|
||||
@@ -1583,10 +1646,10 @@ class SFTP extends EventEmitter {
|
||||
@@ -1583,10 +1646,10 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 23, p);
|
||||
@@ -696,7 +692,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
this._requests[reqid] = {
|
||||
cb: (err, names) => {
|
||||
@@ -1653,7 +1716,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1653,7 +1716,7 @@
|
||||
|
||||
writeUInt32BE(buf, 9, p);
|
||||
p += 4;
|
||||
@@ -705,7 +701,7 @@ index 9f33c02..9751164 100644
|
||||
p += 9;
|
||||
|
||||
writeUInt32BE(buf, srcHandle.length, p);
|
||||
@@ -1708,7 +1771,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1708,7 +1771,7 @@
|
||||
string username
|
||||
*/
|
||||
let p = 0;
|
||||
@@ -714,7 +710,7 @@ index 9f33c02..9751164 100644
|
||||
const buf = Buffer.allocUnsafe(
|
||||
4 + 1
|
||||
+ 4
|
||||
@@ -1728,12 +1791,12 @@ class SFTP extends EventEmitter {
|
||||
@@ -1728,12 +1791,12 @@
|
||||
|
||||
writeUInt32BE(buf, 14, p);
|
||||
p += 4;
|
||||
@@ -729,7 +725,7 @@ index 9f33c02..9751164 100644
|
||||
p += usernameLen;
|
||||
|
||||
this._requests[reqid] = {
|
||||
@@ -1806,7 +1869,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1806,7 +1869,7 @@
|
||||
|
||||
writeUInt32BE(buf, 30, p);
|
||||
p += 4;
|
||||
@@ -738,7 +734,7 @@ index 9f33c02..9751164 100644
|
||||
p += 30;
|
||||
|
||||
writeUInt32BE(buf, 4 * uids.length, p);
|
||||
@@ -1871,7 +1934,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1871,7 +1934,7 @@
|
||||
|
||||
message || (message = '');
|
||||
|
||||
@@ -747,7 +743,7 @@ index 9f33c02..9751164 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 4 + msgLen + 4);
|
||||
|
||||
@@ -1884,7 +1947,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1884,7 +1947,7 @@
|
||||
writeUInt32BE(buf, msgLen, p += 4);
|
||||
p += 4;
|
||||
if (msgLen) {
|
||||
@@ -756,7 +752,7 @@ index 9f33c02..9751164 100644
|
||||
p += msgLen;
|
||||
}
|
||||
|
||||
@@ -1913,7 +1976,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1913,7 +1976,7 @@
|
||||
const dataLen = (
|
||||
isBuffer
|
||||
? data.length
|
||||
@@ -765,7 +761,7 @@ index 9f33c02..9751164 100644
|
||||
);
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + dataLen);
|
||||
@@ -1927,7 +1990,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1927,7 +1990,7 @@
|
||||
if (isBuffer)
|
||||
buf.set(data, p += 4);
|
||||
else if (isUTF8)
|
||||
@@ -774,7 +770,7 @@ index 9f33c02..9751164 100644
|
||||
else
|
||||
buf.write(data, p += 4, dataLen, encoding);
|
||||
}
|
||||
@@ -1959,13 +2022,13 @@ class SFTP extends EventEmitter {
|
||||
@@ -1959,13 +2022,13 @@
|
||||
? ''
|
||||
: name.filename
|
||||
);
|
||||
@@ -790,7 +786,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
if (typeof name.attrs === 'object' && name.attrs !== null) {
|
||||
nameAttrs = attrsToBytes(name.attrs);
|
||||
@@ -2011,11 +2074,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -2011,11 +2074,11 @@
|
||||
? ''
|
||||
: name.filename
|
||||
);
|
||||
@@ -804,7 +800,7 @@ index 9f33c02..9751164 100644
|
||||
p += len;
|
||||
}
|
||||
}
|
||||
@@ -2026,11 +2089,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -2026,11 +2089,11 @@
|
||||
? ''
|
||||
: name.longname
|
||||
);
|
||||
@@ -818,7 +814,7 @@ index 9f33c02..9751164 100644
|
||||
p += len;
|
||||
}
|
||||
}
|
||||
@@ -2749,7 +2812,7 @@ function requestLimits(sftp, cb) {
|
||||
@@ -2749,7 +2812,7 @@
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 18, p);
|
||||
@@ -827,7 +823,7 @@ index 9f33c02..9751164 100644
|
||||
|
||||
sftp._requests[reqid] = { extended: 'limits@openssh.com', cb };
|
||||
|
||||
@@ -2953,18 +3016,28 @@ const CLIENT_HANDLERS = {
|
||||
@@ -2953,18 +3016,28 @@
|
||||
// spec not specifying an encoding because the specs for newer
|
||||
// versions of the protocol all explicitly specify UTF-8 for
|
||||
// filenames
|
||||
@@ -860,41 +856,21 @@ index 9f33c02..9751164 100644
|
||||
if (names !== undefined) {
|
||||
sftp._debug && sftp._debug(
|
||||
diff --git a/node_modules/ssh2/lib/protocol/constants.js b/node_modules/ssh2/lib/protocol/constants.js
|
||||
index ad77592..a249e00 100644
|
||||
--- a/node_modules/ssh2/lib/protocol/constants.js
|
||||
+++ b/node_modules/ssh2/lib/protocol/constants.js
|
||||
@@ -71,9 +71,30 @@ const DEFAULT_SERVER_HOST_KEY = [
|
||||
];
|
||||
if (eddsaSupported)
|
||||
@@ -73,7 +73,11 @@
|
||||
DEFAULT_SERVER_HOST_KEY.unshift('ssh-ed25519');
|
||||
+// OpenSSH host certificates, offered before the matching plain key so a
|
||||
+// cert-only server (for example ssh-ed25519-cert-v01@openssh.com) can
|
||||
+// negotiate. Plain keys stay as fallbacks.
|
||||
+const OPENSSH_HOST_CERT_ALGS = [
|
||||
+ 'ecdsa-sha2-nistp256-cert-v01@openssh.com',
|
||||
+ 'ecdsa-sha2-nistp384-cert-v01@openssh.com',
|
||||
+ 'ecdsa-sha2-nistp521-cert-v01@openssh.com',
|
||||
+ 'rsa-sha2-512-cert-v01@openssh.com',
|
||||
+ 'rsa-sha2-256-cert-v01@openssh.com',
|
||||
+ 'ssh-rsa-cert-v01@openssh.com',
|
||||
+];
|
||||
+if (eddsaSupported)
|
||||
+ OPENSSH_HOST_CERT_ALGS.unshift('ssh-ed25519-cert-v01@openssh.com');
|
||||
+DEFAULT_SERVER_HOST_KEY.unshift(...OPENSSH_HOST_CERT_ALGS);
|
||||
const SUPPORTED_SERVER_HOST_KEY = DEFAULT_SERVER_HOST_KEY.concat([
|
||||
'ssh-dss',
|
||||
+ // FIDO/U2F security keys (client publickey auth / rare host keys).
|
||||
+ 'sk-ecdsa-sha2-nistp256@openssh.com',
|
||||
+ 'sk-ecdsa-sha2-nistp256-cert-v01@openssh.com',
|
||||
]);
|
||||
+if (eddsaSupported) {
|
||||
+if (eddsaSupported)
|
||||
+ SUPPORTED_SERVER_HOST_KEY.push('sk-ssh-ed25519@openssh.com');
|
||||
+ SUPPORTED_SERVER_HOST_KEY.push('sk-ssh-ed25519-cert-v01@openssh.com');
|
||||
+}
|
||||
|
||||
|
||||
const canUseCipher = (() => {
|
||||
@@ -160,6 +181,7 @@ const COMPAT = {
|
||||
@@ -160,6 +164,7 @@
|
||||
DYN_RPORT_BUG: 1 << 2,
|
||||
BUG_DHGEX_LARGE: 1 << 3,
|
||||
IMPLY_RSA_SHA2_SIGALGS: 1 << 4,
|
||||
@@ -902,7 +878,7 @@ index ad77592..a249e00 100644
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -330,6 +352,7 @@ module.exports = {
|
||||
@@ -330,6 +335,7 @@
|
||||
COMPAT_CHECKS: [
|
||||
[ 'Cisco-1.25', COMPAT.BAD_DHGEX ],
|
||||
[ /^Cisco-1[.]/, COMPAT.BUG_DHGEX_LARGE ],
|
||||
@@ -911,198 +887,9 @@ index ad77592..a249e00 100644
|
||||
[ /^OpenSSH_5[.][0-9]+/, COMPAT.DYN_RPORT_BUG ],
|
||||
[ /^OpenSSH_7[.]4/, COMPAT.IMPLY_RSA_SHA2_SIGALGS ],
|
||||
diff --git a/node_modules/ssh2/lib/protocol/kex.js b/node_modules/ssh2/lib/protocol/kex.js
|
||||
index 811e631..422a198 100644
|
||||
--- a/node_modules/ssh2/lib/protocol/kex.js
|
||||
+++ b/node_modules/ssh2/lib/protocol/kex.js
|
||||
@@ -30,7 +30,10 @@ const {
|
||||
createDecipher,
|
||||
MAC_INFO,
|
||||
} = require('./crypto.js');
|
||||
-const { parseDERKey } = require('./keyParser.js');
|
||||
+const {
|
||||
+ parseDERKey,
|
||||
+ getOpenSSHCertSignatureAlgorithm,
|
||||
+} = require('./keyParser.js');
|
||||
const {
|
||||
bufferFill,
|
||||
bufferParser,
|
||||
@@ -49,6 +52,68 @@ const {
|
||||
|
||||
let MESSAGE_HANDLERS;
|
||||
|
||||
+const LEGACY_RSA_HOST_CERT = /^ssh-rsa-cert-v0[01]@openssh\.com$/;
|
||||
+const RSA_SHA2_256_HOST_CERT = /^rsa-sha2-256-cert-v0[01]@openssh\.com$/;
|
||||
+const RSA_SHA2_512_HOST_CERT = /^rsa-sha2-512-cert-v0[01]@openssh\.com$/;
|
||||
+const LEGACY_RSA_HOST_SIGNATURES = new Set([
|
||||
+ 'rsa-sha2-256',
|
||||
+ 'rsa-sha2-512',
|
||||
+ 'ssh-rsa',
|
||||
+]);
|
||||
+
|
||||
+// Client KEX accepts the negotiated host-key name or, for OpenSSH host
|
||||
+// certificates, the underlying key algorithm. Real servers sign with the
|
||||
+// embedded key (ssh-ed25519, ecdsa-sha2-*, rsa-sha2-*) rather than the
|
||||
+// certificate algorithm name. OpenSSH allows any RSA signature only for the
|
||||
+// legacy ssh-rsa certificate. rsa-sha2-256/512 certificates must use the
|
||||
+// hash that was negotiated.
|
||||
+function hostKeySignatureTypeAccepted(negotiatedServerHostKey, sigType) {
|
||||
+ if (RSA_SHA2_256_HOST_CERT.test(negotiatedServerHostKey))
|
||||
+ return sigType === 'rsa-sha2-256';
|
||||
+ if (RSA_SHA2_512_HOST_CERT.test(negotiatedServerHostKey))
|
||||
+ return sigType === 'rsa-sha2-512';
|
||||
+ if (sigType === negotiatedServerHostKey)
|
||||
+ return true;
|
||||
+ if (sigType === getOpenSSHCertSignatureAlgorithm(negotiatedServerHostKey))
|
||||
+ return true;
|
||||
+ return LEGACY_RSA_HOST_CERT.test(negotiatedServerHostKey)
|
||||
+ && LEGACY_RSA_HOST_SIGNATURES.has(sigType);
|
||||
+}
|
||||
+
|
||||
+// Plain rsa-sha2-* keeps an ssh-rsa key blob. An RSA certificate negotiation
|
||||
+// is KEY_RSA_CERT: the blob must be ssh-rsa-cert-v01@openssh.com, never a
|
||||
+// plain ssh-rsa key.
|
||||
+function hostKeyBlobTypeAccepted(negotiatedServerHostKey, hostPubKeyType) {
|
||||
+ if (hostPubKeyType === negotiatedServerHostKey)
|
||||
+ return true;
|
||||
+ switch (negotiatedServerHostKey) {
|
||||
+ case 'rsa-sha2-256':
|
||||
+ case 'rsa-sha2-512':
|
||||
+ return hostPubKeyType === 'ssh-rsa';
|
||||
+ case 'rsa-sha2-256-cert-v01@openssh.com':
|
||||
+ case 'rsa-sha2-512-cert-v01@openssh.com':
|
||||
+ return hostPubKeyType === 'ssh-rsa-cert-v01@openssh.com';
|
||||
+ default:
|
||||
+ return false;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+function hashAlgoForHostKey(negotiatedServerHostKey, sigType) {
|
||||
+ // Negotiated RSA-SHA2 wins over the signature label so a mismatched
|
||||
+ // signature cannot select a weaker hash.
|
||||
+ if (negotiatedServerHostKey === 'rsa-sha2-256'
|
||||
+ || RSA_SHA2_256_HOST_CERT.test(negotiatedServerHostKey))
|
||||
+ return 'sha256';
|
||||
+ if (negotiatedServerHostKey === 'rsa-sha2-512'
|
||||
+ || RSA_SHA2_512_HOST_CERT.test(negotiatedServerHostKey))
|
||||
+ return 'sha512';
|
||||
+ if (sigType === 'rsa-sha2-256')
|
||||
+ return 'sha256';
|
||||
+ if (sigType === 'rsa-sha2-512')
|
||||
+ return 'sha512';
|
||||
+ return undefined;
|
||||
+}
|
||||
+
|
||||
const GEX_MIN_BITS = 2048; // RFC 8270
|
||||
const GEX_MAX_BITS = 8192; // RFC 8270
|
||||
|
||||
@@ -635,7 +700,7 @@ const createKeyExchange = (() => {
|
||||
);
|
||||
}
|
||||
|
||||
- if (sigType !== negotiated.serverHostKey) {
|
||||
+ if (!hostKeySignatureTypeAccepted(negotiated.serverHostKey, sigType)) {
|
||||
return doFatalError(
|
||||
this._protocol,
|
||||
`Wrong signature type: ${sigType}, `
|
||||
@@ -659,7 +724,10 @@ const createKeyExchange = (() => {
|
||||
);
|
||||
}
|
||||
|
||||
- if (!(sigValue = sigSSHToASN1(sigValue, sigType))) {
|
||||
+ if (!(sigValue = sigSSHToASN1(
|
||||
+ sigValue,
|
||||
+ getOpenSSHCertSignatureAlgorithm(sigType)
|
||||
+ ))) {
|
||||
return doFatalError(
|
||||
this._protocol,
|
||||
'Malformed signature',
|
||||
@@ -685,12 +753,10 @@ const createKeyExchange = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
- let hashAlgo;
|
||||
- // Check if we need to override the default hash algorithm
|
||||
- switch (this.negotiated.serverHostKey) {
|
||||
- case 'rsa-sha2-256': hashAlgo = 'sha256'; break;
|
||||
- case 'rsa-sha2-512': hashAlgo = 'sha512'; break;
|
||||
- }
|
||||
+ const hashAlgo = hashAlgoForHostKey(
|
||||
+ this.negotiated.serverHostKey,
|
||||
+ sigType
|
||||
+ );
|
||||
|
||||
this._protocol._debug
|
||||
&& this._protocol._debug('Verifying signature ...');
|
||||
@@ -717,12 +783,7 @@ const createKeyExchange = (() => {
|
||||
} else {
|
||||
// Server
|
||||
|
||||
- let hashAlgo;
|
||||
- // Check if we need to override the default hash algorithm
|
||||
- switch (this.negotiated.serverHostKey) {
|
||||
- case 'rsa-sha2-256': hashAlgo = 'sha256'; break;
|
||||
- case 'rsa-sha2-512': hashAlgo = 'sha512'; break;
|
||||
- }
|
||||
+ const hashAlgo = hashAlgoForHostKey(this.negotiated.serverHostKey);
|
||||
|
||||
this._protocol._debug && this._protocol._debug(
|
||||
'Generating signature ...'
|
||||
@@ -739,7 +800,12 @@ const createKeyExchange = (() => {
|
||||
);
|
||||
}
|
||||
|
||||
- signature = convertSignature(signature, this._hostKey.type);
|
||||
+ // OpenSSH signs a host certificate with the embedded key algorithm,
|
||||
+ // not the certificate algorithm name.
|
||||
+ const sigType = getOpenSSHCertSignatureAlgorithm(
|
||||
+ this.negotiated.serverHostKey
|
||||
+ );
|
||||
+ signature = convertSignature(signature, sigType);
|
||||
if (signature === false) {
|
||||
return doFatalError(
|
||||
this._protocol,
|
||||
@@ -759,7 +825,6 @@ const createKeyExchange = (() => {
|
||||
string <method-specific data>
|
||||
string signature of H
|
||||
*/
|
||||
- const sigType = this.negotiated.serverHostKey;
|
||||
const sigTypeLen = Buffer.byteLength(sigType);
|
||||
const sigLen = 4 + sigTypeLen + 4 + signature.length;
|
||||
let p = this._protocol._packetRW.write.allocStartKEX;
|
||||
@@ -1167,22 +1232,16 @@ const createKeyExchange = (() => {
|
||||
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
||||
);
|
||||
}
|
||||
- if (hostPubKeyType !== this.negotiated.serverHostKey) {
|
||||
- // Check if we need to make an exception
|
||||
- switch (this.negotiated.serverHostKey) {
|
||||
- case 'rsa-sha2-256':
|
||||
- case 'rsa-sha2-512':
|
||||
- if (hostPubKeyType === 'ssh-rsa')
|
||||
- break;
|
||||
- // FALLTHROUGH
|
||||
- default:
|
||||
- return doFatalError(
|
||||
- this._protocol,
|
||||
- 'Host key does not match negotiated type',
|
||||
- 'handshake',
|
||||
- DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
||||
- );
|
||||
- }
|
||||
+ if (!hostKeyBlobTypeAccepted(
|
||||
+ this.negotiated.serverHostKey,
|
||||
+ hostPubKeyType
|
||||
+ )) {
|
||||
+ return doFatalError(
|
||||
+ this._protocol,
|
||||
+ 'Host key does not match negotiated type',
|
||||
+ 'handshake',
|
||||
+ DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
||||
+ );
|
||||
}
|
||||
|
||||
this._hostKey = hostPubKey;
|
||||
@@ -1377,8 +1436,13 @@ const createKeyExchange = (() => {
|
||||
@@ -1377,8 +1377,13 @@
|
||||
this._generator = null;
|
||||
this._minBits = GEX_MIN_BITS;
|
||||
this._prefBits = dhEstimate(this.negotiated);
|
||||
@@ -1117,20 +904,10 @@ index 811e631..422a198 100644
|
||||
this._maxBits = GEX_MAX_BITS;
|
||||
}
|
||||
start() {
|
||||
@@ -1868,6 +1932,8 @@ function trySendNEWKEYS(kex) {
|
||||
|
||||
module.exports = {
|
||||
KexInit,
|
||||
+ hostKeyBlobTypeAccepted,
|
||||
+ hostKeySignatureTypeAccepted,
|
||||
kexinit,
|
||||
onKEXPayload,
|
||||
DEFAULT_KEXINIT_CLIENT: new KexInit({
|
||||
diff --git a/node_modules/ssh2/lib/protocol/keyParser.js b/node_modules/ssh2/lib/protocol/keyParser.js
|
||||
index a276c1a..c8ff9d1 100644
|
||||
--- a/node_modules/ssh2/lib/protocol/keyParser.js
|
||||
+++ b/node_modules/ssh2/lib/protocol/keyParser.js
|
||||
@@ -35,6 +35,9 @@ const SYM_PRIV_PEM = Symbol('Private key PEM');
|
||||
@@ -35,6 +35,9 @@
|
||||
const SYM_PUB_PEM = Symbol('Public key PEM');
|
||||
const SYM_PUB_SSH = Symbol('Public key SSH');
|
||||
const SYM_DECRYPTED = Symbol('Decrypted Key');
|
||||
@@ -1140,7 +917,7 @@ index a276c1a..c8ff9d1 100644
|
||||
|
||||
// Create OpenSSL cipher name -> SSH cipher name conversion table
|
||||
const CIPHER_INFO_OPENSSL = Object.create(null);
|
||||
@@ -258,6 +261,55 @@ function genOpenSSHEdPub(pub) {
|
||||
@@ -258,6 +261,55 @@
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@@ -1196,7 +973,7 @@ index a276c1a..c8ff9d1 100644
|
||||
function genOpenSSLEdPriv(priv) {
|
||||
const asnWriter = new Ber.Writer();
|
||||
asnWriter.startSequence();
|
||||
@@ -451,6 +503,82 @@ const BaseKey = {
|
||||
@@ -451,6 +503,82 @@
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1258,7 +1035,7 @@ index a276c1a..c8ff9d1 100644
|
||||
+ signedData.set(dataHash, p);
|
||||
+
|
||||
+ let toVerify = rawSig;
|
||||
+ if (getOpenSSHCertBaseType(this.type) === 'sk-ecdsa-sha2-nistp256@openssh.com') {
|
||||
+ if (this.type === 'sk-ecdsa-sha2-nistp256@openssh.com') {
|
||||
+ // Convert SSH ECDSA r,s blob to ASN.1 for OpenSSL
|
||||
+ const { sigSSHToASN1 } = require('./utils.js');
|
||||
+ toVerify = sigSSHToASN1(rawSig, 'ecdsa-sha2-nistp256');
|
||||
@@ -1279,7 +1056,7 @@ index a276c1a..c8ff9d1 100644
|
||||
|
||||
function OpenSSH_Private(type, comment, privPEM, pubPEM, pubSSH, algo,
|
||||
decrypted) {
|
||||
@@ -463,6 +591,26 @@ function OpenSSH_Private(type, comment, privPEM, pubPEM, pubSSH, algo,
|
||||
@@ -463,6 +591,26 @@
|
||||
this[SYM_DECRYPTED] = decrypted;
|
||||
}
|
||||
OpenSSH_Private.prototype = BaseKey;
|
||||
@@ -1306,7 +1083,7 @@ index a276c1a..c8ff9d1 100644
|
||||
{
|
||||
const regexp = /^-----BEGIN OPENSSH PRIVATE KEY-----(?:\r\n|\n)([\s\S]+)(?:\r\n|\n)-----END OPENSSH PRIVATE KEY-----$/;
|
||||
OpenSSH_Private.parse = (str, passphrase) => {
|
||||
@@ -771,6 +919,87 @@ OpenSSH_Private.prototype = BaseKey;
|
||||
@@ -771,6 +919,87 @@
|
||||
privPEM = genOpenSSLECDSAPriv(oid, ecpub, ecpriv);
|
||||
break;
|
||||
}
|
||||
@@ -1394,7 +1171,7 @@ index a276c1a..c8ff9d1 100644
|
||||
default:
|
||||
return new Error(`Unsupported OpenSSH private key type: ${type}`);
|
||||
}
|
||||
@@ -1186,31 +1415,97 @@ function OpenSSH_Public(type, comment, pubPEM, pubSSH, algo) {
|
||||
@@ -1186,31 +1415,97 @@
|
||||
this[SYM_DECRYPTED] = false;
|
||||
}
|
||||
OpenSSH_Public.prototype = BaseKey;
|
||||
@@ -1504,7 +1281,7 @@ index a276c1a..c8ff9d1 100644
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1309,7 +1604,7 @@ RFC4716_Public.prototype = BaseKey;
|
||||
@@ -1309,7 +1604,7 @@
|
||||
}
|
||||
|
||||
|
||||
@@ -1513,7 +1290,7 @@ index a276c1a..c8ff9d1 100644
|
||||
if (!isSupportedKeyType(baseType))
|
||||
return new Error(`Unsupported OpenSSH public key type: ${baseType}`);
|
||||
|
||||
@@ -1317,6 +1612,10 @@ function parseDER(data, baseType, comment, fullType) {
|
||||
@@ -1317,6 +1612,10 @@
|
||||
let oid;
|
||||
let pubPEM = null;
|
||||
let pubSSH = null;
|
||||
@@ -1524,7 +1301,7 @@ index a276c1a..c8ff9d1 100644
|
||||
|
||||
switch (baseType) {
|
||||
case 'ssh-rsa': {
|
||||
@@ -1383,11 +1682,66 @@ function parseDER(data, baseType, comment, fullType) {
|
||||
@@ -1383,11 +1682,66 @@
|
||||
pubSSH = genOpenSSHECDSAPub(oid, ecpub);
|
||||
break;
|
||||
}
|
||||
@@ -1592,7 +1369,7 @@ index a276c1a..c8ff9d1 100644
|
||||
}
|
||||
|
||||
function isSupportedKeyType(type) {
|
||||
@@ -1397,8 +1751,10 @@ function isSupportedKeyType(type) {
|
||||
@@ -1397,8 +1751,10 @@
|
||||
case 'ecdsa-sha2-nistp256':
|
||||
case 'ecdsa-sha2-nistp384':
|
||||
case 'ecdsa-sha2-nistp521':
|
||||
@@ -1603,7 +1380,7 @@ index a276c1a..c8ff9d1 100644
|
||||
if (eddsaSupported)
|
||||
return true;
|
||||
// FALLTHROUGH
|
||||
@@ -1460,7 +1816,13 @@ function parseKey(data, passphrase) {
|
||||
@@ -1460,7 +1816,13 @@
|
||||
if (type !== undefined) {
|
||||
data = binaryKeyParser.readRaw();
|
||||
if (data !== undefined) {
|
||||
@@ -1618,12 +1395,11 @@ index a276c1a..c8ff9d1 100644
|
||||
// Ignore potentially useless errors in case the data was not actually
|
||||
// in the binary format
|
||||
if (ret instanceof Error)
|
||||
@@ -1479,6 +1841,7 @@ function parseKey(data, passphrase) {
|
||||
@@ -1479,6 +1841,7 @@
|
||||
module.exports = {
|
||||
isParsedKey,
|
||||
isSupportedKeyType,
|
||||
- parseDERKey: (data, type) => parseDER(data, type, '', type),
|
||||
+ getOpenSSHCertSignatureAlgorithm,
|
||||
+ parseDERKey: (data, type) => parseDER(data, getOpenSSHCertBaseType(type), '', type),
|
||||
parseDERKey: (data, type) => parseDER(data, type, '', type),
|
||||
parseKey,
|
||||
};
|
||||
@@ -128,13 +128,6 @@ test("asarUnpack no longer references removed legacy agent packages", () => {
|
||||
test("asarUnpack keeps MCP server runtime deps unpacked", () => {
|
||||
// @modelcontextprotocol/sdk is now a direct dep and the MCP server hard-requires it.
|
||||
assert.ok(config.asarUnpack.includes("node_modules/@modelcontextprotocol/sdk/**/*"));
|
||||
// externalMcpRedaction.cjs requires re2js. The launcher runs from
|
||||
// app.asar.unpacked with ELECTRON_RUN_AS_NODE, which cannot see modules
|
||||
// that exist only inside app.asar (issue #3910).
|
||||
assert.ok(
|
||||
config.asarUnpack.includes("node_modules/re2js/**/*"),
|
||||
"external MCP bridge must unpack re2js so the stdio launcher can resolve it",
|
||||
);
|
||||
});
|
||||
|
||||
test("asarUnpack keeps Cursor SDK runtime deps unpacked", () => {
|
||||
|
||||
Reference in new issue
Block a user