Skip to content
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 @@ -1093,6 +1093,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 enable 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`) and a `callback` as its four arguments. The callback has the signature `(err, prime, generator)` where `prime` and `generator` are `Buffer`s (see `crypto.createDiffieHellman`). Call the callback with an `Error` as the first argument if no prime matching the client's request is available. The async callback pattern allows offloading the CPU-intensive prime generation/lookup to worker threads or child processes. **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
11 changes: 11 additions & 0 deletions lib/protocol/Protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ class Protocol {
? config.banner
: `${config.banner}\r\n`);
}

if (typeof config.getDHParams === 'function') {
this._getDHParams = config.getDHParams;
} else {
// Default implementation calls callback with error,
// which will cause the key exchange to fail gracefully
this._getDHParams = (min, pref, max, cb) => {
cb(new Error('No DH parameters available'));
};
}

} else {
this._hostKeys = undefined;
}
Expand Down
275 changes: 209 additions & 66 deletions lib/protocol/kex.js
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,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 @@ -1406,7 +1415,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 @@ -1427,6 +1436,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 @@ -1436,73 +1447,143 @@ 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();

// Async callback: (err, prime, generator)
this._protocol._getDHParams(
minBits,
prefBits,
maxBits,
(err, prime, generator) => {
if (err) {
return doFatalError(
this._protocol,
err.message || 'No matching prime for KEXDH_GEX_REQUEST',
'handshake',
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
);
}

this._minBits = minBits;
this._prefBits = prefBits;
this._maxBits = maxBits;

this.setDHParams(prime, generator);
this.generateKeys();
const dh = this.getDHParams();

this._protocol._debug && this._protocol._debug(
'Outbound: Sending KEXDH_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)
);

++this._step;
}
);
}
return;

this._protocol._debug && this._protocol._debug(
'Received DH GEX Group'
);
} else {

/*
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
// 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 @@ -1511,30 +1592,92 @@ 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;
return 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;
if (this._protocol._strictMode)
this._protocol._decipher.inSeqno = 0;
++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 @@ -483,6 +483,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
Loading