The padlock in the address bar is a bundle of promises: nobody on the path can read the conversation, nobody can change it unnoticed, and the machine at the other end really is the one the address names. This chapter opens the padlock: a handshake that agrees on fresh keys in one round trip, a chain of signatures that ends in a list your browser shipped with, and the public logs and browser rules that keep certificate authorities honest.
Common mix-up: the padlock means the connection is private and the server proved it controls the name. It says nothing about whether the site is honest. A phishing site at a look-alike name can get a perfectly valid certificate for that look-alike name in a minute, because domain-validated certificates only ever check the name, not the owner's intentions.
Read first: this chapter assumes you know how a connection is opened and how a name becomes an address. If not, start with TCP, UDP & ports and DHCP & DNS on networks.jelia.nyc. The building blocks themselves (hashes, key exchange, signatures) are in Chapter 02; here we assemble them.
A TLS connection begins with two strangers who share nothing: no password, no key, no prior contact. In about one round trip across the internet they have to end up with a secret only they know, and the browser has to be convinced the other side is really www.example.com. TLS 1.3, published in August 2018 as RFC 8446, does it with a handful of messages.
ClientHello. The browser opens with a list of what it supports (protocol versions, cipher suites, signature algorithms) and, new in 1.3, a key share: its half of an ephemeral Diffie-Hellman exchange, usually on the X25519 curve, sometimes alongside a post-quantum share. It guesses which group the server will accept. The ClientHello also carries the Server Name Indication (SNI), the name of the site it wants, because one IP address often hosts thousands of sites and the server must know which certificate to present.
ServerHello. The server picks the version and cipher suite and sends its own key share. At this moment both sides can compute the same shared secret, and nobody watching can: an observer saw both public halves, but turning them into the secret is the hard problem Diffie-Hellman rests on. Everything the server sends from here on is encrypted.
EncryptedExtensions, Certificate, CertificateVerify, Finished. The server sends the rest of its settings, then its certificate chain, then a CertificateVerify: a signature, made with the private key that matches the certificate, over a hash of every handshake message so far. That signature is what proves identity. Anyone can copy a certificate; only the holder of its private key can sign this exact conversation, which includes the browser's fresh random value and key share. The Finished message is a MAC over the whole transcript, keyed from the shared secret, so any tampering with an earlier message is caught.
Client Finished, then data. The browser checks the chain (Instrument 2 does exactly that), checks the signature, checks the server's Finished, and sends its own Finished together with the first HTTP request. One round trip has passed. In TLS 1.2 the key exchange came later in the conversation, so a full handshake took two.
TLS 1.3 never uses the Diffie-Hellman output directly. It feeds it into HKDF, a key derivation function built from HMAC, in a fixed ladder of steps (RFC 8446 section 7.1). From the shared secret and the transcript hash it derives separate keys for each direction and each stage: handshake traffic secrets that protect the server's certificate and Finished, then application traffic secrets for the real data, plus an exporter secret and a resumption secret for later reconnections.
Because each stage's keys are derived with a label and the transcript so far, a key from one context can never be confused with a key for another. Either side can also ratchet the application keys forward with a KeyUpdate message during a long connection, so a key stolen later does not decrypt what came before it.
If the browser has talked to the server recently, the server will have given it a resumption ticket, a pre-shared key (PSK). Next time the browser can send a request inside its first flight, encrypted with a key derived from that PSK, before any round trip at all. That is 0-RTT early data.
The price is replay. Early data is not bound to anything fresh from the server, so a copy of that first flight, recorded on the path and sent again, can be accepted again. RFC 8446 section 8 spells this out, and RFC 9325 says not to use 0-RTT unless the application profile explicitly allows it. The safe rule: only send requests that do no harm when repeated, such as a plain page load, never "transfer money" or "place order"; servers can also keep a short memory of recent ClientHellos to reject duplicates. The handshake stepper's third mode shows where early data sits.
Pick a protocol version, then step through the messages with the buttons or the arrow keys while the diagram has focus. Below the diagram you can see which keys each side holds at that moment and what an observer on the path can read. The key exchange is real Diffie-Hellman arithmetic, done on toy-sized numbers so you can read them: the two sides really do arrive at the same secret from different directions. Real TLS uses 255-bit X25519 values and HKDF instead of the small hash shown here.
TLS 1.3 is faster than 1.2, but its bigger achievement is what it deleted. Nearly every serious attack on TLS over the previous decade lived in an option that a careful server could switch off but that many servers did not. The designers' answer was to remove the options, so there is nothing left to misconfigure.
The most important removal is RSA key transport. In the old scheme the browser invented the session secret, encrypted it with the server's long-term RSA public key, and sent it across. That works, but it means the server's certificate key unlocks every session ever recorded. Anyone who copied the traffic in 2015 and stole the key in 2020 can decrypt all of 2015. With ephemeral Diffie-Hellman, each connection's secret comes from throwaway values deleted after the handshake. Stealing the server's key later lets an attacker impersonate the server from then on, but it does not open the past. That property is called forward secrecy, and in TLS 1.3 every connection has it.
| Removed in 1.3 | What it was | Why it went |
|---|---|---|
| RSA key transport | Client encrypts the session secret to the server's certificate key | No forward secrecy; a stolen key decrypts recorded traffic; padding-oracle attacks on the RSA decryption |
| Static (EC)DH | Diffie-Hellman with a fixed server value | Same problem: the fixed value is a long-term key, so no forward secrecy |
| CBC + MAC suites | Encrypt with a block cipher in CBC mode, authenticate separately | The MAC-then-encrypt order produced a decade of padding-oracle and timing attacks; 1.3 allows only AEAD ciphers (AES-GCM, ChaCha20-Poly1305) that encrypt and authenticate in one step |
| Renegotiation | Re-running the handshake inside a live connection | Caused a 2009 splicing flaw (patched by RFC 5746) and was complex to get right; 1.3 replaces it with simple key updates |
| Compression | Compressing data before encryption | Compressed size leaks how much the secret resembles attacker-chosen text, which can reveal cookies byte by byte |
| Weak primitives | RC4, 3DES, MD5, SHA-1 signatures, export-grade keys | Broken or too small; several downgrade attacks forced servers back to them |
| Plaintext certificate | The server's certificate sent in the clear | Revealed which site you visited even without SNI; in 1.3 it is encrypted |
Yes, if it is configured with care. RFC 9325 (BCP 195, 2022) says implementations must support TLS 1.2, should support TLS 1.3 and prefer it when both sides can, and must not negotiate SSL 2, SSL 3, TLS 1.0 or TLS 1.1. For 1.2 it recommends only four cipher suites, all ECDHE with AES-GCM, and says not to negotiate RSA key transport or static Diffie-Hellman suites. Followed to the letter, a 1.2 connection is about as safe as 1.3; the difference is that 1.3 does not let you get it wrong.
A certificate is a small signed document in the X.509 format (profiled for the internet in RFC 5280). Strip away the encoding and it is a sentence: "The holder of this public key is entitled to the names listed here, between these two dates. Signed, an issuer." Everything else is detail on that sentence.
Certificate (fictional, decoded for reading)
Subject: CN = www.example.com
Subject Alt Name: DNS:www.example.com, DNS:example.com
Issuer: CN = Example Issuing CA R1, O = Example Trust
Validity: Not Before 2026-08-10 00:00 UTC
Not After 2027-02-25 23:59 UTC
Public key: EC P-256 (the server's key; its private half never leaves the server)
Basic Constraints: CA:FALSE
Key Usage: Digital Signature
Ext. Key Usage: TLS Web Server Authentication
CT SCTs: 2 signed timestamps from public logs
Signature: ECDSA-SHA256 by Example Issuing CA R1
Three fields do most of the work. The Subject Alternative Name (SAN) list holds the names the certificate is good for; browsers ignore the older Common Name entirely and check only the SAN. The validity dates bound when it may be used. And basicConstraints says whether this certificate may itself sign other certificates. A website's certificate says CA:FALSE. If a validator ever forgot to check that flag, any site owner could start issuing certificates for any name; that exact bug existed in widely used software in the early 2000s, and the fix is the reason Instrument 2 checks it.
The issuer is rarely a root. Root keys are kept offline in hardware and used rarely, so roots sign intermediate certificates, and the intermediates sign websites. The server sends its own leaf certificate plus the intermediate. The browser climbs from leaf to intermediate to a root it already holds, checking at every rung that the signature verifies, the dates are current, the issuer is allowed to be an issuer, and the names line up.
The roots live in a root store: a list of a hundred or so organizations' keys shipped inside your operating system or browser. Apple, Microsoft, Google (for Chrome) and Mozilla each run a root program that decides which CAs go in, audits them, and occasionally throws one out. This is the uncomfortable center of web trust: any root in your store can issue a certificate for any name. Certificate Transparency and CAA records, later in this chapter, exist to make that power visible and limited.
| Name rule | Certificate name | Matches | Does not match |
|---|---|---|---|
| Exact | www.example.com | www.example.com | example.com, mail.example.com |
| Wildcard | *.example.com | www.example.com, api.example.com | example.com (no label for the *), a.b.example.com (* covers one label only) |
| Case | WWW.Example.COM | www.example.com (names compare case-insensitively) | – |
| Partial wildcard | w*.example.com | Rejected by modern browsers | Everything |
The issuer takes the to-be-signed part of the certificate (every field above except the signature), hashes it, and signs the hash with its private key, exactly the signature operation from Chapter 02. A validator recomputes the hash and checks the signature with the issuer's public key, which it finds in the next certificate up. If a single bit of the subject or dates were changed, the hash would differ and the check would fail.
Path building is messier than a straight ladder. A CA may have several intermediates, cross-signed by more than one root so that old devices with old root stores still find a path. Validators therefore search for any chain that ends in a trusted root, and different clients may find different ones. Instrument 2 makes you build the chain by hand so you can see each rule fire.
All certificates here are fictional. Choose the name the browser asked for, the leaf the server presents and up to two intermediates in the order sent, then choose which roots are in the trust store and what today's date is. The validator runs five checks in a fixed order and stops at the first failure, reporting the error a browser and the OpenSSL library would show. The preset buttons load the classic failures.
Before a public CA signs a certificate for www.example.com, it must check that whoever is asking really controls that name. For most of the web's history this was a manual, emailed, paid process, and certificates lasted years because nobody wanted to do it twice. ACME (Automatic Certificate Management Environment, RFC 8555), built for Let's Encrypt and now spoken by many CAs, turned it into a short conversation between programs.
http://www.example.com/.well-known/acme-challenge/<token>. For DNS-01 it publishes a TXT record at _acme-challenge.www.example.com. DNS-01 is the only way to get a wildcard certificate.This is domain validation (DV): it proves control of the name at one moment and nothing else. Everything rests on that moment, which is why a CA's view of DNS matters so much. How DNS answers can be spoofed or protected is Chapter 07's subject.
Automation changed the economics of lifetimes. A certificate is a snapshot: the longer it lives, the longer it can outlast the facts it records, such as who owns the domain or whether the key leaked. Public TLS certificates were capped at 398 days in 2020. In April 2025 the CA/Browser Forum, the body of CAs and browser makers that writes the Baseline Requirements, voted (ballot SC-081) to step the cap down: 200 days from 15 March 2026, 100 days from 15 March 2027 and 47 days from 15 March 2029. Let's Encrypt has issued 90-day certificates since it launched and now also offers six-day ones. With lifetimes that short, renewal has to be a cron job, not a calendar reminder.
| Certificates issued | Longest allowed validity | Source |
|---|---|---|
| From Sept 2020 | 398 days | CA/B Forum Baseline Requirements |
| From 15 Mar 2026 | 200 days (in force today) | Ballot SC-081 |
| From 15 Mar 2027 | 100 days | Ballot SC-081 |
| From 15 Mar 2029 | 47 days | Ballot SC-081 |
A domain owner can also narrow who may issue at all. A CAA record (RFC 8659) in DNS lists the CAs allowed to issue for a name, and public CAs are required to check it before issuing. It costs nothing and turns "any of a hundred CAs could mis-issue for me" into "only the one I use could."
; illustrative zone entries for example.com: only one CA may issue, wildcards from nobody, ; and problems are reported to the domain owner example.com. 3600 IN CAA 0 issue "letsencrypt.org" example.com. 3600 IN CAA 0 issuewild ";" example.com. 3600 IN CAA 0 iodef "mailto:security@example.com"
Organization Validation (OV) and Extended Validation (EV) certificates add checks on the legal entity behind the domain and put its name in the certificate. Browsers once displayed EV names in green next to the address. Studies of user behavior found people did not notice the indicator or its absence, and companies with confusable names could obtain EV certificates legitimately, so the major browsers removed the special display around 2019. The cryptographic protection of OV and EV connections is identical to DV.
Sometimes a certificate must die early: the private key leaked, the domain changed hands, or the CA issued it wrongly. The CA then revokes it. The difficult part has never been revoking; it is getting every browser in the world to find out.
The catch is what happens when the check cannot complete. If the revocation server is slow or unreachable, a browser that refused to connect would break the web every time a CA had an outage, so browsers soft-fail: they treat "no answer" as "fine". An attacker who can block traffic to the responder can therefore block the bad news too. Browsers responded by pushing revocation data to themselves in compact batches (Chrome's CRLSets, Firefox's CRLite), and the industry responded with short lifetimes: a certificate that expires in days needs no revocation network at all. Let's Encrypt dropped OCSP during 2025 (its responders were scheduled to switch off on 6 August 2025) and now publishes revocation only through CRLs; its six-day certificates carry no revocation URL at all.
Root stores create a single point of failure: any trusted CA, through error or compromise, can sign a certificate for your domain, and until 2013 nobody would know unless that certificate was caught being used. Certificate Transparency (CT, RFC 6962, updated by RFC 9162) makes issuance public.
Independent operators run append-only logs. Before issuing, a CA submits the certificate (in a "precertificate" form) to several logs, and each log returns a Signed Certificate Timestamp (SCT): a signed promise to publish it within a maximum merge delay, typically 24 hours. The SCTs go inside the certificate. Chrome and Apple's platforms refuse publicly trusted certificates that do not carry enough valid SCTs, so a CA cannot issue a certificate in secret without it being rejected.
The logs are Merkle trees. Every certificate is hashed into a leaf; pairs of hashes are hashed together, up to a single root hash that the log signs. Change one old entry and the root changes, so a log cannot quietly rewrite history. Anyone can ask for an inclusion proof, the handful of sibling hashes needed to recompute the root from one leaf, which is about log₂ n hashes even for a log of billions. Auditors check consistency between successive roots; monitors watch for names. If you run a domain, subscribe to a CT monitoring service, or search a CT log aggregator for your names: a certificate you did not order is the clearest early warning of a mis-issuance or a takeover you will ever get.
Log certificates and watch the root hash change with every append. Pick any entry to see its inclusion proof drawn on the tree, then press Verify: the page re-hashes from that leaf up the path, using only the sibling hashes, and compares the result with the published root. Then try editing history: change an old entry and the recomputed root no longer matches what the log signed. The monitor flags any entry for example.com that the owner did not order.
All of the above protects a connection that starts as HTTPS. Many still begin as plain HTTP, because someone typed a bare name or followed an old link, and the server redirects to HTTPS. That first unencrypted request is a gap: someone on the path, a hostile Wi-Fi hotspot for instance, can answer it instead of the server and simply never redirect, keeping the victim on HTTP while relaying to the real HTTPS site. The padlock never appears, and few people notice.
HTTP Strict Transport Security (HSTS, RFC 6797) closes the gap from the second visit on. The site sends a header over HTTPS; the browser remembers it for max-age seconds and, until then, rewrites every http:// link for that host to https:// internally, before any packet leaves. HSTS also removes the "proceed anyway" button on certificate errors for that site, since a user who clicks through is exactly what an interceptor needs.
The very first visit is still exposed, which is what the preload list is for: a list of HSTS hosts compiled into Chrome and used by the other major browsers. Sites submit themselves at hstspreload.org once they serve the header with includeSubDomains, preload and a max-age of at least a year. Whole top-level domains, such as .dev and .app, are preloaded, so every site under them is HTTPS-only from the start.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Two years in seconds, all subdomains, eligible for preload. Turn it on in steps: start with a short max-age, check that every subdomain really serves HTTPS, then raise it. Once preloaded, removal takes months to reach all browsers.
Inside an HTTPS page, mixed content is the remaining leak: a script, stylesheet or image loaded over plain HTTP. Browsers block mixed scripts and other active content outright and now upgrade or block most passive content, but the fix belongs on the server: load everything over HTTPS, and send Content-Security-Policy: upgrade-insecure-requests while you clean up old pages.
| Failure | What the browser says | Usual cause | Fix |
|---|---|---|---|
| Expired | NET::ERR_CERT_DATE_INVALID | Renewal failed or was manual and forgotten; sometimes a wrong clock on the client | Automate renewal with ACME; monitor expiry dates; alert well before the end |
| Name mismatch | NET::ERR_CERT_COMMON_NAME_INVALID | The name typed is not in the SAN list: bare domain missing, or a two-level subdomain under a wildcard | List every served name in the SAN; check the wildcard covers only one label |
| Missing intermediate | NET::ERR_CERT_AUTHORITY_INVALID (on some clients only) | Server sends only its leaf; desktop browsers that cached the intermediate work, phones and scripts fail | Serve the full chain file the CA provides, leaf first |
| Self-signed | NET::ERR_CERT_AUTHORITY_INVALID | A test certificate left in production; a device's built-in default certificate | Use a public CA for public names; a private CA, distributed deliberately, for internal ones |
| Wrong key or order | Server fails to start, or handshake fails | Certificate and private key do not match; chain out of order | Check the pair matches before deploying; leaf first, then intermediates |
| Weak protocol | ERR_SSL_VERSION_OR_CIPHER_MISMATCH | Server offers only TLS 1.0/1.1 or old ciphers | Follow RFC 9325: TLS 1.2 and 1.3, AEAD suites only |
One failure deserves its own paragraph because it is also a product category. A TLS interception proxy, the kind many companies and some antivirus software use to inspect encrypted traffic, sits between the browser and the site. It finishes the real TLS connection to the site itself, then presents the browser with a certificate for the same name that it minted on the spot, signed by its own root. That only works because an administrator installed the proxy's root into the device's trust store, which is the whole reason browsers warn loudly on devices without it. The lesson for defenders runs both ways: an unexpected root in a trust store is a serious finding worth auditing for, and a sanctioned proxy must itself validate certificates as strictly as a browser, because it now makes that decision for everyone behind it. Instrument 2's last preset shows the mechanism with fictional certificates.
A related bug lives in code rather than configuration. A program that makes its own HTTPS requests and turns certificate checking off, often "temporarily" to get past a test error, accepts any certificate from anyone. MITRE catalogs this as CWE-295, Improper Certificate Validation. The fix is never to disable verification; it is to make the chain valid, or to give the program the private CA it should trust.
# illustrative anti-pattern: certificate checking switched off session.verify = False # any certificate from anyone is now accepted
# the fix: keep verification on; trust only the CA you actually use internally session.verify = "/etc/ssl/internal-ca.pem"
RFC 9325 is the IETF's best current practice for TLS configuration, and OWASP's TLS cheat sheet and NIST SP 800-52 say much the same in different words. Boiled down:
# illustrative web-server settings (nginx-style syntax) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_certificate /etc/ssl/example.com/fullchain.pem; # leaf + intermediate ssl_certificate_key /etc/ssl/example.com/privkey.pem; ssl_early_data off; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
Even with TLS 1.3, an observer sees IP addresses, timing, sizes, and the SNI name in the ClientHello. That last one is how many networks filter sites by name. Encrypted Client Hello (ECH, RFC 9849) fixes it: the browser fetches the server's ECH public key through DNS, then sends an outer ClientHello naming only a shared public front (say, a CDN's name) with the real ClientHello encrypted inside. It works best where many sites share one front, so the outer name reveals little. Switch on the handshake stepper's ECH option to see the difference.
Mutual TLS, where the client also presents a certificate, is how services prove their identities to each other; it comes up in Chapter 11. TLS for mail servers, and how MTA-STS adds an HSTS-like rule for email, is in Chapter 10.