Topics
Networking & Protocols

The TLS Handshake and Certificate Chains

What happens in the round trip before HTTPS sends a byte: key exchange, certificate checks, forward secrecy, 0-RTT, and why handshakes fail in production.

Intermediate·14 min read·Updated Oct 6, 2026

Before an HTTPS request sends a single byte of data, client and server run a TLS (Transport Layer Security) handshake: they agree on fresh encryption keys through an ephemeral Diffie-Hellman exchange that an eavesdropper cannot reproduce, and the server proves it owns the domain by signing the exchange with the key behind a certificate that chains up to an authority the client already trusts. TLS 1.3 does all of this in one round trip, and if anything in the chain does not check out, the connection is refused before the application ever sees a request.

Context

Netscape built SSL (Secure Sockets Layer) in the mid-1990s so people could type a credit card number into a web page. The IETF (Internet Engineering Task Force) took it over and renamed it TLS: version 1.0 in 1999, 1.2 in 2008, and 1.3 in 2018 (RFC 8446), which removed a decade of insecure options and cut the handshake to one round trip. Everyone still says "SSL certificate", but SSL itself has been dead since 2015, and TLS 1.0 and 1.1 were formally deprecated by RFC 8996 in 2021 after browsers had already dropped them in 2020. HTTPS stopped being optional when Let's Encrypt started issuing free, automated certificates (first certificate 2015, out of beta in 2016); today nearly all browser traffic is encrypted.

You meet it every time a browser shows the padlock, every time an Nginx config has ssl_certificate, every time a service mesh like Istio says it gives you "mTLS (mutual TLS) for free", and every time a request fails with certificate has expired. The quickest way to watch a handshake is one command:

watch-a-handshake.sh
curl -sv https://example.com -o /dev/null 2>&1 \
  | grep -E '^\* (SSL|TLS|ALPN)'
# * TLSv1.3 (OUT), TLS handshake, Client hello (1):
# * TLSv1.3 (IN), TLS handshake, Server hello (2):
# * TLSv1.3 (IN), TLS handshake, Certificate (11):
# * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
# * ALPN: server accepted h2
Certificate
A signed statement binding a public key to names (the domains in its SAN, Subject Alternative Name, field) for a limited time.
CA (certificate authority)
An organisation whose signature browsers and operating systems trust. Its root certificate ships in the device trust store.
ECDHE
Elliptic-curve Diffie-Hellman, ephemeral: both sides generate a throwaway key pair per connection and derive the same secret without ever sending it.
Forward secrecy
Recording traffic today and stealing the server key tomorrow still does not decrypt it, because the session keys came from ephemeral keys that no longer exist.
SNI (Server Name Indication)
The hostname the client puts in its first message so one IP address can serve many certificates.
ALPN (Application-Layer Protocol Negotiation)
The handshake field where client and server agree on HTTP/2 or HTTP/1.1, so no extra round trip is needed.

Why it matters

Every HTTPS connection pays for the handshake before the first byte of data, so it sits directly in the latency of every cold request, which is why TLS 1.3, connection reuse and session resumption matter for performance. It is also one of the most common causes of outages that have nothing to do with code: a certificate that expired on a Sunday, a load balancer that serves the leaf without its intermediate, or an internal client with a trust store from 2017. Knowing what each step checks turns "TLS error" into a five-minute diagnosis instead of a guessing game.

The TLS 1.3 handshake, message by message

TLS 1.3 assumes the client will pick a modern key exchange, so the client sends its Diffie-Hellman public key in the very first message instead of waiting to negotiate. The server answers with its own, both sides compute the same shared secret, and everything after the server's first message is already encrypted, including the certificate. One RTT (round-trip time) later the client can send its HTTP request.

ClientServerClientHello · key_share, SNI, ALPNServerHello · key_share{EncryptedExtensions, Certificate,CertificateVerify, Finished}{Finished} + first HTTP request{HTTP response}1 RTT{ } = encrypted with handshake keys
The TLS 1.3 full handshake. Messages in braces are encrypted with keys derived from the key_share exchange; the client sends its HTTP request together with its Finished message, one round trip after starting.
  1. 1
    ClientHello. Supported versions and cipher suites, a random nonce, an ECDHE public key (key_share, usually X25519), the hostname in SNI, and the protocols it speaks in ALPN (h2, http/1.1). SNI travels in plaintext unless the client uses ECH (Encrypted Client Hello).
  2. 2
    ServerHello. The server picks the version and cipher suite and sends its own key_share. Both sides now compute the same Diffie-Hellman secret and derive handshake keys from it; nothing secret crossed the wire.
  3. 3
    Encrypted server flight. EncryptedExtensions carries the ALPN choice; Certificate carries the leaf and intermediates; CertificateVerify is a signature over the whole handshake so far, made with the private key behind the certificate, which is what proves the server owns it; Finished is a MAC (message authentication code) over the transcript, so any tampering is detected.
  4. 4
    Client Finished. The client validates the certificate chain and the signature, sends its own Finished, and can attach the first HTTP request in the same packet. Both sides switch to application keys.
