lemon-tls 0.5.5 → 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 +1 -1
- package/src/compat.js +10 -0
- package/src/crypto.js +146 -48
- package/src/dtls_session.js +236 -53
- 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 +1826 -436
- 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
|
// ============================================================
|
|
@@ -141,6 +147,7 @@ function DTLSSession(options) {
|
|
|
141
147
|
// byte and never the body, so any non-empty sid shears its whole
|
|
142
148
|
// ClientHello parse.)
|
|
143
149
|
sessionId: new Uint8Array(0),
|
|
150
|
+
srtpProfiles: options.srtpProfiles,
|
|
144
151
|
servername: options.servername,
|
|
145
152
|
SNICallback: options.SNICallback,
|
|
146
153
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
@@ -194,8 +201,22 @@ function DTLSSession(options) {
|
|
|
194
201
|
// keyed by msg_seq, and drained in order once the gap is filled.
|
|
195
202
|
pendingMessages: {}, // msg_seq → { type, body }
|
|
196
203
|
|
|
197
|
-
// Flight tracking for retransmission
|
|
198
|
-
|
|
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 }
|
|
199
220
|
// RFC 6347 §4.2.4 flight discipline: a NEW flight begins with the first
|
|
200
221
|
// record we send after having accepted new handshake data from the peer.
|
|
201
222
|
// Set when inbound processing advances state; consumed (and reset) by
|
|
@@ -253,24 +274,17 @@ function DTLSSession(options) {
|
|
|
253
274
|
// Server defaults — TLSSession client auto-populates these, but server expects them set externally
|
|
254
275
|
if (isServer) {
|
|
255
276
|
if (!tlsSetup.local_supported_cipher_suites) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
ciphers.push(0xC02F, 0xC030, 0xC02B, 0xC02C); // TLS 1.2 ECDHE ciphers
|
|
262
|
-
}
|
|
263
|
-
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
|
+
);
|
|
264
282
|
}
|
|
265
283
|
if (!tlsSetup.local_supported_groups) {
|
|
266
|
-
tlsSetup.local_supported_groups =
|
|
284
|
+
tlsSetup.local_supported_groups = ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
|
|
267
285
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
0x0403, 0x0503, 0x0603, // ECDSA
|
|
271
|
-
0x0807, 0x0808, // EdDSA
|
|
272
|
-
0x0401, 0x0501, 0x0601, // PKCS#1
|
|
273
|
-
];
|
|
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();
|
|
274
288
|
}
|
|
275
289
|
|
|
276
290
|
// Server cert/key
|
|
@@ -308,10 +322,18 @@ function DTLSSession(options) {
|
|
|
308
322
|
}
|
|
309
323
|
transcriptMsgSeqs.push(msgSeq);
|
|
310
324
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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;
|
|
315
337
|
};
|
|
316
338
|
|
|
317
339
|
/**
|
|
@@ -332,31 +354,70 @@ function DTLSSession(options) {
|
|
|
332
354
|
// Key derivation
|
|
333
355
|
// ============================================================
|
|
334
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
|
+
|
|
335
371
|
function deriveEpochKeys(secret) {
|
|
336
372
|
let cs = TLS_CIPHER_SUITES[ctx.cipherSuite];
|
|
337
373
|
let empty = new Uint8Array(0);
|
|
338
|
-
let
|
|
339
|
-
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);
|
|
340
377
|
|
|
341
378
|
let result = { key, iv };
|
|
342
379
|
|
|
343
380
|
// DTLS 1.3: derive sn_key for record number encryption
|
|
344
381
|
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
345
|
-
result.snKey = derive_sn_key(cs.hash, secret, ctx.cipherSuite);
|
|
382
|
+
result.snKey = derive_sn_key(cs.hash, secret, ctx.cipherSuite, dtlsLabelPrefix());
|
|
346
383
|
result.algo = getAeadAlgo(ctx.cipherSuite);
|
|
347
384
|
}
|
|
348
385
|
|
|
349
386
|
return result;
|
|
350
387
|
}
|
|
351
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
|
+
|
|
352
416
|
tls.on('handshakeSecrets', function(localSecret, remoteSecret) {
|
|
353
|
-
|
|
354
|
-
if (ctx.selectedVersion === null) ctx.selectedVersion = tls.context.selected_version;
|
|
417
|
+
let dtlsEpoch = dtlsEpochForSecrets(false);
|
|
355
418
|
ctx.cipherSuite = tls.getCipher();
|
|
356
419
|
ctx.hashName = TLS_CIPHER_SUITES[ctx.cipherSuite].hash;
|
|
357
420
|
|
|
358
|
-
let dtlsEpoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 2 : 1;
|
|
359
|
-
|
|
360
421
|
ctx.keys[dtlsEpoch] = {
|
|
361
422
|
write: deriveEpochKeys(localSecret),
|
|
362
423
|
read: deriveEpochKeys(remoteSecret),
|
|
@@ -365,7 +426,7 @@ function DTLSSession(options) {
|
|
|
365
426
|
});
|
|
366
427
|
|
|
367
428
|
tls.on('appSecrets', function(localSecret, remoteSecret) {
|
|
368
|
-
let dtlsEpoch =
|
|
429
|
+
let dtlsEpoch = dtlsEpochForSecrets(true);
|
|
369
430
|
|
|
370
431
|
ctx.keys[dtlsEpoch] = {
|
|
371
432
|
write: deriveEpochKeys(localSecret),
|
|
@@ -445,8 +506,21 @@ function DTLSSession(options) {
|
|
|
445
506
|
// Build DTLS records and emit
|
|
446
507
|
for (let i = 0; i < frags.length; i++) {
|
|
447
508
|
let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
524
|
}
|
|
451
525
|
|
|
452
526
|
// Start retransmit timer after sending flight messages.
|
|
@@ -476,8 +550,21 @@ function DTLSSession(options) {
|
|
|
476
550
|
function buildRecord(epoch, contentType, payload) {
|
|
477
551
|
let epochKeys = ctx.keys[epoch];
|
|
478
552
|
|
|
553
|
+
|
|
479
554
|
if (!epochKeys) {
|
|
480
|
-
//
|
|
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)
|
|
481
568
|
return buildDtlsPlaintext(contentType, epoch, nextWriteSeq(epoch), payload);
|
|
482
569
|
}
|
|
483
570
|
|
|
@@ -538,6 +625,11 @@ function DTLSSession(options) {
|
|
|
538
625
|
*/
|
|
539
626
|
function sendAlertRecord(epoch, alertData) {
|
|
540
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;
|
|
541
633
|
ev.emit('packet', record);
|
|
542
634
|
}
|
|
543
635
|
|
|
@@ -555,11 +647,13 @@ function DTLSSession(options) {
|
|
|
555
647
|
}
|
|
556
648
|
let record = buildDtlsPlaintext(CT.CHANGE_CIPHER_SPEC, 0, nextWriteSeq(0), new Uint8Array([1]));
|
|
557
649
|
// RFC 6347 §4.2.4: the CCS is part of the flight and MUST be included
|
|
558
|
-
// when the flight is retransmitted.
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
|
|
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]) });
|
|
563
657
|
ev.emit('packet', record);
|
|
564
658
|
}
|
|
565
659
|
|
|
@@ -600,6 +694,24 @@ function DTLSSession(options) {
|
|
|
600
694
|
let ackable = [];
|
|
601
695
|
|
|
602
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
|
+
|
|
603
715
|
if (records[i].type === CT.HANDSHAKE &&
|
|
604
716
|
ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
605
717
|
ackable.push({ epoch: records[i].epoch, seq: records[i].seq });
|
|
@@ -751,9 +863,7 @@ function DTLSSession(options) {
|
|
|
751
863
|
// bounds the total effort; the resend rate is naturally bounded by
|
|
752
864
|
// the peer's own retransmission schedule. DTLS 1.3 is excluded —
|
|
753
865
|
// it uses explicit ACKs (RFC 9147 §7) instead of flight inference.
|
|
754
|
-
|
|
755
|
-
ev.emit('packet', ctx.currentFlight[f]);
|
|
756
|
-
}
|
|
866
|
+
resendFlight();
|
|
757
867
|
}
|
|
758
868
|
}
|
|
759
869
|
|
|
@@ -828,6 +938,7 @@ function DTLSSession(options) {
|
|
|
828
938
|
coveredAfter += merged[ca][1] - merged[ca][0];
|
|
829
939
|
}
|
|
830
940
|
|
|
941
|
+
|
|
831
942
|
// Complete when a single range covers [0, totalLength).
|
|
832
943
|
if (merged.length === 1 && merged[0][0] === 0 && merged[0][1] === frag.totalLength) {
|
|
833
944
|
delete ctx.fragments[key];
|
|
@@ -871,11 +982,45 @@ function DTLSSession(options) {
|
|
|
871
982
|
//
|
|
872
983
|
// Note: msg_seq is NOT advanced for a rejected/HVR-triggering ClientHello, so
|
|
873
984
|
// the client's retried CH (which reuses msg_seq per RFC 6347) is still accepted.
|
|
874
|
-
if (type === 1 && isServer
|
|
875
|
-
ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) {
|
|
876
|
-
|
|
985
|
+
if (type === 1 && isServer) {
|
|
877
986
|
let ch = null;
|
|
878
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
|
+
|
|
879
1024
|
let incomingCookie = (ch && ch.dtls_cookie) ? ch.dtls_cookie : new Uint8Array(0);
|
|
880
1025
|
|
|
881
1026
|
if (incomingCookie.length === 0) {
|
|
@@ -911,9 +1056,10 @@ function DTLSSession(options) {
|
|
|
911
1056
|
// feedHandshakeToTls records the incoming wire seq and outgoing
|
|
912
1057
|
// messages record message_sent_seq at emission — which is exactly
|
|
913
1058
|
// what the Finished MAC hashes (RFC 6347 §4.2.6).
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1059
|
+
ctx.nextReadMsgSeq = 1;
|
|
1060
|
+
tls.context.message_sent_seq = 1;
|
|
1061
|
+
// Fall through and process the ClientHello normally.
|
|
1062
|
+
}
|
|
917
1063
|
}
|
|
918
1064
|
|
|
919
1065
|
// ---- In-order delivery (RFC 6347 §4.2.4 / RFC 9147 §5.2) ----
|
|
@@ -1077,8 +1223,8 @@ function DTLSSession(options) {
|
|
|
1077
1223
|
// Build DTLS message and send — msg_seq 1 per the RFC figure above.
|
|
1078
1224
|
let dtlsMsg = build_dtls_handshake(ch2, 1);
|
|
1079
1225
|
let record = buildRecord(0, CT.HANDSHAKE, dtlsMsg);
|
|
1080
|
-
ctx.currentFlight = [
|
|
1081
|
-
ev.emit('packet', record);
|
|
1226
|
+
ctx.currentFlight = [{ epoch: 0, contentType: CT.HANDSHAKE, payload: dtlsMsg }];
|
|
1227
|
+
if (record) ev.emit('packet', record);
|
|
1082
1228
|
|
|
1083
1229
|
tls.context.message_sent_seq = 2;
|
|
1084
1230
|
startRetransmitTimer();
|
|
@@ -1089,6 +1235,21 @@ function DTLSSession(options) {
|
|
|
1089
1235
|
// Flight tracking + retransmission
|
|
1090
1236
|
// ============================================================
|
|
1091
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
|
+
|
|
1092
1253
|
function startRetransmitTimer() {
|
|
1093
1254
|
cancelRetransmit();
|
|
1094
1255
|
ctx.retransmitTimer = setTimeout(function() {
|
|
@@ -1099,10 +1260,8 @@ function DTLSSession(options) {
|
|
|
1099
1260
|
return;
|
|
1100
1261
|
}
|
|
1101
1262
|
|
|
1102
|
-
// Retransmit entire current flight
|
|
1103
|
-
|
|
1104
|
-
ev.emit('packet', ctx.currentFlight[i]);
|
|
1105
|
-
}
|
|
1263
|
+
// Retransmit entire current flight with fresh record seqs.
|
|
1264
|
+
resendFlight();
|
|
1106
1265
|
|
|
1107
1266
|
ctx.retransmitCount++;
|
|
1108
1267
|
ctx.retransmitTimeout = Math.min(ctx.retransmitTimeout * 2, 60000);
|
|
@@ -1200,7 +1359,21 @@ function DTLSSession(options) {
|
|
|
1200
1359
|
|
|
1201
1360
|
function close() {
|
|
1202
1361
|
if (ctx.state === 'closed') return;
|
|
1203
|
-
|
|
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
|
+
}
|
|
1204
1377
|
sendAlertRecord(epoch, new Uint8Array([1, 0])); // warning, close_notify
|
|
1205
1378
|
ctx.state = 'closed';
|
|
1206
1379
|
cancelRetransmit();
|
|
@@ -1255,6 +1428,16 @@ function DTLSSession(options) {
|
|
|
1255
1428
|
return tls.exportKeyingMaterial(length, label, context_value);
|
|
1256
1429
|
},
|
|
1257
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
|
+
|
|
1258
1441
|
/** Peer's hello extensions: [{ type, name, data, value }]. */
|
|
1259
1442
|
getRemoteExtensions: function() { return tls.getRemoteExtensions(); },
|
|
1260
1443
|
|
package/src/dtls_socket.js
CHANGED
|
@@ -52,6 +52,7 @@ function DTLSSocket(udpSocket, options) {
|
|
|
52
52
|
mtu: options.mtu,
|
|
53
53
|
ticketKeys: options.ticketKeys,
|
|
54
54
|
useCookies: options.useCookies,
|
|
55
|
+
srtpProfiles: options.srtpProfiles,
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
// Wire session → UDP
|
|
@@ -136,6 +137,11 @@ function DTLSSocket(udpSocket, options) {
|
|
|
136
137
|
return session.exportKeyingMaterial(length, label, context_value);
|
|
137
138
|
};
|
|
138
139
|
|
|
140
|
+
/** Negotiated DTLS-SRTP protection profile (RFC 5764), or null. */
|
|
141
|
+
self.getSelectedSrtpProfile = function() {
|
|
142
|
+
return session.getSelectedSrtpProfile();
|
|
143
|
+
};
|
|
144
|
+
|
|
139
145
|
/** Peer hello extensions / single extension by numeric type. */
|
|
140
146
|
self.getRemoteExtensions = function() { return session.getRemoteExtensions(); };
|
|
141
147
|
self.getRemoteExtension = function(type) { return session.getRemoteExtension(type); };
|
|
@@ -190,6 +196,7 @@ function createDTLSServer(options, connectionListener) {
|
|
|
190
196
|
mtu: options.mtu,
|
|
191
197
|
ticketKeys: options.ticketKeys,
|
|
192
198
|
useCookies: options.useCookies,
|
|
199
|
+
srtpProfiles: options.srtpProfiles,
|
|
193
200
|
});
|
|
194
201
|
|
|
195
202
|
connections[key] = socket;
|
package/src/record.js
CHANGED
|
@@ -33,12 +33,12 @@ function getAeadAlgo(cipherSuite) {
|
|
|
33
33
|
// ===================== TLS 1.3 primitives =====================
|
|
34
34
|
|
|
35
35
|
/** Derive write key + IV from a TLS 1.3 traffic secret. */
|
|
36
|
-
function deriveKeys(trafficSecret, cipherSuite) {
|
|
36
|
+
function deriveKeys(trafficSecret, cipherSuite, labelPrefix) {
|
|
37
37
|
const empty = new Uint8Array(0);
|
|
38
38
|
let cs = TLS_CIPHER_SUITES[cipherSuite];
|
|
39
39
|
return {
|
|
40
|
-
key: hkdf_expand_label(cs.hash, trafficSecret, 'key', empty, cs.keylen),
|
|
41
|
-
iv: hkdf_expand_label(cs.hash, trafficSecret, 'iv', empty, 12)
|
|
40
|
+
key: hkdf_expand_label(cs.hash, trafficSecret, 'key', empty, cs.keylen, labelPrefix),
|
|
41
|
+
iv: hkdf_expand_label(cs.hash, trafficSecret, 'iv', empty, 12, labelPrefix)
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -956,7 +956,9 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
956
956
|
let seq = reconstructDtlsSeq(truncated, hdr.seqLen2 ? 16 : 8,
|
|
957
957
|
(keys.maxReadSeq === undefined ? -1 : keys.maxReadSeq));
|
|
958
958
|
|
|
959
|
-
// Rebuild AAD with plaintext RN
|
|
959
|
+
// Rebuild AAD with plaintext RN. AAD depends only on the wire bytes (the
|
|
960
|
+
// decrypted record number + length), NOT on the reconstructed full seq —
|
|
961
|
+
// so it's computed once and reused across seq candidates below.
|
|
960
962
|
let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
|
|
961
963
|
let aad = new Uint8Array(hdrLen);
|
|
962
964
|
aad[0] = data[0];
|
|
@@ -966,7 +968,6 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
966
968
|
aad[1 + rnLen + 1] = ctLen & 0xFF;
|
|
967
969
|
}
|
|
968
970
|
|
|
969
|
-
let nonce = getNonce(keys.iv, seq);
|
|
970
971
|
let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
|
|
971
972
|
let isChaCha = algo === 'chacha20-poly1305';
|
|
972
973
|
|
|
@@ -974,20 +975,66 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
974
975
|
let ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
975
976
|
let tag = ciphertext.subarray(ciphertext.length - 16);
|
|
976
977
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
978
|
+
// The wire carries only the low bits of the record seq. reconstructDtlsSeq
|
|
979
|
+
// returns the most-likely full value, but under large gaps or reordering
|
|
980
|
+
// the true seq can be one window away in either direction. Only the NONCE
|
|
981
|
+
// depends on the full seq (AAD does not), so try the neighbouring
|
|
982
|
+
// candidates on AEAD failure before giving up — this is what lets
|
|
983
|
+
// DTLS-Replay-TLS13 / -LargeGaps / -NonMonotonic deprotect instead of
|
|
984
|
+
// failing as "bad record MAC". Candidates are deduped and non-negative.
|
|
985
|
+
let window = hdr.seqLen2 ? 65536 : 256;
|
|
986
|
+
let candidates = [seq];
|
|
987
|
+
if (seq - window >= 0) candidates.push(seq - window);
|
|
988
|
+
candidates.push(seq + window);
|
|
989
|
+
|
|
990
|
+
let pt = null, acceptedSeq = -1;
|
|
991
|
+
for (let ci = 0; ci < candidates.length; ci++) {
|
|
992
|
+
let cand = candidates[ci];
|
|
993
|
+
let nonce = getNonce(keys.iv, cand);
|
|
994
|
+
try {
|
|
995
|
+
let decipher = crypto.createDecipheriv(algo, keys.key, nonce,
|
|
996
|
+
isChaCha ? { authTagLength: 16 } : undefined);
|
|
997
|
+
decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
|
|
998
|
+
decipher.setAuthTag(tag);
|
|
999
|
+
let out = decipher.update(ct);
|
|
1000
|
+
decipher.final();
|
|
1001
|
+
pt = out; acceptedSeq = cand;
|
|
1002
|
+
break;
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
// try next candidate
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
984
1007
|
|
|
1008
|
+
if (pt === null) return null;
|
|
1009
|
+
seq = acceptedSeq;
|
|
1010
|
+
|
|
1011
|
+
{
|
|
985
1012
|
let inner = parseInnerPlaintext(new Uint8Array(pt));
|
|
986
1013
|
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
|
|
1014
|
+
// RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies do we consult
|
|
1015
|
+
// and update the sliding window. Checking before deprotection would let a
|
|
1016
|
+
// forged record number poison the window; checking after means only
|
|
1017
|
+
// genuine records move it. The window lives on the per-epoch key object,
|
|
1018
|
+
// next to maxReadSeq, because both are per-epoch receive state.
|
|
1019
|
+
// replayWindow: bitmask of the 64 seqs at or below maxReadSeq that have
|
|
1020
|
+
// been accepted. Bit 0 = maxReadSeq, bit k = maxReadSeq - k.
|
|
1021
|
+
if (keys.maxReadSeq === undefined || keys.maxReadSeq < 0) {
|
|
1022
|
+
// First accepted record in this epoch: seed the window with this seq as
|
|
1023
|
+
// the high-water mark (bit 0 set), so an immediate replay of it is
|
|
1024
|
+
// caught below rather than sailing through an unset window.
|
|
1025
|
+
keys.maxReadSeq = seq;
|
|
1026
|
+
keys.replayWindow = 1n;
|
|
1027
|
+
} else if (seq > keys.maxReadSeq) {
|
|
1028
|
+
// Advance: shift the window up by the gap, then set bit 0.
|
|
1029
|
+
let shift = BigInt(seq - keys.maxReadSeq);
|
|
1030
|
+
keys.replayWindow = shift >= 64n ? 1n : ((keys.replayWindow << shift) | 1n) & ((1n << 64n) - 1n);
|
|
990
1031
|
keys.maxReadSeq = seq;
|
|
1032
|
+
} else {
|
|
1033
|
+
let back = BigInt(keys.maxReadSeq - seq);
|
|
1034
|
+
if (back >= 64n) return null; // too old — outside window
|
|
1035
|
+
let bit = 1n << back;
|
|
1036
|
+
if (keys.replayWindow & bit) return null; // already seen — replay
|
|
1037
|
+
keys.replayWindow |= bit;
|
|
991
1038
|
}
|
|
992
1039
|
|
|
993
1040
|
return {
|
|
@@ -997,8 +1044,6 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
997
1044
|
content: inner.content,
|
|
998
1045
|
total_length: off + ctLen,
|
|
999
1046
|
};
|
|
1000
|
-
} catch (e) {
|
|
1001
|
-
return null;
|
|
1002
1047
|
}
|
|
1003
1048
|
}
|
|
1004
1049
|
|
|
@@ -1024,15 +1069,36 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1024
1069
|
let keys = keysByEpoch ? keysByEpoch[hdr.epoch] : null;
|
|
1025
1070
|
|
|
1026
1071
|
if (!keys) {
|
|
1027
|
-
//
|
|
1072
|
+
// No keys for this epoch YET. Do not discard the record: a DTLS peer
|
|
1073
|
+
// legitimately packs its whole flight into one datagram, so the very
|
|
1074
|
+
// record that lets us derive these keys (the ServerHello) can sit
|
|
1075
|
+
// ahead of records encrypted under them. Throwing the rest away meant
|
|
1076
|
+
// the entire encrypted flight vanished and the handshake stalled until
|
|
1077
|
+
// the peer retransmitted — or forever, if it does not.
|
|
1078
|
+
//
|
|
1079
|
+
// Keep the raw bytes and mark the record deferred; the caller retries
|
|
1080
|
+
// it after processing each earlier record, once the keys exist. This
|
|
1081
|
+
// generalises the CCS-only re-decrypt that DTLS 1.2 already had.
|
|
1028
1082
|
let rnLen = hdr.seqLen2 ? 2 : 1;
|
|
1029
1083
|
let skip = 1 + rnLen;
|
|
1084
|
+
let total;
|
|
1030
1085
|
if (hdr.hasLength && off + skip + 2 <= data.length) {
|
|
1031
|
-
skip
|
|
1086
|
+
total = skip + 2 + readU16(data, off + skip);
|
|
1032
1087
|
} else {
|
|
1033
|
-
|
|
1088
|
+
total = data.length - off;
|
|
1034
1089
|
}
|
|
1035
|
-
off
|
|
1090
|
+
if (total <= 0 || off + total > data.length) total = data.length - off;
|
|
1091
|
+
|
|
1092
|
+
records.push({
|
|
1093
|
+
type: null,
|
|
1094
|
+
epoch: hdr.epoch,
|
|
1095
|
+
seq: null,
|
|
1096
|
+
content: null,
|
|
1097
|
+
encrypted: false,
|
|
1098
|
+
deferred: true,
|
|
1099
|
+
raw: data.subarray(off, off + total),
|
|
1100
|
+
});
|
|
1101
|
+
off += total;
|
|
1036
1102
|
continue;
|
|
1037
1103
|
}
|
|
1038
1104
|
|
|
@@ -1174,6 +1240,7 @@ export {
|
|
|
1174
1240
|
// DTLS 1.3 unified header
|
|
1175
1241
|
buildUnifiedHdr,
|
|
1176
1242
|
parseUnifiedHdr,
|
|
1243
|
+
reconstructDtlsSeq,
|
|
1177
1244
|
maskRecordNumber,
|
|
1178
1245
|
|
|
1179
1246
|
// DTLS 1.3 encrypted records
|