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 +5 -3
- package/src/dtls_session.js +276 -24
- package/src/tls_session.js +36 -18
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemon-tls",
|
|
3
|
-
"version": "0.5.
|
|
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",
|
package/src/dtls_session.js
CHANGED
|
@@ -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
|
-
//
|
|
617
|
-
|
|
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 →
|
|
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 →
|
|
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
|
-
|
|
814
|
-
|
|
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,
|
|
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 =
|
|
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;
|
package/src/tls_session.js
CHANGED
|
@@ -2867,27 +2867,45 @@ function TLSSession(options){
|
|
|
2867
2867
|
];
|
|
2868
2868
|
context.local_supported_signature_algorithms = ch_sigalgs; // for CH2 reuse
|
|
2869
2869
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
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
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
-
|
|
2888
|
-
|
|
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
|