lemon-tls 0.7.0 → 0.9.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,
@@ -331,6 +336,21 @@ function DTLSSession(options) {
331
336
  if (options.groups) tlsSetup.local_supported_groups = options.groups;
332
337
  if (options.alpnProtocols) tlsSetup.local_supported_alpns = options.alpnProtocols;
333
338
 
339
+ // Default key-exchange groups, BOTH roles. With DTLS 1.3 offered: the
340
+ // hybrid post-quantum group first (X25519MLKEM768, when node:crypto has
341
+ // ML-KEM — see ecdh.js), then the classical ones. In WebRTC either peer can
342
+ // end up the DTLS client, and harvest-now-decrypt-later applies to media
343
+ // too (Chrome rolls PQ DTLS out from 149). A client leading with the hybrid
344
+ // group sends a classical share beside it (tls_session), so a classical
345
+ // server costs nothing extra. DTLS 1.2 only: classical — a KEM does not fit
346
+ // its key exchange, and the server skips KEM groups there anyway.
347
+ if (!tlsSetup.local_supported_groups) {
348
+ // (ECDH_SUPPORTED_GROUPS leads with the hybrid too now; tls_session leaves
349
+ // a KEM group out of a hello that does not offer 1.3.)
350
+ tlsSetup.local_supported_groups = (versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0
351
+ ? SERVER_DEFAULT_GROUPS : ECDH_SUPPORTED_GROUPS).slice();
352
+ }
353
+
334
354
  // Server defaults — TLSSession client auto-populates these, but server expects them set externally
335
355
  if (isServer) {
336
356
  if (!tlsSetup.local_supported_cipher_suites) {
@@ -340,16 +360,20 @@ function DTLSSession(options) {
340
360
  versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0
341
361
  );
342
362
  }
343
- if (!tlsSetup.local_supported_groups) {
344
- tlsSetup.local_supported_groups = ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
345
- }
363
+ // (key-exchange groups: set above, for both roles)
346
364
  // signing.js owns the scheme list; DTLS spans 1.2+1.3 so include PKCS#1.
347
365
  tlsSetup.local_supported_signature_algorithms = default_signature_schemes();
348
366
  }
349
367
 
350
- // Server cert/key
368
+ // Server cert/key.
369
+ //
370
+ // The whole options object goes in, not just key+cert: createSecureContext
371
+ // also reads passphrase, ca, ciphers, sigalgs, ecdhCurve, honorCipherOrder,
372
+ // pfx and ocsp, and picking out two fields silently dropped all of them —
373
+ // an encrypted private key could never be decrypted here no matter what the
374
+ // caller passed. Same defect the TLS paths carried.
351
375
  if (options.cert && options.key) {
352
- let sctx = createSecureContext({ key: options.key, cert: options.cert });
376
+ let sctx = createSecureContext(options);
353
377
  tlsSetup.local_cert_chain = sctx.certificateChain;
354
378
  tlsSetup.cert_private_key = sctx.privateKey;
355
379
  }
@@ -1020,6 +1044,20 @@ function DTLSSession(options) {
1020
1044
  }
1021
1045
 
1022
1046
  function processRecord(record) {
1047
+ // A record in an epoch above 0 that did not decrypt is not the peer's
1048
+ // (forged, corrupted, or a late packet from a previous session on the
1049
+ // same address) — RFC 6347 §4.1.2.7 / RFC 9147 §4.5.2: discard it
1050
+ // silently. Its content is still ciphertext: as an ALERT its second byte
1051
+ // is random, which emitted 'error' and failed the whole connection; as
1052
+ // APPLICATION_DATA it went up to SCTP as data.
1053
+ if (record.epoch > 0 && !record.encrypted) return;
1054
+ // Once connected, everything the peer sends is protected. A plaintext
1055
+ // (epoch 0) alert or application data at that point is unauthenticated —
1056
+ // anyone able to send a UDP packet from the peer's address could close
1057
+ // the connection with a one-record close_notify, or inject data.
1058
+ if (ctx.state === 'connected' && !record.encrypted &&
1059
+ (record.type === CT.ALERT || record.type === CT.APPLICATION_DATA)) return;
1060
+
1023
1061
  if (record.type === CT.HANDSHAKE) {
1024
1062
  // An epoch>0 record that could not be decrypted (keys not yet
1025
1063
  // installed, AEAD failure) must NOT be fed to the handshake
@@ -1841,6 +1879,24 @@ function DTLSSession(options) {
1841
1879
 
1842
1880
  /** Peer certificate. */
1843
1881
  getPeerCertificate: function() { return tls.getPeerCertificate(); },
1882
+
1883
+ /** TLS fingerprints (see TLSSession): JA4, JA5 of the ClientHello, JA4S
1884
+ * of the ServerHello. The transport character resolves to 'd' on its
1885
+ * own — the inner TLSSession reads the 0xFE high byte off the hello's
1886
+ * legacy_version — so no `protocol` override is needed for plain DTLS. */
1887
+ getJA4: function(opts) { return tls.getJA4(opts); },
1888
+ getJA5: function(opts) { return tls.getJA5(opts); },
1889
+ getJA4S: function(opts) { return tls.getJA4S(opts); },
1890
+
1891
+ /** Set the key and certificate for this connection (Node parity). */
1892
+ setKeyCert: function(ctxOrOpts) { return tls.setKeyCert(ctxOrOpts); },
1893
+
1894
+ /** Finished verify_data, ours and the peer's (RFC 5929 channel binding). */
1895
+ getFinished: function() { return tls.getFinished(); },
1896
+ getPeerFinished: function() { return tls.getPeerFinished(); },
1897
+
1898
+ /** Whether this connection resumed an earlier session. */
1899
+ isSessionReused: function() { return !!tls.isResumed; },
1844
1900
  };
1845
1901
 
1846
1902
  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 };
package/src/record.js CHANGED
@@ -373,10 +373,52 @@ function buildAad12(seqNum, recordType, plaintextLen) {
373
373
  }
374
374
 
375
375
  /**
376
- * Encrypt a TLS 1.2 GCM record fragment. Returns explicit_nonce(8) || ciphertext || tag(16).
376
+ * TLS 1.2 ChaCha20-Poly1305 (RFC 7905), shared by the three TLS 1.2 record
377
+ * functions below. Told apart from AES-GCM the way the DTLS 1.2 functions do
378
+ * it: by the write IV the key block gave the suite — 12 bytes for ChaCha20,
379
+ * a 4-byte salt for GCM. No explicit nonce on the wire; the nonce is the IV
380
+ * XOR the sequence number; the AAD is the same as GCM's.
381
+ *
382
+ * The TLS 1.2 functions handled GCM only. A ChaCha20 suite was negotiated —
383
+ * it is in the default offer, and clients without AES hardware prefer it —
384
+ * and then every record failed: the peer's Finished did not decrypt
385
+ * (bad_record_mac), and ours could not have been read either.
386
+ */
387
+ function chacha12Seal(pt, key, iv12, seqNum, recordType) {
388
+ const cipher = crypto.createCipheriv('chacha20-poly1305', key, getChachaNonce12(iv12, seqNum), { authTagLength: 16 });
389
+ cipher.setAAD(buildAad12(seqNum, recordType, pt.length), { plaintextLength: pt.length });
390
+ const ct = cipher.update(pt);
391
+ cipher.final();
392
+ return { ct: ct, tag: cipher.getAuthTag() };
393
+ }
394
+
395
+ function chacha12Open(fragment, key, iv12, seqNum, recordType) {
396
+ if (fragment.length < 16) throw new Error('TLS 1.2 ChaCha20 fragment too short');
397
+ const ct = fragment.subarray(0, fragment.length - 16);
398
+ const tag = fragment.subarray(fragment.length - 16);
399
+ const decipher = crypto.createDecipheriv('chacha20-poly1305', key, getChachaNonce12(iv12, seqNum), { authTagLength: 16 });
400
+ decipher.setAAD(buildAad12(seqNum, recordType, ct.length), { plaintextLength: ct.length });
401
+ decipher.setAuthTag(tag);
402
+ const pt = decipher.update(ct);
403
+ decipher.final();
404
+ return pt;
405
+ }
406
+
407
+ /**
408
+ * Encrypt a TLS 1.2 AEAD record fragment.
409
+ * AES-GCM (4-byte salt): explicit_nonce(8) || ciphertext || tag(16).
410
+ * ChaCha20-Poly1305 (12-byte IV): ciphertext || tag(16).
377
411
  * Perf: computes seq-bytes once for both explicit nonce and AAD (was computed twice).
378
412
  */
379
413
  function encrypt12(pt, key, salt4, seqNum, recordType) {
414
+ if (salt4.length === 12) {
415
+ const sealed = chacha12Seal(pt, key, salt4, seqNum, recordType);
416
+ const out12 = new Uint8Array(sealed.ct.length + 16);
417
+ out12.set(sealed.ct, 0);
418
+ out12.set(sealed.tag, sealed.ct.length);
419
+ return out12;
420
+ }
421
+
380
422
  // Shared big-endian seq encoding (used as both explicit_nonce and AAD[0..8])
381
423
  const hi = (seqNum / 0x100000000) | 0;
382
424
  const lo = seqNum >>> 0;
@@ -437,6 +479,23 @@ function encrypt12(pt, key, salt4, seqNum, recordType) {
437
479
  */
438
480
  function encryptCompleteRecord12(pt, key, salt4, seqNum, recordType, version) {
439
481
  const ptLen = pt.length;
482
+
483
+ // ChaCha20-Poly1305: header || ciphertext || tag — no explicit IV.
484
+ if (salt4.length === 12) {
485
+ const sealed = chacha12Seal(pt, key, salt4, seqNum, recordType);
486
+ const cBodyLen = ptLen + 16;
487
+ const cVer = version || 0x0303;
488
+ const cRec = Buffer.allocUnsafe(5 + cBodyLen);
489
+ cRec[0] = recordType & 0xff;
490
+ cRec[1] = (cVer >>> 8) & 0xff;
491
+ cRec[2] = cVer & 0xff;
492
+ cRec[3] = (cBodyLen >>> 8) & 0xff;
493
+ cRec[4] = cBodyLen & 0xff;
494
+ sealed.ct.copy(cRec, 5);
495
+ sealed.tag.copy(cRec, 5 + ptLen);
496
+ return cRec;
497
+ }
498
+
440
499
  // Record body: 8 explicit_iv + ptLen ciphertext + 16 tag
441
500
  const bodyLen = 8 + ptLen + 16;
442
501
  const ver = version || 0x0303;
@@ -493,11 +552,15 @@ function encryptCompleteRecord12(pt, key, salt4, seqNum, recordType, version) {
493
552
  }
494
553
 
495
554
  /**
496
- * Decrypt a TLS 1.2 GCM record fragment. Input: explicit_nonce(8) || ciphertext || tag(16).
555
+ * Decrypt a TLS 1.2 AEAD record fragment.
556
+ * AES-GCM input: explicit_nonce(8) || ciphertext || tag(16).
557
+ * ChaCha20-Poly1305 input: ciphertext || tag(16).
497
558
  * Perf: subarray (zero-copy views) instead of slice (copies); returns Node Buffer
498
559
  * (which IS a Uint8Array subclass) instead of re-copying to a plain Uint8Array.
499
560
  */
500
561
  function decrypt12(fragment, key, salt4, seqNum, recordType) {
562
+ if (salt4.length === 12) return chacha12Open(fragment, key, salt4, seqNum, recordType);
563
+
501
564
  if (fragment.length < 24) throw new Error('TLS 1.2 fragment too short');
502
565
 
503
566
  // Zero-copy views over the input
@@ -732,7 +795,9 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
732
795
  decipher.setAuthTag(tag);
733
796
  let pt = decipher.update(ct);
734
797
  decipher.final();
735
- return new Uint8Array(pt);
798
+ // A view, not a copy: pt is a fresh Buffer that owns its whole
799
+ // ArrayBuffer and nothing else holds it. Same Uint8Array type as before.
800
+ return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
736
801
  }
737
802
 
738
803
  // AES-GCM: strip the 8-byte explicit nonce.
@@ -748,7 +813,8 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
748
813
  decipher.setAuthTag(tag);
749
814
  let pt = decipher.update(ct);
750
815
  decipher.final();
751
- return new Uint8Array(pt);
816
+ // A view, not a copy — see the ChaCha branch above.
817
+ return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
752
818
  }
753
819
 
754
820
  /** Build complete encrypted DTLS 1.2 record (header + encrypted payload). */
@@ -1102,7 +1168,8 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1102
1168
  seq = acceptedSeq;
1103
1169
 
1104
1170
  {
1105
- let inner = parseInnerPlaintext(new Uint8Array(pt));
1171
+ // A view, not a copy: pt is the decipher's fresh Buffer, owned by no one else.
1172
+ let inner = parseInnerPlaintext(new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength));
1106
1173
 
1107
1174
  // RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies do we consult
1108
1175
  // and update the sliding window. Checking before deprotection would let a
@@ -60,17 +60,28 @@ function makeX509FromDerOrPem(buf) {
60
60
  }
61
61
 
62
62
  function makePrivateKeyFromDerOrPem(buf, passphrase) {
63
+ // node:crypto rejects an explicit `passphrase: null` with ERR_INVALID_ARG_VALUE
64
+ // — for an unencrypted key the property must be ABSENT, not present-and-null.
65
+ // Callers that forward an optional option straight through would otherwise
66
+ // break every unencrypted key, which is the overwhelmingly common case.
67
+ let pass = (passphrase === null || passphrase === undefined) ? undefined : passphrase;
68
+
63
69
  if (isPEM(buf)) {
64
- return crypto.createPrivateKey({ key: buf, format: 'pem', passphrase: passphrase });
70
+ let opts = { key: buf, format: 'pem' };
71
+ if (pass !== undefined) opts.passphrase = pass;
72
+ return crypto.createPrivateKey(opts);
65
73
  } else {
66
74
  let der = Buffer.from(buf), keyObj = null;
67
75
  try {
68
- keyObj = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8', passphrase: passphrase });
76
+ keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'pkcs8' },
77
+ pass !== undefined ? { passphrase: pass } : {}));
69
78
  } catch (e1) {
70
79
  try {
71
- keyObj = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs1', passphrase: passphrase });
80
+ keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'pkcs1' },
81
+ pass !== undefined ? { passphrase: pass } : {}));
72
82
  } catch (e2) {
73
- keyObj = crypto.createPrivateKey({ key: der, format: 'der', type: 'sec1', passphrase: passphrase });
83
+ keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'sec1' },
84
+ pass !== undefined ? { passphrase: pass } : {}));
74
85
  }
