lemon-tls 0.6.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.
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);
@@ -402,13 +618,86 @@ function Server(options, connectionListener) {
402
618
  self._ticketKeys = Buffer.from(keys);
403
619
  };
404
620
 
621
+ // ECH keys. Rotation without a restart: publish the new config in DNS,
622
+ // then setEchKeys with the new key first (retry: true) and the old one
623
+ // kept (retry: false) until the old DNS record's TTL has run out — clients
624
+ // still holding it can then connect. New connections use the new set.
625
+ self.getEchKeys = function() {
626
+ return self._ech;
627
+ };
628
+
629
+ self.setEchKeys = function(ech) {
630
+ prepare_ech_server(ech); // throws on a bad key, keeping the old set
631
+ self._ech = ech || null;
632
+ };
633
+
405
634
  // setSecureContext — replace cert/key without restart (Node.js compat)
406
635
  self.setSecureContext = function(opts) {
407
- if (opts && opts.key && opts.cert) {
408
- defaultCtx = createSecureContext({ key: opts.key, cert: opts.cert });
636
+ if (!opts) return;
637
+ if (opts.secureContext) { defaultCtx = opts.secureContext; return; }
638
+ if (opts.key && opts.cert) defaultCtx = createSecureContext(opts);
639
+ };
640
+
641
+ /**
642
+ * Node-compat: addContext(hostname, context)
643
+ *
644
+ * Register a certificate for an SNI host name or wildcard. `context` is
645
+ * either a compiled context from createSecureContext() or the plain
646
+ * { key, cert, ... } options to compile from — Node accepts both.
647
+ *
648
+ * The declarative alternative to writing an SNICallback by hand. It matters
649
+ * for more than tidiness: the common hand-rolled callback compiles a context
650
+ * per connection, and often reads the cert off disk inside it. Here every
651
+ * context is compiled once at registration and served by reference.
652
+ */
653
+ self.addContext = function(hostname, context) {
654
+ if (!hostname) throw new TypeError('addContext requires a hostname');
655
+ let ctx = context;
656
+ if (!ctx || (!ctx.certificateChain && !ctx.privateKey)) {
657
+ if (!context || !context.key || !context.cert) {
658
+ throw new TypeError('addContext requires a SecureContext or { key, cert }');
659
+ }
660
+ ctx = createSecureContext(context);
409
661
  }
662
+ let pattern = String(hostname).toLowerCase();
663
+ let entry = { pattern: pattern, re: sniPatternToRegExp(pattern), ctx: ctx };
664
+
665
+ // Replace in place rather than appending a duplicate, so re-registering a
666
+ // host swaps its certificate without leaving the old one shadowing it.
667
+ let i = sniContexts.findIndex(function(e){ return e.pattern === pattern; });
668
+ if (i >= 0) sniContexts[i] = entry;
669
+ else sniContexts.push(entry);
670
+ return self;
410
671
  };
411
672
 
673
+ /**
674
+ * Wildcards match ONE label and only as the leftmost one, per RFC 6125:
675
+ * `*.example.com` covers `a.example.com` but neither `a.b.example.com` nor
676
+ * the bare `example.com`. A bare `*` matches any single-or-multi label name
677
+ * and is the documented catch-all.
678
+ */
679
+ function sniPatternToRegExp(pattern) {
680
+ if (pattern === '*') return /^.+$/;
681
+ let esc = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
682
+ return new RegExp('^' + esc.replace(/\*/g, '[^.]+') + '$');
683
+ }
684
+
685
+ /**
686
+ * Resolve a servername to a context. Exact matches beat wildcards; among
687
+ * equals the most recently added wins, which is what Node documents.
688
+ */
689
+ function contextForServername(servername) {
690
+ if (!servername) return defaultCtx;
691
+ let name = String(servername).toLowerCase();
692
+ for (let i = sniContexts.length - 1; i >= 0; i--) {
693
+ if (sniContexts[i].pattern === name) return sniContexts[i].ctx;
694
+ }
695
+ for (let i = sniContexts.length - 1; i >= 0; i--) {
696
+ if (sniContexts[i].re.test(name)) return sniContexts[i].ctx;
697
+ }
698
+ return defaultCtx;
699
+ }
700
+
412
701
  return self;
413
702
  }
414
703
 
@@ -453,20 +742,14 @@ function addCompatMethods(socket) {
453
742
  if (typeof socket[name] !== 'function') socket[name] = fn;
454
743
  }
455
744
 
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
- });
745
+ // isSessionReused, getFinished, getPeerFinished, getEphemeralKeyInfo and
746
+ // address now ship natively on TLSSocket, so their shims here were dead code
747
+ // that def() would never install — and they had drifted from Node in ways
748
+ // the native versions fix: getFinished returned null where Node documents
749
+ // undefined, and getEphemeralKeyInfo reported type 'X25519', which is not
750
+ // one of the three types Node defines ('DH', 'ECDH', 'TLSGroup'), and never
751
+ // returned null on a server socket. Two divergent implementations of one
752
+ // method is the duplication this layer exists to avoid, so they are gone.
470
753
 
471
754
  /** Node.js compat: exportKeyingMaterial(length, label, context).
472
755
  * TLSSocket now ships this natively (RFC 5705 / RFC 8446 §7.5 via the
@@ -476,15 +759,6 @@ function addCompatMethods(socket) {
476
759
  return session.exportKeyingMaterial ? session.exportKeyingMaterial(length, label, context) : null;
477
760
  });
478
761
 
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
762
  /** Node.js compat: setServername(name) */
