lemon-tls 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
 
@@ -75,11 +98,19 @@ function parseVersion(v) {
75
98
 
76
99
 
77
100
  // ==== TLSSocket ====
101
+
78
102
  function TLSSocket(duplex, options){
79
103
  if (!(this instanceof TLSSocket)) return new TLSSocket(duplex, options);
80
104
  options = options || {};
81
105
 
82
- // 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
+ //
83
114
  // highWaterMark: raised to 256KB from the Node default of 16KB. TLS records max
84
115
  // at 16KB of plaintext, so the default meant "pause after one record" — terrible
85
116
  // for bulk transfers where we want many records in flight. 256KB keeps ~16 records
@@ -87,15 +118,41 @@ function TLSSocket(duplex, options){
87
118
  // small bursts interrupted by drain events. Most impactful on the Download path
88
119
  // where the sender is aggressive (tight-loop sock.write).
89
120
  Duplex.call(this, {
90
- 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,
91
126
  readableObjectMode: false,
92
127
  writableObjectMode: false,
93
- highWaterMark: 256 * 1024,
128
+ highWaterMark: options.highWaterMark || 256 * 1024,
94
129
  });
95
130
  const self = this;
96
131
 
97
132
  let _ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
98
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
+
99
156
  let context = {
100
157
 
101
158
  options: options,
@@ -110,17 +167,57 @@ function TLSSocket(duplex, options){
110
167
  ALPNProtocols: options.ALPNProtocols || null,
111
168
  SNICallback: options.SNICallback || null,
112
169
  ticketKeys: _ticketKeys,
170
+ // A session ID only where something stores the session ('newSession').
171
+ sessionIdStore: function() { return self.listenerCount('newSession') > 0; },
113
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,
114
185
  session: options.session || null,
115
186
  psk: options.psk || null,
116
187
  rejectUnauthorized: options.rejectUnauthorized,
117
- 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,
118
201
  sessionTickets: options.sessionTickets,
119
202
  maxHandshakeSize: options.maxHandshakeSize || 0,
120
203
  customExtensions: options.customExtensions || [],
204
+ permuteExtensions: !!options.permuteExtensions,
205
+ grease: !!options.grease,
121
206
  requestCert: !!options.requestCert,
122
- cert: options.cert || null,
123
- 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,
124
221
  }),
125
222
 
126
223
  // Handshake write
@@ -212,6 +309,15 @@ function TLSSocket(duplex, options){
212
309
  hsBuf: null,
213
310
 
214
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,
215
321
  pendingHandshake: [],
216
322
 
217
323
  // General state
@@ -237,6 +343,22 @@ function TLSSocket(duplex, options){
237
343
 
238
344
  let session = context.session;
239
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
+
240
362
 
241
363
  // === Record Layer ===
242
364
  // writeRecord returns the transport's backpressure signal (true = ready for more,
@@ -244,10 +366,32 @@ function TLSSocket(duplex, options){
244
366
  function writeRecord(type, payload){
245
367
  if (!context.transport) throw new Error('No transport attached to TLSSocket');
246
368
  if (context.destroyed || context.transport.destroyed || context.transport.writableEnded) return false;
369
+ coalesceFlight();
247
370
  try { return writeRawRecord(context.transport, type, payload, context.rec_version); }
248
371
  catch(e){ self.emit('error', e); return false; }
249
372
  }
250
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
+
251
395
  const MAX_RECORD_PLAINTEXT = 16384; // TLS max record size (2^14)
252
396
 
253
397
  // writeAppData / writeAppDataSingle return the transport's backpressure signal:
@@ -347,7 +491,10 @@ function TLSSocket(duplex, options){
347
491
  }
348
492
  }
349
493
 
350
- 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){
351
498
  // Hot path — same optimizations as writeAppDataSingle:
352
499
  // - cached context.isTls13 instead of per-record session.getVersion()
353
500
  // - cached context.aeadAlgo instead of per-record getAeadAlgo()
@@ -390,7 +537,7 @@ function TLSSocket(duplex, options){
390
537
  }
391
538
 
392
539
  const recordType = context.using_app_keys ? 0x17 : 0x16;
393
- 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);
394
541
 
395
542
  } else if (isTls13 && context.using_app_keys === true) {
396
543
  // TLS 1.3: decrypt app data with app traffic secret
@@ -435,6 +582,7 @@ function TLSSocket(duplex, options){
435
582
  context.handshake_read_seq++;
436
583
  }
437
584
  } catch (e) {
585
+ if (trial) return false;
438
586
  // Record failed authentication → fatal bad_record_mac (RFC 8446 §5.2).
439
587
  try { session.sendAlert ? session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC) : null; } catch (_) {}
440
588
  self.emit('error', new Error('TLS record decryption failed (bad_record_mac)'));
@@ -461,23 +609,172 @@ function TLSSocket(duplex, options){
461
609
 
462
610
  if (content_type === CT.APPLICATION_DATA) {
463
611
  // zero-copy view into the decrypted plaintext
464
- self.push(out.subarray(0, j));
612
+ pushAppData(out.subarray(0, j));
465
613
  } else if (content_type === CT.HANDSHAKE) {
466
614
  feedHandshakeBytes(out.subarray(0, j));
467
615
  } else if (content_type === CT.ALERT) {
468
- // 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));
469
620
  }
470
621
  } else {
471
622
  // TLS 1.2: no inner plaintext wrapping — `out` is the Buffer from decrypt12
472
623
  if (context.using_app_keys === true) {
473
- self.push(out);
624
+ pushAppData(out);
474
625
  } else {
475
626
  feedHandshakeBytes(out);
476
627
  }
477
628
  }
478
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); }
479
737
  }
