Deep dive
How it works
Parts:
- Client and server — the two endpoints; the server must prove its identity.
- Key exchange — ephemeral X25519 (or hybrid X25519MLKEM768) gives a fresh shared secret per connection.
- Key schedule — HKDF turns the secret and transcript into handshake and traffic keys.
- Certificate chain and CA roots — bind the server's public key to its hostname.
- Record layer — AEAD (see AEAD) protects every record after ServerHello.
- Session tickets — PSKs that allow resumption and 0-RTT.
- Both sides compute the ECDHE shared secret; HKDF derives handshake keys, so everything after ServerHello is encrypted.
- Server proves key ownership: CertificateVerify = signature over the handshake transcript with its certificate key.
- Client validates the chain to a trusted root, the hostname (SAN), validity dates, and the signature.
- Finished MACs over the transcript stop downgrade and tampering; traffic keys are derived; later key updates are possible.
- Resumption: the server issues a PSK ticket; next connection can skip certificates and optionally send 0-RTT early data.
Client Server
ClientHello: versions, suites, key_share(X25519 pub), SNI, ALPN ──►
◄── ServerHello: key_share(server pub)
{EncryptedExtensions} {Certificate} {CertificateVerify} {Finished}
{Finished} + application data ──► (1 RTT total)
Code
nginx (Mozilla "intermediate" profile, condensed):
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# TLS 1.2 only: ECDHE + AEAD. TLS 1.3 suites are fine by default.
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:TLS:10m;
ssl_session_tickets off; # nginx never rotates ticket keys: weakens forward secrecy
ssl_early_data off; # 0-RTT replay risk
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
}
Rust server and client with rustls = "0.23" (verification on by default, no knobs to weaken by accident):
use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer, ServerName};
use std::sync::Arc;
pub fn server_config() -> Result<Arc<rustls::ServerConfig>, Box<dyn std::error::Error>> {
let certs: Vec<CertificateDer<'static>> =
CertificateDer::pem_file_iter("fullchain.pem")?.collect::<Result<_, _>>()?;
let key = PrivateKeyDer::from_pem_file("privkey.pem")?;
let mut cfg = rustls::ServerConfig::builder() // safe defaults: TLS 1.2+1.3, AEAD only
.with_no_client_auth()
.with_single_cert(certs, key)?;
cfg.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Ok(Arc::new(cfg))
}
pub fn client_config() -> Arc<rustls::ClientConfig> {
let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
Arc::new(rustls::ClientConfig::builder().with_root_certificates(roots).with_no_client_auth())
}
pub fn connect(cfg: Arc<rustls::ClientConfig>, host: &str) -> std::io::Result<()> {
let name = ServerName::try_from(host.to_owned()).expect("valid DNS name"); // hostname is checked
let conn = rustls::ClientConnection::new(cfg, name).map_err(std::io::Error::other)?;
let sock = std::net::TcpStream::connect((host, 443))?;
let _tls = rustls::StreamOwned::new(conn, sock); // Read + Write over TLS
Ok(())
}
Use tokio-rustls for async; reqwest with the rustls-tls feature for HTTP clients.
Complexity and constants
- Handshake CPU: X25519 order of 20-50 µs per side; certificate signature: ECDSA P-256 or Ed25519 order of tens of µs, RSA-2048 sign order of 1 ms. Prefer ECDSA certs on busy servers (serve RSA too only for very old clients).
- Hybrid X25519MLKEM768 adds about 1.1 KB to ClientHello and ~1 KB to ServerHello; CPU cost is small.
- Records: up to 16 KiB plaintext + about 22 B overhead. Bulk throughput is AEAD-bound, order of several GB/s per core with AES-NI.
- Latency: 1 RTT for TLS on top of TCP's 1 RTT; QUIC combines them.
Efficiency
Cost: the bound is round-trip latency for the handshake and AEAD throughput afterwards (bulk AEAD at GB/s per core); handshake CPU (order of 0.1-1 ms) matters only on busy terminators.
Levers:
- Reuse connections — keep-alive and HTTP/2 multiplexing amortize one handshake over many requests — all clients.
- Resumption — a PSK ticket skips certificate validation and the signature; 0-RTT early data also saves the round trip but is replayable — idempotent requests only.
- ECDSA P-256 certificate — signs in tens of µs against about 1 ms for RSA-2048 (order of), with a smaller chain; keep one intermediate (each certificate order of 1-2 KB) — busy servers.
- Certificate compression (RFC 8879) — shrinks the chain on the wire — when both sides support it.
- Cipher by hardware — AES-GCM with AES-NI or ARMv8 crypto extensions; ChaCha20-Poly1305 is faster on CPUs without them — bulk transfer.
Naive → standard → state of the art:
- Naive: new connection and full handshake per request, RSA certificate · 1 RTT plus order of 1 ms signing per request
- Standard: TLS 1.3, keep-alive or HTTP/2, ECDSA certificate, terminate once at the edge · one handshake per connection
- State of the art: resumption with 0-RTT for idempotent requests, certificate compression · resumed connections skip certificate and signature, and 0-RTT sends data in the first flight
Implementation notes
- Certificates: ACME (Let's Encrypt, ZeroSSL) with automatic renewal well before expiry. Maximum public certificate lifetimes are shrinking (CA/Browser Forum schedule down to 47 days by 2029), so manual renewal is not an option.
- Keys: ECDSA P-256 is the common default; keep private keys readable only by the TLS process, or terminate in a load balancer/KMS.
- Internal services: mTLS with a private CA and short-lived certs (SPIFFE/SPIRE, service mesh, or step-ca). Validate the client identity (SAN/URI), not just "signed by our CA".
- HSTS: add once all subdomains are on HTTPS; preload only when sure, it's hard to undo.
- Session tickets: if enabled, rotate ticket keys often (hours) and share them across the fleet; or use the session cache only.
- Test with
testssl.shor SSL Labs after every config change.
Pitfalls
- Disabling verification in clients (
verify=False,InsecureSkipVerify,danger_accept_invalid_certs) "temporarily" and shipping it. - Verifying the chain but not the hostname (custom verifiers, raw OpenSSL usage).
- TLS terminated at the load balancer, then plaintext across a "trusted" network to the app.
- Expired certificates from a broken renewal cron: alert at 21 days before expiry.
- Enabling 0-RTT for POST endpoints: replay can repeat payments.
- Missing intermediate certificate in the served chain: works in browsers that cache it, fails in API clients.
Variants and alternatives
In production
- Libraries: BoringSSL (Chrome, Cloudflare), OpenSSL 3, rustls, Go
crypto/tls, s2n-tls (AWS), NSS (Firefox), Windows Schannel (TLS 1.3 suites listed in Microsoft docs). - Cloud and IoT: AWS Application Load Balancer policies (docs), AWS IoT Core (docs).
- Government: NIST SP 800-52r2 (NIST).
- Web transport: QUIC embeds the TLS 1.3 handshake (RFC 9000).
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| rustls | Rust | rustls/rustls | cargo add rustls | — | — | — | — | memory-safe TLS 1.2/1.3; safe defaults |
| s2n-tls | C / Rust bindings | aws/s2n-tls | cargo add s2n-tls | — | — | — | — | AWS TLS library; Rust crate wraps the C core |
| boring | Rust bindings | cloudflare/boring | cargo add boring | — | — | — | — | BoringSSL bindings |
| OpenSSL | C | openssl/openssl | brew install openssl@3 | — | — | — | — | most deployed; cargo add openssl for Rust bindings |
| Go crypto/tls | Go | golang/go | n/a (standard library) | — | — | — | — | TLS 1.3 server and client in the Go standard library |
Sources
- Rescorla, RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3, 2018. rfc
- Sheffer, Saint-Andre, Fossati, RFC 9325: Recommendations for Secure Use of TLS and DTLS, 2022. rfc
- Moriarty, Farrell, RFC 8996: Deprecating TLS 1.0 and TLS 1.1, 2021. rfc
- Ghedini, Vasiliev, RFC 8879: TLS Certificate Compression, 2020. rfc
- Mozilla, Server Side TLS guidelines and SSL Configuration Generator. guidelines · generator