4 · Talking between machines Draft

TLS handshakes and records

6 min read

On this page

4.8 Encryption in the path: what TLS costs and where to run it

Most crypto venues accept connections only over TLS, so every byte of market data arrives encrypted and every order leaves encrypted. That puts a cipher in the hot path and a handshake of round trips in front of every connection, costs every browser and HTTPS server pays as well. This section covers what TLS does, what the handshake and each record cost, where the decrypt can run, and why connections are opened once and kept warm.

What TLS is

TLS, Transport Layer Security, is a layer between TCP and the application: it takes TCP’s reliable, ordered byte stream and gives the application the same stream, which nothing in between can read or alter undetected. It works in two phases. The first is the handshake. The two sides agree a cipher and fresh keys, and the server proves who it is with a certificate, a document naming the server and carrying its public key, signed by an authority the client already trusts. The keys come from an elliptic-curve Diffie–Hellman exchange, ECDHE: each side sends a public share and combines the other’s with a private value of its own, and both reach the same secret, which nothing on the wire reveals.

The second phase is records. The stream is cut into records of at most 16 KB of plaintext, each encrypted and authenticated on its own. A record starts with a 5-byte header in the clear: a content type, two bytes of legacy version and a two-byte big-endian length. Its body is sealed with an AEAD cipher, authenticated encryption with associated data: one operation that encrypts the body and computes a 16-byte tag over the body and the header, so the receiver can tell whether either was changed. AES-128-GCM is the usual choice: AES, a block cipher, is applied to successive values of a counter to make a keystream that is XORed with the plaintext, and GHASH, a multiplication in a binary field over every 16-byte block, makes the tag. In TLS 1.3 each record’s nonce, the value used once that makes its keystream unique, comes from a sequence number both sides count and neither sends, so records must be decrypted in order, and a record whose tag fails ends the connection. TLS 1.3 also hides each record’s real type in the last byte of the plaintext, so each unpadded TLS 1.3 application record with a 16-byte tag adds 5 + 1 + 16 = 22 bytes. Optional padding adds more.

TLS 1.2, from 2008, takes two round trips to handshake and sends an 8-byte explicit nonce in each GCM record, 29 bytes of overhead in all. TLS 1.3 normally handshakes in one round trip by sending a key share in the first flight; a HelloRetryRequest adds another. Its ordinary ECDHE handshake provides forward secrecy. PSK-only resumption and 0-RTT data have different secrecy properties; early data is also replayable. Records cost the same to decrypt in both; the difference a program feels is the handshake.

Read more: Ivan Ristić’s Bulletproof TLS and PKI is the practitioner’s account of the protocol and its deployment; RFC 8446, TLS 1.3 itself, is readable; and the kernel’s Documentation/networking/tls.rst describes kernel TLS and its counters.

The handshake

A fresh connection pays for TCP first, one round trip, before the client can send its ClientHello with its key share. The server answers in one flight: its share, then, encrypted under the new keys, its certificate, a signature over the handshake and its Finished message. The client checks it all and sends its own Finished, with application data in the same flight.

That is approximately 140 ms from connect to the first application byte leaving, one round trip for TCP and one for TLS, with any reply 70 ms later. TLS 1.2’s extra round trip makes it 210 ms. Those are network-delay estimates for the stated handshake, excluding CPU work, retries and server queueing.

Resumption shortens later connections. After a TLS 1.3 handshake the server sends session tickets, opaque blobs that stand for the session’s secret, and a client that presents one next time skips the certificate and its verification. With early data it also sends application data in its first flight, under a key derived from the ticket, so TLS adds no round trip before it: 0-RTT, though TCP’s round trip remains. The catch is replay: an attacker who records the first flight can send it again. Only protocol operations explicitly defined as replay-safe are suitable; even a duplicate subscription may have stateful effects, so servers accept early data, if at all, only for requests that are safe to repeat.

The handshake also costs a few hundred microseconds to a few milliseconds of CPU, for the key share and for checking the certificate chain: nothing against 140 ms of round trips, but hundreds to thousands of budgets on whichever thread runs it, so the hot thread never handshakes while handling traffic.

The records

Every arriving record must be decrypted and its tag checked before any byte of it can be used. AES-NI, the x86 instructions that each perform one round of AES, and PCLMULQDQ, the carry-less multiply that GHASH is built from, run AES-128-GCM at 1–2 GB/s on one core, 0.5–1 ns a byte; AArch64 cores have equivalents of both. A 200-byte record is therefore 100–200 ns of cipher. Each record also pays a fixed cost whatever its size, for parsing the header, deriving the nonce, setting up the cipher, finishing and comparing the tag and managing the library’s buffers: 100–300 ns more, so a 200-byte record costs about 300–500 ns in all, and at this size the fixed part is as large as the cipher. AES-256-GCM, which some TLS configurations prefer, runs 14 rounds instead of 10; the throughput difference depends on implementation and CPU. ChaCha20-Poly1305 uses vectorised arithmetic and can be slower than hardware-accelerated AES-GCM on some server cores; the ratio is backend- and CPU-dependent. Without AES acceleration ChaCha can be faster, which is why some clients offer it first, and why a program should check which cipher it got.

Record boundaries are the sender’s choice. A library usually makes one record per application write, up to 16 KB, so a venue that writes each update separately sends one small record per update, and a WebSocket frame can begin in one record and end in the next; framing runs on the decrypted stream exactly as on a plain TCP stream. One property of records has no counterpart in plain TCP: a record is unusable until all of it has arrived, because its tag covers all of it. A 16 KB record spans twelve segments, so the update at its start waits for the twelfth, and for a retransmission if any of the twelve is lost.