How SSL/TLS actually works
I thought I understood the TLS handshake. Then I dug a little deeper and found out the picture in my head was from an older version of TLS.
A problem
I set up HTTPS all the time. Get a certificate, point nginx or Traefik at it, done. But one day, while debugging a certificate error, I realised I couldn’t actually explain what happens between “the browser connects” and “the padlock shows up”.
So I wrote it down, the way I understood it at the time. And then, a while later, I found out part of it was wrong. This post is both: what I first believed, and what’s actually true.
Two kinds of encryption
To understand TLS, you need two building blocks.
Symmetric encryption uses one key for both locking and unlocking. If we both have the same key, I encrypt, you decrypt. It’s very fast, which makes it perfect for encrypting lots of data. The catch: how do we both get the same key, without someone in the middle seeing it?
Asymmetric encryption (public-key cryptography) uses a key pair. What the public key encrypts, only the private key can decrypt. The server can hand its public key to anyone, because only it holds the private key. It’s much slower than symmetric encryption, but it solves the “how do we share a secret” problem.
TLS uses both: asymmetric crypto to agree on a key safely, then symmetric crypto to encrypt the actual data.
The version I first learned
My original notes looked like this:
1. Browser → Server: "hello"
2. Server → Browser: "hello" + certificate (with the server's public key)
3. Browser checks the certificate is signed by a trusted CA
4. Browser makes a random session key,
encrypts it with the server's public key, and sends it
5. Server decrypts it with its private key
6. Both now have the same session key → switch to symmetric encryption
And that’s roughly right… for the old RSA key exchange, used in TLS 1.2 and earlier. It’s how most tutorials explain it, and it’s a good way to build intuition.
But it has a serious weakness, and it’s not how modern TLS works.
The problem with that version
In the RSA key exchange, the session key travels across the network, encrypted with the server’s long-term public key.
Now imagine someone records all of your encrypted traffic for a year. They can’t read it. But then, one day, the server’s private key leaks. A breach, a misconfigured backup, anything. With that one key, they can go back, decrypt every recorded session key, and read every conversation from the past year.
The property that prevents this is called forward secrecy, and the RSA key exchange doesn’t have it.
What actually happens in TLS 1.3
Modern TLS doesn’t send the session key at all. Instead, both sides compute it together, using (Elliptic Curve) Diffie-Hellman, ECDHE for short.
The idea, simplified:
- The browser makes a temporary key pair, just for this connection, and sends its public half.
- The server makes its own temporary key pair and sends its public half back.
- Each side combines its own private half with the other side’s public half. Thanks to some clever math, both arrive at the same shared secret, and nobody watching the network can compute it from the public halves alone.
- Both sides derive the session keys from that shared secret, and throw the temporary keys away afterwards.
Browser Server
─────── ──────
ClientHello
+ my temporary public key ─────────────────►
ServerHello
◄───────────────── + its temporary public key
+ certificate
+ signature (proves it owns the cert)
+ Finished
both compute the same shared secret → session keys
Finished ─────────────────►
═══════════════ encrypted application data (symmetric) ═══════════════
Because the temporary keys are thrown away, a leaked server key later on can’t decrypt past traffic. That’s forward secrecy.
It’s also faster: TLS 1.3 finishes the handshake in one round trip, where TLS 1.2 needed two.
So what is the certificate for?
This was my real “aha” moment. In my original notes, the certificate’s public key was used to encrypt the session key. In TLS 1.3, it isn’t used for encryption at all.
The certificate is there to prove identity. The server signs the handshake with its private key, and the browser checks that signature with the public key in the certificate. Then it checks the certificate itself:
- Is it signed by a CA I trust (or by an intermediate that chains up to one)?
- Is it for this hostname?
- Is it still valid, and not revoked?
Without that, the Diffie-Hellman exchange would happily agree on a secret key… with an attacker sitting in the middle. Diffie-Hellman gives you a private conversation. The certificate makes sure it’s with the right person.
Seeing it for yourself
openssl will show you a real handshake:
openssl s_client -connect dighard.dev:443 -servername dighard.dev
Look for lines like:
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
Server certificate
subject=CN=dighard.dev
issuer=C=US, O=Let's Encrypt, CN=YE2
Verify return code: 0 (ok)
TLSv1.3 is the protocol, TLS_AES_128_GCM_SHA256 is the symmetric cipher used for the data, and the subject/issuer show the certificate chain the browser verified.
What I took away
- The core idea I started with still holds: asymmetric crypto to set things up, symmetric crypto for the data.
- But the details matter. Modern TLS computes the session key instead of sending it, which gives you forward secrecy.
- The certificate is about who you’re talking to, not about encrypting the key.
The funny part is that I’d been configuring TLS for years with that older picture in my head, and everything worked fine. It was only by digging a bit deeper than “it works” that I found out how much the picture had changed.
Discussion
Enjoy!