TLS 1.3 represents the culmination of over two decades of experience in deploying large-scale transport security. For the most part it simplifies and improves the security of TLS and can act as a drop-in replacement for TLS 1.2. However, one new feature in the protocol represents a significant security risk to some existing applications: TLS 0-RTT (also known as early data). This performance optimization can allow replay attacks in applications that don’t implement their own anti-replay defenses. In some cases, just upgrading your TLS dependencies can introduce application-level vulnerabilities.
Let’s look at an example of a vulnerable application, demonstrate why it’s vulnerable, and discuss what can be done at the various application layers to resolve this issue.
A Vulnerable Application
Your company runs a platform with a buy-and-sell API. For a variety of legacy reasons the company implemented these operations with an API GET
At some later date your operations team upgrades your TLS infrastructure to support TLS 1.3. This could take the form of enabling it on the CDN, upgrading the TLS hardware offload box, or updating the software on your load balancers. They view this update just like any other standard patching process. After all, this is a transparent upgrade just like TLS 1.0 to 1.1, or 1.1 to 1.2…
…except if 0-RTT is enabled. If it is, then the API just described is now vulnerable to arbitrary replay. The design tradeoffs implicit in 0-RTT make possible the following attack:
- A user previously logged into your system walks into a coffee shop, gets on the WiFi, and initiates a buy or sell. Thanks to TLS 1.3 0-RTT this transaction occurs without a round trip for an initial handshake, saving 300ms! And, of course, all communication is still encrypted via TLS.
- An adversary capturing traffic off the WiFi captures that request and sends the same request again to the server. Unlike TLS 1.2, this request is not rejected by the TLS layer, and the buy/sell occurs again.
This is a surprising result! The API in question is now vulnerable to a new attack on the transport layer without any code changes to the actual application. Why? The answer lies in 0-RTT’s design.
What is 0-RTT?
TLS 0-RTT (an abbreviation for “zero round trip time,” officially known as “TLS early data”) is a method of lowering the time to first byte on a TLS connection. TLS 1.3 only requires 1-RTT (a single round trip) of the protocol, where TLS 1.2 and below required two, but the designers wanted more! For connections to a server where the client and server possess a pre-shared key (PSK) the client may choose to encrypt early data under this key and send it along with the ClientHello. This allows the server to respond immediately with the requested data after its own ServerHello/EncryptedExtensions/Finished messages. This cuts an entire round trip off the communication: zero round trip time. In a mobile environment this may save a significant amount of time (hundreds or even thousands of milliseconds). The PSK may be obtained out-of-band, but typically it is retained from an earlier handshake. Early data communication is generally limited to communicating with servers that the client has previously spoken to.
How Can It Break You?
Of course, shaving off a round trip comes with certain tradeoffs. In a typical TLS 1.3 connection model, every session has a property known as forward secrecy. Forward secrecy guarantees that past sessions are secure even if the private key for the current session is somehow disclosed. TLS 1.3 (and 1.2 with ephemeral key exchange modes) provides this by generating a new set of keys for every handshake. Unfortunately, since the PSK can’t be refreshed without a round trip, an initial request sent via 0-RTT is not forward secure. It is encrypted under the previous session’s key.
A much more significant concern, however, is that a 0-RTT request cannot prevent a replay attack. To counter this, the application layer needs to be provided information from its TLS implementation about whether the received request is 0-RTT or not. With that information the application can deny 0-RTT either by refusing to allow 0-RTT requests on non-idempotent operations or via direct anti-replay defenses such as nonces, which can be checked against shared global state to confirm that a given request has never been seen before. RFC 8470 attempts to document mitigations like this as they apply to 0-RTT.
Unfortunately, implementing a robust defense for this is easier said than done. Until now web applications have generally not needed to be aware of the vagaries of their transport security. As of this writing there has been only limited effort in trying to tackle this. Hypothetically, applications should already be capable of handling replay. Reality is never so convenient. Go’s new TLS 1.3 support does not include 0-RTT partially due to concerns around how to expose it safely. Cloudflare has chosen to disallow 0-RTT for all but GET requests with no query string specifically to try to mitigate this issue while also proxying additional information about any given request via added headers. Unfortunately, our example application from earlier is still vulnerable since it uses GET requests!
What Can You Do?
Most importantly, upgrade to TLS 1.3! It is a better and more secure protocol than its predecessors. However, as part of the upgrade, disable 0-RTT until you can audit your application for this class of vulnerability. If you’re using a CDN with TLS termination, read the documentation to determine what information they forward for you to interpret at the application layer. Otherwise, if you don’t have access to the specific connection details you’ll need to ensure you have very robust anti-replay defenses in place for sensitive operations.
If you’re a web framework developer you should think seriously about what APIs you can provide to your consumers to help them manage this risk while providing the performance benefits. This will likely require engaging with the various servers your framework runs on to come up with a common API to proxy the information you need. For example, if the web framework used in the vulnerable application above had an annotation for idempotency then routes annotated in that fashion could be automatically enabled for 0-RTT while all the others would reject 0-RTT requests (thus falling back automatically to a standard handshake).
If you are directly consuming a TLS API like OpenSSL’s in your application you’ll need to implement the various callbacks like
SSL_CTX_set_allow_early_data_cb and carefully consider the implications with regard to your session management vis-à-vis replay protection. 0-RTT support is not enabled unless you consume these new APIs so you can opt-in to them over time.
Cryptographers have been looking at how to obtain usable forward secrecy in the context of a 0-RTT request as well. Some recently published research (Session Resumption Protocols and Efficient Forward Security for TLS 1.3 0-RTT) proposes the use of puncturable pseudorandom functions to significantly reduce the size of a session database, but with trade-offs in computational complexity and post-compromise security. As of publication this is an area of active research with no solution truly suitable for deployment.
If you want to take advantage of TLS 1.3’s performance while ensuring your application and users are secure in a 0-RTT world, contact the Trail of Bits engineering and cryptography teams. We would love to help you engineer your application securely.
Frequently Asked Questions
What do I do if I’m behind a CDN?
For CDN-based termination you’ll need to check their documentation to see what capabilities they provide. Cloudflare (which is one of the only CDN companies that provides public documentation for this) uses a header named
CF-0RTT-Unique and the application needs to track values received from that and reject duplicates on non-idempotent endpoints.
What if I terminate with HAProxy?
By default enabling TLS 1.3 will not enable 0-RTT support.
You can enable 0-RTT by adding
allow-0rtt to the
server lines in the configuration. Once enabled a 0-RTT request will be proxied to the application layer with the header
Early-Data: 1 and a request may be rejected by returning a 425 status code. This method of proxying information is codified in RFC 8470.
What if I terminate with nginx?
By default enabling TLS 1.3 will not enable 0-RTT support.
You can enable 0-RTT with
ssl_early_data on; in the configuration. You’ll also need to add
proxy_set_header Early-Data $ssl_early_data; to your proxy directives to ensure that the
Early-Data header is passed to your application.
What if I terminate with Apache httpd?
httpd 2.4.37 and above supports TLS 1.3, but has no 0-RTT support at present (March 2019).
In-application termination: Go, Python, Ruby, C
Go supports TLS 1.3 as of 1.12, but has no support for 0-RTT.
Python has no early data support at present (March 2019).
Ruby has no early data support at present (March 2019).
C applications can utilize whatever TLS stack they desire. Each individual library is different, but the most common, OpenSSL, only enables 0-RTT when calling
SSL_set_max_early_data) with a value greater than zero. Developers can also use
SSL_CTX_set_allow_early_data_cb to set a callback function that determines whether a given 0-RTT request should be accepted.