Summary

  • RFC 9846 KeyUpdate advances one sender’s TLS 1.3 application traffic secret. The transition message is encrypted under the old sending key; later records use the new generation, and the peer advances the matching receive chain.
  • update_requested creates a bounded reciprocal duty, not remote ownership of the connection. Directions remain independent, crossed requests can advance both sides twice, silent requests coalesce, and local generation and AEAD limits can require refusal or termination.
  • A reliable claim needs four distinct observations: the local API scheduled an update, an old-key transition record was emitted, the peer accepted that transition, and new-generation application data succeeded. None proves old-secret erasure, renewed authentication or application authorization.

The one-byte request inside an encrypted record

Imagine a long-running TLS connection carrying a financial data feed. The server decides that its current sending keys have reached an internal age threshold. Its TLS stack constructs a KeyUpdate with request_update=1. The message body is only the request value, but the transition around it is not one event.

The server encrypts that KeyUpdate with the generation it is leaving. Once it sends the message, all later server records use the next server application traffic secret. The client must authenticate the record before advancing the receive secret associated with server-to-client traffic. If the client then responds, it sends its own KeyUpdate under its old client-to-server key and moves its future sending records onto the next client generation.

At no instant does a single global epoch switch. The two directions have separate secret chains, separate record sequences and separate opportunities for delay. A dashboard with one integer called “TLS key version” destroys this structure precisely when an incident makes it important.

The current rule is RFC 9846

RFC 9846 replaced RFC 8446 in 2026 while keeping TLS 1.3 wire compatibility. The replacement matters here. The basic KeyUpdate mechanism remains: handshake message type 24, permitted after Finished, with update_not_requested(0) and update_requested(1) as the only valid values. But the current text also makes generation limits explicit.

An endpoint receiving KeyUpdate before Finished must abort with unexpected_message. An unknown request value produces illegal_parameter. KeyUpdate must align with the record boundary that precedes the key change. Both sides must require the transition message under the old key before accepting records under the new one; otherwise a truncation attack can become possible.

These are not decorative parser rules. They create the evidence boundary between “new ciphertext arrived” and “the authenticated peer authorized this directional generation transition.” A receiver that speculatively accepts the next key without that old-key bridge has replaced a protocol event with a guess.

A request is stronger than a hint, weaker than ownership

When request_update is zero, the sender announces only its own directional transition. When it is one, the receiver normally must return an update_not_requested KeyUpdate before its next Application Data record. That reciprocal obligation is real. It still has limits.

The current specification forbids another outstanding requested update until a subsequent KeyUpdate arrives from the peer. Several requests received while the peer remains silent can be answered with one update. If both endpoints independently send requested updates and the messages cross, each side also sends a response; each direction can advance by two generations. The protocol therefore anticipates concurrency rather than pretending one participant owns a central rekey clock.

RFC 9846 also caps a sender’s epoch at 2^48 - 1. A receiver must not enforce that limit on someone else, because later specifications may alter the sender-side rule. If a reciprocal update would push the local sender beyond the cap, it must not perform the update and should ignore the request flag. Continued use can eventually meet an AEAD limit and force connection termination.

This is a precise authority design. The peer can submit a valid request. The local endpoint executes only within a finite state machine and its cryptographic ceiling. No authenticated request creates an unbounded right to consume CPU, allocate secrets or keep a connection alive.

Derivation proves continuity, not erasure

Each next application traffic secret is derived from the current directional secret with HKDF-Expand-Label and the label traffic upd. The implementation then derives a new record key and IV. That chain provides continuity: an endpoint without the current secret cannot jump into the next generation merely by observing the wire.

After computing the successor, RFC 9846 says an implementation should delete the previous traffic secret and its associated keys. The distinction between “should” and “proved” is operationally decisive. A new-generation record demonstrates compatible derivation. It cannot inspect a heap, hardware slot, crash dump, kernel offload table or backup process at the remote endpoint.

Forward secrecy after an update depends on deletion of the older material. It is therefore a local memory-governance claim with implementation evidence, not a property that the reciprocal message certifies. An organisation that reports “old keys destroyed” solely because the peer answered KeyUpdate has converted an interoperable transition into a false attestation.

Nor does KeyUpdate renew identity. TLS rekeying keeps the already negotiated connection parameters. It does not ask for a new server certificate, re-run service-identity verification, change the authenticated client, or approve an application command. Fresh record keys can protect the wrong business action just as effectively as the old ones.

Running code exposes the hidden clocks

OpenSSL’s SSL_key_update() can be called only after the initial handshake. A successful call schedules work; the update occurs when later I/O or an explicit handshake-driving call processes it. The application must coordinate outstanding writes. An API return is therefore not the time the transition reached the wire.

GnuTLS makes the direction choice visible. gnutls_session_key_update() with no peer flag refreshes the local sending direction; GNUTLS_KU_PEER also requests the peer’s direction. The operation behaves like record sending and can return nonblocking conditions. Its manual separately distinguishes rekeying from re-authentication.

rustls exposes refresh_traffic_keys() and ordinarily tracks confidentiality limits for the selected cipher suite. Its kernel connection interface demonstrates the other side of abstraction: once an application externalizes record protection, it becomes responsible for approximate record counts, refresh and abort decisions. Moving cryptography out of the library does not move accountability out of the system.

The implementations share the wire mechanism while exposing different scheduling surfaces. A portable control policy should specify desired outcomes and evidence—not assume that three APIs complete the update synchronously or at the same call boundary.

TLS, DTLS and QUIC do not share one rekey event

Transport provenance is mandatory. QUIC uses TLS 1.3 for its handshake but forbids TLS KeyUpdate messages. QUIC version 1 changes packet protection through its Key Phase bit, acknowledgement sequencing and the quic ku derivation. Its initiator changes the send keys, and the responding endpoint changes keys before acknowledging under the new phase. Importing TLS message counters into a QUIC monitor would report nothing even while QUIC rekeys correctly.

DTLS 1.3 retains KeyUpdate but must survive loss and reordering. It assigns epochs, acknowledges update messages, retains older receive material for delayed datagrams and can ignore a reciprocal request near its epoch limit. A response may cross the request and therefore is not automatically an acknowledgement of it.

Calling all three events “TLS rekey” is convenient and evidentially destructive. The same leadership policy may require periodic cryptographic renewal, yet the proof objects differ: ordered transition record in stream TLS, acknowledged epoch movement in DTLS, and packet key phase in QUIC.

What a defensible ledger records

A production event does not need secret bytes. It needs enough structure to reconstruct authority and consequence:

  • stable connection reference, role, transport and negotiated version;
  • send or receive direction and old/new generation numbers;
  • local policy, library safety threshold, operator action or peer request as initiator;
  • update_requested versus update_not_requested and whether a request remained outstanding;
  • successful authentication of the old-key transition record;
  • record-boundary and pending-write state;
  • first successful new-generation record in that direction;
  • coalesced or crossed updates, local rate-limit decision and terminal alert;
  • memory-deletion evidence kept in a separate, locally controlled assurance record.

Four milestones should remain distinct. “Scheduled” is not “emitted.” “Emitted” is not “accepted.” “Accepted” is not “application data succeeded.” Success in all four still does not mean the old secret was scrubbed or the application request was authorized.

Packet capture alone is insufficient because TLS 1.3 encrypts the KeyUpdate. In a controlled test, endpoint callbacks, counters and bounded key logging can map ciphertext to the generation transition. Production key material should not be copied into observability merely to make a graph easier to draw.

Sources