480
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
+ });
481
778
 
482
779
  // ============================================================================
483
780
  // Read buffer management
@@ -558,6 +855,15 @@ function TLSSocket(duplex, options){
558
855
 
559
856
  while (context.hsBuf.length >= 4) {
560
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
+ }
561
867
  if (context.hsBuf.length < 4 + len) break; // partial message — wait for more records
562
868
 
563
869
  const msg = context.hsBuf.subarray(0, 4 + len);
@@ -572,6 +878,11 @@ function TLSSocket(duplex, options){
572
878
  }
573
879
 
574
880
  function parseRecordsAndDispatch(){
881
+ if (context.parsing) return;
882
+ context.parsing = true;
883
+ try { parseRecords(); } finally { context.parsing = false; }
884
+ }
885
+ function parseRecords(){
575
886
  // Hot path — zero-copy parsing:
576
887
  // - Work directly on readBuffer with moving readStart offset (no slicing)
577
888
  // - Body is a subarray view (no copy) passed down to crypto
@@ -602,9 +913,39 @@ function TLSSocket(duplex, options){
602
913
  }
603
914
 
604
915
  const type = rb[off];
605
- // Version at rb[off+1..off+3] — currently unused (legacy 0x0303 always)
606
916
  const len = (rb[off + 3] << 8) | rb[off + 4];
607
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
+
608
949
  if (end - off < 5 + len) break; // record not fully received
609
950
 
610
951
  // Zero-copy views: header (5 bytes) + body
@@ -612,20 +953,6 @@ function TLSSocket(duplex, options){
612
953
  const body = rb.subarray(off + 5, off + 5 + len);
613
954
  off += 5 + len;
614
955
 
615
- // ── Record-layer sanity, applied to every record ──
616
- // RFC 8446 §5.1: the legacy_record_version is 0x0303 for every
617
- // record after the first ClientHello, and 0x03xx before it. A
618
- // record claiming anything else is malformed framing, not a
619
- // version negotiation signal.
620
- const recVer = (rb[off - len - 5 + 1] << 8) | rb[off - len - 5 + 2];
621
- if ((recVer >>> 8) !== 0x03) {
622
- try { session.sendAlert(2, TLS_ALERT.PROTOCOL_VERSION); } catch (e) {}
623
- self.emit('error', new Error('Record with illegal legacy_record_version 0x' + recVer.toString(16)));
624
- markTransportDead();
625
- if (context.transport && !context.transport.destroyed) context.transport.destroy();
626
- return;
627
- }
628
-
629
956
  // A peer may legally send an empty record, and a TLS 1.3 peer may
630
957
  // legally send a compat CCS — but only finitely many. Unbounded
631
958
  // streams of either make no progress and exist only to burn our
@@ -635,7 +962,7 @@ function TLSSocket(duplex, options){
635
962
  try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
636
963
  self.emit('error', new Error('Too many consecutive empty records'));
637
964
  markTransportDead();
638
- if (context.transport && !context.transport.destroyed) context.transport.destroy();
965
+ closeAfterAlert();
639
966
  return;
640
967
  }
641
968
  } else {
@@ -647,13 +974,20 @@ function TLSSocket(duplex, options){
647
974
  try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
648
975
  self.emit('error', new Error('Too many consecutive ChangeCipherSpec records'));
649
976
  markTransportDead();
650
- if (context.transport && !context.transport.destroyed) context.transport.destroy();
977
+ closeAfterAlert();
651
978
  return;
652
979
  }
653
980
  } else {
654
981
  context.consecutive_ccs_records = 0;
655
982
  }
656
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
+ }
657
991
  if (type === CT.APPLICATION_DATA) {
658
992
  processCiphertext(body, header);
659
993
  context.tls12_read_seq++;
@@ -703,23 +1037,7 @@ function TLSSocket(duplex, options){
703
1037
  }
704
1038
  // TLS 1.3, or version not yet negotiated: swallow silently.
705
1039
  } else if (type === CT.ALERT) {
706
- // Alert: 2 bytes — level (1=warning, 2=fatal), description
707
- if (body.length >= 2) {
708
- const level = body[0];
709
- const desc = body[1];
710
- self.emit('alert', { level: level, description: desc });
711
- if (desc === TLS_ALERT.CLOSE_NOTIFY) {
712
- session.close();
713
- if (context.transport && typeof context.transport.end === 'function') {
714
- context.transport.end();
715
- }
716
- }
717
- if (level === TLS_ALERT_LEVEL.FATAL) {
718
- if (context.transport && typeof context.transport.destroy === 'function') {
719
- context.transport.destroy();
720
- }
721
- }
722
- }
1040
+ handleAlert(body);
723
1041
  }
724
1042
  }
