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.
@@ -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
- currentFlight: [], // array of { epoch, data: Uint8Array (complete datagram) }
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
- let ciphers = [];
257
- if (versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0) {
258
- ciphers.push(0x1301, 0x1302, 0x1303); // TLS 1.3 AEAD ciphers
259
- }
260
- if (versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0) {
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 = [0x001d, 0x0017, 0x0018];
284
+ tlsSetup.local_supported_groups = ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
267
285
  }
268
- tlsSetup.local_supported_signature_algorithms = [
269
- 0x0804, 0x0805, 0x0806, // RSA-PSS
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
- if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) {
312
- return build_dtls_handshake(data, msgSeq);
313
- }
314
- return data; // TLS format for DTLS 1.3 or unknown version
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 key = hkdf_expand_label(cs.hash, secret, 'key', empty, cs.keylen);
339
- let iv = hkdf_expand_label(cs.hash, secret, 'iv', empty, 12);
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
- // Ensure version is detected before key derivation
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 = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1;
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
- ctx.currentFlight.push(record);
449
- ev.emit('packet', record);
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
- // No keys → plaintext record
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. Previously it was emitted but never
559
- // captured, so a retransmitted final flight arrived WITHOUT the cipher-
560
- // spec change and the peer could never decrypt the accompanying
561
- // Finished — an unrecoverable stall under single-datagram loss.
562
- ctx.currentFlight.push(record);
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
- for (let f = 0; f < ctx.currentFlight.length; f++) {
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 && options.useCookies === true &&
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
- ctx.nextReadMsgSeq = 1;
915
- tls.context.message_sent_seq = 1;
916
- // Fall through and process the ClientHello normally.
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 = [record];
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
- for (let i = 0; i < ctx.currentFlight.length; i++) {
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
- let epoch = ctx.state === 'connected' ? (ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1) : 0;
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
 
@@ -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
- try {
978
- let decipher = crypto.createDecipheriv(algo, keys.key, nonce,
979
- isChaCha ? { authTagLength: 16 } : undefined);
980
- decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
981
- decipher.setAuthTag(tag);
982
- let pt = decipher.update(ct);
983
- decipher.final();
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
- // Advance the per-epoch high-water mark only after the tag verified, so a
988
- // forged/replayed record number can't poison future reconstruction.
989
- if (keys.maxReadSeq === undefined || seq > keys.maxReadSeq) {
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
- // Can't decrypt — try to skip
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 += 2 + readU16(data, off + skip);
1086
+ total = skip + 2 + readU16(data, off + skip);
1032
1087
  } else {
1033
- skip = data.length - off;
1088
+ total = data.length - off;
1034
1089
  }
1035
- off += skip;
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