75
86
  }
76
87
  return keyObj;
@@ -106,6 +117,40 @@ function dedupeDerArray(arr) {
106
117
  function createSecureContext(options) {
107
118
  if (!options) options = {};
108
119
 
120
+ // Several key/certificate pairs — node:tls's arrays: `key` an array of
121
+ // keys (or { pem, passphrase }), `cert` an array of chains, one per key —
122
+ // for a server that holds certificates of more than one kind: an ML-DSA one
123
+ // for clients that ask for post-quantum authentication, an ECDSA or RSA one
124
+ // for everyone else. Each key is paired with the chain whose leaf carries
125
+ // its public key, in whatever order they were given; the first key's pair
126
+ // is the default, the others `alternates`. Which one a handshake presents
127
+ // is the client's signature_algorithms' to decide (tls_session).
128
+ if (Array.isArray(options.key) && options.key.length > 1) {
129
+ let certs = Array.isArray(options.cert) ? options.cert : [options.cert];
130
+ let contexts = options.key.map(function (k, i) {
131
+ let keyIn = (k && typeof k === 'object' && !Buffer.isBuffer(k) && !(k instanceof Uint8Array) && k.pem) ? k.pem : k;
132
+ let pass = (k && typeof k === 'object' && k.passphrase != null) ? k.passphrase : options.passphrase;
133
+ let pub = crypto.createPublicKey(makePrivateKeyFromDerOrPem(readMaybeFile(keyIn), pass)).export({ type: 'spki', format: 'der' });
134
+ let cert = null;
135
+ for (let j = 0; j < certs.length && cert === null; j++) {
136
+ let raw = readMaybeFile(certs[j]);
137
+ let first = isPEM(raw) ? splitPEMBlocks(raw.toString('utf8')).filter(function (b) { return b.type.indexOf('CERTIFICATE') !== -1; })[0] : { der: new Uint8Array(raw) };
138
+ if (!first) continue;
139
+ let leafPub = makeX509FromDerOrPem(first.der).publicKey.export({ type: 'spki', format: 'der' });
140
+ if (Buffer.compare(Buffer.from(leafPub), Buffer.from(pub)) === 0) cert = certs[j];
141
+ }
142
+ if (cert === null) throw new Error('createSecureContext: no certificate in `cert` for key ' + i);
143
+ return createSecureContext(Object.assign({}, options, { key: keyIn, passphrase: pass, cert: cert }));
144
+ });
145
+ let primary = contexts[0];
146
+ primary.alternates = contexts.slice(1).map(function (c) {
147
+ return { certificateChain: c.certificateChain, privateKey: c.privateKey, keyObj: c.keyObj, certObjs: c.certObjs };
148
+ });
149
+ return primary;
150
+ }
151
+ if (Array.isArray(options.key)) options = Object.assign({}, options, { key: options.key[0] && options.key[0].pem ? options.key[0].pem : options.key[0] });
152
+ if (Array.isArray(options.cert) && options.cert.length === 1) options = Object.assign({}, options, { cert: options.cert[0] });
153
+
109
154
  // --- תעודות (כולל שרשרת בתוך cert אם קיימת) ---
110
155
  let certBlocksDer = [];
111
156
  let certObjs = [];