Loading video…
Sign in to react
CommunicationQuick

TLS: a secret in public

In the last lesson, TCP got every byte of Maya's "buy 14C" across the wire, in order, in 80 ms there and back. This lesson is about what those bytes say, and who else can see them.

Maya is still in the café in London, on the café wifi. This is the request her phone is about to send to tixdrop:

HTTP
POST /orders HTTP/1.1
Host: tixdrop.example
Cookie: session=...
Content-Type: application/x-www-form-urlencoded
 
seat=14C&amount=40&card=...

Her session cookie, the seat, the price ($40), and her card number. (Most shops hand the card number straight to a payment provider's page; here it rides in tixdrop's own request to keep one arrow to follow.)

Now look at who else is on this wifi. A stranger two tables over is on the same network, and Maya's bytes pass right by his laptop. They are exactly as readable to him as they are to you.

Read, Change, Pretend

On an open network, the stranger can do three things.

Maya's phone
café wifi, London
load balancer
tixdrop.example, Virginia
Read
a copy lands on the stranger's laptop
seat=14C&amount=40
Change
the amount that arrives isn't the one sent
amount=40
amount=90
Pretend
a checkout page that isn't tixdrop's
fake tixdrop.example
  • Read. A copy of Maya's request lands on his laptop, cookie and card number included. With the cookie alone he can act as Maya on tixdrop without ever knowing her password.
  • Change. He can edit bytes as they pass, so the seat or the amount that arrives isn't the one Maya sent.
  • Pretend. He can answer in tixdrop's place with a checkout page that looks exactly like the real one. Maya's phone has no way to tell.

The protections against those three are called confidentiality, integrity, and authentication. On the web, the layer that provides all three is TLS (Transport Layer Security), the S in HTTPS. The rest of this lesson builds it one piece at a time.

A Key in the App?

The obvious fix: put a secret key inside the tixdrop app and encrypt everything with it.

It fails twice. Every copy of the app carries the same key, including the copy on the stranger's phone; he digs it out once and can decrypt everyone's traffic. And Maya might not use the app at all: a browser opening tixdrop for the first time has no key built in.

So the real problem is harder. Maya's phone needs a fresh key that only it and tixdrop know, agreed with a server it has never met, over a wire where the stranger hears every word. That sounds impossible, because anything you say out loud, he hears too.

Agreeing on a Key in Public

Here is how you do it anyway, with numbers small enough to check on a napkin.

Both sides agree out loud on two public numbers, a base of 5 and a modulus of 23. The stranger is welcome to hear them. Then each side picks a secret it never sends: the phone picks 7, tixdrop picks 3.

Each side raises 5 to its secret, divides by 23, and sends only the remainder. Then each side does the same thing again to the number it received, with its own secret:

Maya's phonetixdropthe stranger sees
public5, 235, 235, 23
secret73nothing
sends5^7 mod 23 = 175^3 mod 23 = 1017 and 10
computes10^7 mod 23 = 1417^3 mod 23 = 14?

Both ends arrive at 14, because (5^3)^7 and (5^7)^3 are the same number. You can check it in any Python shell:

Python
>>> pow(5, 7, 23), pow(5, 3, 23)    # what crosses the wifi
(17, 10)
>>> pow(10, 7, 23), pow(17, 3, 23)  # what each side computes
(14, 14)

To reach 14, the stranger would have to pull a secret back out of a remainder: find the x with 5^x mod 23 = 17. With numbers this small he can just try all 22. So real connections use enormous ones. TLS 1.3 usually plays the same game on an elliptic curve called X25519, with 32-byte keys, numbers about 77 digits long, where no ordinary computer can undo it in any useful amount of time.

This is Diffie-Hellman key exchange. Nothing secret ever crosses the wire, and both ends still end up holding the same secret.

The Man in the Middle

The stranger doesn't have to break the math. He can sit in the middle of it.

When the phone sends 17, he catches it and sends tixdrop a number of his own instead: with his secret 8, that is 5^8 mod 23 = 16. When tixdrop answers with 10, he swaps that for 16 too. Now there are two complete exchanges, and he is on the other end of both:

computesshared with
Maya's phone16^7 mod 23 = 18the stranger (17^8 mod 23 = 18)
tixdrop16^3 mod 23 = 2the stranger (10^8 mod 23 = 2)

