Observing the TLS handshake with curl -v
The previous lab built and tested a server with no encryption at all — every byte of every request and response readable by anything positioned on the path in between. The TLS handshake article explained, using openssl s_client, exactly what closes that gap: a real handshake, captured and read message by message. This lab captures the same kind of handshake a different way, with a tool already covered in this course — curl -v — against a real HTTPS server on the open internet.
Goal and end result
By the end of this lab, you'll have captured a live TLS 1.3 handshake against a real production server, matched every message curl reports against the handshake this course already explained in depth, and inspected the actual certificate a real site presents.
Requirements
curl, already covered in its own dedicated article for reading the general shape of its verbose output.- A working internet connection — this lab deliberately targets a real external server rather than a local one, since the point is observing a genuine TLS negotiation with a certificate issued by a real certificate authority.
Safety note
Every command in this lab only sends an ordinary HTTPS request to a public web server, identical to what a browser sends visiting the same page — nothing here is intrusive, and nothing changes any configuration on your machine.
Step 1: capture the handshake
Redirecting the response body to /dev/null keeps the output focused on the connection and handshake details rather than the page content, which isn't the point of this lab.
* Trying 104.16.123.96:443...
* Connected to www.cloudflare.com (104.16.123.96) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey
* ALPN: server accepted h2
The exact IP address will differ on your own run — Cloudflare answers from many addresses, and which one you reach depends on routing and load balancing outside this lab's concern.
Step 2: match this against the handshake this course already explained
The TLS handshake article captured the identical exchange using openssl s_client -msg, which names messages slightly differently — ClientHello and ServerHello there, Client hello (1) and Server hello (2) here — but the underlying messages are exactly the same ones, just reported by a different tool:
TLS handshake, Client hello (1)is theClientHellothat article covered — the client proposing every cipher suite, TLS version, and key share it's willing to use, all in the first message, before the server has replied at all.Server hello (2)throughFinished (20)on theINside is the single flight the article described — the server's choice of cipher suite and key share, its certificate, aCERT verifyproving it holds the private key matching that certificate, and its ownFinishedmessage, all arriving together in one round trip rather than as separate exchanges.- The client's own
Finished (20), sent last, is the client's half of the mutual transcript verification that article explained — proof that nothing in the handshake was tampered with in transit. ALPN: server accepted h2is the detail the HTTP evolution module covered from the other direction: the server agreeing, during the TLS handshake itself, to speak HTTP/2 for the request that follows — which is why the very next line of a full, non-redirected capture would show> GET / HTTP/2, notHTTP/1.1.
Step 3: inspect the certificate the server actually presented
* Server certificate:
* subject: CN=www.cloudflare.com
* start date: Jul 19 12:47:34 2026 GMT
* expire date: Oct 17 13:47:32 2026 GMT
* subjectAltName: host "www.cloudflare.com" matched cert's "www.cloudflare.com"
* issuer: C=US; O=Google Trust Services; CN=WE1
* SSL certificate verify ok.
Every field here maps directly onto what a certificate actually claims: a subject naming the domain, a validity window, a subjectAltName curl itself checked against the hostname requested, and an issuer — here, Google Trust Services, a certificate authority — that curl's own trust store already recognized, which is exactly why the final line reads SSL certificate verify ok. rather than a warning.
Practical scenario: forcing an older protocol and watching what changes
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
--tls-max 1.2 forces the negotiation down to TLS 1.2 instead of letting curl and the server agree on 1.3 — and the message list visibly grows: a Server key exchange message and a separate Client key exchange message appear that TLS 1.3's handshake doesn't have at all, and the whole exchange takes two round trips instead of one. This is precisely the concrete difference the TLS handshake article named between the two versions — not an abstract claim about TLS 1.3 being "faster," but a visibly shorter message list doing the identical job.
Troubleshooting
- The handshake fails with a certificate error. Check the system clock first — a certificate validity window failing because the local clock is wrong is a surprisingly common cause, and looks identical to a genuinely invalid certificate from curl's error message alone.
--tls-max 1.2is rejected or ignored. Confirm the installed curl was built against a TLS library that still supports negotiating down to 1.2 — some hardened builds disable older protocol versions entirely, which is itself a reasonable security default, just one that gets in the way of this specific comparison.- The IP address or exact certificate details differ from what's shown here. Expected — a real production service's certificate, issuer, and even serving IP address change over time; the message sequence and structure are what this lab is actually checking, not the exact bytes.
Cleanup
Nothing to tear down — every request in this lab was a read-only HTTPS request to a public server.
Evaluation criterion
You've completed this lab correctly if you can point to your own curl -v output and correctly label the ClientHello/Client hello, the server's single-flight reply, and both Finished messages — and explain, using your own forced-TLS-1.2 capture, which two messages exist in TLS 1.2's handshake but not in TLS 1.3's.
Every lab so far watched traffic that was either unencrypted or, here, encrypted but pointed at infrastructure you don't control. The next lab hands you actual control over a piece of that infrastructure — creating real DNS records for a domain and watching how quickly, or slowly, the change actually propagates.
Sources
- curl, curl(1) manual page