lemon-tls 0.6.0 → 0.7.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.
@@ -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,
@@ -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 || {};
@@ -227,6 +270,23 @@ function DTLSSession(options) {
227
270
  // too — which a connected werift server answers with renegotiation(),
228
271
  // destroying the session instead of recovering it.
229
272
  newFlightPending: false,
273
+
274
+ // ---- Epoch tracking (RFC 9147 §6.1) ----
275
+ // Sending and receiving epochs advance INDEPENDENTLY: a KeyUpdate rekeys
276
+ // one direction only. Until KeyUpdate existed both directions moved
277
+ // together through 0 -> 2 -> 3, so a single number sufficed; it does not
278
+ // any more. ctx.keys[e] therefore holds .write and .read for possibly
279
+ // different generations, and each half is installed on its own.
280
+ writeEpoch: null, // epoch we currently PROTECT outgoing records with
281
+ readEpoch: null, // highest epoch we hold READ keys for
282
+ // A KeyUpdate we have sent whose ACK has not come back. RFC 9147 §8:
283
+ // "implementations MUST NOT send records with the new keys or send a new
284
+ // KeyUpdate until the previous KeyUpdate has been acknowledged". So the
285
+ // new write keys are installed immediately but stay dormant here until
286
+ // the ACK arrives.
287
+ pendingWriteEpoch: null,
288
+ keyUpdateFlight: null, // { payload, epoch, timer, attempts, timeout }
289
+
230
290
  retransmitTimer: null,
231
291
  retransmitCount: 0,
232
292
  retransmitTimeout: 1000,
@@ -322,15 +382,39 @@ function DTLSSession(options) {
322
382
  }
323
383
  transcriptMsgSeqs.push(msgSeq);
324
384
 
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.
385
+ // RFC 9147 §5.9, verbatim: "Hash calculations include entire handshake
386
+ // messages, including DTLS-specific fields: message_seq,
387
+ // fragment_offset, and fragment_length. However, in order to remove
388
+ // sensitivity to handshake message fragmentation, the CertificateVerify
389
+ // and Finished messages MUST be computed as if each handshake message
390
+ // had been sent as a single fragment."
391
+ //
392
+ // So DTLS 1.3 keeps the 12-byte DTLS header in the transcript, exactly
393
+ // like DTLS 1.2 (RFC 6347 §4.2.6) — with fragment_offset 0 and
394
+ // fragment_length equal to the whole body, which is what
395
+ // build_dtls_handshake produces by default.
396
+ //
397
+ // This code did the OPPOSITE for 1.3 and stored TLS-format messages,
398
+ // citing a section number that says the reverse of what it does. The
399
+ // effect is invisible between two copies of this library — both make
400
+ // the same choice and their MACs agree — and only shows up against a
401
+ // conforming peer, where every message parses cleanly and then the
402
+ // Finished MAC fails: "Finished verify_data mismatch" against Chrome,
403
+ // with a transcript whose entries were each 8 bytes short.
404
+ // MEASURED AGAINST CHROME, and it contradicts the plain reading of
405
+ // RFC 9147 §5.9 ("Hash calculations include entire handshake
406
+ // messages, including DTLS-specific fields"):
407
+ // • TLS-format transcript -> Chrome's CertificateVerify VERIFIES,
408
+ // Finished MAC mismatches;
409
+ // • DTLS-format transcript -> Chrome's CertificateVerify FAILS.
410
+ // So BoringSSL signs CertificateVerify over the TLS-format transcript.
411
+ // Since CertificateVerify is the one we can check against a real peer
412
+ // and it passes this way, TLS format stays for 1.3 — the Finished
413
+ // difference is something else and must be found without breaking a
414
+ // signature that demonstrably verifies.
331
415
  let entry = (ctx.selectedVersion === DTLS_VERSION.DTLS1_2)
332
416
  ? build_dtls_handshake(data, msgSeq)
333
- : data; // TLS format for DTLS 1.3 or not-yet-negotiated
417
+ : data;
334
418
 
335
419
 
336
420
  return entry;
@@ -340,15 +424,36 @@ function DTLSSession(options) {
340
424
  * Called when selectedVersion is first determined as DTLS 1.2.
341
425
  * Retroactively converts all existing transcript entries to DTLS format.
342
426
  */
343
- function fixTranscriptForDtls12() {
427
+ /**
428
+ * Convert transcript entries recorded BEFORE the version was known into
429
+ * DTLS handshake format.
430
+ *
431
+ * Both DTLS 1.2 (RFC 6347 §4.2.6) and DTLS 1.3 (RFC 9147 §5.9) hash the
432
+ * 12-byte DTLS handshake header, but the first messages — our ClientHello
433
+ * and the peer's ServerHello — are pushed while selectedVersion is still
434
+ * null, so they land in TLS format and have to be rewritten once the
435
+ * negotiation resolves. Entries already in DTLS format are left alone:
436
+ * a second conversion would wrap a 12-byte header around one that is
437
+ * already there.
438
+ */
439
+ function fixTranscriptForDtls() {
344
440
  let t = tls.context.transcript;
345
441
  for (let i = 0; i < t.length; i++) {
346
- if (i < transcriptMsgSeqs.length) {
347
- t[i] = build_dtls_handshake(t[i], transcriptMsgSeqs[i]);
348
- }
442
+ if (i >= transcriptMsgSeqs.length) continue;
443
+ let e = t[i];
444
+ if (!e || e.length < 4) continue;
445
+ // A DTLS entry is 12 + body; a TLS entry is 4 + body. The declared
446
+ // length lives in the same place in both, so comparing it against the
447
+ // actual size tells them apart without guessing.
448
+ let declared = (e[1] << 16) | (e[2] << 8) | e[3];
449
+ if (e.length === declared + 12) continue; // already DTLS format
450
+ t[i] = build_dtls_handshake(e, transcriptMsgSeqs[i]);
349
451
  }
350
452
  }
351
453
 
454
+ // Kept for callers that still use the old name.
455
+ function fixTranscriptForDtls12() { fixTranscriptForDtls(); }
456
+
352
457
 
353
458
  // ============================================================
354
459
  // Key derivation
@@ -369,17 +474,18 @@ function DTLSSession(options) {
369
474
  }
370
475
 
371
476
  function deriveEpochKeys(secret) {
372
- let cs = TLS_CIPHER_SUITES[ctx.cipherSuite];
373
- let empty = new Uint8Array(0);
374
477
  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);
377
-
378
- let result = { key, iv };
478
+ // record.js owns "traffic secret -> key + iv" and already exports it. This
479
+ // function used to re-derive both inline, which is the same two HKDF calls
480
+ // with the same arguments in a second place — precisely the shape of the
481
+ // label-prefix bug, where one copy of a derivation was fixed and another
482
+ // was not. Only the DTLS-specific extras belong here.
483
+ let result = deriveKeys(secret, ctx.cipherSuite, lp);
379
484
 
380
485
  // DTLS 1.3: derive sn_key for record number encryption
381
486
  if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
382
- result.snKey = derive_sn_key(cs.hash, secret, ctx.cipherSuite, dtlsLabelPrefix());
487
+ result.snKey = derive_sn_key(TLS_CIPHER_SUITES[ctx.cipherSuite].hash, secret,
488
+ ctx.cipherSuite, lp);
383
489
  result.algo = getAeadAlgo(ctx.cipherSuite);
384
490
  }
385
491
 
@@ -407,7 +513,10 @@ function DTLSSession(options) {
407
513
  function dtlsEpochForSecrets(isApplication) {
408
514
  if (ctx.selectedVersion === null && tls.context.selected_version) {
409
515
  ctx.selectedVersion = tls.context.selected_version;
410
- if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) fixTranscriptForDtls12();
516
+ // Both DTLS versions hash the DTLS handshake header (RFC 6347 §4.2.6,
517
+ // RFC 9147 §5.9), so the early TLS-format entries must be rewritten
518
+ // whichever version was negotiated.
519
+ if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) fixTranscriptForDtls();
411
520
  }
412
521
  if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) return isApplication ? 3 : 2;
413
522
  return 1;
@@ -433,9 +542,99 @@ function DTLSSession(options) {
433
542
  read: deriveEpochKeys(remoteSecret),
434
543
  };
435
544
  ctx.writeSeq[dtlsEpoch] = 0;
545
+
546
+ // Baseline for KeyUpdate. Everything after this point counts up from here,
547
+ // per direction.
548
+ ctx.writeEpoch = dtlsEpoch;
549
+ ctx.readEpoch = dtlsEpoch;
436
550
  });