TLS 1.2TLS 1.3
Full handshake2 round trips1 round trip
Key exchangeRSA key transport or (EC)DHEOnly ephemeral (EC)DHE: forward secrecy is mandatory
CertificateSent in plaintextEncrypted
Cipher suitesDozens, including CBC and RC4 legacyFive, all AEAD (authenticated encryption)
ResumptionSession IDs or tickets, 1 round tripPSK (pre-shared key) tickets, 1 round trip or 0-RTT

How the client decides to trust the server

The certificate the server sends is only useful if the client can connect it to someone it already trusts. Browsers and operating systems ship a trust store of a few hundred root CA certificates. Roots almost never sign websites directly; they sign intermediate CAs, which sign the leaf certificates for domains. The server must send the leaf plus the intermediates; the root is already on the client.

Root CAin the trust store
Intermediate CAsent by the server
Leaf: example.comsent by the server
CertificateVerifyproves key ownership
Chain of trust. The client walks signatures from the leaf up to a root it already has; every link must verify, be within its validity dates and allow signing the next.
  1. 1
    Signatures. Each certificate's signature verifies with the public key of the one above it, ending at a root in the local trust store.
  2. 2
    Name. The hostname the client asked for must match a SAN entry on the leaf; *.example.com covers one label, not a.b.example.com. The old Common Name field is ignored by modern clients.
  3. 3
    Dates. Now must be between notBefore and notAfter, which is why a device with a wrong clock fails every handshake. Lifetimes keep shrinking: the CA/Browser Forum voted in 2025 to cut the maximum from 398 days to 200 in 2026 and to 47 days by 2029, so manual renewal stops being viable.
  4. 4
    Revocation. A revoked certificate should be rejected. OCSP (Online Certificate Status Protocol) asked the CA per connection, which was slow and leaked browsing; OCSP stapling has the server attach a fresh signed status instead. In practice browsers lean on pushed revocation lists, and Let's Encrypt shut down its OCSP service in 2025.
  5. 5
    Certificate Transparency. Since 2018 Chrome only trusts publicly issued certificates that were logged in public append-only logs, so a CA mis-issuing a certificate for your domain is visible to anyone monitoring the logs.

Mutual TLS

Normally only the server proves its identity. In mTLS the server also sends a CertificateRequest, and the client must present a certificate from a CA the server trusts and sign the handshake with its key. Service meshes such as Istio issue every workload a short-lived certificate and enforce mTLS between services automatically, which gives both encryption and a cryptographic service identity without API keys to leak or rotate by hand.

mtls.conf
server {
    listen 443 ssl;
    ssl_certificate        /etc/tls/server-fullchain.pem;  # leaf + chain
    ssl_certificate_key    /etc/tls/server.key;
    ssl_protocols          TLSv1.2 TLSv1.3;

    # mTLS: require a client certificate signed by our internal CA
    ssl_client_certificate /etc/tls/internal-ca.pem;
    ssl_verify_client      on;

    location / {
        # pass the verified identity to the app
        proxy_set_header X-Client-DN $ssl_client_s_dn;
        proxy_pass http://app;
    }
}

Resumption, 0-RTT and the replay problem

A full handshake costs a round trip and an asymmetric signature, so returning clients skip most of it. At the end of a TLS 1.3 handshake the server can send a session ticket; next time the client presents it as a PSK (pre-shared key) and the handshake needs no certificate. If the server allows it, the client may go further and send application data in its very first flight, encrypted with keys from the previous session: 0-RTT (zero round-trip time), or early data.

debug-tls.sh
# full handshake details, with SNI (omit -servername and many hosts
# return the wrong certificate)
openssl s_client -connect api.example.com:443 \
  -servername api.example.com -tls1_3 </dev/null

# the chain the server actually sends (look for a missing intermediate)
openssl s_client -connect api.example.com:443 \
  -servername api.example.com -showcerts </dev/null

# expiry date of the live certificate
openssl s_client -connect api.example.com:443 \
  -servername api.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -enddate -ext subjectAltName

