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.
@@ -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
- if (!tlsSetup.local_supported_groups) {
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({ key: options.key, cert: options.cert });
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
- ctx.currentFlight.push({ epoch: dtlsEpoch, contentType: CT.HANDSHAKE, payload: frags[i] });
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 === 'connected' || ctx.state === 'closed') {
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
- let record = buildRecord(f.epoch, f.contentType, f.payload);
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 === 'connected' || ctx.state === 'closed') return;
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
- // Everything else keeps the original behaviour verbatim. In particular the
1612
- // ACK for the client's final flight (which RFC 9147 §7.1 requires the peer
1613
- // to send explicitly, and which arrives at the application epoch) must
1614
- // still cancel that flight's retransmit timer.
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
- cancelRetransmit();
1666
- startNewFlight();
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) {
@@ -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 };