skip to content

TLS 1.3 puts no record sequence number on the wire over a stream transport, so how is each record's nonce formed?

level: middleimportance: should knowfreq 46%

answer

  1. the counter never travels on the wire
  2. one counter per direction, per key
  3. 64-bit value starting at zero
  4. padded, then XORed with the static IV
  5. loss or replay breaks the next record

basics

~20 s

Each side keeps a 64-bit record counter for reading and one for writing, both starting at zero. The counter is left-padded to the length of the static write IV and XORed with it, producing a distinct nonce for every record.

solid answer

~50 s

The counter is implicit: nothing about it appears in the record. Each endpoint keeps one 64-bit value for the records it writes and one for the records it reads, each starting at zero and reset to zero whenever the traffic keys change. To protect a record, the writer pads its counter out to the length of the static write IV derived for that direction, XORs the two, and uses the result as that record's nonce, then increments. The reader recomputes the same value from its own counter. Because the counter is never transmitted, the reader arrives at the right nonce only if it has seen exactly the records the writer sent, in order - so a deleted, reordered or replayed record makes the integrity check fail and the connection ends with `bad_record_mac(20)`. The counter must not wrap: a sender that would reach its maximum rekeys or closes.

code

pseudocode · 12 lines
pseudocode
function record_nonce(static_write_iv, sequence_number):
    counter = big_endian(sequence_number, width = 8)
    padded = left_pad_with_zeros(counter, width = length(static_write_iv))
    return xor(padded, static_write_iv)

function protect(fragment, inner_type):
    if write_sequence_number is at its 64-bit maximum:
        reject("rekey or close before the counter wraps")
    nonce = record_nonce(static_write_iv, write_sequence_number)
    ciphertext = seal(write_key, nonce, fragment, inner_type)
    write_sequence_number = write_sequence_number + 1
    return ciphertext

go deeper

for a junior

Recall that every record gets its own nonce and that nothing about it is transmitted - the two sides count records and derive the same value independently.

for a middle

Explain the construction itself: a 64-bit counter per direction, padded and XORed with a static IV, reset to zero on every key change, and incremented once per record.

for a senior

Show what the implicitness buys operationally: deletion, reordering and replay all collapse into one fatal integrity failure, so a stream with a missing middle is never delivered.

for a principal

The design lesson to articulate is spending state instead of bytes, and what that costs: no recovery path once the counters diverge, and a hard rekey obligation on any long-lived connection.

## Where the nonce comes from Every protected record needs a nonce, and TLS 1.3 builds one without spending a single byte on the wire. The ingredients are: - a **static write IV**, derived once per direction from the traffic secret alongside the key, and constant for as long as that key is in force; - a **64-bit record sequence number**, held by the endpoint and never transmitted over a reliable stream transport. The construction is: take the sequence number as a big-endian 64-bit value, left-pad it with zeros to the length of the static IV, and XOR it with that IV. The result is the nonce for this record. Increment the counter and the next record gets a different one. Because the counter occupies the low-order end of the padded value, consecutive records differ in the last few octets of their nonce and in nothing else - which is all that is required, since the pair of key and nonce never repeats. ## Four counters, and when they reset | counter | kept by | counts | |---|---|---| | write counter | each endpoint | records that endpoint has protected | | read counter | each endpoint | records that endpoint has successfully opened | There are two per endpoint and therefore four on a connection, and the client's write counter mirrors the server's read counter when nothing has gone wrong. Each starts at zero, and each **resets to zero whenever the keys for that direction change** - the first record protected under a new key always uses sequence number zero. That is why the counter can be small enough to be cheap and still never repeat a nonce under one key. ## What happens when the stream is disturbed This is the part worth being able to state precisely, because "it gets blocked" is not a mechanism. The reader's counter advances only when it successfully opens a record. So: 1. **A record is deleted in flight.** The reader's counter is now one behind the writer's. The next record's nonce is computed wrongly, the authenticated-encryption check fails, and the reader ends the connection with `bad_record_mac(20)`. 2. **Two records are swapped.** The same thing: each is opened under the other's nonce, and neither verifies. 3. **A record is replayed.** By the time the copy arrives, the reader's counter has moved past that value, so the replayed record is opened under the wrong nonce and fails. 4. **Bytes inside a record are altered.** The integrity check fails on its own account, with the same alert. In every case the failure is fatal and the connection ends. There is no "skip the bad record and resynchronise": once the counters are out of step, nothing further can be opened, and that is deliberate. ## The ceiling on the counter A 64-bit counter is large but not unlimited, and TLS treats reaching the end as a hard stop rather than a wrap. An implementation whose write counter is about to exceed its maximum must either perform a key update - which installs new keys and resets the counter to zero - or terminate the connection. Wrapping would reuse a nonce under a key that is still in force, and the construction's entire guarantee rests on that never happening. Separately, an authenticated-encryption algorithm may carry its own limit on how many records may be protected under one key, which can be reached long before the counter is. Both limits point to the same operational conclusion: a connection that lives for hours and carries a continuous feed needs a rekeying plan, not just a key. ## What this is not - It is **not** a replay window. Over a reliable stream transport there is no window and no tolerance for gaps; the order is exact or the connection ends. - It is **not** carried in the record. A reader that has lost track of its own count cannot recover it from the wire. - It is **not** shared between directions. The two directions advance independently under different keys and different IVs. ## In practice For a long-lived ingest connection the visible symptom of all this is a connection that dies with an integrity failure rather than one that quietly loses a record. That is the intended behaviour: the record layer would rather stop than deliver a stream whose middle is missing, because it has no way to tell the application which part was lost.

  • What must a sender do as its 64-bit write counter approaches its maximum?
    It must not wrap. It either performs a key update, which installs fresh keys for that direction and resets the counter to zero, or it terminates the connection. Continuing past the maximum would repeat a nonce under a key that is still in force.
  • An on-path party deletes one protected record from the stream. What does the receiver observe?
    Its read counter is now behind the sender's, so the next record is opened under the wrong nonce, the integrity check fails, and the connection ends with `bad_record_mac(20)`. Deletion, reordering and replay all surface as that same failure; none of them passes unnoticed.
  • Why is there no replay window here, when other protocols need one?
    A window exists to tolerate loss and reordering, which a reliable stream transport does not produce - records arrive exactly once and in order or the transport has already failed. Exact counting is therefore affordable, and any gap is treated as tampering rather than as normal network behaviour.

saying these in an interview costs you the question

  • Thinks the record sequence number is sent with each record
  • Says both directions share a single counter
  • Believes the counter keeps running across a key change
  • Assumes a dropped record is skipped and the stream resynchronises
  • Describes the per-record nonce as a fresh random value