Skip to content

Commit 5a16469

Browse files
committed
Added optional support for diffie-hellman-group-exchange-* key exchanges
1 parent 6b4c64c commit 5a16469

File tree

4 files changed

+210
-66
lines changed

4 files changed

+210
-66
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,8 @@ You can find more examples in the `examples` directory of this repository.
10611061

10621062
* **debug** - _function_ - Set this to a function that receives a single string argument to get detailed (local) debug information. **Default:** (none)
10631063

1064+
* **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)
1065+
10641066
* **greeting** - _string_ - A message that is sent to clients immediately upon connection, before handshaking begins. **Note:** Most clients usually ignore this. **Default:** (none)
10651067

10661068
* **highWaterMark** - _integer_ - This is the `highWaterMark` to use for the parser stream. **Default:** `32 * 1024`

lib/protocol/Protocol.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,15 @@ class Protocol {
200200
? config.banner
201201
: `${config.banner}\r\n`);
202202
}
203+
204+
if (typeof config.getDHParams === 'function') {
205+
this._getDHParams = config.getDHParams;
206+
} else {
207+
// Default implementation doesn't return anything,
208+
// which will cause the key exchange to fail
209+
this._getDHParams = () => null;
210+
}
211+
203212
} else {
204213
this._hostKeys = undefined;
205214
}

lib/protocol/kex.js

