lemon-tls 0.9.0 → 0.11.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
@@ -3,26 +3,11 @@ import TLSSession from './tls_session.js';
3
3
  import crypto from 'node:crypto';
4
4
  import { Duplex } from 'node:stream';
5
5
 
6
- import { TLS_CIPHER_SUITES, default_cipher_suites } from './crypto.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
- import { default_signature_schemes } from './session/signing.js';
6
+ import { TLS_CIPHER_SUITES } from './crypto.js';
7
+ import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT, peer_alert_error, read_peer_alert, MAX_WARNING_ALERTS, MAX_RECORD_PLAINTEXT, local_alert_error, version_name } from './wire.js';
8
+ import { group_name as ecdh_group_name, dh_group_info } from './session/ecdh.js';
10
9
  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
- }
10
+ import { versionsInRange, localLists, pskOptions } from './node_options.js';
26
11
 
27
12
  import {
28
13
  getAeadAlgo,
@@ -46,8 +31,6 @@ import {
46
31
  // not for an upload — what does not fit goes after the handshake.
47
32
  const EARLY_DATA_MAX_TCP = 16384;
48
33
 
49
- // Handshake messages: refused on sight past HANDSHAKE_MESSAGE_MAX (wire.js) —
50
- // before their bytes are kept.
51
34
 
52
35
  // legacy_record_version (TLS 1.3 uses 0x0303 in record header)
53
36
  const REC_VERSION = 0x0303;
@@ -77,17 +60,7 @@ function sigalgCodeToName(code) {
77
60
  // ==== עזרי המרה ====
78
61
  function toBuf(u8){ return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8 || []); }
79
62
 
80
- function parseVersion(v) {
81
- if (typeof v === 'number') return v;
82
- if (typeof v === 'string') {
83
- let s = v.toUpperCase().replace(/[^0-9.]/g, '');
84
- if (s === '1.3' || s === '13') return 0x0304;
85
- if (s === '1.2' || s === '12') return 0x0303;
86
- if (s === '1.1' || s === '11') return 0x0302;
87
- if (s === '1.0' || s === '10') return 0x0301;
88
- }
89
- return null;
90
- }
63
+
91
64
 
92
65
 
93
66
 
@@ -103,6 +76,10 @@ function TLSSocket(duplex, options){
103
76
  if (!(this instanceof TLSSocket)) return new TLSSocket(duplex, options);
104
77
  options = options || {};
105
78
 
79
+ // The versions to offer (client) or take (server), newest first — checked
80
+ // here, at construction (node_options.js: one reading for TLS and DTLS).
81
+ const localVersions = versionsInRange(options.minVersion, options.maxVersion, false);
82
+
106
83
  // allowHalfOpen follows Node: false unless the caller asks otherwise, so
107
84
  // that when the peer stops writing our writable side ends too. This was
108
85
  // hardcoded true, which is the OPPOSITE of the documented default — code
@@ -160,8 +137,11 @@ function TLSSocket(duplex, options){
160
137
  // External transport (Duplex)
161
138
  transport: (duplex && typeof duplex.write === 'function') ? duplex : null,
162
139
 
163
- // Internal TLSSession
164
- session: new TLSSession({
140
+ // Internal TLSSession: every option goes on to it (as DTLSSession hands
141
+ // its own on), with what this socket settles itself over them. Picked one
142
+ // by one, an option the list did not name never reached the session —
143
+ // the class of bug that dropped DTLS's (r99) and record_size_limit here.
144
+ session: new TLSSession(Object.assign({}, options, {
165
145
  isServer: !!options.isServer,
166
146
  servername: options.servername,
167
147
  ALPNProtocols: options.ALPNProtocols || null,
@@ -183,21 +163,12 @@ function TLSSocket(duplex, options){
183
163
  ocspRequest: typeof options.ocspRequest === 'function' ? options.ocspRequest : null,
184
164
  requestOCSP: options.requestOCSP === true,
185
165
  session: options.session || null,
186
- psk: options.psk || null,
166
+ // External PSK, as node:tls takes it (pskOptions).
167
+ ...pskOptions(options, self),
187
168
  rejectUnauthorized: options.rejectUnauthorized,
188
169
  checkServerIdentity: options.checkServerIdentity,
189
170
  ALPNCallback: options.ALPNCallback || null,
190
171
  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,
201
172
  sessionTickets: options.sessionTickets,
202
173
  maxHandshakeSize: options.maxHandshakeSize || 0,
203
174
  customExtensions: options.customExtensions || [],
@@ -218,7 +189,7 @@ function TLSSocket(duplex, options){
218
189
  earlyDataContext: options.earlyDataContext,
219
190
  maxEarlyData: typeof options.maxEarlyData === 'number' ? options.maxEarlyData : EARLY_DATA_MAX_TCP,
220
191
  endOfEarlyData: true,
221
- }),
192
+ })),
222
193
 
223
194
  // Handshake write
224
195
  handshake_write_key: null,
@@ -274,6 +245,11 @@ function TLSSocket(duplex, options){
274
245
  // the records are counted — not in any one message handler.
275
246
  consecutive_empty_records: 0,
276
247
  consecutive_ccs_records: 0,
248
+ // Warning alerts with nothing else between them, and peer KeyUpdates
249
+ // with no application data between them: each is legal once, and each
250
+ // costs us work (a KeyUpdate a key derivation) while moving nothing.
251
+ consecutive_warning_alerts: 0,
252
+ consecutive_key_updates: 0,
277
253
 
278
254
  // Buffers and queues.
279
255
  //
@@ -306,7 +282,13 @@ function TLSSocket(duplex, options){
306
282
  // header. Without this, a record carrying EE+Cert+CV+Finished (as
307
283
  // OpenSSL/BoringSSL routinely pack) fed only its FIRST message to the
308
284
  // session and silently dropped the rest.
309
- hsBuf: null,
285
+ // A handshake message in pieces (feedHandshakeBytes): its header so far
286
+ // (under 4 bytes), then its pieces and their total.
287
+ hsHdr: null,
288
+ hsParts: null,
289
+ hsFill: 0,
290
+ hsLen: 0,
291
+ hsBufState: null, // the read state the pending bytes arrived under
310
292
 
311
293
  appWriteQueue: [],
312
294
 
@@ -329,11 +311,10 @@ function TLSSocket(duplex, options){
329
311
  ticketKeys: _ticketKeys,
330
312
 
331
313
  // Advanced options
332
- maxRecordSize: options.maxRecordSize || 16384,
314
+ maxRecordSize: Math.min(options.maxRecordSize || MAX_RECORD_PLAINTEXT, MAX_RECORD_PLAINTEXT),
333
315
  sessionTickets: options.sessionTickets !== false,
334
316
  pins: options.pins || null, // ['sha256/AAAA...'] certificate pinning
335
317
  handshakeTimeout: options.handshakeTimeout || 0, // ms, 0 = no timeout
336
- allowedCipherSuites: options.allowedCipherSuites || null, // [0x1301, ...] whitelist
337
318
  certificateCallback: options.certificateCallback || null, // (info, cb) => cb(null, ctx)
338
319
  handshakeTimer: null,
339
320
 
@@ -392,16 +373,17 @@ function TLSSocket(duplex, options){
392
373
  });
393
374
  }
394
375
 
395
- const MAX_RECORD_PLAINTEXT = 16384; // TLS max record size (2^14)
396
376
 
397
377
  // writeAppData / writeAppDataSingle return the transport's backpressure signal:
398
378
  // true → transport is ready for more writes
399
379
  // false → transport's internal buffer is full; caller should wait for 'drain'
400
380
  // on context.transport before writing more. Propagated up to _write so
401
381
  // user sock.write() returns false and Node's stream flow control kicks in.
382
+ // The most a record of ours may carry: 2^14, or the peer's
383
+ // record_size_limit (RFC 8449) when negotiated.
402
384
  function writeAppData(plain){
403
385
  const total = plain.length;
404
- const maxSize = context.maxRecordSize || MAX_RECORD_PLAINTEXT;
386
+ const maxSize = Math.min(context.maxRecordSize || MAX_RECORD_PLAINTEXT, session.recordSendLimit());
405
387
  if (total > maxSize) {
406
388
  // Fragment across multiple records. Cork+uncork lets Node batch the
407
389
  // individual writes into a single TCP send (fewer syscalls, less overhead).
@@ -491,147 +473,239 @@ function TLSSocket(duplex, options){
491
473
  }
492
474
  }
493
475
 
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){
498
- // Hot path — same optimizations as writeAppDataSingle:
499
- // - cached context.isTls13 instead of per-record session.getVersion()
500
- // - cached context.aeadAlgo instead of per-record getAeadAlgo()
501
- // - getTrafficSecrets() called once per key derivation (not per decrypt)
502
- // - For TLS 1.3 app-data: header view is used directly as AAD, avoiding
503
- // the 5-byte AAD allocation that decryptRecord would otherwise do
504
- let out = null;
505
- const isTls13 = context.isTls13 !== undefined
506
- ? context.isTls13
507
- : session.getVersion() === 0x0304; // early records (pre-secureConnect)
508
-
509
- // AEAD decryption can throw on any forged/corrupted record (GCM/ChaCha
510
- // tag mismatch surfaces as an exception from decipher.final()). Left
511
- // unhandled, a single injected bad segment would throw out of the
512
- // transport 'data' handler — an unhandled exception that tears down the
513
- // connection or the process: a trivial remote DoS.
514
- //
515
- // RFC 8446 §5.2: in TLS (unlike DTLS, which silently drops), a record
516
- // that fails to decrypt MUST be treated as a fatal error. So we catch,
517
- // send a fatal bad_record_mac alert, destroy the connection, and stop.
518
- // The read sequence number is intentionally NOT advanced on failure —
519
- // the rejected record never counts.
520
- try {
521
- if (!isTls13 && context.remote_ccs_seen === true) {
522
- // TLS 1.2: decrypt with key_block derived keys
523
- if (context.app_read_key === null) {
524
- const ts = session.getTrafficSecrets();
476
+ // ============================================================================
477
+ // Reading records: one state, one set of rules
478
+ // ============================================================================
479
+ //
480
+ // What protects the next record from the peer is one of four states, and
481
+ // every rule about what a record may be follows from it — rather than from
482
+ // flags tested here and there (remote_ccs_seen, using_app_keys), which is
483
+ // how a plaintext Finished came to be read after the ServerHello, how a
484
+ // TLS 1.2 record's type came to be guessed instead of authenticated, and
485
+ // how application data came to be taken before the handshake was done.
486
+ //
487
+ // 'plain' — nothing yet: before the TLS 1.3 handshake keys (or before the
488
+ // version is known); a TLS 1.2 handshake before the peer's
489
+ // ChangeCipherSpec.
490
+ // 'hs13' — TLS 1.3 under the handshake traffic keys: after the
491
+ // ServerHello, until the peer's Finished.
492
+ // 'app13' — TLS 1.3 under the application traffic keys.
493
+ // 'tls12' — TLS 1.2 after the peer's ChangeCipherSpec: every record
494
+ // protected, its type authenticated (it is in the AAD).
495
+ function negotiated13() {
496
+ if (context.isTls13 !== undefined) return context.isTls13;
497
+ const v = session.getVersion();
498
+ return v === 0x0304 ? true : v === 0x0303 ? false : undefined;
499
+ }
500
+ // The peer's Finished, verified: from here it writes under its
501
+ // application keys (TLS 1.3), and what it sends is application data it
502
+ // may send (both). Not using_app_keys, which flips at 'secureConnect' —
503
+ // later, for a server (its ticket goes first): a record in between was
504
+ // read under the wrong keys, and a CCS there passed as one before the
505
+ // Finished.
506
+ function peerFinished() {
507
+ return context.using_app_keys === true || (session.context && session.context.remote_finished_ok === true);
508
+ }
509
+ function readState() {
510
+ const v13 = negotiated13();
511
+ if (v13 === false) return context.remote_ccs_seen ? 'tls12' : 'plain';
512
+ if (v13 === true) {
513
+ if (peerFinished()) return 'app13';
514
+ const hs = session.getHandshakeSecrets();
515
+ if (hs && hs.remoteSecret) return 'hs13';
516
+ }
517
+ return 'plain';
518
+ }
519
+
520
+ // Every fatal condition of the record layer ends the same way: our alert,
521
+ // both layers inert, the transport ended after the alert has gone (end(),
522
+ // which also uncorks the flight the alert sits in — destroy() dropped it:
523
+ // 336 of 338 bad_record_mac alerts were never sent), then the report.
524
+ function abort(alert, message, code) {
525
+ if (context.destroyed) return;
526
+ const err = local_alert_error(alert, message, code);
527
+ try { session.sendAlert(2, alert); } catch (e) {}
528
+ markTransportDead();
529
+ closeAfterAlert();
530
+ self.emit('error', err);
531
+ }
525
532
 
526
- // Guard: if master_secret isn't derived yet, we can't compute keys.
527
- if (!ts.masterSecret) {
528
- self.emit('error', new Error('Received encrypted record before master_secret derived'));
533
+ // A record's plaintext, by its content type — the decrypted one, or a
534
+ // cleartext record's own (`clear`). Content longer than a record carries
535
+ // is record_overflow (RFC 8446 §5.4, RFC 5246 §6.2.1); a handshake or
536
+ // alert fragment of zero bytes is not one (§5.1).
537
+ function dispatchContent(contentType, content, state, clear) {
538
+ if (content.length > MAX_RECORD_PLAINTEXT) {
539
+ abort(TLS_ALERT.RECORD_OVERFLOW, 'record of ' + content.length + ' bytes of plaintext, more than 2^14', 'ERR_SSL_RECORD_OVERFLOW');
540
+ return;
541
+ }
542
+ // Ours, once negotiated (RFC 8449 §4: "MUST generate a fatal
543
+ // record_overflow alert") — on protected records: "Unprotected
544
+ // messages are not subject to this limit" (a TLS 1.2 Certificate in
545
+ // the clear, in full records, was refused). As DTLS.
546
+ const rsl = clear ? null : session.getRecordSizeLimit();
547
+ if (rsl && content.length > rsl.receive) {
548
+ abort(TLS_ALERT.RECORD_OVERFLOW, 'record of ' + content.length + ' bytes of plaintext, more than our record_size_limit (' + rsl.receive + ')', 'ERR_SSL_RECORD_OVERFLOW');
549
+ return;
550
+ }
551
+ if (contentType === CT.APPLICATION_DATA) {
552
+ // Only once the handshake is done: under TLS 1.3's application
553
+ // keys; in TLS 1.2 after the peer's Finished.
554
+ // Before that, it was handed to the reader all the same.
555
+ if (state === 'app13' || (state === 'tls12' && peerFinished())) {
556
+ // In the middle of a handshake message (a post-handshake one
557
+ // in pieces): "MUST NOT be interleaved with other record
558
+ // types" (RFC 8446 §5.1).
559
+ if (hsPending()) {
560
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'application data inside a handshake message', 'ERR_SSL_UNEXPECTED_MESSAGE');
529
561
  return;
530
562
  }
531
-
532
- const d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, ts.cipher, ts.isServer);
533
- context.app_read_key = d12.readKey;
534
- context.app_read_iv = d12.readIv;
535
- context.app_write_key = d12.writeKey;
536
- context.app_write_iv = d12.writeIv;
563
+ if (content.length > 0) {
564
+ context.consecutive_key_updates = 0;
565
+ context.consecutive_warning_alerts = 0;
566
+ pushAppData(content);
567
+ }
568
+ return;
537
569
  }
538
-
539
- const recordType = context.using_app_keys ? 0x17 : 0x16;
540
- out = decrypt_tls12_fragment(body, context.app_read_key, context.app_read_iv, context.tls12_read_seq, recordType);
541
-
542
- } else if (isTls13 && context.using_app_keys === true) {
543
- // TLS 1.3: decrypt app data with app traffic secret
544
- if (context.app_read_key === null) {
545
- const ts = session.getTrafficSecrets();
546
- if (ts.remoteAppSecret === null) return;
547
- const d = tls_derive_from_tls_secrets(ts.remoteAppSecret, ts.cipher);
548
- context.app_read_key = d.key;
549
- context.app_read_iv = d.iv;
570
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'application data before the handshake is complete', 'ERR_SSL_UNEXPECTED_MESSAGE');
571
+ return;
572
+ }
573
+ if (contentType === CT.HANDSHAKE) {
574
+ if (content.length === 0) { abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'empty handshake record', 'ERR_SSL_UNEXPECTED_MESSAGE'); return; }
575
+ context.consecutive_warning_alerts = 0;
576
+ // RFC 8446 §5.1: "Handshake messages MUST NOT span key changes"
577
+ // — a message begun under one read state and continued under
578
+ // another (RFC 5246's CCS has its own check, handleChangeCipherSpec).
579
+ if (hsPending() && context.hsBufState !== state) {
580
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'a handshake message spans a key change', 'ERR_SSL_UNEXPECTED_MESSAGE');
581
+ return;
550
582
  }
583
+ context.hsBufState = state;
584
+ feedHandshakeBytes(content);
585
+ return;
586
+ }
587
+ if (contentType === CT.ALERT) { handleAlert(content); return; }
588
+ // Anything else — a ChangeCipherSpec inside protection (§5: "MUST
589
+ // abort"), content type 0, an unknown one: unexpected_message.
590
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'unexpected content type ' + contentType + ' in a ' + state + ' record', 'ERR_SSL_UNEXPECTED_MESSAGE');
591
+ }
551
592
 
593
+ // TLS 1.3 protected record (outer type application_data), under the keys
594
+ // of `state`. trial: early data not taken may come before the client's
595
+ // handshake flight (RFC 8446 §4.2.10): a record the handshake key does not
596
+ // open is that, and is skipped (false) rather than fatal.
597
+ function openRecord13(body, header, state, trial) {
598
+ let out;
599
+ try {
552
600
  const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
553
- // Use header-view AAD path when header is available (parseRecordsAndDispatch).
554
- // Saves a 5-byte allocation per record — small individually but meaningful
555
- // cumulatively on the Download hot path (many records per second).
556
- // get_nonce_into also reuses context._nonceDecScratch to avoid alloc per record.
557
- const nonce = get_nonce_into(context._nonceDecScratch, context.app_read_iv, context.app_read_seq);
558
- if (header !== undefined) {
559
- out = decrypt_tls_record_with_aad(header, body, context.app_read_key, nonce, algo);
601
+ if (state === 'app13') {
602
+ if (context.app_read_key === null) {
603
+ const ts = session.getTrafficSecrets();
604
+ if (ts.remoteAppSecret === null) throw new Error('no application read key');
605
+ const d = tls_derive_from_tls_secrets(ts.remoteAppSecret, ts.cipher);
606
+ context.app_read_key = d.key;
607
+ context.app_read_iv = d.iv;
608
+ }
609
+ out = decrypt_tls_record_with_aad(header, body, context.app_read_key,
610
+ get_nonce_into(context._nonceDecScratch, context.app_read_iv, context.app_read_seq), algo);
611
+ context.app_read_seq++;
560
612
  } else {
561
- out = decrypt_tls_record(body, context.app_read_key, nonce, algo);
562
- }
563
- context.app_read_seq++;
564
-
565
- } else if (isTls13) {
566
- // TLS 1.3: decrypt handshake with handshake traffic secret
567
- if (context.handshake_read_key === null) {
568
- const hs = session.getHandshakeSecrets();
569
- if (hs.remoteSecret === null || hs.cipher === null) return;
570
- const d = tls_derive_from_tls_secrets(hs.remoteSecret, hs.cipher);
571
- context.handshake_read_key = d.key;
572
- context.handshake_read_iv = d.iv;
613
+ if (context.handshake_read_key === null) {
614
+ const hs = session.getHandshakeSecrets();
615
+ if (hs.remoteSecret === null || hs.cipher === null) throw new Error('no handshake read key');
616
+ const d = tls_derive_from_tls_secrets(hs.remoteSecret, hs.cipher);
617
+ context.handshake_read_key = d.key;
618
+ context.handshake_read_iv = d.iv;
619
+ }
620
+ out = decrypt_tls_record_with_aad(header, body, context.handshake_read_key,
621
+ get_nonce_into(context._nonceDecScratch, context.handshake_read_iv, context.handshake_read_seq), algo);
622
+ context.handshake_read_seq++;
573
623
  }
624
+ } catch (e) {
625
+ // RFC 8446 §5.2: a record that does not open is fatal. The read
626
+ // sequence is not advanced — the rejected record never counts.
627
+ if (trial) return false;
628
+ abort(TLS_ALERT.BAD_RECORD_MAC, 'TLS record decryption failed (bad_record_mac)', 'ERR_SSL_BAD_RECORD_MAC');
629
+ return false;
630
+ }
631
+ if (trial) context.early.trialDone = true;
632
+ // TLSInnerPlaintext — content, type and padding together — is at
633
+ // most 2^14 + 1 bytes (RFC 8446 §5.4: "MUST NOT exceed"). Checked on
634
+ // the whole, not on the content once the padding is gone: 2^14 − 8
635
+ // bytes with 9 of padding passed.
636
+ if (out.length > MAX_RECORD_PLAINTEXT + 1) {
637
+ abort(TLS_ALERT.RECORD_OVERFLOW, 'TLSInnerPlaintext of ' + out.length + ' bytes, more than 2^14 + 1', 'ERR_SSL_RECORD_OVERFLOW');
638
+ return true;
639
+ }
640
+ // TLSInnerPlaintext: content, the type, then zeros. All zeros: no
641
+ // type at all (§5.4: unexpected_message) — it was an 'error' event
642
+ // and a connection left open.
643
+ let j = out.length - 1;
644
+ while (j >= 0 && out[j] === 0) j--;
645
+ if (j < 0) {
646
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'TLSInnerPlaintext with no content type', 'ERR_SSL_UNEXPECTED_MESSAGE');
647
+ return true;
648
+ }
649
+ dispatchContent(out[j], out.subarray(0, j), state);
650
+ return true;
651
+ }
574
652
 
575
- const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
576
- const nonce = get_nonce_into(context._nonceDecScratch, context.handshake_read_iv, context.handshake_read_seq);
577
- if (header !== undefined) {
578
- out = decrypt_tls_record_with_aad(header, body, context.handshake_read_key, nonce, algo);
579
- } else {
580
- out = decrypt_tls_record(body, context.handshake_read_key, nonce, algo);
653
+ // TLS 1.2 protected record: its type — the header's — is part of the AAD
654
+ // (RFC 5246 §6.2.3.3). It was guessed from the state (handshake until the
655
+ // handshake was done, application data after), so a record relabelled
656
+ // from one to the other on the way still opened; and an encrypted alert
657
+ // was read as a cleartext one.
658
+ function openRecord12(type, body) {
659
+ let out;
660
+ try {
661
+ if (context.app_read_key === null) {
662
+ const ts = session.getTrafficSecrets();
663
+ if (!ts.masterSecret) throw new Error('no master secret');
664
+ const d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, ts.cipher, ts.isServer);
665
+ context.app_read_key = d12.readKey;
666
+ context.app_read_iv = d12.readIv;
667
+ context.app_write_key = d12.writeKey;
668
+ context.app_write_iv = d12.writeIv;
581
669
  }
582
- context.handshake_read_seq++;
583
- }
670
+ out = decrypt_tls12_fragment(body, context.app_read_key, context.app_read_iv, context.tls12_read_seq, type);
671
+ context.tls12_read_seq++;
584
672
  } catch (e) {
585
- if (trial) return false;
586
- // Record failed authentication → fatal bad_record_mac (RFC 8446 §5.2).
587
- try { session.sendAlert ? session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC) : null; } catch (_) {}
588
- self.emit('error', new Error('TLS record decryption failed (bad_record_mac)'));
589
- self.destroy();
673
+ abort(TLS_ALERT.BAD_RECORD_MAC, 'TLS record decryption failed (bad_record_mac)', 'ERR_SSL_BAD_RECORD_MAC');
590
674
  return;
591
675
  }
676
+ dispatchContent(type, out, 'tls12');
677
+ }
592
678
 
593
-
594
- if (out !== null) {
595
- // Reuse cached isTls13 computed at the top of this function
596
- if (isTls13) {
597
- // Inlined parseInnerPlaintext — parse_tls_inner_plaintext would
598
- // allocate a {type, content} object per record. With 640 records
599
- // per 10MB download, that's 640 short-lived objects just for
600
- // destructuring — enough GC pressure to matter. We do the scan
601
- // and dispatch directly here.
602
- let j = out.length - 1;
603
- while (j >= 0 && out[j] === 0) j--;
604
- if (j < 0) {
605
- self.emit('error', new Error('Malformed TLSInnerPlaintext'));
606
- return;
607
- }
608
- const content_type = out[j];
609
-
610
- if (content_type === CT.APPLICATION_DATA) {
611
- // zero-copy view into the decrypted plaintext
612
- pushAppData(out.subarray(0, j));
613
- } else if (content_type === CT.HANDSHAKE) {
614
- feedHandshakeBytes(out.subarray(0, j));
615
- } else if (content_type === CT.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));
620
- }
621
- } else {
622
- // TLS 1.2: no inner plaintext wrapping — `out` is the Buffer from decrypt12
623
- if (context.using_app_keys === true) {
624
- pushAppData(out);
625
- } else {
626
- feedHandshakeBytes(out);
627
- }
679
+ // ChangeCipherSpec: one byte, 1 (RFC 8446 §5, RFC 5246 §7.1).
680
+ // TLS 1.2: once, when the peer's keys exist (the master secret is
681
+ // derived) and no handshake message is cut by it (§5.1 of 8446 holds
682
+ // the same for 1.2: messages do not straddle a key change).
683
+ // TLS 1.3: a middlebox-compatibility record, dropped — "at any time
684
+ // after the first ClientHello ... and before the peer's Finished"
685
+ // (§5); not before the version is known, not after the Finished.
686
+ // Anything else: unexpected_message. A CCS of two bytes, or one after the
687
+ // Finished, used to be passed over in silence.
688
+ function handleChangeCipherSpec(body, state) {
689
+ if (body.length !== 1 || body[0] !== 0x01) {
690
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'malformed ChangeCipherSpec (' + body.length + ' bytes)', 'ERR_SSL_UNEXPECTED_MESSAGE');
691
+ return;
692
+ }
693
+ const v13 = negotiated13();
694
+ if (v13 === true) {
695
+ if (state === 'app13') abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'ChangeCipherSpec after the Finished', 'ERR_SSL_UNEXPECTED_MESSAGE');
696
+ return; // compatibility record: dropped, no sequence advanced
697
+ }
698
+ if (v13 === false && state === 'plain') {
699
+ const ts = session.getTrafficSecrets();
700
+ if (ts && ts.masterSecret && !hsPending()) {
701
+ context.tls12_read_seq = 0;
702
+ context.remote_ccs_seen = true;
703
+ return;
628
704
  }
629
705
  }
630
- if (trial && out !== null) context.early.trialDone = true;
631
- return out !== null;
706
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'unexpected ChangeCipherSpec', 'ERR_SSL_UNEXPECTED_MESSAGE');
632
707
  }
633
708
 
634
-
635
709
  // ============================================================================
636
710
  // Early data (0-RTT over TCP, RFC 8446 §4.2.10, §4.5)
637
711
  // ============================================================================
@@ -670,10 +744,7 @@ function TLSSocket(duplex, options){
670
744
  }
671
745
 
672
746
  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();
747
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'More early data than max_early_data_size', 'ERR_SSL_UNEXPECTED_MESSAGE');
677
748
  }
