lemon-tls 0.6.0 → 0.9.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/README.md +253 -20
- package/index.d.ts +94 -6
- package/index.js +126 -0
- package/package.json +2 -7
- package/src/compat.js +316 -45
- package/src/crypto.js +33 -3
- package/src/dtls_session.js +477 -41
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +210 -27
- package/src/secure_context.js +49 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +44 -5
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +44 -28
- package/src/tls_session.js +3003 -217
- package/src/tls_socket.js +839 -149
- package/src/utils.js +75 -2
- package/src/wire.js +220 -13
package/src/dtls_session.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import {
|
|
55
55
|
CT,
|
|
56
56
|
getAeadAlgo,
|
|
57
|
+
deriveKeys,
|
|
57
58
|
deriveKeys12,
|
|
58
59
|
decryptDtls12,
|
|
59
60
|
buildDtlsPlaintext,
|
|
@@ -66,7 +67,7 @@ import {
|
|
|
66
67
|
} from './record.js';
|
|
67
68
|
|
|
68
69
|
import { timingSafeEqualU8 } from './utils.js';
|
|
69
|
-
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS } from './session/ecdh.js';
|
|
70
|
+
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS, SERVER_DEFAULT_GROUPS } from './session/ecdh.js';
|
|
70
71
|
import { default_signature_schemes } from './session/signing.js';
|
|
71
72
|
|
|
72
73
|
|
|
@@ -127,6 +128,48 @@ function _ldbg() {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Log an OUTBOUND record. The existing _ldbg traces only what arrives —
|
|
133
|
+
* which is half the picture when a peer rejects something WE sent, and
|
|
134
|
+
* that is precisely the DTLS 1.3 interop case: BoringSSL reads our
|
|
135
|
+
* flight, dislikes it, and closes. Decoding the record header here
|
|
136
|
+
* (content type, epoch, sequence, and the handshake message type when
|
|
137
|
+
* the record is still in the clear) shows exactly what left the socket.
|
|
138
|
+
*/
|
|
139
|
+
function _ldbgTx(record, whereFrom) {
|
|
140
|
+
if (!LEMON_DEBUG || !record || !record.length) return;
|
|
141
|
+
try {
|
|
142
|
+
var ct = record[0];
|
|
143
|
+
// DTLS 1.3 UNIFIED HEADER (RFC 9147 4.1): an encrypted record starts
|
|
144
|
+
// with 001CSLEE — first byte in 0x20..0x3F — instead of the classic
|
|
145
|
+
// content-type/version/epoch/sequence header. Its epoch is the low
|
|
146
|
+
// two bits and the body is ciphertext, so all we can honestly report
|
|
147
|
+
// is that one went out and how big it was. Reading it as a classic
|
|
148
|
+
// header produced nonsense like "ct46 epoch=323".
|
|
149
|
+
if (ct >= 0x20 && ct <= 0x3F) {
|
|
150
|
+
_ldbg('TX [1.3 encrypted] epoch=' + (ct & 0x03) + ' len=' + record.length +
|
|
151
|
+
(whereFrom ? ' <' + whereFrom + '>' : ''));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
var epoch = (record[3] << 8) | record[4];
|
|
155
|
+
var names = { 20: 'ChangeCipherSpec', 21: 'Alert', 22: 'Handshake',
|
|
156
|
+
23: 'ApplicationData', 25: 'Heartbeat', 26: 'ACK' };
|
|
157
|
+
var hsNames = { 1:'ClientHello', 2:'ServerHello', 4:'NewSessionTicket',
|
|
158
|
+
8:'EncryptedExtensions', 11:'Certificate',
|
|
159
|
+
13:'CertificateRequest', 15:'CertificateVerify',
|
|
160
|
+
20:'Finished', 24:'KeyUpdate' };
|
|
161
|
+
var extra = '';
|
|
162
|
+
if (ct === 22 && record.length > 13) {
|
|
163
|
+
extra = ' hs=' + (hsNames[record[13]] || ('type' + record[13]));
|
|
164
|
+
}
|
|
165
|
+
if (ct === 21 && record.length >= 15) {
|
|
166
|
+
extra = ' alert level=' + record[13] + ' desc=' + record[14];
|
|
167
|
+
}
|
|
168
|
+
_ldbg('TX ' + (names[ct] || ('ct' + ct)) + ' epoch=' + epoch +
|
|
169
|
+
' len=' + record.length + extra + (whereFrom ? ' <' + whereFrom + '>' : ''));
|
|
170
|
+
} catch (e) {}
|
|
171
|
+
}
|
|
172
|
+
|
|
130
173
|
function DTLSSession(options) {
|
|
131
174
|
if (!(this instanceof DTLSSession)) return new DTLSSession(options);
|
|
132
175
|
options = options || {};
|
|
@@ -151,6 +194,11 @@ function DTLSSession(options) {
|
|
|
151
194
|
servername: options.servername,
|
|
152
195
|
SNICallback: options.SNICallback,
|
|
153
196
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
197
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
198
|
+
// DTLS peers in WebRTC present self-signed certificates by design and are
|
|
199
|
+
// authenticated by the SDP fingerprint, not by a CA chain. Falling back to
|
|
200
|
+
// the platform trust store here would mark every such peer unauthorized.
|
|
201
|
+
useDefaultCA: options.useDefaultCA === true,
|
|
154
202
|
ca: options.ca,
|
|
155
203
|
noTickets: options.noTickets,
|
|
156
204
|
requestCert: options.requestCert,
|
|
@@ -227,6 +275,23 @@ function DTLSSession(options) {
|
|
|
227
275
|
// too — which a connected werift server answers with renegotiation(),
|
|
228
276
|
// destroying the session instead of recovering it.
|
|
229
277
|
newFlightPending: false,
|
|
278
|
+
|
|
279
|
+
// ---- Epoch tracking (RFC 9147 §6.1) ----
|
|
280
|
+
// Sending and receiving epochs advance INDEPENDENTLY: a KeyUpdate rekeys
|
|
281
|
+
// one direction only. Until KeyUpdate existed both directions moved
|
|
282
|
+
// together through 0 -> 2 -> 3, so a single number sufficed; it does not
|
|
283
|
+
// any more. ctx.keys[e] therefore holds .write and .read for possibly
|
|
284
|
+
// different generations, and each half is installed on its own.
|
|
285
|
+
writeEpoch: null, // epoch we currently PROTECT outgoing records with
|
|
286
|
+
readEpoch: null, // highest epoch we hold READ keys for
|
|
287
|
+
// A KeyUpdate we have sent whose ACK has not come back. RFC 9147 §8:
|
|
288
|
+
// "implementations MUST NOT send records with the new keys or send a new
|
|
289
|
+
// KeyUpdate until the previous KeyUpdate has been acknowledged". So the
|
|
290
|
+
// new write keys are installed immediately but stay dormant here until
|
|
291
|
+
// the ACK arrives.
|
|
292
|
+
pendingWriteEpoch: null,
|
|
293
|
+
keyUpdateFlight: null, // { payload, epoch, timer, attempts, timeout }
|
|
294
|
+
|
|
230
295
|
retransmitTimer: null,
|
|
231
296
|
retransmitCount: 0,
|
|
232
297
|
retransmitTimeout: 1000,
|
|
@@ -271,6 +336,21 @@ function DTLSSession(options) {
|
|
|
271
336
|
if (options.groups) tlsSetup.local_supported_groups = options.groups;
|
|
272
337
|
if (options.alpnProtocols) tlsSetup.local_supported_alpns = options.alpnProtocols;
|
|
273
338
|
|
|
339
|
+
// Default key-exchange groups, BOTH roles. With DTLS 1.3 offered: the
|
|
340
|
+
// hybrid post-quantum group first (X25519MLKEM768, when node:crypto has
|
|
341
|
+
// ML-KEM — see ecdh.js), then the classical ones. In WebRTC either peer can
|
|
342
|
+
// end up the DTLS client, and harvest-now-decrypt-later applies to media
|
|
343
|
+
// too (Chrome rolls PQ DTLS out from 149). A client leading with the hybrid
|
|
344
|
+
// group sends a classical share beside it (tls_session), so a classical
|
|
345
|
+
// server costs nothing extra. DTLS 1.2 only: classical — a KEM does not fit
|
|
346
|
+
// its key exchange, and the server skips KEM groups there anyway.
|
|
347
|
+
if (!tlsSetup.local_supported_groups) {
|
|
348
|
+
// (ECDH_SUPPORTED_GROUPS leads with the hybrid too now; tls_session leaves
|
|
349
|
+
// a KEM group out of a hello that does not offer 1.3.)
|
|
350
|
+
tlsSetup.local_supported_groups = (versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0
|
|
351
|
+
? SERVER_DEFAULT_GROUPS : ECDH_SUPPORTED_GROUPS).slice();
|
|
352
|
+
}
|
|
353
|
+
|
|
274
354
|
// Server defaults — TLSSession client auto-populates these, but server expects them set externally
|
|
275
355
|
if (isServer) {
|
|
276
356
|
if (!tlsSetup.local_supported_cipher_suites) {
|
|
@@ -280,16 +360,20 @@ function DTLSSession(options) {
|
|
|
280
360
|
versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0
|
|
281
361
|
);
|
|
282
362
|
}
|
|
283
|
-
|
|
284
|
-
tlsSetup.local_supported_groups = ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
|
|
285
|
-
}
|
|
363
|
+
// (key-exchange groups: set above, for both roles)
|
|
286
364
|
// signing.js owns the scheme list; DTLS spans 1.2+1.3 so include PKCS#1.
|
|
287
365
|
tlsSetup.local_supported_signature_algorithms = default_signature_schemes();
|
|
288
366
|
}
|
|
289
367
|
|
|
290
|
-
// Server cert/key
|
|
368
|
+
// Server cert/key.
|
|
369
|
+
//
|
|
370
|
+
// The whole options object goes in, not just key+cert: createSecureContext
|
|
371
|
+
// also reads passphrase, ca, ciphers, sigalgs, ecdhCurve, honorCipherOrder,
|
|
372
|
+
// pfx and ocsp, and picking out two fields silently dropped all of them —
|
|
373
|
+
// an encrypted private key could never be decrypted here no matter what the
|
|
374
|
+
// caller passed. Same defect the TLS paths carried.
|
|
291
375
|
if (options.cert && options.key) {
|
|
292
|
-
let sctx = createSecureContext(
|
|
376
|
+
let sctx = createSecureContext(options);
|
|
293
377
|
tlsSetup.local_cert_chain = sctx.certificateChain;
|
|
294
378
|
tlsSetup.cert_private_key = sctx.privateKey;
|
|
295
379
|
}
|
|
@@ -322,15 +406,39 @@ function DTLSSession(options) {
|
|
|
322
406
|
}
|
|
323
407
|
transcriptMsgSeqs.push(msgSeq);
|
|
324
408
|
|
|
325
|
-
// RFC 9147 §5.
|
|
326
|
-
// messages
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
409
|
+
// RFC 9147 §5.9, verbatim: "Hash calculations include entire handshake
|
|
410
|
+
// messages, including DTLS-specific fields: message_seq,
|
|
411
|
+
// fragment_offset, and fragment_length. However, in order to remove
|
|
412
|
+
// sensitivity to handshake message fragmentation, the CertificateVerify
|
|
413
|
+
// and Finished messages MUST be computed as if each handshake message
|
|
414
|
+
// had been sent as a single fragment."
|
|
415
|
+
//
|
|
416
|
+
// So DTLS 1.3 keeps the 12-byte DTLS header in the transcript, exactly
|
|
417
|
+
// like DTLS 1.2 (RFC 6347 §4.2.6) — with fragment_offset 0 and
|
|
418
|
+
// fragment_length equal to the whole body, which is what
|
|
419
|
+
// build_dtls_handshake produces by default.
|
|
420
|
+
//
|
|
421
|
+
// This code did the OPPOSITE for 1.3 and stored TLS-format messages,
|
|
422
|
+
// citing a section number that says the reverse of what it does. The
|
|
423
|
+
// effect is invisible between two copies of this library — both make
|
|
424
|
+
// the same choice and their MACs agree — and only shows up against a
|
|
425
|
+
// conforming peer, where every message parses cleanly and then the
|
|
426
|
+
// Finished MAC fails: "Finished verify_data mismatch" against Chrome,
|
|
427
|
+
// with a transcript whose entries were each 8 bytes short.
|
|
428
|
+
// MEASURED AGAINST CHROME, and it contradicts the plain reading of
|
|
429
|
+
// RFC 9147 §5.9 ("Hash calculations include entire handshake
|
|
430
|
+
// messages, including DTLS-specific fields"):
|
|
431
|
+
// • TLS-format transcript -> Chrome's CertificateVerify VERIFIES,
|
|
432
|
+
// Finished MAC mismatches;
|
|
433
|
+
// • DTLS-format transcript -> Chrome's CertificateVerify FAILS.
|
|
434
|
+
// So BoringSSL signs CertificateVerify over the TLS-format transcript.
|
|
435
|
+
// Since CertificateVerify is the one we can check against a real peer
|
|
436
|
+
// and it passes this way, TLS format stays for 1.3 — the Finished
|
|
437
|
+
// difference is something else and must be found without breaking a
|
|
438
|
+
// signature that demonstrably verifies.
|
|
331
439
|
let entry = (ctx.selectedVersion === DTLS_VERSION.DTLS1_2)
|
|
332
440
|
? build_dtls_handshake(data, msgSeq)
|
|
333
|
-
: data;
|
|
441
|
+
: data;
|
|
334
442
|
|
|
335
443
|
|
|
336
444
|
return entry;
|
|
@@ -340,15 +448,36 @@ function DTLSSession(options) {
|
|
|
340
448
|
* Called when selectedVersion is first determined as DTLS 1.2.
|
|
341
449
|
* Retroactively converts all existing transcript entries to DTLS format.
|
|
342
450
|
*/
|
|
343
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Convert transcript entries recorded BEFORE the version was known into
|
|
453
|
+
* DTLS handshake format.
|
|
454
|
+
*
|
|
455
|
+
* Both DTLS 1.2 (RFC 6347 §4.2.6) and DTLS 1.3 (RFC 9147 §5.9) hash the
|
|
456
|
+
* 12-byte DTLS handshake header, but the first messages — our ClientHello
|
|
457
|
+
* and the peer's ServerHello — are pushed while selectedVersion is still
|
|
458
|
+
* null, so they land in TLS format and have to be rewritten once the
|
|
459
|
+
* negotiation resolves. Entries already in DTLS format are left alone:
|
|
460
|
+
* a second conversion would wrap a 12-byte header around one that is
|
|
461
|
+
* already there.
|
|
462
|
+
*/
|
|
463
|
+
function fixTranscriptForDtls() {
|
|
344
464
|
let t = tls.context.transcript;
|
|
345
465
|
for (let i = 0; i < t.length; i++) {
|
|
346
|
-
if (i
|
|
347
|
-
|
|
348
|
-
|
|
466
|
+
if (i >= transcriptMsgSeqs.length) continue;
|
|
467
|
+
let e = t[i];
|
|
468
|
+
if (!e || e.length < 4) continue;
|
|
469
|
+
// A DTLS entry is 12 + body; a TLS entry is 4 + body. The declared
|
|
470
|
+
// length lives in the same place in both, so comparing it against the
|
|
471
|
+
// actual size tells them apart without guessing.
|
|
472
|
+
let declared = (e[1] << 16) | (e[2] << 8) | e[3];
|
|
473
|
+
if (e.length === declared + 12) continue; // already DTLS format
|
|
474
|
+
t[i] = build_dtls_handshake(e, transcriptMsgSeqs[i]);
|
|
349
475
|
}
|
|
350
476
|
}
|
|
351
477
|
|
|
478
|
+
// Kept for callers that still use the old name.
|
|
479
|
+
function fixTranscriptForDtls12() { fixTranscriptForDtls(); }
|
|
480
|
+
|
|
352
481
|
|
|
353
482
|
// ============================================================
|
|
354
483
|
// Key derivation
|
|
@@ -369,17 +498,18 @@ function DTLSSession(options) {
|
|
|
369
498
|
}
|
|
370
499
|
|
|
371
500
|
function deriveEpochKeys(secret) {
|
|
372
|
-
let cs = TLS_CIPHER_SUITES[ctx.cipherSuite];
|
|
373
|
-
let empty = new Uint8Array(0);
|
|
374
501
|
let lp = dtlsLabelPrefix();
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
502
|
+
// record.js owns "traffic secret -> key + iv" and already exports it. This
|
|
503
|
+
// function used to re-derive both inline, which is the same two HKDF calls
|
|
504
|
+
// with the same arguments in a second place — precisely the shape of the
|
|
505
|
+
// label-prefix bug, where one copy of a derivation was fixed and another
|
|
506
|
+
// was not. Only the DTLS-specific extras belong here.
|
|
507
|
+
let result = deriveKeys(secret, ctx.cipherSuite, lp);
|
|
379
508
|
|
|
380
509
|
// DTLS 1.3: derive sn_key for record number encryption
|
|
381
510
|
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
382
|
-
result.snKey = derive_sn_key(
|
|
511
|
+
result.snKey = derive_sn_key(TLS_CIPHER_SUITES[ctx.cipherSuite].hash, secret,
|
|
512
|
+
ctx.cipherSuite, lp);
|
|
383
513
|
result.algo = getAeadAlgo(ctx.cipherSuite);
|
|
384
514
|
}
|
|
385
515
|
|
|
@@ -407,7 +537,10 @@ function DTLSSession(options) {
|
|
|
407
537
|
function dtlsEpochForSecrets(isApplication) {
|
|
408
538
|
if (ctx.selectedVersion === null && tls.context.selected_version) {
|
|
409
539
|
ctx.selectedVersion = tls.context.selected_version;
|
|
410
|
-
|
|
540
|
+
// Both DTLS versions hash the DTLS handshake header (RFC 6347 §4.2.6,
|
|
541
|
+
// RFC 9147 §5.9), so the early TLS-format entries must be rewritten
|
|
542
|
+
// whichever version was negotiated.
|
|
543
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) fixTranscriptForDtls();
|
|
411
544
|
}
|
|
412
545
|
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) return isApplication ? 3 : 2;
|
|
413
546
|
return 1;
|
|
@@ -433,8 +566,98 @@ function DTLSSession(options) {
|
|
|
433
566
|
read: deriveEpochKeys(remoteSecret),
|
|
434
567
|
};
|
|
435
568
|
ctx.writeSeq[dtlsEpoch] = 0;
|
|
569
|
+
|
|
570
|
+
// Baseline for KeyUpdate. Everything after this point counts up from here,
|
|
571
|
+
// per direction.
|
|
572
|
+
ctx.writeEpoch = dtlsEpoch;
|
|
573
|
+
ctx.readEpoch = dtlsEpoch;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
// ============================================================
|
|
578
|
+
// KeyUpdate (RFC 9147 §8)
|
|
579
|
+
// ============================================================
|
|
580
|
+
//
|
|
581
|
+
// TLSSession derives the new traffic secret and emits 'keyUpdate'; the DTLS
|
|
582
|
+
// layer owns everything the epoch implies. Nothing consumed this event
|
|
583
|
+
// before, so a KeyUpdate updated the session's notion of the traffic secret
|
|
584
|
+
// while the record layer kept protecting and deprotecting with the old keys.
|
|
585
|
+
// That is worse than not implementing it: on DTLS an undeprotectable record
|
|
586
|
+
// is silently discarded (§4.5.2), so the symptom is a connection that simply
|
|
587
|
+
// stops carrying data, with no alert and no error anywhere.
|
|
588
|
+
|
|
589
|
+
tls.on('keyUpdate', function(info) {
|
|
590
|
+
// DTLS 1.2 has no KeyUpdate; the TLS session only emits this for 1.3, but
|
|
591
|
+
// the guard keeps the epoch arithmetic below unreachable on the 1.2 path.
|
|
592
|
+
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return;
|
|
593
|
+
if (!info || !info.secret) return;
|
|
594
|
+
if (ctx.writeEpoch === null || ctx.readEpoch === null) return;
|
|
595
|
+
|
|
596
|
+
if (info.direction === 'send') {
|
|
597
|
+
// §8: only one KeyUpdate may be in flight. requestKeyUpdate() below
|
|
598
|
+
// refuses up front; reaching here with one pending means the TLS session
|
|
599
|
+
// rotated its secret anyway (e.g. an auto-response to the peer's
|
|
600
|
+
// update_requested while ours was still unacknowledged), which would
|
|
601
|
+
// strand an epoch. Say so rather than silently losing the connection.
|
|
602
|
+
if (ctx.pendingWriteEpoch !== null) {
|
|
603
|
+
ev.emit('error', new Error(
|
|
604
|
+
'DTLS: a KeyUpdate is already awaiting acknowledgement (RFC 9147 §8)'));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
let next = ctx.writeEpoch + 1;
|
|
608
|
+
// Install the new write keys, but keep protecting with the old epoch
|
|
609
|
+
// until the peer ACKs (§8). ctx.keys[next] may already carry a .read
|
|
610
|
+
// half from the peer's own update — merge, never overwrite.
|
|
611
|
+
ctx.keys[next] = Object.assign({}, ctx.keys[next], { write: deriveEpochKeys(info.secret) });
|
|
612
|
+
ctx.writeSeq[next] = 0;
|
|
613
|
+
ctx.pendingWriteEpoch = next;
|
|
614
|
+
|
|
615
|
+
} else if (info.direction === 'receive') {
|
|
616
|
+
let next = ctx.readEpoch + 1;
|
|
617
|
+
ctx.keys[next] = Object.assign({}, ctx.keys[next], { read: deriveEpochKeys(info.secret) });
|
|
618
|
+
ctx.readEpoch = next;
|
|
619
|
+
// The previous epoch's read keys are deliberately RETAINED. §8: records
|
|
620
|
+
// already in flight under the old epoch must still deprotect, and our
|
|
621
|
+
// ACK may be lost, so the peer may retransmit under the old keys.
|
|
622
|
+
}
|
|
436
623
|
});
|
|
437
624
|
|
|
625
|
+
/** Arm the post-handshake retransmit for an unacknowledged KeyUpdate. */
|
|
626
|
+
function armKeyUpdateRetransmit(payload, epoch) {
|
|
627
|
+
cancelKeyUpdateRetransmit();
|
|
628
|
+
ctx.keyUpdateFlight = {
|
|
629
|
+
payload: payload, epoch: epoch,
|
|
630
|
+
attempts: 0, timeout: ctx.retransmitTimeout, timer: null,
|
|
631
|
+
};
|
|
632
|
+
scheduleKeyUpdateRetransmit();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function scheduleKeyUpdateRetransmit() {
|
|
636
|
+
let f = ctx.keyUpdateFlight;
|
|
637
|
+
if (!f) return;
|
|
638
|
+
f.timer = setTimeout(function () {
|
|
639
|
+
if (!ctx.keyUpdateFlight) return;
|
|
640
|
+
if (f.attempts >= ctx.maxRetransmits) {
|
|
641
|
+
ev.emit('error', new Error('DTLS: KeyUpdate was never acknowledged'));
|
|
642
|
+
cancelKeyUpdateRetransmit();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
f.attempts++;
|
|
646
|
+
// Rebuild rather than replay: a retransmission is a NEW record and needs
|
|
647
|
+
// a fresh sequence number, or the peer's replay window rejects it.
|
|
648
|
+
let record = buildRecord(f.epoch, CT.HANDSHAKE, f.payload);
|
|
649
|
+
if (record) { _ldbgTx(record, 'keyupdate-rtx'); ev.emit('packet', record); }
|
|
650
|
+
f.timeout = Math.min(f.timeout * 2, 60000);
|
|
651
|
+
scheduleKeyUpdateRetransmit();
|
|
652
|
+
}, f.timeout);
|
|
653
|
+
if (f.timer && typeof f.timer.unref === 'function') f.timer.unref();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function cancelKeyUpdateRetransmit() {
|
|
657
|
+
if (ctx.keyUpdateFlight && ctx.keyUpdateFlight.timer) clearTimeout(ctx.keyUpdateFlight.timer);
|
|
658
|
+
ctx.keyUpdateFlight = null;
|
|
659
|
+
}
|
|
660
|
+
|
|
438
661
|
|
|
439
662
|
// ============================================================
|
|
440
663
|
// Outgoing: TLSSession message → DTLS datagram
|
|
@@ -461,7 +684,11 @@ function DTLSSession(options) {
|
|
|
461
684
|
if (tlsEpoch === 0) {
|
|
462
685
|
dtlsEpoch = 0;
|
|
463
686
|
} else if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
464
|
-
|
|
687
|
+
// epoch 2 = handshake. Anything above rides the CURRENT application
|
|
688
|
+
// epoch, which is 3 until a KeyUpdate moves it to 4, 5, ... Hardcoding 3
|
|
689
|
+
// meant every post-handshake message stayed on the pre-rekey epoch.
|
|
690
|
+
dtlsEpoch = (tlsEpoch === 1) ? 2
|
|
691
|
+
: (ctx.writeEpoch !== null ? ctx.writeEpoch : 3);
|
|
465
692
|
} else {
|
|
466
693
|
dtlsEpoch = 1; // DTLS 1.2: epoch 1 for all encrypted
|
|
467
694
|
}
|
|
@@ -504,12 +731,30 @@ function DTLSSession(options) {
|
|
|
504
731
|
let frags = fragmentMessage(dtlsMsg, data, seq);
|
|
505
732
|
|
|
506
733
|
// Build DTLS records and emit
|
|
734
|
+
// RFC 9147 §5.8.4: post-handshake messages do NOT belong to the handshake
|
|
735
|
+
// flight. Each category gets an independent state machine; folding a
|
|
736
|
+
// KeyUpdate into currentFlight would make a handshake retransmission
|
|
737
|
+
// replay it, and a KeyUpdate ACK cancel the handshake's timer.
|
|
738
|
+
let isPostHandshakeKU = (type === 'key_update' &&
|
|
739
|
+
ctx.selectedVersion === DTLS_VERSION.DTLS1_3);
|
|
740
|
+
if (isPostHandshakeKU) {
|
|
741
|
+
for (let i = 0; i < frags.length; i++) {
|
|
742
|
+
let kuRecord = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
|
|
743
|
+
if (kuRecord) { _ldbgTx(kuRecord, 'keyupdate'); ev.emit('packet', kuRecord); }
|
|
744
|
+
}
|
|
745
|
+
// KeyUpdate must be acknowledged (§8), so it is retransmitted on its own
|
|
746
|
+
// timer until an ACK arrives. Only the last fragment is tracked; a
|
|
747
|
+
// KeyUpdate body is 1 byte and never fragments in practice.
|
|
748
|
+
armKeyUpdateRetransmit(frags[frags.length - 1], dtlsEpoch);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
507
752
|
for (let i = 0; i < frags.length; i++) {
|
|
508
753
|
let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
|
|
509
754
|
// The flight captures the fragment, not the record — see currentFlight.
|
|
510
755
|
ctx.currentFlight.push({ epoch: dtlsEpoch, contentType: CT.HANDSHAKE, payload: frags[i] });
|
|
511
756
|
if (record) {
|
|
512
|
-
ev.emit('packet', record);
|
|
757
|
+
_ldbgTx(record, 'L543'); ev.emit('packet', record);
|
|
513
758
|
} else {
|
|
514
759
|
// buildRecord refused to build (no keys for an encrypted epoch).
|
|
515
760
|
// Dropping the record here without a word is how a key-epoch mismatch
|
|
@@ -630,7 +875,7 @@ function DTLSSession(options) {
|
|
|
630
875
|
// packet pushes `null` into the transport; drop it instead — we already
|
|
631
876
|
// surfaced the reason through 'error'.
|
|
632
877
|
if (!record) return;
|
|
633
|
-
ev.emit('packet', record);
|
|
878
|
+
_ldbgTx(record, 'L664'); ev.emit('packet', record);
|
|
634
879
|
}
|
|
635
880
|
|
|
636
881
|
/**
|
|
@@ -654,7 +899,7 @@ function DTLSSession(options) {
|
|
|
654
899
|
// the peer could never decrypt the accompanying Finished — an
|
|
655
900
|
// unrecoverable stall under single-datagram loss.
|
|
656
901
|
ctx.currentFlight.push({ epoch: 0, contentType: CT.CHANGE_CIPHER_SPEC, payload: new Uint8Array([1]) });
|
|
657
|
-
ev.emit('packet', record);
|
|
902
|
+
_ldbgTx(record, 'L688'); ev.emit('packet', record);
|
|
658
903
|
}
|
|
659
904
|
|
|
660
905
|
|
|
@@ -693,6 +938,15 @@ function DTLSSession(options) {
|
|
|
693
938
|
// NewSessionTicket etc. are optional and skipped here for simplicity).
|
|
694
939
|
let ackable = [];
|
|
695
940
|
|
|
941
|
+
// Whether these records arrived AFTER the handshake must be decided here,
|
|
942
|
+
// before the loop — not after it. The datagram that completes the
|
|
943
|
+
// handshake flips ctx.state to 'connected' partway through, so reading the
|
|
944
|
+
// state afterwards misclassifies the handshake's own epoch-2 records as
|
|
945
|
+
// post-handshake and ACKs them inside the application-data epoch. That is
|
|
946
|
+
// the spurious epoch-3 ACK that makes a peer answer decode_error on the
|
|
947
|
+
// NEXT record — so a perfectly good SCTP COOKIE_ECHO takes the blame.
|
|
948
|
+
let wasConnectedOnEntry = (ctx.state === 'connected');
|
|
949
|
+
|
|
696
950
|
for (let i = 0; i < records.length; i++) {
|
|
697
951
|
// A record we could not decrypt when the datagram was parsed. Keys may
|
|
698
952
|
// have arrived since — processing an earlier record in THIS datagram
|
|
@@ -712,8 +966,19 @@ function DTLSSession(options) {
|
|
|
712
966
|
};
|
|
713
967
|
}
|
|
714
968
|
|
|
969
|
+
// NEVER ACK THE FIRST FLIGHT (RFC 9147 §7.1): "ACKs MUST NOT be
|
|
970
|
+
// sent for the ClientHello". The first flight is acknowledged by
|
|
971
|
+
// the RESPONSE to it — the ServerHello — and a peer that receives
|
|
972
|
+
// an ACK where none belongs answers decode_error.
|
|
973
|
+
// Epoch 0 records are exactly that first flight: everything later
|
|
974
|
+
// is protected under epoch 2 or above. This only bites when the
|
|
975
|
+
// ClientHello is large enough to FRAGMENT, because a single-record
|
|
976
|
+
// flight is consumed before any ACK decision is made — which is
|
|
977
|
+
// why a two-way call worked and an SFU, whose extra transceivers
|
|
978
|
+
// push the ClientHello past the MTU, did not.
|
|
715
979
|
if (records[i].type === CT.HANDSHAKE &&
|
|
716
|
-
ctx.selectedVersion === DTLS_VERSION.DTLS1_3
|
|
980
|
+
ctx.selectedVersion === DTLS_VERSION.DTLS1_3 &&
|
|
981
|
+
records[i].epoch > 0) {
|
|
717
982
|
ackable.push({ epoch: records[i].epoch, seq: records[i].seq });
|
|
718
983
|
}
|
|
719
984
|
|
|
@@ -738,12 +1003,61 @@ function DTLSSession(options) {
|
|
|
738
1003
|
// We ACK from the highest epoch for which we already have write keys (so the
|
|
739
1004
|
// ACK itself is protected once handshake keys exist; epoch 0 before that).
|
|
740
1005
|
if (ackable.length > 0) {
|
|
741
|
-
|
|
742
|
-
|
|
1006
|
+
// ACKs RIDE THE HANDSHAKE EPOCH (RFC 9147 §7): they acknowledge
|
|
1007
|
+
// handshake records, so epoch 2 once handshake keys exist and
|
|
1008
|
+
// epoch 0 before that — never epoch 3. Picking "the highest epoch
|
|
1009
|
+
// we have keys for" sent them under APPLICATION-DATA protection,
|
|
1010
|
+
// where the peer decrypts with app keys and finds a content type
|
|
1011
|
+
// that does not belong. Chrome answers decode_error.
|
|
1012
|
+
// This shows up the moment a ClientHello is large enough to be
|
|
1013
|
+
// FRAGMENTED — an SFU with several transceivers crosses the MTU
|
|
1014
|
+
// and a two-way call does not, which is exactly why bidirectional
|
|
1015
|
+
// worked and party mode did not.
|
|
1016
|
+
// Post-handshake (§7.1: "For post-handshake messages, ACKs SHOULD be
|
|
1017
|
+
// sent once for each received and processed handshake record") the
|
|
1018
|
+
// handshake epoch is gone and §7 requires the HIGHEST available sending
|
|
1019
|
+
// epoch. During the handshake it is still epoch 2, or 0 before keys.
|
|
1020
|
+
// RFC 9147 §7.1 lists the flights that are IMPLICITLY acknowledged by
|
|
1021
|
+
// the responding flight, and the client's final flight is pointedly not
|
|
1022
|
+
// among them: "In general, flights MUST be ACKed unless they are
|
|
1023
|
+
// implicitly acknowledged", excepting "handshake flights other than the
|
|
1024
|
+
// client's final flight of the main handshake".
|
|
1025
|
+
//
|
|
1026
|
+
// So the two roles need opposite answers here, which is why a
|
|
1027
|
+
// state-only test could not get both right:
|
|
1028
|
+
// client — the server's flight is implicitly acked by our own final
|
|
1029
|
+
// flight, so an explicit ACK is both redundant and (at the
|
|
1030
|
+
// application epoch) actively harmful;
|
|
1031
|
+
// server — the client's final flight has no responding flight, so
|
|
1032
|
+
// without an explicit ACK the client retransmits it forever.
|
|
1033
|
+
// §7 then fixes the epoch: after the handshake, the highest available
|
|
1034
|
+
// sending epoch.
|
|
1035
|
+
let justCompleted = (!wasConnectedOnEntry && ctx.state === 'connected');
|
|
1036
|
+
let finalFlightAck = isServer && justCompleted;
|
|
1037
|
+
let atAppEpoch = wasConnectedOnEntry || finalFlightAck;
|
|
1038
|
+
|
|
1039
|
+
let ackEpoch = atAppEpoch
|
|
1040
|
+
? (ctx.writeEpoch !== null ? ctx.writeEpoch : 3)
|
|
1041
|
+
: (ctx.keys[2] ? 2 : 0);
|
|
1042
|
+
sendAck(ackEpoch, ackable, atAppEpoch);
|
|
743
1043
|
}
|
|
744
1044
|
}
|
|
745
1045
|
|
|
746
1046
|
function processRecord(record) {
|
|
1047
|
+
// A record in an epoch above 0 that did not decrypt is not the peer's
|
|
1048
|
+
// (forged, corrupted, or a late packet from a previous session on the
|
|
1049
|
+
// same address) — RFC 6347 §4.1.2.7 / RFC 9147 §4.5.2: discard it
|
|
1050
|
+
// silently. Its content is still ciphertext: as an ALERT its second byte
|
|
1051
|
+
// is random, which emitted 'error' and failed the whole connection; as
|
|
1052
|
+
// APPLICATION_DATA it went up to SCTP as data.
|
|
1053
|
+
if (record.epoch > 0 && !record.encrypted) return;
|
|
1054
|
+
// Once connected, everything the peer sends is protected. A plaintext
|
|
1055
|
+
// (epoch 0) alert or application data at that point is unauthenticated —
|
|
1056
|
+
// anyone able to send a UDP packet from the peer's address could close
|
|
1057
|
+
// the connection with a one-record close_notify, or inject data.
|
|
1058
|
+
if (ctx.state === 'connected' && !record.encrypted &&
|
|
1059
|
+
(record.type === CT.ALERT || record.type === CT.APPLICATION_DATA)) return;
|
|
1060
|
+
|
|
747
1061
|
if (record.type === CT.HANDSHAKE) {
|
|
748
1062
|
// An epoch>0 record that could not be decrypted (keys not yet
|
|
749
1063
|
// installed, AEAD failure) must NOT be fed to the handshake
|
|
@@ -781,6 +1095,12 @@ function DTLSSession(options) {
|
|
|
781
1095
|
} else if (record.type === CT.ALERT) {
|
|
782
1096
|
let level = record.content[0];
|
|
783
1097
|
let desc = record.content[1];
|
|
1098
|
+
// Name the alert either way. A close_notify is a CLEAN shutdown and
|
|
1099
|
+
// a failure is not, but both present to the application as "the
|
|
1100
|
+
// connection ended" — and telling them apart is the difference
|
|
1101
|
+
// between "the peer hung up" and "the peer rejected us".
|
|
1102
|
+
_ldbg('RX Alert level=' + level + ' desc=' + desc +
|
|
1103
|
+
(desc === 0 ? ' (close_notify — clean shutdown)' : ' (failure)'));
|
|
784
1104
|
if (desc === 0) {
|
|
785
1105
|
// close_notify
|
|
786
1106
|
ctx.state = 'closed';
|
|
@@ -1160,7 +1480,7 @@ function DTLSSession(options) {
|
|
|
1160
1480
|
// HVR always uses msg_seq 0 and is sent in the clear (epoch 0).
|
|
1161
1481
|
let hvrMsg = build_dtls_handshake(build_message(3, build_hello_verify_request({ cookie: cookie })), 0);
|
|
1162
1482
|
let record = buildDtlsPlaintext(CT.HANDSHAKE, 0, nextWriteSeq(0), hvrMsg);
|
|
1163
|
-
ev.emit('packet', record);
|
|
1483
|
+
_ldbgTx(record, 'L1194'); ev.emit('packet', record);
|
|
1164
1484
|
}
|
|
1165
1485
|
|
|
1166
1486
|
/**
|
|
@@ -1224,7 +1544,7 @@ function DTLSSession(options) {
|
|
|
1224
1544
|
let dtlsMsg = build_dtls_handshake(ch2, 1);
|
|
1225
1545
|
let record = buildRecord(0, CT.HANDSHAKE, dtlsMsg);
|
|
1226
1546
|
ctx.currentFlight = [{ epoch: 0, contentType: CT.HANDSHAKE, payload: dtlsMsg }];
|
|
1227
|
-
if (record) ev.emit('packet', record);
|
|
1547
|
+
if (record) { _ldbgTx(record, 'L1258'); ev.emit('packet', record); }
|
|
1228
1548
|
|
|
1229
1549
|
tls.context.message_sent_seq = 2;
|
|
1230
1550
|
startRetransmitTimer();
|
|
@@ -1243,15 +1563,37 @@ function DTLSSession(options) {
|
|
|
1243
1563
|
* new nonce, and a monotonic seq the receiver can reconstruct.
|
|
1244
1564
|
*/
|
|
1245
1565
|
function resendFlight() {
|
|
1566
|
+
// NEVER AFTER THE HANDSHAKE COMPLETES. Once we are connected the
|
|
1567
|
+
// handshake flight is finished by definition, and re-emitting it
|
|
1568
|
+
// sends HANDSHAKE content inside an APPLICATION-DATA epoch — the
|
|
1569
|
+
// peer decrypts a record with its app-data keys and finds a message
|
|
1570
|
+
// it cannot place. Chrome tolerated one and then answered
|
|
1571
|
+
// decode_error on the next SCTP record, which read as an SCTP
|
|
1572
|
+
// problem for several rounds and was not one: the giveaway is a
|
|
1573
|
+
// record in the log with no matching sctp->dtls line, because the
|
|
1574
|
+
// payload never came from the application at all.
|
|
1575
|
+
// RFC 9147 §5.8: retransmission is bounded by the handshake; after
|
|
1576
|
+
// it, ACKs are the mechanism, not a timer.
|
|
1577
|
+
if (ctx.state === 'connected' || ctx.state === 'closed') {
|
|
1578
|
+
cancelRetransmit();
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1246
1581
|
for (let i = 0; i < ctx.currentFlight.length; i++) {
|
|
1247
1582
|
let f = ctx.currentFlight[i];
|
|
1248
1583
|
let record = buildRecord(f.epoch, f.contentType, f.payload);
|
|
1249
|
-
if (record) ev.emit('packet', record);
|
|
1584
|
+
if (record) { _ldbgTx(record, 'L1280'); ev.emit('packet', record); }
|
|
1250
1585
|
}
|
|
1251
1586
|
}
|
|
1252
1587
|
|
|
1253
1588
|
function startRetransmitTimer() {
|
|
1254
1589
|
cancelRetransmit();
|
|
1590
|
+
// ONE GATE, AT THE SOURCE. Four call sites arm this timer and only
|
|
1591
|
+
// one of them checks the state, so a timer armed while the handshake
|
|
1592
|
+
// was still running could fire after it finished and re-emit the
|
|
1593
|
+
// flight into the application-data epoch. Refusing to arm once
|
|
1594
|
+
// connected (or closed) covers every caller, including any added
|
|
1595
|
+
// later, without each having to remember the rule.
|
|
1596
|
+
if (ctx.state === 'connected' || ctx.state === 'closed') return;
|
|
1255
1597
|
ctx.retransmitTimer = setTimeout(function() {
|
|
1256
1598
|
if (ctx.retransmitCount >= ctx.maxRetransmits) {
|
|
1257
1599
|
ev.emit('error', new Error('DTLS handshake timeout — max retransmits exceeded'));
|
|
@@ -1291,16 +1633,52 @@ function DTLSSession(options) {
|
|
|
1291
1633
|
|
|
1292
1634
|
function processAck(content) {
|
|
1293
1635
|
let acks = parseDtlsAck(content);
|
|
1294
|
-
|
|
1636
|
+
|
|
1637
|
+
// An outstanding KeyUpdate is what this ACK is for: §8 forbids protecting
|
|
1638
|
+
// anything with the new keys until it arrives, so THIS is the moment the
|
|
1639
|
+
// new epoch becomes live. Keys were installed when the KeyUpdate was sent;
|
|
1640
|
+
// only the switch happens here.
|
|
1641
|
+
if (ctx.pendingWriteEpoch !== null) {
|
|
1642
|
+
ctx.writeEpoch = ctx.pendingWriteEpoch;
|
|
1643
|
+
ctx.pendingWriteEpoch = null;
|
|
1644
|
+
cancelKeyUpdateRetransmit();
|
|
1645
|
+
ev.emit('keyUpdate', { direction: 'send', epoch: ctx.writeEpoch });
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// Everything else keeps the original behaviour verbatim. In particular the
|
|
1650
|
+
// ACK for the client's final flight (which RFC 9147 §7.1 requires the peer
|
|
1651
|
+
// to send explicitly, and which arrives at the application epoch) must
|
|
1652
|
+
// still cancel that flight's retransmit timer.
|
|
1295
1653
|
cancelRetransmit();
|
|
1296
1654
|
startNewFlight();
|
|
1297
1655
|
}
|
|
1298
1656
|
|
|
1299
|
-
function sendAck(epoch, recordsToAck) {
|
|
1657
|
+
function sendAck(epoch, recordsToAck, postHandshake) {
|
|
1300
1658
|
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return;
|
|
1659
|
+
// ACKs BELONG TO THE HANDSHAKE (RFC 9147 §7). They acknowledge
|
|
1660
|
+
// handshake records, and once the handshake is complete there is
|
|
1661
|
+
// nothing left to acknowledge — but this kept firing afterwards and
|
|
1662
|
+
// put an ACK record inside the APPLICATION-DATA epoch. The peer
|
|
1663
|
+
// decrypts it with app-data keys, finds a content type that has no
|
|
1664
|
+
// business there, and the stream is poisoned: Chrome tolerated it
|
|
1665
|
+
// and then answered decode_error on the next record, which made a
|
|
1666
|
+
// perfectly good SCTP COOKIE_ECHO look like the culprit for several
|
|
1667
|
+
// rounds. The giveaway in the log is a record with no matching
|
|
1668
|
+
// sctp->dtls line — the payload never came from the application.
|
|
1669
|
+
// ...that reasoning applies to HANDSHAKE flights. Post-handshake messages
|
|
1670
|
+
// are a separate case the original guard swept up with them: KeyUpdate,
|
|
1671
|
+
// NewSessionTicket and friends have no responding flight to acknowledge
|
|
1672
|
+
// them implicitly, so §7.1 requires an explicit ACK — without one the peer
|
|
1673
|
+
// retransmits its KeyUpdate forever and never activates its new keys.
|
|
1674
|
+
// `postHandshake` is set only for records actually received and processed
|
|
1675
|
+
// after the handshake, so the spurious-ACK bug this guard was written for
|
|
1676
|
+
// cannot return through it.
|
|
1677
|
+
if (!postHandshake && (ctx.state === 'connected' || ctx.state === 'closed')) return;
|
|
1678
|
+
if (ctx.state === 'closed') return;
|
|
1301
1679
|
let payload = buildDtlsAck(recordsToAck);
|
|
1302
1680
|
let record = buildRecord(epoch, CT.ACK, payload);
|
|
1303
|
-
ev.emit('packet', record);
|
|
1681
|
+
_ldbgTx(record, 'L1334'); ev.emit('packet', record);
|
|
1304
1682
|
}
|
|
1305
1683
|
|
|
1306
1684
|
|
|
@@ -1328,6 +1706,13 @@ function DTLSSession(options) {
|
|
|
1328
1706
|
});
|
|
1329
1707
|
|
|
1330
1708
|
tls.on('error', function(e) {
|
|
1709
|
+
// The peer's REASON, spelled out. When BoringSSL rejects part of our
|
|
1710
|
+
// flight it sends an alert and closes; without printing it here the
|
|
1711
|
+
// only symptom is a connection that dies after a handshake that
|
|
1712
|
+
// looked complete, which is exactly the DTLS 1.3 interop case.
|
|
1713
|
+
_ldbg('ERROR ' + (e && e.message ? e.message : e) +
|
|
1714
|
+
(e && e.alertDescription != null ? ' alertDesc=' + e.alertDescription : '') +
|
|
1715
|
+
(e && e.code ? ' code=' + e.code : ''));
|
|
1331
1716
|
ev.emit('error', e);
|
|
1332
1717
|
});
|
|
1333
1718
|
|
|
@@ -1347,9 +1732,11 @@ function DTLSSession(options) {
|
|
|
1347
1732
|
}
|
|
1348
1733
|
if (typeof data === 'string') data = new TextEncoder().encode(data);
|
|
1349
1734
|
|
|
1350
|
-
let epoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3
|
|
1735
|
+
let epoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3
|
|
1736
|
+
? (ctx.writeEpoch !== null ? ctx.writeEpoch : 3)
|
|
1737
|
+
: 1;
|
|
1351
1738
|
let record = buildRecord(epoch, CT.APPLICATION_DATA, data);
|
|
1352
|
-
ev.emit('packet', record);
|
|
1739
|
+
_ldbgTx(record, 'L1383'); ev.emit('packet', record);
|
|
1353
1740
|
}
|
|
1354
1741
|
|
|
1355
1742
|
|
|
@@ -1370,13 +1757,19 @@ function DTLSSession(options) {
|
|
|
1370
1757
|
// picks the highest epoch we hold write keys for.
|
|
1371
1758
|
let epoch;
|
|
1372
1759
|
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
1373
|
-
epoch
|
|
1760
|
+
// After a KeyUpdate the live epoch is 4, 5, ... — the peer has moved on
|
|
1761
|
+
// and no longer reads epoch 3, so a close_notify sent there is dropped
|
|
1762
|
+
// and the shutdown looks like a timeout instead of a clean close.
|
|
1763
|
+
epoch = (ctx.writeEpoch !== null && ctx.keys[ctx.writeEpoch])
|
|
1764
|
+
? ctx.writeEpoch
|
|
1765
|
+
: (ctx.keys[3] ? 3 : (ctx.keys[2] ? 2 : 0));
|
|
1374
1766
|
} else {
|
|
1375
1767
|
epoch = ctx.keys[1] ? 1 : 0;
|
|
1376
1768
|
}
|
|
1377
1769
|
sendAlertRecord(epoch, new Uint8Array([1, 0])); // warning, close_notify
|
|
1378
1770
|
ctx.state = 'closed';
|
|
1379
1771
|
cancelRetransmit();
|
|
1772
|
+
cancelKeyUpdateRetransmit();
|
|
1380
1773
|
ev.emit('close');
|
|
1381
1774
|
}
|
|
1382
1775
|
|
|
@@ -1406,6 +1799,31 @@ function DTLSSession(options) {
|
|
|
1406
1799
|
/** Send application data (after connect). */
|
|
1407
1800
|
send: send,
|
|
1408
1801
|
|
|
1802
|
+
/**
|
|
1803
|
+
* Request a TLS 1.3 KeyUpdate (RFC 9147 §8).
|
|
1804
|
+
*
|
|
1805
|
+
* Returns false without doing anything if the session is not a connected
|
|
1806
|
+
* DTLS 1.3 association, or if an earlier KeyUpdate has not been
|
|
1807
|
+
* acknowledged yet — §8 permits only one in flight. The new epoch does not
|
|
1808
|
+
* become active on send; it activates when the peer's ACK arrives.
|
|
1809
|
+
*
|
|
1810
|
+
* @param {boolean} [requestPeer] also ask the peer to update its own keys
|
|
1811
|
+
* @returns {boolean} whether the update was initiated
|
|
1812
|
+
*/
|
|
1813
|
+
requestKeyUpdate: function(requestPeer) {
|
|
1814
|
+
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return false;
|
|
1815
|
+
if (ctx.state !== 'connected') return false;
|
|
1816
|
+
if (ctx.pendingWriteEpoch !== null) return false;
|
|
1817
|
+
tls.requestKeyUpdate(!!requestPeer);
|
|
1818
|
+
return true;
|
|
1819
|
+
},
|
|
1820
|
+
|
|
1821
|
+
/** Current epoch used to protect outgoing records (null before connect). */
|
|
1822
|
+
getWriteEpoch: function() { return ctx.writeEpoch; },
|
|
1823
|
+
|
|
1824
|
+
/** Highest epoch for which read keys are installed (null before connect). */
|
|
1825
|
+
getReadEpoch: function() { return ctx.readEpoch; },
|
|
1826
|
+
|
|
1409
1827
|
/** Close the DTLS session. */
|
|
1410
1828
|
close: close,
|
|
1411
1829
|
|
|
@@ -1461,6 +1879,24 @@ function DTLSSession(options) {
|
|
|
1461
1879
|
|
|
1462
1880
|
/** Peer certificate. */
|
|
1463
1881
|
getPeerCertificate: function() { return tls.getPeerCertificate(); },
|
|
1882
|
+
|
|
1883
|
+
/** TLS fingerprints (see TLSSession): JA4, JA5 of the ClientHello, JA4S
|
|
1884
|
+
* of the ServerHello. The transport character resolves to 'd' on its
|
|
1885
|
+
* own — the inner TLSSession reads the 0xFE high byte off the hello's
|
|
1886
|
+
* legacy_version — so no `protocol` override is needed for plain DTLS. */
|
|
1887
|
+
getJA4: function(opts) { return tls.getJA4(opts); },
|
|
1888
|
+
getJA5: function(opts) { return tls.getJA5(opts); },
|
|
1889
|
+
getJA4S: function(opts) { return tls.getJA4S(opts); },
|
|
1890
|
+
|
|
1891
|
+
/** Set the key and certificate for this connection (Node parity). */
|
|
1892
|
+
setKeyCert: function(ctxOrOpts) { return tls.setKeyCert(ctxOrOpts); },
|
|
1893
|
+
|
|
1894
|
+
/** Finished verify_data, ours and the peer's (RFC 5929 channel binding). */
|
|
1895
|
+
getFinished: function() { return tls.getFinished(); },
|
|
1896
|
+
getPeerFinished: function() { return tls.getPeerFinished(); },
|
|
1897
|
+
|
|
1898
|
+
/** Whether this connection resumed an earlier session. */
|
|
1899
|
+
isSessionReused: function() { return !!tls.isResumed; },
|
|
1464
1900
|
};
|
|
1465
1901
|
|
|
1466
1902
|
for (let k in api) {
|