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