Maya's phone just finished a perfect key exchange, and the math checks out. So who is it sharing that key with?

The stranger. He decrypts whatever Maya sends with 18, reads it, encrypts it again with 2, and passes it along. Neither side sees anything wrong. This is the man-in-the-middle attack, and it is the whole reason the rest of TLS exists: key agreement gave Maya a secret, but it never told her who was on the other end.

Certificates: Who Signed This?

The phone needs proof that the number it received really came from tixdrop.

tixdrop starts by showing a certificate: a small signed document that says "this public key belongs to tixdrop.example, valid from this date until that date". It is signed by an intermediate, whose own certificate is signed by a root. The root was already on Maya's phone, shipped with the operating system in a list it trusts, the trust store.

Root certificate
already in Maya's phone, in the trust store it shipped with
signs
Intermediate certificate
sent by tixdrop; signed by the root
signs
tixdrop.example
public key, name, validity dates; signed by the intermediate
plus, during this handshake
Signature over the handshake
made with tixdrop's private key, which never leaves its servers

The phone walks up the chain and checks:

  • every signature in the chain, up to a root that was already in its trust store. A root isn't trusted just because a server sent it;
  • that the name in the certificate matches the name it asked for, tixdrop.example;
  • that today is inside the validity dates, and the certificate is allowed to be used for a TLS server.

The organizations that sign these are certificate authorities (CAs). Real chains can have more than one intermediate, and CAs can make mistakes, which is why publicly trusted certificates are also published in Certificate Transparency logs where anyone, including tixdrop, can watch for certificates issued in its name.

A valid chain is not enough on its own, because a certificate is public. The stranger can fetch tixdrop's certificate as easily as Maya's phone can and hand her a perfect copy. So tixdrop does one more thing. With the private key that matches the certificate, it signs the whole handshake so far, including both key-exchange numbers. The stranger swapped those numbers, so the handshake tixdrop signed isn't the one the phone saw. Without the private key he can't sign a new one. The signature fails, the phone hangs up, and the copied certificate proves nothing.

That is the core of TLS: Diffie-Hellman makes a fresh secret, and the signature ties that secret to the name in the certificate.

Integrity: $40 Becomes $90

With a key agreed and tixdrop proven, Maya's request crosses the wifi as ciphertext. The stranger can't read a word of it. Is it safe from being changed?

Not by encryption alone, and this part surprises people. The ciphers TLS uses (AES in counter mode, which is what GCM uses, and ChaCha20) are stream ciphers: they turn the key into a long stream of pseudorandom bits and XOR it with the message. So flipping a bit of the ciphertext flips exactly that bit of the decrypted message. The result isn't garbage; it is a precise edit.

The stranger doesn't need to read anything. A checkout request has the same shape every time, so he knows where the amount sits and what it probably says. The digit 4 is byte 0x34 and 9 is 0x39:

Python
>>> hex(ord("4")), hex(ord("9")), bin(ord("4") ^ ord("9"))
('0x34', '0x39', '0b1101')

XOR the ciphertext byte at that position with 0x0D, three flipped bits, and amount=40 decrypts cleanly as amount=90. Without a check, tixdrop charges Maya $90.

The fix is a tag: 16 bytes computed from the key and every byte of the record, sent along with it. Without the key the stranger can't compute a matching tag, so a single flipped bit fails the check. tixdrop drops the record, sends an alert, and closes the connection. Encryption that carries its own tag is authenticated encryption with associated data (AEAD); the "associated data" is the record header, which the tag covers without encrypting. TLS 1.3 allows nothing else: AES-GCM or ChaCha20-Poly1305 (and AES-CCM). Each record's nonce also comes from its sequence number, so a record that is replayed, dropped or reordered inside the connection fails too.

The TLS 1.3 Handshake

Every piece is built, so here is the real handshake on Maya's arrow, for a new connection.