678
749
 
679
750
  // A record of early data not taken: skipped, counted (its plaintext at
@@ -690,24 +761,23 @@ function TLSSocket(duplex, options){
690
761
  try {
691
762
  out = decrypt_tls_record_with_aad(header, body, r.key, get_nonce_into(context._nonceDecScratch, r.iv, r.seq), r.algo);
692
763
  } 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();
764
+ abort(TLS_ALERT.BAD_RECORD_MAC, 'TLS early data record decryption failed (bad_record_mac)', 'ERR_SSL_BAD_RECORD_MAC');
696
765
  return;
697
766
  }
698
767
  r.seq++;
699
768
  let j = out.length - 1;
700
769
  while (j >= 0 && out[j] === 0) j--;
701
- if (j < 0) { self.emit('error', new Error('Malformed TLSInnerPlaintext')); return; }
770
+ if (j < 0) { abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'TLSInnerPlaintext with no content type', 'ERR_SSL_UNEXPECTED_MESSAGE'); return; }
702
771
  const ct = out[j];
703
772
  if (ct === CT.APPLICATION_DATA) {
773
+ if (j > MAX_RECORD_PLAINTEXT) { abort(TLS_ALERT.RECORD_OVERFLOW, 'early data record of ' + j + ' bytes, more than 2^14', 'ERR_SSL_RECORD_OVERFLOW'); return; }
704
774
  context.early.recv += j;
705
775
  if (context.early.recv > earlyLimit()) { earlyTooMuch(); return; }
706
776
  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));
