lemon-tls 0.5.0 → 0.6.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,8 +3,10 @@ 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 } from './crypto.js';
6
+ import { TLS_CIPHER_SUITES, default_cipher_suites } from './crypto.js';
7
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';
9
+ import { default_signature_schemes } from './session/signing.js';
8
10
  import { decrypt_session_blob } from './session/ticket.js';
9
11
 
10
12
  import {
@@ -159,8 +161,24 @@ function TLSSocket(duplex, options){
159
161
  remote_ccs_seen: false,
160
162
  local_ccs_sent: false,
161
163
 
164
+ // Middlebox-compatibility CCS (TLS 1.3, RFC 8446 §D.4). This is a record-
165
+ // layer artifact — a single 0x01 byte sent as a plaintext CCS record so
166
+ // legacy middleboxes see a "cipher change" and let the flow through. It
167
+ // is NEVER a handshake message; TLSSession neither sends nor receives it.
168
+ // That is exactly why it lives here and not in the session: the QUIC
169
+ // consumer drives TLSSession directly, and RFC 9001 §8.4 forbids CCS in
170
+ // QUIC — keeping this in the TCP record layer means QUIC can never emit it.
171
+ tls13_compat_ccs_sent: false,
172
+
162
173
  tls12_read_seq: 0,
163
174
 
175
+ // Record-layer abuse limits. A peer that never advances the handshake but
176
+ // keeps us parsing is a denial of service even though every individual
177
+ // record is well formed, so the limit belongs at the record layer where
178
+ // the records are counted — not in any one message handler.
179
+ consecutive_empty_records: 0,
180
+ consecutive_ccs_records: 0,
181
+
164
182
  // Buffers and queues.
165
183
  //
166
184
  // readBuffer is a growable receive buffer with two offsets:
@@ -183,6 +201,17 @@ function TLSSocket(duplex, options){
183
201
  readBuffer: Buffer.allocUnsafe(65536),
184
202
  readStart: 0,
185
203
  readEnd: 0,
204
+
205
+ // Handshake MESSAGE stream buffer (distinct from readBuffer, which is the
206
+ // record stream). RFC 8446 §5.1: handshake messages may be coalesced into
207
+ // one record or fragmented across records — record boundaries carry no
208
+ // meaning at the message layer. Record payloads are appended here and
209
+ // complete messages are carved out by their own 4-byte type+length
210
+ // header. Without this, a record carrying EE+Cert+CV+Finished (as
211
+ // OpenSSL/BoringSSL routinely pack) fed only its FIRST message to the
212
+ // session and silently dropped the rest.
213
+ hsBuf: null,
214
+
186
215
  appWriteQueue: [],
187
216
  pendingHandshake: [],
188
217
 
@@ -435,7 +464,7 @@ function TLSSocket(duplex, options){
435
464
  // zero-copy view into the decrypted plaintext
436
465
  self.push(out.subarray(0, j));
437
466
  } else if (content_type === CT.HANDSHAKE) {
438
- session.message(out.subarray(0, j));
467
+ feedHandshakeBytes(out.subarray(0, j));
439
468
  } else if (content_type === CT.ALERT) {
440
469
  // TODO: handle alert
441
470
  }
@@ -444,7 +473,7 @@ function TLSSocket(duplex, options){
444
473
  if (context.using_app_keys === true) {
445
474
  self.push(out);
446
475
  } else {
447
- session.message(out);
476
+ feedHandshakeBytes(out);
448
477
  }
449
478
  }
450
479
  }
@@ -505,6 +534,44 @@ function TLSSocket(duplex, options){
505
534
  context.readEnd = needed;
506
535
  }
507
536
 
537
+ /**
538
+ * Handshake message reassembly (RFC 8446 §5.1).
539
+ *
540
+ * Sits between the record layer and TLSSession: record payloads of type
541
+ * handshake go in; complete handshake messages come out, one
542
+ * session.message() call each. Handles both directions of mismatch —
543
+ * several messages coalesced into one record, and one message fragmented
544
+ * across records. TLSSession keeps receiving exactly what its contract
545
+ * says: one whole handshake message per call.
546
+ *
547
+ * Note on memory: session.message() stashes views of the fed bytes into
548
+ * its transcript, so every message handed over must be backed by memory
549
+ * we never overwrite. Buffer.from()/Buffer.concat() below always copy out
550
+ * of the (reused) record buffers; the carve-out subarrays then alias the
551
+ * fresh copy, which nothing mutates.
552
+ */
553
+ function feedHandshakeBytes(bytes){
554
+ if (context.hsBuf === null || context.hsBuf.length === 0) {
555
+ context.hsBuf = Buffer.from(bytes);
556
+ } else {
557
+ context.hsBuf = Buffer.concat([context.hsBuf, Buffer.from(bytes)]);
558
+ }
559
+
560
+ while (context.hsBuf.length >= 4) {
561
+ const len = (context.hsBuf[1] << 16) | (context.hsBuf[2] << 8) | context.hsBuf[3];
562
+ if (context.hsBuf.length < 4 + len) break; // partial message — wait for more records
563
+
564
+ const msg = context.hsBuf.subarray(0, 4 + len);
565
+ context.hsBuf = context.hsBuf.subarray(4 + len);
566
+
567
+ session.message(msg);
568
+
569
+ // A message may have fatally ended the session (alert sent,
570
+ // transport torn down). Don't keep feeding a corpse.
571
+ if (context.destroyed) return;
572
+ }
573
+ }
574
+
508
575
  function parseRecordsAndDispatch(){
509
576
  // Hot path — zero-copy parsing:
510
577
  // - Work directly on readBuffer with moving readStart offset (no slicing)
@@ -517,6 +584,24 @@ function TLSSocket(duplex, options){
517
584
  const end = context.readEnd;
518
585
 
519
586
  while (end - off >= 5) {
587
+ // ── Stop feeding a session that has already failed ──
588
+ // Once the session aborts (fatalAlert -> state 'error') the
589
+ // handshake is over: we have sent our alert and are closing. Any
590
+ // record still in the buffer, or arriving after, belongs to a
591
+ // conversation that no longer exists — and in TLS 1.3 it is
592
+ // encrypted under keys the peer derived from a transcript we
593
+ // rejected, so decrypting it fails and surfaces as a spurious
594
+ // bad_record_mac that masks the real reason for the abort.
595
+ //
596
+ // This lives here, in the one loop every record passes through,
597
+ // rather than as a guard inside each handler: "the session is
598
+ // dead, stop processing" is a property of the transport, not of
599
+ // any particular record type.
600
+ if (context.destroyed || (session && session.getState &&
601
+ (session.getState() === 'error' || session.getState() === 'closed'))) {
602
+ return;
603
+ }
604
+
520
605
  const type = rb[off];
521
606
  // Version at rb[off+1..off+3] — currently unused (legacy 0x0303 always)
522
607
  const len = (rb[off + 3] << 8) | rb[off + 4];
@@ -528,6 +613,48 @@ function TLSSocket(duplex, options){
528
613
  const body = rb.subarray(off + 5, off + 5 + len);
529
614
  off += 5 + len;
530
615
 
616
+ // ── Record-layer sanity, applied to every record ──
617
+ // RFC 8446 §5.1: the legacy_record_version is 0x0303 for every
618
+ // record after the first ClientHello, and 0x03xx before it. A
619
+ // record claiming anything else is malformed framing, not a
620
+ // version negotiation signal.
621
+ const recVer = (rb[off - len - 5 + 1] << 8) | rb[off - len - 5 + 2];
622
+ if ((recVer >>> 8) !== 0x03) {
623
+ try { session.sendAlert(2, TLS_ALERT.PROTOCOL_VERSION); } catch (e) {}
624
+ self.emit('error', new Error('Record with illegal legacy_record_version 0x' + recVer.toString(16)));
625
+ markTransportDead();
626
+ if (context.transport && !context.transport.destroyed) context.transport.destroy();
627
+ return;
628
+ }
629
+
630
+ // A peer may legally send an empty record, and a TLS 1.3 peer may
631
+ // legally send a compat CCS — but only finitely many. Unbounded
632
+ // streams of either make no progress and exist only to burn our
633
+ // CPU (BoGo: TooManyEmptyFragments / TooManyChangeCipherSpec).
634
+ if (len === 0) {
635
+ if (++context.consecutive_empty_records > 32) {
636
+ try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
637
+ self.emit('error', new Error('Too many consecutive empty records'));
638
+ markTransportDead();
639
+ if (context.transport && !context.transport.destroyed) context.transport.destroy();
640
+ return;
641
+ }
642
+ } else {
643
+ context.consecutive_empty_records = 0;
644
+ }
645
+
646
+ if (type === CT.CHANGE_CIPHER_SPEC) {
647
+ if (++context.consecutive_ccs_records > 8) {
648
+ try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
649
+ self.emit('error', new Error('Too many consecutive ChangeCipherSpec records'));
650
+ markTransportDead();
651
+ if (context.transport && !context.transport.destroyed) context.transport.destroy();
652
+ return;
653
+ }
654
+ } else {
655
+ context.consecutive_ccs_records = 0;
656
+ }
657
+
531
658
  if (type === CT.APPLICATION_DATA) {
532
659
  processCiphertext(body, header);
533
660
  context.tls12_read_seq++;
@@ -547,12 +674,35 @@ function TLSSocket(duplex, options){
547
674
  // those stashed views would point to overwritten bytes — causing
548
675
  // bad key derivations and GCM tag failures much later. We must hand
549
676
  // the session an owned Buffer so its transcript survives.
550
- session.message(Buffer.from(body));
677
+ feedHandshakeBytes(body);
551
678
  }
552
679
  context.tls12_read_seq++;
553
680
  } else if (type === CT.CHANGE_CIPHER_SPEC) {
554
- context.tls12_read_seq = 0;
555
- context.remote_ccs_seen = true;
681
+ // Version-dependent meaning:
682
+ // - TLS 1.2: a real protocol event — the peer's next records
683
+ // are encrypted. Flip remote_ccs_seen so type-22 records are
684
+ // routed to the decrypt path.
685
+ // - TLS 1.3: a middlebox-compat artifact (RFC 8446 §D.4)
686
+ // carrying no meaning; cipher state is driven by the key
687
+ // schedule. It MUST be ignored and MUST NOT advance any
688
+ // read sequence.
689
+ //
690
+ // The test is "do we POSITIVELY know this is TLS 1.2", not
691
+ // "is it not 1.3", and that is the correct test on its own
692
+ // merits: a CCS carries no meaning unless we have actually
693
+ // negotiated 1.2, so an unknown version must not be treated as
694
+ // a cipher change. (It also used to be load-bearing for a
695
+ // second reason — the HelloRetryRequest path did not record
696
+ // selected_version, so mid-HRR the version was simply unknown.
697
+ // That gap is fixed at its source now, but the rule stands.)
698
+ const ccsVersion = (context.isTls13 !== undefined)
699
+ ? (context.isTls13 ? 0x0304 : 0x0303)
700
+ : session.getVersion();
701
+ if (ccsVersion === 0x0303) {
702
+ context.tls12_read_seq = 0;
703
+ context.remote_ccs_seen = true;
704
+ }
705
+ // TLS 1.3, or version not yet negotiated: swallow silently.
556
706
  } else if (type === CT.ALERT) {
557
707
  // Alert: 2 bytes — level (1=warning, 2=fatal), description
558
708
  if (body.length >= 2) {
@@ -584,14 +734,71 @@ function TLSSocket(duplex, options){
584
734
  }
585
735
 
586
736
 
737
+ /**
738
+ * The single truth for "may we still write to the wire".
739
+ * `writable` (not just destroyed/writableEnded) matters: after the peer
740
+ * resets the connection, Node flips writable to false before 'close'
741
+ * fires — a write() in that window is the EPIPE the session's reactive
742
+ * loop used to trigger while continuing the handshake against a dead
743
+ * transport.
744
+ */
745
+ function canWrite() {
746
+ const t = context.transport;
747
+ return !!t && !context.destroyed && !t.destroyed && !t.writableEnded && t.writable !== false;
748
+ }
749
+
750
+ /**
751
+ * Transport is gone → make BOTH layers inert:
752
+ * - context.destroyed short-circuits every write path here, and
753
+ * - session.close() flips TLSSession into a terminal state, which stops
754
+ * the reactive loop from producing further handshake messages at the
755
+ * source (its close_notify emission is dropped by canWrite()).
756
+ */
757
+ /**
758
+ * Emit the TLS 1.3 middlebox-compatibility ChangeCipherSpec (RFC 8446 §D.4).
759
+ *
760
+ * ONE implementation for all three points at which it can legally appear,
761
+ * because "send the compat record, exactly once" is a single rule. It was
762
+ * previously written out three times — client-before-CH2,
763
+ * server-after-SH/HRR, and client-before-its-first-encrypted-record — each
764
+ * repeating the guard, the write and the flag. Three copies of a
765
+ * send-once rule is three chances for them to disagree.
766
+ *
767
+ * The record is meaningless to the protocol: it exists so middleboxes that
768
+ * expect a TLS 1.2-shaped flow do not drop the connection. It is plaintext,
769
+ * a single 0x01 byte, never enters the transcript, and never advances a
770
+ * sequence number.
771
+ *
772
+ * WHERE it goes differs by role, and the two are on opposite sides of the
773
+ * hello:
774
+ * server — immediately AFTER ServerHello / HelloRetryRequest
775
+ * client — immediately BEFORE its second flight (CH2, or failing that
776
+ * the first encrypted record)
777
+ * Callers state their position; this function owns the once-only rule.
778
+ *
779
+ * @returns {boolean} true if a record was actually written
780
+ */
781
+ function sendCompatCCS() {
782
+ if (context.tls13_compat_ccs_sent) return false;
783
+ context.tls13_compat_ccs_sent = true;
784
+ writeRecord(CT.CHANGE_CIPHER_SPEC, Buffer.from([0x01]));
785
+ return true;
786
+ }
787
+
788
+ function markTransportDead() {
789
+ if (context.destroyed) return;
790
+ context.destroyed = true;
791
+ try { session.close(); } catch (e) {}
792
+ }
793
+
587
794
  function bindTransport(){
588
795
  if (!context.transport) return;
589
796
  context.transport.on('data', function(chunk){
590
797
  _appendChunk(chunk);
591
798
  parseRecordsAndDispatch();
592
799
  });
593
- context.transport.on('error', function(err){ self.emit('error', err); });
594
- context.transport.on('close', function(){ self.emit('close'); });
800
+ context.transport.on('error', function(err){ markTransportDead(); self.emit('error', err); });
801
+ context.transport.on('close', function(){ markTransportDead(); self.emit('close'); });
595
802
  }
596
803
 
597
804
  session.on('message', function(epoch, seq, type, data){
@@ -613,16 +820,88 @@ function TLSSocket(duplex, options){
613
820
 
614
821
  if (isTls13) {
615
822
  // TLS 1.3: wrap alert as APPLICATION_DATA (inner type is ALERT)
823
+ //
824
+ // The app write key may not have been materialised yet: it is
825
+ // derived lazily on first application write, and an abort can
826
+ // happen before any application data flows. Derive it here the
827
+ // same way the data path does, rather than silently falling
828
+ // back to a cleartext alert — a peer that has switched to
829
+ // application keys cannot read a cleartext record and reports a
830
+ // record-header error instead of the reason we sent. This is
831
+ // exactly what turned "certificate_required" into an opaque
832
+ // decrypt failure.
833
+ // Materialise the write key for THIS epoch if it does not exist
834
+ // yet. Both epochs derive lazily on first write, and an abort
835
+ // can precede any write at either level:
836
+ //
837
+ // epoch 2 — abort after our Finished, before app data.
838
+ // epoch 1 — abort while the PEER's flight is still arriving,
839
+ // before we have written anything at all. A client
840
+ // rejecting the server's Certificate/Finished is
841
+ // exactly here, and it was NOT covered: the code
842
+ // fell through to the cleartext path below and sent
843
+ // `15 03 03` where the peer required `17 03 03`.
844
+ // The peer then reports a decrypt failure instead
845
+ // of the reason we gave it — the same symptom the
846
+ // epoch-2 case used to have, one level down.
847
+ if (epoch === 2 && context.app_write_key === null) {
848
+ const ts = session.getTrafficSecrets();
849
+ if (ts && ts.localAppSecret) {
850
+ const d = tls_derive_from_tls_secrets(ts.localAppSecret, ts.cipher);
851
+ context.app_write_key = d.key;
852
+ context.app_write_iv = d.iv;
853
+ }
854
+ } else if (epoch === 1 && !context.handshake_write_key) {
855
+ const ts = session.getTrafficSecrets();
856
+ if (ts && ts.localHandshakeSecret) {
857
+ const d = tls_derive_from_tls_secrets(ts.localHandshakeSecret, ts.cipher);
858
+ context.handshake_write_key = d.key;
859
+ context.handshake_write_iv = d.iv;
860
+ if (context.handshake_write_seq === undefined ||
861
+ context.handshake_write_seq === null) {
862
+ context.handshake_write_seq = 0;
863
+ }
864
+ }
865
+ }
866
+
616
867
  const writeKey = (epoch === 2) ? context.app_write_key : context.handshake_write_key;
617
868
  const writeIv = (epoch === 2) ? context.app_write_iv : context.handshake_write_iv;
618
869
  const writeSeq = (epoch === 2) ? context.app_write_seq : context.handshake_write_seq;
619
870
 
620
871
  if (!writeKey) {
621
- // Keys not ready — fall back to plaintext (defensive)
622
- writeRecord(CT.ALERT, buf);
872
+ // We are past epoch 0, so the peer is reading ciphertext.
873
+ // A cleartext alert here is NOT defensive: the peer cannot
874
+ // read it, reports a decrypt failure, and the reason we
875
+ // were conveying is lost behind that. This exact fallback
876
+ // turned three separate correct validations into "bad
877
+ // record MAC" — in DTLS, at epoch 2, and at epoch 1.
878
+ //
879
+ // If we cannot protect the record, say so loudly and send
880
+ // nothing: a missing alert and an unreadable alert look the
881
+ // same to the peer, but only one is diagnosable from our
882
+ // side. Mirrors the DTLS record layer, which likewise
883
+ // refuses to emit an unprotectable record.
884
+ self.emit('error', new Error(
885
+ 'TLS 1.3: cannot protect an alert at epoch ' + epoch +
886
+ ' — no write key available; alert not sent'));
623
887
  return;
624
888
  }
625
889
 
890
+ // Middlebox-compatibility CCS (RFC 8446 §D.4) before our FIRST
891
+ // encrypted record — not merely before the first encrypted
892
+ // HANDSHAKE record.
893
+ //
894
+ // The handshake path already does this a few lines below, but
895
+ // the alert path returns before reaching it. When a client
896
+ // rejects something in the server's flight it never writes a
897
+ // handshake record at all, so its whole output was
898
+ // ClientHello + encrypted alert with no CCS in between, and a
899
+ // peer enforcing compatibility mode rejects the stream before
900
+ // it ever reads the alert. The abort path is still a flight and
901
+ // carries the same obligation. sendCompatCCS owns the
902
+ // once-only rule, so calling it here cannot double-send.
903
+ if (!session.isServer) sendCompatCCS();
904
+
626
905
  const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
627
906
  const enc = encrypt_tls_record(CT.ALERT, buf, writeKey, get_nonce(writeIv, writeSeq), algo);
628
907
 
@@ -634,7 +913,12 @@ function TLSSocket(duplex, options){
634
913
  // TLS 1.2: post-CCS alerts are encrypted with app keys (AES-GCM).
635
914
  // Outer record type remains ALERT (21), body is encrypted.
636
915
  if (!context.app_write_key) {
637
- writeRecord(CT.ALERT, buf);
916
+ // Same rule as the 1.3 path above: past CCS the peer reads
917
+ // encrypted records, so a cleartext alert is unreadable
918
+ // rather than best-effort.
919
+ self.emit('error', new Error(
920
+ 'TLS 1.2: cannot protect an alert after CCS — no ' +
921
+ 'application write key; alert not sent'));
638
922
  return;
639
923
  }
640
924
 
@@ -654,12 +938,57 @@ function TLSSocket(duplex, options){
654
938
  context.pendingHandshake.push({ type: CT.HANDSHAKE, data: buf });
655
939
  return;
656
940
  }
941
+ // ── Middlebox-compat CCS (RFC 8446 §D.4) ──
942
+ // The two roles place it on OPPOSITE sides of their hello, and the
943
+ // spec is explicit about both:
944
+ //
945
+ // server — "immediately AFTER its first handshake message",
946
+ // i.e. after ServerHello or HelloRetryRequest.
947
+ // client — "immediately BEFORE its second flight", i.e. BEFORE
948
+ // the second ClientHello.
949
+ //
950
+ // Getting the client side backwards is not cosmetic: a peer that
951
+ // requires the compat record reads the next thing on the wire and
952
+ // finds a handshake record where the CCS should be, and reports
953
+ // exactly that ("invalid TLS 1.3 ChangeCipherSpec: 160303...").
954
+ // So the client's CCS is emitted BEFORE writeRecord below, and the
955
+ // server's after.
956
+ //
957
+ // We cannot rely on context.isTls13 yet (it is set at
958
+ // secureConnect), so both gate on the session's negotiated
959
+ // version. For the client that is only known once a
960
+ // HelloRetryRequest has told us — which is precisely the CH2 case
961
+ // this branch exists for. In the no-HRR flow the version is still
962
+ // unknown at CH1 time, so the flag stays false and the CCS goes out
963
+ // just before the first encrypted record instead (epoch-1 path
964
+ // below); that is the same "before its second flight" placement.
965
+ if (!session.isServer && type === 'hello' && session.getVersion() === 0x0304) {
966
+ sendCompatCCS();
967
+ }
968
+
657
969
  writeRecord(CT.HANDSHAKE, buf);
970
+
971
+ if (session.isServer &&
972
+ (type === 'hello' || type === 'hello_retry_request') &&
973
+ session.getVersion() === 0x0304) {
974
+ sendCompatCCS();
975
+ }
658
976
  return;
659
977
  }
660
978
 
661
979
  if (epoch === 1) {
662
980
  if (isTls13) {
981
+ // Middlebox-compat CCS (RFC 8446 §D.4): a TLS 1.3 client sends
982
+ // the fake CCS after its ClientHello (or, under HRR, after CH2)
983
+ // and before its first encrypted handshake record — i.e. right
984
+ // here, the moment we're about to emit the first epoch-1 record.
985
+ // Plaintext, one 0x01 byte. Server side handled after the SH.
986
+ // Fallback placement: reached only when the CH2 path above did
987
+ // not fire (no HelloRetryRequest, so the version was still
988
+ // unknown at ClientHello time). Same "before the second
989
+ // flight" position §D.4 describes.
990
+ if (!session.isServer) sendCompatCCS();
991
+
663
992
  // TLS 1.3: encrypt handshake messages with handshake traffic secret
664
993
  if (context.handshake_write_key === null) {
665
994
  const hs = session.getHandshakeSecrets();
@@ -684,7 +1013,7 @@ function TLSSocket(duplex, options){
684
1013
  context.rec_version
685
1014
  );
686
1015
  context.handshake_write_seq++;
687
- if (context.transport && !context.transport.destroyed && !context.transport.writableEnded) {
1016
+ if (canWrite()) {
688
1017
  context.transport.write(rec);
689
1018
  }
690
1019
  } catch(e) {
@@ -714,7 +1043,7 @@ function TLSSocket(duplex, options){
714
1043
  context.app_write_seq, CT.HANDSHAKE, context.rec_version
715
1044
  );
716
1045
  context.app_write_seq++;
717
- if (context.transport && !context.transport.destroyed && !context.transport.writableEnded) {
1046
+ if (canWrite()) {
718
1047
  context.transport.write(rec);
719
1048
  }
720
1049
  } catch(e) {
@@ -744,7 +1073,7 @@ function TLSSocket(duplex, options){
744
1073
  context.rec_version
745
1074
  );
746
1075
  context.app_write_seq++;
747
- if (context.transport && !context.transport.destroyed && !context.transport.writableEnded) {
1076
+ if (canWrite()) {
748
1077
  context.transport.write(rec);
749
1078
  }
750
1079
  } catch(e) {
@@ -772,35 +1101,17 @@ function TLSSocket(duplex, options){
772
1101
  if (versions.length === 0) versions.push(0x0303); // fallback
773
1102
 
774
1103
  // Cipher suites based on supported versions
775
- let ciphers = [];
776
- if (versions.includes(0x0304)) {
777
- ciphers.push(0x1301, 0x1302, 0x1303); // TLS_AES_128_GCM, TLS_AES_256_GCM, TLS_CHACHA20
778
- }
779
- if (versions.includes(0x0303)) {
780
- ciphers.push(
781
- 0xC02F, // ECDHE_RSA_WITH_AES_128_GCM_SHA256
782
- 0xC030, // ECDHE_RSA_WITH_AES_256_GCM_SHA384
783
- 0xC02B, // ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
784
- 0xCCA8 // ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
785
- );
786
- }
1104
+ // crypto.js owns the default suite list (see default_cipher_suites).
1105
+ let ciphers = default_cipher_suites(versions.includes(0x0304), versions.includes(0x0303));
787
1106
 
788
1107
  // Signature algorithms: override or default
789
- let sigalgs = [];
790
- if (options.signatureAlgorithms) {
791
- sigalgs = options.signatureAlgorithms;
792
- } else {
793
- if (versions.includes(0x0304)) {
794
- sigalgs.push(0x0804, 0x0805, 0x0806); // RSA-PSS
795
- sigalgs.push(0x0403, 0x0503, 0x0603); // ECDSA
796
- }
797
- if (versions.includes(0x0303)) {
798
- sigalgs.push(0x0401, 0x0501, 0x0601); // RSA-PKCS1
799
- }
800
- }
1108
+ // signing.js owns the scheme list (see default_signature_schemes):
1109
+ // the PKCS#1 set is only added when TLS 1.2 is actually in range.
1110
+ let sigalgs = options.signatureAlgorithms
1111
+ || default_signature_schemes();
801
1112
 
802
1113
  // Groups: override or default
803
- let groups = options.groups || [0x001d, 0x0017]; // X25519, P-256
1114
+ let groups = options.groups || ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
804
1115
 
805
1116
  // prioritizeChaCha: move ChaCha20 to front of cipher list
806
1117
  if (options.prioritizeChaCha) {
@@ -833,6 +1144,19 @@ function TLSSocket(duplex, options){
833
1144
  });
834
1145
 
835
1146
  session.on('secureConnect', function(){
1147
+ // RFC 8446 §5.1: handshake messages must not straddle a key change,
1148
+ // and the flight ending in Finished must end EXACTLY there. Leftover
1149
+ // bytes in the message stream at completion are trailing garbage the
1150
+ // peer packed after its Finished — a framing violation, not padding.
1151
+ if (context.hsBuf !== null && context.hsBuf.length > 0) {
1152
+ context.hsBuf = null;
1153
+ markTransportDead();
1154
+ self.emit('error', new Error('Trailing data after handshake completion (decode_error)'));
1155
+ const t = context.transport;
1156
+ if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
1157
+ return;
1158
+ }
1159
+
836
1160
  context.using_app_keys=true;
837
1161
  context.secureEstablished=true;
838
1162
  context.aeadAlgo = getAeadAlgo(session.getCipher());
@@ -904,6 +1228,21 @@ function TLSSocket(duplex, options){
904
1228
  self.emit('session', ticketData);
905
1229
  });
906
1230
 
1231
+ // Bridge fatal protocol errors from the session (malformed peer message,
1232
+ // invalid key share, Finished mismatch, ...). The session has already
1233
+ // emitted the fatal alert as an epoch-tagged 'message' — which the handler
1234
+ // above framed and wrote — and flipped itself to a terminal state. Here we
1235
+ // surface it to the owner and tear down the transport. Without this bridge
1236
+ // the session's 'error' emit has no listener, and Node throws it as an
1237
+ // uncaught exception out of the transport's data callback — the very DoS
1238
+ // the validation was added to prevent.
1239
+ session.on('error', function(err) {
1240
+ markTransportDead();
1241
+ self.emit('error', err);
1242
+ const t = context.transport;
1243
+ if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
1244
+ });
1245
+
907
1246
  // Forward handshakeMessage event (for debugging/inspection)
908
1247
  session.on('handshakeMessage', function(type, raw, parsed) {
909
1248
  self.emit('handshakeMessage', type, raw, parsed);