Lines changed: 198 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,16 @@ const createKeyExchange = (() => {
742742
true
743743
);
744744

745-
packet[p] = MESSAGE.KEXDH_REPLY;
745+
switch (this.type) {
746+
case 'group':
747+
packet[p] = MESSAGE.KEXDH_REPLY;
748+
break;
749+
case 'groupex':
750+
packet[p] = MESSAGE.KEXDH_GEX_REPLY;
751+
break;
752+
default:
753+
packet[p] = MESSAGE.KEXECDH_REPLY;
754+
}
746755

747756
writeUInt32BE(packet, serverPublicHostKey.length, ++p);
748757
packet.set(serverPublicHostKey, p += 4);
@@ -1359,7 +1368,7 @@ const createKeyExchange = (() => {
13591368
this._public = this._dh.generateKeys();
13601369
}
13611370
}
1362-
setDHParams(prime, generator) {
1371+
setDHParams(prime, generator = Buffer.from([0x02])) {
13631372
if (!Buffer.isBuffer(prime))
13641373
throw new Error('Invalid prime value');
13651374
if (!Buffer.isBuffer(generator))
@@ -1380,6 +1389,8 @@ const createKeyExchange = (() => {
13801389
switch (this._step) {
13811390
case 1:
13821391
if (this._protocol._server) {
1392+
1393+
// Server
13831394
if (type !== MESSAGE.KEXDH_GEX_REQUEST) {
13841395
return doFatalError(
13851396
this._protocol,
@@ -1389,72 +1400,133 @@ const createKeyExchange = (() => {
13891400
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
13901401
);
13911402
}
1392-
// TODO: allow user implementation to provide safe prime and
1393-
// generator on demand to support group exchange on server side
1394-
return doFatalError(
1395-
this._protocol,
1396-
'Group exchange not implemented for server',
1397-
'handshake',
1398-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1403+
1404+
this._protocol._debug && this._protocol._debug(
1405+
'Received DH GEX Request'
13991406
);
1400-
}
14011407

1402-
if (type !== MESSAGE.KEXDH_GEX_GROUP) {
1403-
return doFatalError(
1404-
this._protocol,
1405-
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`,
1406-
'handshake',
1407-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1408+
/*
1409+
byte SSH_MSG_KEY_DH_GEX_REQUEST
1410+
uint32 min, minimal size in bits of an acceptable group
1411+
uint32 n, preferred size in bits of the group the server
1412+
will send
1413+
uint32 max, maximal size in bits of an acceptable group
1414+
*/
1415+
bufferParser.init(payload, 1);
1416+
let minBits;
1417+
let prefBits;
1418+
let maxBits;
1419+
if ((minBits = bufferParser.readUInt32BE()) === undefined
1420+
|| (prefBits = bufferParser.readUInt32BE()) === undefined
1421+
|| (maxBits = bufferParser.readUInt32BE()) === undefined) {
1422+
bufferParser.clear();
1423+
return doFatalError(
1424+
this._protocol,
1425+
'Received malformed KEXDH_GEX_REQUEST',
1426+
'handshake',
1427+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1428+
);
1429+
}
1430+
bufferParser.clear();
1431+
1432+
const primeGenerator =
1433+
this._protocol._getDHParams(minBits, prefBits, maxBits);
1434+
if (!Array.isArray(primeGenerator)) {
1435+
return doFatalError(
1436+
this._protocol,
1437+
'No matching prime for KEXDH_GEX_REQUEST',
1438+
'handshake',
1439+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1440+
);
1441+
}
1442+
1443+
this._minBits = minBits;
1444+
this._prefBits = prefBits;
1445+
this._maxBits = maxBits;
1446+
1447+
this.setDHParams(...primeGenerator);
1448+
this.generateKeys();
1449+
const dh = this.getDHParams();
1450+
1451+
this._protocol._debug && this._protocol._debug(
1452+
'Outbound: Sending KEXDH_GEX_GROUP'
14081453
);
1409-
}
14101454

1411-
this._protocol._debug && this._protocol._debug(
1412-
'Received DH GEX Group'
1413-
);
1455+
let p = this._protocol._packetRW.write.allocStartKEX;
1456+
const packet =
1457+
this._protocol._packetRW.write.alloc(
1458+
1 + 4 + dh.prime.length + 4 + dh.generator.length, true);
1459+
packet[p] = MESSAGE.KEXDH_GEX_GROUP;
1460+
writeUInt32BE(packet, dh.prime.length, ++p);
1461+
packet.set(dh.prime, p += 4);
1462+
writeUInt32BE(packet, dh.generator.length,
1463+
p += dh.prime.length);
1464+
packet.set(dh.generator, p += 4);
1465+
this._protocol._cipher.encrypt(
1466+
this._protocol._packetRW.write.finalize(packet, true)
1467+
);
14141468

1415-
/*
1416-
byte SSH_MSG_KEX_DH_GEX_GROUP
1417-
mpint p, safe prime
1418-
mpint g, generator for subgroup in GF(p)
1419-
*/
1420-
bufferParser.init(payload, 1);
1421-
let prime;
1422-
let gen;
1423-
if ((prime = bufferParser.readString()) === undefined
1424-
|| (gen = bufferParser.readString()) === undefined) {
1425-
bufferParser.clear();
1426-
return doFatalError(
1427-
this._protocol,
1428-
'Received malformed KEXDH_GEX_GROUP',
1429-
'handshake',
1430-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1469+
} else {
1470+
1471+
// Client
1472+
if (type !== MESSAGE.KEXDH_GEX_GROUP) {
1473+
return doFatalError(
1474+
this._protocol,
1475+
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`,
1476+
'handshake',
1477+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1478+
);
1479+
}
1480+
1481+
this._protocol._debug && this._protocol._debug(
1482+
'Received DH GEX Group'
14311483
);
1432-
}
1433-
bufferParser.clear();
14341484

1435-
// TODO: validate prime
1436-
this.setDHParams(prime, gen);
1437-
this.generateKeys();
1438-
const pubkey = this.getPublicKey();
1485+
/*
1486+
byte SSH_MSG_KEX_DH_GEX_GROUP
1487+
mpint p, safe prime
1488+
mpint g, generator for subgroup in GF(p)
1489+
*/
1490+
bufferParser.init(payload, 1);
1491+
let prime;
1492+
let gen;
1493+
if ((prime = bufferParser.readString()) === undefined
1494+
|| (gen = bufferParser.readString()) === undefined) {
1495+
bufferParser.clear();
1496+
return doFatalError(
1497+
this._protocol,
1498+
'Received malformed KEXDH_GEX_GROUP',
1499+
'handshake',
1500+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1501+
);
1502+
}
1503+
bufferParser.clear();
14391504

1440-
this._protocol._debug && this._protocol._debug(
1441-
'Outbound: Sending KEXDH_GEX_INIT'
1442-
);
1505+
// TODO: validate prime
1506+
this.setDHParams(prime, gen);
1507+
this.generateKeys();
1508+
const pubkey = this.getPublicKey();
14431509

1444-
let p = this._protocol._packetRW.write.allocStartKEX;
1445-
const packet =
1446-
this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true);
1447-
packet[p] = MESSAGE.KEXDH_GEX_INIT;
1448-
writeUInt32BE(packet, pubkey.length, ++p);
1449-
packet.set(pubkey, p += 4);
1450-
this._protocol._cipher.encrypt(
1451-
this._protocol._packetRW.write.finalize(packet, true)
1452-
);
1510+
this._protocol._debug && this._protocol._debug(
1511+
'Outbound: Sending KEXDH_GEX_INIT'
1512+
);
14531513

1514+
let p = this._protocol._packetRW.write.allocStartKEX;
1515+
const packet =
1516+
this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true);
1517+
packet[p] = MESSAGE.KEXDH_GEX_INIT;
1518+
writeUInt32BE(packet, pubkey.length, ++p);
1519+
packet.set(pubkey, p += 4);
1520+
this._protocol._cipher.encrypt(
1521+
this._protocol._packetRW.write.finalize(packet, true)
1522+
);
1523+
}
14541524
++this._step;
14551525
break;
14561526
case 2:
14571527
if (this._protocol._server) {
1528+
1529+
// Server
14581530
if (type !== MESSAGE.KEXDH_GEX_INIT) {
14591531
return doFatalError(
14601532
this._protocol,
@@ -1463,30 +1535,90 @@ const createKeyExchange = (() => {
14631535
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
14641536
);
14651537
}
1538+
14661539
this._protocol._debug && this._protocol._debug(
14671540
'Received DH GEX Init'
14681541
);
1469-
return doFatalError(
1470-
this._protocol,
1471-
'Group exchange not implemented for server',
1472-
'handshake',
1473-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1542+
1543+
/*
1544+
byte SSH_MSG_KEX_DH_GEX_INIT
1545+
mpint e
1546+
*/
1547+
bufferParser.init(payload, 1);
1548+
let dhData;
1549+
if ((dhData = bufferParser.readString()) === undefined) {
1550+
bufferParser.clear();
1551+
return doFatalError(
1552+
this._protocol,
1553+
'Received malformed KEXDH_GEX_INIT',
1554+
'handshake',
1555+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1556+
);
1557+
}
1558+
bufferParser.clear();
1559+
1560+
this._dhData = dhData;
1561+
1562+
let hostKey =
1563+
this._protocol._hostKeys[this.negotiated.serverHostKey];
1564+
if (Array.isArray(hostKey))
1565+
hostKey = hostKey[0];
1566+
this._hostKey = hostKey;
1567+
1568+
this.finish();
1569+
1570+
} else {
1571+
1572+
// Client
1573+
if (type !== MESSAGE.KEXDH_GEX_REPLY) {
1574+
return doFatalError(
1575+
this._protocol,
1576+
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`,
1577+
'handshake',
1578+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1579+
);
1580+
}
1581+
1582+
this._protocol._debug && this._protocol._debug(
1583+
'Received DH GEX Reply'
14741584
);
1475-
} else if (type !== MESSAGE.KEXDH_GEX_REPLY) {
1585+
this._step = 1;
1586+
payload[0] = MESSAGE.KEXDH_REPLY;
1587+
this.parse = KeyExchange.prototype.parse;
1588+
this.parse(payload);
1589+
}
1590+
1591+
++this._step;
1592+
break;
1593+
1594+
case 3:
1595+
1596+
if (type !== MESSAGE.NEWKEYS) {
14761597
return doFatalError(
14771598
this._protocol,
1478-
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`,
1599+
`Received packet ${type} instead of ${MESSAGE.NEWKEYS}`,
14791600
'handshake',
14801601
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
14811602
);
14821603
}
14831604
this._protocol._debug && this._protocol._debug(
1484-
'Received DH GEX Reply'
1605+
'Inbound: NEWKEYS'
1606+
);
1607+
this._receivedNEWKEYS = true;
1608+
++this._step;
1609+
if (this._protocol._server || this._hostVerified)
1610+
return this.finish();
1611+
1612+
// Signal to current decipher that we need to change to a new decipher
1613+
// for the next packet
1614+
return false;
1615+
default:
1616+
return doFatalError(
1617+
this._protocol,
1618+
`Received unexpected packet ${type} after NEWKEYS`,
1619+
'handshake',
1620+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
14851621
);
1486-
this._step = 1;
1487-
payload[0] = MESSAGE.KEXDH_REPLY;
1488-
this.parse = KeyExchange.prototype.parse;
1489-
this.parse(payload);
14901622
}
14911623
}
14921624
}

lib/server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ class Client extends EventEmitter {
477477
onPacket,
478478
greeting: srvCfg.greeting,
479479
banner: srvCfg.banner,
480+
getDHParams: srvCfg.getDHParams,
480481
onWrite: (data) => {
481482
if (isWritable(socket))
482483
socket.write(data);

0 commit comments

Comments
 (0)