777
+ } else {
778
+ // EndOfEarlyData (the session checks it is that), an alert, or
779
+ // anything else — the same rules as any other record.
780
+ dispatchContent(ct, out.subarray(0, j), 'hs13');
711
781
  }
712
782
  }
713
783
 
@@ -830,6 +900,35 @@ function TLSSocket(duplex, options){
830
900
  context.readEnd = needed;
831
901
  }
832
902
 
903
+ // A handshake message, protected: in its records (handshakeFragments),
904
+ // each sealed and framed in one step (as writeAppDataSingle) under the
905
+ // keys named — TLS 1.3's (handshake or application) or 1.2's.
906
+ function writeProtectedHandshake(buf, v13, keyField, ivField, seqField) {
907
+ const algo = v13 ? (context.aeadAlgo || getAeadAlgo(session.getCipher())) : null;
908
+ for (const part of handshakeFragments(buf)) {
909
+ const rec = v13
910
+ ? encryptCompleteRecord13(CT.HANDSHAKE, part, context[keyField],
911
+ get_nonce_into(context._nonceEncScratch, context[ivField], context[seqField]), algo, context.rec_version)
912
+ : encryptCompleteRecord12(part, context[keyField], context[ivField], context[seqField], CT.HANDSHAKE, context.rec_version);
913
+ context[seqField]++;
914
+ if (canWrite()) context.transport.write(rec);
915
+ }
916
+ }
917
+
918
+ // A handshake message, in records of at most 2^14 bytes (RFC 8446 §5.1,
919
+ // RFC 5246 §6.2.1: "MUST NOT exceed 2^14 bytes") — and of at most the
920
+ // peer's record_size_limit, once it is known. It went in one record
921
+ // however long: a chain over 16 KB was refused by OpenSSL (record
922
+ // overflow), and over 64 KB the record's 16-bit length wrapped — the
923
+ // stream after it unreadable. Post-quantum (ML-DSA) chains reach both.
924
+ function handshakeFragments(buf) {
925
+ const max = session.recordSendLimit();
926
+ if (buf.length <= max) return [buf];
927
+ let out = [];
928
+ for (let o = 0; o < buf.length; o += max) out.push(buf.subarray(o, Math.min(o + max, buf.length)));
929
+ return out;
930
+ }
931
+
833
932
  /**
834
933
  * Handshake message reassembly (RFC 8446 §5.1).
835
934
  *
@@ -842,38 +941,63 @@ function TLSSocket(duplex, options){
842
941
  *
843
942
  * Note on memory: session.message() stashes views of the fed bytes into
844
943
  * its transcript, so every message handed over must be backed by memory
845
- * we never overwrite. Buffer.from()/Buffer.concat() below always copy out
846
- * of the (reused) record buffers; the carve-out subarrays then alias the
847
- * fresh copy, which nothing mutates.
944
+ * we never overwrite: each is copied out of the (reused) record buffers.
945
+ *
946
+ * Linear in the message: its pieces are kept as they come and joined
947
+ * once, when the last arrives. The buffer was joined anew with every
948
+ * record — a 128 KB ClientHello in 1-byte records cost 1.9 s of CPU and
949
+ * 200 MB, before anything about it was checked.
848
950
  */
