Internet Protocols · TLS
Session Resumption in TLS 1.3: Tickets, Binders, and 0-RTT Replay
How a ticket becomes a pre-shared key, what the binder actually signs over, why early data cannot be made replay-proof, and what servers do about it.
A full handshake costs a round trip and, more expensively, a signature operation on the server and a chain validation on the client. Resumption replaces both with a shared secret that was established earlier. The mechanism is the same one that produces the handshake keys, run with an additional input: a pre-shared key supplied by the client and identified by a ticket the server issued previously.
From resumption secret to pre-shared key
When a handshake completes, both sides hold a resumption master secret derived from the master secret and the transcript hash through the client’s Finished. A server that wants to support resumption sends one or more NewSessionTicket messages after its Finished, and those messages can arrive at any time during the connection, not only at the start. Each ticket carries a lifetime in seconds, an age addition, a nonce, the ticket itself as an opaque byte string, and optionally an early data indication with a maximum size.
The PSK is derived from the resumption master secret with a label and the ticket nonce. The nonce is per-ticket and increments, so two tickets issued on the same connection produce different PSKs even though they derive from the same master secret. The ticket body is opaque to the client. It may be a database key, or it may be an encrypted blob containing the PSK and its metadata, which is how a server avoids storing state per client.
The server’s choice between those two shapes is a real trade. Storing state means a lookup per resumption and a shared store across the server fleet, which is why tickets are often encrypted under a key that all front ends hold. Encrypting the state into the ticket means no lookup, but it moves the state into the client’s hands: the ticket must be authenticated, its contents must be treated as untrusted until verified, and rotating the encryption key invalidates every outstanding ticket at once. The lifetime field is advisory on the client side and enforced on the server side, because a client can lie about the age it reports.
What the binder covers, and why the extension must be last
The client offers resumption by including pre_shared_key in its ClientHello, listing one or more identities, each an identity plus an obfuscated ticket age, and one binder per identity. The binders are HMACs computed with a key derived from each PSK, over a truncated transcript hash.
The truncation point is defined precisely: the transcript is the ClientHello with the binders list removed, so it covers the identities and the obfuscated ages but not the binder values themselves. A binder cannot cover itself, and this is the smallest truncation that keeps the rest of the offer authenticated. Because the binders list is the final field of the pre_shared_key extension, and that extension must be last, the truncation lands at the end of the message. If anything followed the extension, it would sit outside the binder’s coverage and an attacker could modify it without detection, which is why the specification makes the ordering a requirement rather than a convention.
The binder is what makes resumption safe against an active attacker. Without it, a client could be induced to offer a ticket belonging to a different server, or a server could be induced to accept a ticket whose associated handshake was not the one being resumed. The HMAC binds the PSK to the specific ClientHello, including the key shares, the cipher suite list, and the SNI value. A server that sees a binder mismatch rejects the offer and falls back to a full handshake.
The age field is separate from the binder and is not covered by it in the same way; it is a hint used by the server to decide whether to accept early data, and the server is expected to allow a tolerance for clock differences. The obfuscation applied to it, the ticket’s age addition added modulo 2^32, keeps a passive observer from reading a client’s ticket age directly, which would otherwise be a coarse clock signal.
Two secrets, not one
When a PSK is offered and accepted, the key schedule changes shape. The early_secret becomes the extract of the PSK rather than the extract of zeros, so everything downstream depends on the PSK. A server that selects a PSK but also sends a key_share gets both: the handshake_secret is derived from the PSK and the shared secret together, which preserves forward secrecy for the connection even though the PSK itself may be long-lived.
A server that selects a PSK and omits key_share gets a handshake with no forward secrecy at all: recovering the PSK recovers the session. The specification permits this, and implementations vary in whether they allow it. The distinction is visible in the handshake and matters for policy: a connection that resumed without a key share should not be treated as equivalent to one that did.
Resumption does not authenticate the server in the same way a full handshake does. The client checks the server’s Finished, which proves the server holds the PSK, and that is the entire authentication. No certificate is sent and no chain is validated. A client that resumes a session established with a server whose certificate has since been revoked learns nothing about the revocation, which is one of the reasons revocation checking is difficult to bolt onto resumption.
Early data and the replay problem
With a valid ticket that carries an early data indication, the client may send application data in its first flight, encrypted under the early traffic secret derived from the PSK and the ClientHello transcript. That data arrives before the server has sent anything, so it saves the round trip entirely. The saving is real: on a connection with a long round trip, the first request can be processed without waiting.
The cost is replay. A client cannot know, at the moment it sends, whether the server will accept the PSK, and it cannot know whether the request it sent is the one the server processes. An attacker who captures the ClientHello with its early data can retransmit it to the same server, and a server that accepts early data again will process the request twice. Nothing in the protocol prevents this, because the server has no state that would let it recognise the replay: the whole point of tickets is that the server keeps no per-client state.
The specification is explicit that early data has no replay protection and that applications must consider whether the requests they send are safe to repeat. Requests that change state are the hazard: a transfer of funds, an increment of a counter, a POST that creates an object. Requests that only read are safe to repeat in the sense that the second response is redundant, though even those can leak information through timing if the response differs.
Servers have several options, and each has a cost. Refusing early data entirely is the simplest and costs the round trip it was meant to save. Accepting it only for requests whose method is idempotent by definition is a common compromise, and it relies on the client and server agreeing about what those methods are. Single-use tickets reduce the window: a server that records which tickets have been used for early data can reject a second use, but that requires the state that tickets were designed to avoid, and it only works within the window the record is retained. Binding early data to a token issued out of band, such as a cookie set by a previous connection, is stronger and requires the client to have connected before.
There is a further constraint on the client side. A client that sends early data and then receives a HelloRetryRequest must not send the same data again on the retried handshake; the transcript has changed and the early secret no longer applies. Implementations typically drop the early data and retry the handshake without it, which means the request is sent twice at the application level unless the client tracks the failure.
Tickets, lifetimes, and what a resumed connection remembers
A ticket’s lifetime is bounded by the server, and the client is expected to discard it after that period plus a small allowance. Servers commonly issue short lifetimes, on the order of hours to a day, so that a compromised ticket encryption key has a bounded blast radius. Rotating the key is the only way to invalidate tickets early, and rotation invalidates all of them, not a subset.
The resumption secret is bound to the transcript of the original handshake, so a ticket cannot be moved to a different server identity without the binder failing. What a resumed connection inherits is the authentication decision made at the time of the original handshake, along with whatever the client concluded about revocation then; the revocation checks are not repeated on the resumed connection. The record layer on the resumed connection protects data under keys derived from the PSK, and the per-key data limits apply to those keys just as they do to any other.