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/package.json CHANGED
@@ -1,10 +1,26 @@
1
1
  {
2
2
  "name": "lemon-tls",
3
- "version": "0.7.0",
3
+ "version": "0.10.0",
4
4
  "description": "TLS 1.3/1.2 DTLS 1.3/1.2 implementation for Node.js - full control over cryptographic keys, record layer, and handshake. Drop-in replacement for node:tls with advanced options impossible in OpenSSL.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "types": "index.d.ts",
8
+ "typesVersions": {
9
+ "*": {
10
+ "wire": [
11
+ "types/wire.d.ts"
12
+ ],
13
+ "crypto": [
14
+ "types/crypto.d.ts"
15
+ ],
16
+ "record": [
17
+ "types/record.d.ts"
18
+ ],
19
+ "session": [
20
+ "types/session.d.ts"
21
+ ]
22
+ }
23
+ },
8
24
  "exports": {
9
25
  ".": {
10
26
  "types": "./index.d.ts",
@@ -12,22 +28,35 @@
12
28
  "require": "./index.cjs",
13
29
  "default": "./index.js"
14
30
  },
15
- "./wire": "./src/wire.js",
16
- "./crypto": "./src/crypto.js",
17
- "./record": "./src/record.js",
18
- "./session": "./src/tls_session.js"
31
+ "./wire": {
32
+ "types": "./types/wire.d.ts",
33
+ "default": "./src/wire.js"
34
+ },
35
+ "./crypto": {
36
+ "types": "./types/crypto.d.ts",
37
+ "default": "./src/crypto.js"
38
+ },
39
+ "./record": {
40
+ "types": "./types/record.d.ts",
41
+ "default": "./src/record.js"
42
+ },
43
+ "./session": {
44
+ "types": "./types/session.d.ts",
45
+ "default": "./src/tls_session.js"
46
+ }
19
47
  },
20
48
  "files": [
21
49
  "index.js",
22
50
  "index.cjs",
23
51
  "index.d.ts",
52
+ "types/",
24
53
  "src/",
25
54
  "README.md",
26
55
  "LICENSE",
27
56
  "lemontls.svg"
28
57
  ],
29
58
  "engines": {
30
- "node": ">=16.0.0"
59
+ "node": ">=24.0.0"
31
60
  },
32
61
  "keywords": [
33
62
  "tls",
@@ -70,6 +99,10 @@
70
99
  "alpn",
71
100
  "ja3",
72
101
  "ja3-fingerprint",
102
+ "ja4",
103
+ "ja4-fingerprint",
104
+ "ja5",
105
+ "ja5-fingerprint",
73
106
  "fingerprint",
74
107
  "keylog",
75
108
  "wireshark",
package/src/compat.js CHANGED
@@ -6,12 +6,15 @@
6
6
  */
7
7
 
8
8
  import net from 'node:net';
9
+ import nodeTls from 'node:tls'; // for the shipped CA bundle only
9
10
  import crypto from 'node:crypto';
10
11
  import { EventEmitter } from 'node:events';
11
12
  import { TLS_CIPHER_SUITES } from './crypto.js';
12
13
  import TLSSession from './tls_session.js';
13
14
  import TLSSocket from './tls_socket.js';
14
15
  import createSecureContext from './secure_context.js';
16
+ import { parseCiphers, parseEcdhCurve, parseSigalgs } from './node_options.js';
17
+ import { prepare_ech_server } from './session/ech.js';
15
18
 
16
19
  // ===================== Constants =====================
17
20
 
@@ -44,6 +47,46 @@ function getCiphers() {
44
47
  return out;
45
48
  }
46
49
 
50
+ /* ===================== Default CA store ===================== */
51
+
52
+ /**
53
+ * The trust anchors Node ships (Mozilla's CA bundle). Exposed under the same
54
+ * name Node uses so `tls.rootCertificates` works unchanged, and so this
55
+ * library has a real default to chain against.
56
+ *
57
+ * That default matters more than parity. Until now, "no `ca` configured" meant
58
+ * "authorize every certificate", while `rejectUnauthorized` defaulted to true
59
+ * — the connection reported itself verified without ever verifying anything.
60
+ * Node never behaved that way: it falls back to this bundle.
61
+ *
62
+ * Parsing all ~144 certificates costs a few tens of milliseconds, so it is
63
+ * done once, lazily, and only if something actually needs it.
64
+ */
65
+ let _rootPems = null;
66
+ function getRootCertificates() {
67
+ if (_rootPems === null) {
68
+ try { _rootPems = nodeTls.rootCertificates.slice(); }
69
+ catch (e) { _rootPems = []; } // a build without the bundle
70
+ }
71
+ return _rootPems;
72
+ }
73
+
74
+ /** Node-compat: tls.getCACertificates([type]). 'default' and 'bundled' both
75
+ * resolve to the shipped bundle; 'extra' and 'system' are not sourced here
76
+ * and return empty rather than pretending. */
77
+ function getCACertificates(type) {
78
+ let t = type || 'default';
79
+ if (t === 'default' || t === 'bundled') return getRootCertificates().slice();
80
+ return [];
81
+ }
82
+
83
+ /** Node-compat: tls.setDefaultCACertificates(certs). Replaces the bundle for
84
+ * every subsequent connection that does not pass its own `ca`. */
85
+ function setDefaultCACertificates(certs) {
86
+ if (!Array.isArray(certs)) throw new TypeError('certs must be an array');
87
+ _rootPems = certs.slice();
88
+ }
89
+
47
90
  // ===================== tls.checkServerIdentity() =====================
48
91
 
49
92
  /**
@@ -177,20 +220,49 @@ function connect(/* ...args */) {
177
220
  isServer: false,
178
221
  servername: options.servername || host,
179
222
  rejectUnauthorized: options.rejectUnauthorized,
223
+ checkServerIdentity: options.checkServerIdentity,
224
+ // Stream-shape options Node forwards to the socket. Both were dropped
225
+ // here, so allowHalfOpen could not be set through connect() at all.
226
+ allowHalfOpen: options.allowHalfOpen,
227
+ highWaterMark: options.highWaterMark,
228
+ secureContext: options.secureContext,
229
+ passphrase: options.passphrase,
180
230
  ca: options.ca,
231
+ useDefaultCA: options.useDefaultCA,
232
+ defaultCA: getRootCertificates,
181
233
  session: options.session,
182
234
  ALPNProtocols: options.ALPNProtocols,
183
235
  minVersion: options.minVersion,
184
236
  maxVersion: options.maxVersion,
185
- signatureAlgorithms: options.signatureAlgorithms,
186
- groups: options.groups,
237
+ // node:tls's own strings (ciphers, ecdhCurve, sigalgs) — taken by
238
+ // nothing until now — or lemon-tls's code lists, which win.
239
+ signatureAlgorithms: options.signatureAlgorithms || parseSigalgs(options.sigalgs),
240
+ groups: options.groups || parseEcdhCurve(options.ecdhCurve),
187
241
  prioritizeChaCha: options.prioritizeChaCha,
242
+ // Forwarded on the server side but dropped here, so a client could not
243
+ // narrow the suites it offers.
244
+ allowedCipherSuites: options.allowedCipherSuites || parseCiphers(options.ciphers),
188
245
  maxRecordSize: options.maxRecordSize,
189
246
  sessionTickets: options.sessionTickets,
190
247
  ticketLifetime: options.ticketLifetime,
248
+ // RFC 8879: algorithms to offer for the server's certificate chain.
249
+ certificateCompression: options.certificateCompression,
250
+ // node:tls: ask the server for a stapled OCSP response ('OCSPResponse').
251
+ requestOCSP: options.requestOCSP === true,
191
252
  cert: options.cert,
192
253
  key: options.key,
193
254
  maxHandshakeSize: options.maxHandshakeSize,
255
+ // Encrypted Client Hello (RFC 9849): the server's ECHConfigList (bytes or
256
+ // base64 — the `ech=` of its HTTPS record), or GREASE ECH without one.
257
+ ech: options.ech,
258
+ echGrease: options.echGrease === true,
259
+ // ClientHello shaping, documented as connect() options and dropped
260
+ // here: neither reached the socket.
261
+ grease: options.grease,
262
+ permuteExtensions: options.permuteExtensions,
263
+ // 0-RTT: with `session` whose ticket allows it, what is written before
264
+ // 'secureConnect' goes in the first flight ('earlyData' says if taken).
265
+ earlyData: options.earlyData === true,
194
266
  });