951
+ function hsPending() { return context.hsHdr !== null || context.hsParts !== null; }
952
+ function hsReset() { context.hsHdr = null; context.hsParts = null; context.hsFill = 0; context.hsLen = 0; }
849
953
  function feedHandshakeBytes(bytes){
850
- if (context.hsBuf === null || context.hsBuf.length === 0) {
851
- context.hsBuf = Buffer.from(bytes);
852
- } else {
853
- context.hsBuf = Buffer.concat([context.hsBuf, Buffer.from(bytes)]);
854
- }
855
-
856
- while (context.hsBuf.length >= 4) {
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;
954
+ let p = 0;
955
+ while (p < bytes.length) {
956
+ if (context.hsParts === null) {
957
+ // The header: 4 bytes, maybe across records.
958
+ let have = context.hsHdr === null ? 0 : context.hsHdr.length;
959
+ let take = Math.min(4 - have, bytes.length - p);
960
+ let hdr = have === 0 ? Buffer.from(bytes.subarray(p, p + take))
961
+ : Buffer.concat([context.hsHdr, Buffer.from(bytes.subarray(p, p + take))]);
962
+ p += take;
963
+ if (hdr.length < 4) { context.hsHdr = hdr; return; }
964
+ context.hsHdr = null;
965
+ const len = (hdr[1] << 16) | (hdr[2] << 8) | hdr[3];
966
+ const maxMsg = session.handshakeMessageLimit();
967
+ if (len > maxMsg) {
968
+ // OpenSSL's answer to an excessive message: illegal_parameter.
969
+ hsReset();
970
+ abort(TLS_ALERT.ILLEGAL_PARAMETER, 'TLS handshake message of ' + len + ' bytes, more than ' + maxMsg);
971
+ return;
972
+ }
973
+ context.hsParts = [hdr]; context.hsFill = 4; context.hsLen = 4 + len;
974
+ }
975
+ let take = Math.min(context.hsLen - context.hsFill, bytes.length - p);
976
+ if (take > 0) {
977
+ context.hsParts.push(Buffer.from(bytes.subarray(p, p + take)));
978
+ context.hsFill += take; p += take;
866
979
  }
867
- if (context.hsBuf.length < 4 + len) break; // partial message — wait for more records
980
+ if (context.hsFill < context.hsLen) return; // partial message — wait for more records
868
981
 
869
- const msg = context.hsBuf.subarray(0, 4 + len);
870
- context.hsBuf = context.hsBuf.subarray(4 + len);
982
+ const msg = context.hsParts.length === 1 ? context.hsParts[0] : Buffer.concat(context.hsParts, context.hsLen);
983
+ hsReset();
871
984
 
872
985
  session.message(msg);
873
986
 
874
987
  // A message may have fatally ended the session (alert sent,
875
988
  // transport torn down). Don't keep feeding a corpse.
876
989
  if (context.destroyed) return;
990
+
991
+ // The message changed the read keys (a ServerHello, a Finished, a
992
+ // KeyUpdate): what follows it in the same record was protected
993
+ // under the old ones, and is no message of the new state (RFC
994
+ // 8446 §5.1: "MUST align with a record boundary"). A KeyUpdate
995
+ // leaves the read state's name as it was ('app13'), so it is
996
+ // named here: two in one record were taken.
997
+ if (p < bytes.length && (readState() !== context.hsBufState || msg[0] === 24)) {
998
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'handshake data after a key change, in the record before it', 'ERR_SSL_UNEXPECTED_MESSAGE');
999
+ return;
1000
+ }
877
1001
  }
878
1002
  }
