lemon-tls 0.5.0 → 0.5.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lemon-tls",
3
- "version": "0.5.0",
4
- "description": "TLS 1.3/1.2 implementation for Node.js - full control over cryptographic keys, record layer, and handshake. Drop-in replacement for node:tls with advanced options impossible in OpenSSL.",
3
+ "version": "0.5.5",
4
+ "description": "TLS 1.3/1.2 DTLS 1.3/1.2 implementation for Node.js - full control over cryptographic keys, record layer, and handshake. Drop-in replacement for node:tls with advanced options impossible in OpenSSL.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "types": "index.d.ts",
@@ -31,8 +31,11 @@
31
31
  },
32
32
  "keywords": [
33
33
  "tls",
34
+ "dtls",
34
35
  "tls13",
35
36
  "tls12",
37
+ "dtls13",
38
+ "dtls12",
36
39
  "tls-1.3",
37
40
  "tls-1.2",
38
41
  "ssl",
@@ -72,7 +75,6 @@
72
75
  "wireshark",
73
76
  "quic",
74
77
  "http3",
75
- "dtls",
76
78
  "record-layer",
77
79
  "pure-javascript",
78
80
  "zero-dependencies",
@@ -107,6 +107,20 @@ function insertCookieIntoClientHello(ch1, cookie) {
107
107
  return ch2;
108
108
  }
109
109
 
110
+ // Handshake diagnostics — off by default; enabled via LEMON_DEBUG=1 (or
111
+ // WEBRTC_DEBUG=1 so one flag lights up the whole webrtc-server stack).
112
+ // Interop failures in DTLS are notoriously silent (a record that fails
113
+ // decryption or ordering simply vanishes); these logs make every inbound
114
+ // record, ordering decision, and completion visible without a packet dump.
115
+ var LEMON_DEBUG = typeof process !== 'undefined' && process.env &&
116
+ (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1');
117
+ function _ldbg() {
118
+ if (!LEMON_DEBUG) return;
119
+ if (typeof console !== 'undefined' && console.log) {
120
+ console.log.apply(console, ['[lemon-dtls]'].concat([].slice.call(arguments)));
121
+ }
122
+ }
123
+
110
124
  function DTLSSession(options) {
111
125
  if (!(this instanceof DTLSSession)) return new DTLSSession(options);
112
126
  options = options || {};
@@ -117,6 +131,16 @@ function DTLSSession(options) {
117
131
  // ---- Internal TLSSession ----
118
132
  let tls = new TLSSession({
119
133
  isServer: isServer,
134
+ // RFC 9147 §5.3: the DTLS ClientHello's legacy_session_id "MUST be
135
+ // set to a zero-length vector" — the 32-byte compatibility sid is a
136
+ // TLS-over-TCP middlebox trick (RFC 8446 §4.1.2) that DTLS excludes
137
+ // (8446 appendix D.4: no compatibility mode over DTLS). The TLS
138
+ // engine generates the compat sid when none is supplied, so pin it
139
+ // empty for DTLS, both 1.2 and 1.3 profiles. (Also sidesteps a
140
+ // deployed-parser landmine: webrtc-dtls ≤0.7 reads the sid LENGTH
141
+ // byte and never the body, so any non-empty sid shears its whole
142
+ // ClientHello parse.)
143
+ sessionId: new Uint8Array(0),
120
144
  servername: options.servername,
121
145
  SNICallback: options.SNICallback,
122
146
  rejectUnauthorized: options.rejectUnauthorized,
@@ -172,6 +196,16 @@ function DTLSSession(options) {
172
196
 
173
197
  // Flight tracking for retransmission
174
198
  currentFlight: [], // array of { epoch, data: Uint8Array (complete datagram) }
199
+ // RFC 6347 §4.2.4 flight discipline: a NEW flight begins with the first
200
+ // record we send after having accepted new handshake data from the peer.
201
+ // Set when inbound processing advances state; consumed (and reset) by
202
+ // the outgoing path, which then starts a fresh currentFlight. Without
203
+ // this boundary, flights ACCUMULATED: after the HVR exchange the client's
204
+ // flight 5 (CKE/CCS/Finished) was appended onto the CH2 still sitting in
205
+ // currentFlight, so a flight-5 retransmission replayed the ClientHello
206
+ // too — which a connected werift server answers with renegotiation(),
207
+ // destroying the session instead of recovering it.
208
+ newFlightPending: false,
175
209
  retransmitTimer: null,
176
210
  retransmitCount: 0,
177
211
  retransmitTimeout: 1000,
@@ -389,6 +423,14 @@ function DTLSSession(options) {
389
423
  }
390
424
  }
391
425
 
426
+ // Flight boundary: first outgoing record after accepting peer data
427
+ // starts a fresh flight (clears the previous flight's records and
428
+ // resets the retransmit budget — RFC 6347 §4.2.4).
429
+ if (ctx.newFlightPending) {
430
+ ctx.newFlightPending = false;
431
+ startNewFlight();
432
+ }
433
+
392
434
  // DTLS 1.2: send CCS before first encrypted message (Finished)
393
435
  if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && tlsEpoch === 1 && !ctx.localCcsSent) {
394
436
  sendCCS();
@@ -407,7 +449,22 @@ function DTLSSession(options) {
407
449
  ev.emit('packet', record);
408
450
  }
409
451
 
410
- // Start retransmit timer after sending flight messages
452
+ // Start retransmit timer after sending flight messages.
453
+ //
454
+ // BUGFIX (one-shot initial ClientHello): ctx.state only left 'idle'
455
+ // inside feedDatagram — i.e. on the first INBOUND datagram. A
456
+ // client's first flight is sent before anything has been received,
457
+ // so the 'handshaking'-only guard below never armed the retransmit
458
+ // timer for flight 1. A lost or early-dropped ClientHello — e.g. one
459
+ // arriving while the peer's ICE layer still discards non-STUN
460
+ // traffic, a routine race right after nomination — was therefore
461
+ // never retransmitted, and the handshake hung silently forever
462
+ // (surfacing as connectionState stuck at 'connecting'). RFC 6347
463
+ // §4.2.4's flight state machine has no unguarded flights: every
464
+ // flight except the handshake-final one must sit under a retransmit
465
+ // timer. Sending a handshake flight IS handshaking — reflect that in
466
+ // the state before consulting it.
467
+ if (ctx.state === 'idle') ctx.state = 'handshaking';
411
468
  if (ctx.state === 'handshaking') {
412
469
  startRetransmitTimer();
413
470
  }
@@ -490,7 +547,19 @@ function DTLSSession(options) {
490
547
  function sendCCS() {
491
548
  if (ctx.localCcsSent) return;
492
549
  ctx.localCcsSent = true;
550
+ // The server's final flight OPENS with CCS — honor the flight boundary
551
+ // here too, before this record is captured.
552
+ if (ctx.newFlightPending) {
553
+ ctx.newFlightPending = false;
554
+ startNewFlight();
555
+ }
493
556
  let record = buildDtlsPlaintext(CT.CHANGE_CIPHER_SPEC, 0, nextWriteSeq(0), new Uint8Array([1]));
557
+ // RFC 6347 §4.2.4: the CCS is part of the flight and MUST be included
558
+ // when the flight is retransmitted. Previously it was emitted but never
559
+ // captured, so a retransmitted final flight arrived WITHOUT the cipher-
560
+ // spec change and the peer could never decrypt the accompanying
561
+ // Finished — an unrecoverable stall under single-datagram loss.
562
+ ctx.currentFlight.push(record);
494
563
  ev.emit('packet', record);
495
564
  }
496
565
 
@@ -513,6 +582,17 @@ function DTLSSession(options) {
513
582
 
514
583
  let records = parseDtlsDatagram(data, keysByEpoch);
515
584
 
585
+ if (LEMON_DEBUG) {
586
+ var _summ = [];
587
+ for (var _ri = 0; _ri < records.length; _ri++) {
588
+ var _r = records[_ri];
589
+ _summ.push('type=' + _r.type + ' epoch=' + _r.epoch + ' seq=' + _r.seq +
590
+ (_r.epoch > 0 && !_r.encrypted ? ' UNDECRYPTED' : ''));
591
+ }
592
+ _ldbg('rx datagram len=' + data.length + ' state=' + ctx.state +
593
+ ' records: [' + _summ.join(' | ') + ']');
594
+ }
595
+
516
596
  // RFC 9147 §7: collect the record numbers of handshake records we received
517
597
  // in this datagram so we can ACK them once, after processing. Only for
518
598
  // DTLS 1.3, and only while we're still handshaking (post-handshake ACKs of
@@ -553,6 +633,18 @@ function DTLSSession(options) {
553
633
 
554
634
  function processRecord(record) {
555
635
  if (record.type === CT.HANDSHAKE) {
636
+ // An epoch>0 record that could not be decrypted (keys not yet
637
+ // installed, AEAD failure) must NOT be fed to the handshake
638
+ // parser: its content is ciphertext, and parsing it yields a
639
+ // garbage "message" with attacker-controllable type/length fields.
640
+ // Drop it — DTLS is loss-tolerant by design, and the peer's (or
641
+ // our own flight-elicited) retransmission delivers a fresh copy
642
+ // once it can be decrypted.
643
+ if (record.epoch > 0 && !record.encrypted) {
644
+ _ldbg('skip undecrypted epoch=' + record.epoch + ' seq=' + record.seq +
645
+ ' len=' + (record.content ? record.content.length : 0));
646
+ return;
647
+ }
556
648
  processHandshakeRecord(record.content, record.epoch, record.encrypted);
557
649
  } else if (record.type === CT.APPLICATION_DATA) {
558
650
  if (ctx.state === 'connected') {
@@ -593,6 +685,8 @@ function DTLSSession(options) {
593
685
  function processHandshakeRecord(data, epoch, encrypted) {
594
686
  // A handshake record may contain multiple handshake messages
595
687
  let off = 0;
688
+ let sawMessage = false; // record contained ≥1 parseable handshake message
689
+ let anyNew = false; // ≥1 message advanced our state (not a duplicate)
596
690
  while (off + 12 <= data.length) {
597
691
  let parsed = parse_dtls_handshake(data.subarray(off));
598
692
 
@@ -600,21 +694,67 @@ function DTLSSession(options) {
600
694
  let totalLen = parsed.length;
601
695
  let fragOffset = parsed.frag_offset;
602
696
  let fragLen = parsed.frag_length;
697
+ sawMessage = true;
698
+ _ldbg('hs msg: type=' + parsed.type + ' msgSeq=' + msgSeq +
699
+ ' len=' + totalLen + ' frag=' + fragOffset + '+' + fragLen +
700
+ ' nextReadMsgSeq=' + ctx.nextReadMsgSeq);
603
701
 
604
702
  // Is this a complete message or a fragment?
605
703
  if (fragOffset === 0 && fragLen === totalLen) {
606
704
  // Complete message — feed directly
607
- deliverHandshakeMessage(parsed.type, parsed.body, msgSeq);
705
+ if (deliverHandshakeMessage(parsed.type, parsed.body, msgSeq)) anyNew = true;
608
706
  } else {
609
707
  // Fragment — reassemble
610
- reassembleFragment(parsed);
708
+ if (reassembleFragment(parsed)) anyNew = true;
611
709
  }
612
710
 
613
711
  off += 12 + fragLen;
614
712
  }
615
713
 
616
- // Received handshake data → cancel retransmit (implicit ACK)
617
- cancelRetransmit();
714
+ // ── Implicit-ACK discipline (RFC 6347 §4.2.4) ──
715
+ // Receiving a message from the peer's NEXT flight is the implicit ACK
716
+ // of ours — only then may our retransmit timer be cancelled. The
717
+ // previous unconditional cancelRetransmit() here also fired on pure
718
+ // RETRANSMISSIONS of the peer's PREVIOUS flight, which mean the exact
719
+ // opposite: our reply was lost. Two concrete deadlocks that caused:
720
+ // 1. Our cookie-bearing CH2 is lost → server retransmits its HVR
721
+ // (seq 0, discarded as a duplicate) → cancel killed the CH2
722
+ // timer → CH2 never resent → handshake dead.
723
+ // 2. Our final flight (CKE/CCS/Finished) is lost → server
724
+ // retransmits its flight (all seqs below nextReadMsgSeq,
725
+ // discarded) → cancel killed the flight timer → dead.
726
+ // NOTE: the implicit-ACK cancel deliberately does NOT live here.
727
+ // Everything in this function runs AFTER deliverHandshakeMessage,
728
+ // and TLS produces our response flight synchronously inside the
729
+ // feed — so a cancel at this point lands on the freshly-armed timer
730
+ // of the flight we JUST sent, not on the acknowledged one. The
731
+ // cancel happens at the acceptance points inside
732
+ // deliverHandshakeMessage instead, before the feed.
733
+ if (anyNew) {
734
+ // handled at acceptance time
735
+ } else if (sawMessage && ctx.currentFlight.length > 0 &&
736
+ ctx.state !== 'connected' &&
737
+ ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 &&
738
+ // Damping: a peer retransmits its flight as SEVERAL
739
+ // datagrams; without a guard, EACH duplicate datagram
740
+ // elicited a full resend of our flight — a ×N
741
+ // amplification burst that itself increased loss
742
+ // pressure. One resend per 250ms answers the peer's
743
+ // whole retransmitted flight exactly once.
744
+ (Date.now() - (ctx._lastDupElicitedResend || 0)) >= 250) {
745
+ ctx._lastDupElicitedResend = Date.now();
746
+ // Pure duplicate of a previous peer flight while we still have an
747
+ // outstanding flight of our own: the peer is telling us it never
748
+ // got our reply. Resend it immediately (RFC 6347 §4.2.4 responder
749
+ // behavior) rather than waiting out our own backoff. The regular
750
+ // timer keeps running unchanged, so the retransmit budget still
751
+ // bounds the total effort; the resend rate is naturally bounded by
752
+ // the peer's own retransmission schedule. DTLS 1.3 is excluded —
753
+ // it uses explicit ACKs (RFC 9147 §7) instead of flight inference.
754
+ for (let f = 0; f < ctx.currentFlight.length; f++) {
755
+ ev.emit('packet', ctx.currentFlight[f]);
756
+ }
757
+ }
618
758
  }
619
759
 
620
760
  /**
@@ -631,7 +771,7 @@ function DTLSSession(options) {
631
771
  let key = parsed.msg_seq;
632
772
 
633
773
  // Ignore fragments for messages we've already delivered.
634
- if (key < ctx.nextReadMsgSeq) return;
774
+ if (key < ctx.nextReadMsgSeq) return false;
635
775
 
636
776
  // DoS guard: the 24-bit `length` is attacker-controlled on the first
637
777
  // fragment. Reject anything claiming more than our cap BEFORE allocating,
@@ -639,7 +779,7 @@ function DTLSSession(options) {
639
779
  if (parsed.length > ctx.maxHandshakeMessageSize) {
640
780
  ev.emit('error', new Error('DTLS handshake message exceeds maxHandshakeMessageSize (' +
641
781
  parsed.length + ' > ' + ctx.maxHandshakeMessageSize + ')'));
642
- return;
782
+ return false;
643
783
  }
644
784
 
645
785
  let frag = ctx.fragments[key];
@@ -656,7 +796,15 @@ function DTLSSession(options) {
656
796
  let end = parsed.frag_offset + parsed.frag_length;
657
797
 
658
798
  // Bounds guard against a malformed fragment claiming to exceed totalLength.
659
- if (end > frag.totalLength) return;
799
+ if (end > frag.totalLength) return false;
800
+
801
+ // Coverage before this fragment — used to decide whether it contributed
802
+ // anything new (a retransmitted fragment that only re-covers known bytes
803
+ // is a duplicate for implicit-ACK purposes).
804
+ let coveredBefore = 0;
805
+ for (let cr = 0; cr < frag.ranges.length; cr++) {
806
+ coveredBefore += frag.ranges[cr][1] - frag.ranges[cr][0];
807
+ }
660
808
 
661
809
  // Copy the fragment bytes in (overlaps simply overwrite with identical data).
662
810
  frag.buf.set(parsed.body.subarray(0, parsed.frag_length), start);
@@ -675,27 +823,43 @@ function DTLSSession(options) {
675
823
  }
676
824
  frag.ranges = merged;
677
825
 
826
+ let coveredAfter = 0;
827
+ for (let ca = 0; ca < merged.length; ca++) {
828
+ coveredAfter += merged[ca][1] - merged[ca][0];
829
+ }
830
+
678
831
  // Complete when a single range covers [0, totalLength).
679
832
  if (merged.length === 1 && merged[0][0] === 0 && merged[0][1] === frag.totalLength) {
680
833
  delete ctx.fragments[key];
681
- deliverHandshakeMessage(frag.type, frag.buf, key);
834
+ return deliverHandshakeMessage(frag.type, frag.buf, key);
682
835
  }
836
+ return coveredAfter > coveredBefore;
683
837
  }
684
838
 
685
839
  /**
686
840
  * Deliver a complete handshake message to TLSSession.
687
841
  * Strips DTLS reconstruction → builds TLS format.
842
+ *
843
+ * @returns {boolean} true if the message advanced our handshake state
844
+ * (delivered in-order, buffered out-of-order, or consumed as a valid
845
+ * HVR / cookie-exchange step); false if it was a duplicate or was
846
+ * dropped. processHandshakeRecord uses this for the implicit-ACK
847
+ * decision — only genuinely new data may cancel our retransmit timer.
688
848
  */
689
849
  function deliverHandshakeMessage(type, body, msgSeq) {
690
850
  // Skip if we've already processed this msg_seq (duplicate / retransmit).
691
- if (msgSeq < ctx.nextReadMsgSeq) return;
851
+ if (msgSeq < ctx.nextReadMsgSeq) return false;
692
852
 
693
853
  // Check for HelloVerifyRequest (DTLS 1.2 server→client, type=3)
694
854
  if (type === 3 && !isServer) {
695
855
  ctx.nextReadMsgSeq = msgSeq + 1;
856
+ // The HVR acknowledges CH1 (implicit ACK, RFC 6347 §4.2.4). No
857
+ // explicit cancel is needed: triggerClientHelloWithCookie arms the
858
+ // CH2 flight's own timer, and startRetransmitTimer clears any
859
+ // previous timer as its first step.
696
860
  let hvr = parse_hello_verify_request(body);
697
861
  triggerClientHelloWithCookie(hvr.cookie);
698
- return;
862
+ return true;
699
863
  }
700
864
 
701
865
  // ---- Server-side HelloVerifyRequest cookie exchange (RFC 6347 §4.2.1) ----
@@ -717,17 +881,39 @@ function DTLSSession(options) {
717
881
  if (incomingCookie.length === 0) {
718
882
  // First ClientHello → send HVR with a fresh cookie. Don't process the CH,
719
883
  // don't advance msg_seq (the retry reuses the same msg_seq).
884
+ // Counts as progress: a retransmitted cookieless CH means the client
885
+ // never got our HVR — resending it here IS the correct response.
720
886
  sendHelloVerifyRequest();
721
- return;
887
+ return true;
722
888
  }
723
889
 
724
890
  // Second ClientHello: the cookie MUST match what we issued.
725
891
  if (!ctx.hvrCookie || !timingSafeEqualU8(incomingCookie, ctx.hvrCookie)) {
726
892
  // Cookie mismatch → silently drop (RFC 6347: server discards). Don't
727
893
  // advance msg_seq; a correct retry can still arrive.
728
- return;
894
+ return false;
729
895
  }
730
- // Cookie verified → fall through and process the ClientHello normally.
896
+ // Cookie verified → accept this ClientHello as the handshake anchor.
897
+ //
898
+ // Strict RFC 6347 §4.2.2 (see the figure in the RFC): the cookie-
899
+ // bearing CH2 arrives with message_seq 1, and our ServerHello must
900
+ // be numbered 1 as well. The previous code assumed the retried CH
901
+ // "reuses msg_seq" 0 — that is only true for a pure retransmission
902
+ // of CH1 (HVR lost), not for the cookie-bearing CH2, so RFC-
903
+ // compliant clients (pion, OpenSSL, fixed lemon-tls) had their CH2
904
+ // buffered as "out of order" forever and the handshake deadlocked.
905
+ //
906
+ // Set the read cursor and the outgoing counter to 1; the in-order
907
+ // check below then accepts exactly a seq-1 CH2 and rejects anything
908
+ // else (including seq-0 CH2s from pre-fix lemon-tls clients — non-
909
+ // compliant numbering is intentionally not accommodated). The
910
+ // transcript stays consistent on both sides because
911
+ // feedHandshakeToTls records the incoming wire seq and outgoing
912
+ // messages record message_sent_seq at emission — which is exactly
913
+ // what the Finished MAC hashes (RFC 6347 §4.2.6).
914
+ ctx.nextReadMsgSeq = 1;
915
+ tls.context.message_sent_seq = 1;
916
+ // Fall through and process the ClientHello normally.
731
917
  }
732
918
 
733
919
  // ---- In-order delivery (RFC 6347 §4.2.4 / RFC 9147 §5.2) ----
@@ -739,21 +925,56 @@ function DTLSSession(options) {
739
925
  // Out of order — hold it (ignore a duplicate already buffered).
740
926
  if (!(msgSeq in ctx.pendingMessages)) {
741
927
  ctx.pendingMessages[msgSeq] = { type: type, body: body };
928
+ return true; // new information, even if not yet deliverable
742
929
  }
743
- return;
930
+ return false; // duplicate of an already-buffered message
744
931
  }
745
932
 
746
- // msgSeq === nextReadMsgSeq → feed it and advance.
933
+ // msgSeq === nextReadMsgSeq → accept. Timer discipline, per the
934
+ // RFC 6347 §4.2.4 state machine, in two halves:
935
+ //
936
+ // 1. A message from the peer's next flight is the implicit ACK of
937
+ // ours — but it must RE-ARM the timer, not cancel it. While we
938
+ // are in WAITING (peer's flight only partially received — e.g.
939
+ // one datagram of the server's SH..SHD flight lost), timer
940
+ // expiry retransmits OUR last flight, which elicits a full
941
+ // retransmission of the peer's flight (a cookie-valid duplicate
942
+ // CH2 makes werift resend flight 4, a duplicate Finished makes
943
+ // it resend flight 6). An earlier revision cancelled here: one
944
+ // lost datagram inside the peer's flight then left BOTH sides
945
+ // timer-less and deadlocked — ~50% handshake failure at just 5%
946
+ // random loss.
947
+ //
948
+ // 2. The re-arm must happen BEFORE feeding TLS: the feed can
949
+ // synchronously produce our entire next flight, whose fresh
950
+ // timer must not be clobbered afterwards (the original code
951
+ // cancelled at the end of processHandshakeRecord — after the
952
+ // response — so no responder flight was EVER protected by
953
+ // retransmission).
954
+ //
955
+ // When the feed does respond, the flight boundary (newFlightPending
956
+ // → startNewFlight) resets the retransmit budget and the outgoing
957
+ // path arms the new flight's timer, superseding this re-arm.
958
+ startRetransmitTimer();
959
+ ctx.newFlightPending = true;
747
960
  feedHandshakeToTls(type, body, msgSeq);
748
961
  ctx.nextReadMsgSeq = msgSeq + 1;
749
962
 
750
- // Drain consecutively-numbered buffered messages.
963
+ // Drain consecutively-numbered buffered messages. Same pre-feed
964
+ // ordering applies; for a timer armed by an earlier feed in this very
965
+ // drain, cancelling here would be wrong — but a mid-flight response
966
+ // does not occur in TLS flows (a flight is answered only after its
967
+ // final message), so each pre-feed cancel can only ever hit an
968
+ // already-acknowledged flight's timer (usually already null).
751
969
  while (ctx.nextReadMsgSeq in ctx.pendingMessages) {
752
970
  let next = ctx.pendingMessages[ctx.nextReadMsgSeq];
753
971
  delete ctx.pendingMessages[ctx.nextReadMsgSeq];
972
+ startRetransmitTimer();
973
+ ctx.newFlightPending = true;
754
974
  feedHandshakeToTls(next.type, next.body, ctx.nextReadMsgSeq);
755
975
  ctx.nextReadMsgSeq++;
756
976
  }
977
+ return true;
757
978
  }
758
979
 
759
980
  /**
@@ -806,12 +1027,41 @@ function DTLSSession(options) {
806
1027
  // RFC 6347: "the client MUST use the same parameter values"
807
1028
  let ch2 = insertCookieIntoClientHello(ctx.savedClientHello, cookie);
808
1029
 
809
- // Reset state
1030
+ // Reset state for the post-HVR handshake restart.
1031
+ //
1032
+ // RFC 6347 §4.2.1: the handshake transcript restarts at CH2 — the
1033
+ // cookieless CH1 and the HelloVerifyRequest are NOT included in the
1034
+ // handshake hash. Resetting the transcript is therefore correct.
1035
+ //
1036
+ // RFC 6347 §4.2.2, however, is explicit (see the figure in the RFC)
1037
+ // that message_seq does NOT restart:
1038
+ //
1039
+ // ClientHello (seq=1) ------> ← CH2 carries seq 1
1040
+ // (with cookie)
1041
+ // <------ ServerHello (seq=1)
1042
+ //
1043
+ // The previous code reset BOTH directions of the counter to 0: it
1044
+ // sent CH2 with msg_seq=0 and reset nextReadMsgSeq to 0. Against a
1045
+ // spec-compliant server (pion, werift, OpenSSL — all send an HVR and
1046
+ // then number ServerHello=1), the entire server flight (seq 1..N)
1047
+ // landed in pendingMessages waiting forever for a seq-0 message that
1048
+ // never comes: the handshake stalled silently — no error, no alert.
1049
+ // It only ever worked lemon-tls↔lemon-tls because our own server
1050
+ // sends an HVR only when options.useCookies is set, so the broken
1051
+ // path was never exercised same-stack.
1052
+ //
1053
+ // nextReadMsgSeq is deliberately NOT touched here: the HVR branch in
1054
+ // deliverHandshakeMessage already advanced it to 1, which is exactly
1055
+ // where the server's ServerHello will arrive.
810
1056
  tls.context.transcript = [];
811
1057
  tls.context.hello_sent = true;
812
1058
  tls.context.dtls_cookie = cookie;
813
- tls.context.message_sent_seq = 0;
814
- ctx.nextReadMsgSeq = 0;
1059
+ // CH2 is message_seq 1. Set message_sent_seq BEFORE the transcript
1060
+ // push so transcriptHook / transcriptMsgSeqs record seq 1 for CH2 —
1061
+ // the Finished MAC hashes the DTLS reconstruction data including
1062
+ // message_seq (RFC 6347 §4.2.6), so a transcript recording seq 0 for
1063
+ // a wire message sent with seq 1 would fail Finished verification.
1064
+ tls.context.message_sent_seq = 1;
815
1065
  ctx.currentFlight = [];
816
1066
  ctx.fragments = {}; // discard any partial reassembly from CH1 flight
817
1067
  ctx.pendingMessages = {}; // discard any out-of-order buffer from CH1 flight
@@ -824,13 +1074,13 @@ function DTLSSession(options) {
824
1074
  tls.context.transcriptHook ? tls.context.transcriptHook(ch2) : ch2
825
1075
  );
826
1076
 
827
- // Build DTLS message and send
828
- let dtlsMsg = build_dtls_handshake(ch2, 0);
1077
+ // Build DTLS message and send — msg_seq 1 per the RFC figure above.
1078
+ let dtlsMsg = build_dtls_handshake(ch2, 1);
829
1079
  let record = buildRecord(0, CT.HANDSHAKE, dtlsMsg);
830
1080
  ctx.currentFlight = [record];
831
1081
  ev.emit('packet', record);
832
1082
 
833
- tls.context.message_sent_seq = 1;
1083
+ tls.context.message_sent_seq = 2;
834
1084
  startRetransmitTimer();
835
1085
  }
836
1086
 
@@ -910,6 +1160,8 @@ function DTLSSession(options) {
910
1160
  });
911
1161
 
912
1162
  tls.on('secureConnect', function() {
1163
+ _ldbg('handshake complete — secureConnect (version=0x' +
1164
+ ((ctx.selectedVersion || tls.context.selected_version || 0).toString(16)) + ')');
913
1165
  ctx.state = 'connected';
914
1166
  cancelRetransmit();
915
1167
  startNewFlight();
@@ -1041,4 +1293,4 @@ function DTLSSession(options) {
1041
1293
  return this;
1042
1294
  }
1043
1295
 
1044
- export default DTLSSession;
1296
+ export default DTLSSession;
@@ -2867,27 +2867,45 @@ function TLSSession(options){
2867
2867
  ];
2868
2868
  context.local_supported_signature_algorithms = ch_sigalgs; // for CH2 reuse
2869
2869
 
2870
- let extensions = [
2871
- {
2872
- type: 'SUPPORTED_VERSIONS',
2870
+ // supported_versions (43) and key_share (51) are the TLS 1.3
2871
+ // negotiation machinery (RFC 8446 §4.1.2, §4.2.8). Offer them only
2872
+ // when 1.3 is actually in the offered version set — a pure 1.2
2873
+ // hello must look like a 1.2 hello, exactly as OpenSSL/BoringSSL
2874
+ // behave with max_version=1.2. Previously both were emitted
2875
+ // unconditionally, so even a maxVersion:'DTLSv1.2' profile (the
2876
+ // WebRTC default) advertised a single-entry supported_versions and
2877
+ // a dangling key_share: spec-legal (receivers MUST ignore unknown
2878
+ // extensions, RFC 5246 §7.4.1.4) but non-canonical — and fatal to
2879
+ // at least one deployed stack (webrtc-dtls ≤0.7 hard-errors on any
2880
+ // extension outside its known set, then poisons its replay window
2881
+ // so no retransmission can ever recover the handshake).
2882
+ let offers13 = context.local_supported_versions.some(function (v) {
2883
+ return v === wire.TLS_VERSION.TLS1_3 || v === wire.DTLS_VERSION.DTLS1_3;
2884
+ });
2885
+ let extensions = [];
2886
+ if (offers13) {
2887
+ extensions.push({
2888
+ type: 'SUPPORTED_VERSIONS',
2873
2889
  value: context.local_supported_versions
2874
- },
2875
- {
2876
- type: 'SUPPORTED_GROUPS',
2877
- value: context.local_supported_groups
2878
- },
2879
- {
2880
- type: 'KEY_SHARE',
2890
+ });
2891
+ }
2892
+ extensions.push({
2893
+ type: 'SUPPORTED_GROUPS',
2894
+ value: context.local_supported_groups
2895
+ });
2896
+ if (offers13) {
2897
+ extensions.push({
2898
+ type: 'KEY_SHARE',
2881
2899
  value: [{
2882
- group: 0x001d,
2883
- key_exchange: public_key
2900
+ group: 0x001d,
2901
+ key_exchange: public_key
2884
2902
  }]
2885
- },
2886
- {
2887
- type: 'SIGNATURE_ALGORITHMS',
2888
- value: ch_sigalgs
2889
- }
2890
- ];
2903
+ });
2904
+ }
2905
+ extensions.push({
2906
+ type: 'SIGNATURE_ALGORITHMS',
2907
+ value: ch_sigalgs
2908
+ });
2891
2909
  if (!tls13Only) {
2892
2910
  // TLS 1.2 compatibility — meaningless (and suspicious to strict
2893
2911
  // stacks) in a 1.3-only/QUIC hello