TLS Early Data Needs Replay-Safe Application Semantics

TLS 1.3 can carry application data in the client’s first flight when the client and server share a suitable pre-shared key (PSK), commonly from an earlier connection. This mode is called early data or 0-RTT. It removes a handshake wait from the critical path for eligible application traffic, but the latency reduction comes with a narrower security contract.

The central constraint is replay. TLS does not provide a non-replay guarantee for 0-RTT data across connections. A server can deploy anti-replay mechanisms, yet the application still has to treat early data as potentially replayed. That distinction matters whenever one accepted request can change durable state, consume a one-time capability, trigger an external action, or produce another effect that should occur only once.

This is not a defect in TLS encryption. Early data is encrypted. The issue comes from when its keys and request bytes become available relative to the new handshake.

Early data precedes the new ServerHello

For a resumed TLS 1.3 connection, a client can derive early traffic keys from the PSK and send application data with its ClientHello. The server has not yet contributed the fresh ServerHello values that participate in later traffic keys.

A simplified flow is:

Client                                      Server

ClientHello
+ pre_shared_key
+ early_data
Application Data (0-RTT)  ------------->

                                  ServerHello
                                  EncryptedExtensions
                                  Finished
                         <-------------

Finished
Application Data (1-RTT)  ------------->

The current TLS 1.3 specification, RFC 9846, states two important limitations for early data: the protocol does not provide the same forward-secrecy guarantee as later traffic, and it provides no guarantee of non-replay between connections. The second property is the application-design problem addressed here.

An attacker does not need to decrypt a captured early-data flight to attempt a replay. Reproducing the relevant encrypted bytes can be enough if server-side acceptance rules permit another processing of that flight.

Anti-replay state reduces risk but does not change request semantics

TLS 1.3 describes server mechanisms that can limit replay acceptance. A deployment can record recently observed ClientHello values, constrain a ticket to an authoritative zone, or otherwise coordinate acceptance so the same early-data handshake is not accepted repeatedly.

Those controls have operational boundaries. A distributed service may have several regions, processes, or storage partitions. Anti-replay state can be unavailable, delayed, partitioned, or intentionally scoped for cost and availability reasons. RFC 9846 consequently requires clients to restrict early data to messages they consider safe to replay and safe to retry on another connection.

Application semantics remain the final boundary. If replaying a request twice creates two purchases, sends two irreversible commands, or consumes two units of a scarce resource, transport-level replay reduction is not a sufficient basis for accepting that request in 0-RTT.

This also means that “encrypted” and “fresh” are separate properties. AEAD authentication protects early data from undetected modification under the traffic key. It does not assign globally unique execution semantics to the application operation carried inside that data.

HTTP exposes early-data status to the application layer

RFC 8470 defines how HTTP uses TLS early data. It adds the Early-Data request header field so information about early-data use can survive an intermediary hop, and it defines status code 425 Too Early for a server that is unwilling to process a request carrying replay risk.

The important boundary is that TLS acceptance and HTTP acceptance are different decisions. At the TLS layer, a server accepts or rejects early data as a unit; it cannot selectively accept only one request from that early-data flight. Once HTTP sees a request, the application or intermediary can still refuse to perform the operation.

A typical decision path looks like this:

request arrives
      |
      v
was it received as early data?
      |
   +--+--+
   |     |
  no    yes
   |     |
normal   is this operation replay-safe?
path     |
      +--+--+
      |     |
     yes    no
      |     |
 process   425 Too Early
             |
             v
       retry after handshake

RFC 8470 specifies that a retry after 425 Too Early must not itself be sent in early data. This lets a server defer a risky operation until the connection has the security properties of ordinary post-handshake traffic.

HTTP method names are useful signals, not complete policy

It is tempting to map 0-RTT eligibility directly to HTTP methods: allow GET and HEAD, reject POST, and stop there. That rule is a useful conservative starting point, but method names do not fully describe application effects.

A nominally safe request can still trigger application behavior such as consuming a single-use link, recording a state transition, starting an expensive computation, or interacting with a backend whose behavior is not replay-safe. Conversely, an API can design a state-changing operation around an application-level idempotency key, but that mechanism needs its own precise scope, persistence, collision behavior, and retry contract.

The policy therefore belongs close to the operation semantics. Routing metadata can provide a default, while the handler or service definition records whether replay is acceptable for that specific operation.

Idempotency limits duplicate effects only when its scope is durable

An idempotency key can make some retried operations safer, but merely accepting an Idempotency-Key-style field does not create replay safety. The server has to bind the key to the intended principal and operation, persist the result or execution state for an adequate interval, and make concurrent duplicates converge on one outcome.

For example:

(principal, operation, idempotency_key)
                  |
                  v
          atomic lookup/create
             /          \
        first call     duplicate
            |              |
         execute       return stored
         operation        outcome
            |
        store outcome

If two regions maintain independent idempotency stores, the same key can still execute once in each region. If the record expires before a replay can arrive, the protection no longer covers that replay. If the key is not bound to request parameters, accidental reuse can return an outcome for a different operation.

These are application invariants, not TLS properties. 0-RTT can use them when they already provide the required duplicate-execution control, but it does not strengthen them.

Authentication inside early data also inherits replay exposure

An application request might carry a cookie, bearer token, signed request, or another credential inside early data. Encryption prevents a passive observer from reading those fields, but replay can reproduce the authenticated request as a whole.

A request signature is not automatically replay-resistant either. Replay resistance depends on the signed material and verifier policy: a nonce, timestamp, unique request identifier, server challenge, or durable replay cache may be needed depending on the protocol. A valid signature establishes integrity and possession of signing authority under its scheme; it does not by itself mean that the same signed message has not already been accepted.

The same separation applies to authorization. A request can be fully authorized for a principal and still be unsafe to execute twice.

Proxies need to preserve the early-data signal

A TLS terminator may accept 0-RTT before forwarding an HTTP request to an origin over a different connection. The origin can no longer infer early-data status from its own TLS session in that architecture.

RFC 8470 uses the Early-Data: 1 header field to carry that signal. Trust around the header matters. Infrastructure that terminates TLS should set or propagate it according to the protocol, while deployments need to prevent an untrusted client from erasing or forging trusted forwarding metadata as requests cross proxy boundaries.

The origin’s decision then remains explicit: operations classified as replay-sensitive can reject the request with 425 Too Early, while eligible operations can proceed under the service’s replay policy.

0-RTT should be an operation-level capability

Enabling early data globally because a TLS library supports it puts the decision at the wrong abstraction layer. The TLS stack knows whether bytes arrived before handshake completion; it does not know whether those bytes mean “read a cached document” or “transfer value.”

A safer design makes 0-RTT eligibility an explicit property of the application operation. The default can remain disabled. An operation becomes eligible only when its replay behavior is acceptable, including effects in downstream systems and retry behavior through intermediaries.

That approach keeps the performance optimization narrow. TLS 1.3 early data can remove handshake latency for selected traffic without pretending that encryption also supplies exactly-once execution. The useful security boundary is simple: any operation accepted in 0-RTT must remain correct if the request is replayed.

References