879
1003
 
@@ -923,25 +1047,25 @@ function TLSSocket(duplex, options){
923
1047
  // in silence; OpenSSL rejects on the header ("wrong version
924
1048
  // number"), and so does this now, with node:tls's codes.
925
1049
  // 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).
1050
+ // one of the four. The length, by the state the record is read
1051
+ // in: a cleartext record carries at most 2^14 bytes; a TLS 1.3
1052
+ // protected one 2^14 + 256 (§5.2) — early data, still before
1053
+ // the handshake keys, is one; a TLS 1.2 one 2^14 + 2048
1054
+ // (RFC 5246 §6.2.3). It was 2^14 + 2048 for all.
1055
+ const state = readState();
928
1056
  {
929
1057
  const recVer = (rb[off + 1] << 8) | rb[off + 2];
930
- let bad = null, alert = 0;
1058
+ const maxLen = state === 'tls12' ? MAX_RECORD_PLAINTEXT + 2048
1059
+ : (state === 'hs13' || state === 'app13' || type === CT.APPLICATION_DATA) ? MAX_RECORD_PLAINTEXT + 256
1060
+ : MAX_RECORD_PLAINTEXT;
931
1061
  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;
1062
+ abort((recVer >>> 8) !== 0x03 ? TLS_ALERT.PROTOCOL_VERSION : TLS_ALERT.UNEXPECTED_MESSAGE,
1063
+ 'wrong version number: not a TLS record (type 0x' + type.toString(16) + ', version 0x' + recVer.toString(16) + ')',
1064
+ 'ERR_SSL_WRONG_VERSION_NUMBER');
1065
+ return;
939
1066
  }
940
- if (bad) {
941
- try { session.sendAlert(2, alert); } catch (e) {}
942
- self.emit('error', bad);
943
- markTransportDead();
944
- closeAfterAlert();
1067
+ if (len > maxLen) {
1068
+ abort(TLS_ALERT.RECORD_OVERFLOW, 'record overflow: ' + len + ' bytes, more than ' + maxLen, 'ERR_SSL_RECORD_OVERFLOW');
945
1069
  return;
946
1070
  }
947
1071
  }
@@ -959,86 +1083,57 @@ function TLSSocket(duplex, options){
959
1083
  // CPU (BoGo: TooManyEmptyFragments / TooManyChangeCipherSpec).
960
1084
  if (len === 0) {
961
1085
  if (++context.consecutive_empty_records > 32) {
962
- try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
963
- self.emit('error', new Error('Too many consecutive empty records'));
964
- markTransportDead();
965
- closeAfterAlert();
1086
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'Too many consecutive empty records');
966
1087
  return;
967
1088
  }
968
1089
  } else {
969
1090
  context.consecutive_empty_records = 0;
970
1091
  }
971
-
972
1092
  if (type === CT.CHANGE_CIPHER_SPEC) {
973
1093
  if (++context.consecutive_ccs_records > 8) {
974
- try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
975
- self.emit('error', new Error('Too many consecutive ChangeCipherSpec records'));
976
- markTransportDead();
977
- closeAfterAlert();
1094
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'Too many consecutive ChangeCipherSpec records');
978
1095
  return;
979
1096
  }
980
1097
  } else {
981
1098
  context.consecutive_ccs_records = 0;
982
1099
  }