437
551
 
438
552
 
553
+ // ============================================================
554
+ // KeyUpdate (RFC 9147 §8)
555
+ // ============================================================
556
+ //
557
+ // TLSSession derives the new traffic secret and emits 'keyUpdate'; the DTLS
558
+ // layer owns everything the epoch implies. Nothing consumed this event
559
+ // before, so a KeyUpdate updated the session's notion of the traffic secret
560
+ // while the record layer kept protecting and deprotecting with the old keys.
561
+ // That is worse than not implementing it: on DTLS an undeprotectable record
562
+ // is silently discarded (§4.5.2), so the symptom is a connection that simply
563
+ // stops carrying data, with no alert and no error anywhere.
564
+
565
+ tls.on('keyUpdate', function(info) {
566
+ // DTLS 1.2 has no KeyUpdate; the TLS session only emits this for 1.3, but
567
+ // the guard keeps the epoch arithmetic below unreachable on the 1.2 path.
568
+ if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return;
569
+ if (!info || !info.secret) return;
570
+ if (ctx.writeEpoch === null || ctx.readEpoch === null) return;
571
+
572
+ if (info.direction === 'send') {
573
+ // §8: only one KeyUpdate may be in flight. requestKeyUpdate() below
574
+ // refuses up front; reaching here with one pending means the TLS session
575
+ // rotated its secret anyway (e.g. an auto-response to the peer's
576
+ // update_requested while ours was still unacknowledged), which would
577
+ // strand an epoch. Say so rather than silently losing the connection.
578
+ if (ctx.pendingWriteEpoch !== null) {
579
+ ev.emit('error', new Error(
580
+ 'DTLS: a KeyUpdate is already awaiting acknowledgement (RFC 9147 §8)'));
581
+ return;
582
+ }
583
+ let next = ctx.writeEpoch + 1;
584
+ // Install the new write keys, but keep protecting with the old epoch
585
+ // until the peer ACKs (§8). ctx.keys[next] may already carry a .read
586
+ // half from the peer's own update — merge, never overwrite.
587
+ ctx.keys[next] = Object.assign({}, ctx.keys[next], { write: deriveEpochKeys(info.secret) });
588
+ ctx.writeSeq[next] = 0;
589
+ ctx.pendingWriteEpoch = next;
590
+
591
+ } else if (info.direction === 'receive') {
592
+ let next = ctx.readEpoch + 1;
593
+ ctx.keys[next] = Object.assign({}, ctx.keys[next], { read: deriveEpochKeys(info.secret) });
594
+ ctx.readEpoch = next;
595
+ // The previous epoch's read keys are deliberately RETAINED. §8: records
596
+ // already in flight under the old epoch must still deprotect, and our
597
+ // ACK may be lost, so the peer may retransmit under the old keys.
598
+ }
599
+ });
600
+
601
+ /** Arm the post-handshake retransmit for an unacknowledged KeyUpdate. */
602
+ function armKeyUpdateRetransmit(payload, epoch) {
603
+ cancelKeyUpdateRetransmit();
604
+ ctx.keyUpdateFlight = {
605
+ payload: payload, epoch: epoch,
606
+ attempts: 0, timeout: ctx.retransmitTimeout, timer: null,
607
+ };
608
+ scheduleKeyUpdateRetransmit();
609
+ }
610
+
611
+ function scheduleKeyUpdateRetransmit() {
612
+ let f = ctx.keyUpdateFlight;
613
+ if (!f) return;
614
+ f.timer = setTimeout(function () {
615
+ if (!ctx.keyUpdateFlight) return;
616
+ if (f.attempts >= ctx.maxRetransmits) {
617
+ ev.emit('error', new Error('DTLS: KeyUpdate was never acknowledged'));
618
+ cancelKeyUpdateRetransmit();
619
+ return;
620
+ }
621
+ f.attempts++;
622
+ // Rebuild rather than replay: a retransmission is a NEW record and needs
623
+ // a fresh sequence number, or the peer's replay window rejects it.
624
+ let record = buildRecord(f.epoch, CT.HANDSHAKE, f.payload);
625
+ if (record) { _ldbgTx(record, 'keyupdate-rtx'); ev.emit('packet', record); }
626
+ f.timeout = Math.min(f.timeout * 2, 60000);
627
+ scheduleKeyUpdateRetransmit();
628
+ }, f.timeout);
629
+ if (f.timer && typeof f.timer.unref === 'function') f.timer.unref();
630
+ }
631
+
632
+ function cancelKeyUpdateRetransmit() {
633
+ if (ctx.keyUpdateFlight && ctx.keyUpdateFlight.timer) clearTimeout(ctx.keyUpdateFlight.timer);
634
+ ctx.keyUpdateFlight = null;
635
+ }
636
+
637
+
439
638
  // ============================================================
