TLS on the Wire
ClientHello → Certificate → Finished — what bytes go across.
TLS on the Wire
Most engineers know "TLS encrypts the connection" and stop there. This lesson is about the actual bytes — what messages go across, in what order, and how to decode them yourself with openssl and Wireshark.
The handshake at 30,000 ft (TLS 1.3)
Client Server
────── ──────
ClientHello
client_random
cipher_suites
extensions
- server_name (SNI)
- supported_groups (curves)
- key_share (DH public)
- signature_algorithms
- alpn (h2, http/1.1)
───────────────────────────────────────────▶
ServerHello
server_random
cipher_suite
key_share
EncryptedExtensions
Certificate
leaf_cert
intermediate_cert
CertificateVerify
Finished
◀───────────────────────────────────────────
Finished
───────────────────────────────────────────▶
[application data — encrypted]
◀──────────────────────────────────────────▶
That's it. Two round-trips of records, one in each direction (with TLS 1.3 it's actually one — the server's records all flow back together). Everything after Finished is encrypted application traffic.
Decoding it yourself with openssl
The single most useful TLS-debug command:
openssl s_client -connect example.com:443 -servername example.com -msg -debug 2>&1 | head -120
What you'll see:
>>> TLS 1.3, RecordHeader [length 0512] ...— outgoing record>>> TLS 1.3, Handshake [length 050e], ClientHello— handshake message type<<< TLS 1.3, RecordHeader [length 007a] ...— incoming record<<< TLS 1.3, Handshake [length 0076], ServerHello<<< TLS 1.3, Handshake [length 0c4d], Certificate— the cert chain<<< TLS 1.3, Handshake [length 0114], CertificateVerify<<< TLS 1.3, Handshake [length 0034], Finished
-msg prints message types; -debug dumps the raw bytes alongside an ASCII view. Ugly but precise.
SNI — server name indication
The first thing the server learns about you is the hostname you're trying to reach. SNI is a TLS extension carrying the hostname in plaintext during ClientHello:
extension server_name
type: host_name (0)
length: 11
name: example.com
Why plaintext? Because the server might host a thousand sites on one IP, and it needs to know which certificate to send back. ESNI / ECH (Encrypted Client Hello) hides this by encrypting under a public key fetched from DNS, but adoption is patchy — most TLS connections in 2026 still leak the hostname here.
For network admins this is a privacy/observability seam: you can identify which host a TLS client is reaching just by reading SNI.
ALPN — application-layer protocol negotiation
After the cipher is chosen, TLS lets the two sides negotiate which protocol they'll speak inside the encrypted tunnel. The relevant extension:
extension application_layer_protocol_negotiation
client_offers:
h2
http/1.1
server_picks:
h2
This is how HTTP/2 (h2) and HTTP/3 know to start without a separate Upgrade negotiation. If you've ever wondered "how does the server know to speak HTTP/2 instead of HTTP/1.1" — ALPN is the answer.
The certificate dance
The server's Certificate message carries an X.509 chain:
Certificate
certificate_list:
- leaf: CN=example.com, issuer=R3
- intermediate: CN=R3, issuer=ISRG Root X1
(root not sent — the client already trusts it)
The client validates:
- Each certificate's signature was produced by the next one's public key.
- The chain ends at a trusted root in the system trust store.
- The leaf's
subject_alt_nameincludes the hostname the client was trying to reach. - Nothing in the chain has expired or been revoked (CRL / OCSP).
The CertificateVerify message that follows is a signature over the handshake transcript, proving the server holds the leaf cert's private key. Without this, anyone could replay a captured Certificate message — they wouldn't have the private key.
Wireshark walkthrough
For a deeper look, capture the conversation in Wireshark:
- Capture on the right interface (probably
loif your test client and server are on localhost, oren0for live traffic). - Filter:
tls.handshake && tcp.port == 443 - The first packet should be ClientHello. Expand it to see SNI, cipher suites, and extensions.
- The next-direction packet has ServerHello, Certificate, CertificateVerify, Finished.
To decrypt the application data, you need the session keys. OpenSSL emits them with SSLKEYLOGFILE:
SSLKEYLOGFILE=/tmp/keys.log curl -v https://example.com/
Then in Wireshark: Edit → Preferences → Protocols → TLS → "(Pre)-Master-Secret log filename" → /tmp/keys.log. Now the encrypted application data shows up as plaintext HTTP/2.
TLS 1.2 vs 1.3 — the practical differences
| Feature | TLS 1.2 | TLS 1.3 |
|---|---|---|
| Round trips for full handshake | 2 RTT | 1 RTT |
| 0-RTT resumption | No (RFC 5077 tickets exist but separate) | Yes (with replay caveats) |
| Cipher suite count | ~30 (many weak) | 5 (all AEAD) |
| Encrypted handshake | Only after key exchange | Most of it (Certificate is encrypted) |
| Forward secrecy | Optional (depends on cipher) | Mandatory (every cipher uses ephemeral DH) |
In 2026, you should be running TLS 1.3 everywhere with TLS 1.2 as the fallback. TLS 1.0 and 1.1 are deprecated and refused by every modern client.
Common things that go wrong
- SNI mismatch. Server has 100 vhosts, certificate doesn't match the SNI you sent. Returns the default vhost cert; client sees
name does not match certificateand aborts. - Cert expired by 2 days, deploy didn't catch it. Browser shows the scary red page. Use
cert-manageror Let's Encrypt's automated renewals, plus a "cert expiring in less than 30 days" alert. - Client trust store too old. A new root CA isn't in the client's trust list yet. Common on long-lived embedded devices.
- MITM proxy at corporate firewall. Fake leaf cert signed by a corporate root that's been distributed to managed devices. Behaves like an attack from a security perspective, but is sanctioned. Hard to detect — that's the point.
Summary
You can decode every byte of a TLS handshake yourself with openssl s_client -msg -debug or Wireshark. The order is:
- ClientHello (with SNI, cipher list, ALPN, key share)
- ServerHello (cipher choice, key share)
- EncryptedExtensions
- Certificate (chain)
- CertificateVerify (proves private-key possession)
- Finished (both sides — handshake complete)
- Application data, encrypted
Knowing this lets you debug the failures that actually happen in production — name mismatches, missing intermediates, expired roots — rather than treating TLS as a black box that "works or doesn't".
Tools in the wild
4 tools- cliWiresharkfree tier
Decode TLS handshakes byte-by-byte; with a key-log file, decrypt application data.
- cliopenssl s_clientfree tier
TLS debug client. -msg prints every record; -debug dumps raw bytes.
- serviceQualys SSL Labsfree tier
Free public scanner — grades a TLS endpoint and explains every weak cipher and missing extension.
- servicetlsfingerprint.iofree tier
Catalogues JA3 fingerprints — useful for spotting odd clients in your traffic.