Pitfalls

  • Serving the leaf without its intermediate

    Browsers often still connect because they cache intermediates or fetch them via the AIA (Authority Information Access) extension, so the site looks fine. Then curl, a mobile app or a server-side client fails with "unable to get local issuer certificate". Always deploy the full chain file and test with openssl s_client -showcerts, not a browser.

  • Renewing by hand

    A yearly calendar reminder fails eventually, and certificate lifetimes are shrinking toward 47 days. Automate issuance and renewal with ACME (Automatic Certificate Management Environment) clients or your platform's managed certificates, and alert on days-to-expiry from an external probe, because the renewal job itself can silently break.

  • Disabling verification to make an error go away

    rejectUnauthorized: false, verify=False or curl -k keep the encryption but drop authentication, so any machine in the path can present its own certificate and read everything. Fix the trust store or the chain instead; for internal CAs, add the CA explicitly to the client.

  • Terminating TLS and forgetting the hop behind it

    A load balancer or CDN that terminates TLS forwards plain HTTP unless you configure otherwise. Inside a shared network or across regions that traffic is readable. Re-encrypt to the backend, or use mTLS between services, and make the app trust the forwarded protocol header only from the proxy.

  • Enabling 0-RTT for everything

    Early data saves a round trip for returning visitors but can be replayed. If a CDN or server turns it on globally, a captured non-idempotent request can run twice. Limit it to safe methods or reject early data with 425 on endpoints that change state.

Interview questions

Q1Walk me through a TLS 1.3 handshake.

The client sends ClientHello with its supported ciphers, an ephemeral Diffie-Hellman public key, the hostname in SNI and its protocols in ALPN. The server replies with ServerHello and its own key share; both derive the same secret, so the rest of the server's flight is encrypted: the certificate chain, a CertificateVerify signature over the transcript proving it holds the private key, and Finished. The client validates the chain and signature, sends Finished and its first request, all within one round trip.

Q2How does the client know it is talking to the real server and not an attacker?

The server must present a certificate for that hostname that chains to a root in the client's trust store, and sign the handshake with the matching private key. An attacker can copy the certificate but not produce the CertificateVerify signature without the key, and cannot get a CA to issue a certificate for a domain it does not control, which Certificate Transparency logs would expose anyway.

Q3What is forward secrecy and why does TLS 1.3 require it?

Forward secrecy means a leaked long-term server key cannot decrypt past sessions. It comes from ephemeral Diffie-Hellman: each connection's keys derive from throwaway key pairs, and the certificate key only signs. TLS 1.3 removed RSA key transport, where the session secret was encrypted to the server's key, because one stolen key exposed all recorded traffic.

Q4What happens when a certificate expires in production?

Every new handshake fails the date check, so browsers show a full-page error and API clients fail with a certificate error, while existing connections keep working until they reconnect, which makes it look intermittent at first. The fix is to deploy a renewed certificate and full chain; the prevention is automated ACME renewal plus external monitoring of days to expiry.

Q5Why is 0-RTT risky and how do you use it safely?

Data sent in the first flight with resumption keys can be captured and replayed, and the server cannot distinguish the copy. So 0-RTT is acceptable only for idempotent, safe requests like GETs; for anything that changes state the server rejects early data with 425 Too Early and the client retries after the full handshake.

Q6What is mTLS and when would you use it instead of API keys?

Mutual TLS makes the client present and prove a certificate too, so both sides are authenticated at the connection level. It fits service-to-service traffic: identities are cryptographic, certificates can be short-lived and rotated automatically by a mesh or internal CA, and there is no bearer secret to leak in logs. API keys remain simpler for third-party developers who cannot manage certificates.

Q7A browser loads the site fine but a Node.js client gets "unable to get local issuer certificate". What is wrong?

Almost always the server sends the leaf without the intermediate certificate. Browsers paper over it by caching or fetching intermediates; most HTTP libraries do not. I would confirm with openssl s_client -showcerts and fix the server to send the full chain, rather than disabling verification in the client.

Key takeaways
  • TLS gives confidentiality, integrity and server authentication; it says nothing about whether the server or the request is trustworthy.
  • TLS 1.3 needs one round trip: key shares in the first two messages, everything after that encrypted, including the certificate.
  • Ephemeral Diffie-Hellman provides forward secrecy; the certificate key only signs the handshake to prove identity.
  • Trust is a chain: leaf and intermediates from the server, root from the local trust store, plus name, date and revocation checks.
  • Most production failures are expiry, a missing intermediate, a wrong SNI or clock skew. Automate renewal and test with openssl, not a browser.
  • Resumption saves a round trip; 0-RTT saves another but can be replayed, so keep it to idempotent requests.

Preparing for interviews? DevRecall turns a job description into a prep plan that points at topics like this one.

Start free