725
1043
 
@@ -784,7 +1102,77 @@ function TLSSocket(duplex, options){
784
1102
  return true;
785
1103
  }
786
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
+
787
1174
  function markTransportDead() {
1175
+ clearHandshakeTimer();
788
1176
  if (context.destroyed) return;
789
1177
  context.destroyed = true;
790
1178
  try { session.close(); } catch (e) {}
@@ -792,16 +1180,67 @@ function TLSSocket(duplex, options){
792
1180
 
793
1181
  function bindTransport(){
794
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');
795
1191
  context.transport.on('data', function(chunk){
796
1192
  _appendChunk(chunk);
797
1193
  parseRecordsAndDispatch();
798
1194
  });
799
1195
  context.transport.on('error', function(err){ markTransportDead(); self.emit('error', err); });
800
- 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
+ });
801
1230
  }
802
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
+
803
1241
  session.on('message', function(epoch, seq, type, data){
804
1242
  const buf = toBuf(data || []);
1243
+ coalesceFlight();
805
1244
 
806
1245
  // Resolve TLS version: use cached isTls13 if set (post-secureConnect); otherwise query.
807
1246
  // During handshake we may not yet have context.isTls13 set — only after secureConnect.
@@ -921,7 +1360,7 @@ function TLSSocket(duplex, options){
921
1360
  return;
922
1361
  }
923
1362
 
924
- const fragment = encrypt_tls12_gcm_fragment(
1363
+ const fragment = encrypt_tls12_fragment(
925
1364
  buf, context.app_write_key, context.app_write_iv,
926
1365
  context.app_write_seq, CT.ALERT
927
1366
  );
@@ -933,45 +1372,24 @@ function TLSSocket(duplex, options){
933
1372
 
934
1373
  if (epoch === 0) {
935
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.
936
1381
  if (!context.transport) {
937
- context.pendingHandshake.push({ type: CT.HANDSHAKE, data: buf });
1382
+ context.pendingHandshake.push({ hsType: type, data: buf });
938
1383
  return;
939
1384
  }
940
- // ── Middlebox-compat CCS (RFC 8446 §D.4) ──
941
- // The two roles place it on OPPOSITE sides of their hello, and the
942
- // spec is explicit about both:
943
- //
944
- // server — "immediately AFTER its first handshake message",
945
- // i.e. after ServerHello or HelloRetryRequest.
946
- // client — "immediately BEFORE its second flight", i.e. BEFORE
947
- // the second ClientHello.
948
- //
949
- // Getting the client side backwards is not cosmetic: a peer that
950
- // requires the compat record reads the next thing on the wire and
951
- // finds a handshake record where the CCS should be, and reports
952
- // exactly that ("invalid TLS 1.3 ChangeCipherSpec: 160303...").
953
- // So the client's CCS is emitted BEFORE writeRecord below, and the
954
- // server's after.
955
- //
956
- // We cannot rely on context.isTls13 yet (it is set at
957
- // secureConnect), so both gate on the session's negotiated
958
- // version. For the client that is only known once a
959
- // HelloRetryRequest has told us — which is precisely the CH2 case
960
- // this branch exists for. In the no-HRR flow the version is still
961
- // unknown at CH1 time, so the flag stays false and the CCS goes out
962
- // just before the first encrypted record instead (epoch-1 path
963
- // below); that is the same "before its second flight" placement.
964
- if (!session.isServer && type === 'hello' && session.getVersion() === 0x0304) {
965
- sendCompatCCS();
966
- }
967
-
968
- writeRecord(CT.HANDSHAKE, buf);
1385
+ writeCleartextHandshake(type, buf);
1386
+ return;
1387
+ }
969
1388
 
970
- if (session.isServer &&
971
- (type === 'hello' || type === 'hello_retry_request') &&
972
- session.getVersion() === 0x0304) {
973
- sendCompatCCS();
974
- }
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;
975
1393
  return;
976
1394
  }
977
1395
 
@@ -1015,6 +1433,10 @@ function TLSSocket(duplex, options){
1015
1433
  if (canWrite()) {
1016
1434
  context.transport.write(rec);
1017
1435
  }
1436
+ if (session.isServer && type === 'finished' && context.early.status === 'accepted' && !context.early.srvWrite) {
1437
+ context.early.srvWrite = true;
1438
+ process.nextTick(flushServerEarlyWrites);
1439
+ }
1018
1440
  } catch(e) {
1019
1441
  self.emit('error', e);
1020
1442
  }
@@ -1110,19 +1532,11 @@ function TLSSocket(duplex, options){
1110
1532
  || default_signature_schemes();
1111
1533
 
1112
1534
  // Groups: override or default
1113
- let groups = options.groups || ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
1114
-
1115
- // prioritizeChaCha: move ChaCha20 to front of cipher list
1116
- if (options.prioritizeChaCha) {
1117
- let chacha = ciphers.filter(c => c === 0x1303 || c === 0xCCA8);
1118
- let rest = ciphers.filter(c => c !== 0x1303 && c !== 0xCCA8);
1119
- ciphers = [...chacha, ...rest];
1120
- }
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();
1121
1538
 
1122
- // allowedCipherSuites: whitelist filter
1123
- if (context.allowedCipherSuites) {
1124
- ciphers = ciphers.filter(c => context.allowedCipherSuites.includes(c));
1125
- }
1539
+ ciphers = chooseCipherSuites(ciphers, options.prioritizeChaCha, context.allowedCipherSuites);
1126
1540
 
1127
1541
  // ALPN from options
1128
1542
  let alpns = Array.isArray(options.ALPNProtocols) ? options.ALPNProtocols : [];
@@ -1215,7 +1629,21 @@ function TLSSocket(duplex, options){
1215
1629
  if (corkable) t.uncork();
1216
1630
  }
1217
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
+ }
1218
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');
1219
1647
  self.emit('secureConnect');
1220
1648
  });
1221
1649
 
@@ -1238,8 +1666,7 @@ function TLSSocket(duplex, options){
1238
1666
  session.on('error', function(err) {
1239
1667
  markTransportDead();
1240
1668
  self.emit('error', err);
1241
- const t = context.transport;
1242
- if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
1669
+ closeAfterAlert(); // the session sent its alert just before reporting
1243
1670
  });
1244
1671
 
1245
1672
  // Forward handshakeMessage event (for debugging/inspection)
@@ -1248,11 +1675,17 @@ function TLSSocket(duplex, options){
1248
1675
  });
1249
1676
 
1250
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
+
1251
1684
  session.on('keylog', function(line) {
1252
1685
  self.emit('keylog', line);
1253
1686
  });
1254
1687
 
1255
- // Forward raw clienthello event (server-side, for JA3/inspection)
1688
+ // Forward raw clienthello event (server-side, for fingerprints/inspection)
1256
1689
  session.on('clienthello', function(raw, parsed) {
1257
1690
  self.emit('clienthello', raw, parsed);
1258
1691
  });
@@ -1312,24 +1745,16 @@ function TLSSocket(duplex, options){
1312
1745
  self.emit('certificateRequest', msg);
1313
1746
  });
