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/package.json +5 -3
- package/src/compat.js +10 -0
- package/src/crypto.js +146 -48
- package/src/dtls_session.js +501 -66
- package/src/dtls_socket.js +7 -0
- package/src/record.js +88 -21
- package/src/session/ecdh.js +124 -0
- package/src/session/message.js +35 -0
- package/src/session/signing.js +240 -47
- package/src/tls_session.js +1839 -431
- package/src/tls_socket.js +378 -39
- package/src/wire.js +272 -14
package/src/dtls_session.js
CHANGED
|
@@ -44,8 +44,11 @@ import {
|
|
|
44
44
|
|
|
45
45
|
import {
|
|
46
46
|
TLS_CIPHER_SUITES,
|
|
47
|
+
default_cipher_suites,
|
|
47
48
|
hkdf_expand_label,
|
|
48
49
|
derive_sn_key,
|
|
50
|
+
LABEL_PREFIX_TLS13,
|
|
51
|
+
LABEL_PREFIX_DTLS13,
|
|
49
52
|
} from './crypto.js';
|
|
50
53
|
|
|
51
54
|
import {
|
|
@@ -59,9 +62,12 @@ import {
|
|
|
59
62
|
parseDtlsDatagram,
|
|
60
63
|
buildDtlsAck,
|
|
61
64
|
parseDtlsAck,
|
|
65
|
+
decryptEncryptedDtls13,
|
|
62
66
|
} from './record.js';
|
|
63
67
|
|
|
64
68
|
import { timingSafeEqualU8 } from './utils.js';
|
|
69
|
+
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS } from './session/ecdh.js';
|
|
70
|
+
import { default_signature_schemes } from './session/signing.js';
|
|
65
71
|
|
|
66
72
|
|
|
67
73
|
// ============================================================
|
|
@@ -107,6 +113,20 @@ function insertCookieIntoClientHello(ch1, cookie) {
|
|
|
107
113
|
return ch2;
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
// Handshake diagnostics — off by default; enabled via LEMON_DEBUG=1 (or
|
|
117
|
+
// WEBRTC_DEBUG=1 so one flag lights up the whole webrtc-server stack).
|
|
118
|
+
// Interop failures in DTLS are notoriously silent (a record that fails
|
|
119
|
+
// decryption or ordering simply vanishes); these logs make every inbound
|
|
120
|
+
// record, ordering decision, and completion visible without a packet dump.
|
|
121
|
+
var LEMON_DEBUG = typeof process !== 'undefined' && process.env &&
|
|
122
|
+
(process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1');
|
|
123
|
+
function _ldbg() {
|
|
124
|
+
if (!LEMON_DEBUG) return;
|
|
125
|
+
if (typeof console !== 'undefined' && console.log) {
|
|
126
|
+
console.log.apply(console, ['[lemon-dtls]'].concat([].slice.call(arguments)));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
function DTLSSession(options) {
|
|
111
131
|
if (!(this instanceof DTLSSession)) return new DTLSSession(options);
|
|
112
132
|
options = options || {};
|
|
@@ -117,6 +137,17 @@ function DTLSSession(options) {
|
|
|
117
137
|
// ---- Internal TLSSession ----
|
|
118
138
|
let tls = new TLSSession({
|
|
119
139
|
isServer: isServer,
|
|
140
|
+
// RFC 9147 §5.3: the DTLS ClientHello's legacy_session_id "MUST be
|
|
141
|
+
// set to a zero-length vector" — the 32-byte compatibility sid is a
|
|
142
|
+
// TLS-over-TCP middlebox trick (RFC 8446 §4.1.2) that DTLS excludes
|
|
143
|
+
// (8446 appendix D.4: no compatibility mode over DTLS). The TLS
|
|
144
|
+
// engine generates the compat sid when none is supplied, so pin it
|
|
145
|
+
// empty for DTLS, both 1.2 and 1.3 profiles. (Also sidesteps a
|
|
146
|
+
// deployed-parser landmine: webrtc-dtls ≤0.7 reads the sid LENGTH
|
|
147
|
+
// byte and never the body, so any non-empty sid shears its whole
|
|
148
|
+
// ClientHello parse.)
|
|
149
|
+
sessionId: new Uint8Array(0),
|
|
150
|
+
srtpProfiles: options.srtpProfiles,
|
|
120
151
|
servername: options.servername,
|
|
121
152
|
SNICallback: options.SNICallback,
|
|
122
153
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
@@ -170,8 +201,32 @@ function DTLSSession(options) {
|
|
|
170
201
|
// keyed by msg_seq, and drained in order once the gap is filled.
|
|
171
202
|
pendingMessages: {}, // msg_seq → { type, body }
|
|
172
203
|
|
|
173
|
-
// Flight tracking for retransmission
|
|
174
|
-
|
|
204
|
+
// Flight tracking for retransmission.
|
|
205
|
+
//
|
|
206
|
+
// Stores PRE-ENCRYPTION descriptors — { epoch, contentType, payload } —
|
|
207
|
+
// never built records. Rebuilding at retransmit time (resendFlight) gives
|
|
208
|
+
// every retransmitted record a FRESH record sequence number, which is not
|
|
209
|
+
// an optimization but a hard requirement on two independent grounds:
|
|
210
|
+
// 1. DTLS 1.3 (RFC 9147 §4.2.1): the AEAD nonce is derived from the
|
|
211
|
+
// record seq. Re-emitting a stored ciphertext repeats (key, nonce) —
|
|
212
|
+
// catastrophic for GCM (key-stream reuse reveals plaintext XOR and
|
|
213
|
+
// enables tag forgery). The old code stored fully-built records and
|
|
214
|
+
// replayed the exact bytes.
|
|
215
|
+
// 2. Receivers reconstruct the full seq from its truncated wire form
|
|
216
|
+
// relative to the highest seq seen (and BoGo outright requires
|
|
217
|
+
// monotonic seqs from the shim) — a replayed old seq reconstructs to
|
|
218
|
+
// a wrong full value and fails deprotection as "bad record MAC".
|
|
219
|
+
currentFlight: [], // array of { epoch, contentType, payload: Uint8Array }
|
|
220
|
+
// RFC 6347 §4.2.4 flight discipline: a NEW flight begins with the first
|
|
221
|
+
// record we send after having accepted new handshake data from the peer.
|
|
222
|
+
// Set when inbound processing advances state; consumed (and reset) by
|
|
223
|
+
// the outgoing path, which then starts a fresh currentFlight. Without
|
|
224
|
+
// this boundary, flights ACCUMULATED: after the HVR exchange the client's
|
|
225
|
+
// flight 5 (CKE/CCS/Finished) was appended onto the CH2 still sitting in
|
|
226
|
+
// currentFlight, so a flight-5 retransmission replayed the ClientHello
|
|
227
|
+
// too — which a connected werift server answers with renegotiation(),
|
|
228
|
+
// destroying the session instead of recovering it.
|
|
229
|
+
newFlightPending: false,
|
|
175
230
|
retransmitTimer: null,
|
|
176
231
|
retransmitCount: 0,
|
|
177
232
|
retransmitTimeout: 1000,
|
|
@@ -219,24 +274,17 @@ function DTLSSession(options) {
|
|
|
219
274
|
// Server defaults — TLSSession client auto-populates these, but server expects them set externally
|
|
220
275
|
if (isServer) {
|
|
221
276
|
if (!tlsSetup.local_supported_cipher_suites) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
ciphers.push(0xC02F, 0xC030, 0xC02B, 0xC02C); // TLS 1.2 ECDHE ciphers
|
|
228
|
-
}
|
|
229
|
-
tlsSetup.local_supported_cipher_suites = ciphers;
|
|
277
|
+
// crypto.js owns the default suite list (see default_cipher_suites).
|
|
278
|
+
tlsSetup.local_supported_cipher_suites = default_cipher_suites(
|
|
279
|
+
versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0,
|
|
280
|
+
versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0
|
|
281
|
+
);
|
|
230
282
|
}
|
|
231
283
|
if (!tlsSetup.local_supported_groups) {
|
|
232
|
-
tlsSetup.local_supported_groups =
|
|
284
|
+
tlsSetup.local_supported_groups = ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
|
|
233
285
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
0x0403, 0x0503, 0x0603, // ECDSA
|
|
237
|
-
0x0807, 0x0808, // EdDSA
|
|
238
|
-
0x0401, 0x0501, 0x0601, // PKCS#1
|
|
239
|
-
];
|
|
286
|
+
// signing.js owns the scheme list; DTLS spans 1.2+1.3 so include PKCS#1.
|
|
287
|
+
tlsSetup.local_supported_signature_algorithms = default_signature_schemes();
|
|
240
288
|
}
|
|
241
289
|
|
|
242
290
|
// Server cert/key
|
|
@@ -274,10 +322,18 @@ function DTLSSession(options) {
|
|
|
274
322
|
}
|
|
275
323
|
transcriptMsgSeqs.push(msgSeq);
|
|
276
324
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
325
|
+
// RFC 9147 §5.5: the DTLS 1.3 transcript is made of *TLS-style* Handshake
|
|
326
|
+
// messages — message_seq, fragment_offset and fragment_length are REMOVED.
|
|
327
|
+
// DTLS 1.2 is the opposite: RFC 6347 §4.2.6 keeps the 12-byte DTLS header
|
|
328
|
+
// in the transcript. Getting this backwards breaks the Finished MAC only,
|
|
329
|
+
// long after every message parsed cleanly — which is why it presents as a
|
|
330
|
+
// late, unrelated-looking failure.
|
|
331
|
+
let entry = (ctx.selectedVersion === DTLS_VERSION.DTLS1_2)
|
|
332
|
+
? build_dtls_handshake(data, msgSeq)
|
|
333
|
+
: data; // TLS format for DTLS 1.3 or not-yet-negotiated
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
return entry;
|
|
281
337
|
};
|
|
282
338
|
|
|
283
339
|
/**
|
|
@@ -298,31 +354,70 @@ function DTLSSession(options) {
|
|
|
298
354
|
// Key derivation
|
|
299
355
|
// ============================================================
|
|
300
356
|
|
|
357
|
+
/**
|
|
358
|
+
* RFC 9147 §5.9: DTLS 1.3 derives with the "dtls13" label prefix (no trailing
|
|
359
|
+
* space), NOT TLS 1.3's "tls13 ". Using the TLS prefix yields a handshake in
|
|
360
|
+
* which every message parses and the transcripts match on both sides, and
|
|
361
|
+
* only the keys differ — the peer reports "bad record MAC" and nothing else.
|
|
362
|
+
* DTLS 1.2 does not use HKDF-Expand-Label at all (it uses the 1.2 PRF), so
|
|
363
|
+
* the branch only has to distinguish 1.3.
|
|
364
|
+
*/
|
|
365
|
+
function dtlsLabelPrefix() {
|
|
366
|
+
return ctx.selectedVersion === DTLS_VERSION.DTLS1_3
|
|
367
|
+
? LABEL_PREFIX_DTLS13
|
|
368
|
+
: LABEL_PREFIX_TLS13;
|
|
369
|
+
}
|
|
370
|
+
|
|
301
371
|
function deriveEpochKeys(secret) {
|
|
302
372
|
let cs = TLS_CIPHER_SUITES[ctx.cipherSuite];
|
|
303
373
|
let empty = new Uint8Array(0);
|
|
304
|
-
let
|
|
305
|
-
let
|
|
374
|
+
let lp = dtlsLabelPrefix();
|
|
375
|
+
let key = hkdf_expand_label(cs.hash, secret, 'key', empty, cs.keylen, lp);
|
|
376
|
+
let iv = hkdf_expand_label(cs.hash, secret, 'iv', empty, 12, lp);
|
|
306
377
|
|
|
307
378
|
let result = { key, iv };
|
|
308
379
|
|
|
309
380
|
// DTLS 1.3: derive sn_key for record number encryption
|
|
310
381
|
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
311
|
-
result.snKey = derive_sn_key(cs.hash, secret, ctx.cipherSuite);
|
|
382
|
+
result.snKey = derive_sn_key(cs.hash, secret, ctx.cipherSuite, dtlsLabelPrefix());
|
|
312
383
|
result.algo = getAeadAlgo(ctx.cipherSuite);
|
|
313
384
|
}
|
|
314
385
|
|
|
315
386
|
return result;
|
|
316
387
|
}
|
|
317
388
|
|
|
389
|
+
/**
|
|
390
|
+
* The DTLS epoch a given key generation belongs to.
|
|
391
|
+
*
|
|
392
|
+
* DTLS 1.3 (RFC 9147 §6.1) numbers epochs 0=plaintext, 1=early data,
|
|
393
|
+
* 2=handshake, 3=application. DTLS 1.2 uses a single encrypted epoch 1.
|
|
394
|
+
* Which numbering applies depends on the negotiated version, so this must
|
|
395
|
+
* resolve the version FIRST and never assume a previous sync happened.
|
|
396
|
+
*
|
|
397
|
+
* The two callers used to differ: the handshake-secrets handler synced the
|
|
398
|
+
* version from the TLS session, the application-secrets handler did not sync
|
|
399
|
+
* at all. That asymmetry is only harmless while the version happens to be
|
|
400
|
+
* committed by the time each event fires. When it is not — the TLS engine
|
|
401
|
+
* commits negotiated values at the END of a reactive pass, and these events
|
|
402
|
+
* are emitted DURING one — the keys are filed under epoch 1 while records are
|
|
403
|
+
* later built for epoch 2/3. buildRecord then finds no keys, returns null,
|
|
404
|
+
* and the record is dropped silently: the peer sees nothing at all and times
|
|
405
|
+
* out. One helper, so both generations decide the same way.
|
|
406
|
+
*/
|
|
407
|
+
function dtlsEpochForSecrets(isApplication) {
|
|
408
|
+
if (ctx.selectedVersion === null && tls.context.selected_version) {
|
|
409
|
+
ctx.selectedVersion = tls.context.selected_version;
|
|
410
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) fixTranscriptForDtls12();
|
|
411
|
+
}
|
|
412
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) return isApplication ? 3 : 2;
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
|
|
318
416
|
tls.on('handshakeSecrets', function(localSecret, remoteSecret) {
|
|
319
|
-
|
|
320
|
-
if (ctx.selectedVersion === null) ctx.selectedVersion = tls.context.selected_version;
|
|
417
|
+
let dtlsEpoch = dtlsEpochForSecrets(false);
|
|
321
418
|
ctx.cipherSuite = tls.getCipher();
|
|
322
419
|
ctx.hashName = TLS_CIPHER_SUITES[ctx.cipherSuite].hash;
|
|
323
420
|
|
|
324
|
-
let dtlsEpoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 2 : 1;
|
|
325
|
-
|
|
326
421
|
ctx.keys[dtlsEpoch] = {
|
|
327
422
|
write: deriveEpochKeys(localSecret),
|
|
328
423
|
read: deriveEpochKeys(remoteSecret),
|
|
@@ -331,7 +426,7 @@ function DTLSSession(options) {
|
|
|
331
426
|
});
|
|
332
427
|
|
|
333
428
|
tls.on('appSecrets', function(localSecret, remoteSecret) {
|
|
334
|
-
let dtlsEpoch =
|
|
429
|
+
let dtlsEpoch = dtlsEpochForSecrets(true);
|
|
335
430
|
|
|
336
431
|
ctx.keys[dtlsEpoch] = {
|
|
337
432
|
write: deriveEpochKeys(localSecret),
|
|
@@ -389,6 +484,14 @@ function DTLSSession(options) {
|
|
|
389
484
|
}
|
|
390
485
|
}
|
|
391
486
|
|
|
487
|
+
// Flight boundary: first outgoing record after accepting peer data
|
|
488
|
+
// starts a fresh flight (clears the previous flight's records and
|
|
489
|
+
// resets the retransmit budget — RFC 6347 §4.2.4).
|
|
490
|
+
if (ctx.newFlightPending) {
|
|
491
|
+
ctx.newFlightPending = false;
|
|
492
|
+
startNewFlight();
|
|
493
|
+
}
|
|
494
|
+
|
|
392
495
|
// DTLS 1.2: send CCS before first encrypted message (Finished)
|
|
393
496
|
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && tlsEpoch === 1 && !ctx.localCcsSent) {
|
|
394
497
|
sendCCS();
|
|
@@ -403,11 +506,39 @@ function DTLSSession(options) {
|
|
|
403
506
|
// Build DTLS records and emit
|
|
404
507
|
for (let i = 0; i < frags.length; i++) {
|
|
405
508
|
let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
|
|
406
|
-
|
|
407
|
-
|
|
509
|
+
// The flight captures the fragment, not the record — see currentFlight.
|
|
510
|
+
ctx.currentFlight.push({ epoch: dtlsEpoch, contentType: CT.HANDSHAKE, payload: frags[i] });
|
|
511
|
+
if (record) {
|
|
512
|
+
ev.emit('packet', record);
|
|
513
|
+
} else {
|
|
514
|
+
// buildRecord refused to build (no keys for an encrypted epoch).
|
|
515
|
+
// Dropping the record here without a word is how a key-epoch mismatch
|
|
516
|
+
// turns into an unexplained peer timeout instead of a diagnosable
|
|
517
|
+
// failure: nothing reaches the wire and nothing is reported. Surface
|
|
518
|
+
// it — the handshake cannot proceed either way.
|
|
519
|
+
ev.emit('error', new Error(
|
|
520
|
+
'DTLS: could not build outgoing handshake record for epoch ' + dtlsEpoch +
|
|
521
|
+
' — no keys installed for that epoch'));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
408
524
|
}
|
|
409
525
|
|
|
410
|
-
// Start retransmit timer after sending flight messages
|
|
526
|
+
// Start retransmit timer after sending flight messages.
|
|
527
|
+
//
|
|
528
|
+
// BUGFIX (one-shot initial ClientHello): ctx.state only left 'idle'
|
|
529
|
+
// inside feedDatagram — i.e. on the first INBOUND datagram. A
|
|
530
|
+
// client's first flight is sent before anything has been received,
|
|
531
|
+
// so the 'handshaking'-only guard below never armed the retransmit
|
|
532
|
+
// timer for flight 1. A lost or early-dropped ClientHello — e.g. one
|
|
533
|
+
// arriving while the peer's ICE layer still discards non-STUN
|
|
534
|
+
// traffic, a routine race right after nomination — was therefore
|
|
535
|
+
// never retransmitted, and the handshake hung silently forever
|
|
536
|
+
// (surfacing as connectionState stuck at 'connecting'). RFC 6347
|
|
537
|
+
// §4.2.4's flight state machine has no unguarded flights: every
|
|
538
|
+
// flight except the handshake-final one must sit under a retransmit
|
|
539
|
+
// timer. Sending a handshake flight IS handshaking — reflect that in
|
|
540
|
+
// the state before consulting it.
|
|
541
|
+
if (ctx.state === 'idle') ctx.state = 'handshaking';
|
|
411
542
|
if (ctx.state === 'handshaking') {
|
|
412
543
|
startRetransmitTimer();
|
|
413
544
|
}
|
|
@@ -419,8 +550,21 @@ function DTLSSession(options) {
|
|
|
419
550
|
function buildRecord(epoch, contentType, payload) {
|
|
420
551
|
let epochKeys = ctx.keys[epoch];
|
|
421
552
|
|
|
553
|
+
|
|
422
554
|
if (!epochKeys) {
|
|
423
|
-
//
|
|
555
|
+
// DTLS 1.3 epochs 2 (handshake) and 3 (application) are ALWAYS
|
|
556
|
+
// encrypted — they carry the unified header (first byte 0x2c–0x2f). If
|
|
557
|
+
// we reach here without keys for such an epoch, emitting a classic
|
|
558
|
+
// 13-byte plaintext header (first byte = content type, e.g. 0x16/0x1a)
|
|
559
|
+
// would make the peer reject the record as "bad type byte" (RFC 9147
|
|
560
|
+
// §4). That only happens on a caller ordering bug (record built before
|
|
561
|
+
// its epoch's keys were installed); surface it instead of putting a
|
|
562
|
+
// malformed record on the wire. Epoch 0 is legitimately plaintext.
|
|
563
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3 && epoch >= 2) {
|
|
564
|
+
ev.emit('error', new Error('DTLS 1.3: attempted to build epoch ' + epoch + ' record before keys installed'));
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
// No keys → plaintext record (epoch 0, or DTLS 1.2 pre-CCS)
|
|
424
568
|
return buildDtlsPlaintext(contentType, epoch, nextWriteSeq(epoch), payload);
|
|
425
569
|
}
|
|
426
570
|
|
|
@@ -481,6 +625,11 @@ function DTLSSession(options) {
|
|
|
481
625
|
*/
|
|
482
626
|
function sendAlertRecord(epoch, alertData) {
|
|
483
627
|
let record = buildRecord(epoch, CT.ALERT, alertData);
|
|
628
|
+
// buildRecord returns null when it refuses to emit a malformed record
|
|
629
|
+
// (DTLS 1.3 epoch >= 2 with no keys installed). Emitting that null as a
|
|
630
|
+
// packet pushes `null` into the transport; drop it instead — we already
|
|
631
|
+
// surfaced the reason through 'error'.
|
|
632
|
+
if (!record) return;
|
|
484
633
|
ev.emit('packet', record);
|
|
485
634
|
}
|
|
486
635
|
|
|
@@ -490,7 +639,21 @@ function DTLSSession(options) {
|
|
|
490
639
|
function sendCCS() {
|
|
491
640
|
if (ctx.localCcsSent) return;
|
|
492
641
|
ctx.localCcsSent = true;
|
|
642
|
+
// The server's final flight OPENS with CCS — honor the flight boundary
|
|
643
|
+
// here too, before this record is captured.
|
|
644
|
+
if (ctx.newFlightPending) {
|
|
645
|
+
ctx.newFlightPending = false;
|
|
646
|
+
startNewFlight();
|
|
647
|
+
}
|
|
493
648
|
let record = buildDtlsPlaintext(CT.CHANGE_CIPHER_SPEC, 0, nextWriteSeq(0), new Uint8Array([1]));
|
|
649
|
+
// RFC 6347 §4.2.4: the CCS is part of the flight and MUST be included
|
|
650
|
+
// when the flight is retransmitted. Captured as a descriptor (epoch 0,
|
|
651
|
+
// plaintext), so resendFlight rebuilds it with a fresh seq like every
|
|
652
|
+
// other record. Previously it was emitted but never captured, so a
|
|
653
|
+
// retransmitted final flight arrived WITHOUT the cipher-spec change and
|
|
654
|
+
// the peer could never decrypt the accompanying Finished — an
|
|
655
|
+
// unrecoverable stall under single-datagram loss.
|
|
656
|
+
ctx.currentFlight.push({ epoch: 0, contentType: CT.CHANGE_CIPHER_SPEC, payload: new Uint8Array([1]) });
|
|
494
657
|
ev.emit('packet', record);
|
|
495
658
|
}
|
|
496
659
|
|
|
@@ -513,6 +676,17 @@ function DTLSSession(options) {
|
|
|
513
676
|
|
|
514
677
|
let records = parseDtlsDatagram(data, keysByEpoch);
|
|
515
678
|
|
|
679
|
+
if (LEMON_DEBUG) {
|
|
680
|
+
var _summ = [];
|
|
681
|
+
for (var _ri = 0; _ri < records.length; _ri++) {
|
|
682
|
+
var _r = records[_ri];
|
|
683
|
+
_summ.push('type=' + _r.type + ' epoch=' + _r.epoch + ' seq=' + _r.seq +
|
|
684
|
+
(_r.epoch > 0 && !_r.encrypted ? ' UNDECRYPTED' : ''));
|
|
685
|
+
}
|
|
686
|
+
_ldbg('rx datagram len=' + data.length + ' state=' + ctx.state +
|
|
687
|
+
' records: [' + _summ.join(' | ') + ']');
|
|
688
|
+
}
|
|
689
|
+
|
|
516
690
|
// RFC 9147 §7: collect the record numbers of handshake records we received
|
|
517
691
|
// in this datagram so we can ACK them once, after processing. Only for
|
|
518
692
|
// DTLS 1.3, and only while we're still handshaking (post-handshake ACKs of
|
|
@@ -520,6 +694,24 @@ function DTLSSession(options) {
|
|
|
520
694
|
let ackable = [];
|
|
521
695
|
|
|
522
696
|
for (let i = 0; i < records.length; i++) {
|
|
697
|
+
// A record we could not decrypt when the datagram was parsed. Keys may
|
|
698
|
+
// have arrived since — processing an earlier record in THIS datagram
|
|
699
|
+
// (the ServerHello) is exactly what installs them. Retry now, in order,
|
|
700
|
+
// so a peer that packs its whole flight into one datagram is handled
|
|
701
|
+
// without waiting for a retransmit.
|
|
702
|
+
if (records[i].deferred) {
|
|
703
|
+
let epochKeys = ctx.keys[records[i].epoch];
|
|
704
|
+
let readKeys = epochKeys ? epochKeys.read : null;
|
|
705
|
+
if (!readKeys) continue; // still unreadable — let the peer retransmit
|
|
706
|
+
let dec = null;
|
|
707
|
+
try { dec = decryptEncryptedDtls13(records[i].raw, readKeys); } catch (e) { dec = null; }
|
|
708
|
+
if (!dec) continue;
|
|
709
|
+
records[i] = {
|
|
710
|
+
type: dec.type, epoch: dec.epoch, seq: dec.seq,
|
|
711
|
+
content: dec.content, encrypted: true, deferred: false,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
523
715
|
if (records[i].type === CT.HANDSHAKE &&
|
|
524
716
|
ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
525
717
|
ackable.push({ epoch: records[i].epoch, seq: records[i].seq });
|
|
@@ -553,6 +745,18 @@ function DTLSSession(options) {
|
|
|
553
745
|
|
|
554
746
|
function processRecord(record) {
|
|
555
747
|
if (record.type === CT.HANDSHAKE) {
|
|
748
|
+
// An epoch>0 record that could not be decrypted (keys not yet
|
|
749
|
+
// installed, AEAD failure) must NOT be fed to the handshake
|
|
750
|
+
// parser: its content is ciphertext, and parsing it yields a
|
|
751
|
+
// garbage "message" with attacker-controllable type/length fields.
|
|
752
|
+
// Drop it — DTLS is loss-tolerant by design, and the peer's (or
|
|
753
|
+
// our own flight-elicited) retransmission delivers a fresh copy
|
|
754
|
+
// once it can be decrypted.
|
|
755
|
+
if (record.epoch > 0 && !record.encrypted) {
|
|
756
|
+
_ldbg('skip undecrypted epoch=' + record.epoch + ' seq=' + record.seq +
|
|
757
|
+
' len=' + (record.content ? record.content.length : 0));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
556
760
|
processHandshakeRecord(record.content, record.epoch, record.encrypted);
|
|
557
761
|
} else if (record.type === CT.APPLICATION_DATA) {
|
|
558
762
|
if (ctx.state === 'connected') {
|
|
@@ -593,6 +797,8 @@ function DTLSSession(options) {
|
|
|
593
797
|
function processHandshakeRecord(data, epoch, encrypted) {
|
|
594
798
|
// A handshake record may contain multiple handshake messages
|
|
595
799
|
let off = 0;
|
|
800
|
+
let sawMessage = false; // record contained ≥1 parseable handshake message
|
|
801
|
+
let anyNew = false; // ≥1 message advanced our state (not a duplicate)
|
|
596
802
|
while (off + 12 <= data.length) {
|
|
597
803
|
let parsed = parse_dtls_handshake(data.subarray(off));
|
|
598
804
|
|
|
@@ -600,21 +806,65 @@ function DTLSSession(options) {
|
|
|
600
806
|
let totalLen = parsed.length;
|
|
601
807
|
let fragOffset = parsed.frag_offset;
|
|
602
808
|
let fragLen = parsed.frag_length;
|
|
809
|
+
sawMessage = true;
|
|
810
|
+
_ldbg('hs msg: type=' + parsed.type + ' msgSeq=' + msgSeq +
|
|
811
|
+
' len=' + totalLen + ' frag=' + fragOffset + '+' + fragLen +
|
|
812
|
+
' nextReadMsgSeq=' + ctx.nextReadMsgSeq);
|
|
603
813
|
|
|
604
814
|
// Is this a complete message or a fragment?
|
|
605
815
|
if (fragOffset === 0 && fragLen === totalLen) {
|
|
606
816
|
// Complete message — feed directly
|
|
607
|
-
deliverHandshakeMessage(parsed.type, parsed.body, msgSeq);
|
|
817
|
+
if (deliverHandshakeMessage(parsed.type, parsed.body, msgSeq)) anyNew = true;
|
|
608
818
|
} else {
|
|
609
819
|
// Fragment — reassemble
|
|
610
|
-
reassembleFragment(parsed);
|
|
820
|
+
if (reassembleFragment(parsed)) anyNew = true;
|
|
611
821
|
}
|
|
612
822
|
|
|
613
823
|
off += 12 + fragLen;
|
|
614
824
|
}
|
|
615
825
|
|
|
616
|
-
//
|
|
617
|
-
|
|
826
|
+
// ── Implicit-ACK discipline (RFC 6347 §4.2.4) ──
|
|
827
|
+
// Receiving a message from the peer's NEXT flight is the implicit ACK
|
|
828
|
+
// of ours — only then may our retransmit timer be cancelled. The
|
|
829
|
+
// previous unconditional cancelRetransmit() here also fired on pure
|
|
830
|
+
// RETRANSMISSIONS of the peer's PREVIOUS flight, which mean the exact
|
|
831
|
+
// opposite: our reply was lost. Two concrete deadlocks that caused:
|
|
832
|
+
// 1. Our cookie-bearing CH2 is lost → server retransmits its HVR
|
|
833
|
+
// (seq 0, discarded as a duplicate) → cancel killed the CH2
|
|
834
|
+
// timer → CH2 never resent → handshake dead.
|
|
835
|
+
// 2. Our final flight (CKE/CCS/Finished) is lost → server
|
|
836
|
+
// retransmits its flight (all seqs below nextReadMsgSeq,
|
|
837
|
+
// discarded) → cancel killed the flight timer → dead.
|
|
838
|
+
// NOTE: the implicit-ACK cancel deliberately does NOT live here.
|
|
839
|
+
// Everything in this function runs AFTER deliverHandshakeMessage,
|
|
840
|
+
// and TLS produces our response flight synchronously inside the
|
|
841
|
+
// feed — so a cancel at this point lands on the freshly-armed timer
|
|
842
|
+
// of the flight we JUST sent, not on the acknowledged one. The
|
|
843
|
+
// cancel happens at the acceptance points inside
|
|
844
|
+
// deliverHandshakeMessage instead, before the feed.
|
|
845
|
+
if (anyNew) {
|
|
846
|
+
// handled at acceptance time
|
|
847
|
+
} else if (sawMessage && ctx.currentFlight.length > 0 &&
|
|
848
|
+
ctx.state !== 'connected' &&
|
|
849
|
+
ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 &&
|
|
850
|
+
// Damping: a peer retransmits its flight as SEVERAL
|
|
851
|
+
// datagrams; without a guard, EACH duplicate datagram
|
|
852
|
+
// elicited a full resend of our flight — a ×N
|
|
853
|
+
// amplification burst that itself increased loss
|
|
854
|
+
// pressure. One resend per 250ms answers the peer's
|
|
855
|
+
// whole retransmitted flight exactly once.
|
|
856
|
+
(Date.now() - (ctx._lastDupElicitedResend || 0)) >= 250) {
|
|
857
|
+
ctx._lastDupElicitedResend = Date.now();
|
|
858
|
+
// Pure duplicate of a previous peer flight while we still have an
|
|
859
|
+
// outstanding flight of our own: the peer is telling us it never
|
|
860
|
+
// got our reply. Resend it immediately (RFC 6347 §4.2.4 responder
|
|
861
|
+
// behavior) rather than waiting out our own backoff. The regular
|
|
862
|
+
// timer keeps running unchanged, so the retransmit budget still
|
|
863
|
+
// bounds the total effort; the resend rate is naturally bounded by
|
|
864
|
+
// the peer's own retransmission schedule. DTLS 1.3 is excluded —
|
|
865
|
+
// it uses explicit ACKs (RFC 9147 §7) instead of flight inference.
|
|
866
|
+
resendFlight();
|
|
867
|
+
}
|
|
618
868
|
}
|
|
619
869
|
|
|
620
870
|
/**
|
|
@@ -631,7 +881,7 @@ function DTLSSession(options) {
|
|
|
631
881
|
let key = parsed.msg_seq;
|
|
632
882
|
|
|
633
883
|
// Ignore fragments for messages we've already delivered.
|
|
634
|
-
if (key < ctx.nextReadMsgSeq) return;
|
|
884
|
+
if (key < ctx.nextReadMsgSeq) return false;
|
|
635
885
|
|
|
636
886
|
// DoS guard: the 24-bit `length` is attacker-controlled on the first
|
|
637
887
|
// fragment. Reject anything claiming more than our cap BEFORE allocating,
|
|
@@ -639,7 +889,7 @@ function DTLSSession(options) {
|
|
|
639
889
|
if (parsed.length > ctx.maxHandshakeMessageSize) {
|
|
640
890
|
ev.emit('error', new Error('DTLS handshake message exceeds maxHandshakeMessageSize (' +
|
|
641
891
|
parsed.length + ' > ' + ctx.maxHandshakeMessageSize + ')'));
|
|
642
|
-
return;
|
|
892
|
+
return false;
|
|
643
893
|
}
|
|
644
894
|
|
|
645
895
|
let frag = ctx.fragments[key];
|
|
@@ -656,7 +906,15 @@ function DTLSSession(options) {
|
|
|
656
906
|
let end = parsed.frag_offset + parsed.frag_length;
|
|
657
907
|
|
|
658
908
|
// Bounds guard against a malformed fragment claiming to exceed totalLength.
|
|
659
|
-
if (end > frag.totalLength) return;
|
|
909
|
+
if (end > frag.totalLength) return false;
|
|
910
|
+
|
|
911
|
+
// Coverage before this fragment — used to decide whether it contributed
|
|
912
|
+
// anything new (a retransmitted fragment that only re-covers known bytes
|
|
913
|
+
// is a duplicate for implicit-ACK purposes).
|
|
914
|
+
let coveredBefore = 0;
|
|
915
|
+
for (let cr = 0; cr < frag.ranges.length; cr++) {
|
|
916
|
+
coveredBefore += frag.ranges[cr][1] - frag.ranges[cr][0];
|
|
917
|
+
}
|
|
660
918
|
|
|
661
919
|
// Copy the fragment bytes in (overlaps simply overwrite with identical data).
|
|
662
920
|
frag.buf.set(parsed.body.subarray(0, parsed.frag_length), start);
|
|
@@ -675,27 +933,44 @@ function DTLSSession(options) {
|
|
|
675
933
|
}
|
|
676
934
|
frag.ranges = merged;
|
|
677
935
|
|
|
936
|
+
let coveredAfter = 0;
|
|
937
|
+
for (let ca = 0; ca < merged.length; ca++) {
|
|
938
|
+
coveredAfter += merged[ca][1] - merged[ca][0];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
|
|
678
942
|
// Complete when a single range covers [0, totalLength).
|
|
679
943
|
if (merged.length === 1 && merged[0][0] === 0 && merged[0][1] === frag.totalLength) {
|
|
680
944
|
delete ctx.fragments[key];
|
|
681
|
-
deliverHandshakeMessage(frag.type, frag.buf, key);
|
|
945
|
+
return deliverHandshakeMessage(frag.type, frag.buf, key);
|
|
682
946
|
}
|
|
947
|
+
return coveredAfter > coveredBefore;
|
|
683
948
|
}
|
|
684
949
|
|
|
685
950
|
/**
|
|
686
951
|
* Deliver a complete handshake message to TLSSession.
|
|
687
952
|
* Strips DTLS reconstruction → builds TLS format.
|
|
953
|
+
*
|
|
954
|
+
* @returns {boolean} true if the message advanced our handshake state
|
|
955
|
+
* (delivered in-order, buffered out-of-order, or consumed as a valid
|
|
956
|
+
* HVR / cookie-exchange step); false if it was a duplicate or was
|
|
957
|
+
* dropped. processHandshakeRecord uses this for the implicit-ACK
|
|
958
|
+
* decision — only genuinely new data may cancel our retransmit timer.
|
|
688
959
|
*/
|
|
689
960
|
function deliverHandshakeMessage(type, body, msgSeq) {
|
|
690
961
|
// Skip if we've already processed this msg_seq (duplicate / retransmit).
|
|
691
|
-
if (msgSeq < ctx.nextReadMsgSeq) return;
|
|
962
|
+
if (msgSeq < ctx.nextReadMsgSeq) return false;
|
|
692
963
|
|
|
693
964
|
// Check for HelloVerifyRequest (DTLS 1.2 server→client, type=3)
|
|
694
965
|
if (type === 3 && !isServer) {
|
|
695
966
|
ctx.nextReadMsgSeq = msgSeq + 1;
|
|
967
|
+
// The HVR acknowledges CH1 (implicit ACK, RFC 6347 §4.2.4). No
|
|
968
|
+
// explicit cancel is needed: triggerClientHelloWithCookie arms the
|
|
969
|
+
// CH2 flight's own timer, and startRetransmitTimer clears any
|
|
970
|
+
// previous timer as its first step.
|
|
696
971
|
let hvr = parse_hello_verify_request(body);
|
|
697
972
|
triggerClientHelloWithCookie(hvr.cookie);
|
|
698
|
-
return;
|
|
973
|
+
return true;
|
|
699
974
|
}
|
|
700
975
|
|
|
701
976
|
// ---- Server-side HelloVerifyRequest cookie exchange (RFC 6347 §4.2.1) ----
|
|
@@ -707,27 +982,84 @@ function DTLSSession(options) {
|
|
|
707
982
|
//
|
|
708
983
|
// Note: msg_seq is NOT advanced for a rejected/HVR-triggering ClientHello, so
|
|
709
984
|
// the client's retried CH (which reuses msg_seq per RFC 6347) is still accepted.
|
|
710
|
-
if (type === 1 && isServer
|
|
711
|
-
ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) {
|
|
712
|
-
|
|
985
|
+
if (type === 1 && isServer) {
|
|
713
986
|
let ch = null;
|
|
714
987
|
try { ch = parse_hello({ kind: 'client', body: body }); } catch (e) { ch = null; }
|
|
988
|
+
|
|
989
|
+
// Does this ClientHello offer DTLS 1.3?
|
|
990
|
+
//
|
|
991
|
+
// RULE: a decision derived from an incoming message is read from that
|
|
992
|
+
// message, never from state the message itself is about to establish.
|
|
993
|
+
// (Stated in full next to params_to_set in tls_session.js; this is the
|
|
994
|
+
// fourth bug it has caused.) Read it from the MESSAGE, never
|
|
995
|
+
// from ctx.selectedVersion — when the first ClientHello arrives nothing
|
|
996
|
+
// has been negotiated yet, so a state-based test reads null and fails
|
|
997
|
+
// OPEN. That matters because the two rules below are MUST-level and
|
|
998
|
+
// version-specific.
|
|
999
|
+
let offersDtls13 = false;
|
|
1000
|
+
if (ch && Array.isArray(ch.supported_versions)) {
|
|
1001
|
+
offersDtls13 = ch.supported_versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// RFC 9147 §5.3: "A DTLS 1.3-only client MUST set the legacy_cookie field
|
|
1005
|
+
// to zero length. If a DTLS 1.3 ClientHello is received with any other
|
|
1006
|
+
// value in this field, the server MUST abort the handshake with an
|
|
1007
|
+
// 'illegal_parameter' alert." DTLS 1.3 carries cookies in the `cookie`
|
|
1008
|
+
// EXTENSION; the legacy body field is vestigial and must stay empty.
|
|
1009
|
+
if (offersDtls13 && ch && ch.dtls_cookie && ch.dtls_cookie.length > 0) {
|
|
1010
|
+
tls.sendAlert(2, 47); // fatal, illegal_parameter
|
|
1011
|
+
ev.emit('error', new Error(
|
|
1012
|
+
'DTLS 1.3 ClientHello carries a non-empty legacy_cookie (' +
|
|
1013
|
+
ch.dtls_cookie.length + ' bytes) — RFC 9147 §5.3 requires zero length'));
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// RFC 9147 §5.1: "DTLS 1.3-compliant implementations MUST NOT use the
|
|
1018
|
+
// HelloVerifyRequest to execute a return-routability check." DTLS 1.3
|
|
1019
|
+
// does return-routability with HelloRetryRequest + the cookie extension
|
|
1020
|
+
// instead. The old guard tested ctx.selectedVersion, which is null here,
|
|
1021
|
+
// so a DTLS 1.3 handshake could still be answered with an HVR.
|
|
1022
|
+
if (options.useCookies === true && !offersDtls13) {
|
|
1023
|
+
|
|
715
1024
|
let incomingCookie = (ch && ch.dtls_cookie) ? ch.dtls_cookie : new Uint8Array(0);
|
|
716
1025
|
|
|
717
1026
|
if (incomingCookie.length === 0) {
|
|
718
1027
|
// First ClientHello → send HVR with a fresh cookie. Don't process the CH,
|
|
719
1028
|
// don't advance msg_seq (the retry reuses the same msg_seq).
|
|
1029
|
+
// Counts as progress: a retransmitted cookieless CH means the client
|
|
1030
|
+
// never got our HVR — resending it here IS the correct response.
|
|
720
1031
|
sendHelloVerifyRequest();
|
|
721
|
-
return;
|
|
1032
|
+
return true;
|
|
722
1033
|
}
|
|
723
1034
|
|
|
724
1035
|
// Second ClientHello: the cookie MUST match what we issued.
|
|
725
1036
|
if (!ctx.hvrCookie || !timingSafeEqualU8(incomingCookie, ctx.hvrCookie)) {
|
|
726
1037
|
// Cookie mismatch → silently drop (RFC 6347: server discards). Don't
|
|
727
1038
|
// advance msg_seq; a correct retry can still arrive.
|
|
728
|
-
return;
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
// Cookie verified → accept this ClientHello as the handshake anchor.
|
|
1042
|
+
//
|
|
1043
|
+
// Strict RFC 6347 §4.2.2 (see the figure in the RFC): the cookie-
|
|
1044
|
+
// bearing CH2 arrives with message_seq 1, and our ServerHello must
|
|
1045
|
+
// be numbered 1 as well. The previous code assumed the retried CH
|
|
1046
|
+
// "reuses msg_seq" 0 — that is only true for a pure retransmission
|
|
1047
|
+
// of CH1 (HVR lost), not for the cookie-bearing CH2, so RFC-
|
|
1048
|
+
// compliant clients (pion, OpenSSL, fixed lemon-tls) had their CH2
|
|
1049
|
+
// buffered as "out of order" forever and the handshake deadlocked.
|
|
1050
|
+
//
|
|
1051
|
+
// Set the read cursor and the outgoing counter to 1; the in-order
|
|
1052
|
+
// check below then accepts exactly a seq-1 CH2 and rejects anything
|
|
1053
|
+
// else (including seq-0 CH2s from pre-fix lemon-tls clients — non-
|
|
1054
|
+
// compliant numbering is intentionally not accommodated). The
|
|
1055
|
+
// transcript stays consistent on both sides because
|
|
1056
|
+
// feedHandshakeToTls records the incoming wire seq and outgoing
|
|
1057
|
+
// messages record message_sent_seq at emission — which is exactly
|
|
1058
|
+
// what the Finished MAC hashes (RFC 6347 §4.2.6).
|
|
1059
|
+
ctx.nextReadMsgSeq = 1;
|
|
1060
|
+
tls.context.message_sent_seq = 1;
|
|
1061
|
+
// Fall through and process the ClientHello normally.
|
|
729
1062
|
}
|
|
730
|
-
// Cookie verified → fall through and process the ClientHello normally.
|
|
731
1063
|
}
|
|
732
1064
|
|
|
733
1065
|
// ---- In-order delivery (RFC 6347 §4.2.4 / RFC 9147 §5.2) ----
|
|
@@ -739,21 +1071,56 @@ function DTLSSession(options) {
|
|
|
739
1071
|
// Out of order — hold it (ignore a duplicate already buffered).
|
|
740
1072
|
if (!(msgSeq in ctx.pendingMessages)) {
|
|
741
1073
|
ctx.pendingMessages[msgSeq] = { type: type, body: body };
|
|
1074
|
+
return true; // new information, even if not yet deliverable
|
|
742
1075
|
}
|
|
743
|
-
return;
|
|
1076
|
+
return false; // duplicate of an already-buffered message
|
|
744
1077
|
}
|
|
745
1078
|
|
|
746
|
-
// msgSeq === nextReadMsgSeq →
|
|
1079
|
+
// msgSeq === nextReadMsgSeq → accept. Timer discipline, per the
|
|
1080
|
+
// RFC 6347 §4.2.4 state machine, in two halves:
|
|
1081
|
+
//
|
|
1082
|
+
// 1. A message from the peer's next flight is the implicit ACK of
|
|
1083
|
+
// ours — but it must RE-ARM the timer, not cancel it. While we
|
|
1084
|
+
// are in WAITING (peer's flight only partially received — e.g.
|
|
1085
|
+
// one datagram of the server's SH..SHD flight lost), timer
|
|
1086
|
+
// expiry retransmits OUR last flight, which elicits a full
|
|
1087
|
+
// retransmission of the peer's flight (a cookie-valid duplicate
|
|
1088
|
+
// CH2 makes werift resend flight 4, a duplicate Finished makes
|
|
1089
|
+
// it resend flight 6). An earlier revision cancelled here: one
|
|
1090
|
+
// lost datagram inside the peer's flight then left BOTH sides
|
|
1091
|
+
// timer-less and deadlocked — ~50% handshake failure at just 5%
|
|
1092
|
+
// random loss.
|
|
1093
|
+
//
|
|
1094
|
+
// 2. The re-arm must happen BEFORE feeding TLS: the feed can
|
|
1095
|
+
// synchronously produce our entire next flight, whose fresh
|
|
1096
|
+
// timer must not be clobbered afterwards (the original code
|
|
1097
|
+
// cancelled at the end of processHandshakeRecord — after the
|
|
1098
|
+
// response — so no responder flight was EVER protected by
|
|
1099
|
+
// retransmission).
|
|
1100
|
+
//
|
|
1101
|
+
// When the feed does respond, the flight boundary (newFlightPending
|
|
1102
|
+
// → startNewFlight) resets the retransmit budget and the outgoing
|
|
1103
|
+
// path arms the new flight's timer, superseding this re-arm.
|
|
1104
|
+
startRetransmitTimer();
|
|
1105
|
+
ctx.newFlightPending = true;
|
|
747
1106
|
feedHandshakeToTls(type, body, msgSeq);
|
|
748
1107
|
ctx.nextReadMsgSeq = msgSeq + 1;
|
|
749
1108
|
|
|
750
|
-
// Drain consecutively-numbered buffered messages.
|
|
1109
|
+
// Drain consecutively-numbered buffered messages. Same pre-feed
|
|
1110
|
+
// ordering applies; for a timer armed by an earlier feed in this very
|
|
1111
|
+
// drain, cancelling here would be wrong — but a mid-flight response
|
|
1112
|
+
// does not occur in TLS flows (a flight is answered only after its
|
|
1113
|
+
// final message), so each pre-feed cancel can only ever hit an
|
|
1114
|
+
// already-acknowledged flight's timer (usually already null).
|
|
751
1115
|
while (ctx.nextReadMsgSeq in ctx.pendingMessages) {
|
|
752
1116
|
let next = ctx.pendingMessages[ctx.nextReadMsgSeq];
|
|
753
1117
|
delete ctx.pendingMessages[ctx.nextReadMsgSeq];
|
|
1118
|
+
startRetransmitTimer();
|
|
1119
|
+
ctx.newFlightPending = true;
|
|
754
1120
|
feedHandshakeToTls(next.type, next.body, ctx.nextReadMsgSeq);
|
|
755
1121
|
ctx.nextReadMsgSeq++;
|
|
756
1122
|
}
|
|
1123
|
+
return true;
|
|
757
1124
|
}
|
|
758
1125
|
|
|
759
1126
|
/**
|
|
@@ -806,12 +1173,41 @@ function DTLSSession(options) {
|
|
|
806
1173
|
// RFC 6347: "the client MUST use the same parameter values"
|
|
807
1174
|
let ch2 = insertCookieIntoClientHello(ctx.savedClientHello, cookie);
|
|
808
1175
|
|
|
809
|
-
// Reset state
|
|
1176
|
+
// Reset state for the post-HVR handshake restart.
|
|
1177
|
+
//
|
|
1178
|
+
// RFC 6347 §4.2.1: the handshake transcript restarts at CH2 — the
|
|
1179
|
+
// cookieless CH1 and the HelloVerifyRequest are NOT included in the
|
|
1180
|
+
// handshake hash. Resetting the transcript is therefore correct.
|
|
1181
|
+
//
|
|
1182
|
+
// RFC 6347 §4.2.2, however, is explicit (see the figure in the RFC)
|
|
1183
|
+
// that message_seq does NOT restart:
|
|
1184
|
+
//
|
|
1185
|
+
// ClientHello (seq=1) ------> ← CH2 carries seq 1
|
|
1186
|
+
// (with cookie)
|
|
1187
|
+
// <------ ServerHello (seq=1)
|
|
1188
|
+
//
|
|
1189
|
+
// The previous code reset BOTH directions of the counter to 0: it
|
|
1190
|
+
// sent CH2 with msg_seq=0 and reset nextReadMsgSeq to 0. Against a
|
|
1191
|
+
// spec-compliant server (pion, werift, OpenSSL — all send an HVR and
|
|
1192
|
+
// then number ServerHello=1), the entire server flight (seq 1..N)
|
|
1193
|
+
// landed in pendingMessages waiting forever for a seq-0 message that
|
|
1194
|
+
// never comes: the handshake stalled silently — no error, no alert.
|
|
1195
|
+
// It only ever worked lemon-tls↔lemon-tls because our own server
|
|
1196
|
+
// sends an HVR only when options.useCookies is set, so the broken
|
|
1197
|
+
// path was never exercised same-stack.
|
|
1198
|
+
//
|
|
1199
|
+
// nextReadMsgSeq is deliberately NOT touched here: the HVR branch in
|
|
1200
|
+
// deliverHandshakeMessage already advanced it to 1, which is exactly
|
|
1201
|
+
// where the server's ServerHello will arrive.
|
|
810
1202
|
tls.context.transcript = [];
|
|
811
1203
|
tls.context.hello_sent = true;
|
|
812
1204
|
tls.context.dtls_cookie = cookie;
|
|
813
|
-
|
|
814
|
-
|
|
1205
|
+
// CH2 is message_seq 1. Set message_sent_seq BEFORE the transcript
|
|
1206
|
+
// push so transcriptHook / transcriptMsgSeqs record seq 1 for CH2 —
|
|
1207
|
+
// the Finished MAC hashes the DTLS reconstruction data including
|
|
1208
|
+
// message_seq (RFC 6347 §4.2.6), so a transcript recording seq 0 for
|
|
1209
|
+
// a wire message sent with seq 1 would fail Finished verification.
|
|
1210
|
+
tls.context.message_sent_seq = 1;
|
|
815
1211
|
ctx.currentFlight = [];
|
|
816
1212
|
ctx.fragments = {}; // discard any partial reassembly from CH1 flight
|
|
817
1213
|
ctx.pendingMessages = {}; // discard any out-of-order buffer from CH1 flight
|
|
@@ -824,13 +1220,13 @@ function DTLSSession(options) {
|
|
|
824
1220
|
tls.context.transcriptHook ? tls.context.transcriptHook(ch2) : ch2
|
|
825
1221
|
);
|
|
826
1222
|
|
|
827
|
-
// Build DTLS message and send
|
|
828
|
-
let dtlsMsg = build_dtls_handshake(ch2,
|
|
1223
|
+
// Build DTLS message and send — msg_seq 1 per the RFC figure above.
|
|
1224
|
+
let dtlsMsg = build_dtls_handshake(ch2, 1);
|
|
829
1225
|
let record = buildRecord(0, CT.HANDSHAKE, dtlsMsg);
|
|
830
|
-
ctx.currentFlight = [
|
|
831
|
-
ev.emit('packet', record);
|
|
1226
|
+
ctx.currentFlight = [{ epoch: 0, contentType: CT.HANDSHAKE, payload: dtlsMsg }];
|
|
1227
|
+
if (record) ev.emit('packet', record);
|
|
832
1228
|
|
|
833
|
-
tls.context.message_sent_seq =
|
|
1229
|
+
tls.context.message_sent_seq = 2;
|
|
834
1230
|
startRetransmitTimer();
|
|
835
1231
|
}
|
|
836
1232
|
|
|
@@ -839,6 +1235,21 @@ function DTLSSession(options) {
|
|
|
839
1235
|
// Flight tracking + retransmission
|
|
840
1236
|
// ============================================================
|
|
841
1237
|
|
|
1238
|
+
/**
|
|
1239
|
+
* Rebuild and re-emit the current flight with FRESH record sequence numbers.
|
|
1240
|
+
* See currentFlight's comment for why re-encryption (not byte replay) is
|
|
1241
|
+
* mandatory. Each descriptor goes back through buildRecord, which pulls the
|
|
1242
|
+
* next write seq for its epoch — so a retransmitted DTLS 1.3 record gets a
|
|
1243
|
+
* new nonce, and a monotonic seq the receiver can reconstruct.
|
|
1244
|
+
*/
|
|
1245
|
+
function resendFlight() {
|
|
1246
|
+
for (let i = 0; i < ctx.currentFlight.length; i++) {
|
|
1247
|
+
let f = ctx.currentFlight[i];
|
|
1248
|
+
let record = buildRecord(f.epoch, f.contentType, f.payload);
|
|
1249
|
+
if (record) ev.emit('packet', record);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
842
1253
|
function startRetransmitTimer() {
|
|
843
1254
|
cancelRetransmit();
|
|
844
1255
|
ctx.retransmitTimer = setTimeout(function() {
|
|
@@ -849,10 +1260,8 @@ function DTLSSession(options) {
|
|
|
849
1260
|
return;
|
|
850
1261
|
}
|
|
851
1262
|
|
|
852
|
-
// Retransmit entire current flight
|
|
853
|
-
|
|
854
|
-
ev.emit('packet', ctx.currentFlight[i]);
|
|
855
|
-
}
|
|
1263
|
+
// Retransmit entire current flight with fresh record seqs.
|
|
1264
|
+
resendFlight();
|
|
856
1265
|
|
|
857
1266
|
ctx.retransmitCount++;
|
|
858
1267
|
ctx.retransmitTimeout = Math.min(ctx.retransmitTimeout * 2, 60000);
|
|
@@ -910,6 +1319,8 @@ function DTLSSession(options) {
|
|
|
910
1319
|
});
|
|
911
1320
|
|
|
912
1321
|
tls.on('secureConnect', function() {
|
|
1322
|
+
_ldbg('handshake complete — secureConnect (version=0x' +
|
|
1323
|
+
((ctx.selectedVersion || tls.context.selected_version || 0).toString(16)) + ')');
|
|
913
1324
|
ctx.state = 'connected';
|
|
914
1325
|
cancelRetransmit();
|
|
915
1326
|
startNewFlight();
|
|
@@ -948,7 +1359,21 @@ function DTLSSession(options) {
|
|
|
948
1359
|
|
|
949
1360
|
function close() {
|
|
950
1361
|
if (ctx.state === 'closed') return;
|
|
951
|
-
|
|
1362
|
+
// Choose the epoch by what the PEER can read, not by whether WE consider
|
|
1363
|
+
// the handshake finished — the same rule the TLS branch uses for alerts.
|
|
1364
|
+
//
|
|
1365
|
+
// "not connected yet" does not mean "no keys": once handshake keys exist
|
|
1366
|
+
// the peer has switched to reading protected records, and a plaintext
|
|
1367
|
+
// close_notify arrives with a classic DTLSPlaintext header where the peer
|
|
1368
|
+
// requires the RFC 9147 §4 unified header. It then reports a bad record
|
|
1369
|
+
// type instead of a clean shutdown. Mirror the ACK path, which already
|
|
1370
|
+
// picks the highest epoch we hold write keys for.
|
|
1371
|
+
let epoch;
|
|
1372
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
1373
|
+
epoch = ctx.keys[3] ? 3 : (ctx.keys[2] ? 2 : 0);
|
|
1374
|
+
} else {
|
|
1375
|
+
epoch = ctx.keys[1] ? 1 : 0;
|
|
1376
|
+
}
|
|
952
1377
|
sendAlertRecord(epoch, new Uint8Array([1, 0])); // warning, close_notify
|
|
953
1378
|
ctx.state = 'closed';
|
|
954
1379
|
cancelRetransmit();
|
|
@@ -1003,6 +1428,16 @@ function DTLSSession(options) {
|
|
|
1003
1428
|
return tls.exportKeyingMaterial(length, label, context_value);
|
|
1004
1429
|
},
|
|
1005
1430
|
|
|
1431
|
+
/**
|
|
1432
|
+
* The negotiated DTLS-SRTP protection profile (RFC 5764), or null.
|
|
1433
|
+
* Surfaced here because DTLS-SRTP is the reason the exporter exists for
|
|
1434
|
+
* this transport — a caller that can export keying material must also be
|
|
1435
|
+
* able to learn which profile those keys are for.
|
|
1436
|
+
*/
|
|
1437
|
+
getSelectedSrtpProfile: function() {
|
|
1438
|
+
return tls.getSelectedSrtpProfile();
|
|
1439
|
+
},
|
|
1440
|
+
|
|
1006
1441
|
/** Peer's hello extensions: [{ type, name, data, value }]. */
|
|
1007
1442
|
getRemoteExtensions: function() { return tls.getRemoteExtensions(); },
|
|
1008
1443
|
|
|
@@ -1041,4 +1476,4 @@ function DTLSSession(options) {
|
|
|
1041
1476
|
return this;
|
|
1042
1477
|
}
|
|
1043
1478
|
|
|
1044
|
-
export default DTLSSession;
|
|
1479
|
+
export default DTLSSession;
|