How TLS and HTTPS Actually Work
TL;DR
TLS authenticates the server via certificates signed by a trusted CA, then uses asymmetric crypto to establish a shared symmetric key, then encrypts all traffic with that key. The handshake takes one round trip in TLS 1.3.
I ignored TLS for years. It was the thing that made the lock appear, and I knew not to store tokens in localStorage. That was enough.
Then I had to debug a certificate chain issue in production, and I realized I had no idea what was actually happening. Spent a day reading RFCs. Here's the short version.
What TLS Provides
Without TLS:
Your data → network → server
Anyone on the network can read it
Anyone can impersonate the server
With TLS:
Your data → encrypted → network → decrypted → server
Encrypted: only you and the server can read it
Authenticated: you've verified you're talking to the real server
Integrity: tampered data is detected and dropped
TLS does three things: encryption, authentication, and integrity. Each one is necessary; none is sufficient alone.
The TLS Handshake (TLS 1.3)
Client Server
| |
|── ClientHello ───────────────→| "I support these cipher suites,
| (client random, cipher list) | here's my random nonce"
| |
|←─ ServerHello ───────────────| "Let's use AES-256-GCM,
| (server random, cipher) | here's my random nonce"
|←─ Certificate ───────────────| "Here's my identity (cert chain)"
|←─ CertificateVerify ─────────| "Proof I hold the private key"
|←─ Finished ──────────────────| "Handshake hash, encrypted"
| |
|── Finished ─────────────────→| "I verify your Finished,
| | here's mine"
| |
|══ Encrypted Application Data ═| (your HTTP request/response)
TLS 1.3 takes one round trip before application data flows. TLS 1.2 took two.
Certificates and the Chain of Trust
The server sends a certificate. Your browser doesn't blindly trust it — it verifies it was signed by someone you already trust.
Root CA (built into your OS/browser)
└── Intermediate CA (signed by Root CA)
└── example.com certificate (signed by Intermediate CA)
Your browser has ~150 root CAs pre-installed. If the certificate chain traces back to any of them, the connection is trusted. This is why Let's Encrypt works — their root cert is in every browser.
What's in a certificate:
openssl x509 -in cert.pem -text -noout
# Shows:
# Subject: CN=example.com
# Issuer: CN=Let's Encrypt Authority X3
# Not Before: Jan 1 00:00:00 2026 GMT
# Not After: Apr 1 00:00:00 2026 GMT ← expires in 90 days (Let's Encrypt)
# Subject Alternative Name:
# DNS: example.com
# DNS: www.example.com
# Public Key: (RSA 2048 or EC P-256)
The certificate says "this public key belongs to example.com, and Let's Encrypt vouches for that."
How Authentication Actually Works
Owning a certificate isn't enough. Anyone can get a cert for a domain they control — the server has to prove it controls the private key corresponding to the public key in the cert.
During handshake:
1. Server sends Certificate (contains public key)
2. Server sends CertificateVerify:
- Takes a hash of all handshake messages so far
- Signs it with its private key
- Sends the signature
Client:
3. Verifies the cert chain (trusted CA, not expired, domain matches)
4. Verifies the signature using the public key from the cert
- If valid: server definitely holds the private key
- If not: abort connection (MITM attempt)
This is why stealing a cert file isn't enough — you need the private key too.
The Key Exchange
You can't send a symmetric encryption key in plaintext (the attacker would intercept it). So TLS uses asymmetric crypto to establish a shared secret:
TLS 1.3 uses Diffie-Hellman Ephemeral (DHE/ECDHE):
Both sides independently compute the same shared secret
without ever transmitting it:
Client: picks random a, sends g^a mod p
Server: picks random b, sends g^b mod p
Both: compute g^(ab) mod p = shared secret
Attacker sees g^a and g^b but cannot compute g^(ab)
(discrete logarithm problem)
"Ephemeral" means new keys each session → forward secrecy
(compromising today's key doesn't decrypt past traffic)
This shared secret is then used to derive the symmetric keys for encrypting the actual data.
Symmetric Encryption (The Fast Part)
Asymmetric crypto (RSA, ECDH) is slow — only used for the handshake. Once both sides have the shared secret, they switch to fast symmetric encryption:
TLS 1.3 cipher suites:
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256
AES-GCM: fast with hardware acceleration (AES-NI instruction)
ChaCha20-Poly1305: faster on mobile/embedded without AES-NI
Each packet is encrypted + includes an authentication tag. Any modification to the ciphertext is detected.
Debugging Certificate Issues
# Check what cert a server is presenting
openssl s_client -connect example.com:443 -showcerts
# Check cert expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Verify cert chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt cert.pem
# Check what TLS versions the server supports
nmap --script ssl-enum-ciphers -p 443 example.com
# Test with curl (verbose shows TLS info)
curl -v https://example.com 2>&1 | grep -E "TLS|SSL|cert|issuer"
# Simulate what a browser sees
openssl s_client -connect example.com:443 -servername example.com
Common Certificate Errors
ERR_CERT_AUTHORITY_INVALID
→ Cert signed by unknown CA (self-signed, internal CA not installed)
→ Fix: install the CA cert, or use a public CA
ERR_CERT_DATE_INVALID
→ Cert expired or not yet valid
→ Fix: renew cert, check server clock (NTP sync)
ERR_CERT_COMMON_NAME_INVALID
→ Cert doesn't cover this domain (CN or SAN mismatch)
→ Fix: get a cert that includes this domain in SAN
SSL_ERROR_RX_RECORD_TOO_LONG
→ TLS on a non-TLS port, or HTTP on HTTPS port
→ Fix: check port numbers
CERTIFICATE_VERIFY_FAILED (Python requests)
→ Missing CA bundle or self-signed cert
→ requests.get(url, verify='/path/to/ca-bundle.crt')
Let's Encrypt in Practice
# Install certbot
apt install certbot python3-certbot-nginx
# Get cert (nginx)
certbot --nginx -d example.com -d www.example.com
# Renew (Let's Encrypt certs expire in 90 days)
certbot renew --dry-run
# Add to cron:
# 0 12 * * * /usr/bin/certbot renew --quiet
Let's Encrypt uses the ACME protocol to verify domain ownership before issuing a cert. It creates a file on your web server or sets a DNS record, then checks it from their side.
The Bottom Line
TLS combines asymmetric crypto (authentication + key exchange) with symmetric crypto (bulk encryption). Certificates prove identity by chaining back to a trusted root CA.
The rules:
- TLS 1.3 is one round trip — use it; disable TLS 1.0 and 1.1
- Forward secrecy requires ephemeral key exchange (ECDHE) — verify your server uses it
- Certificate expiry breaks things silently — automate renewal
openssl s_clientis your debug tool for any TLS connection issue- Self-signed certs are fine for internal services if you install the CA
The lock icon means three specific things: encrypted, server authenticated, data integrity guaranteed. Not "this website is trustworthy."