From ccdb2836d29da36c778af3082a747431ef92dc1f Mon Sep 17 00:00:00 2001 From: Sam <32103189+schantaraud@users.noreply.github.com> Date: Mon, 29 Nov 2021 11:43:51 -0500 Subject: [PATCH] Added optional support for `diffie-hellman-group-exchange-*` key exchanges --- README.md | 2 + lib/protocol/Protocol.js | 9 ++ lib/protocol/kex.js | 264 +++++++++++++++++++++++++++++---------- lib/server.js | 1 + 4 files changed, 210 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index e05e6544..cacc72a7 100644 --- a/README.md +++ b/README.md @@ -1061,6 +1061,8 @@ You can find more examples in the `examples` directory of this repository. * **debug** - _function_ - Set this to a function that receives a single string argument to get detailed (local) debug information. **Default:** (none) + * **getDHParams** - _function_ - To unable support for `diffie-hellman-group-exchange-*` key exchanges, set this to a function that receives the client's prime size requirements and preference (`minBits`, `prefBits`, `maxBits`) as its three arguments, and returns either an array containing the secure prime (see `crypto.createDiffieHellman`) as a `Buffer` (array index 0), and optionally the matching generator as a `Buffer` (array index 1 - **default**: `Buffer.from([0x02])`) or a falsy value if no prime matching the client's request is available. Note that processing these primes is a very CPU-intensive synchronous operation that blocks Node.js' event loop for a long time upon each new handshake, therefore, the use of this property is not recommended. **Default:** (none) + * **greeting** - _string_ - A message that is sent to clients immediately upon connection, before handshaking begins. **Note:** Most clients usually ignore this. **Default:** (none) * **highWaterMark** - _integer_ - This is the `highWaterMark` to use for the parser stream. **Default:** `32 * 1024` diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js index 94e12bc7..6da5479d 100644 --- a/lib/protocol/Protocol.js +++ b/lib/protocol/Protocol.js @@ -200,6 +200,15 @@ class Protocol { ? config.banner : `${config.banner}\r\n`); } + + if (typeof config.getDHParams === 'function') { + this._getDHParams = config.getDHParams; + } else { + // Default implementation doesn't return anything, + // which will cause the key exchange to fail + this._getDHParams = () => null; + } + } else { this._hostKeys = undefined; } diff --git a/lib/protocol/kex.js b/lib/protocol/kex.js index 49b28f54..54b7274a 100644 --- a/lib/protocol/kex.js +++ b/lib/protocol/kex.js @@ -742,7 +742,16 @@ const createKeyExchange = (() => { true ); - packet[p] = MESSAGE.KEXDH_REPLY; + switch (this.type) { + case 'group': + packet[p] = MESSAGE.KEXDH_REPLY; + break; + case 'groupex': + packet[p] = MESSAGE.KEXDH_GEX_REPLY; + break; + default: + packet[p] = MESSAGE.KEXECDH_REPLY; + } writeUInt32BE(packet, serverPublicHostKey.length, ++p); packet.set(serverPublicHostKey, p += 4); @@ -1359,7 +1368,7 @@ const createKeyExchange = (() => { this._public = this._dh.generateKeys(); } } - setDHParams(prime, generator) { + setDHParams(prime, generator = Buffer.from([0x02])) { if (!Buffer.isBuffer(prime)) throw new Error('Invalid prime value'); if (!Buffer.isBuffer(generator)) @@ -1380,6 +1389,8 @@ const createKeyExchange = (() => { switch (this._step) { case 1: if (this._protocol._server) { + + // Server if (type !== MESSAGE.KEXDH_GEX_REQUEST) { return doFatalError( this._protocol, @@ -1389,72 +1400,133 @@ const createKeyExchange = (() => { DISCONNECT_REASON.KEY_EXCHANGE_FAILED ); } - // TODO: allow user implementation to provide safe prime and - // generator on demand to support group exchange on server side - return doFatalError( - this._protocol, - 'Group exchange not implemented for server', - 'handshake', - DISCONNECT_REASON.KEY_EXCHANGE_FAILED + + this._protocol._debug && this._protocol._debug( + 'Received DH GEX Request' ); - } - if (type !== MESSAGE.KEXDH_GEX_GROUP) { - return doFatalError( - this._protocol, - `Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`, - 'handshake', - DISCONNECT_REASON.KEY_EXCHANGE_FAILED + /* + byte SSH_MSG_KEY_DH_GEX_REQUEST + uint32 min, minimal size in bits of an acceptable group + uint32 n, preferred size in bits of the group the server + will send + uint32 max, maximal size in bits of an acceptable group + */ + bufferParser.init(payload, 1); + let minBits; + let prefBits; + let maxBits; + if ((minBits = bufferParser.readUInt32BE()) === undefined + || (prefBits = bufferParser.readUInt32BE()) === undefined + || (maxBits = bufferParser.readUInt32BE()) === undefined) { + bufferParser.clear(); + return doFatalError( + this._protocol, + 'Received malformed KEXDH_GEX_REQUEST', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + bufferParser.clear(); + + const primeGenerator = + this._protocol._getDHParams(minBits, prefBits, maxBits); + if (!Array.isArray(primeGenerator)) { + return doFatalError( + this._protocol, + 'No matching prime for KEXDH_GEX_REQUEST', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + this._minBits = minBits; + this._prefBits = prefBits; + this._maxBits = maxBits; + + this.setDHParams(...primeGenerator); + this.generateKeys(); + const dh = this.getDHParams(); + + this._protocol._debug && this._protocol._debug( + 'Outbound: Sending KEXDH_GEX_GROUP' ); - } - this._protocol._debug && this._protocol._debug( - 'Received DH GEX Group' - ); + let p = this._protocol._packetRW.write.allocStartKEX; + const packet = + this._protocol._packetRW.write.alloc( + 1 + 4 + dh.prime.length + 4 + dh.generator.length, true); + packet[p] = MESSAGE.KEXDH_GEX_GROUP; + writeUInt32BE(packet, dh.prime.length, ++p); + packet.set(dh.prime, p += 4); + writeUInt32BE(packet, dh.generator.length, + p += dh.prime.length); + packet.set(dh.generator, p += 4); + this._protocol._cipher.encrypt( + this._protocol._packetRW.write.finalize(packet, true) + ); - /* - byte SSH_MSG_KEX_DH_GEX_GROUP - mpint p, safe prime - mpint g, generator for subgroup in GF(p) - */ - bufferParser.init(payload, 1); - let prime; - let gen; - if ((prime = bufferParser.readString()) === undefined - || (gen = bufferParser.readString()) === undefined) { - bufferParser.clear(); - return doFatalError( - this._protocol, - 'Received malformed KEXDH_GEX_GROUP', - 'handshake', - DISCONNECT_REASON.KEY_EXCHANGE_FAILED + } else { + + // Client + if (type !== MESSAGE.KEXDH_GEX_GROUP) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + this._protocol._debug && this._protocol._debug( + 'Received DH GEX Group' ); - } - bufferParser.clear(); - // TODO: validate prime - this.setDHParams(prime, gen); - this.generateKeys(); - const pubkey = this.getPublicKey(); + /* + byte SSH_MSG_KEX_DH_GEX_GROUP + mpint p, safe prime + mpint g, generator for subgroup in GF(p) + */ + bufferParser.init(payload, 1); + let prime; + let gen; + if ((prime = bufferParser.readString()) === undefined + || (gen = bufferParser.readString()) === undefined) { + bufferParser.clear(); + return doFatalError( + this._protocol, + 'Received malformed KEXDH_GEX_GROUP', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + bufferParser.clear(); - this._protocol._debug && this._protocol._debug( - 'Outbound: Sending KEXDH_GEX_INIT' - ); + // TODO: validate prime + this.setDHParams(prime, gen); + this.generateKeys(); + const pubkey = this.getPublicKey(); - let p = this._protocol._packetRW.write.allocStartKEX; - const packet = - this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true); - packet[p] = MESSAGE.KEXDH_GEX_INIT; - writeUInt32BE(packet, pubkey.length, ++p); - packet.set(pubkey, p += 4); - this._protocol._cipher.encrypt( - this._protocol._packetRW.write.finalize(packet, true) - ); + this._protocol._debug && this._protocol._debug( + 'Outbound: Sending KEXDH_GEX_INIT' + ); + let p = this._protocol._packetRW.write.allocStartKEX; + const packet = + this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true); + packet[p] = MESSAGE.KEXDH_GEX_INIT; + writeUInt32BE(packet, pubkey.length, ++p); + packet.set(pubkey, p += 4); + this._protocol._cipher.encrypt( + this._protocol._packetRW.write.finalize(packet, true) + ); + } ++this._step; break; case 2: if (this._protocol._server) { + + // Server if (type !== MESSAGE.KEXDH_GEX_INIT) { return doFatalError( this._protocol, @@ -1463,30 +1535,90 @@ const createKeyExchange = (() => { DISCONNECT_REASON.KEY_EXCHANGE_FAILED ); } + this._protocol._debug && this._protocol._debug( 'Received DH GEX Init' ); - return doFatalError( - this._protocol, - 'Group exchange not implemented for server', - 'handshake', - DISCONNECT_REASON.KEY_EXCHANGE_FAILED + + /* + byte SSH_MSG_KEX_DH_GEX_INIT + mpint e + */ + bufferParser.init(payload, 1); + let dhData; + if ((dhData = bufferParser.readString()) === undefined) { + bufferParser.clear(); + return doFatalError( + this._protocol, + 'Received malformed KEXDH_GEX_INIT', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + bufferParser.clear(); + + this._dhData = dhData; + + let hostKey = + this._protocol._hostKeys[this.negotiated.serverHostKey]; + if (Array.isArray(hostKey)) + hostKey = hostKey[0]; + this._hostKey = hostKey; + + this.finish(); + + } else { + + // Client + if (type !== MESSAGE.KEXDH_GEX_REPLY) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + this._protocol._debug && this._protocol._debug( + 'Received DH GEX Reply' ); - } else if (type !== MESSAGE.KEXDH_GEX_REPLY) { + this._step = 1; + payload[0] = MESSAGE.KEXDH_REPLY; + this.parse = KeyExchange.prototype.parse; + this.parse(payload); + } + + ++this._step; + break; + + case 3: + + if (type !== MESSAGE.NEWKEYS) { return doFatalError( this._protocol, - `Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`, + `Received packet ${type} instead of ${MESSAGE.NEWKEYS}`, 'handshake', DISCONNECT_REASON.KEY_EXCHANGE_FAILED ); } this._protocol._debug && this._protocol._debug( - 'Received DH GEX Reply' + 'Inbound: NEWKEYS' + ); + this._receivedNEWKEYS = true; + ++this._step; + if (this._protocol._server || this._hostVerified) + return this.finish(); + + // Signal to current decipher that we need to change to a new decipher + // for the next packet + return false; + default: + return doFatalError( + this._protocol, + `Received unexpected packet ${type} after NEWKEYS`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED ); - this._step = 1; - payload[0] = MESSAGE.KEXDH_REPLY; - this.parse = KeyExchange.prototype.parse; - this.parse(payload); } } } diff --git a/lib/server.js b/lib/server.js index 4f6fd1f6..397c78d8 100644 --- a/lib/server.js +++ b/lib/server.js @@ -477,6 +477,7 @@ class Client extends EventEmitter { onPacket, greeting: srvCfg.greeting, banner: srvCfg.banner, + getDHParams: srvCfg.getDHParams, onWrite: (data) => { if (isWritable(socket)) socket.write(data);