Maya's phone
café wifi, London
load balancer
tixdrop.example, Virginia
SYN
TCP handshake, from the last lesson
→
SYN-ACK
←
TCP 80 ms
ClientHello
versions, ciphers, and her key share (the real 17)
→
ServerHello
its key share; then, encrypted: certificate, signature, Finished
←
TLS 160 ms
Finished + buy 14C
the request rides right behind
→
response
first response
←
first response 240 ms
  1. ClientHello. The phone offers TLS versions, ciphers and key-exchange groups, and already includes its key share, guessing which group the server will pick.
  2. ServerHello. tixdrop picks the parameters and sends its own key share. From here both sides can derive handshake keys, so everything after it is encrypted: the certificate, the signature over the handshake (CertificateVerify), and a Finished message, a MAC over the whole handshake that seals it.
  3. Client Finished. The phone checks the chain, the name, the signature and the Finished, sends its own Finished, and "buy 14C" follows in the same flight.

TLS costs one round trip on top of TCP's. Maya's first response arrives after three:

connectionround trips before the responseat 80 ms
new, TLS 1.3TCP + TLS + request = 3240 ms
new, TLS 1.2TCP + 2 for TLS + request = 4320 ms
resumed, TLS 1.3 with 0-RTT early dataTCP + first flight carrying the request = 2160 ms
already openrequest = 180 ms

TLS 1.2 needed a second round trip because the key exchange came after the certificate. (TLS False Start let some 1.2 clients skip it.) TLS 1.3 also adds a round trip if the phone guesses the wrong key-exchange group and the server has to ask again (HelloRetryRequest). From London, you pay that toll on every new connection, which is why reusing connections matters even more once TLS is on top of TCP.

Resumption and 0-RTT Early Data

When Maya comes back, her phone can go faster. At the end of the last connection, tixdrop handed the phone a session ticket, so the next ClientHello can resume that session with a pre-shared key instead of doing the certificate work again.

It can even go one step further: 0-RTT early data. The request rides inside the first flight, encrypted with a key derived from the ticket, and the first response lands after 160 ms instead of 240. So why not send the purchase in the very first packet?

Because the stranger can't read that first flight, but he can copy it. He sends it to tixdrop again, and the copy decrypts just as cleanly. Now tixdrop sees "buy 14C, charge $40" twice. The TLS 1.3 specification says it plainly:

There are no guarantees of non-replay between connections.

Servers can narrow the window (single-use tickets, remembering recent ClientHellos), but across many servers and data centers they can't close it. So the rule is about the application, not the protocol:

  • Only requests that are safe to repeat may ride in early data: loading the seat map, fetching the poster.
  • A purchase must not. If one arrives as early data, the server answers 425 Too Early (RFC 8470), and the phone resends it after the handshake completes. A proxy that forwards early data marks the request with an Early-Data: 1 header so the app server can make that call.
  • "It's a GET" isn't proof. Check what the endpoint actually does.

Early data also lacks the forward secrecy described next, because its key comes from the ticket rather than a fresh exchange. Replays are one more way the same request reaches your server twice; API design gives the application a way to recognize them.

Forward Secrecy

Suppose the stranger records every encrypted byte of Maya's purchase and keeps it. A year from now tixdrop's private key leaks. Can he open the recording?

No. That private key only ever signed the handshake. The keys that encrypted Maya's bytes came from fresh Diffie-Hellman secrets made for that one connection and thrown away when it closed. There is nothing left to steal. That property is forward secrecy, and TLS 1.3 requires it for every full handshake by dropping the older key exchanges that lacked it.

A patient attacker has one more option: record today and wait for a large quantum computer, which could break X25519 itself. So since late 2024, Chrome and Firefox have offered a hybrid key exchange by default, X25519MLKEM768, which combines X25519 with ML-KEM, the post-quantum key agreement NIST standardized in 2024. A recording made today would need both broken. The hybrid key share is about 1 KB larger, which pushes the ClientHello past one packet; old middleboxes that choke on that are the usual cause of "only Chrome fails" handshake bugs.

TLS Inside a System

Zoom out to all of tixdrop, the shape it grew into when it scaled out, and follow Maya's lock inside.

It ends at the load balancer, which holds tixdrop's certificate and private key, decrypts her request, and hands it to an app server. That is TLS termination. (The CDN serving the poster terminates its own TLS at the edge, the same way.) It is a good trade: the key lives in one place, the load balancer can read HTTP to route requests, and app servers never touch a certificate.

