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/tls_socket.js CHANGED
@@ -4,10 +4,25 @@ import crypto from 'node:crypto';
4
4
  import { Duplex } from 'node:stream';
5
5
 
6
6
  import { TLS_CIPHER_SUITES, default_cipher_suites } from './crypto.js';
7
- import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT } from './wire.js';
8
- import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS } from './session/ecdh.js';
7
+ import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT, HANDSHAKE_MESSAGE_MAX } from './wire.js';
8
+ import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS, SERVER_DEFAULT_GROUPS, group_name as ecdh_group_name } from './session/ecdh.js';
9
9
  import { default_signature_schemes } from './session/signing.js';
10
- import { decrypt_session_blob } from './session/ticket.js';
10
+ import { certificateObject } from './utils.js';
11
+
12
+ // The suite list a socket offers or accepts, from the defaults: ChaCha20
13
+ // suites moved to the front when asked (prioritizeChaCha), then narrowed to an
14
+ // allow-list. One function for the client and server paths, which each kept a
15
+ // copy — and both copies knew ChaCha20 by two codes (0x1303, 0xCCA8), so
16
+ // ECDHE_ECDSA_WITH_CHACHA20_POLY1305 (0xCCA9) was never moved forward. The
17
+ // suite table says which suites are ChaCha20.
18
+ function chooseCipherSuites(ciphers, prioritizeChaCha, allowed) {
19
+ if (prioritizeChaCha) {
20
+ let isChaCha = function (c) { return !!TLS_CIPHER_SUITES[c] && TLS_CIPHER_SUITES[c].cipher === 'CHACHA20_POLY1305'; };
21
+ ciphers = ciphers.filter(isChaCha).concat(ciphers.filter(function (c) { return !isChaCha(c); }));
22
+ }
23
+ if (allowed) ciphers = ciphers.filter(function (c) { return allowed.includes(c); });
24
+ return ciphers;
25
+ }
11
26
 