489
763
  def('setServername', function(name) {
490
764
  session.set_context({ local_sni: name });
@@ -493,11 +767,6 @@ function addCompatMethods(socket) {
493
767
  /** Node.js compat: disableRenegotiation() — no-op (renegotiation not supported) */
494
768
  def('disableRenegotiation', function() {});
495
769
 
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
770
  }
502
771
 
503
772
  // ===================== Exports =====================
@@ -510,6 +779,8 @@ export {
510
779
  getCiphers,
511
780
  checkServerIdentity,
512
781
  addCompatMethods,
782
+ getCACertificates,
783
+ setDefaultCACertificates,
513
784
  DEFAULT_MIN_VERSION,
514
785
  DEFAULT_MAX_VERSION,
515
786
  DEFAULT_CIPHERS,
package/src/crypto.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  import {
3
2
  concatUint8Arrays
4
3
  } from './utils.js';
@@ -130,6 +129,7 @@ const TLS_CIPHER_SUITES = {
130
129
  hash: 'sha256'
131
130
  },
132
131
  0xCCAA: { // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
132
+ name: 'DHE-RSA-CHACHA20-POLY1305', standardName: 'TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
133
133
  tls: 12,
134
134
  kex: 'DHE_RSA',
135
135
  sig: 'RSA',
@@ -172,6 +172,7 @@ const TLS_CIPHER_SUITES = {
172
172
  // TLS 1.2 CBC (Legacy)
173
173
  // ----------------------
174
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',
175
176
  tls: 12,
176
177
  kex: 'ECDHE_RSA',
177
178
  sig: 'RSA',
@@ -184,6 +185,7 @@ const TLS_CIPHER_SUITES = {
184
185
  hash: 'sha256'
185
186
  },
186
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',
187
189
  tls: 12,
188
190
  kex: 'ECDHE_RSA',
189
191
  sig: 'RSA',
@@ -196,6 +198,7 @@ const TLS_CIPHER_SUITES = {
196
198
  hash: 'sha256'
197
199
  },
198
200
  0x003C: { // TLS_RSA_WITH_AES_128_CBC_SHA256
201
+ name: 'AES128-SHA256', standardName: 'TLS_RSA_WITH_AES_128_CBC_SHA256',
199
202
  tls: 12,
200
203
  kex: 'RSA',
201
204
  sig: 'RSA',
@@ -208,6 +211,7 @@ const TLS_CIPHER_SUITES = {
208
211
  hash: 'sha256'
209
212
  },
210
213
  0x003D: { // TLS_RSA_WITH_AES_256_CBC_SHA256
214
+ name: 'AES256-SHA256', standardName: 'TLS_RSA_WITH_AES_256_CBC_SHA256',
211
215
  tls: 12,
212
216
  kex: 'RSA',
213
217
  sig: 'RSA',
@@ -237,10 +241,12 @@ function makeHashFn(algorithm, outputLen) {
237
241
 
238
242
  let sha256 = makeHashFn('sha256', 32);
239
243
  let sha384 = makeHashFn('sha384', 48);
244
+ let sha512 = makeHashFn('sha512', 64); // HPKE: HKDF-SHA512, DHKEM(P-521 / X448)
240
245
 
241
246
  function getHashFn(hashName) {
242
247
  if (hashName === 'sha256') return sha256;
243
248
  if (hashName === 'sha384') return sha384;
249
+ if (hashName === 'sha512') return sha512;
244
250
  throw new Error('Unsupported hash: ' + hashName);
245
251
  }
246
252
 
@@ -535,6 +541,17 @@ function derive_handshake_traffic_secrets_psk(hashName, psk, shared_secret, tran
535
541
  };
536
542
  }
537
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
+
538
555
  /**
539
556
  * Like derive_handshake_traffic_secrets_psk but accepts a pre-computed transcript hash.
540
557
  */
@@ -735,7 +752,19 @@ function tls13_exporter(hashName, exporter_master_secret, label, context_value,
735
752
  let h_empty = hashFn(new Uint8Array(0));
736
753
  let derived = hkdf_expand_label(hashName, exporter_master_secret, label, h_empty, hashLen, labelPrefix);
737
754
  let h_ctx = hashFn(context_value || new Uint8Array(0));
738
- return hkdf_expand(hashName, derived, build_hkdf_label('exporter', h_ctx, length), length);
755
+ // BOTH expansions carry the prefix. RFC 8446 §7.5 defines the exporter
756
+ // as two HKDF-Expand-Label steps, and in DTLS 1.3 both use "dtls13"
757
+ // (RFC 9147 §5.9) — this second one was omitting it and falling back
758
+ // to "tls13 ", so the exported key differed from a conforming peer's.
759
+ // For WebRTC that key IS the DTLS-SRTP keying material: the handshake
760
+ // completes, SRTP reports ready, packets flow in both directions, and
761
+ // NOTHING decrypts. Two peers running this library derive the same
762
+ // wrong key and interoperate perfectly with each other, which is why
763
+ // it only appears against Chrome — and appears as "media does not
764
+ // flow" rather than as a crypto error, since a failed unprotect is
765
+ // just a dropped packet.
766
+ return hkdf_expand(hashName, derived,
767
+ build_hkdf_label('exporter', h_ctx, length, labelPrefix), length);
739
768
  }
740
769
 
741
770
  /**
@@ -888,6 +917,7 @@ export {
888
917
  compute_psk_binder,
889
918
  derive_handshake_traffic_secrets_psk,
890
919
  derive_handshake_traffic_secrets_psk_with_hash,
920
+ derive_client_early_traffic_secret,
891
921
  build_cert_verify_tbs,
892
922
  build_cert_verify_tbs_with_hash,
893
923
  get_handshake_finished,
@@ -896,4 +926,4 @@ export {
896
926
  derive_exporter_master_secret_with_hash,
897
927
  tls13_exporter,
898
928
  tls12_exporter,
899
- };
929
+ };