networking · level 5

TLS on the Wire

ClientHello → Certificate → Finished — what bytes go across.

200 XP

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:

  1. Each certificate's signature was produced by the next one's public key.
  2. The chain ends at a trusted root in the system trust store.
  3. The leaf's subject_alt_name includes the hostname the client was trying to reach.
  4. 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:

  1. Capture on the right interface (probably lo if your test client and server are on localhost, or en0 for live traffic).
  2. Filter: tls.handshake && tcp.port == 443
  3. The first packet should be ClientHello. Expand it to see SNI, cipher suites, and extensions.
  4. 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 certificate and aborts.
  • Cert expired by 2 days, deploy didn't catch it. Browser shows the scary red page. Use cert-manager or 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:

  1. ClientHello (with SNI, cipher list, ALPN, key share)
  2. ServerHello (cipher choice, key share)
  3. EncryptedExtensions
  4. Certificate (chain)
  5. CertificateVerify (proves private-key possession)
  6. Finished (both sides — handshake complete)
  7. 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
  • Wiresharkfree tier

    Decode TLS handshakes byte-by-byte; with a key-log file, decrypt application data.

    cli
  • TLS debug client. -msg prints every record; -debug dumps raw bytes.

    cli
  • Free public scanner — grades a TLS endpoint and explains every weak cipher and missing extension.

    service
  • Catalogues JA3 fingerprints — useful for spotting odd clients in your traffic.

    service