Skip to content

Commit d5af906

Browse files
committed
Added optional support for diffie-hellman-group-exchange-* key exchanges
1 parent 844f1ed commit d5af906

5 files changed

Lines changed: 418 additions & 66 deletions

File tree

README.md

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

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

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

10981100
* **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
@@ -210,6 +210,15 @@ class Protocol {
210210
? config.banner
211211
: `${config.banner}\r\n`);
212212
}
213+
214+
if (typeof config.getDHParams === 'function') {
215+
this._getDHParams = config.getDHParams;
216+
} else {
217+
// Default implementation doesn't return anything,
218+
// which will cause the key exchange to fail
219+
this._getDHParams = () => null;
220+
}
221+
213222
} else {
214223
this._hostKeys = undefined;
215224
}

lib/protocol/kex.js

Lines changed: 209 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,16 @@ const createKeyExchange = (() => {
771771
true
772772
);
773773

774-
packet[p] = MESSAGE.KEXDH_REPLY;
774+
switch (this.type) {
775+
case 'group':
776+
packet[p] = MESSAGE.KEXDH_REPLY;
777+
break;
778+
case 'groupex':
779+
packet[p] = MESSAGE.KEXDH_GEX_REPLY;
780+
break;
781+
default:
782+
packet[p] = MESSAGE.KEXECDH_REPLY;
783+
}
775784

776785
writeUInt32BE(packet, serverPublicHostKey.length, ++p);
777786
packet.set(serverPublicHostKey, p += 4);
@@ -1406,7 +1415,7 @@ const createKeyExchange = (() => {
14061415
this._public = this._dh.generateKeys();
14071416
}
14081417
}
1409-
setDHParams(prime, generator) {
1418+
setDHParams(prime, generator = Buffer.from([0x02])) {
14101419
if (!Buffer.isBuffer(prime))
14111420
throw new Error('Invalid prime value');
14121421
if (!Buffer.isBuffer(generator))
@@ -1427,6 +1436,8 @@ const createKeyExchange = (() => {
14271436
switch (this._step) {
14281437
case 1: {
14291438
if (this._protocol._server) {
1439+
1440+
// Server
14301441
if (type !== MESSAGE.KEXDH_GEX_REQUEST) {
14311442
return doFatalError(
14321443
this._protocol,
@@ -1436,73 +1447,143 @@ const createKeyExchange = (() => {
14361447
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
14371448
);
14381449
}
1439-
// TODO: allow user implementation to provide safe prime and
1440-
// generator on demand to support group exchange on server side
1441-
return doFatalError(
1442-
this._protocol,
1443-
'Group exchange not implemented for server',
1444-
'handshake',
1445-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1450+
1451+
this._protocol._debug && this._protocol._debug(
1452+
'Received DH GEX Request'
14461453
);
1447-
}
14481454

1449-
if (type !== MESSAGE.KEXDH_GEX_GROUP) {
1450-
return doFatalError(
1451-
this._protocol,
1452-
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`,
1453-
'handshake',
1454-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1455+
/*
1456+
byte SSH_MSG_KEY_DH_GEX_REQUEST
1457+
uint32 min, minimal size in bits of an acceptable group
1458+
uint32 n, preferred size in bits of the group the server
1459+
will send
1460+
uint32 max, maximal size in bits of an acceptable group
1461+
*/
1462+
bufferParser.init(payload, 1);
1463+
let minBits;
1464+
let prefBits;
1465+
let maxBits;
1466+
if ((minBits = bufferParser.readUInt32BE()) === undefined
1467+
|| (prefBits = bufferParser.readUInt32BE()) === undefined
1468+
|| (maxBits = bufferParser.readUInt32BE()) === undefined) {
1469+
bufferParser.clear();
1470+
return doFatalError(
1471+
this._protocol,
1472+
'Received malformed KEXDH_GEX_REQUEST',
1473+
'handshake',
1474+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1475+
);
1476+
}
1477+
bufferParser.clear();
1478+
1479+
// Async callback: (err, prime, generator)
1480+
this._protocol._getDHParams(
1481+
minBits,
1482+
prefBits,
1483+
maxBits,
1484+
(err, prime, generator) => {
1485+
if (err) {
1486+
return doFatalError(
1487+
this._protocol,
1488+
err.message || 'No matching prime for KEXDH_GEX_REQUEST',
1489+
'handshake',
1490+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1491+
);
1492+
}
1493+
1494+
this._minBits = minBits;
1495+
this._prefBits = prefBits;
1496+
this._maxBits = maxBits;
1497+
1498+
this.setDHParams(prime, generator);
1499+
this.generateKeys();
1500+
const dh = this.getDHParams();
1501+
1502+
this._protocol._debug && this._protocol._debug(
1503+
'Outbound: Sending KEXDH_GEX_GROUP'
1504+
);
1505+
1506+
let p = this._protocol._packetRW.write.allocStartKEX;
1507+
const packet =
1508+
this._protocol._packetRW.write.alloc(
1509+
1 + 4 + dh.prime.length + 4 + dh.generator.length, true);
1510+
packet[p] = MESSAGE.KEXDH_GEX_GROUP;
1511+
writeUInt32BE(packet, dh.prime.length, ++p);
1512+
packet.set(dh.prime, p += 4);
1513+
writeUInt32BE(packet, dh.generator.length,
1514+
p += dh.prime.length);
1515+
packet.set(dh.generator, p += 4);
1516+
this._protocol._cipher.encrypt(
1517+
this._protocol._packetRW.write.finalize(packet, true)
1518+
);
1519+
1520+
++this._step;
1521+
}
14551522
);
1456-
}
1523+
return;
14571524

1458-
this._protocol._debug && this._protocol._debug(
1459-
'Received DH GEX Group'
1460-
);
1525+
} else {
14611526

1462-
/*
1463-
byte SSH_MSG_KEX_DH_GEX_GROUP
1464-
mpint p, safe prime
1465-
mpint g, generator for subgroup in GF(p)
1466-
*/
1467-
bufferParser.init(payload, 1);
1468-
let prime;
1469-
let gen;
1470-
if ((prime = bufferParser.readString()) === undefined
1471-
|| (gen = bufferParser.readString()) === undefined) {
1472-
bufferParser.clear();
1473-
return doFatalError(
1474-
this._protocol,
1475-
'Received malformed KEXDH_GEX_GROUP',
1476-
'handshake',
1477-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1527+
// Client
1528+
if (type !== MESSAGE.KEXDH_GEX_GROUP) {
1529+
return doFatalError(
1530+
this._protocol,
1531+
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`,
1532+
'handshake',
1533+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1534+
);
1535+
}
1536+
1537+
this._protocol._debug && this._protocol._debug(
1538+
'Received DH GEX Group'
14781539
);
1479-
}
1480-
bufferParser.clear();
14811540

1482-
// TODO: validate prime
1483-
this.setDHParams(prime, gen);
1484-
this.generateKeys();
1485-
const pubkey = this.getPublicKey();
1541+
/*
1542+
byte SSH_MSG_KEX_DH_GEX_GROUP
1543+
mpint p, safe prime
1544+
mpint g, generator for subgroup in GF(p)
1545+
*/
1546+
bufferParser.init(payload, 1);
1547+
let prime;
1548+
let gen;
1549+
if ((prime = bufferParser.readString()) === undefined
1550+
|| (gen = bufferParser.readString()) === undefined) {
1551+
bufferParser.clear();
1552+
return doFatalError(
1553+
this._protocol,
1554+
'Received malformed KEXDH_GEX_GROUP',
1555+
'handshake',
1556+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1557+
);
1558+
}
1559+
bufferParser.clear();
14861560

1487-
this._protocol._debug && this._protocol._debug(
1488-
'Outbound: Sending KEXDH_GEX_INIT'
1489-
);
1561+
// TODO: validate prime
1562+
this.setDHParams(prime, gen);
1563+
this.generateKeys();
1564+
const pubkey = this.getPublicKey();
14901565

1491-
let p = this._protocol._packetRW.write.allocStartKEX;
1492-
const packet =
1493-
this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true);
1494-
packet[p] = MESSAGE.KEXDH_GEX_INIT;
1495-
writeUInt32BE(packet, pubkey.length, ++p);
1496-
packet.set(pubkey, p += 4);
1497-
this._protocol._cipher.encrypt(
1498-
this._protocol._packetRW.write.finalize(packet, true)
1499-
);
1566+
this._protocol._debug && this._protocol._debug(
1567+
'Outbound: Sending KEXDH_GEX_INIT'
1568+
);
15001569

1570+
let p = this._protocol._packetRW.write.allocStartKEX;
1571+
const packet =
1572+
this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true);
1573+
packet[p] = MESSAGE.KEXDH_GEX_INIT;
1574+
writeUInt32BE(packet, pubkey.length, ++p);
1575+
packet.set(pubkey, p += 4);
1576+
this._protocol._cipher.encrypt(
1577+
this._protocol._packetRW.write.finalize(packet, true)
1578+
);
1579+
}
15011580
++this._step;
15021581
break;
15031582
}
15041583
case 2:
15051584
if (this._protocol._server) {
1585+
1586+
// Server
15061587
if (type !== MESSAGE.KEXDH_GEX_INIT) {
15071588
return doFatalError(
15081589
this._protocol,
@@ -1511,30 +1592,92 @@ const createKeyExchange = (() => {
15111592
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
15121593
);
15131594
}
1595+
15141596
this._protocol._debug && this._protocol._debug(
15151597
'Received DH GEX Init'
15161598
);
1517-
return doFatalError(
1518-
this._protocol,
1519-
'Group exchange not implemented for server',
1520-
'handshake',
1521-
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1599+
1600+
/*
1601+
byte SSH_MSG_KEX_DH_GEX_INIT
1602+
mpint e
1603+
*/
1604+
bufferParser.init(payload, 1);
1605+
let dhData;
1606+
if ((dhData = bufferParser.readString()) === undefined) {
1607+
bufferParser.clear();
1608+
return doFatalError(
1609+
this._protocol,
1610+
'Received malformed KEXDH_GEX_INIT',
1611+
'handshake',
1612+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1613+
);
1614+
}
1615+
bufferParser.clear();
1616+
1617+
this._dhData = dhData;
1618+
1619+
let hostKey =
1620+
this._protocol._hostKeys[this.negotiated.serverHostKey];
1621+
if (Array.isArray(hostKey))
1622+
hostKey = hostKey[0];
1623+
this._hostKey = hostKey;
1624+
1625+
this.finish();
1626+
1627+
} else {
1628+
1629+
// Client
1630+
if (type !== MESSAGE.KEXDH_GEX_REPLY) {
1631+
return doFatalError(
1632+
this._protocol,
1633+
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`,
1634+
'handshake',
1635+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
1636+
);
1637+
}
1638+
1639+
this._protocol._debug && this._protocol._debug(
1640+
'Received DH GEX Reply'
15221641
);
1523-
} else if (type !== MESSAGE.KEXDH_GEX_REPLY) {
1642+
this._step = 1;
1643+
payload[0] = MESSAGE.KEXDH_REPLY;
1644+
this.parse = KeyExchange.prototype.parse;
1645+
return this.parse(payload);
1646+
}
1647+
1648+
++this._step;
1649+
break;
1650+
1651+
case 3:
1652+
1653+
if (type !== MESSAGE.NEWKEYS) {
15241654
return doFatalError(
15251655
this._protocol,
1526-
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`,
1656+
`Received packet ${type} instead of ${MESSAGE.NEWKEYS}`,
15271657
'handshake',
15281658
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
15291659
);
15301660
}
15311661
this._protocol._debug && this._protocol._debug(
1532-
'Received DH GEX Reply'
1662+
'Inbound: NEWKEYS'
1663+
);
1664+
this._receivedNEWKEYS = true;
1665+
if (this._protocol._strictMode)
1666+
this._protocol._decipher.inSeqno = 0;
1667+
++this._step;
1668+
if (this._protocol._server || this._hostVerified)
1669+
return this.finish();
1670+
1671+
// Signal to current decipher that we need to change to a new decipher
1672+
// for the next packet
1673+
return false;
1674+
default:
1675+
return doFatalError(
1676+
this._protocol,
1677+
`Received unexpected packet ${type} after NEWKEYS`,
1678+
'handshake',
1679+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
15331680
);
1534-
this._step = 1;
1535-
payload[0] = MESSAGE.KEXDH_REPLY;
1536-
this.parse = KeyExchange.prototype.parse;
1537-
this.parse(payload);
15381681
}
15391682
}
15401683
}

lib/server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ class Client extends EventEmitter {
483483
onPacket,
484484
greeting: srvCfg.greeting,
485485
banner: srvCfg.banner,
486+
getDHParams: srvCfg.getDHParams,
486487
onWrite: (data) => {
487488
if (isWritable(socket))
488489
socket.write(data);

0 commit comments

Comments
 (0)