Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added optional support for diffie-hellman-group-exchange-* kex #1095

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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