195
267
 
196
268
  addCompatMethods(socket);
@@ -199,9 +271,32 @@ function connect(/* ...args */) {
199
271
  socket.on('secureConnect', connectListener);
200
272
  }
201
273
 
202
- let tcp = net.connect(port, host, function() {
274
+ let tcp = null;
275
+
276
+ if (options.socket) {
277
+ // Node: "Establish secure connection on a given socket rather than
278
+ // creating a new socket... If this option is specified, path, host, and
279
+ // port are ignored, except for certificate validation." Crucially it also
280
+ // says connection and teardown of that socket remain the caller's job and
281
+ // that tls.connect() will not call net.connect() at all — so we attach and
282
+ // nothing else. This is what STARTTLS needs: the caller speaks cleartext
283
+ // first (SMTP, IMAP, PostgreSQL), then upgrades the same connection.
284
+ //
285
+ // The socket is usually already connected, but it need not be; the
286
+ // handshake simply starts once it is writable.
287
+ tcp = options.socket;
203
288
  socket.setSocket(tcp);
204
- });
289
+ } else {
290
+ // The rest of the options go to net.connect, as node:tls does ("any
291
+ // socket.connect() option not already listed"): noDelay, keepAlive,
292
+ // keepAliveInitialDelay, localAddress, localPort, family, lookup, timeout.
293
+ // Dropped here, none of them could be set through connect() at all.
294
+ let netOptions = Object.assign({}, options, { port: port, host: host });
295
+ delete netOptions.socket;
296
+ tcp = net.connect(netOptions, function() {
297
+ socket.setSocket(tcp);
298
+ });
299
+ }
205
300
 
