Skip to content

Commit

Permalink
Added optional support for diffie-hellman-group-exchange-* key exch…
Browse files Browse the repository at this point in the history
…anges
  • Loading branch information
schantaraud committed Nov 29, 2021
1 parent 6b4c64c commit 5a16469
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 66 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
9 changes: 9 additions & 0 deletions lib/protocol/Protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
264 changes: 198 additions & 66 deletions lib/protocol/kex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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))
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 5a16469

Please sign in to comment.