1314
1747
 
1315
- // Server: automatic PSK handler (decrypts TLS 1.3 tickets with ticketKeys).
1316
- // 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.)
1317
1755
  if (options.isServer) {
1318
1756
  session.on('psk', function(info, callback) {
1319
- // First check if user has a custom handler on the socket
1320
- if (self.listenerCount('psk') > 0) {
1321
- // Let user handle it — pass through the same payload
1322
- self.emit('psk', info, callback);
1323
- return;
1324
- }
1325
-
1326
- // Auto-decrypt ticket using ticketKeys (unified format)
1327
- let state = decrypt_session_blob(info.identity, context.ticketKeys);
1328
- if (state && state.v === 13 && state.psk && state.cipher) {
1329
- callback({ psk: state.psk, cipher: state.cipher });
1330
- } else {
1331
- callback(null); // Decryption failed / not our ticket → full handshake
1332
- }
1757
+ if (self.listenerCount('psk') > 0) self.emit('psk', info, callback);
1333
1758
  });
1334
1759
  }
1335
1760
 
@@ -1354,6 +1779,65 @@ function TLSSocket(duplex, options){
1354
1779
  }
1355
1780
  });
1356
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
+
1357
1841
  // If duplex was passed in constructor, start reading
1358
1842
  if (context.transport) {
1359
1843
  bindTransport();
@@ -1400,9 +1884,11 @@ function TLSSocket(duplex, options){
1400
1884
  if (context.destroyed) { callback(new Error('Socket destroyed')); return; }
1401
1885
  const buf = toBuf(chunk);
1402
1886
 
1403
- if (!context.using_app_keys) {
1404
- // 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.
1405
1890
  context.appWriteQueue.push(buf);
1891
+ if (context.early.write) flushEarly();
1406
1892
  process.nextTick(callback);
1407
1893
  return;
1408
1894
  }
@@ -1448,8 +1934,9 @@ function TLSSocket(duplex, options){
1448
1934
  }
1449
1935
  }
1450
1936
 
1451
- if (!context.using_app_keys) {
1937
+ if (!context.using_app_keys && !context.early.srvWrite) {
1452
1938
  context.appWriteQueue.push(combined);
1939
+ if (context.early.write) flushEarly();
1453
1940
  process.nextTick(callback);
1454
1941
  return;
1455
1942
  }
@@ -1462,8 +1949,103 @@ function TLSSocket(duplex, options){
1462
1949
  }
1463
1950
  };
1464
1951
 
1465
- /** Duplex _read — data is pushed when received, no pull needed */
1466
- 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
+ }
1467
2049
 