206
301
  tcp.on('error', function(e) {
207
302
  socket.emit('error', e);
@@ -224,6 +319,7 @@ function connect(/* ...args */) {
224
319
  * Provides:
225
320
  * - listen(), close(), address(), getConnections()
226
321
  * - getTicketKeys(), setTicketKeys(), setSecureContext()
322
+ * - getEchKeys(), setEchKeys() — Encrypted Client Hello keys (RFC 9849)
227
323
  */
228
324
  function Server(options, connectionListener) {
229
325
  if (!(this instanceof Server)) return new Server(options, connectionListener);
@@ -232,9 +328,28 @@ function Server(options, connectionListener) {
232
328
  let self = this;
233
329
  options = options || {};
234
330
 
331
+ // Node throws on this combination rather than picking one, and so do we: the
332
+ // two answer the same question by different means, and silently preferring
333
+ // either would leave a server running a policy its author did not write.
334
+ if (options.ALPNProtocols && options.ALPNCallback) {
335
+ throw new TypeError(
336
+ 'The ALPNProtocols and ALPNCallback options are mutually exclusive');
337
+ }
338
+
235
339
  // Shared ticketKeys across all connections
236
340
  self._ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
237
341
 
342
+ // Encrypted Client Hello keys, checked here — a key that does not match
343
+ // its config throws now, not as every client's handshake failing.
344
+ prepare_ech_server(options.ech);
345
+ self._ech = options.ech || null;
346
+
347
+ // node:tls's strings, read once (node_options.js) — a bad one throws here,
348
+ // as Node's createServer does, not on the first connection.
349
+ let nodeSuites = options.allowedCipherSuites || parseCiphers(options.ciphers);
350
+ let nodeGroups = options.groups || parseEcdhCurve(options.ecdhCurve);
351
+ let nodeSigalgs = options.signatureAlgorithms || parseSigalgs(options.sigalgs);
352
+
238
353
  // sessionTimeout: seconds to cache a TLS 1.2 Session ID (Node default: 300)
239
354
  let sessionTimeoutSec = (typeof options.sessionTimeout === 'number' && options.sessionTimeout > 0)
240
355
  ? (options.sessionTimeout >>> 0)
@@ -251,10 +366,28 @@ function Server(options, connectionListener) {
251
366
  sessionIdContextHex = sidCtxBuf.toString('hex');
252
367
  }
253
368
 
254
- // Pre-compiled default SecureContext if key+cert provided directly (no SNICallback)
369
+ // Pre-compiled default SecureContext.
370
+ //
371
+ // Node parity: a caller may hand us a context they built once with
372
+ // tls.createSecureContext() and reuse across servers, instead of paying to
373
+ // re-parse the same PEM on every construction. An explicit `secureContext`
374
+ // wins over key/cert — same precedence Node uses.
375
+ // SNI contexts registered through addContext(). Each is compiled ONCE here
376
+ // and handed out by reference, which is the whole point: a hand-written
377
+ // SNICallback that calls createSecureContext() per connection re-parses the
378
+ // same PEM every time, and if it reads the files inside the callback it also
379
+ // does disk I/O on the handshake path.
380
+ //
381
+ // Node's matching rule: last registration wins on a tie, and an exact
382
+ // hostname beats a wildcard. Insertion order is preserved, so a later
383
+ // addContext() for the same pattern replaces the earlier one in place.
384
+ let sniContexts = []; // [{ pattern, re, ctx }]
385
+
255
386
  let defaultCtx = null;
256
- if (options.key && options.cert) {
257
- defaultCtx = createSecureContext({ key: options.key, cert: options.cert });
387
+ if (options.secureContext) {
388
+ defaultCtx = options.secureContext;
389
+ } else if (options.key && options.cert) {
390
+ defaultCtx = createSecureContext(options);
258
391
  }
259
392
 
260
393
  // In-memory fallback session store for TLS 1.2 Session IDs.
@@ -271,12 +404,40 @@ function Server(options, connectionListener) {
271
404
  let socketOpts = {
272
405
  isServer: true,
273
406
  ticketKeys: self._ticketKeys,
274
- ticketLifetime: options.ticketLifetime,
407
+ // node:tls's sessionTimeout is how long a session stays good — a
408
+ // ticket's lifetime as much as a cached Session ID's. It set the cache
409
+ // alone, and tickets went out with the default 7200 s whatever it said.
410
+ // ticketLifetime, lemon-tls's own name, where given.
411
+ ticketLifetime: options.ticketLifetime != null ? options.ticketLifetime
412
+ : (typeof options.sessionTimeout === 'number' ? options.sessionTimeout : undefined),
413
+ ticketData: options.ticketData,
414
+ // RFC 8879: compress the chain for a client that offers it (all
415
+ // algorithms unless set; false: never).
416
+ certificateCompression: options.certificateCompression,
417
+ // node:tls: the cipher suite by the server's order (default: the client's).
418
+ honorCipherOrder: options.honorCipherOrder === true,
419
+ // Encrypted Client Hello (RFC 9849): { keys: [{ config, privateKey, retry }] }.
420
+ ech: self._ech,
421
+ // node:tls's 'OCSPRequest': (certificate, issuer, callback) when a
422
+ // client asks for a stapled OCSP response. Without a listener nothing
423
+ // is stapled — and nothing waits.
424
+ ocspRequest: function (certificate, issuer, callback) {
425
+ if (self.listenerCount('OCSPRequest') === 0) return callback(null, null);
426
+ self.emit('OCSPRequest', Buffer.from(certificate), issuer ? Buffer.from(issuer) : null, callback);
427
+ },
275
428
  ALPNProtocols: options.ALPNProtocols,
429
+ ALPNCallback: options.ALPNCallback,
430
+ // 0-RTT (off unless asked for): tickets that allow it, and taking it
431
+ // under the session's checks — read on each socket as it comes
432
+ // (socket.isEarlyData), before 'secureConnection'.
433
+ earlyData: options.earlyData === true,
434
+ earlyDataMaxAge: options.earlyDataMaxAge,
435
+ earlyDataReplay: options.earlyDataReplay,
436
+ maxEarlyData: options.maxEarlyData,
276
437
  minVersion: options.minVersion || DEFAULT_MIN_VERSION,
277
438
  maxVersion: options.maxVersion || DEFAULT_MAX_VERSION,
278
- signatureAlgorithms: options.signatureAlgorithms,
279
- groups: options.groups,
439
+ signatureAlgorithms: nodeSigalgs,
440
+ groups: nodeGroups,
280
441
  prioritizeChaCha: options.prioritizeChaCha,
281
442
  maxRecordSize: options.maxRecordSize,
282
443
  sessionTickets: options.sessionTickets,
@@ -292,22 +453,37 @@ function Server(options, connectionListener) {
292
453
  rejectUnauthorized: options.rejectUnauthorized,
293
454
  ca: options.ca,
294
455
  maxHandshakeSize: options.maxHandshakeSize,
295
- allowedCipherSuites: options.allowedCipherSuites,
296
- handshakeTimeout: options.handshakeTimeout,
456
+ allowedCipherSuites: nodeSuites,
457
+ // node:tls's default for a server, 120 s. TLSSocket's own is none
458
+ // (it is also the client's, where Node has none either), and a server
459
+ // left at it let a client that sent half a ClientHello hold its socket
460
+ // for as long as it liked.
461
+ handshakeTimeout: options.handshakeTimeout != null ? options.handshakeTimeout : 120000,
462
+ // The server's default certificate. With an explicit SNICallback it was
463
+ // never handed to the socket, so a client sending no server_name got
464
+ // the callback asked with null instead of this certificate, as in Node.
465
+ secureContext: defaultCtx || undefined,
297
466
  };
298
467
 
299
468
  if (options.SNICallback) {
469
+ // An explicit callback wins outright — it may do things the registry
470
+ // cannot, such as fetching a certificate on demand.
300
471
  socketOpts.SNICallback = options.SNICallback;
301
- } else if (defaultCtx) {
472
+ } else if (defaultCtx || sniContexts.length > 0) {
302
473
  socketOpts.SNICallback = function(servername, cb) {
303
- cb(null, defaultCtx);
474
+ cb(null, contextForServername(servername));
304
475
  };
305
476
  }
306
477
 
307
478
  return socketOpts;
308
479
  }
309
480
 
310
- self._tcpServer = net.createServer(function(tcp) {
481
+ function acceptConnection(tcp) {
482
+ // Node: "emitted when a new TCP stream is established, before the TLS
483
+ // handshake begins... can also be explicitly emitted by users to inject
484
+ // connections into the TLS server. In that case, any Duplex stream can be
485
+ // passed." Emitting first is what makes that injection path work.
486
+ self.emit('connection', tcp);
311
487
  let socket;
312
488
  try {
313
489
  socket = new TLSSocket(tcp, buildSocketOpts());
@@ -368,8 +544,48 @@ function Server(options, connectionListener) {
368
544
  self.emit('tlsClientError', err, socket);
369
545
  }
370
546
  });
547
+ }
548
+
549
+ // node:tls's Server IS a net.Server, built from the same options object, so
550
+ // the TCP ones apply to every connection it accepts. Only those: the stream
551
+ // shape of the raw socket (allowHalfOpen, pauseOnConnect) is this layer's
552
+ // to manage, and the TLS socket takes allowHalfOpen itself.
553
+ self._tcpServer = net.createServer({
554
+ noDelay: options.noDelay === true,
555
+ keepAlive: options.keepAlive === true,
556
+ keepAliveInitialDelay: options.keepAliveInitialDelay || 0,
557
+ }, acceptConnection);
558
+
559
+ // The net.Server's own events, re-emitted: node:tls's Server IS a net.Server
560
+ // and emits these itself, and code written for it listens for them here.
561
+ // Without this, 'listening' never fired, and a failed bind (EADDRINUSE,
562
+ // EACCES) was emitted on an inner object nobody could reach — an uncaught
563
+ // exception that took the whole process down.
564
+ ['listening', 'error', 'close', 'drop'].forEach(function (name) {
565
+ self._tcpServer.on(name, function () {
566
+ if (name === 'error' && self.listenerCount('error') === 0) {
567
+ // Same as node:tls when nobody listens: it throws. Preserved, rather
568
+ // than swallowed — but from the Server a caller can now listen on.
569
+ throw arguments[0];
570
+ }
571
+ self.emit.apply(self, [name].concat(Array.prototype.slice.call(arguments)));
572
+ });
371
573
  });
372
574
 
575
+ /**
576
+ * Node documents 'connection' as injectable: emitting it by hand feeds an
577
+ * arbitrary Duplex into the TLS server. That cannot be done by listening for
578
+ * our own event — acceptConnection emits it, so the listener would re-enter
579
+ * on every real connection and loop. A method is the honest shape.
580
+ */
581
+ self.injectConnection = function(stream) {
582
+ if (!stream || typeof stream.write !== 'function') {
583
+ throw new TypeError('injectConnection requires a Duplex stream');
584
+ }
585
+ acceptConnection(stream);
586
+ return self;
587
+ };
588
+
373
589
  // Delegate net.Server methods
374
590
  self.listen = function() {
375
591
  return self._tcpServer.listen.apply(self._tcpServer, arguments);
@@ -387,6 +603,12 @@ function Server(options, connectionListener) {
387
603
  return self._tcpServer.getConnections(cb);
388
604
  };
389
605
 
606
+ // node:net's: whether it is listening, and whether the process may exit
607
+ // while it does.
608
+ Object.defineProperty(self, 'listening', { get: function() { return self._tcpServer.listening; }, enumerable: true });
609
+ self.ref = function() { self._tcpServer.ref(); return self; };
610
+ self.unref = function() { self._tcpServer.unref(); return self; };
611
+
390
612
  // Ticket key management
391
613
  self.getTicketKeys = function() {
392
614
  return Buffer.from(self._ticketKeys);
@@ -402,13 +624,88 @@ function Server(options, connectionListener) {
402
624
  self._ticketKeys = Buffer.from(keys);
403
625
  };
404
626
 
405
- // setSecureContext — replace cert/key without restart (Node.js compat)
627
+ // ECH keys. Rotation without a restart: publish the new config in DNS,
628
+ // then setEchKeys with the new key first (retry: true) and the old one
629
+ // kept (retry: false) until the old DNS record's TTL has run out — clients
630
+ // still holding it can then connect. New connections use the new set.
631
+ self.getEchKeys = function() {
632
+ return self._ech;
633
+ };
634
+
635
+ self.setEchKeys = function(ech) {
636
+ prepare_ech_server(ech); // throws on a bad key, keeping the old set
637
+ self._ech = ech || null;
638
+ };
639
+
640
+ // setSecureContext — replace cert/key without restart (Node.js compat).
641
+ // A bad one throws, as Node's does, and the certificate in use stays: it
642
+ // returned quietly when key or cert was missing, and a renewal that read
643
+ // an empty file went unnoticed until the old certificate expired.
406
644
  self.setSecureContext = function(opts) {
407
- if (opts && opts.key && opts.cert) {
408
- defaultCtx = createSecureContext({ key: opts.key, cert: opts.cert });
645
+ if (!opts || typeof opts !== 'object') throw new TypeError('setSecureContext requires { key, cert } or { secureContext }');
646
+ defaultCtx = opts.secureContext ? opts.secureContext : createSecureContext(opts);
647
+ };
648
+
649
+ /**
650
+ * Node-compat: addContext(hostname, context)
651
+ *
652
+ * Register a certificate for an SNI host name or wildcard. `context` is
653
+ * either a compiled context from createSecureContext() or the plain
654
+ * { key, cert, ... } options to compile from — Node accepts both.
655
+ *
656
+ * The declarative alternative to writing an SNICallback by hand. It matters
657
+ * for more than tidiness: the common hand-rolled callback compiles a context
658
+ * per connection, and often reads the cert off disk inside it. Here every
659
+ * context is compiled once at registration and served by reference.
660
+ */
661
+ self.addContext = function(hostname, context) {
662
+ if (!hostname) throw new TypeError('addContext requires a hostname');
663
+ let ctx = context;
664
+ if (!ctx || (!ctx.certificateChain && !ctx.privateKey)) {
665
+ if (!context || !context.key || !context.cert) {
666
+ throw new TypeError('addContext requires a SecureContext or { key, cert }');
667
+ }
668
+ ctx = createSecureContext(context);
409
669
  }
670
+ let pattern = String(hostname).toLowerCase();
671
+ let entry = { pattern: pattern, re: sniPatternToRegExp(pattern), ctx: ctx };
672
+
673
+ // Replace in place rather than appending a duplicate, so re-registering a
674
+ // host swaps its certificate without leaving the old one shadowing it.
675
+ let i = sniContexts.findIndex(function(e){ return e.pattern === pattern; });
676
+ if (i >= 0) sniContexts[i] = entry;
677
+ else sniContexts.push(entry);
678
+ return self;
410
679
  };
411
680
 
681
+ /**
682
+ * Wildcards match ONE label and only as the leftmost one, per RFC 6125:
683
+ * `*.example.com` covers `a.example.com` but neither `a.b.example.com` nor
684
+ * the bare `example.com`. A bare `*` matches any single-or-multi label name
685
+ * and is the documented catch-all.
686
+ */
687
+ function sniPatternToRegExp(pattern) {
688
+ if (pattern === '*') return /^.+$/;
689
+ let esc = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
690
+ return new RegExp('^' + esc.replace(/\*/g, '[^.]+') + '$');
691
+ }
692
+
693
+ /**
694
+ * Resolve a servername to a context. Exact matches beat wildcards; among
695
+ * equals the most recently added wins, which is what Node documents.
696
+ */
697
+ function contextForServername(servername) {
698
+ if (!servername) return defaultCtx;
699
+ let name = String(servername).toLowerCase();
700
+ for (let i = sniContexts.length - 1; i >= 0; i--) {
701
+ if (sniContexts[i].pattern === name) return sniContexts[i].ctx;
702
+ }
703
+ for (let i = sniContexts.length - 1; i >= 0; i--) {
704
+ if (sniContexts[i].re.test(name)) return sniContexts[i].ctx;
705
+ }
706
+ return defaultCtx;
707
+ }
708
+
412
709
  return self;
413
710
  }
414
711
 
@@ -453,20 +750,14 @@ function addCompatMethods(socket) {
453
750
  if (typeof socket[name] !== 'function') socket[name] = fn;
454
751
  }
455
752
 
456
- /** Node.js compat: isSessionReused() — method form (same as isResumed getter) */
457
- def('isSessionReused', function() {
458
- return socket.isResumed;
459
- });
460
-
461
- /** Node.js compat: getFinished() */
462
- def('getFinished', function() {
463
- return session.getFinished ? session.getFinished() : null;
464
- });
465
-
466
- /** Node.js compat: getPeerFinished() */
467
- def('getPeerFinished', function() {
468
- return session.getPeerFinished ? session.getPeerFinished() : null;
469
- });
753
+ // isSessionReused, getFinished, getPeerFinished, getEphemeralKeyInfo and
754
+ // address now ship natively on TLSSocket, so their shims here were dead code
755
+ // that def() would never install — and they had drifted from Node in ways
756
+ // the native versions fix: getFinished returned null where Node documents
757
+ // undefined, and getEphemeralKeyInfo reported type 'X25519', which is not
758
+ // one of the three types Node defines ('DH', 'ECDH', 'TLSGroup'), and never
759
+ // returned null on a server socket. Two divergent implementations of one
760
+ // method is the duplication this layer exists to avoid, so they are gone.
470
761
 
471
762
  /** Node.js compat: exportKeyingMaterial(length, label, context).
472
763
  * TLSSocket now ships this natively (RFC 5705 / RFC 8446 §7.5 via the
@@ -476,15 +767,6 @@ function addCompatMethods(socket) {
476
767
  return session.exportKeyingMaterial ? session.exportKeyingMaterial(length, label, context) : null;
477
768
  });
478
769
 
479
- /** Node.js compat: getEphemeralKeyInfo() */
480
- def('getEphemeralKeyInfo', function() {
481
- let group = session.context.selected_group;
482
- if (group === 0x001d) return { type: 'X25519', size: 253 };
483
- if (group === 0x0017) return { type: 'ECDH', name: 'prime256v1', size: 256 };
484
- if (group === 0x0018) return { type: 'ECDH', name: 'secp384r1', size: 384 };
485
- return {};
486
- });
487
-
488
770
  /** Node.js compat: setServername(name) */
489
771
  def('setServername', function(name) {
490
772
  session.set_context({ local_sni: name });
@@ -493,11 +775,6 @@ function addCompatMethods(socket) {
493
775
  /** Node.js compat: disableRenegotiation() — no-op (renegotiation not supported) */
494
776
  def('disableRenegotiation', function() {});
495
777
 
496
- /** Node.js compat: address() — delegates to underlying transport */
497
- def('address', function() {
498
- try { return session.context && session.context.transport ? session.context.transport.address() : {}; }
499
- catch(e) { return {}; }
500
- });
501
778
  }
502
779
 
503
780
  // ===================== Exports =====================
@@ -510,6 +787,8 @@ export {
510
787
  getCiphers,
511
788
  checkServerIdentity,
512
789
  addCompatMethods,
790
+ getCACertificates,
791
+ setDefaultCACertificates,
513
792
  DEFAULT_MIN_VERSION,
514
793
  DEFAULT_MAX_VERSION,
515
794
  DEFAULT_CIPHERS,
package/src/crypto.js CHANGED
@@ -129,6 +129,7 @@ const TLS_CIPHER_SUITES = {
129
129
  hash: 'sha256'
130
130
  },
131
131
  0xCCAA: { // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
132
+ name: 'DHE-RSA-CHACHA20-POLY1305', standardName: 'TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
132
133
  tls: 12,
133
134
  kex: 'DHE_RSA',
134
135
  sig: 'RSA',
@@ -171,6 +172,7 @@ const TLS_CIPHER_SUITES = {
171
172
  // TLS 1.2 CBC (Legacy)
172
173
  // ----------------------
173
174
  0xC013: { // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
175
+ name: 'ECDHE-RSA-AES128-SHA', standardName: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
174
176
  tls: 12,
175
177
  kex: 'ECDHE_RSA',
176
178
  sig: 'RSA',
@@ -183,6 +185,7 @@ const TLS_CIPHER_SUITES = {
183
185
  hash: 'sha256'
184
186
  },
185
187
  0xC014: { // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
188
+ name: 'ECDHE-RSA-AES256-SHA', standardName: 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
186
189
  tls: 12,
187
190
  kex: 'ECDHE_RSA',
188
191
  sig: 'RSA',
@@ -195,6 +198,7 @@ const TLS_CIPHER_SUITES = {
195
198
  hash: 'sha256'
196
199
  },
197
200
  0x003C: { // TLS_RSA_WITH_AES_128_CBC_SHA256
201
+ name: 'AES128-SHA256', standardName: 'TLS_RSA_WITH_AES_128_CBC_SHA256',
198
202
  tls: 12,
199
203
  kex: 'RSA',
200
204
  sig: 'RSA',
@@ -207,6 +211,7 @@ const TLS_CIPHER_SUITES = {
207
211
  hash: 'sha256'
208
212
  },
209
213
  0x003D: { // TLS_RSA_WITH_AES_256_CBC_SHA256
214
+ name: 'AES256-SHA256', standardName: 'TLS_RSA_WITH_AES_256_CBC_SHA256',
210
215
  tls: 12,
211
216
  kex: 'RSA',
212
217
  sig: 'RSA',
@@ -236,10 +241,12 @@ function makeHashFn(algorithm, outputLen) {
236
241
 
237
242
  let sha256 = makeHashFn('sha256', 32);
238
243
  let sha384 = makeHashFn('sha384', 48);
244
+ let sha512 = makeHashFn('sha512', 64); // HPKE: HKDF-SHA512, DHKEM(P-521 / X448)
239
245
 
240
246
  function getHashFn(hashName) {
241
247
  if (hashName === 'sha256') return sha256;
242
248
  if (hashName === 'sha384') return sha384;
249
+ if (hashName === 'sha512') return sha512;
243
250
  throw new Error('Unsupported hash: ' + hashName);
244
251
  }
245
252
 
@@ -534,6 +541,17 @@ function derive_handshake_traffic_secrets_psk(hashName, psk, shared_secret, tran
534
541
  };
535
542
  }
536
543
 
544
+ /**
545
+ * client_early_traffic_secret (RFC 8446 §7.1): Derive-Secret(Early Secret,
546
+ * "c e traffic", ClientHello) — the 0-RTT key's secret, from the PSK and the
547
+ * hash of the ClientHello alone.
548
+ */
549
+ function derive_client_early_traffic_secret(hashName, psk, clienthello_hash, labelPrefix) {
550
+ let hashLen = getHashFn(hashName).outputLen | 0;
551
+ let early_secret = hkdf_extract(hashName, new Uint8Array(0), psk);
552
+ return hkdf_expand_label(hashName, early_secret, 'c e traffic', clienthello_hash, hashLen, labelPrefix);
553
+ }
554
+
537
555
  /**
538
556
  * Like derive_handshake_traffic_secrets_psk but accepts a pre-computed transcript hash.
539
557
  */
@@ -899,6 +917,7 @@ export {
899
917
  compute_psk_binder,
900
918
  derive_handshake_traffic_secrets_psk,
901
919
  derive_handshake_traffic_secrets_psk_with_hash,
920
+ derive_client_early_traffic_secret,
902
921
  build_cert_verify_tbs,
903
922
  build_cert_verify_tbs_with_hash,
904
923
  get_handshake_finished,