skip to content

In a DTLS 1.2 record, what do the epoch and sequence_number fields do that a stream-mode TLS record never needed?

level: middleimportance: should knowfreq 45%

answer

  1. a receiver can infer nothing here
  2. two fields, not one
  3. key changes have two sides
  4. numbering restarts per key set
  5. window check SHOULD, size MUST

basics

~20 s

The epoch names which key set protects the record and the 6-byte sequence_number numbers it within that epoch, so a receiver can decrypt records that arrive late, out of order or twice. Stream-mode TLS infers both because the transport guarantees order.

solid answer

~40 s

In stream mode the record layer can count: records arrive once and in order, so the 64-bit counter feeding the AEAD nonce is never transmitted. Over datagrams a receiver can infer nothing, so DTLS 1.2 puts both halves on the wire — a `uint16` `epoch` and a `uint48` `sequence_number`. The `epoch` is bumped on every cipher-state change, so a record sent just before a `change_cipher_spec(20)` can still arrive after it and the receiver knows which key set to use. Sequence numbers are kept **per epoch** and restart at 0 for each one, which means a retransmitted epoch-0 record can legitimately carry a lower number than an epoch-1 record that arrived earlier. The pair, not the number alone, identifies a record. The same pair also drives duplicate detection through a sliding receive window.

code

pseudocode · 18 lines
pseudocode
on record arriving with (epoch, sequence_number):

  if I hold no key set for this epoch:
      discard silently

  if sequence_number falls left of the window for this epoch:
      discard silently                 -- too old to judge

  if sequence_number is already marked in that window:
      discard silently                 -- duplicate

  if authenticated decryption fails:
      discard silently                 -- send no alert

  otherwise:
      mark sequence_number in the window
      slide the window right if this is the highest seen
      deliver the payload

go deeper

for a junior

Know that a DTLS record carries its own numbering while a stream-mode TLS record does not, and that the epoch is about which key is in force rather than about time.

for a middle

Be able to explain why the pair is needed: the epoch selects the key set across a key change that both sides cross at different moments, and the sequence number positions the record within that epoch and feeds the nonce.

for a senior

Show the operational edges: per-epoch numbering means low numbers are not older, the replay window is checked before decryption but marked after it, and unverifiable records are dropped without an alert on purpose.

for a principal

Weigh the trade-off the fields buy. Explicit numbering costs header bytes on every small telemetry record, and DTLS 1.3 spends real complexity shrinking and protecting exactly those bytes for constrained links.

## Why anything is on the wire at all Every AEAD record needs a nonce that is never reused under one key, and TLS builds it from a per-record counter combined with a static write IV. Over a reliable ordered stream the counter is free: the sender increments, the receiver increments, and they agree without exchanging a byte. Over datagrams the receiver has no idea whether the record in its hand is the next one, the one after a gap, or one it has already seen — so DTLS carries what TLS infers. DTLS 1.2 adds exactly two fields to the record header: - **`epoch`** — 2 bytes. A counter of cipher-state changes, starting at 0 for the unprotected handshake records and incrementing every time the key set in force changes (each `change_cipher_spec(20)` in DTLS 1.2, each key change in DTLS 1.3). - **`sequence_number`** — 6 bytes, giving a 2^48 record space. The record's position **within its epoch**. The header therefore runs to 13 bytes against stream mode's 5. ## What the epoch actually solves A key change is an event with two sides and no shared clock. The sender switches keys and starts protecting records with the new set, but records protected with the old set may still be in flight, and datagrams reorder. Without the `epoch`, a receiver that has already switched has no way to tell 'a record I cannot decrypt' from 'a record protected with the key set I just retired'. With it, the rule is mechanical: **look at the epoch, select that key set, then use the sequence number to build the nonce.** A receiver may hold more than one epoch's keys briefly for exactly this reason. In a buoy uplink, where the handshake's last flight and the first telemetry burst can cross on a multi-second path, this is not a corner case — it is the normal ordering. ## Numbering is per epoch, and that surprises people Sequence numbers start again at 0 for each new epoch. The consequence is worth stating out loud: **a record with a lower sequence number is not necessarily older.** A retransmitted epoch-0 handshake record carrying number 3 can arrive after an epoch-1 application record carrying number 1, and nothing is wrong. Ordering is only ever defined within one epoch, which is why DTLS 1.3 models the pair as a single 128-bit `RecordNumber` structure — an epoch and a sequence number together — and why an acknowledgement in DTLS 1.3 cites that pair rather than a bare number. ## Duplicate detection, and the two different strengths in one paragraph Explicit numbering also makes replay detection possible, and RFC 6347 states the requirement in two different strengths that are worth quoting separately: 1. The anti-replay check itself **SHOULD** be performed — an application may legitimately disable it. 2. Where it is performed, the receive window **MUST** be at least 32 records. The mechanics are a sliding window over the sequence numbers of one epoch: - a number to the **left** of the window is too old — discard; - a number **already marked** in the window is a duplicate — discard; - a number **inside or to the right** is candidate — attempt authenticated decryption, and only mark and slide **after** it succeeds. Marking before authentication would let an attacker who guesses a future number poison the window with a forged record. And the discards are **silent**: DTLS deliberately does not send an alert for a record it cannot verify, because answering every junk datagram with an alert is itself a denial-of-service lever. ## What changed in DTLS 1.3 | Aspect | DTLS 1.2 | DTLS 1.3 | |---|---|---| | Header shape | fixed 13 bytes | variable `unified_hdr` inside `DTLSCiphertext` | | Version field | present in every record | removed | | Epoch on the wire | full 2-byte value, in the clear | only the two low-order bits, in the first byte | | Sequence number | 6 bytes, in the clear | 1 or 2 bytes, cryptographically protected | | The pair as an object | implicit | the 128-bit `RecordNumber` structure | DTLS 1.3 also has to keep the two record shapes tellable apart, because both share a UDP port: a first byte of `alert(21)`, `handshake(22)` or `ack(26)` means a `DTLSPlaintext` record, while leading bits `001` mean a `DTLSCiphertext` record with a unified header. The short version to carry into an interview: **the epoch says which key, the sequence number says which record under that key, and only the two together order anything.**

  • How strong is the anti-replay requirement in DTLS 1.2, exactly?
    Two different strengths sit in the same paragraph. Performing the check at all is a SHOULD — an application may switch it off where duplicate delivery is harmless. Where it is performed, the window MUST hold at least 32 records. Promoting the first to a MUST, or forgetting the minimum on the second, are the two usual misquotations.
  • Why does a receiver mark the window only after authenticated decryption succeeds?
    Because the window is a record of what was genuinely accepted. If a forged datagram carrying a plausible future number could mark the window, an off-path attacker could make the receiver discard the real record when it arrives — turning an anti-replay mechanism into a denial-of-service one. Check first, decrypt, then mark.
  • A retransmitted epoch-0 record arrives after an epoch-1 record. Is something wrong?
    No. Sequence numbers restart at 0 in every epoch, so numbers are only comparable within one epoch. The receiver selects the epoch-0 key set for that record and judges its number against the epoch-0 window. Treating the sequence number as a global clock is the classic error here.

saying these in an interview costs you the question

  • Says the sequence number orders records globally across epochs
  • Thinks the epoch is a timestamp or a lifetime in seconds
  • Claims DTLS 1.3 still sends the sequence number in the clear
  • Believes a duplicate record should be answered with an alert
  • States the replay check is mandatory with no window minimum
  • Marks the replay window before the record is authenticated