983
1100
 
984
- if (type === CT.APPLICATION_DATA && session.isServer && !context.using_app_keys && context.isTls13 !== false) {
1101
+ // Early data, at a TLS 1.3 server that has not yet the client's
1102
+ // Finished: its own rules (see "Early data").
1103
+ if (type === CT.APPLICATION_DATA && session.isServer && state !== 'app13' && state !== 'tls12' && negotiated13() !== false) {
985
1104
  const em = serverEarlyMode();
986
1105
  if (em === 'pending') { off -= 5 + len; break; } // the decision first; this record waits
987
1106
  if (em === 'skip') { if (!skipEarly(len)) return; continue; }
988
1107
  if (em === 'accepted') { processEarlyRecord(body, header); if (context.destroyed) return; continue; }
989
- if (em === 'trial') { if (!processCiphertext(body, header, true) && !skipEarly(len)) return; continue; }
1108
+ if (em === 'trial' && state === 'hs13') { if (!openRecord13(body, header, state, true) && !skipEarly(len)) return; continue; }
990
1109
  }
991
- if (type === CT.APPLICATION_DATA) {
992
- processCiphertext(body, header);
993
- context.tls12_read_seq++;
994
- } else if (type === CT.HANDSHAKE) {
995
- if (context.remote_ccs_seen === true) {
996
- // Encrypted handshake (TLS 1.2 Finished post-CCS). decrypt output is
997
- // a FRESH Buffer per record — safe to pass views of it downstream.
998
- processCiphertext(body, header);
1110
+
1111
+ if (type === CT.CHANGE_CIPHER_SPEC) {
1112
+ handleChangeCipherSpec(body, state);
1113
+ } else if (state === 'tls12') {
1114
+ // Everything after the peer's CCS is protected, its type
1115
+ // authenticated with it.
1116
+ openRecord12(type, body);
1117
+ } else if (state === 'hs13' || state === 'app13') {
1118
+ if (type === CT.APPLICATION_DATA) {
1119
+ openRecord13(body, header, state, false);
1120
+ } else if (type === CT.ALERT && state === 'hs13') {
1121
+ // A peer that cannot yet protect its alert — a client
1122
+ // refusing the ServerHello before it has the keys.
1123
+ dispatchContent(CT.ALERT, body, state, true);
999
1124
  } else {
1000
- // Plaintext handshake (ClientHello, ServerHello, Certificate, ...).
1001
- // `body` is a zero-copy view into readBuffer. session.message()
1002
- // stashes the raw bytes into the handshake transcript (used later
1003
- // for Finished MAC verification and TLS 1.3 key derivation), and
1004
- // also extracts fields like remote_random as subarray views.
1005
- //
1006
- // When the NEXT incoming chunk triggers readBuffer compact/reuse,
1007
- // those stashed views would point to overwritten bytes — causing
1008
- // bad key derivations and GCM tag failures much later. We must hand
1009
- // the session an owned Buffer so its transcript survives.
1010
- feedHandshakeBytes(body);
1125
+ // A cleartext handshake record — or alert — where only
1126
+ // protected records may be (RFC 8446 §5: unexpected_message).
1127
+ // A cleartext Finished was taken here, and a KeyUpdate
1128
+ // injected on the path was acted on.
1129
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'unprotected record (type ' + type + ') after the handshake keys', 'ERR_SSL_UNEXPECTED_MESSAGE');
1011
1130
  }
1012
- context.tls12_read_seq++;
1013
- } else if (type === CT.CHANGE_CIPHER_SPEC) {
1014
- // Version-dependent meaning:
1015
- // - TLS 1.2: a real protocol event — the peer's next records
1016
- // are encrypted. Flip remote_ccs_seen so type-22 records are
1017
- // routed to the decrypt path.
1018
- // - TLS 1.3: a middlebox-compat artifact (RFC 8446 §D.4)
1019
- // carrying no meaning; cipher state is driven by the key
1020
- // schedule. It MUST be ignored and MUST NOT advance any
1021
- // read sequence.
1022
- //
1023
- // The test is "do we POSITIVELY know this is TLS 1.2", not
1024
- // "is it not 1.3", and that is the correct test on its own
1025
- // merits: a CCS carries no meaning unless we have actually
1026
- // negotiated 1.2, so an unknown version must not be treated as
1027
- // a cipher change. (It also used to be load-bearing for a
1028
- // second reason — the HelloRetryRequest path did not record
1029
- // selected_version, so mid-HRR the version was simply unknown.
1030
- // That gap is fixed at its source now, but the rule stands.)
1031
- const ccsVersion = (context.isTls13 !== undefined)
1032
- ? (context.isTls13 ? 0x0304 : 0x0303)
1033
- : session.getVersion();
1034
- if (ccsVersion === 0x0303) {
1035
- context.tls12_read_seq = 0;
1036
- context.remote_ccs_seen = true;
1037
- }
1038
- // TLS 1.3, or version not yet negotiated: swallow silently.
1039
- } else if (type === CT.ALERT) {
1040
- handleAlert(body);
1131
+ } else {
1132
+ // 'plain': handshake and alerts in the clear; application data
1133
+ // has no place before the keys.
1134
+ dispatchContent(type, body, state, true);
1041
1135
  }
1136
+ if (context.destroyed) return;
1042
1137
  }
1043
1138
 
1044
1139
  // Reset offsets when buffer is fully drained (enables fast path on next chunk)
@@ -1108,10 +1203,16 @@ function TLSSocket(duplex, options){
1108
1203
  // certificate") — without it the socket just closed and nothing learned
1109
1204
  // why: a server could not see that clients were refusing its certificate.
1110
1205
  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 });
1206
+ const a = read_peer_alert(body, negotiated13() === true);
1207
+ if (a.error) { abort(a.error.alert, a.error.message); return; }
1208
+ const desc = a.desc;
1209
+ const level = a.fatal ? TLS_ALERT_LEVEL.FATAL : TLS_ALERT_LEVEL.WARNING;
1210
+ self.emit('alert', { level: a.level, description: desc });
1211
+ // A run of warnings, nothing else between them.
1212
+ if (!a.fatal && desc !== TLS_ALERT.CLOSE_NOTIFY && ++context.consecutive_warning_alerts > MAX_WARNING_ALERTS) {
1213
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'too many consecutive warning alerts', 'ERR_SSL_UNEXPECTED_MESSAGE');
1214
+ return;
1215
+ }
1115
1216
  if (desc === TLS_ALERT.CLOSE_NOTIFY) {
1116
1217
  // The peer is done sending: end our readable side, so 'end' is
1117
1218
  // emitted once what came before it is read.
@@ -1135,12 +1236,7 @@ function TLSSocket(duplex, options){
1135
1236
  // reacting to it finds the session already over.
1136
1237
  markTransportDead();
1137
1238
  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);
1239
+ self.emit('error', peer_alert_error(desc));
1144
1240
  }
1145
1241
  // Nothing of ours to flush: the peer has ended the connection.
1146
1242
  if (context.transport && typeof context.transport.destroy === 'function') context.transport.destroy();
