Is your TLS resuming?
There are two main ways that a TLS handshake can go: Full handshake, or resume.
There are two benefits to resumption:
- it can save a round trip between the client and server.
- it saves CPU cost of a public key operation.
Round trip
Saving a round trip is important for latency. Some websites don’t use a CDN, so a roundtrip could take a while. And even those on a CDN can be tens of milliseconds away. Maybe won’t matter much for a human, but roundtrips can kill the performance of something that needs to do sequential connections.
E.g. Australia is far away:
$ ping -c 1 -n www.treasury.gov.au
PING treasury.gov.au (3.104.80.4) 56(84) bytes of data.
64 bytes from 3.104.80.4: icmp_seq=1 ttl=39 time=369 ms
That’s about a third of a second. Certainly noticeable to a human. Especially since rendering a web page usually requires many connections to different hosts.
For TCP based web requests (in other words: not QUIC), there’s usually four roundtrips involved (slightly simplified):
- TCP connection establishment.
- ClientHello & ServerHello.
- Client & Server ChangeCipherSpec.
- HTTP request & response.
So from the UK to Australia, that’s about one full seconds, just setting up the connection. And then another third of a second for the request.
So that could be better.
CPU cost of public key operation
This can be a problem even if latency is low. Public key operations can take a lot of CPU. Especially because as key sizes grow, the cost grows superlinerarly.
$ openssl speed rsa2048 rsa4096
[…]
sign verify sign/s verify/s
rsa 2048 bits 0.000520s 0.000015s 1921.7 65540.4
rsa 4096 bits 0.003473s 0.000054s 287.9 18544.8
Doubling key size makes signatures (server) almost 7x as expensive, and verifications (client) 3.5x as expensive. (on my laptop. YMMV)
At scale, if you’re doing millions of QPS, this can add up. This is only 287 handshakes per second, per CPU. Not Web Scale™.
TLS before 1.3, and resumptions
The ClientHello sent by the client will contain a session ticket or session ID (the difference doesn’t matter here), basically saying “Hey, we’ve talked before. How about we skip some negotiation, and get right back to where we were?”. If the Server agrees, then that skips roundtrip 3.
This takes www.treasury.gov.au down to ~1s for any followup (resumed) connections, but it still takes 1.2s for the first request.
TLS 1.3 and round trips
TLS 1.3 shaves off a roundtrip to the initial request by optimistically making assumptions about what the server supports. That way it can say “Hello”, introducing which ciphers it supports, and select one, assuming that the server does, too.
In the normal case, this merges roundtrips 2&3, and now it’s only three roundtrips.
In our Australia example, this get the initial request down to ~1s as well, (except www.treasury.gov.au doesn’t support TLS 1.3).
TLS 1.3 and resumptions
Alright, so with resumptions, TLS brings us down to just two roundtrips, right? ~600ms total request time to Australia!
Not so fast. (heh)
The handshake is now:
- TCP connecting.
- Hello & optimistically choose cipher parameters.
- HTTP GET.
Which one would resuming remove? They’re all needed, and you can’t anything before you get a reply from the previous… or can you?
In the general case, no you can’t. But if you’re willing to sacrifice
a specific bit of security, there’s something we can do. Specifically
if the server is fine with being vulnerable to a session being
replayed, then we send the GET
without waiting for the handshake.
From the server’s point of view, it now sees “Hello, you and I are resuming, so please use the previous key, and here’s the encrypted payload”. If all looks good then the server can process the request, and return the reply. TLS calls this 0RTT, in that after the the TCP connection has been established, there’s only… uh… one RTT left. So 0RTT as in no additional roundtrip?
Sending the data early, like this, is called “early data”, and is new with TLS 1.3. It can’t be used on initial requests, since no session key has been negotiated yet.
TLS 1.3 early data and resumptions
Unlike previous versions, TLS 1.3 only saves a roundtrip if it has
early data. An HTTP POST
cannot be used for early data (because of
the replay problem), so is the same number of roundtrips whether
resumed or not.
So who supports the perfect setup?
For my Australia example I’m actually struggling a bit to find an Australian service that can fully optimize for latency.
- www.treasury.gov.au doesn’t support TLS 1.3
- queenslandtech.com.au doesn’t support early data
- All others I’m randomly guessing use a CDN.
- I’m too lazy to set up a VM there just to make the numbers bigger.
Oh well, we’re going to have to pretend, using a more nearby server. That means we’ll deal with tens of milliseconds instead of hundreds, though.
Measuring TLS handshakes times
I made a tool: tlshake.
TLS 1.3 with a request in early data
With a GET
request in early data, TLS 1.3 resumes, and saves us a
roundtrip. Note that the handshake time is approximately the same on
both requests, but total time is very much shorter on the resumed
request, since TLS and HTTP used the same roundtrip.
$ tlshake --http-get / www.example.com
Connection: initial
Target: www.example.com
Endpoint: www.example.com:443
Connect time: 67.414ms
Handshake time: 42.208ms
Handshake kind: Full
Protocol version: TLSv1_3
Cipher suite: TLS13_AES_256_GCM_SHA384
ALPN protocol: None
Early data: Not attempted
Request time: 95.782ms
Reply first line: HTTP/1.1 200 OK
Total time: 205.450ms
Connection: resume
Target: www.example.com
Endpoint: www.example.com:443
Connect time: 57.422ms
Handshake time: 39.769ms
Handshake kind: Resumed
Protocol version: TLSv1_3
Cipher suite: TLS13_AES_256_GCM_SHA384
ALPN protocol: None
Early data: accepted
Request time: 62.583ms
Reply first line: HTTP/1.1 200 OK
Total time: 159.804ms
Resumption without early data
We can resume the session without using early data. That saves the CPU usage for the handshake, but won’t save any roundtrip. Notice the very similar total time in this case:
$ tlshake --http-get / --disable-early-data www.example.com
Connection: initial
Target: www.example.com
Endpoint: www.example.com:443
Connect time: 68.945ms
Handshake time: 41.140ms
Handshake kind: Full
Protocol version: TLSv1_3
Cipher suite: TLS13_AES_256_GCM_SHA384
ALPN protocol: None
Early data: Not attempted
Request time: 89.317ms
Reply first line: HTTP/1.1 200 OK
Total time: 199.437ms
Connection: resume
Target: www.example.com
Endpoint: www.example.com:443
Connect time: 66.183ms
Handshake time: 39.028ms
Handshake kind: Resumed
Protocol version: TLSv1_3
Cipher suite: TLS13_AES_256_GCM_SHA384
ALPN protocol: None
Early data: Not attempted
Request time: 90.413ms
Reply first line: HTTP/1.1 200 OK
Total time: 195.653ms
Only the TLS handshake
If we only handshake, without sending a request, then it won’t try to
resume at all (note that both requests are of the Full
kind).
$ tlshake www.example.com
Connection: initial
Target: www.example.com
Endpoint: www.example.com:443
Connect time: 66.218ms
Handshake time: 43.683ms
Handshake kind: Full
Protocol version: TLSv1_3
Cipher suite: TLS13_AES_256_GCM_SHA384
ALPN protocol: None
Early data: Not attempted
Total time: 109.930ms
Connection: resume
Target: www.example.com
Endpoint: www.example.com:443
Connect time: 66.416ms
Handshake time: 41.626ms
Handshake kind: Full
Protocol version: TLSv1_3
Cipher suite: TLS13_AES_256_GCM_SHA384
ALPN protocol: None
Early data: Not attempted
Total time: 108.068ms
This seems to be because the server doesn’t provide the session ticket until it receives a request. It’s pretty clever. If a TLS session has been established, but no request was received, then the client should probably not try to reuse it. There’s a good chance that something went wrong, so we should redo the full handshake next time.
For comparison: TLS 1.2-only server
With TLS 1.2 a resumed handshake has one less roundtrip.
$ tlshake --http-get / www.treasury.gov.au
Connection: initial
Target: www.treasury.gov.au
Endpoint: www.treasury.gov.au:443
Connect time: 330.905ms
Handshake time: 617.084ms
Handshake kind: Full
Protocol version: TLSv1_2
Cipher suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
ALPN protocol: None
Early data: Not attempted
Request time: 279.625ms
Reply first line: HTTP/1.1 301 Moved Permanently
Total time: 1227.643ms
Connection: resume
Target: www.treasury.gov.au
Endpoint: www.treasury.gov.au:443
Connect time: 332.880ms
Handshake time: 306.455ms
Handshake kind: Resumed
Protocol version: TLSv1_2
Cipher suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
ALPN protocol: None
Early data: Not attempted
Request time: 308.770ms
Reply first line: HTTP/1.1 301 Moved Permanently
Total time: 948.132ms
Play around with TLS handshakes for your own service
Check out tlshake, and see if your TLS resumptions work as you expect, and with the expected number of roundtrips.
If you’re on Cloudflare, you need to enable “0-RTT Connection Resumption” for it to accept early data. It’s under “Speed -> Optimization” per domain.