But it leaves a question about every arrow behind it: are those bytes plaintext? There are three answers:

  • Plaintext inside. Many systems trust the internal network. Then one compromised machine inside can read and change whatever passes it.
  • Re-encrypt. The load balancer opens a new TLS connection to the app server. Each hop is protected, and each hop pays its own handshake (which pooling amortizes).
  • Passthrough. The load balancer forwards the encrypted TCP stream untouched, and the app servers terminate TLS themselves. The load balancer can no longer see paths or headers, so it can only balance connections.

At tixdrop the inside arrows get locks too, and between orders and payments both sides show a certificate. That is mutual TLS (mTLS). Payments now knows the caller really is orders, because the certificate names a service rather than a host, and only orders holds its private key.

mTLS proves who is calling, not what they may ask for. Payments still checks every charge: that the order exists, that the amount matches, that orders is allowed to request it. Service meshes automate the certificates (issuing short-lived ones to every workload and rotating them) and do the TLS in a sidecar proxy, but the authorization policy is still yours to write.

Operating Certificates

Every one of those locks rests on a certificate, and every certificate has an end date.

On April 6, 2021, an internal wildcard certificate at Epic Games expired. From their incident report:

At 12:00PM UTC traffic effectively halted between back-end systems.

A renewed certificate went in 37 minutes later, at 12:37. The report is candid about why it happened: "Despite our best efforts to monitor our certificates for expiration, we didn't fully cover every area where certificates were being used." At tixdrop, the equivalent is the certificate between orders and payments. When it expires, every $40 charge stops at once, and nothing else is broken.

The end dates are also arriving sooner, by rule. The CA/Browser Forum's Baseline Requirements (ballot SC-081) cap the lifetime of publicly trusted TLS certificates:

certificates issuedmaximum lifetime
before March 15, 2026398 days
from March 15, 2026200 days
from March 15, 2027100 days
from March 15, 202947 days

At 47 days, a calendar reminder can't keep up. What to do:

  • Automate renewal with ACME, the protocol Let's Encrypt made standard. Caddy, cert-manager and most CDNs speak it, and managed load balancers renew their own.
  • Monitor what is served, not what is on disk. A renewed file that the process never reloaded still expires in production. Check the live endpoint:
Bash
echo | openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null \
  | openssl x509 -noout -subject -issuer -enddate
  • Cover internal certificates too. The public rules don't apply to your private CA, so no browser vendor will force you to notice them. Epic's outage was an internal certificate.
  • Alert early, when a third of the lifetime is left, so a failed renewal is a ticket and not an outage.

What TLS Doesn't Protect

TLS is precise about what it covers. The stranger still knows:

  • Which site Maya is visiting. The name tixdrop.example goes out in plain text in her ClientHello (Server Name Indication), unless both sides support Encrypted Client Hello (ECH). Her DNS lookup and the server's IP address give it away too.
  • Sizes and timing. A 500 KB download looks a lot like the poster loading.

And TLS doesn't:

  • make tixdrop honest or bug-free. It proves Maya reached the real tixdrop, not that tixdrop does the right thing;
  • decide authorization. It authenticates the peer; your application decides what that peer may do;
  • stop duplicates. If Maya's phone retries a purchase, TLS delivers both copies perfectly.

What TLS 1.3 Removed

TLS 1.3 got simpler by deleting the parts of 1.2 that kept causing attacks:

  • Static RSA key exchange and static Diffie-Hellman: no forward secrecy.
  • CBC-mode and RC4 ciphers, and separate MAC-then-encrypt constructions: padding-oracle attacks (Lucky Thirteen, POODLE) and biased keystreams. Only AEAD remains.
  • Compression: leaked secrets through ciphertext length (CRIME).
  • Renegotiation: complex and attack-prone.
  • Custom Diffie-Hellman groups, MD5 and SHA-1 in signatures: weak parameters.

What's Next

The one sentence to keep: TLS lets two machines that have never met agree on a fresh secret in public, and a certificate plus a signature over the handshake proves who is on the other end. Everything else here, the tags, the round trips, the replay rules, the expiring certificates, is what it takes to keep that promise on a real network.

Maya's lock is solid now. Inside tixdrop, orders is about to open its own mTLS connection to payments. But at 10:01, a minute into the drop, payments grows from three machines to six, and then one of them dies. So how does orders even find payments? That is service discovery, and it's next.

References