32.0 What this chapter gives you#
- You will be able to write a complete HTTP request by hand, byte by byte,
and read a real response header block without guessing at any line.
- You will be able to name the three separate problems that TLS solves, and
say why solving only the first one is close to useless.
- You will be able to explain symmetric encryption, hashing, message
authentication, public keys and key exchange in plain words, and then work
a Diffie-Hellman exchange by hand with small numbers.
- You will be able to walk through both the TLS 1.2 and the TLS 1.3
handshake message by message, and say exactly what TLS 1.3 removed.
- You will be able to read an X.509 certificate field by field with
openssl, and say what every field is for.
- You will be able to explain the chain of trust from leaf to intermediate
to root, say where the root store lives on each operating system, and say
who decides what goes into it.
- You will be able to state precisely what a browser checks before it shows
the padlock, and, more importantly, the long list of things it does not
check.
- You will be able to explain why accepting a self-signed certificate on
the reader’s own router at
192.168.0.1 is a reasonable risk, and why
accepting the identical warning on a public site destroys everything TLS
was for.
- You will be able to spot a company proxy or an antivirus product opening
your encrypted traffic, using one command and one line of its output.
- You will be able to describe HTTP/1.1, HTTP/2 and HTTP/3 and say what
problem each new version was built to fix.
32.1 HTTP first, because TLS wraps it#
PLAIN32.1.1 in simple words#
- HTTP is the language a browser and a web server use to talk.
- It is text. Real, readable, typed-out text. Not a secret binary format.
- The conversation has exactly two parts: you ask, the server answers.
- The thing you send is called a request. The thing that comes back is
called a response.
- A request says: which verb, which path, which version, then some labelled
facts, then maybe some data.
- A response says: which version, a three-digit number saying how it went,
then some labelled facts, then maybe some data.
- The labelled facts are called headers. Each is one line, of the form
Name: value.
- The data at the end is called the body. A request often has no body. A
response usually has one.
- So to understand HTTPS you must first be able to read HTTP. That is what
this section does.
PLAIN32.1.2 a picture in your head#
- Think of posting a form to a large government office.
- On the envelope you write the department and the reference number. That is
the request line: the verb and the path.
- Inside, the first page is a cover sheet of small boxes. Your name, your
language, the date, whether you want a reply by post. Those are the
headers.
- After the cover sheet come the actual pages you are submitting. That is
the body.
- The office writes back. Their first line is a stamp: APPROVED, REJECTED,
MOVED TO ANOTHER OFFICE. That is the status code.
- Under the stamp is their own cover sheet of boxes, then their pages.
Where this comparison breaks: a real office keeps a file on you. HTTP keeps
nothing. The server may keep a file, but the protocol itself has no memory at
all, and that single fact is the reason cookies had to be invented. Also, a
government office handles one form at a time. A modern browser fires off
dozens of these exchanges at once over a single connection.
PLAIN32.1.3 a worked example#
- Here is a real request, typed by hand and sent to
github.com. The
command that sent it is shown further down.
- Every line ends with two invisible characters, carriage return and line
feed, written
\r\n. The header block ends with one empty line.
GET /robots.txt HTTP/1.1\r\n
Host: github.com\r\n
User-Agent: kedbyte/1.0\r\n
Accept: */*\r\n
Connection: close\r\n
\r\n
- Line 1 is the request line: verb
GET, path /robots.txt, version
HTTP/1.1.
Host says which site you want. One server may host a thousand sites on
one address, so this line is compulsory in HTTP/1.1.
User-Agent says which program is asking. Accept says which formats you
can read. Connection: close asks the server to hang up after replying.
- The empty line is the end of the headers. Anything after it would be body.
A
GET has no body, so there is nothing after it.
- Here is the real response that came back, trimmed only where marked.
HTTP/1.1 200 OK
date: Thu, 13 Aug 2026 02:28:27 GMT
content-type: text/plain
last-modified: Thu, 13 Aug 2026 02:17:47 GMT
etag: W/"6a7d294b-8e2"
vary: Accept-Encoding, Accept, X-Requested-With
server: github.com
x-frame-options: DENY
strict-transport-security: max-age=31536000;
includeSubDomains; preload
accept-ranges: bytes
set-cookie: _octo=GH1.1.1717559084.1786588114;
expires=Fri, 13 Aug 2027 02:28:34 GMT; domain=.github.com;
path=/; secure; SameSite=Lax
content-length: 2274
x-github-request-id: 4003:3C7715:235C0C:2CA82C:6A7D2BCD
x-github-edge-region: iad
# If you would like to crawl GitHub contact us via ...
User-agent: bingbot
Disallow: /ekansa/Open-Context-Data
- Line 1 is the status line: version
HTTP/1.1, code 200, and the
human word OK.
content-length: 2274 says the body is exactly 2274 bytes long. The
reader knows when to stop reading.
PLAIN32.1.4 what is really happening inside#
- The browser has already opened a TCP connection, which is a two-way
stream of bytes to the server. Chapter 30 covers how.
- HTTP is only an agreement about what to write into that stream.
- The client writes the request bytes and then waits.
- The server reads until it sees the blank line, works out what is wanted,
and writes the response bytes back.
- The server must tell the client where the body ends. There are only two
ways: give a
Content-Length, or use Transfer-Encoding: chunked, where
each piece is preceded by its own size in hexadecimal.
- Nothing in HTTP is encrypted. Anyone who can read the TCP stream can read
every byte shown above, including any password in the body.
- HTTPS changes exactly one thing: those same bytes are handed to a TLS
layer, which encrypts them before they reach TCP, and decrypts them at the
far end.
- The verb, the path, the headers, the cookies and the body are all inside
the encryption. The IP address and the port number are not, because the
network needs them to deliver anything at all.
TECHNICAL32.1.5 the engineer’s version#
- HTTP/1.0 is RFC 1945, May 1996. HTTP/1.1 was first RFC 2068 in January
1997, then RFC 2616 in June 1999, then split into RFC 7230 to RFC 7235 in
June 2014.
- The current specification is the June 2022 trio: RFC 9110 HTTP Semantics,
RFC 9111 HTTP Caching, RFC 9112 HTTP/1.1. Quote these, not RFC 2616.
- A method is safe if it is not meant to change anything on the server.
A method is idempotent if doing it five times leaves the same state as
doing it once.
| Method |
Safe |
Idempotent |
Body |
| GET |
yes |
yes |
no |
| HEAD |
yes |
yes |
no |
| OPTIONS |
yes |
yes |
rare |
| PUT |
no |
yes |
yes |
| DELETE |
no |
yes |
rare |
| POST |
no |
no |
yes |
| PATCH |
no |
no |
yes |
GET fetches. HEAD fetches only the headers, so you can check size or
freshness without downloading the body.
PUT replaces a resource at a known path with what you send. POST
submits data to a handler and the server decides what to create.
PATCH applies a partial change and is defined in RFC 5789, March 2010.
It is not idempotent in general because a patch may be relative.
OPTIONS asks what is allowed. Its main real use today is the CORS
preflight, covered in section 32.11.
- Status codes come in five families.
| Family |
Meaning |
Example |
| 1xx |
Interim, keep waiting |
101, 103 |
| 2xx |
It worked |
200, 201, 204 |
| 3xx |
Go somewhere else |
301, 304, 307 |
| 4xx |
Your request is wrong |
400, 403, 404 |
| 5xx |
Our server is wrong |
500, 502, 503 |
- The codes actually worth memorizing: 200 OK, 201 Created, 204 No Content,
206 Partial Content, 301 Moved Permanently, 302 Found, 304 Not Modified,
307 Temporary Redirect, 308 Permanent Redirect, 400 Bad Request,
401 Unauthorized, 403 Forbidden, 404 Not Found, 405 Method Not Allowed,
409 Conflict, 429 Too Many Requests, 500 Internal Server Error,
502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout.
- 401 means “you have not proved who you are”. 403 means “I know who you
are and you still may not”. People mix these up constantly.
- Request headers that matter:
Host, User-Agent, Accept,
Accept-Encoding, Authorization, Cookie, Referer (misspelled in
the original 1996 specification and never fixed), If-None-Match,
Range, Origin, Content-Type, Content-Length.
- This is the exact command used to produce the real output above. It opens
a TLS connection and then types HTTP into it by hand.
printf 'GET /robots.txt HTTP/1.1\r\nHost: github.com\r\n\
Connection: close\r\n\r\n' \
| openssl s_client -quiet -connect github.com:443 \
-servername github.com
WORDS32.1.6 remember these#
- HTTP — the ask-and-answer language of the web — a stateless application
protocol defined by RFC 9110 to RFC 9112, 2022.
- Request line — the first line you send — method, request-target and
HTTP-version separated by single spaces.
- Header — one labelled fact per line — a case-insensitive field name, a
colon, optional whitespace, and a field value.
- Body — the actual data — the payload delimited by
Content-Length or by
chunked transfer encoding.
- Safe method — does not change anything — a method with no intended state
change on the origin server, so it may be cached or prefetched.
32.2 The three problems TLS solves#
PLAIN32.2.1 in simple words#
- TLS stands for Transport Layer Security. It is the lock on HTTPS.
- People think it solves one problem, secrecy. It actually solves three, and
the third is the one that matters most.
- Confidentiality: nobody in the middle can read what you send.
- Integrity: nobody in the middle can change what you send without being
caught.
- Authenticity: you are really talking to the machine that owns the name
you typed, and not to somebody pretending.
- Confidentiality without authenticity is nearly worthless.
- Here is why, in one line: if an attacker sits in the middle and you set up
a perfectly encrypted tunnel to the attacker, your secrets are encrypted
all the way into the attacker’s hands.
- So the first job of TLS is not encryption. It is proving who the other end
is. Encryption only becomes useful after that proof.
PLAIN32.2.2 a picture in your head#
- Imagine sending a valuable parcel to a friend in another city.
- Confidentiality is the sealed opaque box. Nobody along the way can see
what is inside.
- Integrity is the tamper-evident seal. If somebody opens the box and swaps
the contents, the broken seal shows it.
- Authenticity is checking the identity of the person who signs for it at
the other end.
- Now picture a courier who quietly redirects your parcel to his own flat,
signs for it himself, opens it, and repacks it in an identical box with a
fresh seal before sending it on.
- The box was sealed. The seal was intact on arrival. And you were robbed
anyway, because you never checked who signed for it.
- That is exactly a man-in-the-middle attack, and it is why authenticity
comes first.
Where this comparison breaks: a courier must physically hold your parcel. On
a network an attacker only needs to sit on any link along a path that may be
thousands of kilometres long. The reader’s own traceroute crossed at least
three organizations before Pune. Any one of them is a place a box could be
opened.
PLAIN32.2.3 a worked example#
- Take three concrete attacks, one per property.
- Attack on confidentiality: passive sniffing. The reader is on public
cafe wireless. An attacker runs a laptop in monitor mode and records every
frame in the air. Over plain HTTP the attacker reads the session cookie
and logs in as the reader.
- What defeats it: encryption. The recorded bytes are meaningless without
the key.
- Attack on integrity: content injection. An ISP or a hotel network
rewrites a plain HTTP page as it passes, adding an advertising script, or
swapping a download link for a modified installer. This has been done
commercially and by governments.
- What defeats it: a message authentication tag on every record, so any
changed byte makes the receiver throw the whole record away.
- Attack on authenticity: impersonation. An attacker answers the DNS
query for
github.com with his own address, or announces a false route,
and your browser connects to his server. He offers you a perfectly good
TLS connection to himself.
- Now combine them. Encryption alone stops attack 2 from being read but not
attack 6 from happening. Authentication alone stops attack 6 but leaves
attack 2 wide open. You need all three, and TLS gives all three in one
protocol.
PLAIN32.2.4 what is really happening inside#
- TLS sits between the application and TCP. HTTP hands it bytes; it hands
TCP encrypted records; TCP hands the network packets.
- During the handshake the two sides agree on which algorithms to use, prove
identity, and jointly create secret keys.
- Identity is proved by a certificate plus a signature. The server sends a
certificate that says “this public key belongs to github.com”, and then
signs a value derived from the live conversation with the matching private
key.
- That second step is what stops replay. Anybody can copy a certificate. Only
the true owner can sign fresh data with the private key inside it.
- Once identity is settled, the two sides derive symmetric keys and switch
to fast bulk encryption for everything after.
TECHNICAL32.2.5 the engineer’s version#
- TLS provides confidentiality and integrity through an AEAD cipher, which
means Authenticated Encryption with Associated Data, and authenticity
through X.509 certificate-based server authentication.
- In TLS 1.3, defined in RFC 8446 of August 2018, all cipher suites are
AEAD. Non-authenticated modes were removed entirely.
- Server authentication is mandatory in practice on the web. Client
authentication with a client certificate is optional and rare outside
enterprises and machine-to-machine APIs.
- Anonymous key exchange suites exist in the specifications and are
disabled everywhere. They give encryption with no authentication, which is
the useless case described above.
- A property TLS does not provide: hiding who you are talking to. The
destination IP address is in the clear, and until Encrypted Client Hello
the server name was in the clear too.
- Encrypted Client Hello was published as RFC 9849 in March 2026. It
encrypts the server name and the protocol list in the first message.
Deployment is uneven; treat it as new rather than universal.
WORDS32.2.6 remember these#
- Confidentiality — nobody can read it — an attacker with the full
ciphertext learns nothing about the plaintext beyond its length.
- Integrity — nobody can change it undetected — any modification is rejected
by an authentication tag check.
- Authenticity — you are talking to the right machine — the peer proved
possession of the private key matching a certified public key.
- Man in the middle — someone sitting between you and the server —
an active on-path attacker who terminates and re-originates the connection.
32.3 The crypto building blocks, taught gently#
PLAIN32.3.1 in simple words#
- TLS is built from five simple ideas. None of them is hard on its own.
- Symmetric encryption: one shared secret scrambles and unscrambles.
Fast. Both sides need the same key.
- Hashing: turn any amount of data into a short fixed fingerprint. Easy
forwards, impossible backwards.
- Message authentication code: a fingerprint that only somebody with the
secret can produce, so it proves the data was not changed by an outsider.
- Asymmetric encryption: two matching keys. One is public, one is
private. What one does, only the other can undo.
- Key exchange: a way for two strangers to end up holding the same secret
number, even though everything they said was heard by everyone.
- TLS uses asymmetric maths only at the start, to prove identity and agree a
key, because it is slow. Then it uses symmetric encryption for all the
real traffic, because it is fast.
PLAIN32.3.2 a picture in your head#
- For Diffie-Hellman key exchange, use paint.
- You and a stranger stand at opposite ends of a crowded room. Everyone can
see what you carry across.
- First you both agree, out loud, on a common colour. Say yellow. Everybody
hears this. That is fine.
- You privately pick a secret colour, red. You keep the tin hidden. The
stranger privately picks blue and hides it.
- You mix your red into the yellow and get orange. He mixes his blue into
the yellow and gets green.
- You swap. You hand him orange in public. He hands you green in public.
The crowd sees orange and green.
- Now you add your hidden red to the green you received. He adds his hidden
blue to the orange he received.
- You both hold the same colour: yellow plus red plus blue. The crowd, who
only saw yellow, orange and green, cannot make it.
- The trick is that mixing paint is easy and unmixing it is very hard.
Where this comparison breaks: unmixing paint is only messy, not impossible.
The real system uses modular exponentiation, and reversing it means solving
the discrete logarithm problem, which nobody can do quickly. Also, the shared
result is a number, not a key; it is passed through a key derivation function
first. And on its own it tells you nothing about who the stranger is, which
is why certificates must be bolted on.
PLAIN32.3.3 a worked example#
- Let us do the real arithmetic with numbers small enough to check by hand.
- Public, agreed out loud: a prime
p = 23 and a base g = 5. Everyone
knows both.
- Alice secretly picks
a = 6. Bob secretly picks b = 15.
- Alice computes
A = g^a mod p, that is 5^6 mod 23. Now 5^6 = 15625,
and 15625 = 679 * 23 + 8, so A = 8.
- Bob computes
B = g^b mod p = 5^15 mod 23 = 19.
- They send
8 and 19 across the room. An eavesdropper now knows
p = 23, g = 5, A = 8, B = 19.
- Alice computes
B^a mod p = 19^6 mod 23 = 2.
- Bob computes
A^b mod p = 8^15 mod 23 = 2.
- Both hold
2, and it was never sent. This is real output from python3:
p = 23 g = 5
Alice secret a = 6 -> A = g^a mod p = 8
Bob secret b = 15 -> B = g^b mod p = 19
Alice computes B^a mod p = 2
Bob computes A^b mod p = 2
- Why it works:
B^a = (g^b)^a = g^(ba) and A^b = (g^a)^b = g^(ab), and
ab equals ba, so both are g^(ab) mod p.
- Why the eavesdropper is stuck: to get
a from A = 8 he must find which
power of 5 gives 8 modulo 23. With p = 23 he can just try all of them.
Here are the powers of 5 modulo 23, in order:
5, 2, 10, 4, 20, 8, 17, 16, 11, 9, 22, 18, 21, 13, 19, 3,
15, 6, 7, 12, 14, 1
- The sixth entry is 8, so
a = 6. Trivial at this size.
- In real use
p is 2048 or 3072 bits, so that list has more entries than
there are atoms in the observable universe, and there is no known
shortcut.
- Now hashing, with real
openssl output. Change one letter and the whole
fingerprint changes:
$ echo -n "hello" | openssl dgst -sha256
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362
938b9824
$ echo -n "hellp" | openssl dgst -sha256
fdd7585e08c4e2afd71dcabdb4636c89d557a3f42db9e2040c8bbd17
08aa4ce7
- Each output is 64 hexadecimal characters, which is 256 bits, which is
why it is called SHA-256. Both were wrapped over two lines to fit this
page.
PLAIN32.3.4 what is really happening inside#
- Symmetric encryption. AES takes a key of 128 or 256 bits and a block
of 16 bytes, and shuffles the block using the key through 10 to 14 rounds
of substitution and mixing. The same key run in reverse undoes it.
- Modern TLS uses AES-GCM, which encrypts and also produces a 16-byte
authentication tag over the ciphertext and the record header. One
primitive, both properties.
- Here is a real AES-256-GCM run. Note the tag, and note what happens when
one single bit of the ciphertext is flipped:
plaintext = meet me at six (14 bytes)
ciphertext = aaacf7bb4bf766de898f5ea22006
tag = 1cee6a97e098c053433578dfe18e3d22
flip one bit -> decrypt raises InvalidTag
honest decrypt -> meet me at six
- Hashing. SHA-256 chews the input in 64-byte blocks, keeping eight
32-bit working values that get stirred by each block. The final eight
values, joined, are the digest. There is no key and no way back.
- The property we rely on is collision resistance: nobody can find two
different inputs with the same digest. When that broke for MD5 in 2004 and
for SHA-1 in 2017, both had to be removed from certificates.
- Asymmetric. RSA works with a public pair
(n, e) and a private
exponent d. Encrypting is c = m^e mod n. Decrypting is m = c^d mod n.
Signing is the same operation with the private key, and verifying uses the
public key.
- Signatures in practice. You never sign the message. You hash it, then
sign the hash. That is why every certificate says something like
sha256WithRSAEncryption: hash with SHA-256, sign with RSA.
- Here is real signing and verifying, with a genuine 2048-bit RSA key made
for this chapter:
$ openssl dgst -sha256 -sign ca.key -out msg.sig msg.txt
$ openssl dgst -sha256 -verify ca.pub -signature msg.sig \
msg.txt
Verified OK
$ openssl dgst -sha256 -verify ca.pub -signature msg.sig \
bad.txt
rsa routines:ossl_rsa_verify:bad signature
Verification failure
TECHNICAL32.3.5 the engineer’s version#
- AES is FIPS 197, published November 2001, from the Rijndael cipher by
Joan Daemen and Vincent Rijmen. Block size 128 bits; key sizes 128, 192
and 256 bits; 10, 12 and 14 rounds.
- HMAC is RFC 2104, February 1997. It is defined as
H((K xor opad) || H((K xor ipad) || M)), which nests two hashes so that
length-extension tricks do not work.
- RSA was described by Ron Rivest, Adi Shamir and Leonard Adleman in 1977
and published in February 1978. Diffie-Hellman came first, in the November
1976 paper “New Directions in Cryptography” by Whitfield Diffie and Martin
Hellman.
- Elliptic curve cryptography was proposed independently by Neal Koblitz and
Victor Miller in 1985. Curve25519 was published by Daniel J. Bernstein in
2006 and is specified for use in TLS by RFC 7748, January 2016.
- Elliptic curves replaced RSA for new deployments because they give the
same security with far smaller keys and far cheaper operations. Here are
real key sizes measured on this machine:
| Key |
Private PEM |
Public DER |
| RSA 2048 |
1708 bytes |
294 bytes |
| RSA 3072 |
2488 bytes |
422 bytes |
| P-256 |
227 bytes |
91 bytes |
| P-384 |
288 bytes |
120 bytes |
- Forward secrecy means that recording today’s traffic and stealing the
server’s private key tomorrow does not let the attacker decrypt what was
recorded.
- It works because the actual session key comes from an ephemeral
Diffie-Hellman exchange, with a key pair generated for that one
connection and then thrown away. The long-term private key only signs; it
never encrypts the session key.
- Old RSA key transport, where the client encrypted the premaster secret to
the server’s RSA public key, had no forward secrecy. That is precisely
why TLS 1.3 removed it. Every TLS 1.3 handshake is forward secret.
- You can see the ephemeral key in real output. This line from a live
handshake to
github.com shows an ephemeral X25519 key was used:
Server Temp Key: X25519, 253 bits
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
- The
E at the end of ECDHE is the important letter. It stands for
ephemeral. ECDH without the E is static and not forward secret.
- Chapter 44 goes much further into cryptography, including post-quantum
key exchange, which is now being deployed in TLS to protect against
future quantum computers recording traffic today.
WORDS32.3.6 remember these#
- Symmetric encryption — one shared key both ways — a block or stream cipher
such as AES-256-GCM, keyed identically at both ends.
- Hash — a short fingerprint of any data — a one-way function such as
SHA-256 producing a fixed 256-bit digest.
- Public key — the half you publish — the key used to verify signatures and,
in RSA, to encrypt to the holder.
- Private key — the half you never share — the key used to sign and to
decrypt, and the only thing that proves identity.
- Diffie-Hellman — how strangers agree a secret in public — a key agreement
using modular exponentiation or elliptic curve point multiplication.
- Ephemeral — thrown away after one use — a per-connection key pair, giving
forward secrecy, marked by the
E in ECDHE.
32.4 The TLS handshake, in full#
PLAIN32.4.1 in simple words#
- Before any web page byte moves, the two machines have a short conversation
to set up the lock. That conversation is the handshake.
- It does four jobs: agree the version, agree the algorithms, prove the
server’s identity, and create the shared keys.
- In the old design this took two full round trips before you could send
your first HTTP request.
- A round trip is one message out and one message back. Over a long path
that costs real time, and the reader’s own path went from India to
Microsoft edge sites in Delhi, Mumbai and Pune.
- The current design, TLS 1.3, does the same four jobs in one round trip.
- It manages this by guessing. The client sends its key-exchange material in
the very first message, assuming the server will accept it. Almost always
the guess is right.
- There is also a mode with zero round trips, for repeat visits. It is
faster and it has a real weakness, described later in this section.
PLAIN32.4.2 a picture in your head#
- Think of two people meeting to do business, who have never met before.
- TLS 1.2 is the polite, slow version. “Hello, I speak English, French and
Hindi.” “Hello, let us use English.” “Here is my identity card.” “Here is
a number I have signed to prove the card is mine.” “Now here is my half of
the secret.” “Here is my half.” “Agreed, switching to code.” “Agreed.”
- Two complete there-and-back exchanges before a single word of business.
- TLS 1.3 is the version where you have done this a hundred times before and
you know how it will go. “Hello, I speak English, and here is my half of
the secret already, assuming you agree.” “Agreed, here is mine, here is my
card, here is my signature, and I am already speaking in code.”
- One there-and-back. The business starts on your second message.
Where this comparison breaks: people can recognize a face. Machines cannot,
so the identity card must be checked mathematically every single time. And
the “half of the secret” is not half of anything; it is a full public value
that is useless without the matching private one.
PLAIN32.4.3 a worked example#
- Here is a real TLS 1.2 handshake, captured live against
github.com. The
>>> lines were sent by us, the <<< lines came back.
>>> TLS 1.2, Handshake, ClientHello
<<< TLS 1.2, Handshake, ServerHello
<<< TLS 1.2, Handshake, Certificate
<<< TLS 1.2, Handshake, ServerKeyExchange
<<< TLS 1.2, Handshake, ServerHelloDone
>>> TLS 1.2, Handshake, ClientKeyExchange
>>> TLS 1.2, ChangeCipherSpec
>>> TLS 1.2, Handshake, Finished
<<< TLS 1.2, Handshake, NewSessionTicket
<<< TLS 1.2, ChangeCipherSpec
<<< TLS 1.2, Handshake, Finished
- Count the direction changes: out, in, out, in. That is two round trips
before application data.
- Now the same server, forced to TLS 1.3:
>>> TLS 1.3, Handshake, ClientHello
<<< TLS 1.3, Handshake, ServerHello
<<< TLS 1.3, Handshake, EncryptedExtensions
<<< TLS 1.3, Handshake, Certificate
<<< TLS 1.3, Handshake, CertificateVerify
<<< TLS 1.3, Handshake, Finished
>>> TLS 1.3, ChangeCipherSpec
>>> TLS 1.3, Handshake, Finished
- Out, in, out. One round trip, and the client can attach its HTTP request
to that last outbound flight.
- Notice what disappeared:
ServerKeyExchange, ServerHelloDone and
ClientKeyExchange are gone. Their work moved into the two hello
messages.
- Notice what appeared:
EncryptedExtensions and CertificateVerify. And
notice that from EncryptedExtensions onwards, everything is already
encrypted, including the server’s certificate.
- The single
ChangeCipherSpec still there in TLS 1.3 is a fake. It does
nothing. It is sent only so that old network middleboxes, which expected
to see one, do not break the connection.
PLAIN32.4.4 what is really happening inside#
- In
ClientHello the client sends: the highest version it supports, 32
random bytes, a session identifier, a list of cipher suites it will
accept, and a list of extensions.
- Two extensions matter enormously.
server_name carries the hostname, so
one address can serve many sites. supported_versions is how TLS 1.3
actually announces itself, because the old version field had to be frozen
at 1.2 for compatibility.
- In TLS 1.3 the client also sends
key_share: an actual ephemeral public
key, usually X25519, guessed in advance.
- In
ServerHello the server picks one cipher suite, sends its own 32
random bytes, and in TLS 1.3 sends its own key_share.
- At that instant both sides can compute the shared secret, so everything
after the
ServerHello can be encrypted. That is the core trick of
TLS 1.3.
Certificate carries the server’s certificate and the intermediates
needed to build a chain. In TLS 1.3 it is encrypted, so a passive
observer no longer learns which certificate was served.
CertificateVerify is a signature, made with the certificate’s private
key, over a hash of the entire handshake so far. This is the step that
proves the server actually holds the private key and is not just replaying
a copied certificate.
Finished is a MAC over the whole transcript, sent by each side. If any
earlier message was altered, the two transcripts differ and the check
fails. This is what protects the cipher negotiation from downgrade.
- In TLS 1.2 the equivalent of step 7 was
ServerKeyExchange, which carried
the server’s ephemeral DH parameters plus a signature over them. The
client then sent ClientKeyExchange with its own half.
TECHNICAL32.4.5 the engineer’s version#
- The version history, with real dates:
| Version |
Published |
Reference |
| SSL 2.0 |
Feb 1995 |
Netscape, no RFC |
| SSL 3.0 |
1996 |
RFC 6101 (2011) |
| TLS 1.0 |
Jan 1999 |
RFC 2246 |
| TLS 1.1 |
Apr 2006 |
RFC 4346 |
| TLS 1.2 |
Aug 2008 |
RFC 5246 |
| TLS 1.3 |
Aug 2018 |
RFC 8446 |
- SSL 1.0 was never released publicly; it had known flaws. SSL 2.0 shipped
in Netscape Navigator 1.1 in 1995. SSL 3.0 was a full redesign by Paul
Kocher with Phil Karlton and Alan Freier at Netscape. Taher Elgamal, then
Netscape’s chief scientist, is widely credited as a driving force behind
SSL.
- TLS 1.3 removed, deliberately: RSA key transport, static Diffie-Hellman,
custom DH groups, renegotiation, compression, CBC mode ciphers, all
non-AEAD ciphers, RC4, DSA, MD5 and SHA-1 signatures in the handshake, and
the change cipher spec protocol as a real message.
- TLS 1.3 defines only five cipher suites, and OpenSSL 3.0.13 on this
machine offers three of them:
TLS_AES_256_GCM_SHA384 Enc=AESGCM(256) Mac=AEAD
TLS_CHACHA20_POLY1305_SHA256 Enc=CHACHA20 Mac=AEAD
TLS_AES_128_GCM_SHA256 Enc=AESGCM(128) Mac=AEAD
- Reading a TLS 1.2 cipher suite string. Take
ECDHE-RSA-AES256-GCM-SHA384, which is what github.com actually
negotiated when forced to TLS 1.2 in the test above.
ECDHE is the key exchange: elliptic curve Diffie-Hellman, ephemeral.
RSA is the authentication: the certificate holds an RSA key used for the
signature. AES256 is the bulk cipher and its key size. GCM is the
mode, giving AEAD. SHA384 is the hash used by the key derivation
function and, in older suites, by the MAC.
- TLS 1.3 suite names dropped the key exchange and authentication parts
entirely, because those are now negotiated by separate extensions. That
is why
TLS_AES_128_GCM_SHA256 looks so short.
- Here are the two handshakes side by side.
TLS 1.2 - two round trips before data
Client Server
|-- ClientHello ------------------->|
|<------------------- ServerHello --|
|<------------------- Certificate --|
|<-------------- ServerKeyExchange -|
|<---------------- ServerHelloDone -|
|-- ClientKeyExchange ------------->|
|-- ChangeCipherSpec -------------->|
|-- Finished ---------------------->|
|<---------------- ChangeCipherSpec-|
|<------------------------ Finished-|
|-- HTTP GET ---------------------->|
TLS 1.3 - one round trip before data
Client Server
|-- ClientHello + key_share ------->|
|<-------- ServerHello + key_share -|
|<------------ EncryptedExtensions -| encrypted
|<-------------------- Certificate -| encrypted
|<-------------- CertificateVerify -| encrypted
|<---------------------- Finished --| encrypted
|-- Finished + HTTP GET ----------->| encrypted
- 0-RTT, also called early data. On a repeat visit the client has a
pre-shared key from a
NewSessionTicket sent during the previous
connection. It can encrypt application data with a key derived from that
ticket and put it in the very first flight, alongside the ClientHello.
- The caveat, stated plainly in RFC 8446 section 8 and appendix E.5: 0-RTT
data has no replay protection. An attacker who records the early-data
flight can send it again later, and the server may accept it a second
time.
- Also, 0-RTT data is not forward secret, because it is protected by keys
derived from the earlier ticket rather than by a fresh exchange.
- The practical rule: only allow 0-RTT for safe, idempotent requests. Never
for a
POST that moves money. Cloudflare and other providers restrict
early data to GET requests without query parameters by default.
WORDS32.4.6 remember these#
- Handshake — the setup conversation — the sub-protocol that negotiates
parameters, authenticates the peer and establishes keys.
- ClientHello — the opening message — carries versions, random bytes, cipher
suites, and extensions including
server_name and key_share.
- Cipher suite — the chosen set of algorithms — key exchange, authentication,
bulk cipher and hash, named as one string.
- key_share — the client’s guess at the key exchange — an ephemeral public
key sent in the first message so TLS 1.3 needs only one round trip.
- CertificateVerify — proof the server owns the key — a signature over the
handshake transcript using the certificate’s private key.
- Finished — the tamper check on the whole conversation — a MAC over the
full transcript that defeats downgrade attacks.
32.5 Certificates: what is actually in one#
PLAIN32.5.1 in simple words#
- A certificate is a small signed document that says: “this public key
belongs to this name”.
- It is not secret. It is sent to everyone who connects. There is nothing
private inside it.
- It contains a name, a public key, two dates, the name of whoever vouched
for it, and that voucher’s signature.
- The signature is the whole point. Without it the document is just a claim
anybody could type.
- Your browser does not trust the certificate because of what it says. It
trusts it because of who signed it, and because it can check that
signature mathematically.
- The format is called X.509. It is old, it is complicated, and every
browser and server on earth uses it.
PLAIN32.5.2 a picture in your head#
- Think of a passport.
- The photograph is the public key: the part that identifies you and can be
shown to anyone.
- Your face is the private key: the thing that must match the photograph,
that you carry with you, and that cannot be copied out of the book.
- The name page is the subject: who this passport is for.
- The issuing authority printed on the cover is the issuer: who vouched.
- The expiry date is the validity period.
- The holographic security printing is the signature: hard to forge, easy to
check, and it covers the whole book so nothing can be swapped.
Where this comparison breaks: a passport is checked by a human who can use
judgement. A certificate is checked by software that follows fixed rules and
has none. Also, a passport proves who you are. A certificate only proves you
control a name. It says nothing about whether you are honest, and section
32.7 makes a great deal of that difference.
PLAIN32.5.3 a worked example#
- Here is a real certificate, created with
openssl for this chapter. It is
for a home router, and it is used again in section 32.8.
- It was made with
openssl req -x509 for the authority, then
openssl req for the request, then openssl x509 -req -CA ca.crt to
sign. An extensions file supplied the modern half of the certificate:
subjectAltName=DNS:router.home,IP:192.168.0.1
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
- Now read it back. This is genuine
openssl x509 -text output, with the
long key and signature byte columns removed to fit the page:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 6EBE14595FF0AED0A54EF7E6925D84514B5421DC
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = IN, O = KedByte Home CA,
CN = KedByte Home Root CA
Validity
Not Before: Aug 13 02:27:52 2026 GMT
Not After : Sep 14 02:27:52 2027 GMT
Subject: CN = router.home
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:router.home, IP Address:192.168.0.1
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Key Identifier:
A4:68:C7:64:BF:C2:49:91:9E:73:CE:44:3C:86:19:70:
52:25:4B:3B
Signature Algorithm: sha256WithRSAEncryption
- Every one of those lines matters. Read them in order and the whole design
becomes obvious.
- Now the verification, which is the part people never actually try:
$ openssl verify router.crt
CN = router.home
error 20 at 0 depth lookup: unable to get local issuer
certificate
error router.crt: verification failed
$ openssl verify -CAfile ca.crt router.crt
router.crt: OK
- The same certificate failed and then passed. Nothing about the certificate
changed. Only the set of trusted issuers changed. That single fact is the
entire subject of section 32.6.
PLAIN32.5.4 what is really happening inside#
- A certificate is a structure written in a binary encoding called DER, then
often wrapped in base 64 text between
BEGIN CERTIFICATE and
END CERTIFICATE lines, which is called PEM format.
- The structure has three top-level parts: the body, the algorithm
identifier, and the signature bits.
- Signing means: take the exact bytes of the body, hash them with SHA-256,
and then apply the issuer’s private key operation to that hash.
- Verifying means: take the same body bytes, hash them the same way, and use
the issuer’s public key to check that the signature corresponds to that
hash.
- Because the signature covers the body bytes exactly, changing any single
character anywhere in the certificate breaks it. You cannot edit the
expiry date. You cannot add a hostname.
- The subject field is the old way of naming the site, using a Common
Name. It is dead. Since RFC 9525 of November 2023, the Common Name must
not be used for identity at all.
- The living way is the Subject Alternative Name extension, usually
shortened to SAN. It is a list, so one certificate can cover
example.com, www.example.com and *.api.example.com at once.
basicConstraints: CA:FALSE says this certificate may not sign other
certificates. This is what stops a normal website certificate from being
used to mint fake ones. It is checked, and it is critical.
TECHNICAL32.5.5 the engineer’s version#
- X.509 began in 1988 as part of the CCITT X.500 directory series. The
profile used on the internet is RFC 5280, May 2008, “Internet X.509 Public
Key Infrastructure Certificate and CRL Profile”.
- Field by field, the body of a certificate contains: version, serial
number, signature algorithm, issuer distinguished name, validity
notBefore and notAfter, subject distinguished name, subject public key
info, and the extensions.
- Extensions that matter in practice:
subjectAltName, basicConstraints,
keyUsage, extendedKeyUsage, authorityKeyIdentifier,
subjectKeyIdentifier, crlDistributionPoints,
authorityInfoAccess which carries the OCSP responder and issuer URLs,
certificatePolicies, and the signed certificate timestamp list used by
Certificate Transparency.
- Maximum lifetime is set by the CA/Browser Forum Baseline Requirements, not
by any RFC. It was 398 days for certificates issued on or after 1
September 2020.
- Ballot SC-081v3, passed in April 2025, sets a reducing schedule:
| From |
Max lifetime |
| until 14 Mar 2026 |
398 days |
| 15 Mar 2026 |
200 days |
| 15 Mar 2027 |
100 days |
| 15 Mar 2029 |
47 days |
- Domain validation reuse periods shrink on the same schedule, down to 10
days from March 2029. The direction of travel is short lifetimes and full
automation.
- Here is a real, unmodified root certificate from this machine’s trust
store, which you can check yourself on any Linux box:
$ openssl x509 -in /etc/ssl/certs/ISRG_Root_X1.pem \
-noout -subject -issuer -dates
subject=C = US, O = Internet Security Research Group,
CN = ISRG Root X1
issuer=C = US, O = Internet Security Research Group,
CN = ISRG Root X1
notBefore=Jun 4 11:04:38 2015 GMT
notAfter=Jun 4 11:04:38 2035 GMT
- Subject equals issuer. That is the definition of a self-signed
certificate, and every root in every trust store on earth is one.
- The honest version: the sandbox used to write this chapter sends all
outbound traffic through a company proxy that re-issues certificates.
So the live chain captured here shows that proxy as the issuer, not a
public authority. Section 32.9 uses that as the worked example, because
it is a perfect real specimen of interception rather than a flaw in the
method.
WORDS32.5.6 remember these#
- Certificate — a signed statement binding a name to a key — an X.509
version 3 structure profiled by RFC 5280.
- Subject — who the certificate is about — a distinguished name, now
effectively decorative for TLS identity.
- Issuer — who signed it — the distinguished name of the certificate
authority whose private key produced the signature.
- SAN — the real list of names this certificate covers — the Subject
Alternative Name extension, the only source of identity since RFC 9525.
- Validity — the two dates it works between —
notBefore and notAfter,
compared against the verifier’s own clock.
- basicConstraints — whether this may sign other certificates — a critical
extension carrying
CA:TRUE or CA:FALSE and an optional path length.
32.6 The chain of trust#
PLAIN32.6.1 in simple words#
- Your browser does not know
github.com. It has never heard of it.
- What your browser does know is a list of about 150 organizations it was
told to trust, which came with your operating system or your browser.
- Those are the root certificate authorities.
- A root does not sign website certificates directly. It signs a small
number of intermediate certificates.
- Intermediates sign the actual website certificates, called leaf
certificates.
- So the shape is: root signs intermediate, intermediate signs leaf, leaf
belongs to the website.
- When you connect, the server sends you the leaf and usually the
intermediate. Your browser already has the root.
- The browser then checks each signature in turn until it reaches something
in its own list. If it gets there, the chain is trusted. If it does not,
you get a warning.
PLAIN32.6.2 a picture in your head#
- Think of a university degree certificate.
- You have never met the person holding it. You cannot verify their study.
- But the degree is signed by a department head. The department head was
appointed by the university. The university is recognized by a national
body you already accept.
- You do not check the student. You check the chain of signatures upward
until you reach a body you already decided to accept, and then you stop.
- The national body is the root. The university is the intermediate. The
degree is the leaf.
- Now the important part: the national body keeps its own official seal in a
locked vault and almost never takes it out. It delegates day-to-day
signing to the universities.
Where this comparison breaks: you can choose not to accept a degree. Software
cannot exercise judgement, so the list of accepted bodies must be exactly
right. Also, a national body has one country. A root certificate authority is
trusted for every name on the internet, which is a very strange amount of
power for a private company to hold.
PLAIN32.6.3 a worked example#
- Here is a real chain, captured live in this sandbox. Read the
s: line as
“subject” and the i: line as “issuer”.
Certificate chain
0 s:CN = github.com
i:O = Anthropic,
CN = Egress Gateway SDS Issuing CA (production)
a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
v:NotBefore: Aug 6 23:49:45 2026 GMT;
NotAfter: Sep 5 23:50:45 2026 GMT
1 s:O = Anthropic,
CN = Egress Gateway SDS Issuing CA (production)
i:O = Anthropic,
CN = sandbox-egress-gateway-production Egress Gateway CA
2 s:O = Anthropic,
CN = sandbox-egress-gateway-production Egress Gateway CA
i:O = Anthropic,
CN = sandbox-egress-gateway-production Egress Gateway CA
- Read it as a ladder. Certificate 0 is the leaf for
github.com. Its
issuer is the subject of certificate 1. Certificate 1’s issuer is the
subject of certificate 2. Certificate 2 is its own issuer, so it is a
root.
- That is exactly the shape you want: leaf, intermediate, root.
- What is wrong with it is who those names are. That is section 32.9.
- On the reader’s own Mac at home, running the same command against
github.com, positions 1 and 2 would carry the name of a public
certificate authority instead, and position 2 would match a certificate
already in the system trust store.
- Here is the trust store on this machine, which is a Debian-family Linux:
$ ls /etc/ssl/certs/*.pem | wc -l
152
$ grep -c "BEGIN CERTIFICATE" \
/etc/ssl/certs/ca-certificates.crt
152
- And here are four real roots from it, with their real dates:
| Root |
Not before |
Not after |
| GlobalSign Root CA |
Sep 1998 |
Jan 2028 |
| Baltimore CyberTrust |
May 2000 |
May 2025 |
| DigiCert Global Root |
Nov 2006 |
Nov 2031 |
| ISRG Root X1 |
Jun 2015 |
Jun 2035 |
PLAIN32.6.4 what is really happening inside#
- Why intermediates exist at all, in four reasons.
- Protection. The root private key can be kept offline, in a hardware
module in a safe, powered up a few times a year under camera. The
intermediate key is online and doing daily work. If the online one is
stolen, you replace the intermediate, not the root.
- Recovery. You cannot fix a compromised root. It is baked into a
billion devices and updating them takes years. You can fix a compromised
intermediate in days.
- Constraint. An intermediate can be limited by policy or, in some
cases, by a name constraint, so it can only issue for certain domains.
- Now path building. The browser has a leaf and possibly some extra
certificates. It must find a route to a trusted root.
- It looks at the leaf’s issuer name and authority key identifier, and
searches: first the certificates the server sent, then its own store, then
any cached intermediates it has collected before.
- This is a search, not a straight line. There can be several valid paths.
A single intermediate is often cross-signed by two different roots so it
can be validated by both old and new devices.
- Once a candidate path is found, it validates every link: signature
correct, dates valid,
CA:TRUE present on every non-leaf,
keyUsage including certificate signing, path length not exceeded, and
the final certificate present in the trust store.
- A very common production bug: the server forgets to send the
intermediate. It works in a browser, because browsers cache intermediates
from earlier visits, and fails in
curl or in a Java service, which do
not. Always test with a fresh tool, not with your browser.
TECHNICAL32.6.5 the engineer’s version#
- Where the root store actually lives:
| System |
Location |
| macOS |
System Roots keychain |
| Windows |
Trusted Root cert store |
| Debian/Ubuntu |
/etc/ssl/certs |
| Firefox |
its own NSS store |
- On macOS, open Keychain Access and look at System Roots, or use
security find-certificate and
security dump-trust-settings from the terminal. The file behind it is
SystemRootCertificates.keychain under /System/Library/Keychains.
- On Windows, run
certlm.msc for the machine store or certmgr.msc for
the user store, and look under Trusted Root Certification Authorities.
Windows can also fetch roots on demand from Microsoft Update.
- On Debian and Ubuntu the source files live in
/usr/share/ca-certificates, local additions go in
/usr/local/share/ca-certificates, and update-ca-certificates rebuilds
/etc/ssl/certs/ca-certificates.crt. On Red Hat family systems the
directory is /etc/pki/ca-trust/source/anchors and the command is
update-ca-trust.
- Firefox has always shipped its own store, so a root added to the operating
system does not automatically work in Firefox. There is an enterprise
policy to make Firefox also read the system store.
- Chrome historically used the operating system store. It began shipping its
own Chrome Root Store in 2022, from Chrome 105 onwards, becoming the
default on Windows and macOS later that year. Treat the exact version
numbers as approximate; the direction is not in doubt.
- Who decides what goes in: the four root programmes are Mozilla, Microsoft,
Apple and Google. Each publishes its own policy. The CA/Browser Forum,
founded in 2005, publishes the Baseline Requirements that the programmes
reference.
- To get thrown out takes much less. Real examples: DigiNotar removed in
September 2011 after a breach, and bankrupt within the month. WoSign and
StartCom distrusted through 2016 and 2017 for backdating certificates and
misissuance. Symantec’s entire authority business distrusted by Chrome in
stages through 2018, with the business sold to DigiCert in 2017.
- What a certificate authority actually verifies before issuing, in three
levels.
- Domain Validation (DV). Proves only that the applicant controls the
domain right now. Methods, from the Baseline Requirements: put a specific
token at a specific path over HTTP, publish a specific DNS TXT record,
respond to email at
admin@ or the address in WHOIS, or answer a
TLS-ALPN challenge. Time taken: seconds, fully automated.
- Organisation Validation (OV). Adds a check that a legal entity of
that name exists, usually against a government register and a phone
directory. Time taken: hours to days.
- Extended Validation (EV). Adds documents, a lawyer’s or accountant’s
confirmation, and a callback. Guidelines published by the CA/Browser
Forum in 2007. Time taken: days to weeks.
- How weak is this really? Domain Validation, which is what almost every
site uses, proves control of DNS or of a web path. Anyone who can hijack
DNS or BGP for a few minutes can pass it. It says nothing about identity.
- The blunt summary: the modern web’s certificate system proves control of
a name. It stopped trying to prove who anybody is, because that turned
out not to work.
- Certificate Transparency, RFC 6962 of June 2013 and RFC 9162 of December
2021, is the real check today. Every publicly trusted certificate must be
logged to public append-only logs, and Chrome refuses certificates
without proof of logging. Domain owners can therefore see certificates
issued for their names by anyone.
WORDS32.6.6 remember these#
- Leaf — the website’s own certificate — the end-entity certificate with
CA:FALSE and the site’s names in its SAN list.
- Intermediate — the middle link — a subordinate CA certificate signed by a
root and used for day-to-day issuance.
- Root — the anchor of trust — a self-signed CA certificate present in the
verifier’s trust store, kept offline where possible.
- Trust store — your machine’s list of accepted anchors — an operating
system or application collection of root certificates.
- Path building — finding a route to a trusted anchor — the search through
supplied, stored and cached certificates for a valid chain.
- Certificate Transparency — public logging of every certificate — append-
only logs, mandatory for Chrome, defined in RFC 6962 and RFC 9162.
32.7 What the browser verifies, and what it does NOT#
PLAIN32.7.1 in simple words#
- When the padlock appears, the browser has checked a short and very
specific list.
- It checked the signature chain reaches a root it trusts.
- It checked today’s date sits between the two dates in the certificate.
- It checked the hostname you typed appears in the certificate’s list of
names.
- It checked that no certificate in the chain claims permissions it should
not have.
- It checked revocation, badly, and often not at all.
- That is the entire list. Now the other list.
- It did not check that the site is honest.
- It did not check that the company behind it is real, registered, or
solvent.
- It did not check that the content is safe, that the download is not
malware, or that the form is not a phishing page.
- It did not check that the name is the one you meant. A certificate
for
paypa1.com is perfectly valid; it just is not PayPal.
- The padlock has never meant “this site is safe”. It means “the pipe to
whoever owns this name is private”. Those are completely different
claims.
PLAIN32.7.2 a picture in your head#
- Think of a sealed pneumatic tube between your desk and an office
somewhere.
- The padlock is a guarantee about the tube. Nobody can read what goes
through it. Nobody can slip a different note in. The tube really does end
at the office with that name on the door.
- It is not a guarantee about the office. The people in that office may be
thieves. The name on the door may have been chosen last night to look like
a bank.
- A criminal can rent an office, put any name on the door, and order a
perfectly good sealed tube from the tube company. The tube company only
checks that the criminal really does control that door.
- And that is exactly what happens: phishing sites almost all use HTTPS now,
because certificates are free and automated.
Where this comparison breaks: a physical office costs money and can be
raided. A domain costs a few dollars, is registered in minutes, and can be
abandoned the moment it is reported. The economics are entirely different,
which is why the “green padlock means safe” idea was retired.
PLAIN32.7.3 a worked example#
- Take a real user story. Someone receives a message about a parcel and
opens a link.
- The address bar shows a padlock. There is no warning. The certificate is
valid, issued minutes earlier by a free automated authority.
- Every browser check passes: chain to a trusted root, dates fine, hostname
matches the SAN entry exactly,
CA:FALSE, correct key usage.
- The page is a copy of a courier company’s site, asking for a card number.
- TLS did its job perfectly. It delivered the card number, encrypted and
untampered, straight to the criminal.
- Now contrast that with what the browser actually would have blocked: an
attacker sitting on the cafe wireless trying to intercept the real courier
site. That attacker cannot get a certificate for the real name, so the
browser stops him.
- Two different threats. TLS solves one of them completely and the other one
not at all.
- Modern browsers do have a defence against the phishing case, but it is a
separate system: Google Safe Browsing, Microsoft SmartScreen and similar
reputation lists. That is content safety, not transport security, and it
works on a blocklist rather than on mathematics.
PLAIN32.7.4 what is really happening inside#
- The hostname check is more subtle than people think.
- The browser takes the name from the URL, not from DNS. If you typed
github.com, the answer must contain github.com, whatever address the
connection went to.
- It compares against the SAN list only. The Common Name is ignored by
modern clients.
- Wildcards match one label only, and only at the leftmost position.
*.example.com matches api.example.com but not example.com itself and
not a.b.example.com.
- The date check uses the client’s own clock. A device with a badly wrong
clock reports every certificate as expired or not yet valid. This is a
very common cause of “all HTTPS is broken” on cheap devices and on
machines whose battery died.
TECHNICAL32.7.5 the engineer’s version#
- The full check list a conforming client performs, per RFC 5280 and
RFC 9525:
| Check |
Enforced |
| Chain to trusted root |
strictly |
| Signature on each link |
strictly |
| notBefore / notAfter |
strictly |
| Hostname against SAN |
strictly |
| basicConstraints CA |
strictly |
| keyUsage, extKeyUsage |
strictly |
| Name constraints |
strictly |
| Certificate Transparency |
Chrome: yes |
| Revocation |
weakly or not |
- CRL, Certificate Revocation List. The authority publishes a signed
list of revoked serial numbers. Problem: the list grows without bound,
downloading it is slow, and it may be hours or days out of date.
- OCSP, Online Certificate Status Protocol, RFC 6960 of June 2013. The
client asks the authority about one specific serial number. Problems: it
adds a network round trip to a third party during page load; it tells the
authority which sites you visit, which is a privacy leak; and if the
responder is unreachable, clients “soft-fail” and continue anyway.
- Soft-fail is the fatal flaw. An attacker who can intercept your connection
can also block your OCSP query, and then the check silently passes. A
check an attacker can turn off is not a check.
- OCSP stapling, the
status_request extension from RFC 6066. The
server fetches its own signed OCSP response periodically and staples it
into the handshake. This fixes the privacy leak and the extra round trip.
It does not fix soft-fail, because a server can simply not staple.
- In 2024 and 2025 Let’s Encrypt withdrew from OCSP entirely for privacy and
cost reasons. It announced the plan in December 2024, dropped OCSP URLs
from certificates on 7 May 2025, and switched off its OCSP responders on
6 August 2025. It relies on CRLs and on short lifetimes instead.
- Short-lived certificates are the real direction of travel. If a
certificate only lives days, revocation barely matters, because expiry
does the job.
- Let’s Encrypt issued its first six-day certificate on 19 February 2025.
Combined with the CA/Browser Forum schedule that takes the maximum to 47
days in March 2029, the industry answer to broken revocation is simply to
make certificates expire faster than problems can spread.
- Where experts disagree: some argue hard-fail revocation should have been
mandated from the start and that soft-fail was a failure of nerve. Others
argue hard-fail would have made the web fragile and dependent on
authority uptime, and that short lifetimes are the better engineering
answer. Both positions are held by serious people.
WORDS32.7.6 remember these#
- Padlock — the browser’s “connection is private” mark — an indicator that
chain, dates, hostname and constraints validated; nothing more.
- Hostname verification — the name you typed is in the certificate — a match
against
subjectAltName entries per RFC 9525.
- Revocation — cancelling a certificate before expiry — publishing its
serial as invalid via CRL or OCSP.
- OCSP — asking the authority about one certificate — an online status
query, RFC 6960, usually soft-failing and now being retired.
- Stapling — the server carries its own status proof — the
status_request
TLS extension, removing the client’s third-party lookup.
- Soft-fail — carrying on when the check does not answer — the default
behaviour that makes revocation checking defeatable by an attacker.
32.8 Self-signed certificates and the reader’s own router#
PLAIN32.8.1 in simple words#
- A self-signed certificate is one where the subject and the issuer are
the same. The key vouches for itself.
- Nobody else has checked anything. There is no chain. There is nothing to
follow upward.
- So the browser cannot answer the question “does this reach a root I
trust”, and it stops and warns you.
- The warning is not saying the encryption is weak. The encryption is
exactly as strong as any other TLS connection.
- The warning is saying: I cannot tell you who is at the other end.
- That is the honest message. Everything else follows from it.
- The reader’s home router lives at
192.168.0.1. Opening it in a browser
over HTTPS produces exactly this warning, on every home router ever made.
PLAIN32.8.2 a picture in your head#
- Somebody hands you a business card that says “I am the manager”.
- The card was printed by that same person. There is no company logo, no
reference you can call.
- If you are standing inside your own house and the person handing it over
is the box on your own shelf that you plugged in yourself, the card tells
you nothing you did not already know, and you do not need it.
- If you are on a street in another city and a stranger hands you the same
self-printed card claiming to be your bank manager, the card is worthless
and the situation is alarming.
- Same card. Completely different meaning, because of where you are standing
and what else you know.
Where this comparison breaks: you can see a person and a shelf. You cannot
see a network path. The reason the router case is safe is not that it feels
close, but that the path is provably short, which the next block explains.
PLAIN32.8.3 a worked example#
- Take the reader’s real setup. The default gateway is
192.168.0.1. The
Mac is on the same wireless link as that box.
- The certificate the router presents is self-signed. Here is the shape of
one, generated for this chapter:
Subject: CN = router.home
Issuer : CN = KedByte Home Root CA
SAN : DNS:router.home, IP Address:192.168.0.1
- Now ask the only question that matters: could anything be sitting between
the Mac and the router, pretending to be the router?
- To do that, an attacker must already be on the reader’s own wireless
network or inside the flat with a cable. They would need the wireless
password, or physical access.
- But if an attacker is already on the local network with the wireless
password, they can attack the router directly. Accepting the certificate
warning does not make that worse.
- So the risk added by clicking through, in this one situation, is close to
zero. The endpoint is known, the path is one hop, and the attacker who
could exploit it already has better options.
- Now change one thing. The reader is on cafe wireless, types a bank
address, and the same warning appears.
- Here the path is long, the endpoint is unknown, and the warning is
consistent with exactly one thing: somebody on the path is terminating the
connection and cannot produce a real certificate.
- That is the definition of an active interception attempt in progress.
Clicking through hands them everything.
- The asymmetry in one line: on the router you already know the endpoint,
so the certificate adds nothing. On a public site the certificate is the
only thing that identifies the endpoint, so ignoring it removes
everything.
PLAIN32.8.4 what is really happening inside#
- Clicking “proceed anyway” does not weaken the encryption. The session is
still AES-GCM with an ephemeral key exchange.
- What it removes is the authenticity property from section 32.2. You keep
two of the three guarantees and throw away the one that made them useful
against an active attacker.
- Doing it properly instead has two options, and both remove the warning
without removing the guarantee.
- Option one: trust that one certificate. Export the router’s
certificate once, check its fingerprint over a channel you trust, and add
it to your machine’s trust store as an anchor.
- Option two: run a tiny private authority. Create your own root once,
add that root to your machines, and issue certificates for every device on
your network from it.
- Option two scales. Option one does not, but for one router it is fine.
- Either way you have made a deliberate, narrow decision, instead of
teaching yourself to click through warnings, which is the real long-term
harm.
TECHNICAL32.8.5 the engineer’s version#
- A self-signed certificate for an IP address must carry that address as an
IP Address entry in the SAN extension. A DNS entry with the same digits
does not match, and modern clients will reject it.
- Adding a private root, per platform:
| System |
Command or place |
| macOS |
security add-trusted-cert |
| Debian |
/usr/local/share/ca-certificates |
| RHEL |
/etc/pki/ca-trust/source/anchors |
| Firefox |
Settings, View Certificates |
- Understand what you have done: a root you add is trusted for every name
on the internet, not just your router. A private root whose key is on a
laptop that gets stolen is a total compromise of that laptop’s browsing.
Use
nameConstraints if your tooling supports it.
- Certificate pinning narrows trust in the other direction. The
application refuses any certificate except a specific key it was built to
expect, ignoring the trust store entirely.
- On the web, pinning was standardized as HTTP Public Key Pinning, RFC 7469
of April 2015. It was a failure: a wrong pin bricked your own site for
months with no recovery. Chrome removed support in version 72, early 2019.
Do not use it.
- HSTS, HTTP Strict Transport Security, RFC 6797 of November 2012, is
the mechanism that survived. A response header tells the browser to use
HTTPS for this host for a period, and to refuse to let the user click
through certificate warnings at all.
- Here is a real one, from the
github.com response captured earlier in
this chapter:
strict-transport-security: max-age=31536000;
includeSubDomains; preload
max-age=31536000 is one year in seconds. includeSubDomains extends it
to every subdomain. preload is a request to be added to a list that
ships inside the browser itself.
- The preload list matters because HSTS normally needs one successful HTTPS
visit before it can protect you. Preloading closes that first-visit gap
by baking the rule into the browser binary.
- HSTS is also why you cannot click through a warning on many major sites,
and that is deliberate.
WORDS32.8.6 remember these#
- Self-signed — the key vouches for itself — subject equals issuer, no chain
to any external anchor.
- Trust anchor — a certificate you accept without proof — a root installed
in a trust store, trusted for all names unless constrained.
- Private CA — your own small authority — a locally generated root used to
issue certificates for internal hosts.
- Pinning — accept only one specific key — an application-level restriction
that bypasses the trust store entirely.
- HSTS — always use HTTPS for this host — the
Strict-Transport-Security
header from RFC 6797, which also blocks warning bypass.
- Preload — the rule ships inside the browser — inclusion in the browser’s
built-in HSTS list, protecting the very first visit.
32.9 When TLS is deliberately intercepted#
PLAIN32.9.1 in simple words#
- Everything in this chapter says a man in the middle cannot read your
traffic, because he cannot produce a trusted certificate.
- There is one way around that: put a certificate authority of your own into
the machine’s trust store.
- Once that root is trusted, the middlebox can mint a certificate for any
site on demand, and the browser will accept it happily.
- This is not a bug. It is the system working exactly as designed. Trust
stores are lists, and whoever controls the list controls the trust.
- Employers do this on company laptops. Antivirus products do it on home
machines. Developers do it on purpose with debugging tools.
PLAIN32.9.2 a picture in your head#
- Imagine your office post room is instructed to open every sealed envelope,
read it, photocopy it, put the contents in a fresh envelope, and reseal it
with a company seal.
- You were also given, on your first day, a stamp that says company seals
are just as good as official ones.
- So every letter you receive looks correctly sealed. You have no way to
tell, from the seal alone, that it was opened, unless you look closely at
whose crest is on it.
- That closer look is exactly what the next block teaches.
Where this comparison breaks: a post room reads letters slowly. A TLS
middlebox reads everything, all day, for thousands of people, and stores it.
And unlike a post room, it can also modify content on the way through.
PLAIN32.9.3 a worked example#
- This is real. The sandbox in which this chapter was written routes all
outbound traffic through a company proxy.
- Here is the actual command and the actual first lines of output when
connecting to
github.com:
openssl s_client -connect github.com:443 \
-servername github.com </dev/null
depth=2 O = Anthropic,
CN = sandbox-egress-gateway-production Egress Gateway CA
verify return:1
depth=1 O = Anthropic,
CN = Egress Gateway SDS Issuing CA (production)
verify return:1
depth=0 CN = github.com
verify return:1
...
Verification: OK
Verify return code: 0 (ok)
- Read it slowly. The subject at depth 0 is
github.com. Correct.
- The issuer, at depth 1, is not a public certificate authority. It is the
proxy. The root at depth 2 is the proxy’s own root.
- And yet the last two lines say
OK. Verification passed, because that
root was installed into the machine’s trust store on purpose.
- Notice the validity dates on that leaf, from the same output:
NotBefore: Aug 6 23:49:45 2026 and NotAfter: Sep 5 23:50:45 2026.
Exactly 30 days, minted by the box, not by GitHub.
- If this were the reader’s Mac at home, the issuer line would name a public
authority instead. One line of output is the whole diagnosis.
PLAIN32.9.4 what is really happening inside#
- The middlebox sits in the connection path. It sees the
ClientHello and
reads the server name extension.
- It opens its own TLS connection outward to the real server, and validates
that one properly, using the real public trust store.
- It then generates a certificate for the requested name, signs it with its
own private authority key, and completes a second, separate TLS handshake
back towards you.
- Two connections, joined in the middle, with the plaintext exposed at the
join. That join is where scanning, logging or blocking happens.
- Anything you type goes through that plaintext point: passwords, session
cookies, private messages, source code.
- Certificate pinning is the one thing that breaks this. An application that
only accepts its own key refuses the forged certificate and fails
loudly. That is why some mobile applications stop working on corporate
networks.
TECHNICAL32.9.5 the engineer’s version#
- Categories of deliberate interception you will actually meet:
| Kind |
Who runs it |
Typical issuer text |
| Corporate proxy |
employer |
company name, “SSL” |
| Antivirus scan |
local software |
product name |
| Debug proxy |
you |
mitmproxy, Charles |
| Captive portal |
venue |
portal or gateway name |
- Antivirus TLS scanning did the same thing on consumer machines. It has a
poor security record; the 2017 paper “The Security Impact of HTTPS
Interception” by Durumeric and others found many interceptors downgraded
the outward connection to weaker cryptography than the browser would have
used.
- Debugging proxies are the honest version:
mitmproxy, Charles Proxy,
Fiddler and Burp Suite all install a local root and are indispensable for
development. You install the root yourself and you should remove it when
done.
- Captive portals are not usually interception in this sense. They intercept
plain HTTP and DNS to redirect you to a login page, and they cannot
intercept HTTPS without a warning. That is exactly why an HTTPS-only
device often fails to show the portal page at all.
- How to detect it, in order of effort:
openssl s_client -connect github.com:443 \
-servername github.com </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -dates
- If the issuer is not a public authority you recognize, your traffic is
being terminated somewhere. Compare with the same command from a phone on
mobile data.
- Certificate Transparency helps here too: an intercepting certificate is
not logged publicly, whereas a genuine one must be. Chrome enforces this
only for publicly trusted roots, and deliberately exempts locally
installed roots, precisely so corporate interception keeps working.
- Say this honestly: in many workplaces this is normal, disclosed in the
acceptable use policy, and legal. It is also a genuine risk, because it
concentrates every employee’s plaintext at one appliance, and appliances
get breached.
WORDS32.9.6 remember these#
- TLS interception — somebody opens and re-seals your traffic — an on-path
proxy terminating and re-originating TLS with a locally trusted root.
- Secure web gateway — the corporate box doing it — an appliance performing
inspection, filtering and data loss prevention.
- Locally installed root — a trust anchor added by an administrator — a CA
certificate placed in the machine store, exempt from Certificate
Transparency enforcement.
- mitmproxy — a developer’s interception tool — an open-source intercepting
proxy used for debugging and testing.
- Captive portal — the login page on public wireless — a redirect of plain
HTTP and DNS, which cannot transparently intercept HTTPS.
32.10 HTTP versions: 1.1, 2 and 3#
PLAIN32.10.1 in simple words#
- HTTP/1.0 opened a fresh TCP connection for every single file. A page with
40 images meant 40 connections.
- HTTP/1.1 added keep-alive: reuse one connection for many requests.
That was a huge saving.
- But on one HTTP/1.1 connection the answers must come back in the order the
questions were asked.
- So one slow file blocks everything queued behind it. That is called
head-of-line blocking.
- HTTP/2 fixed it properly by chopping everything into small labelled
pieces, so many exchanges share one connection and interleave.
- But HTTP/2 still runs on TCP, and TCP itself insists on delivering bytes
in order. So one lost packet still stalls every stream.
- HTTP/3 fixed that by abandoning TCP and building a new transport on UDP,
called QUIC, which keeps streams genuinely independent.
PLAIN32.10.2 a picture in your head#
- HTTP/1.1 is a single-lane road. Cars arrive in order. One breakdown stops
the lane.
- HTTP/2 paints six lanes on that road. Cars can overtake. But the road is
still one bridge, and if the bridge is blocked, all six lanes stop.
- HTTP/3 replaces the bridge with six separate bridges. One collapsing
affects only its own lane.
Where this comparison breaks: the HTTP/2 lanes are not physical; they are
labels inside one byte stream. And the HTTP/3 bridges are still one UDP flow,
sharing congestion control; only loss recovery is separated.
PLAIN32.10.3 a worked example#
- A page needs one HTML file, one stylesheet and eight images: ten items.
- Round trip time to the server is 100 milliseconds.
- Under HTTP/1.1 with six connections and keep-alive: six handshakes, then
ten requests spread over six lanes, roughly 400 milliseconds.
- Under HTTP/2 on one connection: one handshake, all ten requests sent at
once, answers interleaved, roughly 250 milliseconds.
- Under HTTP/3: same as HTTP/2, plus setup folded into one round trip, plus
no stall if a packet is lost, so the worst case improves most.
PLAIN32.10.4 what is really happening inside#
- HTTP/2 is binary. Every message becomes frames with a type, a length
and a stream identifier.
- Because each frame carries its stream number, frames from different
requests can be interleaved on one connection and reassembled at the far
end. That is multiplexing.
- Headers repeat enormously between requests on one page. HTTP/2 compresses
them with HPACK, which keeps a shared table of previously seen headers so
the second request sends an index instead of the text.
- Server push let the server send a file you had not asked for, guessing
you would need it. It sounded good and failed in practice: servers pushed
things browsers already had cached, wasting bandwidth.
- QUIC moves the reliability, ordering and encryption out of the kernel and
into the application. It runs on UDP because UDP is the only transport
that middleboxes will pass without inspecting.
- QUIC merges the transport handshake and the TLS 1.3 handshake into one, so
a new connection is one round trip, and a resumed one can be zero.
- Each QUIC stream has its own delivery order. Losing a packet belonging to
stream 3 does not hold up stream 7. That is the head-of-line fix.
- Connection migration: a QUIC connection is identified by a connection
ID, not by the four-part address pair. Move from wireless to mobile data
and the connection survives with a different IP address.
- It had to live in user space because changing TCP means changing every
operating system kernel and every middlebox on earth, which takes decades.
Shipping a library inside a browser takes weeks.
TECHNICAL32.10.5 the engineer’s version#
| Version |
Year |
Transport |
Framing |
| HTTP/1.0 |
1996 |
TCP |
text |
| HTTP/1.1 |
1997/1999 |
TCP |
text |
| HTTP/2 |
2015 |
TCP + TLS |
binary |
| HTTP/3 |
2022 |
QUIC on UDP |
binary |
- Specifications: HTTP/1.0 is RFC 1945, May 1996. HTTP/1.1 was RFC 2068
January 1997 and RFC 2616 June 1999, now replaced by RFC 9112, June 2022.
- HTTP/2 is RFC 7540, May 2015, now RFC 9113, June 2022. HPACK header
compression is RFC 7541, May 2015.
- QUIC is RFC 9000, May 2021, with TLS integration in RFC 9001 and loss
recovery in RFC 9002. HTTP/3 is RFC 9114, June 2022, with QPACK header
compression in RFC 9204.
- HTTP/2 requires TLS in every browser implementation, though the
specification permits cleartext. The ALPN token is
h2; for HTTP/3 it is
h3.
- Server push was defined in RFC 7540 and disabled by default in Chrome 106
in 2022. Google reported that only about 1.25 percent of HTTP/2 sites used
it, with no clear net performance gain. The replacement is the 103 Early
Hints status code, RFC 8297, December 2017.
- HTTP/3 is discovered, not requested. The server advertises it with an
Alt-Svc response header or an HTTPS DNS record, and the client upgrades
on a later connection.
- Costs of QUIC: encryption and reassembly happen in user space, so CPU cost
per byte is higher than kernel TCP, and some networks rate-limit or block
UDP port 443 entirely. Clients therefore always keep a TCP fallback.
WORDS32.10.6 remember these#
- Keep-alive — reuse one connection for many requests — persistent
connections, default in HTTP/1.1.
- Head-of-line blocking — one slow item stalls the rest — an in-order
delivery constraint at the HTTP or TCP layer.
- Multiplexing — many exchanges on one connection — independent streams
identified by stream ID in HTTP/2 frames.
- HPACK — squeeze repeated headers — the HTTP/2 header compression format,
RFC 7541, using static and dynamic tables.
- QUIC — a new transport on UDP — RFC 9000, providing streams, reliability
and TLS 1.3 in user space.
- Connection migration — keep the session when your address changes — QUIC’s
use of connection IDs instead of address pairs.
32.11 Cookies, sessions, and CORS#
PLAIN32.11.1 in simple words#
- HTTP has no memory. Each request stands alone. The server does not know
the last request came from you.
- That was deliberate, and it is why the web scaled. But logging in needs
memory.
- A cookie is the fix. The server sends a small piece of text. The
browser stores it and sends it back on every later request to that site.
- That is all a cookie is: a string the browser repeats back automatically.
- A session is what the server does with it. Usually the cookie holds a
random identifier and the server keeps the real data.
- Authentication is proving who you are, once. Session management is
staying recognized afterwards. They are different jobs and are attacked
differently.
PLAIN32.11.2 a picture in your head#
- You check into a hotel. At the desk you show your passport once. That is
authentication.
- They hand you a plastic key card with no name on it. That is the session
cookie.
- From then on, no one asks who you are. The card opens the door. That is
session management.
- Anyone who finds your card in the corridor can open your room, without
ever seeing your passport.
- The card stops working at checkout time, or if the desk cancels it. That
is session expiry and revocation.
Where this comparison breaks: a card must be physically carried. A cookie is
copied invisibly by any script that can read it, which is why HttpOnly
exists to hide it from scripts entirely.
PLAIN32.11.3 a worked example#
- Here are three real
Set-Cookie headers, captured live from github.com
in this chapter. The long session value has been shortened:
set-cookie: _gh_sess=Xkj3u...JWODdiSIORH1F; path=/;
HttpOnly; secure; SameSite=Lax
set-cookie: _octo=GH1.1.1717559084.1786588114;
expires=Fri, 13 Aug 2027 02:28:34 GMT;
domain=.github.com; path=/; secure; SameSite=Lax
set-cookie: logged_in=no;
expires=Fri, 13 Aug 2027 02:28:34 GMT;
domain=.github.com; path=/; HttpOnly; secure;
SameSite=Lax
_gh_sess has no expires, so it is a session cookie: it dies when the
browser closes.
_octo has an expiry one year ahead, so it is persistent and survives
restarts.
domain=.github.com widens it to every subdomain. Leaving domain out,
as _gh_sess does, restricts it to exactly the host that set it, which is
the safer default.
path=/ means send it for every path on the site.
secure means never send this over plain HTTP.
HttpOnly means JavaScript cannot read it. That is what limits the damage
of a cross-site scripting bug.
SameSite=Lax means do not send this cookie on cross-site requests except
top-level navigations. That is the main defence against cross-site request
forgery.
- Note that
logged_in=no is HttpOnly too. Even trivial state gets the
protection, because it is free.
PLAIN32.11.4 what is really happening inside#
- Mechanically: the server sends
Set-Cookie in a response header. The
browser stores name, value and attributes in a jar keyed by domain and
path.
- On every later request the browser looks up matching cookies and sends
them in a single
Cookie request header, without asking anyone.
- Session cookie versus token. A session cookie is an opaque identifier;
the server holds the state and can revoke instantly. A token, such as a
signed JWT, carries the claims inside itself; the server needs no lookup
but also cannot easily revoke it before expiry.
- Local storage is a separate browser store that JavaScript reads and
writes explicitly. It is never sent automatically, so it is immune to
cross-site request forgery, and fully exposed to cross-site scripting
because scripts can read it. Neither store is simply safer than the other.
- Now the same-origin policy. An origin is the triple of scheme, host
and port.
https://a.com and https://a.com:8443 are different origins.
So are http://a.com and https://a.com.
- By default a page may send a request to another origin, but its JavaScript
may not read the response. That rule, from Netscape Navigator 2.0 in 1995,
is what stops a random page reading your webmail.
- CORS is the controlled exception. The receiving server opts in, using
response headers, to let a named origin read the answer.
- For anything beyond a simple request, the browser first sends a
preflight: an
OPTIONS request asking whether the real one is
allowed.
- This is the key point that confuses everyone: the server almost always
processed the request perfectly. The browser then refused to hand the
response to the page. So the error appears only in the browser console,
and the server log shows a normal 200.
- It follows that
curl never sees a CORS error, and that adding headers
on the client cannot fix one. Only the server can grant permission.
TECHNICAL32.11.5 the engineer’s version#
- Cookies were invented by Lou Montulli at Netscape in 1994. The current
specification is RFC 6265, April 2011, with
SameSite and other modern
rules in the long-running RFC 6265bis work.
- Attributes and what they do:
| Attribute |
Effect |
| Domain |
widens to subdomains |
| Path |
limits by URL prefix |
| Expires / Max-Age |
persistent, not session |
| Secure |
HTTPS only |
| HttpOnly |
hidden from JavaScript |
| SameSite |
cross-site sending rule |
SameSite values: Strict never sends cross-site, Lax sends only on
top-level GET navigations, None always sends and requires Secure.
Chrome made Lax the default in Chrome 80, rolled out during 2020.
- Here is a real preflight and its answer, captured from a small local
server written for this chapter:
OPTIONS /api/profile HTTP/1.1
Host: api.kedbyte.test:8099
Origin: https://app.kedbyte.test
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.kedbyte.test
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Content-Length: 0
Access-Control-Max-Age: 600 lets the browser cache that permission for
ten minutes, so it does not preflight every call.
- A request avoids preflight only if it is
GET, HEAD or POST, uses no
custom headers, and has a Content-Type of
application/x-www-form-urlencoded, multipart/form-data or
text/plain. Sending JSON triggers a preflight, which surprises everyone
building an API.
- To send cookies cross-origin you need
credentials: "include" on the
client and, on the server, Access-Control-Allow-Credentials: true plus
an exact origin. The wildcard * is rejected with credentials, by design.
WORDS32.11.6 remember these#
- Cookie — a string the browser repeats back — a name-value pair with
attributes, stored per domain and path, RFC 6265.
- Session — being recognized after login — server-side state referenced by
an opaque identifier in a cookie.
- HttpOnly — hidden from page scripts — a cookie attribute blocking
document.cookie access, limiting XSS damage.
- SameSite — the cross-site sending rule —
Strict, Lax or None,
the main defence against cross-site request forgery.
- Origin — scheme plus host plus port — the security boundary of the
same-origin policy.
- Preflight — asking permission before the real request — an
OPTIONS
request with Access-Control-Request-Method.
- CORS — a server opting in to cross-origin reads — response headers telling
the browser to release the response to a named origin.
32.12 The whole journey, assembled#
PLAIN32.12.1 in simple words#
- This section answers the classic question in full: what happens when you
type a web address and press Enter.
- It uses the reader’s own machine, the reader’s own router at
192.168.0.1, the reader’s own resolver at 1.1.1.1, and the address
github.com resolved to 20.207.73.82.
- Everything in Part F of this book meets here in one list.
- The reader’s real session did not finish this journey. It stopped at a
precise step, and the list below says exactly which one.
PLAIN32.12.2 a picture in your head#
- Think of posting a letter to a company in another country.
- You look up their address in a directory. That is DNS.
- You walk to the post box, which is the only exit from your building. That
is the default gateway.
- The letter changes hands between postal services, none of which reads it.
That is routing.
- At the far end, a receptionist confirms the company’s identity to you
before you hand over anything valuable. That is the TLS handshake.
Where this comparison breaks: a letter goes once. A web page needs dozens of
these exchanges, most of them to other companies entirely, and they overlap.
PLAIN32.12.3 a worked example#
- The complete walkthrough. Times are realistic for the reader’s connection.
key press -> DNS -> TCP -> TLS -> HTTP -> paint
- Step 1. A key is pressed. The keyboard sends a scan code; the operating
system turns it into a character and gives it to the browser.
- Step 2. The browser’s address bar decides: is this a search or a URL. A
dot and no spaces means URL.
- Step 3. The host name is normalized: lowercased, and if it contains
non-ASCII it is converted to Punycode.
- Step 4. The browser checks its HSTS list.
github.com is preloaded, so
even http:// would be rewritten to https:// before anything is sent.
- Step 5. The browser checks its own HTTP cache. A fresh copy would end the
story here with no network at all.
- Step 6. The browser checks whether it already has an open connection to
this host. Reusing one skips steps 8 to 26 entirely.
- Step 7. Name resolution starts. The browser’s own DNS cache is checked.
- Step 8. Miss. The Mac sends a DNS query to the configured resolver,
1.1.1.1, not to the router. This is the reader’s real configuration.
- Step 9. To send that packet the Mac needs a next hop.
1.1.1.1 is not
on the local subnet, so the route table selects the default gateway,
192.168.0.1.
- Step 10. The Mac needs the gateway’s hardware address, so it sends an ARP
request for
192.168.0.1 and caches the reply.
- Step 11. The resolver at
1.1.1.1 answers, from cache or by walking the
root, then .com, then GitHub’s own name servers.
- Step 12. The answer arrives:
github.com is 20.207.73.82, with a time
to live saying how long it may be cached.
- Step 13. That address is in a Microsoft-owned range. GitHub has been
owned by Microsoft since 2018 and fronts traffic through Microsoft’s
network edge.
- Step 14. IPv6 was reported as none on this connection, so there is no
happy-eyeballs race between families. IPv4 only.
- Step 15. The browser opens a socket and starts a TCP connection to
20.207.73.82 port 443.
- Step 16. The SYN goes to the gateway,
192.168.0.1, which is hop 1 of
the reader’s traceroute.
- Step 17. Hop 3,
137.97.29.249, is a public address on the same ISP.
- Step 18. Several hops showed two or three addresses. That is per-flow
load balancing across parallel links, not an error.
- Step 19. This is where the reader’s session stopped. The SYN went out
and nothing came back. No SYN-ACK, no reset, no ICMP unreachable.
Silence, then a 15 second timeout in
curl.
- Step 20. What that proves: the connection never opened, so not one byte
of TLS or HTTP was ever attempted. Steps 27 onwards did not happen on
that day.
- Step 21. In the normal case the server replies SYN-ACK and the client
sends ACK. One round trip. The connection is open.
- Step 22. TLS begins. The client sends
ClientHello with its versions,
random bytes, cipher suites, the server_name extension carrying
github.com, ALPN offering h2 and http/1.1, and a key_share.
- Step 23. The server replies
ServerHello with its chosen suite and its
own key_share. Both sides now compute the shared secret.
- Step 24. Everything after this point is encrypted, including the
certificate.
- Step 25. The server sends
EncryptedExtensions, Certificate,
CertificateVerify and Finished.
- Step 26. The browser builds a chain from the leaf through the
intermediate to a root in its trust store.
- Step 27. It checks dates,
basicConstraints, key usage, and that
github.com appears in the certificate’s SAN list.
- Step 28. It checks Certificate Transparency proofs, and revocation, the
latter weakly.
- Step 29. It verifies
CertificateVerify, which proves the server holds
the private key right now.
- Step 30. The client sends its own
Finished. The handshake is complete
in one round trip. Measured here, about 0.22 seconds.
- Step 31. ALPN selected
h2, so the browser speaks HTTP/2 and sends its
request as compressed binary frames.
- Step 32. The request may not reach GitHub’s own servers at all. It may be
answered by a load balancer or CDN edge that terminated TLS. Section
32.13.
- Step 33. The response headers come back:
200 OK, content-type,
strict-transport-security, set-cookie, content-security-policy.
- Step 34. The body streams in, usually compressed with gzip or brotli, and
is decompressed on arrival.
- Step 35. The HTML parser starts building the DOM tree while bytes are
still arriving.
- Step 36. It meets links to stylesheets, scripts, fonts and images, and
issues those requests immediately over the same connection.
- Step 37. DOM and CSSOM combine into a render tree of things that will
actually be drawn.
- Step 38. Layout computes the exact box and position of every element.
- Step 39. Paint fills in pixels per layer, and compositing assembles the
layers, often on the GPU.
- Step 40. Remaining scripts run, fonts swap in, images decode, and the
page fires its load event.
- Step 41. The connection stays open for later requests, and a
NewSessionTicket is stored so the next visit can resume faster.
PLAIN32.12.4 what is really happening inside#
- Count the round trips in the good case: one for DNS, one for TCP, one for
TLS 1.3. Three before the first HTTP byte.
- The reader’s failure sat in the middle of that list, at the TCP layer, and
the shape of the failure was the clue.
- A refused connection sends a reset. A dead server on a live network often
produces an ICMP unreachable. A silent drop produces nothing.
- Silence is what a firewall or filter configured to drop rather than reject
produces, and it is also what a broken path produces.
- What is proven: no response of any kind on that path, while the same
request succeeded over mobile data from the same phone.
- What is only suggested: where on the path the drop happened, and who
caused it. The evidence does not name a party, and neither will we.
TECHNICAL32.12.5 the engineer’s version#
- The layered view, with the reader’s real values.
Layer Thing Reader's value
----------- -------------------- --------------------
Application HTTP/2 request GET / on github.com
Security TLS 1.3 record AES-GCM, X25519
Transport TCP segment dst port 443
Network IPv4 packet dst 20.207.73.82
Link Ethernet / 802.11 next hop 192.168.0.1
- Commands that observe each step, in order:
dig github.com,
dig @1.1.1.1 github.com, netstat -rn or route -n get default,
arp -a, traceroute github.com, nc -vz github.com 443,
curl -v https://github.com, openssl s_client -connect github.com:443,
and the browser’s network panel.
- The single most useful diagnostic split: if
nc -vz host 443 succeeds but
curl fails, the problem is above TCP. If nc itself hangs with no
response, the problem is at or below TCP, which is exactly what happened
here.
WORDS32.12.6 remember these#
- Happy eyeballs — trying IPv6 and IPv4 together — RFC 8305, racing address
families so a broken one costs milliseconds, not seconds.
- Punycode — non-ASCII names in ASCII form — the encoding that turns
international domain names into
xn-- labels.
- Critical rendering path — the shortest route to first pixels — the DOM,
CSSOM, render tree, layout, paint and composite sequence.
- Round trip time — one message out and back — the quantity that dominates
page load time on long paths.
- Silent drop — a packet discarded with no reply — the failure mode that
produces a timeout rather than an error, as in the reader’s session.
32.13 CDNs, load balancers and caching in front#
PLAIN32.13.1 in simple words#
- The server you connect to is usually not the server that owns the site.
- In front of it sit machines whose job is to be closer to you, to spread
load, and to answer from memory when they can.
- A load balancer takes one incoming connection and hands the work to
one of many identical servers behind it.
- A CDN, a content delivery network, is a large set of such machines
placed in many cities, so most users reach one within a few milliseconds.
- The real server behind all of it is called the origin.
- This is why
github.com answered from an edge site rather than from one
room somewhere.
PLAIN32.13.2 a picture in your head#
- A national newspaper does not post every copy from its head office.
- It prints in regional plants near readers. Delivery is fast because the
paper travelled a short distance.
- The head office is the origin. The regional plants are the edge.
- If a story changes after printing, every plant must be told to reprint.
That is cache invalidation, and it is genuinely hard.
Where this comparison breaks: a newspaper is identical for everyone. Web
responses vary by user, by language and by device, so the edge must know
exactly which parts are shareable and which are personal.
PLAIN32.13.3 a worked example#
- Real evidence from this chapter’s live capture of
github.com:
x-github-edge-region: iad
server: github.com
etag: W/"6a7d294b-8e2"
last-modified: Thu, 13 Aug 2026 02:17:47 GMT
cache-control: no-cache
x-github-edge-region: iad names the edge site, using the airport code
for Washington Dulles. The answer came from an edge machine.
etag is a version tag for the content. On the next request the browser
may send If-None-Match with that tag.
- If the content has not changed, the server replies
304 Not Modified with
no body. That is a full round trip saved on bytes, though not on latency.
cache-control: no-cache does not mean “do not cache”. It means “you may
store it, but revalidate before every use”. The header that forbids
storage is no-store.
PLAIN32.13.4 what is really happening inside#
- TLS is normally terminated at the edge. The edge holds a certificate and a
private key for the site’s name, decrypts your request, and then opens its
own connection to the origin.
- So the CDN sees your plaintext. That is unavoidable if it is going to
cache or inspect anything.
- That adds a party you must trust: your data is private from the network,
and fully visible to the CDN operator.
- Cache freshness has two mechanisms. A time to live says how long a
copy may be used without asking. Validation asks the origin whether a
stored copy is still good, using
ETag or Last-Modified.
- Invalidation is the hard part, because copies live in thousands of places.
The industry answer is to avoid it: give every version of a file a unique
name, cache it forever, and change the name when the content changes.
- That is why built assets have hashed file names, and why the HTML that
points at them is served with a very short cache time.
TECHNICAL32.13.5 the engineer’s version#
- Caching is specified in RFC 9111, June 2022. The directives that matter:
| Directive |
Meaning |
| max-age=N |
fresh for N seconds |
| s-maxage=N |
N seconds, shared caches |
| no-cache |
store, but revalidate |
| no-store |
do not store at all |
| private |
browser only, not CDN |
| immutable |
never revalidate |
| stale-while-revalidate |
serve old, refresh |
Vary tells caches which request headers change the answer. The real
capture above showed vary: Accept-Encoding, Accept, X-Requested-With.
A Vary on a high-cardinality header such as User-Agent destroys hit
rates.
- Layers you may meet in front of an origin, in order: DNS-based traffic
steering or anycast, a network load balancer at layer 4, a reverse proxy
or application load balancer at layer 7, a web application firewall, and
the cache itself.
- Anycast means the same IP address is announced from many locations, and
routing delivers you to the nearest. It is why one address such as
1.1.1.1 answers from a different machine in every country.
- The trade-off, stated plainly: a CDN reduces latency, absorbs attacks, and
cuts origin cost. It also inserts a company that can read, modify, log and
block every request, and whose outage becomes your outage.
WORDS32.13.6 remember these#
- Origin — the real server behind everything — the authoritative source a
cache or proxy fetches from on a miss.
- Edge — a machine near the user — a point of presence terminating TLS and
serving cached content.
- TLS termination — where encryption ends — the point at which the request
becomes plaintext, usually the edge, not the origin.
- ETag — a version tag for content — an opaque validator compared with
If-None-Match to produce a 304 Not Modified.
- Anycast — one address, many locations — the same prefix announced from
many sites, with routing choosing the nearest.
- Cache invalidation — telling every copy to forget — the hard problem
usually avoided by giving each version a unique name.
32.98 Common wrong ideas#
- Wrong: the padlock means the site is safe. Right: it means the connection
to whoever controls that name is private and untampered. Criminals get
certificates too, in seconds, for free.
- Wrong: HTTPS hides which site I visit. Right: the destination IP address
is always visible, and until Encrypted Client Hello, RFC 9849 of March
2026, the server name was in the clear as well. DNS may leak it too.
- Wrong: self-signed means insecure. Right: it means unverified. The
encryption is identical. What is missing is any proof of who is at the
other end, which matters enormously on a public site and hardly at all on
your own router at
192.168.0.1.
- Wrong: encryption alone is enough. Right: without authentication you may
have a perfectly encrypted tunnel to the attacker. Authenticity comes
first; confidentiality is only useful afterwards.
- Wrong: a certificate authority checks that the company is real. Right:
almost all certificates are Domain Validated, which proves control of a
name for a few seconds and nothing else.
- Wrong: a CORS error means the server rejected my request. Right: the
server usually processed it fine. The browser refused to give the response
to your script. Only the server can grant permission.
- Wrong: HTTP/2 solved head-of-line blocking. Right: it solved it at the
HTTP layer, but TCP still delivers in order, so one lost packet still
stalls every stream. HTTP/3 over QUIC fixed the transport layer.
- Wrong: my company cannot read my HTTPS traffic. Right: if they installed
a root certificate on the machine, they can read all of it, and one
openssl s_client command shows you whether they do.
32.99 Chapter summary in 20 lines#
- HTTP is plain text: a request line, headers, a blank line, and a body.
HTTPS is exactly that, carried inside TLS.
- Methods differ by whether they are safe and whether they are idempotent,
and those two properties drive caching, retries and 0-RTT rules.
- Status codes come in five families; 4xx blames the request and 5xx blames
the server, while 502 and 504 usually blame something in front of it.
- TLS solves three problems: confidentiality, integrity and authenticity.
- Encryption without authentication is nearly useless, because the tunnel
may end at the attacker.
- The building blocks are symmetric ciphers, hashes, message authentication
codes, public key pairs and key exchange.
- Diffie-Hellman lets two strangers agree a secret in public, using
modular exponentiation that nobody can reverse at real key sizes.
- Elliptic curves replaced RSA for new work because P-256 matches RSA 3072
in about a fifth of the bytes.
- Ephemeral keys give forward secrecy, so a stolen server key cannot decrypt
yesterday’s recorded traffic.
- TLS 1.2, RFC 5246 of 2008, needs two round trips. TLS 1.3, RFC 8446 of
August 2018, needs one, by sending a key share in the first message.
- 0-RTT saves another round trip and has no replay protection, so use it
only for safe, idempotent requests.
- A certificate binds a name to a public key and is trusted only because of
who signed it, never because of what it says.
- The chain runs leaf, intermediate, root; intermediates exist so the root
key can stay offline and a compromise can be repaired.
- Browsers check chain, dates, hostname against the SAN list, basic
constraints and, in Chrome, Certificate Transparency.
- Browsers do not check honesty, legitimacy, content safety, or whether the
brand matches the name. The padlock never meant safe.
- Revocation is largely broken; the industry answer is short-lived
certificates, falling to a 47 day maximum from March 2029.
- On your own router the endpoint is known and the link is one hop, so
accepting a self-signed certificate once is a small risk. On a public
site the same click may be surrendering to an active interception.
- A locally installed root lets a proxy read everything. One
openssl s_client command reveals it, by showing the issuer.
- HTTP/2 multiplexed over one TCP connection; HTTP/3 over QUIC, RFC 9114 of
June 2022, moved to UDP to remove head-of-line blocking in the transport.
- Cookies give HTTP the memory it lacks; the same-origin policy and CORS
decide who may read what, and a CORS error is always the browser’s
decision, never the server’s.