1468
2050
  // === Node-compatible API ===
1469
2051
 
@@ -1475,22 +2057,64 @@ function TLSSocket(duplex, options){
1475
2057
  // Flush any pending handshake messages (e.g. ClientHello queued before transport was ready)
1476
2058
  while (context.pendingHandshake.length > 0) {
1477
2059
  let msg = context.pendingHandshake.shift();
1478
- writeRecord(msg.type, Buffer.from(msg.data));
2060
+ writeCleartextHandshake(msg.hsType, Buffer.from(msg.data));
1479
2061
  }
1480
2062
  };
1481
2063
 
1482
- /** 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
+ */
1483
2077
  self.end = (function(originalEnd) {
1484
2078
  return function(data, encoding, callback) {
1485
2079
  if (context.destroyed) return this;
1486
- session.close(); // sends close_notify alert
1487
- try { context.transport && context.transport.end && context.transport.end(); } catch(e){}
1488
2080
  return originalEnd.call(this, data, encoding, callback);
1489
2081
  };
1490
2082
  })(self.end);
1491
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
+
1492
2115
  self.destroy = (function(originalDestroy) {
1493
2116
  return function(err) {
2117
+ clearHandshakeTimer();
1494
2118
  if (context.destroyed) return this;
1495
2119
  context.destroyed = true;
1496
2120
  try { context.transport && context.transport.destroy && context.transport.destroy(); } catch(e){}
@@ -1534,6 +2158,9 @@ function TLSSocket(duplex, options){
1534
2158
  };
1535
2159
 
1536
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'; } });
1537
2164
  Object.defineProperty(self, 'alpnProtocol', {
1538
2165
  get: function(){ return session.getALPN() || false; },
1539
2166
  enumerable: true
@@ -1579,23 +2206,11 @@ function TLSSocket(duplex, options){
1579
2206
  self.getPeerCertificate = function(){
1580
2207
  let chain = session.getPeerCertificate();
1581
2208
  if (!chain || chain.length === 0) return null;
1582
- try {
1583
- let certDer = chain[0].cert;
1584
- let x509 = new crypto.X509Certificate(certDer);
1585
- return {
1586
- subject: x509.subject,
1587
- issuer: x509.issuer,
1588
- subjectaltname: x509.subjectAltName,
1589
- valid_from: x509.validFrom,
1590
- valid_to: x509.validTo,
1591
- fingerprint: x509.fingerprint,
1592
- fingerprint256: x509.fingerprint256,
1593
- serialNumber: x509.serialNumber,
1594
- raw: certDer
1595
- };
1596
- } catch(e) {
1597
- return { raw: chain[0].cert };
1598
- }
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);
1599
2214
  };
1600
2215
 
1601
2216
  /** Node-compat: return the peer's leaf cert as a native X509Certificate
@@ -1625,8 +2240,7 @@ function TLSSocket(duplex, options){
1625
2240
  * object (mirror of getPeerCertificate). Empty object {} if we didn't
1626
2241
  * present a cert — this matches Node's observed behavior. */
1627
2242
  self.getCertificate = function(){
1628
- let sctx = session.context;
1629
- let localChain = sctx && sctx.local_cert_chain;
2243
+ let localChain = session.getCertificate();
1630
2244
  if (!localChain || localChain.length === 0) return {};
1631
2245
  try {
1632
2246
  let der = localChain[0].cert || localChain[0];
@@ -1775,12 +2389,89 @@ function TLSSocket(duplex, options){
1775
2389
  enumerable: true
1776
2390
  });
1777
2391
 
1778
- /** JA3 fingerprint from ClientHello (server-side only). Returns { hash, raw } or null. */
1779
- 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
+ };
1780
2406
 
1781
2407
  /** ECDHE shared secret (Buffer), or null. For research/advanced use. */
1782
2408
  self.getSharedSecret = function(){ return session.getSharedSecret ? session.getSharedSecret() : null; };
1783
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
+
1784
2475
  /** Full negotiation result — all selected parameters in one object. */
1785
2476
  self.getNegotiationResult = function(){ return session.getNegotiationResult ? session.getNegotiationResult() : null; };
1786
2477