12
27
  import {
13
28
  getAeadAlgo,
@@ -19,13 +34,21 @@ import {
19
34
  decryptRecord as decrypt_tls_record,
20
35
  decryptRecordWithAadView as decrypt_tls_record_with_aad, // zero-alloc AAD path
21
36
  parseInnerPlaintext as parse_tls_inner_plaintext,
22
- encrypt12 as encrypt_tls12_gcm_fragment,
37
+ encrypt12 as encrypt_tls12_fragment,
23
38
  encryptCompleteRecord12, // fused encrypt + header for TLS 1.2 app-data hot path
24
- decrypt12 as decrypt_tls12_gcm_fragment,
39
+ decrypt12 as decrypt_tls12_fragment,
25
40
  deriveKeys12,
26
41
  writeRecord as writeRawRecord,
27
42
  } from './record.js';
28
43
 
44
+ // What a ticket of ours lets a client send early over TCP, by default: room
45
+ // for a request (an HTTP/2 preface, SETTINGS and HEADERS; an HTTP/1.1 GET),
46
+ // not for an upload — what does not fit goes after the handshake.
47
+ const EARLY_DATA_MAX_TCP = 16384;
48
+
49
+ // Handshake messages: refused on sight past HANDSHAKE_MESSAGE_MAX (wire.js) —
50
+ // before their bytes are kept.
51
+
29
52
  // legacy_record_version (TLS 1.3 uses 0x0303 in record header)
30
53
  const REC_VERSION = 0x0303;
31
54
 
@@ -53,7 +76,6 @@ function sigalgCodeToName(code) {
53
76
 
54
77
  // ==== עזרי המרה ====
55
78
  function toBuf(u8){ return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8 || []); }
56
- function toU8(buf){ return (buf instanceof Uint8Array) ? buf : new Uint8Array(buf || []); }
57
79
 
58
80
  function parseVersion(v) {
59
81
  if (typeof v === 'number') return v;
@@ -76,11 +98,19 @@ function parseVersion(v) {
76
98
 
77
99
 
78
100
  // ==== TLSSocket ====
101
+
79
102
  function TLSSocket(duplex, options){
80
103
  if (!(this instanceof TLSSocket)) return new TLSSocket(duplex, options);
81
104
  options = options || {};
82
105
 
83
- // Inherit from Duplex stream
106
+ // allowHalfOpen follows Node: false unless the caller asks otherwise, so
107
+ // that when the peer stops writing our writable side ends too. This was
108
+ // hardcoded true, which is the OPPOSITE of the documented default — code
109
+ // written against Node's semantics waits for a close that never arrives and
110
+ // the connection just sits there, looking like a leak with no clue as to
111
+ // why. Node also notes the option has no effect when `socket` is supplied,
112
+ // since the underlying stream's own setting governs.
113
+ //
84
114
  // highWaterMark: raised to 256KB from the Node default of 16KB. TLS records max
85
115
  // at 16KB of plaintext, so the default meant "pause after one record" — terrible
86
116
  // for bulk transfers where we want many records in flight. 256KB keeps ~16 records
@@ -88,15 +118,41 @@ function TLSSocket(duplex, options){
88
118
  // small bursts interrupted by drain events. Most impactful on the Download path
89
119
  // where the sender is aggressive (tight-loop sock.write).
90
120
  Duplex.call(this, {
91
- allowHalfOpen: true,
121
+ allowHalfOpen: options.allowHalfOpen === true,
122
+ // 'close' is emitted by this socket itself, when its transport closes —
123
+ // as it always has. Left on, autoDestroy would add a second 'close' of its
124
+ // own once the readable side can end (it now does, see endReadable).
125
+ autoDestroy: false,
92
126
  readableObjectMode: false,
93
127
  writableObjectMode: false,
94
- highWaterMark: 256 * 1024,
128
+ highWaterMark: options.highWaterMark || 256 * 1024,
95
129
  });
96
130
  const self = this;
97
131
 
98
132
  let _ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
99
133
 
134
+ /**
135
+ * Read a credential out of a caller-supplied `secureContext` (Node parity).
136
+ *
137
+ * tls.connect() accepts a context built once by createSecureContext() and
138
+ * reused across connections, so the same PEM is not re-parsed per socket.
139
+ * Ours returns { certificateChain, privateKey, ca }, so map those onto the
140
+ * cert/key/ca the session expects. Direct options always win, matching
141
+ * Node's precedence.
142
+ */
143
+ let _sc = function(what){
144
+ let sc = options.secureContext;
145
+ if (!sc) return null;
146
+ if (what === 'ca') return sc.ca && sc.ca.length ? sc.ca : null;
147
+ if (what === 'key') return sc.privateKey || null;
148
+ if (what === 'cert') {
149
+ if (!sc.certificateChain || sc.certificateChain.length === 0) return null;
150
+ // The session takes a single cert/PEM here; hand it the leaf.
151
+ return sc.certificateChain[0].cert || sc.certificateChain[0];
152
+ }
153
+ return null;
154
+ };
155
+
100
156
  let context = {
101
157
 
102
158
  options: options,
@@ -111,17 +167,57 @@ function TLSSocket(duplex, options){
111
167
  ALPNProtocols: options.ALPNProtocols || null,
112
168
  SNICallback: options.SNICallback || null,
113
169
  ticketKeys: _ticketKeys,
170
+ // A session ID only where something stores the session ('newSession').
171
+ sessionIdStore: function() { return self.listenerCount('newSession') > 0; },
114
172
  ticketLifetime: options.ticketLifetime,
173
+ ticketData: options.ticketData || null,
174
+ certificateCompression: options.certificateCompression,
175
+ honorCipherOrder: options.honorCipherOrder === true,
176
+ // Encrypted Client Hello: server — the keys, { keys: [{ config,
177
+ // privateKey, retry }] }; client — the ECHConfigList to encrypt the
178
+ // real ClientHello to, and echGrease (GREASE ECH without one).
179
+ ech: options.ech,
180
+ echGrease: options.echGrease === true,
181
+ // OCSP stapling (RFC 6066): server — (certificate, issuer, cb), what
182
+ // tls.Server's 'OCSPRequest' event is wired to; client — ask for it.
183
+ ocspRequest: typeof options.ocspRequest === 'function' ? options.ocspRequest : null,
184
+ requestOCSP: options.requestOCSP === true,
115
185
  session: options.session || null,
116
186
  psk: options.psk || null,
117
187
  rejectUnauthorized: options.rejectUnauthorized,
118
- ca: options.ca || null,
188
+ checkServerIdentity: options.checkServerIdentity,
189
+ ALPNCallback: options.ALPNCallback || null,
190
+ ca: options.ca || _sc('ca') || null,
191
+ // The platform trust store, for when no `ca` is given. compat.js
192
+ // hands both to this socket (defaultCA: Node's root certificates);
193
+ // they were never passed on, so the session had no trust anchors at
194
+ // all, and with none it checks only the name in the certificate. A
195
+ // self-signed certificate for the right name — anyone can make one —
196
+ // was accepted as verified: every connection this library opened
197
+ // (quico.request, WebSocket over wss://) was open to a
198
+ // man-in-the-middle, with rejectUnauthorized true.
199
+ useDefaultCA: options.useDefaultCA,
200
+ defaultCA: options.defaultCA,
119
201
  sessionTickets: options.sessionTickets,
120
202
  maxHandshakeSize: options.maxHandshakeSize || 0,
121
203
  customExtensions: options.customExtensions || [],
204
+ permuteExtensions: !!options.permuteExtensions,
205
+ grease: !!options.grease,
122
206
  requestCert: !!options.requestCert,
123
- cert: options.cert || null,
124
- key: options.key || null,
207
+ cert: options.cert || _sc('cert') || null,
208
+ key: options.key || _sc('key') || null,
209
+ passphrase: options.passphrase,
210
+ // 0-RTT over TCP (RFC 8446 §4.2.10, §4.5). Server: tickets that allow
211
+ // it (maxEarlyData bytes, 16 KB by default — a request, not an
212
+ // upload) and taking it, under the session's checks. Client: with a
213
+ // session whose ticket allows it, what is written before the
214
+ // handshake ends goes in the first flight (see "Early data" below).
215
+ earlyData: options.earlyData === true && (!!options.isServer || !!options.session),
216
+ earlyDataMaxAge: options.earlyDataMaxAge,
217
+ earlyDataReplay: options.earlyDataReplay,
218
+ earlyDataContext: options.earlyDataContext,
219
+ maxEarlyData: typeof options.maxEarlyData === 'number' ? options.maxEarlyData : EARLY_DATA_MAX_TCP,
220
+ endOfEarlyData: true,
125
221
  }),
126
222
 
127
223
  // Handshake write
@@ -213,6 +309,15 @@ function TLSSocket(duplex, options){
213
309
  hsBuf: null,
214
310
 
215
311
  appWriteQueue: [],
312
+
313
+ // 0-RTT (see "Early data"). Client: the early write key, what it may
314
+ // still send early (max − sent), and copies of what went, to send again
315
+ // if the server does not take it. Server: the early read key, what came
316
+ // early (recv), what was skipped of early data not taken, and whether
317
+ // it may answer before the client's Finished (srvWrite: 0.5-RTT).
318
+ early: { status: null, write: null, read: null, max: 0, sent: 0, copies: [], helloSent: false,
319
+ recv: 0, skipped: 0, trialDone: false, eoed: false, srvWrite: false },
320
+ parsing: false,
216
321
  pendingHandshake: [],
217
322
 
218
323
  // General state
@@ -238,6 +343,22 @@ function TLSSocket(duplex, options){
238
343
 
239
344
  let session = context.session;
240
345
 
346
+ // A server's own key/cert given up front. They went into the session as
347
+ // cert/key — the CLIENT credentials it sends when a server asks for one —
348
+ // and never became the certificate this server presents. createServer()
349
+ // hid that, since every certificate there comes through SNICallback; a
350
+ // server socket built directly (`new TLSSocket(socket, { isServer, key,
351
+ // cert })`, which is what every STARTTLS upgrade does) presented nothing
352
+ // and failed the handshake. Direct options win over secureContext, as in
353
+ // Node. SNICallback, when it answers, still replaces this per name.
354
+ if (options.isServer) {
355
+ if (options.key && options.cert) {
356
+ session.setKeyCert({ key: options.key, cert: options.cert, passphrase: options.passphrase });
357
+ } else if (options.secureContext) {
358
+ session.setKeyCert(options.secureContext);
359
+ }
360
+ }
361
+
241
362
 
242
363
  // === Record Layer ===
243
364
  // writeRecord returns the transport's backpressure signal (true = ready for more,
@@ -245,10 +366,32 @@ function TLSSocket(duplex, options){
245
366
  function writeRecord(type, payload){
246
367
  if (!context.transport) throw new Error('No transport attached to TLSSocket');
247
368
  if (context.destroyed || context.transport.destroyed || context.transport.writableEnded) return false;
369
+ coalesceFlight();
248
370
  try { return writeRawRecord(context.transport, type, payload, context.rec_version); }
249
371
  catch(e){ self.emit('error', e); return false; }
250
372
  }
251
373
 
374
+ // One flight, one send. A handshake flight is several records written one
375
+ // after another in the same tick — ServerHello, ChangeCipherSpec,
376
+ // EncryptedExtensions, Certificate, CertificateVerify, Finished. Written
377
+ // one by one, each is a send of its own; on a socket where Nagle is on
378
+ // (net.Socket's default, and tls.createServer's), everything after the
379
+ // first then waits for the peer's ACK: a full round trip, and TLS 1.3's
380
+ // one-round-trip handshake takes two. node:tls writes a flight at once.
381
+ // Corked until the end of the tick, the records leave as one writev.
382
+ let corkedTransport = null;
383
+ function coalesceFlight(){
384
+ const t = context.transport;
385
+ if (!t || corkedTransport === t || t.destroyed) return;
386
+ if (typeof t.cork !== 'function' || typeof t.uncork !== 'function') return;
387
+ corkedTransport = t;
388
+ t.cork();
389
+ process.nextTick(function(){
390
+ if (corkedTransport === t) corkedTransport = null;
391
+ try { t.uncork(); } catch(e) {}
392
+ });
393
+ }
394
+
252
395
  const MAX_RECORD_PLAINTEXT = 16384; // TLS max record size (2^14)
253
396
 
254
397
  // writeAppData / writeAppDataSingle return the transport's backpressure signal:
@@ -348,7 +491,10 @@ function TLSSocket(duplex, options){
348
491
  }
349
492
  }
350
493
 
351
- function processCiphertext(body, header){
494
+ // trial: early data not taken may come before the client's handshake
495
+ // flight (RFC 8446 §4.2.10): a record that the handshake key does not
496
+ // open is that, and is skipped (false) rather than fatal.
497
+ function processCiphertext(body, header, trial){
352
498
  // Hot path — same optimizations as writeAppDataSingle:
353
499
  // - cached context.isTls13 instead of per-record session.getVersion()
354
500
  // - cached context.aeadAlgo instead of per-record getAeadAlgo()
@@ -391,7 +537,7 @@ function TLSSocket(duplex, options){
391
537
  }
392
538
 
393
539
  const recordType = context.using_app_keys ? 0x17 : 0x16;
394
- out = decrypt_tls12_gcm_fragment(body, context.app_read_key, context.app_read_iv, context.tls12_read_seq, recordType);
540
+ out = decrypt_tls12_fragment(body, context.app_read_key, context.app_read_iv, context.tls12_read_seq, recordType);
395
541
 
396
542
  } else if (isTls13 && context.using_app_keys === true) {
397
543
  // TLS 1.3: decrypt app data with app traffic secret
@@ -436,6 +582,7 @@ function TLSSocket(duplex, options){
436
582
  context.handshake_read_seq++;
437
583
  }
438
584
  } catch (e) {
585
+ if (trial) return false;
439
586
  // Record failed authentication → fatal bad_record_mac (RFC 8446 §5.2).
440
587
  try { session.sendAlert ? session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC) : null; } catch (_) {}
441
588
  self.emit('error', new Error('TLS record decryption failed (bad_record_mac)'));
@@ -462,23 +609,172 @@ function TLSSocket(duplex, options){
462
609
 
463
610
  if (content_type === CT.APPLICATION_DATA) {
464
611
  // zero-copy view into the decrypted plaintext
465
- self.push(out.subarray(0, j));
612
+ pushAppData(out.subarray(0, j));
466
613
  } else if (content_type === CT.HANDSHAKE) {
467
614
  feedHandshakeBytes(out.subarray(0, j));
468
615
  } else if (content_type === CT.ALERT) {
469
- // TODO: handle alert
616
+ // TLS 1.3 alerts after ServerHello are encrypted — a client
617
+ // refusing our certificate always sends its alert this way.
618
+ // They were decrypted and dropped here.
619
+ handleAlert(out.subarray(0, j));
470
620
  }
471
621
  } else {
472
622
  // TLS 1.2: no inner plaintext wrapping — `out` is the Buffer from decrypt12
473
623
  if (context.using_app_keys === true) {
474
- self.push(out);
624
+ pushAppData(out);
475
625
  } else {
476
626
  feedHandshakeBytes(out);
477
627
  }
478
628
  }
479
629
  }
630
+ if (trial && out !== null) context.early.trialDone = true;
631
+ return out !== null;
632
+ }
633
+
634
+
635
+ // ============================================================================
636
+ // Early data (0-RTT over TCP, RFC 8446 §4.2.10, §4.5)
637
+ // ============================================================================
638
+ //
639
+ // The session decides, and hands out the key ('earlySecret'); the records
640
+ // are this layer's. A client sends, after its ClientHello and the
641
+ // compatibility ChangeCipherSpec (§D.4: right after the first hello when
642
+ // offering early data), what was written before the handshake ends — up
643
+ // to the ticket's max_early_data_size — under the early key; then, the
644
+ // server having taken it, EndOfEarlyData under the same key, first in its
645
+ // second flight. Not taken, the same bytes go again after the handshake,
646
+ // as if written then: the server never processed them.
647
+ //
648
+ // A server reads the records after the ClientHello as follows: before
649
+ // the decision (an asynchronous replay check), they wait in the buffer;
650
+ // taken, they are opened with the early key and given to the reader as
651
+ // they come, before 'secure' (isEarlyData true), until EndOfEarlyData;
652
+ // not taken, each record that the handshake key does not open is early
653
+ // data and is skipped; after a HelloRetryRequest, every application_data
654
+ // record until the second ClientHello is. Skipped or taken, no more than
655
+ // maxEarlyData bytes (unexpected_message past it). Taken, the server may
656
+ // answer as soon as its Finished is out (0.5-RTT), not only once the
657
+ // client's has come.
658
+
659
+ function serverEarlyMode() {
660
+ const sc = session.context;
661
+ if (!sc || !sc.early || !sc.early.clientOffered || context.early.eoed || context.early.trialDone) return 'none';
662
+ if (sc.helloRetried) return sc.client_hellos_received < 2 ? 'skip' : 'none';
663
+ if (sc.early.status === 'accepted') return 'accepted';
664
+ if (sc.early.offered && sc.early.status === null) return 'pending';
665
+ return 'trial';
666
+ }
667
+
668
+ function earlyLimit() {
669
+ return typeof options.maxEarlyData === 'number' ? options.maxEarlyData : EARLY_DATA_MAX_TCP;
670
+ }
671
+
672
+ function earlyTooMuch() {
673
+ try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
674
+ self.emit('error', new Error('More early data than max_early_data_size'));
675
+ markTransportDead();
676
+ closeAfterAlert();
677
+ }
678
+
679
+ // A record of early data not taken: skipped, counted (its plaintext at
680
+ // most: the record less the tag and the content type).
681
+ function skipEarly(len) {
682
+ context.early.skipped += Math.max(0, len - 17);
683
+ if (context.early.skipped > earlyLimit()) { earlyTooMuch(); return false; }
684
+ return true;
685
+ }
686
+
687
+ function processEarlyRecord(body, header) {
688
+ const r = context.early.read;
689
+ let out;
690
+ try {
691
+ out = decrypt_tls_record_with_aad(header, body, r.key, get_nonce_into(context._nonceDecScratch, r.iv, r.seq), r.algo);
692
+ } catch (e) {
693
+ try { session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC); } catch (_) {}
694
+ self.emit('error', new Error('TLS early data record decryption failed (bad_record_mac)'));
695
+ self.destroy();
696
+ return;
697
+ }
698
+ r.seq++;
699
+ let j = out.length - 1;
700
+ while (j >= 0 && out[j] === 0) j--;
701
+ if (j < 0) { self.emit('error', new Error('Malformed TLSInnerPlaintext')); return; }
702
+ const ct = out[j];
703
+ if (ct === CT.APPLICATION_DATA) {
704
+ context.early.recv += j;
705
+ if (context.early.recv > earlyLimit()) { earlyTooMuch(); return; }
706
+ if (j > 0) pushAppData(out.subarray(0, j));
707
+ } else if (ct === CT.HANDSHAKE) {
708
+ feedHandshakeBytes(out.subarray(0, j)); // EndOfEarlyData, and nothing else (the session checks)
709
+ } else if (ct === CT.ALERT) {
710
+ handleAlert(out.subarray(0, j));
711
+ }
712
+ }
713
+
714
+ // Client: what is queued goes early, as much as the ticket allows.
715
+ function flushEarly() {
716
+ const e = context.early;
717
+ if (!e.write || e.status !== 'offered' || !e.helloSent) return;
718
+ coalesceFlight();
719
+ while (context.appWriteQueue.length > 0 && e.sent < e.max) {
720
+ let buf = context.appWriteQueue.shift();
721
+ const room = e.max - e.sent;
722
+ if (buf.length > room) { context.appWriteQueue.unshift(buf.subarray(room)); buf = buf.subarray(0, room); }
723
+ for (let off = 0; off < buf.length; off += MAX_RECORD_PLAINTEXT) writeEarlyRecord(CT.APPLICATION_DATA, buf.subarray(off, Math.min(buf.length, off + MAX_RECORD_PLAINTEXT)));
724
+ e.copies.push(buf);
725
+ e.sent += buf.length;
726
+ }
727
+ }
728
+
729
+ function writeEarlyRecord(innerType, plain) {
730
+ const w = context.early.write;
731
+ if (!w || !canWrite()) return;
732
+ try {
733
+ const rec = encryptCompleteRecord13(innerType, plain, w.key, get_nonce_into(context._nonceEncScratch, w.iv, w.seq), w.algo, context.rec_version);
734
+ w.seq++;
735
+ context.transport.write(rec);
736
+ } catch (err) { self.emit('error', err); }
480
737
  }
481
738
 
739
+ session.on('earlySecret', function(secret, info){
740
+ if (session.isServer) {
741
+ const cs = session.getCipher();
742
+ const d = tls_derive_from_tls_secrets(secret, cs);
743
+ context.early.read = { key: d.key, iv: d.iv, seq: 0, algo: getAeadAlgo(cs) };
744
+ } else {
745
+ const cs = info && info.cipher;
746
+ if (!cs || !TLS_CIPHER_SUITES[cs]) return;
747
+ const d = tls_derive_from_tls_secrets(secret, cs);
748
+ context.early.write = { key: d.key, iv: d.iv, seq: 0, algo: getAeadAlgo(cs) };
749
+ context.early.max = Math.min((info.maxEarlyData >>> 0) || 0, 0x7fffffff);
750
+ context.early.status = 'offered';
751
+ }
752
+ });
753
+
754
+ session.on('earlyData', function(accepted){
755
+ const e = context.early;
756
+ e.status = accepted ? 'accepted' : 'rejected';
757
+ if (session.isServer) {
758
+ if (!accepted) e.read = null;
759
+ // Records held for the decision: read now (the decision may come
760
+ // from an asynchronous replay check, outside any read).
761
+ if (!context.parsing) process.nextTick(function(){ if (!context.destroyed) parseRecordsAndDispatch(); });
762
+ } else if (!accepted) {
763
+ // Not taken: written again after the handshake, ahead of what
764
+ // waited behind it.
765
+ e.write = null;
766
+ if (e.copies.length) context.appWriteQueue = e.copies.concat(context.appWriteQueue);
767
+ e.copies = [];
768
+ } else {
769
+ e.copies = [];
770
+ }
771
+ self.emit('earlyData', accepted);
772
+ });
773
+
774
+ session.on('endOfEarlyData', function(){
775
+ context.early.eoed = true;
776
+ context.early.read = null;
777
+ });
482
778
 
483
779
  // ============================================================================
484
780
  // Read buffer management
@@ -559,6 +855,15 @@ function TLSSocket(duplex, options){
559
855
 
560
856
  while (context.hsBuf.length >= 4) {
561
857
  const len = (context.hsBuf[1] << 16) | (context.hsBuf[2] << 8) | context.hsBuf[3];
858
+ const maxMsg = options.maxHandshakeSize > 0 ? Math.min(options.maxHandshakeSize, HANDSHAKE_MESSAGE_MAX) : HANDSHAKE_MESSAGE_MAX;
859
+ if (len > maxMsg) {
860
+ // OpenSSL's answer to an excessive message: illegal_parameter.
861
+ context.hsBuf = null;
862
+ try { session.sendAlert(2, TLS_ALERT.ILLEGAL_PARAMETER); } catch (_) {}
863
+ self.emit('error', new Error('TLS handshake message of ' + len + ' bytes, more than ' + maxMsg));
864
+ self.destroy();
865
+ return;
866
+ }
562
867
  if (context.hsBuf.length < 4 + len) break; // partial message — wait for more records
563
868
 
564
869
  const msg = context.hsBuf.subarray(0, 4 + len);
@@ -573,6 +878,11 @@ function TLSSocket(duplex, options){
573
878
  }
574
879
 
575
880
  function parseRecordsAndDispatch(){
881
+ if (context.parsing) return;
882
+ context.parsing = true;
883
+ try { parseRecords(); } finally { context.parsing = false; }
884
+ }
885
+ function parseRecords(){
576
886
  // Hot path — zero-copy parsing:
577
887
  // - Work directly on readBuffer with moving readStart offset (no slicing)
578
888
  // - Body is a subarray view (no copy) passed down to crypto
@@ -603,9 +913,39 @@ function TLSSocket(duplex, options){
603
913
  }
604
914
 
605
915
  const type = rb[off];
606
- // Version at rb[off+1..off+3] — currently unused (legacy 0x0303 always)
607
916
  const len = (rb[off + 3] << 8) | rb[off + 4];
608
917
 
918
+ // ── Record header, checked as soon as its 5 bytes are here ──
919
+ // Not once the whole record has arrived: a header that is not TLS
920
+ // claims a length from whatever bytes sit there — "GET /" asks for
921
+ // 8 239 — and the socket waited for a body that never comes. Plain
922
+ // HTTP sent to the TLS port, or a scanner, held a connection open
923
+ // in silence; OpenSSL rejects on the header ("wrong version
924
+ // number"), and so does this now, with node:tls's codes.
925
+ // RFC 8446 §5.1: legacy_record_version is 0x03xx, ContentType
926
+ // one of the four; §5.2: no record beyond 2^14 + 256 bytes of
927
+ // ciphertext (2^14 + 2048 in TLS 1.2, RFC 5246 §6.2.3).
928
+ {
929
+ const recVer = (rb[off + 1] << 8) | rb[off + 2];
930
+ let bad = null, alert = 0;
931
+ if ((recVer >>> 8) !== 0x03 || type < CT.CHANGE_CIPHER_SPEC || type > CT.APPLICATION_DATA) {
932
+ bad = new Error('wrong version number: not a TLS record (type 0x' + type.toString(16) + ', version 0x' + recVer.toString(16) + ')');
933
+ bad.code = 'ERR_SSL_WRONG_VERSION_NUMBER';
934
+ alert = (recVer >>> 8) !== 0x03 ? TLS_ALERT.PROTOCOL_VERSION : TLS_ALERT.UNEXPECTED_MESSAGE;
935
+ } else if (len > 16384 + 2048) {
936
+ bad = new Error('record overflow: ' + len + ' bytes');
937
+ bad.code = 'ERR_SSL_RECORD_OVERFLOW';
938
+ alert = TLS_ALERT.RECORD_OVERFLOW;
939
+ }
940
+ if (bad) {
941
+ try { session.sendAlert(2, alert); } catch (e) {}
942
+ self.emit('error', bad);
943
+ markTransportDead();
944
+ closeAfterAlert();
945
+ return;
946
+ }
947
+ }
948
+
609
949
  if (end - off < 5 + len) break; // record not fully received
610
950
 
611
951
  // Zero-copy views: header (5 bytes) + body
@@ -613,20 +953,6 @@ function TLSSocket(duplex, options){
613
953
  const body = rb.subarray(off + 5, off + 5 + len);
614
954
  off += 5 + len;
615
955
 
616
- // ── Record-layer sanity, applied to every record ──
617
- // RFC 8446 §5.1: the legacy_record_version is 0x0303 for every
618
- // record after the first ClientHello, and 0x03xx before it. A
619
- // record claiming anything else is malformed framing, not a
620
- // version negotiation signal.
621
- const recVer = (rb[off - len - 5 + 1] << 8) | rb[off - len - 5 + 2];
622
- if ((recVer >>> 8) !== 0x03) {
623
- try { session.sendAlert(2, TLS_ALERT.PROTOCOL_VERSION); } catch (e) {}
624
- self.emit('error', new Error('Record with illegal legacy_record_version 0x' + recVer.toString(16)));
625
- markTransportDead();
626
- if (context.transport && !context.transport.destroyed) context.transport.destroy();
627
- return;
628
- }
629
-
630
956
  // A peer may legally send an empty record, and a TLS 1.3 peer may
631
957
  // legally send a compat CCS — but only finitely many. Unbounded
632
958
  // streams of either make no progress and exist only to burn our
@@ -636,7 +962,7 @@ function TLSSocket(duplex, options){
636
962
  try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
637
963
  self.emit('error', new Error('Too many consecutive empty records'));
638
964
  markTransportDead();
639
- if (context.transport && !context.transport.destroyed) context.transport.destroy();
965
+ closeAfterAlert();
640
966
  return;
641
967
  }
642
968
  } else {
@@ -648,13 +974,20 @@ function TLSSocket(duplex, options){
648
974
  try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
649
975
  self.emit('error', new Error('Too many consecutive ChangeCipherSpec records'));
650
976
  markTransportDead();
651
- if (context.transport && !context.transport.destroyed) context.transport.destroy();
977
+ closeAfterAlert();
652
978
  return;
653
979
  }
654
980
  } else {
655
981
  context.consecutive_ccs_records = 0;
656
982
  }
657
983
 
984
+ if (type === CT.APPLICATION_DATA && session.isServer && !context.using_app_keys && context.isTls13 !== false) {
985
+ const em = serverEarlyMode();
986
+ if (em === 'pending') { off -= 5 + len; break; } // the decision first; this record waits
987
+ if (em === 'skip') { if (!skipEarly(len)) return; continue; }
988
+ if (em === 'accepted') { processEarlyRecord(body, header); if (context.destroyed) return; continue; }
989
+ if (em === 'trial') { if (!processCiphertext(body, header, true) && !skipEarly(len)) return; continue; }
990
+ }
658
991
  if (type === CT.APPLICATION_DATA) {
659
992
  processCiphertext(body, header);
660
993
  context.tls12_read_seq++;
@@ -704,23 +1037,7 @@ function TLSSocket(duplex, options){
704
1037
  }
705
1038
  // TLS 1.3, or version not yet negotiated: swallow silently.
706
1039
  } else if (type === CT.ALERT) {
707
- // Alert: 2 bytes — level (1=warning, 2=fatal), description
708
- if (body.length >= 2) {
709
- const level = body[0];
710
- const desc = body[1];
711
- self.emit('alert', { level: level, description: desc });
712
- if (desc === TLS_ALERT.CLOSE_NOTIFY) {
713
- session.close();
714
- if (context.transport && typeof context.transport.end === 'function') {
715
- context.transport.end();
716
- }
717
- }
718
- if (level === TLS_ALERT_LEVEL.FATAL) {
719
- if (context.transport && typeof context.transport.destroy === 'function') {
720
- context.transport.destroy();
721
- }
722
- }
723
- }
1040
+ handleAlert(body);
724
1041
  }
725
1042
  }
726
1043
 
@@ -785,7 +1102,77 @@ function TLSSocket(duplex, options){
785
1102
  return true;
786
1103
  }
787
1104
 
1105
+ // An alert from the peer, cleartext or decrypted: 2 bytes, level and
1106
+ // description. close_notify ends our readable side; a fatal alert ends
1107
+ // the connection and is an 'error', as node:tls reports one ("alert bad
1108
+ // certificate") — without it the socket just closed and nothing learned
1109
+ // why: a server could not see that clients were refusing its certificate.
1110
+ function handleAlert(body) {
1111
+ if (!body || body.length < 2) return;
1112
+ const level = body[0];
1113
+ const desc = body[1];
1114
+ self.emit('alert', { level: level, description: desc });
1115
+ if (desc === TLS_ALERT.CLOSE_NOTIFY) {
1116
+ // The peer is done sending: end our readable side, so 'end' is
1117
+ // emitted once what came before it is read.
1118
+ endReadable();
1119
+ // Ours: TLS 1.2 answers at once (RFC 5246 §7.2.1: "respond with
1120
+ // a close_notify alert of its own and close down the connection
1121
+ // immediately"). TLS 1.3 does not — a close_notify closes the
1122
+ // sender's side only, "This does not have any effect on its read
1123
+ // side" (RFC 8446 §6.1, a change it calls out from earlier
1124
+ // versions): with allowHalfOpen we go on writing, and send ours
1125
+ // when we end(). Without it, the Duplex ends us now anyway — its
1126
+ // automatic end() after 'end' — and _final sends it. Answering
1127
+ // at once in TLS 1.3 cut off every response a server was still
1128
+ // writing to a client that had merely finished sending.
1129
+ if (session.getVersion() !== 0x0304 || !self.allowHalfOpen) sendCloseNotify();
1130
+ }
1131
+ if (level === TLS_ALERT_LEVEL.FATAL) {
1132
+ // Both layers inert first (markTransportDead: the session goes
1133
+ // terminal, so the reactive loop produces nothing more), then the
1134
+ // report — the order of session.on('error') below: a listener
1135
+ // reacting to it finds the session already over.
1136
+ markTransportDead();
1137
+ if (desc !== TLS_ALERT.CLOSE_NOTIFY) {
1138
+ let name = null;
1139
+ for (const k in TLS_ALERT) if (TLS_ALERT[k] === desc) { name = k.toLowerCase(); break; }
1140
+ const err = new Error('peer sent fatal alert: ' + (name || 'alert') + ' (' + desc + ')');
1141
+ err.code = 'ERR_TLS_ALERT_RECEIVED';
1142
+ err.alert = desc;
1143
+ self.emit('error', err);
1144
+ }
1145
+ // Nothing of ours to flush: the peer has ended the connection.
1146
+ if (context.transport && typeof context.transport.destroy === 'function') context.transport.destroy();
1147
+ }
1148
+ }
1149
+
1150
+ // Close the transport after we have sent a fatal alert: end() — the
1151
+ // alert, written just before, leaves first, then FIN — not destroy(),
1152
+ // which drops whatever is still buffered. It dropped the alert: neither
1153
+ // side of a refused handshake learned why (a server could not see that
1154
+ // clients were refusing its certificate). A peer that does not close its
1155
+ // side in time is cut off.
1156
+ const ALERT_LINGER_MS = 1000;
1157
+ function closeAfterAlert() {
1158
+ const t = context.transport;
1159
+ if (!t || t.destroyed) return;
1160
+ if (typeof t.end !== 'function') { if (typeof t.destroy === 'function') t.destroy(); return; }
1161
+ try { t.end(); } catch (e) { try { t.destroy(); } catch (e2) {} return; }
1162
+ const timer = setTimeout(function () { if (!t.destroyed) t.destroy(); }, ALERT_LINGER_MS);
1163
+ if (timer.unref) timer.unref();
1164
+ }
1165
+
1166
+ // The handshake's deadline, gone with the handshake: cleared at 'secure'
1167
+ // and here, when the socket goes. It stayed set on a handshake that
1168
+ // failed — 120 s for every refused or abandoned one, holding the socket's
1169
+ // state in memory and keeping the process from exiting.
1170
+ function clearHandshakeTimer() {
1171
+ if (context.handshakeTimer) { clearTimeout(context.handshakeTimer); context.handshakeTimer = null; }
1172
+ }
1173
+
788
1174
  function markTransportDead() {
1175
+ clearHandshakeTimer();
789
1176
  if (context.destroyed) return;
790
1177
  context.destroyed = true;
791
1178
  try { session.close(); } catch (e) {}
@@ -793,16 +1180,67 @@ function TLSSocket(duplex, options){
793
1180
 
794
1181
  function bindTransport(){
795
1182
  if (!context.transport) return;
1183
+ // Take over the stream's reads, as node:tls does: once a socket is
1184
+ // wrapped, what arrives on it is TLS records and belongs to us alone.
1185
+ // A 'data' listener the caller had on it before — the plaintext
1186
+ // protocol reader of a STARTTLS upgrade — kept receiving every
1187
+ // encrypted record alongside us and fed it into that protocol's
1188
+ // parser. Under node:tls that listener simply goes quiet; code
1189
+ // written for it never removes it, and here it hung the connection.
1190
+ context.transport.removeAllListeners('data');
796
1191
  context.transport.on('data', function(chunk){
797
1192
  _appendChunk(chunk);
798
1193
  parseRecordsAndDispatch();
799
1194
  });
800
1195
  context.transport.on('error', function(err){ markTransportDead(); self.emit('error', err); });
801
- context.transport.on('close', function(){ markTransportDead(); self.emit('close'); });
1196
+ // The transport's end of data: the readable side ends too — as Node's
1197
+ // TLSSocket does on EOF, close_notify or not. Before the handshake is
1198
+ // done it is the handshake's end: a peer that has sent its FIN can
1199
+ // send nothing more, so it cannot finish. It was left waiting — the
1200
+ // socket half-open (allowHalfOpen) until handshakeTimeout, 120 s by
1201
+ // default, for every client that connected, sent part of a hello and
1202
+ // closed. Reported as the close before the handshake is (below), and
1203
+ // the socket let go.
1204
+ context.transport.on('end', function(){
1205
+ if (!context.secureEstablished && !errorEmitted && !context.destroyed) {
1206
+ const e = new Error('Client network socket disconnected before secure TLS connection was established');
1207
+ e.code = 'ECONNRESET';
1208
+ self.destroy(e);
1209
+ return;
1210
+ }
1211
+ endReadable();
1212
+ });
1213
+ context.transport.on('close', function(){
1214
+ const destroyedByUs = context.destroyed; // destroy() before the transport went
1215
+ markTransportDead();
1216
+ // Closed under us before the handshake finished, and nothing said
1217
+ // why (no alert, no transport error): node:tls reports it as an
1218
+ // error — ECONNRESET, "Client network socket disconnected before
1219
+ // secure TLS connection was established". Reporting only 'close',
1220
+ // a caller waiting for the handshake (to send its request) was
1221
+ // never told it would not come, and waited forever: an HTTP/2
1222
+ // attempt refused by a server without h2 hung the request.
1223
+ if (!context.secureEstablished && !errorEmitted && !destroyedByUs) {
1224
+ const e = new Error('Client network socket disconnected before secure TLS connection was established');
1225
+ e.code = 'ECONNRESET';
1226
+ self.emit('error', e);
1227
+ }
1228
+ self.emit('close');
1229
+ });
802
1230
  }
803
1231
 
1232
+ // Whether an 'error' has been emitted — every path that emits one goes
1233
+ // through emit, the stream's own destroy(err) included.
1234
+ let errorEmitted = false;
1235
+ const emitOriginal = self.emit;
1236
+ self.emit = function (name) {
1237
+ if (name === 'error') errorEmitted = true;
1238
+ return emitOriginal.apply(this, arguments);
1239
+ };
1240
+
804
1241
  session.on('message', function(epoch, seq, type, data){
805
1242
  const buf = toBuf(data || []);
1243
+ coalesceFlight();
806
1244
 
807
1245
  // Resolve TLS version: use cached isTls13 if set (post-secureConnect); otherwise query.
808
1246
  // During handshake we may not yet have context.isTls13 set — only after secureConnect.
@@ -922,7 +1360,7 @@ function TLSSocket(duplex, options){
922
1360
  return;
923
1361
  }
924
1362
 
925
- const fragment = encrypt_tls12_gcm_fragment(
1363
+ const fragment = encrypt_tls12_fragment(
926
1364
  buf, context.app_write_key, context.app_write_iv,
927
1365
  context.app_write_seq, CT.ALERT
928
1366
  );
@@ -934,45 +1372,24 @@ function TLSSocket(duplex, options){
934
1372
 
935
1373
  if (epoch === 0) {
936
1374
  // Cleartext handshake (ClientHello/ServerHello)
1375
+ // Before the transport is there (a host name still being
1376
+ // looked up): queued, and written by the same function once it
1377
+ // is (setSocket) — the hello's early data included. Queued as
1378
+ // bare records, the early data never went: the hello was written
1379
+ // later without it, and every 0-RTT connection made by host name
1380
+ // sent its request after the handshake.
937
1381
  if (!context.transport) {
938
- context.pendingHandshake.push({ type: CT.HANDSHAKE, data: buf });
1382
+ context.pendingHandshake.push({ hsType: type, data: buf });
939
1383
  return;
940
1384
  }
941
- // ── Middlebox-compat CCS (RFC 8446 §D.4) ──
942
- // The two roles place it on OPPOSITE sides of their hello, and the
943
- // spec is explicit about both:
944
- //
945
- // server — "immediately AFTER its first handshake message",
946
- // i.e. after ServerHello or HelloRetryRequest.
947
- // client — "immediately BEFORE its second flight", i.e. BEFORE
948
- // the second ClientHello.
949
- //
950
- // Getting the client side backwards is not cosmetic: a peer that
951
- // requires the compat record reads the next thing on the wire and
952
- // finds a handshake record where the CCS should be, and reports
953
- // exactly that ("invalid TLS 1.3 ChangeCipherSpec: 160303...").
954
- // So the client's CCS is emitted BEFORE writeRecord below, and the
955
- // server's after.
956
- //
957
- // We cannot rely on context.isTls13 yet (it is set at
958
- // secureConnect), so both gate on the session's negotiated
959
- // version. For the client that is only known once a
960
- // HelloRetryRequest has told us — which is precisely the CH2 case
961
- // this branch exists for. In the no-HRR flow the version is still
962
- // unknown at CH1 time, so the flag stays false and the CCS goes out
963
- // just before the first encrypted record instead (epoch-1 path
964
- // below); that is the same "before its second flight" placement.
965
- if (!session.isServer && type === 'hello' && session.getVersion() === 0x0304) {
966
- sendCompatCCS();
967
- }
968
-
969
- writeRecord(CT.HANDSHAKE, buf);
1385
+ writeCleartextHandshake(type, buf);
1386
+ return;
1387
+ }
970
1388
 
971
- if (session.isServer &&
972
- (type === 'hello' || type === 'hello_retry_request') &&
973
- session.getVersion() === 0x0304) {
974
- sendCompatCCS();
975
- }
1389
+ if (epoch === 3) {
1390
+ // EndOfEarlyData: under the early key, and the last thing it seals.
1391
+ writeEarlyRecord(CT.HANDSHAKE, buf);
1392
+ context.early.write = null;
976
1393
  return;
977
1394
  }
978
1395
 
@@ -1016,6 +1433,10 @@ function TLSSocket(duplex, options){
1016
1433
  if (canWrite()) {
1017
1434
  context.transport.write(rec);
1018
1435
  }
1436
+ if (session.isServer && type === 'finished' && context.early.status === 'accepted' && !context.early.srvWrite) {
1437
+ context.early.srvWrite = true;
1438
+ process.nextTick(flushServerEarlyWrites);
1439
+ }
1019
1440
  } catch(e) {
1020
1441
  self.emit('error', e);
1021
1442
  }
@@ -1111,19 +1532,11 @@ function TLSSocket(duplex, options){
1111
1532
  || default_signature_schemes();
1112
1533
 
1113
1534
  // Groups: override or default
1114
- let groups = options.groups || ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
1115
-
1116
- // prioritizeChaCha: move ChaCha20 to front of cipher list
1117
- if (options.prioritizeChaCha) {
1118
- let chacha = ciphers.filter(c => c === 0x1303 || c === 0xCCA8);
1119
- let rest = ciphers.filter(c => c !== 0x1303 && c !== 0xCCA8);
1120
- ciphers = [...chacha, ...rest];
1121
- }
1535
+ // A server's default: post-quantum hybrid first (ecdh.js). A client that
1536
+ // offers it gets it; any other gets the classical group it asked for.
1537
+ let groups = options.groups || SERVER_DEFAULT_GROUPS.slice();
1122
1538
 
1123
- // allowedCipherSuites: whitelist filter
1124
- if (context.allowedCipherSuites) {
1125
- ciphers = ciphers.filter(c => context.allowedCipherSuites.includes(c));
1126
- }
1539
+ ciphers = chooseCipherSuites(ciphers, options.prioritizeChaCha, context.allowedCipherSuites);
1127
1540
 
1128
1541
  // ALPN from options
1129
1542
  let alpns = Array.isArray(options.ALPNProtocols) ? options.ALPNProtocols : [];
@@ -1216,7 +1629,21 @@ function TLSSocket(duplex, options){
1216
1629
  if (corkable) t.uncork();
1217
1630
  }
1218
1631
  }
1632
+ // end() was called before the handshake completed: its close was held
1633
+ // back until the writes queued ahead of it were out (see _final).
1634
+ if (context.closeAfterFlush) {
1635
+ context.closeAfterFlush = false;
1636
+ sendCloseNotify();
1637
+ }
1219
1638
 
1639
+ // 'secure' fires on BOTH sides and on sockets built with
1640
+ // `new TLSSocket()`, which is precisely where 'secureConnect' does
1641
+ // NOT fire (Node documents that exclusion). It is therefore the only
1642
+ // completion signal a STARTTLS caller — who wraps an existing socket
1643
+ // rather than going through connect() — can rely on. Emitted first so
1644
+ // a listener on both sees the handshake-complete event before the
1645
+ // client-only one.
1646
+ self.emit('secure');
1220
1647
  self.emit('secureConnect');
1221
1648
  });
1222
1649
 
@@ -1239,8 +1666,7 @@ function TLSSocket(duplex, options){
1239
1666
  session.on('error', function(err) {
1240
1667
  markTransportDead();
1241
1668
  self.emit('error', err);
1242
- const t = context.transport;
1243
- if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
1669
+ closeAfterAlert(); // the session sent its alert just before reporting
1244
1670
  });
1245
1671
 
1246
1672
  // Forward handshakeMessage event (for debugging/inspection)
@@ -1249,11 +1675,17 @@ function TLSSocket(duplex, options){
1249
1675
  });
1250
1676
 
1251
1677
  // Forward keylog event (Node.js compat — NSS SSLKEYLOGFILE format for Wireshark)
1678
+ // node:tls: emitted on a client that set requestOCSP, with the server's
1679
+ // stapled OCSP response (DER), before 'secureConnect'.
1680
+ session.on('OCSPResponse', function(response) {
1681
+ self.emit('OCSPResponse', Buffer.from(response));
1682
+ });
1683
+
1252
1684
  session.on('keylog', function(line) {
1253
1685
  self.emit('keylog', line);
1254
1686
  });
1255
1687
 
1256
- // Forward raw clienthello event (server-side, for JA3/inspection)
1688
+ // Forward raw clienthello event (server-side, for fingerprints/inspection)
1257
1689
  session.on('clienthello', function(raw, parsed) {
1258
1690
  self.emit('clienthello', raw, parsed);
1259
1691
  });
@@ -1313,24 +1745,16 @@ function TLSSocket(duplex, options){
1313
1745
  self.emit('certificateRequest', msg);
1314
1746
  });
1315
1747
 
1316
- // Server: automatic PSK handler (decrypts TLS 1.3 tickets with ticketKeys).
1317
- // The new 'psk' event payload is { identity, obfuscatedAge } (object), not raw identity.
1748
+ // Server: a 'psk' listener on the socket may decide a TLS 1.3 PSK identity
1749
+ // itself; payload { identity, obfuscatedAge }, answered synchronously with
1750
+ // { psk, cipher, data? } or null. Without one, nothing is answered here and
1751
+ // the session opens its own ticket — it issued it, with the same
1752
+ // ticketKeys. (This used to decrypt the ticket right here, which left a
1753
+ // TLSSession used without a socket — QUIC — unable to accept its own
1754
+ // tickets, and skipped the lifetime check the session now makes.)
1318
1755
  if (options.isServer) {
1319
1756
  session.on('psk', function(info, callback) {
1320
- // First check if user has a custom handler on the socket
1321
- if (self.listenerCount('psk') > 0) {
1322
- // Let user handle it — pass through the same payload
1323
- self.emit('psk', info, callback);
1324
- return;
1325
- }
1326
-
1327
- // Auto-decrypt ticket using ticketKeys (unified format)
1328
- let state = decrypt_session_blob(info.identity, context.ticketKeys);
1329
- if (state && state.v === 13 && state.psk && state.cipher) {
1330
- callback({ psk: state.psk, cipher: state.cipher });
1331
- } else {
1332
- callback(null); // Decryption failed / not our ticket → full handshake
1333
- }
1757
+ if (self.listenerCount('psk') > 0) self.emit('psk', info, callback);
1334
1758
  });
1335
1759
  }
1336
1760
 
@@ -1355,6 +1779,65 @@ function TLSSocket(duplex, options){
1355
1779
  }
1356
1780
  });
1357
1781
 
1782
+ /**
1783
+ * Push the client's negotiable preferences into the session before its
1784
+ * ClientHello is built.
1785
+ *
1786
+ * The server does this from the 'hello' event, because a server only
1787
+ * learns what to offer once it has seen the client's hello. A client has
1788
+ * no such event to wait for — its hello IS the first message — so the
1789
+ * equivalent has to happen at construction. TLSSession builds the
1790
+ * ClientHello from a setTimeout, so a synchronous set_context here lands
1791
+ * first.
1792
+ *
1793
+ * ALPNProtocols is the one that matters most: RFC 7301 negotiation cannot
1794
+ * start unless the client offers something, and HTTP/2 over TLS does not
1795
+ * merely benefit from ALPN but REQUIRES it (RFC 9113 §3.2). A client that
1796
+ * silently offers nothing can never speak h2 to any server.
1797
+ *
1798
+ * The other four are applied for the same reason — each is documented as a
1799
+ * per-connection option, and each was being dropped on the client side.
1800
+ */
1801
+ function applyClientContext() {
1802
+ if (session.isServer) return;
1803
+
1804
+ let update = {};
1805
+
1806
+ if (Array.isArray(options.ALPNProtocols) && options.ALPNProtocols.length > 0) {
1807
+ update.local_supported_alpns = options.ALPNProtocols.map(function (p) {
1808
+ return Buffer.isBuffer(p) ? p.toString('latin1') : String(p);
1809
+ });
1810
+ }
1811
+
1812
+ let maxVer = parseVersion(options.maxVersion);
1813
+ let minVer = parseVersion(options.minVersion);
1814
+ if (maxVer || minVer) {
1815
+ maxVer = maxVer || 0x0304;
1816
+ minVer = minVer || 0x0303;
1817
+ let versions = [];
1818
+ if (maxVer >= 0x0304 && minVer <= 0x0304) versions.push(0x0304);
1819
+ if (maxVer >= 0x0303 && minVer <= 0x0303) versions.push(0x0303);
1820
+ if (versions.length === 0) versions.push(0x0303);
1821
+ update.local_supported_versions = versions;
1822
+ }
1823
+
1824
+ if (options.groups) update.local_supported_groups = options.groups;
1825
+ if (options.signatureAlgorithms) {
1826
+ update.local_supported_signature_algorithms = options.signatureAlgorithms;
1827
+ }
1828
+
1829
+ if (options.allowedCipherSuites || options.prioritizeChaCha) {
1830
+ let vers = update.local_supported_versions || [0x0304, 0x0303];
1831
+ let ciphers = default_cipher_suites(vers.indexOf(0x0304) >= 0,
1832
+ vers.indexOf(0x0303) >= 0);
1833
+ update.local_supported_cipher_suites = chooseCipherSuites(ciphers, options.prioritizeChaCha, options.allowedCipherSuites);
1834
+ }
1835
+
1836
+ if (Object.keys(update).length > 0) session.set_context(update);
1837
+ }
1838
+
1839
+ applyClientContext();
1840
+
1358
1841
  // If duplex was passed in constructor, start reading
1359
1842
  if (context.transport) {
1360
1843
  bindTransport();
@@ -1401,9 +1884,11 @@ function TLSSocket(duplex, options){
1401
1884
  if (context.destroyed) { callback(new Error('Socket destroyed')); return; }
1402
1885
  const buf = toBuf(chunk);
1403
1886
 
1404
- if (!context.using_app_keys) {
1405
- // Pre-handshake: queue for flush at secureConnect.
1887
+ if (!context.using_app_keys && !context.early.srvWrite) {
1888
+ // Pre-handshake: queue for flush at secureConnect — or, a client
1889
+ // offering early data, sent early from the queue now.
1406
1890
  context.appWriteQueue.push(buf);
1891
+ if (context.early.write) flushEarly();
1407
1892
  process.nextTick(callback);
1408
1893
  return;
1409
1894
  }
@@ -1449,8 +1934,9 @@ function TLSSocket(duplex, options){
1449
1934
  }
1450
1935
  }
1451
1936
 
1452
- if (!context.using_app_keys) {
1937
+ if (!context.using_app_keys && !context.early.srvWrite) {
1453
1938
  context.appWriteQueue.push(combined);
1939
+ if (context.early.write) flushEarly();
1454
1940
  process.nextTick(callback);
1455
1941
  return;
1456
1942
  }
@@ -1463,8 +1949,103 @@ function TLSSocket(duplex, options){
1463
1949
  }
1464
1950
  };
1465
1951
 
1466
- /** Duplex _read — data is pushed when received, no pull needed */
1467
- self._read = function() {};
1952
+ /**
1953
+ * Read backpressure, as net.Socket has it. Decrypted data is pushed as
1954
+ * records arrive; when the reader stops taking it (push() returns false —
1955
+ * a paused stream, a consumer that has fallen behind) the transport is
1956
+ * paused, so the kernel's buffer fills and TCP slows the peer down. The
1957
+ * stream asks for more (_read) once its buffer has room, and the
1958
+ * transport resumes. Without it pause() on this socket stopped only the
1959
+ * 'data' events: the transport read on, and everything the peer sent
1960
+ * piled up decrypted in this stream's buffer, without bound.
1961
+ */
1962
+ let transportPausedForRead = false;
1963
+ function pushAppData(buf) {
1964
+ if (self.push(buf)) return;
1965
+ const t = context.transport;
1966
+ if (!transportPausedForRead && t && typeof t.pause === 'function') {
1967
+ transportPausedForRead = true;
1968
+ t.pause();
1969
+ }
1970
+ }
1971
+ self._read = function() {
1972
+ if (!transportPausedForRead) return;
1973
+ transportPausedForRead = false;
1974
+ const t = context.transport;
1975
+ if (t && typeof t.resume === 'function' && !t.destroyed) t.resume();
1976
+ };
1977
+
1978
+ // The server, early data taken and its Finished out: what was written
1979
+ // meanwhile goes now, in order, under the application key.
1980
+ function flushServerEarlyWrites() {
1981
+ if (context.destroyed || context.appWriteQueue.length === 0) return;
1982
+ const q = context.appWriteQueue;
1983
+ context.appWriteQueue = [];
1984
+ for (let i = 0; i < q.length; i++) writeAppData(q[i]);
1985
+ }
1986
+
1987
+ // 0-RTT. Server: whether what is read now came as early data (taken,
1988
+ // its EndOfEarlyData not yet here) — it could be a replay; client:
1989
+ // whether it is sending early. earlyDataBytes: what came (server) or
1990
+ // went (client) early. earlyDataStatus: null (none), 'offered',
1991
+ // 'accepted', 'rejected'. And 'earlyData' (accepted) when it is decided.
1992
+ Object.defineProperty(self, 'isEarlyData', { configurable: true, get: function () {
1993
+ return session.isServer ? (context.early.status === 'accepted' && !context.early.eoed)
1994
+ : (context.early.status === 'offered' && !!context.early.write);
1995
+ } });
1996
+ Object.defineProperty(self, 'earlyDataBytes', { configurable: true, get: function () {
1997
+ return session.isServer ? context.early.recv : context.early.sent;
1998
+ } });
1999
+ Object.defineProperty(self, 'earlyDataStatus', { configurable: true, get: function () { return context.early.status; } });
2000
+
2001
+ // A cleartext handshake message (ClientHello, ServerHello, HRR) onto
2002
+ // the transport, with what goes around it: the compatibility CCS
2003
+ // (RFC 8446 §D.4), and a client's early data after its first hello.
2004
+ function writeCleartextHandshake(type, buf) {
2005
+ // ── Middlebox-compat CCS (RFC 8446 §D.4) ──
2006
+ // The two roles place it on OPPOSITE sides of their hello, and the
2007
+ // spec is explicit about both:
2008
+ //
2009
+ // server — "immediately AFTER its first handshake message",
2010
+ // i.e. after ServerHello or HelloRetryRequest.
2011
+ // client — "immediately BEFORE its second flight", i.e. BEFORE
2012
+ // the second ClientHello.
2013
+ //
2014
+ // Getting the client side backwards is not cosmetic: a peer that
2015
+ // requires the compat record reads the next thing on the wire and
2016
+ // finds a handshake record where the CCS should be, and reports
2017
+ // exactly that ("invalid TLS 1.3 ChangeCipherSpec: 160303...").
2018
+ // So the client's CCS is emitted BEFORE writeRecord below, and the
2019
+ // server's after.
2020
+ //
2021
+ // We cannot rely on context.isTls13 yet (it is set at
2022
+ // secureConnect), so both gate on the session's negotiated
2023
+ // version. For the client that is only known once a
2024
+ // HelloRetryRequest has told us — which is precisely the CH2 case
2025
+ // this branch exists for. In the no-HRR flow the version is still
2026
+ // unknown at CH1 time, so the flag stays false and the CCS goes out
2027
+ // just before the first encrypted record instead (epoch-1 path
2028
+ // below); that is the same "before its second flight" placement.
2029
+ if (!session.isServer && type === 'hello' && session.getVersion() === 0x0304) {
2030
+ sendCompatCCS();
2031
+ }
2032
+
2033
+ writeRecord(CT.HANDSHAKE, buf);
2034
+
2035
+ // Offering early data: the compatibility CCS right after this
2036
+ // first hello (§D.4), then the early data.
2037
+ if (!session.isServer && type === 'hello' && context.early.write && context.early.status === 'offered' && !context.early.helloSent) {
2038
+ context.early.helloSent = true;
2039
+ sendCompatCCS();
2040
+ flushEarly();
2041
+ }
2042
+
2043
+ if (session.isServer &&
2044
+ (type === 'hello' || type === 'hello_retry_request') &&
2045
+ session.getVersion() === 0x0304) {
2046
+ sendCompatCCS();
2047
+ }
2048
+ }
1468
2049
 
1469
2050
  // === Node-compatible API ===
1470
2051
 
@@ -1476,22 +2057,64 @@ function TLSSocket(duplex, options){
1476
2057
  // Flush any pending handshake messages (e.g. ClientHello queued before transport was ready)
1477
2058
  while (context.pendingHandshake.length > 0) {
1478
2059
  let msg = context.pendingHandshake.shift();
1479
- writeRecord(msg.type, Buffer.from(msg.data));
2060
+ writeCleartextHandshake(msg.hsType, Buffer.from(msg.data));
1480
2061
  }
1481
2062
  };
1482
2063
 
1483
- /** Send close_notify and gracefully close. */
2064
+ /**
2065
+ * end(): the Writable's own. It finishes the writes still queued, THEN
2066
+ * calls _final below, which sends close_notify and closes the transport.
2067
+ *
2068
+ * This used to send close_notify and end the transport first, and only
2069
+ * then hand over to the Writable — so everything still queued was written
2070
+ * into a closed connection and lost. And something always is queued:
2071
+ * _write defers its callback a tick so that writes in the same tick
2072
+ * coalesce into one record. So `write(head); write(body); end()` sent the
2073
+ * head and dropped the body, and end(data) dropped data outright. Seen as
2074
+ * every HTTP/1.1 response to a request with Connection: close arriving
2075
+ * with its headers and without its body.
2076
+ */
1484
2077
  self.end = (function(originalEnd) {
1485
2078
  return function(data, encoding, callback) {
1486
2079
  if (context.destroyed) return this;
1487
- session.close(); // sends close_notify alert
1488
- try { context.transport && context.transport.end && context.transport.end(); } catch(e){}
1489
2080
  return originalEnd.call(this, data, encoding, callback);
1490
2081
  };
1491
2082
  })(self.end);
1492
2083
 
2084
+ // The Writable calls this once every write before end() has been handed
2085
+ // down. Before the handshake completes, those writes are only queued
2086
+ // (appWriteQueue, flushed at secureConnect) — closing here would drop
2087
+ // them, so the close waits for that flush.
2088
+ // Once: our end(), and the automatic end() that follows the readable
2089
+ // side ending (allowHalfOpen off), would otherwise each send one.
2090
+ function sendCloseNotify() {
2091
+ if (context.destroyed || context.closeNotifySent) return;
2092
+ context.closeNotifySent = true;
2093
+ session.close(); // sends close_notify alert
2094
+ try { context.transport && context.transport.end && context.transport.end(); } catch(e){}
2095
+ }
2096
+ /**
2097
+ * The peer will send nothing more (close_notify, or EOF on the transport):
2098
+ * end the readable side, so 'end' is emitted after what is already
2099
+ * buffered — the event a reader waits on to know it has everything. It
2100
+ * was never emitted; only 'close' was, so a response delimited by the
2101
+ * connection closing could not be told apart from one cut short.
2102
+ */
2103
+ function endReadable() {
2104
+ if (context.readEnded || self.destroyed) return;
2105
+ context.readEnded = true;
2106
+ self.push(null);
2107
+ }
2108
+
2109
+ self._final = function(callback) {
2110
+ if (!context.using_app_keys) context.closeAfterFlush = true;
2111
+ else sendCloseNotify();
2112
+ callback();
2113
+ };
2114
+
1493
2115
  self.destroy = (function(originalDestroy) {
1494
2116
  return function(err) {
2117
+ clearHandshakeTimer();
1495
2118
  if (context.destroyed) return this;
1496
2119
  context.destroyed = true;
1497
2120
  try { context.transport && context.transport.destroy && context.transport.destroy(); } catch(e){}
@@ -1535,6 +2158,9 @@ function TLSSocket(duplex, options){
1535
2158
  };
1536
2159
 
1537
2160
  /** Returns negotiated ALPN protocol string, or false. */
2161
+ // Encrypted Client Hello (server): null (not offered) | 'accepted' | 'rejected'.
2162
+ Object.defineProperty(self, 'echStatus', { get: function(){ return session.getEchStatus(); } });
2163
+ Object.defineProperty(self, 'echAccepted', { get: function(){ return session.getEchStatus() === 'accepted'; } });
1538
2164
  Object.defineProperty(self, 'alpnProtocol', {
1539
2165
  get: function(){ return session.getALPN() || false; },
1540
2166
  enumerable: true
@@ -1580,23 +2206,11 @@ function TLSSocket(duplex, options){
1580
2206
  self.getPeerCertificate = function(){
1581
2207
  let chain = session.getPeerCertificate();
1582
2208
  if (!chain || chain.length === 0) return null;
1583
- try {
1584
- let certDer = chain[0].cert;
1585
- let x509 = new crypto.X509Certificate(certDer);
1586
- return {
1587
- subject: x509.subject,
1588
- issuer: x509.issuer,
1589
- subjectaltname: x509.subjectAltName,
1590
- valid_from: x509.validFrom,
1591
- valid_to: x509.validTo,
1592
- fingerprint: x509.fingerprint,
1593
- fingerprint256: x509.fingerprint256,
1594
- serialNumber: x509.serialNumber,
1595
- raw: certDer
1596
- };
1597
- } catch(e) {
1598
- return { raw: chain[0].cert };
1599
- }
2209
+ // Subject and issuer as objects keyed by RDN type ({ C, ST, L, O,
2210
+ // OU, CN }), as Node documents them — not the newline-separated text
2211
+ // X509Certificate hands back, which stopped tls.checkServerIdentity()
2212
+ // (it reads cert.subject.CN) from working against this method.
2213
+ return certificateObject(chain[0].cert);
1600
2214
  };
1601
2215
 
1602
2216
  /** Node-compat: return the peer's leaf cert as a native X509Certificate
@@ -1626,8 +2240,7 @@ function TLSSocket(duplex, options){
1626
2240
  * object (mirror of getPeerCertificate). Empty object {} if we didn't
1627
2241
  * present a cert — this matches Node's observed behavior. */
1628
2242
  self.getCertificate = function(){
1629
- let sctx = session.context;
1630
- let localChain = sctx && sctx.local_cert_chain;
2243
+ let localChain = session.getCertificate();
1631
2244
  if (!localChain || localChain.length === 0) return {};
1632
2245
  try {
1633
2246
  let der = localChain[0].cert || localChain[0];
@@ -1776,12 +2389,89 @@ function TLSSocket(duplex, options){
1776
2389
  enumerable: true
1777
2390
  });
1778
2391
 
1779
- /** JA3 fingerprint from ClientHello (server-side only). Returns { hash, raw } or null. */
1780
- self.getJA3 = function(){ return session.getJA3 ? session.getJA3() : null; };
2392
+ /** TLS fingerprints, { hash, raw } or null (see TLSSession): JA4 and JA5
2393
+ * of the ClientHello, JA4S of the ServerHello — in either role.
2394
+ * `opts.protocol` overrides the transport character (e.g. 'q' for QUIC). */
2395
+ self.getJA4 = function(opts){ return session.getJA4(opts); };
2396
+ self.getJA5 = function(opts){ return session.getJA5(opts); };
2397
+ self.getJA4S = function(opts){ return session.getJA4S(opts); };
2398
+
2399
+ /** Node-compat: set the key and certificate for THIS connection. Accepts a
2400
+ * context from createSecureContext() or a plain { key, cert }. Mainly for
2401
+ * picking a server certificate from inside ALPNCallback, once the
2402
+ * negotiated protocol is known. */
2403
+ self.setKeyCert = function(ctxOrOpts){
2404
+ if (session.setKeyCert) session.setKeyCert(ctxOrOpts);
2405
+ };
1781
2406
 
1782
2407
  /** ECDHE shared secret (Buffer), or null. For research/advanced use. */
1783
2408
  self.getSharedSecret = function(){ return session.getSharedSecret ? session.getSharedSecret() : null; };
1784
2409
 
2410
+ /** Node-compat: the Finished verify_data we sent, or undefined.
2411
+ * The session has kept this all along (context.local_finished_data); only
2412
+ * the proxy was missing, so the documented method did not exist on the
2413
+ * socket. Used for the tls-unique channel binding of RFC 5929. */
2414
+ self.getFinished = function(){
2415
+ let v = session.getFinished ? session.getFinished() : null;
2416
+ return v == null ? undefined : v; // Node returns undefined, not null
2417
+ };
2418
+
2419
+ /** Node-compat: the peer's Finished verify_data, or undefined. */
2420
+ self.getPeerFinished = function(){
2421
+ let v = session.getPeerFinished ? session.getPeerFinished() : null;
2422
+ return v == null ? undefined : v;
2423
+ };
2424
+
2425
+ // This connection's ticket, from the first connection on: { data,
2426
+ // resumed, offered } (see TLSSession.getTicketInfo).
2427
+ self.getTicketInfo = function(){ return session.getTicketInfo(); };
2428
+
2429
+ // The underlying TLSSession, as the README documents (socket.session):
2430
+ // the state machine's own methods — setTicketState() and sendTicket(),
2431
+ // exportKeyingMaterial(), the secrets — on a TCP connection too. Read-only.
2432
+ Object.defineProperty(self, 'session', { get: function(){ return session; }, configurable: true, enumerable: false });
2433
+
2434
+ /** Node-compat: whether this connection resumed an earlier session.
2435
+ * Same value as the `isResumed` property; Node spells it as a method. */
2436
+ self.isSessionReused = function(){ return !!session.isResumed; };
2437
+
2438
+ /** Node-compat: bound address of the underlying transport,
2439
+ * `{ port, family, address }`. Empty object when not connected. */
2440
+ self.address = function(){
2441
+ let t = context.transport;
2442
+ if (t && typeof t.address === 'function') {
2443
+ try { return t.address() || {}; } catch(e) { return {}; }
2444
+ }
2445
+ return {};
2446
+ };
2447
+
2448
+ /** Node-compat: describes the ephemeral key agreement.
2449
+ *
2450
+ * Node returns {} when the agreement is not ephemeral, and null when
2451
+ * asked on a server socket — it only reports the peer's temporary key,
2452
+ * which a server does not receive. TLS 1.3 is always (EC)DHE, so on 1.3
2453
+ * there is always a group to report. */
2454
+ self.getEphemeralKeyInfo = function(){
2455
+ if (session.context && session.context.isServer) return null;
2456
+ let g = session.context ? session.context.selected_group : null;
2457
+ if (g == null) return {};
2458
+ let known = {
2459
+ 0x0017: { name: 'prime256v1', size: 256 },
2460
+ 0x0018: { name: 'secp384r1', size: 384 },
2461
+ 0x0019: { name: 'secp521r1', size: 521 },
2462
+ 0x001d: { name: 'x25519', size: 253 },
2463
+ 0x001e: { name: 'x448', size: 448 }
2464
+ };
2465
+ let k = known[g];
2466
+ // Not an ECDH curve, so not in that table, but a group with a name.
2467
+ if (!k && ecdh_group_name(g)) return { type: 'TLSGroup', name: ecdh_group_name(g) };
2468
+ // Anything outside that table is still a negotiated TLS group — report
2469
+ // it as such rather than as an ECDH curve we cannot name, which is what
2470
+ // Node's 'TLSGroup' type is for.
2471
+ if (!k) return { type: 'TLSGroup', name: '0x' + g.toString(16) };
2472
+ return { type: 'ECDH', name: k.name, size: k.size };
2473
+ };
2474
+
1785
2475
  /** Full negotiation result — all selected parameters in one object. */
1786
2476
  self.getNegotiationResult = function(){ return session.getNegotiationResult ? session.getNegotiationResult() : null; };
1787
2477