PKI & TLS
TLS Handshake
What actually happens during a TLS 1.3 handshake.
The TLS handshake is where two strangers negotiate encryption, authenticate the server, and derive a shared secret, all before a single byte of your HTTP request is sent. TLS 1.3 rewrote this process to be faster and safer, cutting a round trip and removing decades of accumulated cruft. Seeing it step by step demystifies both normal operation and the failures you will debug in production.
Why This Matters
A handshake failure blocks every request. When curl reports SSL_ERROR_SYSCALL or a browser shows ERR_SSL_VERSION_OR_CIPHER_MISMATCH, the cause is in the handshake: an unsupported protocol version, no common cipher suite, or a certificate the client will not trust. Understanding the sequence lets you read openssl s_client -v output and pinpoint the exact step that broke.
The 1.3 Handshake Step by Step
TLS 1.3 completes in one round trip (1-RTT). The key insight: the client guesses which key exchange group the server supports and sends its key share immediately, so the server can finish the exchange in its first reply.
| Step | Message | Purpose |
|---|---|---|
| 1 | ClientHello | Client sends supported versions, cipher suites, and an ephemeral key share |
| 2 | ServerHello | Server picks the suite and sends its own key share |
| 3 | (encrypted) EncryptedExtensions, Certificate, CertificateVerify | Server proves identity: sends its cert and signs the transcript |
| 4 | Finished | Both sides confirm they derived the same keys |
| 5 | Application Data | Encrypted request flows |
After ServerHello, both parties compute the same shared secret via ephemeral Diffie-Hellman (ECDHE), so everything from EncryptedExtensions onward is already encrypted. This is a major TLS 1.3 improvement: the certificate itself is sent encrypted. Ephemeral keys give forward secrecy: recording the ciphertext today does not decrypt it even if the server’s long-term key leaks tomorrow.
Watching a Handshake
openssl s_client narrates the whole exchange.
cd ~/crypto-lab
openssl s_client -connect cloudflare.com:443 -servername cloudflare.com -tls1_3 </dev/null 2>/dev/null \
| grep -E "Protocol|Cipher|Server public key|Verification"
Expected output:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Server public key is 256 bit
Verification: OK
curl -v shows the same negotiation from the client side.
curl -vI https://cloudflare.com 2>&1 | grep -E "SSL connection|subject:|issuer:"
Expected output:
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* subject: CN=cloudflare.com
* issuer: C=US; O=Google Trust Services; CN=WE1
Run your own local server to watch both ends, using the self-signed cert from the sibling X.509 Certificates page.
cd ~/crypto-lab
openssl s_server -accept 4433 -cert dev.crt -key dev.key -www &
openssl s_client -connect 127.0.0.1:4433 </dev/null 2>/dev/null | grep "Cipher is"
kill %1
Expected output:
Cipher is TLS_AES_256_GCM_SHA384
Cipher Suites and What to Disable
A TLS 1.3 cipher suite names only the symmetric cipher and hash; key exchange and signatures are negotiated separately and are always forward-secret. This is a deliberate simplification over the sprawling TLS 1.2 suite names.
| Configuration | Status | Action |
|---|---|---|
| TLS 1.3 with AEAD suites (AES-GCM, ChaCha20-Poly1305) | Recommended | Enable |
| TLS 1.2 with ECDHE and AEAD | Acceptable | Keep for compatibility |
| TLS 1.0 / 1.1 | Deprecated | Disable |
| CBC-mode suites | Weak, prone to padding oracles | Disable |
| RC4, 3DES, export ciphers | Broken | Never enable |
CBC suites are the reason the sibling Padding Oracle page in Attacks & Pitfalls exists; disabling them removes a whole attack class. When the client also authenticates with a certificate, the handshake adds a CertificateRequest step, covered in Mutual TLS.
Practical Guidance
- Standardize on TLS 1.3 and allow TLS 1.2 only where a client demands it; disable 1.0 and 1.1 at the server.
- Insist on forward-secret, AEAD-only cipher suites; disable CBC-mode suites to eliminate padding-oracle risk.
- When a handshake fails, reproduce it with
openssl s_client -connect host:443and read the version, cipher, and Verification lines before touching anything else. - Verify your server sends its full chain here too; a chain error surfaces as a handshake failure, not an application error.
- Prefer session resumption over 0-RTT early data unless you understand its replay risk; 0-RTT trades a round trip for weaker guarantees.