diff --git a/electron/bridges/sshAlgorithms.test.cjs b/electron/bridges/sshAlgorithms.test.cjs index ab630f92..95f28b30 100644 --- a/electron/bridges/sshAlgorithms.test.cjs +++ b/electron/bridges/sshAlgorithms.test.cjs @@ -1,6 +1,8 @@ const test = require("node:test"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); +const { KexInit, HANDLERS: KEX_HANDLERS } = require("../../node_modules/ssh2/lib/protocol/kex.js"); +const { COMPAT, COMPAT_CHECKS, MESSAGE } = require("../../node_modules/ssh2/lib/protocol/constants.js"); const sshBridge = require("./sshBridge.cjs"); const sftpBridge = require("./sftpBridge.cjs"); @@ -45,6 +47,81 @@ function withAlgorithmRuntime({ unsupportedGroups = new Set(), hashes = ["sha1", } } +function kexPayloadFrom(init) { + const payload = Buffer.alloc(1 + 16 + init.totalSize + 1 + 4); + payload[0] = MESSAGE.KEXINIT; + init.copyAllTo(payload, 17); + return payload; +} + +function buildKexInit(algorithms) { + return new KexInit({ + kex: algorithms.kex, + serverHostKey: algorithms.serverHostKey, + cs: { + cipher: algorithms.cipher, + mac: algorithms.hmac, + compress: algorithms.compress, + lang: [], + }, + sc: { + cipher: algorithms.cipher, + mac: algorithms.hmac, + compress: algorithms.compress, + lang: [], + }, + }); +} + +function readLegacyGexRequestBits(compatFlags) { + const algorithms = sshBridge.buildAlgorithms(true); + const writtenPackets = []; + const protocol = { + _server: false, + _compatFlags: compatFlags, + _offer: buildKexInit(algorithms), + _debug: undefined, + _strictMode: undefined, + _kex: undefined, + _kexinit: Buffer.from("local-kexinit"), + _identRaw: Buffer.from("SSH-2.0-netcatty-test"), + _remoteIdentRaw: Buffer.from("SSH-2.0-Comware-5.20"), + _packetRW: { + write: { + allocStartKEX: 0, + alloc(size) { + return Buffer.alloc(size); + }, + finalize(packet) { + return packet; + }, + }, + }, + _cipher: { + encrypt(packet) { + writtenPackets.push(Buffer.from(packet)); + }, + }, + }; + const remote = buildKexInit({ + kex: ["diffie-hellman-group-exchange-sha1"], + serverHostKey: ["ecdsa-sha2-nistp256", "ssh-rsa"], + cipher: ["aes128-ctr"], + hmac: ["hmac-sha2-256"], + compress: ["none"], + }); + + KEX_HANDLERS[MESSAGE.KEXINIT](protocol, kexPayloadFrom(remote)); + + const request = writtenPackets.find((packet) => packet[0] === MESSAGE.KEXDH_GEX_REQUEST); + assert.ok(request, "expected a DH group-exchange request packet"); + return { + min: request.readUInt32BE(1), + preferred: request.readUInt32BE(5), + max: request.readUInt32BE(9), + }; +} + for (const [label, buildAlgorithms] of [ ["SSH", sshBridge.buildAlgorithms], ["SFTP", sftpBridge.buildSftpAlgorithms], @@ -123,3 +200,17 @@ test("legacy HMAC algorithms skip MD5 when the runtime disables it", () => { } }); }); + +test("Comware legacy group-exchange requests OpenSSH 6.4-sized DH groups", () => { + const comwareCompatRule = COMPAT_CHECKS.find(([pattern, flags]) => ( + pattern instanceof RegExp + && pattern.test("Comware-5.20") + && (flags & COMPAT.COMWARE_DHGEX_1024) + )); + + assert.ok(comwareCompatRule, "Comware servers should opt into the old DH group-exchange request size"); + assert.deepEqual( + readLegacyGexRequestBits(COMPAT.COMWARE_DHGEX_1024), + { min: 1024, preferred: 1024, max: 8192 }, + ); +}); diff --git a/patches/ssh2+1.17.0.patch b/patches/ssh2+1.17.0.patch index 062e12c8..f284d975 100644 --- a/patches/ssh2+1.17.0.patch +++ b/patches/ssh2+1.17.0.patch @@ -738,3 +738,40 @@ 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..4b3f71a 100644 +--- a/node_modules/ssh2/lib/protocol/constants.js ++++ b/node_modules/ssh2/lib/protocol/constants.js +@@ -160,4 +160,5 @@ const COMPAT = { + DYN_RPORT_BUG: 1 << 2, + BUG_DHGEX_LARGE: 1 << 3, + IMPLY_RSA_SHA2_SIGALGS: 1 << 4, ++ COMWARE_DHGEX_1024: 1 << 5, + }; +@@ -330,6 +331,7 @@ module.exports = { + COMPAT_CHECKS: [ + [ 'Cisco-1.25', COMPAT.BAD_DHGEX ], + [ /^Cisco-1[.]/, COMPAT.BUG_DHGEX_LARGE ], ++ [ /^Comware-/, COMPAT.COMWARE_DHGEX_1024 ], + [ /^[0-9.]+$/, COMPAT.OLD_EXIT ], // old SSH.com implementations + [ /^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..4b5f792 100644 +--- a/node_modules/ssh2/lib/protocol/kex.js ++++ b/node_modules/ssh2/lib/protocol/kex.js +@@ -1377,8 +1377,13 @@ const createKeyExchange = (() => { + this._generator = null; + this._minBits = GEX_MIN_BITS; + this._prefBits = dhEstimate(this.negotiated); +- if (this._protocol._compatFlags & COMPAT.BUG_DHGEX_LARGE) ++ if (hashName === 'sha1' ++ && (this._protocol._compatFlags & COMPAT.COMWARE_DHGEX_1024)) { ++ this._minBits = 1024; ++ this._prefBits = 1024; ++ } else if (this._protocol._compatFlags & COMPAT.BUG_DHGEX_LARGE) { + this._prefBits = Math.min(this._prefBits, 4096); ++ } + this._maxBits = GEX_MAX_BITS; + } + start() {