lemon-tls 0.7.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -20
- package/index.cjs +9 -7
- package/index.d.ts +323 -23
- package/index.js +126 -0
- package/package.json +39 -6
- package/src/compat.js +325 -46
- package/src/crypto.js +19 -0
- package/src/dtls_session.js +141 -17
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +192 -57
- package/src/secure_context.js +63 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +39 -4
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +38 -25
- package/src/tls_session.js +2835 -205
- package/src/tls_socket.js +839 -148
- package/src/utils.js +68 -1
- package/src/wire.js +148 -9
- package/types/crypto.d.ts +40 -0
- package/types/record.d.ts +40 -0
- package/types/session.d.ts +3 -0
- package/types/wire.d.ts +62 -0
package/src/dtls_session.js
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
} from './record.js';
|
|
68
68
|
|
|
69
69
|
import { timingSafeEqualU8 } from './utils.js';
|
|
70
|
-
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS } from './session/ecdh.js';
|
|
70
|
+
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS, SERVER_DEFAULT_GROUPS } from './session/ecdh.js';
|
|
71
71
|
import { default_signature_schemes } from './session/signing.js';
|
|
72
72
|
|
|
73
73
|
|
|
@@ -194,6 +194,11 @@ function DTLSSession(options) {
|
|
|
194
194
|
servername: options.servername,
|
|
195
195
|
SNICallback: options.SNICallback,
|
|
196
196
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
197
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
198
|
+
// DTLS peers in WebRTC present self-signed certificates by design and are
|
|
199
|
+
// authenticated by the SDP fingerprint, not by a CA chain. Falling back to
|
|
200
|
+
// the platform trust store here would mark every such peer unauthorized.
|
|
201
|
+
useDefaultCA: options.useDefaultCA === true,
|
|
197
202
|
ca: options.ca,
|
|
198
203
|
noTickets: options.noTickets,
|
|
199
204
|
requestCert: options.requestCert,
|
|
@@ -286,6 +291,7 @@ function DTLSSession(options) {
|
|
|
286
291
|
// the ACK arrives.
|
|
287
292
|
pendingWriteEpoch: null,
|
|
288
293
|
keyUpdateFlight: null, // { payload, epoch, timer, attempts, timeout }
|
|
294
|
+
finalFlightPending: false, // DTLS 1.3 client: final flight not yet confirmed (see secureConnect)
|
|
289
295
|
|
|
290
296
|
retransmitTimer: null,
|
|
291
297
|
retransmitCount: 0,
|
|
@@ -331,6 +337,21 @@ function DTLSSession(options) {
|
|
|
331
337
|
if (options.groups) tlsSetup.local_supported_groups = options.groups;
|
|
332
338
|
if (options.alpnProtocols) tlsSetup.local_supported_alpns = options.alpnProtocols;
|
|
333
339
|
|
|
340
|
+
// Default key-exchange groups, BOTH roles. With DTLS 1.3 offered: the
|
|
341
|
+
// hybrid post-quantum group first (X25519MLKEM768, when node:crypto has
|
|
342
|
+
// ML-KEM — see ecdh.js), then the classical ones. In WebRTC either peer can
|
|
343
|
+
// end up the DTLS client, and harvest-now-decrypt-later applies to media
|
|
344
|
+
// too (Chrome rolls PQ DTLS out from 149). A client leading with the hybrid
|
|
345
|
+
// group sends a classical share beside it (tls_session), so a classical
|
|
346
|
+
// server costs nothing extra. DTLS 1.2 only: classical — a KEM does not fit
|
|
347
|
+
// its key exchange, and the server skips KEM groups there anyway.
|
|
348
|
+
if (!tlsSetup.local_supported_groups) {
|
|
349
|
+
// (ECDH_SUPPORTED_GROUPS leads with the hybrid too now; tls_session leaves
|
|
350
|
+
// a KEM group out of a hello that does not offer 1.3.)
|
|
351
|
+
tlsSetup.local_supported_groups = (versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0
|
|
352
|
+
? SERVER_DEFAULT_GROUPS : ECDH_SUPPORTED_GROUPS).slice();
|
|
353
|
+
}
|
|
354
|
+
|
|
334
355
|
// Server defaults — TLSSession client auto-populates these, but server expects them set externally
|
|
335
356
|
if (isServer) {
|
|
336
357
|
if (!tlsSetup.local_supported_cipher_suites) {
|
|
@@ -340,16 +361,20 @@ function DTLSSession(options) {
|
|
|
340
361
|
versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0
|
|
341
362
|
);
|
|
342
363
|
}
|
|
343
|
-
|
|
344
|
-
tlsSetup.local_supported_groups = ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
|
|
345
|
-
}
|
|
364
|
+
// (key-exchange groups: set above, for both roles)
|
|
346
365
|
// signing.js owns the scheme list; DTLS spans 1.2+1.3 so include PKCS#1.
|
|
347
366
|
tlsSetup.local_supported_signature_algorithms = default_signature_schemes();
|
|
348
367
|
}
|
|
349
368
|
|
|
350
|
-
// Server cert/key
|
|
369
|
+
// Server cert/key.
|
|
370
|
+
//
|
|
371
|
+
// The whole options object goes in, not just key+cert: createSecureContext
|
|
372
|
+
// also reads passphrase, ca, ciphers, sigalgs, ecdhCurve, honorCipherOrder,
|
|
373
|
+
// pfx and ocsp, and picking out two fields silently dropped all of them —
|
|
374
|
+
// an encrypted private key could never be decrypted here no matter what the
|
|
375
|
+
// caller passed. Same defect the TLS paths carried.
|
|
351
376
|
if (options.cert && options.key) {
|
|
352
|
-
let sctx = createSecureContext(
|
|
377
|
+
let sctx = createSecureContext(options);
|
|
353
378
|
tlsSetup.local_cert_chain = sctx.certificateChain;
|
|
354
379
|
tlsSetup.cert_private_key = sctx.privateKey;
|
|
355
380
|
}
|
|
@@ -726,9 +751,10 @@ function DTLSSession(options) {
|
|
|
726
751
|
}
|
|
727
752
|
|
|
728
753
|
for (let i = 0; i < frags.length; i++) {
|
|
729
|
-
let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
|
|
730
754
|
// The flight captures the fragment, not the record — see currentFlight.
|
|
731
|
-
|
|
755
|
+
let entry = { epoch: dtlsEpoch, contentType: CT.HANDSHAKE, payload: frags[i] };
|
|
756
|
+
let record = buildFlightRecord(entry);
|
|
757
|
+
ctx.currentFlight.push(entry);
|
|
732
758
|
if (record) {
|
|
733
759
|
_ldbgTx(record, 'L543'); ev.emit('packet', record);
|
|
734
760
|
} else {
|
|
@@ -1020,6 +1046,20 @@ function DTLSSession(options) {
|
|
|
1020
1046
|
}
|
|
1021
1047
|
|
|
1022
1048
|
function processRecord(record) {
|
|
1049
|
+
// A record in an epoch above 0 that did not decrypt is not the peer's
|
|
1050
|
+
// (forged, corrupted, or a late packet from a previous session on the
|
|
1051
|
+
// same address) — RFC 6347 §4.1.2.7 / RFC 9147 §4.5.2: discard it
|
|
1052
|
+
// silently. Its content is still ciphertext: as an ALERT its second byte
|
|
1053
|
+
// is random, which emitted 'error' and failed the whole connection; as
|
|
1054
|
+
// APPLICATION_DATA it went up to SCTP as data.
|
|
1055
|
+
if (record.epoch > 0 && !record.encrypted) return;
|
|
1056
|
+
// Once connected, everything the peer sends is protected. A plaintext
|
|
1057
|
+
// (epoch 0) alert or application data at that point is unauthenticated —
|
|
1058
|
+
// anyone able to send a UDP packet from the peer's address could close
|
|
1059
|
+
// the connection with a one-record close_notify, or inject data.
|
|
1060
|
+
if (ctx.state === 'connected' && !record.encrypted &&
|
|
1061
|
+
(record.type === CT.ALERT || record.type === CT.APPLICATION_DATA)) return;
|
|
1062
|
+
|
|
1023
1063
|
if (record.type === CT.HANDSHAKE) {
|
|
1024
1064
|
// An epoch>0 record that could not be decrypted (keys not yet
|
|
1025
1065
|
// installed, AEAD failure) must NOT be fed to the handshake
|
|
@@ -1036,6 +1076,9 @@ function DTLSSession(options) {
|
|
|
1036
1076
|
processHandshakeRecord(record.content, record.epoch, record.encrypted);
|
|
1037
1077
|
} else if (record.type === CT.APPLICATION_DATA) {
|
|
1038
1078
|
if (ctx.state === 'connected') {
|
|
1079
|
+
// Protected data from the peer means it completed the handshake,
|
|
1080
|
+
// so it has our Finished.
|
|
1081
|
+
if (ctx.finalFlightPending) finalFlightDone();
|
|
1039
1082
|
ev.emit('data', record.content);
|
|
1040
1083
|
}
|
|
1041
1084
|
} else if (record.type === CT.CHANGE_CIPHER_SPEC) {
|
|
@@ -1536,17 +1579,31 @@ function DTLSSession(options) {
|
|
|
1536
1579
|
// payload never came from the application at all.
|
|
1537
1580
|
// RFC 9147 §5.8: retransmission is bounded by the handshake; after
|
|
1538
1581
|
// it, ACKs are the mechanism, not a timer.
|
|
1539
|
-
if (ctx.state === '
|
|
1582
|
+
if (ctx.state === 'closed' || (ctx.state === 'connected' && !ctx.finalFlightPending)) {
|
|
1540
1583
|
cancelRetransmit();
|
|
1541
1584
|
return;
|
|
1542
1585
|
}
|
|
1543
1586
|
for (let i = 0; i < ctx.currentFlight.length; i++) {
|
|
1544
1587
|
let f = ctx.currentFlight[i];
|
|
1545
|
-
|
|
1588
|
+
// DTLS 1.3 (RFC 9147 §7.1): records the peer has ACKed are not resent.
|
|
1589
|
+
if (f.acked) continue;
|
|
1590
|
+
let record = buildFlightRecord(f);
|
|
1546
1591
|
if (record) { _ldbgTx(record, 'L1280'); ev.emit('packet', record); }
|
|
1547
1592
|
}
|
|
1548
1593
|
}
|
|
1549
1594
|
|
|
1595
|
+
/**
|
|
1596
|
+
* Build the record for a flight entry and remember its record number
|
|
1597
|
+
* (epoch, sequence) — every transmission gets a fresh one, and a DTLS 1.3
|
|
1598
|
+
* ACK names the records it covers by those numbers.
|
|
1599
|
+
*/
|
|
1600
|
+
function buildFlightRecord(f) {
|
|
1601
|
+
let seq = (f.epoch in ctx.writeSeq) ? ctx.writeSeq[f.epoch] : 0;
|
|
1602
|
+
let record = buildRecord(f.epoch, f.contentType, f.payload);
|
|
1603
|
+
if (record) (f.sent || (f.sent = [])).push(f.epoch * 0x1000000000000 + seq);
|
|
1604
|
+
return record;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1550
1607
|
function startRetransmitTimer() {
|
|
1551
1608
|
cancelRetransmit();
|
|
1552
1609
|
// ONE GATE, AT THE SOURCE. Four call sites arm this timer and only
|
|
@@ -1555,8 +1612,13 @@ function DTLSSession(options) {
|
|
|
1555
1612
|
// flight into the application-data epoch. Refusing to arm once
|
|
1556
1613
|
// connected (or closed) covers every caller, including any added
|
|
1557
1614
|
// later, without each having to remember the rule.
|
|
1558
|
-
if (ctx.state === '
|
|
1615
|
+
if (ctx.state === 'closed' || (ctx.state === 'connected' && !ctx.finalFlightPending)) return;
|
|
1559
1616
|
ctx.retransmitTimer = setTimeout(function() {
|
|
1617
|
+
if (ctx.finalFlightPending && ctx.retransmitCount >= ctx.maxRetransmits) {
|
|
1618
|
+
// The connection is up on our side; stop resending without failing it.
|
|
1619
|
+
finalFlightDone();
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1560
1622
|
if (ctx.retransmitCount >= ctx.maxRetransmits) {
|
|
1561
1623
|
ev.emit('error', new Error('DTLS handshake timeout — max retransmits exceeded'));
|
|
1562
1624
|
ctx.state = 'closed';
|
|
@@ -1575,6 +1637,13 @@ function DTLSSession(options) {
|
|
|
1575
1637
|
if (ctx.retransmitTimer.unref) ctx.retransmitTimer.unref();
|
|
1576
1638
|
}
|
|
1577
1639
|
|
|
1640
|
+
// The client's final flight is known to have arrived: stop resending it.
|
|
1641
|
+
function finalFlightDone() {
|
|
1642
|
+
ctx.finalFlightPending = false;
|
|
1643
|
+
cancelRetransmit();
|
|
1644
|
+
startNewFlight();
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1578
1647
|
function cancelRetransmit() {
|
|
1579
1648
|
if (ctx.retransmitTimer !== null) {
|
|
1580
1649
|
clearTimeout(ctx.retransmitTimer);
|
|
@@ -1608,10 +1677,37 @@ function DTLSSession(options) {
|
|
|
1608
1677
|
return;
|
|
1609
1678
|
}
|
|
1610
1679
|
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1680
|
+
// During the handshake an ACK may cover only PART of our flight (RFC 9147
|
|
1681
|
+
// §7.1): the peer lists the records it received. Those are done; the
|
|
1682
|
+
// rest stay under the retransmit timer and are what the next
|
|
1683
|
+
// retransmission resends. Treating any ACK as acknowledging the whole
|
|
1684
|
+
// flight cancelled the timer while a record was still missing — the
|
|
1685
|
+
// peer, waiting for it, ACKed again and again, and the handshake hung
|
|
1686
|
+
// (≈ half of all handshakes at 5% loss, when the lost record was inside
|
|
1687
|
+
// the server's flight).
|
|
1688
|
+
if ((ctx.state !== 'connected' || ctx.finalFlightPending) && ctx.currentFlight.length > 0) {
|
|
1689
|
+
let got = new Set();
|
|
1690
|
+
for (let i = 0; i < acks.length; i++) got.add(acks[i].epoch * 0x1000000000000 + acks[i].seq);
|
|
1691
|
+
let remaining = 0;
|
|
1692
|
+
for (let i = 0; i < ctx.currentFlight.length; i++) {
|
|
1693
|
+
let f = ctx.currentFlight[i];
|
|
1694
|
+
if (!f.acked && f.sent) {
|
|
1695
|
+
for (let k = 0; k < f.sent.length; k++) {
|
|
1696
|
+
if (got.has(f.sent[k])) { f.acked = true; break; }
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
// Plaintext epoch-0 records (ServerHello, CCS) are never ACKed — the
|
|
1700
|
+
// peer acknowledges them implicitly by answering — so they do not
|
|
1701
|
+
// keep the timer alive on their own.
|
|
1702
|
+
if (!f.acked && f.epoch !== 0) remaining++;
|
|
1703
|
+
}
|
|
1704
|
+
if (remaining > 0) return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// The whole flight is acknowledged — or, the ACK for the client's final
|
|
1708
|
+
// flight (which RFC 9147 §7.1 requires the peer to send explicitly, and
|
|
1709
|
+
// which arrives at the application epoch): cancel the retransmit timer.
|
|
1710
|
+
ctx.finalFlightPending = false;
|
|
1615
1711
|
cancelRetransmit();
|
|
1616
1712
|
startNewFlight();
|
|
1617
1713
|
}
|
|
@@ -1662,8 +1758,18 @@ function DTLSSession(options) {
|
|
|
1662
1758
|
_ldbg('handshake complete — secureConnect (version=0x' +
|
|
1663
1759
|
((ctx.selectedVersion || tls.context.selected_version || 0).toString(16)) + ')');
|
|
1664
1760
|
ctx.state = 'connected';
|
|
1665
|
-
|
|
1666
|
-
|
|
1761
|
+
// DTLS 1.3 client (RFC 9147 §5.8.3, §7.1): our final flight (…Finished)
|
|
1762
|
+
// is answered by nothing but an ACK, so it must stay under the
|
|
1763
|
+
// retransmit timer until that ACK — or anything else from the peer at
|
|
1764
|
+
// the application epoch — shows it arrived. Dropping it here meant one
|
|
1765
|
+
// lost Finished left us "connected" and the peer waiting for good.
|
|
1766
|
+
if (!isServer && ctx.selectedVersion === DTLS_VERSION.DTLS1_3 && ctx.currentFlight.length > 0) {
|
|
1767
|
+
ctx.finalFlightPending = true;
|
|
1768
|
+
startRetransmitTimer();
|
|
1769
|
+
} else {
|
|
1770
|
+
cancelRetransmit();
|
|
1771
|
+
startNewFlight();
|
|
1772
|
+
}
|
|
1667
1773
|
ev.emit('connect');
|
|
1668
1774
|
});
|
|
1669
1775
|
|
|
@@ -1841,6 +1947,24 @@ function DTLSSession(options) {
|
|
|
1841
1947
|
|
|
1842
1948
|
/** Peer certificate. */
|
|
1843
1949
|
getPeerCertificate: function() { return tls.getPeerCertificate(); },
|
|
1950
|
+
|
|
1951
|
+
/** TLS fingerprints (see TLSSession): JA4, JA5 of the ClientHello, JA4S
|
|
1952
|
+
* of the ServerHello. The transport character resolves to 'd' on its
|
|
1953
|
+
* own — the inner TLSSession reads the 0xFE high byte off the hello's
|
|
1954
|
+
* legacy_version — so no `protocol` override is needed for plain DTLS. */
|
|
1955
|
+
getJA4: function(opts) { return tls.getJA4(opts); },
|
|
1956
|
+
getJA5: function(opts) { return tls.getJA5(opts); },
|
|
1957
|
+
getJA4S: function(opts) { return tls.getJA4S(opts); },
|
|
1958
|
+
|
|
1959
|
+
/** Set the key and certificate for this connection (Node parity). */
|
|
1960
|
+
setKeyCert: function(ctxOrOpts) { return tls.setKeyCert(ctxOrOpts); },
|
|
1961
|
+
|
|
1962
|
+
/** Finished verify_data, ours and the peer's (RFC 5929 channel binding). */
|
|
1963
|
+
getFinished: function() { return tls.getFinished(); },
|
|
1964
|
+
getPeerFinished: function() { return tls.getPeerFinished(); },
|
|
1965
|
+
|
|
1966
|
+
/** Whether this connection resumed an earlier session. */
|
|
1967
|
+
isSessionReused: function() { return !!tls.isResumed; },
|
|
1844
1968
|
};
|
|
1845
1969
|
|
|
1846
1970
|
for (let k in api) {
|
package/src/dtls_socket.js
CHANGED
|
@@ -44,12 +44,14 @@ function DTLSSocket(udpSocket, options) {
|
|
|
44
44
|
cert: options.cert,
|
|
45
45
|
key: options.key,
|
|
46
46
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
47
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
47
48
|
requestCert: options.requestCert,
|
|
48
49
|
ca: options.ca,
|
|
49
50
|
alpnProtocols: options.alpnProtocols,
|
|
50
51
|
minVersion: options.minVersion,
|
|
51
52
|
maxVersion: options.maxVersion,
|
|
52
53
|
mtu: options.mtu,
|
|
54
|
+
groups: options.groups,
|
|
53
55
|
ticketKeys: options.ticketKeys,
|
|
54
56
|
useCookies: options.useCookies,
|
|
55
57
|
srtpProfiles: options.srtpProfiles,
|
|
@@ -130,6 +132,33 @@ function DTLSSocket(udpSocket, options) {
|
|
|
130
132
|
self.getALPN = function() { return session.getALPN(); };
|
|
131
133
|
self.getPeerCertificate = function() { return session.getPeerCertificate(); };
|
|
132
134
|
|
|
135
|
+
/** TLS fingerprints, { hash, raw } or null (see TLSSession): JA4 and JA5
|
|
136
|
+
* of the ClientHello, JA4S of the ServerHello — in either role. */
|
|
137
|
+
self.getJA4 = function(opts) { return session.getJA4(opts); };
|
|
138
|
+
self.getJA5 = function(opts) { return session.getJA5(opts); };
|
|
139
|
+
self.getJA4S = function(opts) { return session.getJA4S(opts); };
|
|
140
|
+
|
|
141
|
+
/** Set the key and certificate for this connection (Node parity). */
|
|
142
|
+
self.setKeyCert = function(ctxOrOpts) {
|
|
143
|
+
if (session.setKeyCert) session.setKeyCert(ctxOrOpts);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Finished verify_data — the tls-unique channel binding of RFC 5929.
|
|
147
|
+
* DTLS-SRTP callers use this alongside exportKeyingMaterial(). */
|
|
148
|
+
self.getFinished = function() {
|
|
149
|
+
let v = session.getFinished ? session.getFinished() : null;
|
|
150
|
+
return v == null ? undefined : v;
|
|
151
|
+
};
|
|
152
|
+
self.getPeerFinished = function() {
|
|
153
|
+
let v = session.getPeerFinished ? session.getPeerFinished() : null;
|
|
154
|
+
return v == null ? undefined : v;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Whether this connection resumed an earlier session. */
|
|
158
|
+
self.isSessionReused = function() {
|
|
159
|
+
return session.isSessionReused ? session.isSessionReused() : false;
|
|
160
|
+
};
|
|
161
|
+
|
|
133
162
|
/** RFC 5705 / RFC 8446 §7.5 exporter (Node tls.TLSSocket parity).
|
|
134
163
|
* DTLS-SRTP: exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp') — no context.
|
|
135
164
|
* Returns Buffer, or null before secrets are available. */
|
|
@@ -194,6 +223,7 @@ function createDTLSServer(options, connectionListener) {
|
|
|
194
223
|
minVersion: options.minVersion,
|
|
195
224
|
maxVersion: options.maxVersion,
|
|
196
225
|
mtu: options.mtu,
|
|
226
|
+
groups: options.groups,
|
|
197
227
|
ticketKeys: options.ticketKeys,
|
|
198
228
|
useCookies: options.useCookies,
|
|
199
229
|
srtpProfiles: options.srtpProfiles,
|
|
@@ -270,12 +300,14 @@ function connectDTLS(options, callback) {
|
|
|
270
300
|
remotePort: port,
|
|
271
301
|
servername: options.servername || host,
|
|
272
302
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
303
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
273
304
|
requestCert: options.requestCert,
|
|
274
305
|
ca: options.ca,
|
|
275
306
|
alpnProtocols: options.alpnProtocols,
|
|
276
307
|
minVersion: options.minVersion,
|
|
277
308
|
maxVersion: options.maxVersion,
|
|
278
309
|
mtu: options.mtu,
|
|
310
|
+
groups: options.groups,
|
|
279
311
|
});
|
|
280
312
|
|
|
281
313
|
if (callback) socket.on('connect', callback);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// node:tls option strings → the codes lemon-tls works with
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// node:tls takes `ciphers`, `ecdhCurve` and `sigalgs` as OpenSSL strings,
|
|
6
|
+
// colon-separated; lemon-tls works with code points (allowedCipherSuites,
|
|
7
|
+
// groups, signatureAlgorithms). compat.js promised Node's API and never read
|
|
8
|
+
// the three — a server given `ciphers` or `ecdhCurve` ran with the defaults
|
|
9
|
+
// and said nothing. These turn them into the lists, once, where an option is
|
|
10
|
+
// taken (compat.js, and QUICO for both TCP and QUIC), and refuse what cannot
|
|
11
|
+
// be honoured, as Node does ("no cipher match", "Failed to set ECDH curve").
|
|
12
|
+
//
|
|
13
|
+
// What is understood:
|
|
14
|
+
// ciphers suite names — OpenSSL's for TLS 1.2 (ECDHE-RSA-AES128-GCM-SHA256)
|
|
15
|
+
// and the standard ones (TLS_AES_128_GCM_SHA256, or for 1.2
|
|
16
|
+
// TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), in the order given;
|
|
17
|
+
// `!name` / `-name` take one out; DEFAULT, HIGH and ALL stand for
|
|
18
|
+
// every suite this library implements — it implements no weak
|
|
19
|
+
// ones. Other OpenSSL keywords (aNULL, MD5, …) select nothing
|
|
20
|
+
// here and are skipped: what remains must name something.
|
|
21
|
+
// ecdhCurve 'auto' (the default list) or names: X25519, P-256
|
|
22
|
+
// (prime256v1, secp256r1), P-384 (secp384r1), and post-quantum:
|
|
23
|
+
// X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024,
|
|
24
|
+
// MLKEM512, MLKEM768, MLKEM1024 (OpenSSL's names; case ignored).
|
|
25
|
+
// sigalgs scheme names (ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256,
|
|
26
|
+
// ed25519, …) or OpenSSL's SIG+HASH form (ECDSA+SHA256,
|
|
27
|
+
// RSA-PSS+SHA256, RSA+SHA256).
|
|
28
|
+
|
|
29
|
+
import { TLS_CIPHER_SUITES, DEFAULT_CIPHER_SUITES_TLS13, DEFAULT_CIPHER_SUITES_TLS12 } from './crypto.js';
|
|
30
|
+
import { SERVER_DEFAULT_GROUPS } from './session/ecdh.js';
|
|
31
|
+
import { MLDSA_AVAILABLE } from './session/signing.js';
|
|
32
|
+
|
|
33
|
+
function splitList(s) {
|
|
34
|
+
return String(s).split(/[:,\s]+/).filter(function (x) { return x !== ''; });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var cipherByName = null;
|
|
38
|
+
function cipherTable() {
|
|
39
|
+
if (cipherByName) return cipherByName;
|
|
40
|
+
cipherByName = {};
|
|
41
|
+
for (var code in TLS_CIPHER_SUITES) {
|
|
42
|
+
var info = TLS_CIPHER_SUITES[code];
|
|
43
|
+
if (info.name) cipherByName[info.name.toUpperCase()] = Number(code);
|
|
44
|
+
if (info.standardName) cipherByName[info.standardName.toUpperCase()] = Number(code);
|
|
45
|
+
}
|
|
46
|
+
return cipherByName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `ciphers` → [code, …] in the order given. Throws when nothing is left. */
|
|
50
|
+
function parseCiphers(str) {
|
|
51
|
+
if (str === undefined || str === null) return null;
|
|
52
|
+
if (typeof str !== 'string') throw new TypeError('ciphers must be a string, got ' + typeof str);
|
|
53
|
+
var table = cipherTable(), all = DEFAULT_CIPHER_SUITES_TLS13.concat(DEFAULT_CIPHER_SUITES_TLS12);
|
|
54
|
+
var out = [], removed = [];
|
|
55
|
+
var tokens = splitList(str);
|
|
56
|
+
for (var i = 0; i < tokens.length; i++) {
|
|
57
|
+
var t = tokens[i], neg = t[0] === '!' || t[0] === '-';
|
|
58
|
+
if (neg || t[0] === '+') t = t.slice(1);
|
|
59
|
+
var u = t.toUpperCase(), codes;
|
|
60
|
+
if (u === 'DEFAULT' || u === 'HIGH' || u === 'ALL') codes = all;
|
|
61
|
+
else if (u in table) codes = [table[u]];
|
|
62
|
+
else continue; // a keyword this library has nothing for
|
|
63
|
+
for (var j = 0; j < codes.length; j++) {
|
|
64
|
+
if (neg) { removed.push(codes[j]); var k = out.indexOf(codes[j]); if (k >= 0) out.splice(k, 1); }
|
|
65
|
+
else if (out.indexOf(codes[j]) < 0 && removed.indexOf(codes[j]) < 0) out.push(codes[j]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (out.length === 0) throw new Error('ciphers: no cipher match in "' + str + '"');
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var GROUPS = {
|
|
73
|
+
'X25519': 0x001d,
|
|
74
|
+
'P-256': 0x0017, 'PRIME256V1': 0x0017, 'SECP256R1': 0x0017,
|
|
75
|
+
'P-384': 0x0018, 'SECP384R1': 0x0018,
|
|
76
|
+
'X25519MLKEM768': 0x11ec,
|
|
77
|
+
'SECP256R1MLKEM768': 0x11eb, 'SECP384R1MLKEM1024': 0x11ed,
|
|
78
|
+
'MLKEM512': 0x0200, 'MLKEM768': 0x0201, 'MLKEM1024': 0x0202,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** `ecdhCurve` → [group, …]; 'auto' → null, the role's own default list
|
|
82
|
+
* (a server's has the hybrid first, a client's does not offer it unasked).
|
|
83
|
+
* Throws on a name it does not implement. */
|
|
84
|
+
function parseEcdhCurve(str) {
|
|
85
|
+
if (str === undefined || str === null) return null;
|
|
86
|
+
if (typeof str !== 'string') throw new TypeError('ecdhCurve must be a string, got ' + typeof str);
|
|
87
|
+
if (str.trim().toLowerCase() === 'auto') return null;
|
|
88
|
+
var out = [], tokens = splitList(str);
|
|
89
|
+
for (var i = 0; i < tokens.length; i++) {
|
|
90
|
+
var g = GROUPS[tokens[i].toUpperCase()];
|
|
91
|
+
if (g === undefined) throw new Error('ecdhCurve: unsupported curve "' + tokens[i] + '"');
|
|
92
|
+
// The hybrid group only where node:crypto has ML-KEM (session/ecdh.js).
|
|
93
|
+
if (SERVER_DEFAULT_GROUPS.indexOf(g) < 0) throw new Error('ecdhCurve: "' + tokens[i] + '" is not available in this Node.js');
|
|
94
|
+
if (out.indexOf(g) < 0) out.push(g);
|
|
95
|
+
}
|
|
96
|
+
if (out.length === 0) throw new Error('ecdhCurve: no curve in "' + str + '"');
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
var SCHEMES = {
|
|
101
|
+
'RSA_PKCS1_SHA256': 0x0401, 'RSA_PKCS1_SHA384': 0x0501, 'RSA_PKCS1_SHA512': 0x0601,
|
|
102
|
+
'ECDSA_SECP256R1_SHA256': 0x0403, 'ECDSA_SECP384R1_SHA384': 0x0503, 'ECDSA_SECP521R1_SHA512': 0x0603,
|
|
103
|
+
'RSA_PSS_RSAE_SHA256': 0x0804, 'RSA_PSS_RSAE_SHA384': 0x0805, 'RSA_PSS_RSAE_SHA512': 0x0806,
|
|
104
|
+
'ED25519': 0x0807, 'ED448': 0x0808,
|
|
105
|
+
'RSA_PSS_PSS_SHA256': 0x0809, 'RSA_PSS_PSS_SHA384': 0x080a, 'RSA_PSS_PSS_SHA512': 0x080b,
|
|
106
|
+
// ML-DSA (FIPS 204), as OpenSSL names it.
|
|
107
|
+
'MLDSA44': 0x0904, 'MLDSA65': 0x0905, 'MLDSA87': 0x0906,
|
|
108
|
+
// OpenSSL's SIG+HASH spelling.
|
|
109
|
+
'ECDSA+SHA256': 0x0403, 'ECDSA+SHA384': 0x0503, 'ECDSA+SHA512': 0x0603,
|
|
110
|
+
'RSA-PSS+SHA256': 0x0804, 'RSA-PSS+SHA384': 0x0805, 'RSA-PSS+SHA512': 0x0806,
|
|
111
|
+
'RSA+SHA256': 0x0401, 'RSA+SHA384': 0x0501, 'RSA+SHA512': 0x0601,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** `sigalgs` → [scheme, …]. Throws on a name it does not know. */
|
|
115
|
+
function parseSigalgs(str) {
|
|
116
|
+
if (str === undefined || str === null) return null;
|
|
117
|
+
if (typeof str !== 'string') throw new TypeError('sigalgs must be a string, got ' + typeof str);
|
|
118
|
+
var out = [], tokens = splitList(str);
|
|
119
|
+
for (var i = 0; i < tokens.length; i++) {
|
|
120
|
+
var s = SCHEMES[tokens[i].toUpperCase()];
|
|
121
|
+
if (s === undefined) throw new Error('sigalgs: unsupported signature algorithm "' + tokens[i] + '"');
|
|
122
|
+
// ML-DSA only where node:crypto has it (session/signing.js).
|
|
123
|
+
if (s >= 0x0904 && s <= 0x0906 && !MLDSA_AVAILABLE) throw new Error('sigalgs: "' + tokens[i] + '" is not available in this Node.js');
|
|
124
|
+
if (out.indexOf(s) < 0) out.push(s);
|
|
125
|
+
}
|
|
126
|
+
if (out.length === 0) throw new Error('sigalgs: none in "' + str + '"');
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 'TLSv1.2' / 'TLSv1.3' → 0x0303 / 0x0304, as node:tls names them. */
|
|
131
|
+
function parseTlsVersion(v, name) {
|
|
132
|
+
if (v === undefined || v === null) return null;
|
|
133
|
+
if (v === 'TLSv1.3') return 0x0304;
|
|
134
|
+
if (v === 'TLSv1.2') return 0x0303;
|
|
135
|
+
throw new Error(name + ': "' + v + '" is not supported (TLSv1.2 or TLSv1.3)');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { parseCiphers, parseEcdhCurve, parseSigalgs, parseTlsVersion };
|