440
639
  // Outgoing: TLSSession message → DTLS datagram
441
640
  // ============================================================
@@ -461,7 +660,11 @@ function DTLSSession(options) {
461
660
  if (tlsEpoch === 0) {
462
661
  dtlsEpoch = 0;
463
662
  } else if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
464
- dtlsEpoch = tlsEpoch === 1 ? 2 : 3; // epoch 2=handshake, 3=app
663
+ // epoch 2 = handshake. Anything above rides the CURRENT application
664
+ // epoch, which is 3 until a KeyUpdate moves it to 4, 5, ... Hardcoding 3
665
+ // meant every post-handshake message stayed on the pre-rekey epoch.
666
+ dtlsEpoch = (tlsEpoch === 1) ? 2
667
+ : (ctx.writeEpoch !== null ? ctx.writeEpoch : 3);
465
668
  } else {
466
669
  dtlsEpoch = 1; // DTLS 1.2: epoch 1 for all encrypted
467
670
  }
@@ -504,12 +707,30 @@ function DTLSSession(options) {
504
707
  let frags = fragmentMessage(dtlsMsg, data, seq);
505
708
 
506
709
  // Build DTLS records and emit
710
+ // RFC 9147 §5.8.4: post-handshake messages do NOT belong to the handshake
711
+ // flight. Each category gets an independent state machine; folding a
712
+ // KeyUpdate into currentFlight would make a handshake retransmission
713
+ // replay it, and a KeyUpdate ACK cancel the handshake's timer.
714
+ let isPostHandshakeKU = (type === 'key_update' &&
715
+ ctx.selectedVersion === DTLS_VERSION.DTLS1_3);
716
+ if (isPostHandshakeKU) {
717
+ for (let i = 0; i < frags.length; i++) {
718
+ let kuRecord = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
719
+ if (kuRecord) { _ldbgTx(kuRecord, 'keyupdate'); ev.emit('packet', kuRecord); }
720
+ }
721
+ // KeyUpdate must be acknowledged (§8), so it is retransmitted on its own
722
+ // timer until an ACK arrives. Only the last fragment is tracked; a
723
+ // KeyUpdate body is 1 byte and never fragments in practice.
724
+ armKeyUpdateRetransmit(frags[frags.length - 1], dtlsEpoch);
725
+ return;
726
+ }
727
+
507
728
  for (let i = 0; i < frags.length; i++) {
508
729
  let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
509
730
  // The flight captures the fragment, not the record — see currentFlight.
510
731
  ctx.currentFlight.push({ epoch: dtlsEpoch, contentType: CT.HANDSHAKE, payload: frags[i] });
511
732
  if (record) {
512
- ev.emit('packet', record);
733
+ _ldbgTx(record, 'L543'); ev.emit('packet', record);
513
734
  } else {
514
735
  // buildRecord refused to build (no keys for an encrypted epoch).
515
736
  // Dropping the record here without a word is how a key-epoch mismatch
@@ -630,7 +851,7 @@ function DTLSSession(options) {
630
851
  // packet pushes `null` into the transport; drop it instead — we already
631
852
  // surfaced the reason through 'error'.
632
853
  if (!record) return;
633
- ev.emit('packet', record);
854
+ _ldbgTx(record, 'L664'); ev.emit('packet', record);
634
855
  }
635
856
 
636
857
  /**
@@ -654,7 +875,7 @@ function DTLSSession(options) {
654
875
  // the peer could never decrypt the accompanying Finished — an
655
876
  // unrecoverable stall under single-datagram loss.
656
877
  ctx.currentFlight.push({ epoch: 0, contentType: CT.CHANGE_CIPHER_SPEC, payload: new Uint8Array([1]) });
657
- ev.emit('packet', record);
878
+ _ldbgTx(record, 'L688'); ev.emit('packet', record);
658
879
  }
659
880
 
660
881
 
@@ -693,6 +914,15 @@ function DTLSSession(options) {
693
914
  // NewSessionTicket etc. are optional and skipped here for simplicity).
694
915
  let ackable = [];
695
916
 
917
+ // Whether these records arrived AFTER the handshake must be decided here,
918
+ // before the loop — not after it. The datagram that completes the
919
+ // handshake flips ctx.state to 'connected' partway through, so reading the
920
+ // state afterwards misclassifies the handshake's own epoch-2 records as
921
+ // post-handshake and ACKs them inside the application-data epoch. That is
922
+ // the spurious epoch-3 ACK that makes a peer answer decode_error on the
923
+ // NEXT record — so a perfectly good SCTP COOKIE_ECHO takes the blame.
924
+ let wasConnectedOnEntry = (ctx.state === 'connected');
925
+
696
926
  for (let i = 0; i < records.length; i++) {
697
927
  // A record we could not decrypt when the datagram was parsed. Keys may
698
928
  // have arrived since — processing an earlier record in THIS datagram
@@ -712,8 +942,19 @@ function DTLSSession(options) {
712
942
  };
713
943
  }
714
944
 
945
+ // NEVER ACK THE FIRST FLIGHT (RFC 9147 §7.1): "ACKs MUST NOT be
946
+ // sent for the ClientHello". The first flight is acknowledged by
947
+ // the RESPONSE to it — the ServerHello — and a peer that receives
948
+ // an ACK where none belongs answers decode_error.
949
+ // Epoch 0 records are exactly that first flight: everything later
950
+ // is protected under epoch 2 or above. This only bites when the
951
+ // ClientHello is large enough to FRAGMENT, because a single-record
952
+ // flight is consumed before any ACK decision is made — which is
953
+ // why a two-way call worked and an SFU, whose extra transceivers
954
+ // push the ClientHello past the MTU, did not.
715
955
  if (records[i].type === CT.HANDSHAKE &&
716
- ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
956
+ ctx.selectedVersion === DTLS_VERSION.DTLS1_3 &&
957
+ records[i].epoch > 0) {
717
958
  ackable.push({ epoch: records[i].epoch, seq: records[i].seq });
718
959
  }
719
960
 
@@ -738,8 +979,43 @@ function DTLSSession(options) {
738
979
  // We ACK from the highest epoch for which we already have write keys (so the
739
980
  // ACK itself is protected once handshake keys exist; epoch 0 before that).
740
981
  if (ackable.length > 0) {
741
- let ackEpoch = ctx.keys[3] ? 3 : (ctx.keys[2] ? 2 : 0);
742
- sendAck(ackEpoch, ackable);
982
+ // ACKs RIDE THE HANDSHAKE EPOCH (RFC 9147 §7): they acknowledge
983
+ // handshake records, so epoch 2 once handshake keys exist and
984
+ // epoch 0 before that — never epoch 3. Picking "the highest epoch
985
+ // we have keys for" sent them under APPLICATION-DATA protection,
986
+ // where the peer decrypts with app keys and finds a content type
987
+ // that does not belong. Chrome answers decode_error.
988
+ // This shows up the moment a ClientHello is large enough to be
989
+ // FRAGMENTED — an SFU with several transceivers crosses the MTU
990
+ // and a two-way call does not, which is exactly why bidirectional
991
+ // worked and party mode did not.
992
+ // Post-handshake (§7.1: "For post-handshake messages, ACKs SHOULD be
993
+ // sent once for each received and processed handshake record") the
994
+ // handshake epoch is gone and §7 requires the HIGHEST available sending
995
+ // epoch. During the handshake it is still epoch 2, or 0 before keys.
996
+ // RFC 9147 §7.1 lists the flights that are IMPLICITLY acknowledged by
997
+ // the responding flight, and the client's final flight is pointedly not
998
+ // among them: "In general, flights MUST be ACKed unless they are
999
+ // implicitly acknowledged", excepting "handshake flights other than the
1000
+ // client's final flight of the main handshake".
1001
+ //
1002
+ // So the two roles need opposite answers here, which is why a
1003
+ // state-only test could not get both right:
1004
+ // client — the server's flight is implicitly acked by our own final
1005
+ // flight, so an explicit ACK is both redundant and (at the
1006
+ // application epoch) actively harmful;
1007
+ // server — the client's final flight has no responding flight, so
1008
+ // without an explicit ACK the client retransmits it forever.
1009
+ // §7 then fixes the epoch: after the handshake, the highest available
1010
+ // sending epoch.
1011
+ let justCompleted = (!wasConnectedOnEntry && ctx.state === 'connected');
1012
+ let finalFlightAck = isServer && justCompleted;
1013
+ let atAppEpoch = wasConnectedOnEntry || finalFlightAck;
1014
+
1015
+ let ackEpoch = atAppEpoch
1016
+ ? (ctx.writeEpoch !== null ? ctx.writeEpoch : 3)
1017
+ : (ctx.keys[2] ? 2 : 0);
1018
+ sendAck(ackEpoch, ackable, atAppEpoch);
743
1019
  }
744
1020
  }
745
1021
 
@@ -781,6 +1057,12 @@ function DTLSSession(options) {
781
1057
  } else if (record.type === CT.ALERT) {
782
1058
  let level = record.content[0];
783
1059
  let desc = record.content[1];
1060
+ // Name the alert either way. A close_notify is a CLEAN shutdown and
1061
+ // a failure is not, but both present to the application as "the
1062
+ // connection ended" — and telling them apart is the difference
1063
+ // between "the peer hung up" and "the peer rejected us".
1064
+ _ldbg('RX Alert level=' + level + ' desc=' + desc +
1065
+ (desc === 0 ? ' (close_notify — clean shutdown)' : ' (failure)'));
784
1066
  if (desc === 0) {
785
1067
  // close_notify
786
1068
  ctx.state = 'closed';
@@ -1160,7 +1442,7 @@ function DTLSSession(options) {
1160
1442
  // HVR always uses msg_seq 0 and is sent in the clear (epoch 0).
1161
1443
  let hvrMsg = build_dtls_handshake(build_message(3, build_hello_verify_request({ cookie: cookie })), 0);
1162
1444
  let record = buildDtlsPlaintext(CT.HANDSHAKE, 0, nextWriteSeq(0), hvrMsg);
1163
- ev.emit('packet', record);
1445
+ _ldbgTx(record, 'L1194'); ev.emit('packet', record);
1164
1446
  }
1165
1447
 
1166
1448
  /**
@@ -1224,7 +1506,7 @@ function DTLSSession(options) {
1224
1506
  let dtlsMsg = build_dtls_handshake(ch2, 1);
1225
1507
  let record = buildRecord(0, CT.HANDSHAKE, dtlsMsg);
1226
1508
  ctx.currentFlight = [{ epoch: 0, contentType: CT.HANDSHAKE, payload: dtlsMsg }];
1227
- if (record) ev.emit('packet', record);
1509
+ if (record) { _ldbgTx(record, 'L1258'); ev.emit('packet', record); }
1228
1510
 
1229
1511
  tls.context.message_sent_seq = 2;
1230
1512
  startRetransmitTimer();
@@ -1243,15 +1525,37 @@ function DTLSSession(options) {
1243
1525
  * new nonce, and a monotonic seq the receiver can reconstruct.
1244
1526
  */
1245
1527
  function resendFlight() {
1528
+ // NEVER AFTER THE HANDSHAKE COMPLETES. Once we are connected the
1529
+ // handshake flight is finished by definition, and re-emitting it
1530
+ // sends HANDSHAKE content inside an APPLICATION-DATA epoch — the
1531
+ // peer decrypts a record with its app-data keys and finds a message
1532
+ // it cannot place. Chrome tolerated one and then answered
1533
+ // decode_error on the next SCTP record, which read as an SCTP
1534
+ // problem for several rounds and was not one: the giveaway is a
1535
+ // record in the log with no matching sctp->dtls line, because the
1536
+ // payload never came from the application at all.
1537
+ // RFC 9147 §5.8: retransmission is bounded by the handshake; after
1538
+ // it, ACKs are the mechanism, not a timer.
1539
+ if (ctx.state === 'connected' || ctx.state === 'closed') {
1540
+ cancelRetransmit();
1541
+ return;
1542
+ }
1246
1543
  for (let i = 0; i < ctx.currentFlight.length; i++) {
1247
1544
  let f = ctx.currentFlight[i];
1248
1545
  let record = buildRecord(f.epoch, f.contentType, f.payload);
1249
- if (record) ev.emit('packet', record);
1546
+ if (record) { _ldbgTx(record, 'L1280'); ev.emit('packet', record); }
1250
1547
  }
1251
1548
  }
1252
1549
 
1253
1550
  function startRetransmitTimer() {
1254
1551
  cancelRetransmit();
1552
+ // ONE GATE, AT THE SOURCE. Four call sites arm this timer and only
1553
+ // one of them checks the state, so a timer armed while the handshake
1554
+ // was still running could fire after it finished and re-emit the
1555
+ // flight into the application-data epoch. Refusing to arm once
1556
+ // connected (or closed) covers every caller, including any added
1557
+ // later, without each having to remember the rule.
1558
+ if (ctx.state === 'connected' || ctx.state === 'closed') return;
1255
1559
  ctx.retransmitTimer = setTimeout(function() {
1256
1560
  if (ctx.retransmitCount >= ctx.maxRetransmits) {
1257
1561
  ev.emit('error', new Error('DTLS handshake timeout — max retransmits exceeded'));
@@ -1291,16 +1595,52 @@ function DTLSSession(options) {
1291
1595
 
1292
1596
  function processAck(content) {
1293
1597
  let acks = parseDtlsAck(content);
1294
- // ACK received — flight was acknowledged
1598
+
1599
+ // An outstanding KeyUpdate is what this ACK is for: §8 forbids protecting
1600
+ // anything with the new keys until it arrives, so THIS is the moment the
1601
+ // new epoch becomes live. Keys were installed when the KeyUpdate was sent;
1602
+ // only the switch happens here.
1603
+ if (ctx.pendingWriteEpoch !== null) {
1604
+ ctx.writeEpoch = ctx.pendingWriteEpoch;
1605
+ ctx.pendingWriteEpoch = null;
1606
+ cancelKeyUpdateRetransmit();
1607
+ ev.emit('keyUpdate', { direction: 'send', epoch: ctx.writeEpoch });
1608
+ return;
1609
+ }
1610
+
1611
+ // Everything else keeps the original behaviour verbatim. In particular the
1612
+ // ACK for the client's final flight (which RFC 9147 §7.1 requires the peer
1613
+ // to send explicitly, and which arrives at the application epoch) must
1614
+ // still cancel that flight's retransmit timer.
1295
1615
  cancelRetransmit();
1296
1616
  startNewFlight();
1297
1617
  }
1298
1618
 
1299
- function sendAck(epoch, recordsToAck) {
1619
+ function sendAck(epoch, recordsToAck, postHandshake) {
1300
1620
  if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return;
1621
+ // ACKs BELONG TO THE HANDSHAKE (RFC 9147 §7). They acknowledge
1622
+ // handshake records, and once the handshake is complete there is
1623
+ // nothing left to acknowledge — but this kept firing afterwards and
1624
+ // put an ACK record inside the APPLICATION-DATA epoch. The peer
1625
+ // decrypts it with app-data keys, finds a content type that has no
1626
+ // business there, and the stream is poisoned: Chrome tolerated it
1627
+ // and then answered decode_error on the next record, which made a
1628
+ // perfectly good SCTP COOKIE_ECHO look like the culprit for several
1629
+ // rounds. The giveaway in the log is a record with no matching
1630
+ // sctp->dtls line — the payload never came from the application.
1631
+ // ...that reasoning applies to HANDSHAKE flights. Post-handshake messages
1632
+ // are a separate case the original guard swept up with them: KeyUpdate,
1633
+ // NewSessionTicket and friends have no responding flight to acknowledge
1634
+ // them implicitly, so §7.1 requires an explicit ACK — without one the peer
1635
+ // retransmits its KeyUpdate forever and never activates its new keys.
1636
+ // `postHandshake` is set only for records actually received and processed
1637
+ // after the handshake, so the spurious-ACK bug this guard was written for
1638
+ // cannot return through it.
1639
+ if (!postHandshake && (ctx.state === 'connected' || ctx.state === 'closed')) return;
1640
+ if (ctx.state === 'closed') return;
1301
1641
  let payload = buildDtlsAck(recordsToAck);
1302
1642
  let record = buildRecord(epoch, CT.ACK, payload);
1303
- ev.emit('packet', record);
1643
+ _ldbgTx(record, 'L1334'); ev.emit('packet', record);
1304
1644
  }
1305
1645
 
1306
1646
 
@@ -1328,6 +1668,13 @@ function DTLSSession(options) {
1328
1668
  });
1329
1669
 
1330
1670
  tls.on('error', function(e) {
1671
+ // The peer's REASON, spelled out. When BoringSSL rejects part of our
1672
+ // flight it sends an alert and closes; without printing it here the
1673
+ // only symptom is a connection that dies after a handshake that
1674
+ // looked complete, which is exactly the DTLS 1.3 interop case.
1675
+ _ldbg('ERROR ' + (e && e.message ? e.message : e) +
1676
+ (e && e.alertDescription != null ? ' alertDesc=' + e.alertDescription : '') +
1677
+ (e && e.code ? ' code=' + e.code : ''));
1331
1678
  ev.emit('error', e);
1332
1679
  });
1333
1680
 
@@ -1347,9 +1694,11 @@ function DTLSSession(options) {
1347
1694
  }
1348
1695
  if (typeof data === 'string') data = new TextEncoder().encode(data);
1349
1696
 
1350
- let epoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1;
1697
+ let epoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3
1698
+ ? (ctx.writeEpoch !== null ? ctx.writeEpoch : 3)
1699
+ : 1;
1351
1700
  let record = buildRecord(epoch, CT.APPLICATION_DATA, data);
1352
- ev.emit('packet', record);
1701
+ _ldbgTx(record, 'L1383'); ev.emit('packet', record);
1353
1702
  }
1354
1703
 
1355
1704
 
@@ -1370,13 +1719,19 @@ function DTLSSession(options) {
1370
1719
  // picks the highest epoch we hold write keys for.
1371
1720
  let epoch;
1372
1721
  if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
1373
- epoch = ctx.keys[3] ? 3 : (ctx.keys[2] ? 2 : 0);
1722
+ // After a KeyUpdate the live epoch is 4, 5, ... — the peer has moved on
1723
+ // and no longer reads epoch 3, so a close_notify sent there is dropped
1724
+ // and the shutdown looks like a timeout instead of a clean close.
1725
+ epoch = (ctx.writeEpoch !== null && ctx.keys[ctx.writeEpoch])
1726
+ ? ctx.writeEpoch
1727
+ : (ctx.keys[3] ? 3 : (ctx.keys[2] ? 2 : 0));
1374
1728
  } else {
1375
1729
  epoch = ctx.keys[1] ? 1 : 0;
1376
1730
  }
1377
1731
  sendAlertRecord(epoch, new Uint8Array([1, 0])); // warning, close_notify
1378
1732
  ctx.state = 'closed';
1379
1733
  cancelRetransmit();
1734
+ cancelKeyUpdateRetransmit();
1380
1735
  ev.emit('close');
1381
1736
  }
1382
1737
 
@@ -1406,6 +1761,31 @@ function DTLSSession(options) {
1406
1761
  /** Send application data (after connect). */
1407
1762
  send: send,
1408
1763
 
1764
+ /**
1765
+ * Request a TLS 1.3 KeyUpdate (RFC 9147 §8).
1766
+ *
1767
+ * Returns false without doing anything if the session is not a connected
1768
+ * DTLS 1.3 association, or if an earlier KeyUpdate has not been
1769
+ * acknowledged yet — §8 permits only one in flight. The new epoch does not
1770
+ * become active on send; it activates when the peer's ACK arrives.
1771
+ *
1772
+ * @param {boolean} [requestPeer] also ask the peer to update its own keys
1773
+ * @returns {boolean} whether the update was initiated
1774
+ */
1775
+ requestKeyUpdate: function(requestPeer) {
1776
+ if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return false;
1777
+ if (ctx.state !== 'connected') return false;
1778
+ if (ctx.pendingWriteEpoch !== null) return false;
1779
+ tls.requestKeyUpdate(!!requestPeer);
1780
+ return true;
1781
+ },
1782
+
1783
+ /** Current epoch used to protect outgoing records (null before connect). */
1784
+ getWriteEpoch: function() { return ctx.writeEpoch; },
1785
+
1786
+ /** Highest epoch for which read keys are installed (null before connect). */
1787
+ getReadEpoch: function() { return ctx.readEpoch; },
1788
+
1409
1789
  /** Close the DTLS session. */
1410
1790
  close: close,
1411
1791