@@ -1242,11 +1338,8 @@ function TLSSocket(duplex, options){
1242
1338
  const buf = toBuf(data || []);
1243
1339
  coalesceFlight();
1244
1340
 
1245
- // Resolve TLS version: use cached isTls13 if set (post-secureConnect); otherwise query.
1246
- // During handshake we may not yet have context.isTls13 set — only after secureConnect.
1247
- const isTls13 = context.isTls13 !== undefined
1248
- ? context.isTls13
1249
- : session.getVersion() === 0x0304;
1341
+ noteNegotiated();
1342
+ const isTls13 = context.isTls13 === true;
1250
1343
 
1251
1344
  // Alert messages — encrypt based on current epoch (0=plaintext, 1=handshake, 2=app)
1252
1345
  if (type === 'alert') {
@@ -1418,21 +1511,8 @@ function TLSSocket(duplex, options){
1418
1511
  context.handshake_write_iv = d.iv;
1419
1512
  }
1420
1513
 
1421
- const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
1422
1514
  try {
1423
- // Fused encrypt+frame — produces a complete TLS record buffer
1424
- // ready for transport.write. Same optimization as writeAppDataSingle.
1425
- const rec = encryptCompleteRecord13(
1426
- CT.HANDSHAKE, buf,
1427
- context.handshake_write_key,
1428
- get_nonce_into(context._nonceEncScratch, context.handshake_write_iv, context.handshake_write_seq),
1429
- algo,
1430
- context.rec_version
1431
- );
1432
- context.handshake_write_seq++;
1433
- if (canWrite()) {
1434
- context.transport.write(rec);
1435
- }
1515
+ writeProtectedHandshake(buf, true, 'handshake_write_key', 'handshake_write_iv', 'handshake_write_seq');
1436
1516
  if (session.isServer && type === 'finished' && context.early.status === 'accepted' && !context.early.srvWrite) {
1437
1517
  context.early.srvWrite = true;
1438
1518
  process.nextTick(flushServerEarlyWrites);
@@ -1459,14 +1539,7 @@ function TLSSocket(duplex, options){
1459
1539
  }
1460
1540
 
1461
1541
  try {
1462
- const rec = encryptCompleteRecord12(
1463
- buf, context.app_write_key, context.app_write_iv,
1464
- context.app_write_seq, CT.HANDSHAKE, context.rec_version
1465
- );
1466
- context.app_write_seq++;
1467
- if (canWrite()) {
1468
- context.transport.write(rec);
1469
- }
1542
+ writeProtectedHandshake(buf, false, 'app_write_key', 'app_write_iv', 'app_write_seq');
1470
1543
  } catch(e) {
1471
1544
  self.emit('error', e);
1472
1545
  }
@@ -1484,19 +1557,8 @@ function TLSSocket(duplex, options){
1484
1557
  }
1485
1558
  }
1486
1559
  if (context.app_write_key) {
1487
- const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
1488
1560
  try {
1489
- const rec = encryptCompleteRecord13(
1490
- CT.HANDSHAKE, buf,
1491
- context.app_write_key,
1492
- get_nonce_into(context._nonceEncScratch, context.app_write_iv, context.app_write_seq),
1493
- algo,
1494
- context.rec_version
1495
- );
1496
- context.app_write_seq++;
1497
- if (canWrite()) {
1498
- context.transport.write(rec);
1499
- }
1561
+ writeProtectedHandshake(buf, true, 'app_write_key', 'app_write_iv', 'app_write_seq');
1500
1562
  } catch(e) {
1501
1563
  self.emit('error', e);
1502
1564
  }
@@ -1506,54 +1568,23 @@ function TLSSocket(duplex, options){
1506
1568
  });
1507
1569
 
1508
1570
 
1571
+ // The version and AEAD, cached for the hot paths once negotiated: a
1572
+ // client's at its ServerHello ('hello'), a server's when it answers —
1573
+ // after its 'hello', which comes first (the application may still
1574
+ // configure the session there): its ServerHello's 'message'. Read at
1575
+ // 'hello' alone, a server had neither, and its 0.5-RTT data went out
1576
+ // as TLS 1.2.
1577
+ function noteNegotiated() {
1578
+ if (context.isTls13 !== undefined) return;
1579
+ const v = session.getVersion();
1580
+ if (!v) return;
1581
+ context.isTls13 = v === 0x0304;
1582
+ if (session.getCipher()) context.aeadAlgo = getAeadAlgo(session.getCipher());
1583
+ }
1584
+
1509
1585
  session.on('hello', function(){
1510
1586
  context.rec_version = 0x0303; // legacy record version (both 1.2 and 1.3)
1511
-
1512
- // Client already sent its preferences in ClientHello — don't override
1513
- if (!session.isServer) return;
1514
-
1515
- // Parse version range from options
1516
- let maxVer = parseVersion(options.maxVersion) || 0x0304;
1517
- let minVer = parseVersion(options.minVersion) || 0x0303;
1518
-
1519
- let versions = [];
1520
- if (maxVer >= 0x0304 && minVer <= 0x0304) versions.push(0x0304);
1521
- if (maxVer >= 0x0303 && minVer <= 0x0303) versions.push(0x0303);
1522
- if (versions.length === 0) versions.push(0x0303); // fallback
1523
-
1524
- // Cipher suites based on supported versions
1525
- // crypto.js owns the default suite list (see default_cipher_suites).
1526
- let ciphers = default_cipher_suites(versions.includes(0x0304), versions.includes(0x0303));
1527
-
1528
- // Signature algorithms: override or default
1529
- // signing.js owns the scheme list (see default_signature_schemes):
1530
- // the PKCS#1 set is only added when TLS 1.2 is actually in range.
1531
- let sigalgs = options.signatureAlgorithms
1532
- || default_signature_schemes();
1533
-
1534
- // Groups: override or default
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();
1538
-
1539
- ciphers = chooseCipherSuites(ciphers, options.prioritizeChaCha, context.allowedCipherSuites);
1540
-
1541
- // ALPN from options
1542
- let alpns = Array.isArray(options.ALPNProtocols) ? options.ALPNProtocols : [];
1543
-
1544
- session.set_context({
1545
- local_supported_versions: versions,
1546
- local_supported_alpns: alpns,
1547
- local_supported_groups: groups,
1548
- local_supported_cipher_suites: ciphers,
1549
- local_supported_signature_algorithms: sigalgs,
1550
- });
1551
-
1552
- // Resolve AEAD algorithm + isTls13 flag once cipher/version are negotiated.
1553
- // For client, both are known after receiving ServerHello (this event fires after).
1554
- // For server, these are cached again at secureConnect as a fallback.
1555
- if (session.getCipher()) context.aeadAlgo = getAeadAlgo(session.getCipher());
1556
- if (session.getVersion()) context.isTls13 = (session.getVersion() === 0x0304);
1587
+ noteNegotiated();
1557
1588
  });
1558
1589
 
1559
1590
  session.on('secureConnect', function(){
@@ -1561,12 +1592,10 @@ function TLSSocket(duplex, options){
1561
1592
  // and the flight ending in Finished must end EXACTLY there. Leftover
1562
1593
  // bytes in the message stream at completion are trailing garbage the
1563
1594
  // peer packed after its Finished — a framing violation, not padding.
1564
- if (context.hsBuf !== null && context.hsBuf.length > 0) {
1565
- context.hsBuf = null;
1566
- markTransportDead();
1567
- self.emit('error', new Error('Trailing data after handshake completion (decode_error)'));
1568
- const t = context.transport;
1569
- if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
1595
+ if (hsPending()) {
1596
+ hsReset();
1597
+ // An alert this time: the peer learns why.
1598
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'Trailing data after handshake completion', 'ERR_SSL_UNEXPECTED_MESSAGE');
1570
1599
  return;
1571
1600
  }
1572
1601
 
@@ -1579,19 +1608,20 @@ function TLSSocket(duplex, options){
1579
1608
  // Clear handshake timeout
1580
1609
  if (context.handshakeTimer) { clearTimeout(context.handshakeTimer); context.handshakeTimer = null; }
1581
1610
 
1582
- // Certificate pinning
1583
- if (context.pins && context.pins.length > 0) {
1584
- try {
1585
- let cert = session.getPeerCertificate();
1586
- if (cert && cert.raw) {
1587
- let hash = 'sha256/' + crypto.createHash('sha256').update(cert.raw).digest('base64');
1588
- if (!context.pins.includes(hash)) {
1589
- self.emit('error', new Error('Certificate pin mismatch: ' + hash));
1590
- self.destroy();
1591
- return;
1592
- }
1593
- }
1594
- } catch(e) { /* pinning is best-effort if no raw cert */ }
1611
+ // Certificate pinning: the leaf's DER, 'sha256/' + base64 of its
1612
+ // SHA-256. The chain is a list of { cert } entries — this check once
1613
+ // read `.raw` off the list itself, found nothing, and let every
1614
+ // certificate through behind a "best-effort" catch. A pin fails closed:
1615
+ // no certificate to hash is a mismatch too. A client's option (the
1616
+ // server's certificate), as documented.
1617
+ if (!context.isServer && context.pins && context.pins.length > 0) {
1618
+ let chain = session.getPeerCertificate();
1619
+ let leaf = chain && chain.length > 0 ? chain[0].cert : null;
1620
+ let hash = leaf ? 'sha256/' + crypto.createHash('sha256').update(leaf).digest('base64') : null;
1621
+ if (hash === null || !context.pins.includes(hash)) {
1622
+ abort(TLS_ALERT.BAD_CERTIFICATE, 'Certificate pin mismatch: ' + (hash || 'no peer certificate'), 'ERR_TLS_CERT_PIN_MISMATCH');
1623
+ return;
1624
+ }
1595
1625
  }
1596
1626
 
1597
1627
  // Emit keylog lines (SSLKEYLOGFILE compatible)
@@ -1731,6 +1761,13 @@ function TLSSocket(duplex, options){
1731
1761
  context.app_write_iv = d.iv;
1732
1762
  context.app_write_seq = 0;
1733
1763
  } else if (info.direction === 'receive') {
1764
+ // A run of KeyUpdates with no data between them (BoGo:
1765
+ // TooManyKeyUpdates) — 32, BoringSSL's limit. The peer's answers
1766
+ // to ours aside (info.answer): those we asked for.
1767
+ if (!info.answer && ++context.consecutive_key_updates > 32) {
1768
+ abort(TLS_ALERT.UNEXPECTED_MESSAGE, 'too many KeyUpdates without application data', 'ERR_SSL_UNEXPECTED_MESSAGE');
1769
+ return;
1770
+ }
1734
1771
  // Peer updated their send secret → derive new read keys, reset seq
1735
1772
  let d = tls_derive_from_tls_secrets(info.secret, session.getCipher());
1736
1773
  context.app_read_key = d.key;
@@ -1798,45 +1835,12 @@ function TLSSocket(duplex, options){
1798
1835
  * The other four are applied for the same reason — each is documented as a
1799
1836
  * per-connection option, and each was being dropped on the client side.
1800
1837
  */
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();
1838
+ // What the options say about the lists the session negotiates with
1839
+ // (node_options.js localLists — the versions, and suites, groups,
1840
+ // signature schemes and ALPN where given); TLSSession fills the rest by
1841
+ // its role. For a server this was done on its 'hello', a client's
1842
+ // separately, each its own way.
1843
+ session.set_context(localLists(options, localVersions));
1840
1844
 
1841
1845
  // If duplex was passed in constructor, start reading
1842
1846
  if (context.transport) {
@@ -2030,7 +2034,7 @@ function TLSSocket(duplex, options){
2030
2034
  sendCompatCCS();
2031
2035
  }
2032
2036
 
2033
- writeRecord(CT.HANDSHAKE, buf);
2037
+ for (const part of handshakeFragments(buf)) writeRecord(CT.HANDSHAKE, part);
2034
2038
 
2035
2039
  // Offering early data: the compatibility CCS right after this
2036
2040
  // first hello (§D.4), then the early data.
@@ -2040,9 +2044,11 @@ function TLSSocket(duplex, options){
2040
2044
  flushEarly();
2041
2045
  }
2042
2046
 
2047
+ // A HelloRetryRequest is TLS 1.3 by definition — the version need not
2048
+ // be recorded yet, and was not: the CCS went after the second
2049
+ // ServerHello instead of right after the HRR.
2043
2050
  if (session.isServer &&
2044
- (type === 'hello' || type === 'hello_retry_request') &&
2045
- session.getVersion() === 0x0304) {
2051
+ (type === 'hello_retry_request' || (type === 'hello' && session.getVersion() === 0x0304))) {
2046
2052
  sendCompatCCS();
2047
2053
  }
2048
2054
  }
@@ -2136,12 +2142,7 @@ function TLSSocket(duplex, options){
2136
2142
 
2137
2143
  /** Returns negotiated protocol string: 'TLSv1.3', 'TLSv1.2', etc. */
2138
2144
  self.getProtocol = function(){
2139
- let v = session.getVersion();
2140
- if (v === 0x0304) return 'TLSv1.3';
2141
- if (v === 0x0303) return 'TLSv1.2';
2142
- if (v === 0x0302) return 'TLSv1.1';
2143
- if (v === 0x0301) return 'TLSv1';
2144
- return null;
2145
+ return version_name(session.getVersion());
2145
2146
  };
2146
2147
 
2147
2148
  /** Returns cipher info: { name, standardName, version }. */
@@ -2161,6 +2162,11 @@ function TLSSocket(duplex, options){
2161
2162
  // Encrypted Client Hello (server): null (not offered) | 'accepted' | 'rejected'.
2162
2163
  Object.defineProperty(self, 'echStatus', { get: function(){ return session.getEchStatus(); } });
2163
2164
  Object.defineProperty(self, 'echAccepted', { get: function(){ return session.getEchStatus() === 'accepted'; } });
2165
+ // Server, split mode: ECH accepted by the client-facing server in front of this one.
2166
+ Object.defineProperty(self, 'echBackend', { get: function(){ return session.isEchBackend(); } });
2167
+ /** External PSK: the identity this connection was authenticated with
2168
+ * (a Buffer), or null. */
2169
+ self.getPskIdentity = function(){ let id = session.getPskIdentity(); return id ? Buffer.from(id) : null; };
2164
2170
  Object.defineProperty(self, 'alpnProtocol', {
2165
2171
  get: function(){ return session.getALPN() || false; },
2166
2172
  enumerable: true
@@ -2294,7 +2300,7 @@ function TLSSocket(duplex, options){
2294
2300
  * encrypt path chunk on this boundary; values outside the range throw. */
2295
2301
  self.setMaxSendFragment = function(size){
2296
2302
  size = Number(size);
2297
- if (!Number.isInteger(size) || size < 512 || size > 16384) {
2303
+ if (!Number.isInteger(size) || size < 512 || size > MAX_RECORD_PLAINTEXT) {
2298
2304
  throw new RangeError('setMaxSendFragment: size must be an integer in [512, 16384]');
2299
2305
  }
2300
2306
  self._maxSendFragment = size;
@@ -2455,14 +2461,7 @@ function TLSSocket(duplex, options){
2455
2461
  if (session.context && session.context.isServer) return null;
2456
2462
  let g = session.context ? session.context.selected_group : null;
2457
2463
  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];
2464
+ let k = dh_group_info(g);
2466
2465
  // Not an ECDH curve, so not in that table, but a group with a name.
2467
2466
  if (!k && ecdh_group_name(g)) return { type: 'TLSGroup', name: ecdh_group_name(g) };
2468
2467
  // Anything outside that table is still a negotiated TLS group — report