From handshake internals to Heartbleed — every detail explained
What is SSL/TLS?
When you type https:// into your browser, you trigger one of the most important cryptographic protocols on the internet — TLS (Transport Layer Security). Without it, every login, every credit-card number, every private message would travel the network in plaintext for anyone to read.
TLS is the modern successor to SSL (Secure Sockets Layer). Although "SSL" survives in casual speech ("SSL certificate", "SSL handshake"), every actually-deployed version of SSL is deprecated — the protocol you're using is TLS.
What TLS gives you
| Property | What it means in practice |
|---|---|
| Confidentiality | Eavesdroppers see only encrypted ciphertext, not your passwords or session cookies. |
| Integrity | If anyone tampers with a packet in transit, both sides detect it and abort. |
| Authentication | You can prove the server is who it claims to be (via its X.509 certificate). Optionally the client too (mTLS). |
| Forward secrecy | Even if the server's private key is later stolen, recorded past traffic stays unreadable (with modern cipher suites). |
Postcard vs sealed envelope — the analogy
HTTP without TLS is like writing on the back of a postcard — every mail handler along the way can read it. HTTPS wraps that postcard in a tamper-evident sealed envelope addressed only to the intended recipient. Anyone can still touch it, but they see only the envelope, and any attempt to open it leaves a visible trace.
Where TLS is used (it's not just web)
| Protocol | Default Port | Purpose |
|---|---|---|
| HTTPS | Port 443 | Web traffic — by far the largest user. |
| SMTPS | Port 465 | Email submission, encrypted. |
| IMAPS | Port 993 | Encrypted IMAP mail retrieval. |
| POP3S | Port 995 | Encrypted POP3. |
| FTPS | Port 990 | FTP over TLS. |
| LDAPS | Port 636 | Active Directory over TLS. |
| MQTTS | Port 8883 | IoT / message broker traffic. |
| DoT | Port 853 | DNS-over-TLS — encrypted DNS resolution. |
| XMPP-TLS | Port 5223 | Encrypted instant-messaging. |
| OpenVPN | Port 1194 | Uses TLS for control channel. |
SSL vs TLS — A Brief History
TLS didn't spring into existence fully-formed. It evolved over nearly 30 years of broken versions, attacks, and patches. Knowing this timeline helps you understand why SSLv3 is poison and why TLS 1.3 looks the way it does.
| Version | Year | Standardised by | Current Status |
|---|---|---|---|
| SSL 1.0 | 1994 | Netscape | Never released — too broken to ship. |
| SSL 2.0 | 1995 | Netscape | Deprecated 2011 (RFC 6176). Vulnerable to MitM downgrade. |
| SSL 3.0 | 1996 | Netscape | Deprecated 2015 (RFC 7568). POODLE killed it. |
| TLS 1.0 | 1999 | IETF | Deprecated 2020. BEAST showed CBC weaknesses. |
| TLS 1.1 | 2006 | IETF | Deprecated 2020. Mostly a TLS 1.0 fix. |
| TLS 1.2 | 2008 | IETF (RFC 5246) | Still widely deployed. Safe with strong ciphers. |
| TLS 1.3 | 2018 | IETF (RFC 8446) | Current best practice. Faster, simpler, safer. |
Why "SSL" lingers
When the IETF took over the protocol from Netscape in 1999, they renamed it from SSL to TLS for political reasons (the IETF wanted ownership). The wire-format change from SSL 3.0 to TLS 1.0 was tiny. Vendors kept calling their products "SSL certificates" because customers already knew that name. Today, when someone says "SSL", they almost always mean TLS.
What's deprecated as of 2024
How TLS Works — The Big Picture
Before we dive into byte-level handshake details, it helps to see the whole flow visually. The diagram below walks through every message of a TLS 1.2 RSA handshake — click play, or step through each phase.
Three phases of every TLS connection
| Phase | What happens |
|---|---|
| 1. Negotiate | Client and server agree on the protocol version, cipher suite, and exchange random nonces. |
| 2. Authenticate & Key-exchange | Server presents its certificate. Both sides derive a shared symmetric key (without anyone sending it in the clear). |
| 3. Bulk encrypt | From now on, every record is encrypted + MAC'd with the symmetric session keys. Fast (symmetric crypto), forward-secret (with modern KEX). |
Why both asymmetric and symmetric crypto?
Asymmetric crypto (RSA / ECDH) is slow but doesn't need a shared secret. Symmetric crypto (AES) is fast but both sides need the same key. TLS solves the chicken-and-egg by using asymmetric ops once at the start (the handshake) to negotiate a symmetric session key — then uses fast symmetric ops for the rest of the connection.
Inside the TLS Handshake — Step by Step
Now let's dissect each message on the wire. This is the TLS 1.2 RSA variant (still common in legacy systems and the easiest to learn). TLS 1.3 streamlines this — covered in §7.
Message 1: ClientHello
Handshake Type: Client Hello (1) Length: 215 Version: TLS 1.2 (0x0303) Random: 5f3a...d2b4c1 (32 bytes — first 4 bytes = unix timestamp, rest = random) Session ID: <empty> or <previous session id> Cipher Suites Length: 32 Cipher Suites: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) ... Compression Methods: null (0x00) Extensions: server_name: example.com (SNI — virtual hosting) supported_groups: x25519, secp256r1 signature_algorithms: rsa_pss_rsae_sha256, ecdsa_secp256r1_sha256, ... application_layer_protocol_negotiation (ALPN): h2, http/1.1
Key field: SNI (Server Name Indication). Without it, hosting one IP for many TLS sites would be impossible — the server wouldn't know which certificate to present until after the handshake.
Message 2: ServerHello
Handshake Type: Server Hello (2) Version: TLS 1.2 (0x0303) Random: 9c1d...8e7f3a (32 bytes) Session ID: 5a3b...8d2e (28 bytes — server may issue a new one for resumption) Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) Compression Method: null Extensions: renegotiation_info, application_layer_protocol_negotiation: h2
The server picks exactly one cipher suite from the client's list — usually the strongest both support. The client now knows: ECDHE for key exchange, RSA for authentication, AES-256-GCM for bulk, SHA-384 for the handshake PRF.
Message 3: Certificate
Handshake Type: Certificate (11)
Certificate List:
Certificate 1 (leaf):
Subject: CN=example.com, O=Example Corp, C=US
Issuer: CN=DigiCert TLS RSA SHA256 2020 CA1
Validity: 2024-01-01 to 2025-01-01
Public Key: RSA 2048-bit
SAN: DNS:example.com, DNS:www.example.com
Signature Algorithm: sha256WithRSAEncryption
Certificate 2 (intermediate):
Subject: CN=DigiCert TLS RSA SHA256 2020 CA1
Issuer: CN=DigiCert Global Root CA
Certificate 3 (root, usually omitted):
pre-installed in client's trust storeThe client now does:
1) Walks the chain back to a root it already trusts (in /etc/ssl/certs or the OS trust store)
2) Verifies every signature in the chain
3) Confirms the leaf cert's SAN/CN matches the hostname requested in SNI
4) Checks revocation (OCSP / CRL)
Message 4: ServerKeyExchange (ECDHE only)
Handshake Type: Server Key Exchange (12)
Curve: x25519
Server Public Key: 3d8b...f72a (32 bytes — server's ephemeral ECDH key share)
Signature Algorithm: rsa_pss_rsae_sha256
Signature: <RSA signature over (client_random || server_random || params)>Message 5: ServerHelloDone
Just a marker: "I'm done sending; your turn."
Message 6: ClientKeyExchange
RSA mode: EncryptedPreMasterSecret: <RSA-encrypted 48-byte premaster secret with server's public key> ECDHE mode: ClientECDHKeyShare: <client's ephemeral ECDH public key, 32 bytes for x25519>
Both sides now derive the master secret from premaster_secret + client_random + server_random using the PRF. From the master secret they derive 4 keys: client_write_key, server_write_key, client_write_IV, server_write_IV.
Messages 7–10: ChangeCipherSpec + Finished (both sides)
ChangeCipherSpec: 1 byte (0x01)
"From here, my records are encrypted with the derived keys."
Finished: ENCRYPTED hash of the entire handshake transcript so far.
If decryption + hash both verify, no attacker tampered with the negotiation.Each side sends ChangeCipherSpec then Finished. If both Finished messages verify, the secure channel is open and application data flows.
Wireshark capture — what a real handshake looks like
# Capture HTTPS traffic sudo tcpdump -i eth0 -w tls.pcap port 443 # In Wireshark, filter to handshake messages: tls.handshake # To decrypt: set Edit > Preferences > Protocols > TLS # Pre-Master-Secret log file: $SSLKEYLOGFILE # (Modern Chrome/Firefox honor SSLKEYLOGFILE env var) export SSLKEYLOGFILE=~/sslkeys.log
Interactive: ClientHello field-by-field
The visualisation below shows a real ClientHello as hex bytes — step through every named field (record header, handshake type, random nonce, cipher suites, SNI, signature algorithms…).
Cipher Suites Explained
A cipher suite is the bundle of algorithms used for one TLS session. In TLS 1.2 the suite name encodes four roles; TLS 1.3 reduced this dramatically.
Watch each segment of a real suite name light up:
Decoding a TLS 1.2 cipher suite name
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
^ ^ ^ ^ ^ ^
| | | | | +-- PRF / MAC hash
| | | | +------ Mode of operation
| | | +------------- Symmetric bulk cipher
| | +--------------------- Authentication algorithm
| +------------------------- Key-exchange algorithm
+-------------------------------- Protocol family| Component | Example | Meaning |
|---|---|---|
| Kex | ECDHE | Elliptic Curve Diffie-Hellman Ephemeral — provides forward secrecy. |
| Auth | RSA | Server's certificate uses an RSA signing key. |
| Cipher | AES_256 | AES with a 256-bit key. AES_128 is also fine. |
| Mode | GCM | Galois/Counter Mode — AEAD (authenticated encryption with associated data). Built-in integrity, no separate MAC needed. |
| Hash | SHA384 | Hash used in the PRF, certificate verification, and Finished message hash. |
TLS 1.3 cipher suite naming
TLS_AES_256_GCM_SHA384
^ ^ ^ ^
| | | +-- Hash for HKDF
| | +------ Mode
| +-------------- Cipher
+--------------------- FamilyKey-exchange and authentication are now negotiated separately via the supported_groups and signature_algorithms extensions, so the suite name only needs the bulk cipher + hash. Cleaner, less footgun-prone.
Strong vs weak — what to allow in 2024
| Verdict | Suite | Why |
|---|---|---|
| ✓ Allow | TLS_AES_256_GCM_SHA384 | TLS 1.3, AEAD, strong |
| ✓ Allow | TLS_AES_128_GCM_SHA256 | TLS 1.3, AEAD, fast on hardware |
| ✓ Allow | TLS_CHACHA20_POLY1305_SHA256 | TLS 1.3, AEAD, fast in software |
| ✓ Allow | TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | TLS 1.2, AEAD, forward-secret |
| ✓ Allow | TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | TLS 1.2, AEAD, forward-secret |
| ✗ Disable | *_CBC_* | CBC mode → Lucky 13, BEAST, padding-oracle attacks |
| ✗ Forbid | *_RC4_* | RC4 biases — practically broken |
| ✗ Forbid | *_3DES_* | SWEET32 — 64-bit block birthday attack |
| ✗ Forbid | TLS_RSA_* | No forward secrecy — key compromise reveals all past sessions |
| ✗ Forbid | *_NULL_* / *_EXPORT_* / *_anon_* | Test/legacy weakness ciphers |
Public Key Infrastructure (PKI) & Certificates
TLS's authentication guarantee hinges on a sprawling system called PKI (Public Key Infrastructure). The short version: your browser trusts ~100 root Certificate Authorities (CAs), and any of them can vouch for any domain. This is both PKI's genius and its most criticised feature.
Anatomy of an X.509 certificate
$ openssl x509 -in cert.pem -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: 0e:64:c5:fb... Signature Algorithm: sha256WithRSAEncryption Issuer: C=US, O=Let's Encrypt, CN=R3 Validity Not Before: Mar 1 00:00:00 2024 GMT Not After : May 30 23:59:59 2024 GMT Subject: CN=example.com Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:b8:d3:... Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Subject Alternative Name: DNS:example.com, DNS:www.example.com, DNS:api.example.com X509v3 Basic Constraints: critical CA:FALSE X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server Authentication Authority Information Access: OCSP - URI:http://r3.o.lencr.org Signature Algorithm: sha256WithRSAEncryption <issuer's signature over the above>
Chain of trust
| Tier | Role |
|---|---|
| Root CA | Self-signed. Pre-installed in browsers / OS. ~100 of them. Long validity (20-30 years). |
| Intermediate CA | Signed by a root. Day-to-day signing happens here so root keys can stay offline. |
| Leaf (end-entity) | Your domain's cert. Signed by an intermediate. Short validity (90 days for Let's Encrypt, 1 year for others). |
Inspecting a real cert
# Connect and print the cert chain openssl s_client -showcerts -connect example.com:443 < /dev/null # Just the leaf cert, with parsed fields echo | openssl s_client -connect example.com:443 2>/dev/null \ | openssl x509 -noout -text # Just the expiry date echo | openssl s_client -connect example.com:443 2>/dev/null \ | openssl x509 -noout -dates # Check if your cert is in CT logs curl -s 'https://crt.sh/?q=example.com&output=json' | jq '.[].name_value'
OCSP, CRL, OCSP-stapling
A cert can be revoked before its expiry (key leak, etc.). The browser learns about revocations via:
| Mechanism | How it works |
|---|---|
| CRL (Certificate Revocation List) | Big list of revoked serial numbers downloaded periodically. Slow, often stale. |
| OCSP (Online Cert Status Protocol) | Browser asks CA "is this serial still valid?" in real time. Privacy leak: CA learns what sites you visit. |
| OCSP Stapling | Server fetches the OCSP response and presents it during handshake. No client→CA traffic. |
Let's Encrypt — free + automated PKI
# Issue a cert via certbot sudo certbot --nginx -d example.com -d www.example.com # Renew (run twice daily via cron / systemd timer) sudo certbot renew --quiet
Certificate Transparency (CT)
Every publicly-trusted cert issued since 2018 must be logged to multiple CT logs — append-only, cryptographically-auditable ledgers. This means rogue CAs can't silently issue certs for your domain: the cert appears in CT and you can be alerted. Tools like crt.sh and certspotter.com let you search.
Watch the chain of trust in action
The visualisation below walks through how a browser verifies a server's certificate — from the leaf back to a pre-installed root CA.
TLS 1.3 — What Changed and Why
TLS 1.3 (RFC 8446, 2018) is the biggest change in 20 years. Almost every dangerous footgun from TLS 1.2 was either fixed or removed entirely.
Visually, the biggest win is latency — see how the two handshakes race side-by-side:
What changed
| Feature | Description |
|---|---|
| 1-RTT handshake | Client sends its key share in ClientHello. Server responds with everything (cert, key, Finished) in one shot. Cuts latency in half. |
| 0-RTT resumption | For repeat connections, client can send application data with the ClientHello. (Warning: replayable — only use for idempotent requests.) |
| Encrypted extensions | Server certificate is encrypted from message 2 onward. Passive eavesdroppers can't see which cert was used. |
| Only AEAD ciphers | CBC mode is gone. Only AES-GCM, ChaCha20-Poly1305, AES-CCM remain. |
| Forward secrecy mandatory | RSA key exchange is removed. Every TLS 1.3 connection uses ephemeral DH. |
| Static handshake hash | PRF replaced by HKDF + SHA-256/384. Cleaner key derivation. |
| Removed | Compression (CRIME fix), renegotiation, custom DHE groups, weak elliptic curves. |
TLS 1.3 handshake in one picture
Client Server
====== ======
ClientHello -->
+ key_share (e.g. x25519 pub)
+ signature_algorithms
+ supported_groups
+ cipher_suites: AES_GCM, CHACHA20...
<-- ServerHello
+ key_share (server's x25519 pub)
+ selected cipher
{EncryptedExtensions}
{Certificate}
{CertificateVerify} (server signs handshake hash)
{Finished}
[Application Data]
{Finished} -->
[Application Data] <-->
{} = encrypted with handshake-traffic keys
[] = encrypted with application-traffic keys
Watch the TLS 1.3 handshake animate
Same client + server endpoints as §3, but following the streamlined TLS 1.3 sequence — note how encryption kicks in starting at message 2 (notice the 🔒 padlock on the envelopes).
0-RTT and replay risk
Common TLS Attacks — The Hall of Fame
Two decades of TLS attacks have hardened the protocol. Here are the famous ones — every pentester should recognise their fingerprints.
Heartbleed (CVE-2014-0160)
| Year | 2014 |
| Affected | OpenSSL 1.0.1 — 1.0.1f |
| Severity | Critical (CVSS 7.5) — disclosure of memory contents |
| Cause | TLS Heartbeat extension: client sends payload + claimed length. Server didn't check that claimed length matched real length, returned that many bytes of memory. |
| Leaked | Private keys, session cookies, passwords — anything in process memory. |
# Detect with nmap nmap -p 443 --script ssl-heartbleed example.com # Exploit (educational reference — patched everywhere now) # https://github.com/musalbas/heartbleed-masstest
POODLE (CVE-2014-3566)
| Year | 2014 |
| Affected | SSL 3.0 (any cipher with CBC mode) |
| Cause | SSLv3 didn't verify padding bytes. Adaptive chosen-plaintext attack against the CBC padding lets an attacker decrypt one byte at a time. |
| Fix | Disable SSLv3 globally. TLS_FALLBACK_SCSV signals client deliberately tried a lower version, server rejects. |
BEAST (CVE-2011-3389)
| Affected | TLS 1.0 + CBC ciphers |
| Cause | IV for record N was the last ciphertext block of record N-1 — predictable. Attacker who can inject ciphertext into a session can recover plaintext. |
| Fix | TLS 1.1 randomized IVs. Or use TLS 1.2 + AEAD (GCM). |
DROWN (CVE-2016-0800)
| Affected | Servers that still supported SSLv2 with the same key as a TLS server |
| Cause | Bleichenbacher-style attack against SSLv2 export ciphers. About 22% of HTTPS servers were vulnerable at disclosure. |
| Fix | Disable SSLv2 (and don't share keys across servers). |
LOGJAM (CVE-2015-4000)
| Affected | TLS clients accepting export-grade (512-bit) Diffie-Hellman |
| Cause | Active attacker downgrades the DH group to 512-bit, then breaks it offline (a few hours with academic-scale hardware). |
| Fix | Server uses 2048+ bit DH groups. Disable export ciphers. Use ECDHE instead of plain DHE. |
SWEET32 (CVE-2016-2183)
64-bit block ciphers (3DES, Blowfish) hit birthday collisions after ~32 GB. An attacker who can keep a long-lived HTTPS session active and inject known plaintext can recover cookies. Fix: disable all 64-bit block ciphers.
CRIME and BREACH
| Attack | How | Mitigation |
|---|---|---|
| CRIME (2012) | TLS-level compression leaked size info → attacker recovers session cookies by chosen-plaintext expansion ratios. | Fix: disable TLS compression (already disabled by default in modern stacks). |
| BREACH (2013) | HTTP-level (gzip) compression of responses that include reflected user input + a secret allows recovery. | Fix: don't reflect user input in compressed responses, disable gzip on sensitive pages, or use length-randomising padding. |
Lucky 13 (CVE-2013-0169)
Timing side-channel against CBC + HMAC: the time to verify a record depends on whether the padding was valid. With enough samples, an attacker recovers plaintext. AEAD ciphers (GCM, ChaCha20-Poly1305) are immune.
Renegotiation attacks (CVE-2009-3555)
Attacker prepends data to a victim's HTTPS session by performing a separate handshake first and then injecting a renegotiation. Fixed by RFC 5746 (Secure Renegotiation extension) — disabled in TLS 1.3 entirely.
Quick reference: vulnerability vs cipher matrix
| Attack | Root cause | Mitigation status today |
|---|---|---|
| Heartbleed | OpenSSL bug | Patched |
| POODLE | SSLv3 padding | SSLv3 disabled |
| BEAST | TLS 1.0 + CBC | Use TLS 1.2+ AEAD |
| DROWN | SSLv2 sharing key | SSLv2 disabled |
| LOGJAM | Export DH downgrade | Disable export ciphers |
| SWEET32 | 64-bit block ciphers | Disable 3DES/Blowfish |
| CRIME | TLS compression | Compression disabled |
| BREACH | HTTP gzip | App-level care needed |
| Lucky 13 | CBC timing | Use AEAD ciphers |
| Reneg | TLS 1.0/1.1 | RFC 5746 / TLS 1.3 |
Man-in-the-Middle Scenarios
TLS is meant to defeat the man-in-the-middle. But misconfigurations and human factors still create attack windows. Watch what changes when you switch from plain HTTP to HTTPS:
Scenario 1: SSL Strip
# Moxie Marlinspike's classic — strip HTTPS from HTTP redirects # Attacker on the LAN, ARP-poisons victim's gateway: sudo arpspoof -i eth0 -t 192.168.1.10 192.168.1.1 # Run sslstrip to rewrite https:// links to http:// sudo sslstrip -l 8080 & echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
Defence: HSTS preload. Browsers refuse plain HTTP for sites on the HSTS list — even if the user types example.com, the request is upgraded to HTTPS before any DNS lookup.
Scenario 2: Burp Suite intercepting your own traffic
You install Burp's CA cert in your browser/device trust store. Burp generates a leaf cert for each visited domain, signed by its own CA. Your browser trusts it. Burp can now see and modify every TLS request.
# In Burp: Proxy > Options > Import / export CA certificate # Save as portswigger-cacert.der # Install on Android (rooted) or browser trust store # For mobile apps that pin certs: # - Use Frida + universal SSL unpinning script: # https://codeshare.frida.re/@masbog/frida-ios-ssl-unpinning/ # - Or Objection: objection --gadget com.target.app explore
Scenario 3: Bypass certificate pinning
Pinning hard-codes which CA (or specific public key) is acceptable for an app. Defeats Burp-style MitM. Bypass approaches:
| Technique | How |
|---|---|
| Frida unpinning | Runtime hook of certificate validation methods. Works on Android/iOS. Universal scripts exist. |
| Patch the APK | Decompile with apktool, replace pinning logic with always-true, rebuild + sign. |
| Trust user CAs (older Android) | Pre Android 7, user-installed CAs were trusted by all apps. Modern apps must explicitly opt-in via network_security_config.xml. |
| xposed / LSPosed module | SSLUnpinning xposed module on rooted devices. |
Scenario 4: Rogue CA / nation-state
If an attacker controls (or compels) any CA in the browser's trust store, they can mint a valid cert for any domain. Real-world cases:
| Incident | What happened |
|---|---|
| DigiNotar (2011) | Compromised; issued ~500 rogue certs including google.com. Used to MitM Iranian Gmail users. CA removed from all trust stores → company went bankrupt within weeks. |
| TURKTRUST (2012) | Issued intermediate CA certs by accident — used to MitM Google. |
| WoSign / StartCom (2016) | Backdated certs to avoid SHA-1 deprecation. Untrusted by all major browsers. |
| Symantec (2017) | Many improperly-issued certs. Google distrusted Symantec roots in phases. |
certspotter or crt.sh alert you to issuance you didn't authorise.Inspecting TLS — Tools & Techniques
Every TLS pentester needs these. Memorize them.
openssl s_client — the swiss army knife
# Basic connect, see cert + handshake openssl s_client -connect example.com:443 # With SNI (required for vhosts) openssl s_client -connect 1.2.3.4:443 -servername example.com # Force a specific protocol version openssl s_client -connect example.com:443 -tls1_2 openssl s_client -connect example.com:443 -tls1_3 openssl s_client -connect example.com:443 -ssl3 # to confirm SSLv3 is off # Force a specific cipher openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384' # Dump just the leaf cert openssl s_client -connect example.com:443 < /dev/null 2>/dev/null \ | openssl x509 -noout -text # Check OCSP stapling openssl s_client -connect example.com:443 -status < /dev/null 2>/dev/null \ | grep -A 17 'OCSP response:'
testssl.sh — comprehensive scanner
# One-line install git clone --depth 1 https://github.com/drwetter/testssl.sh.git cd testssl.sh # Full scan ./testssl.sh example.com # Just protocol + cipher checks ./testssl.sh -p -E example.com # All known vulnerabilities at once ./testssl.sh -U example.com # Output JSON for tooling ./testssl.sh --jsonfile out.json example.com
nmap NSE TLS scripts
# Enumerate ciphers nmap --script ssl-enum-ciphers -p 443 example.com # Check Heartbleed nmap --script ssl-heartbleed -p 443 example.com # Check for known weak certs (Debian-OpenSSL bug, ROBOT, etc.) nmap --script ssl-known-key -p 443 example.com nmap --script tls-poodle -p 443 example.com nmap --script tls-ticketbleed -p 443 example.com
sslyze — Python-native scanner
pip install sslyze # Full scan against example.com:443 sslyze --regular example.com # Specific checks sslyze --certinfo --reneg --heartbleed --robot example.com
Wireshark — decrypting TLS traffic
# Method 1: SSLKEYLOGFILE (works for Chrome/Firefox/curl built with NSS) export SSLKEYLOGFILE=~/sslkeys.log chromium & # Then in Wireshark: Preferences > Protocols > TLS > (Pre)-Master-Secret log filename # Method 2: server's private key (RSA only, no forward secrecy) # Preferences > TLS > RSA keys list # Add entry: 1.2.3.4, 443, http, /path/to/server.key # Method 3: live decryption with Burp # Set browser's HTTP/HTTPS proxy to Burp (127.0.0.1:8080)
SSL Labs (online — by Qualys)
ssllabs.com/ssltest grades any public HTTPS server A+ → F. The defacto standard for TLS health checks. Run it on your own servers; aim for A+ (HSTS + strong ciphers + OCSP-stapling).
Real-World Attack Workflow
A pentester's typical TLS workflow against a target. This is what you'd run on an external engagement.
Step 1: Discover TLS endpoints
# All TLS-enabled ports on the target nmap -p- --open --script ssl-enum-ciphers target.com # Or limit to common TLS ports nmap -p 443,8443,465,993,995,636,5061,5223,5269,5671 --script ssl-cert target.com # Pull all subdomains from CT logs curl -s 'https://crt.sh/?q=%25.target.com&output=json' \ | jq -r '.[].name_value' | sort -u
Step 2: Cert recon — find more attack surface
Subdomains, IP addresses, and internal hostnames often leak in SAN fields.
# Pull every SAN from a cert openssl s_client -connect target.com:443 < /dev/null 2>/dev/null \ | openssl x509 -noout -ext subjectAltName # Or use censys.io / shodan to find every cert with the org name shodan search 'ssl.cert.subject.O:"Target Corp"'
Step 3: Profile each endpoint
# Versions and ciphers nmap --script ssl-enum-ciphers -p 443 target.com # Famous vulns ./testssl.sh --severity HIGH --jsonfile target.json target.com:443 # Look for these red flags in output: # - SSLv2 / SSLv3 enabled # - TLS 1.0 / 1.1 enabled # - CBC ciphers offered # - 3DES / RC4 / NULL ciphers # - Self-signed or expired certs # - Weak DH groups (< 2048) # - Heartbleed, ROBOT, etc.
Step 4: If a vuln is found — exploit safely
Step 5: Report
A good TLS finding includes:
- Hostname + port + IP
- Affected protocol version / cipher / extension
- CVE / CWE reference
- Proof — testssl.sh or sslyze output snippet
- Concrete remediation: server config snippet, package version to upgrade, etc.
Mitigation & Best Practices
Hardening checklist for any production HTTPS server.
1. Protocol versions
# nginx ssl_protocols TLSv1.2 TLSv1.3; # Apache SSLProtocol -all +TLSv1.2 +TLSv1.3 # HAProxy ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
2. Cipher list — Mozilla intermediate (2024)
# nginx 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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; # Use Mozilla's generator: https://ssl-config.mozilla.org/
3. HSTS — never serve plaintext again
# Tells browser to ALWAYS use HTTPS for this site + subdomains for 1 year add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # Submit to https://hstspreload.org/ so even first connection is forced HTTPS
4. OCSP stapling
# nginx
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;5. Certificate hygiene
- Use ECDSA (P-256 or P-384) where possible — smaller, faster than RSA
- RSA: 2048-bit minimum; 4096-bit is overkill for TLS
- Automated renewal (certbot + cron / systemd timer)
- Monitor expiry: add Prometheus blackbox exporter or use
uptime-kuma - CAA DNS record to lock down which CAs may issue for your domain
# CAA record - only Let's Encrypt may issue example.com. IN CAA 0 issue "letsencrypt.org" example.com. IN CAA 0 issuewild "letsencrypt.org" example.com. IN CAA 0 iodef "mailto:[email protected]"
6. DH parameters
# Generate strong DH params (one-time, save and reuse) openssl dhparam -out /etc/ssl/dhparam.pem 4096 # nginx ssl_dhparam /etc/ssl/dhparam.pem;
7. Disable session tickets if you don't need them
Session tickets compromise forward secrecy if the ticket-key is leaked. Either rotate ticket keys frequently (hourly) or disable them and rely on session IDs.
# nginx
ssl_session_tickets off;8. Verify your config grades A+
# After deploying changes: ./testssl.sh example.com # Or check at https://www.ssllabs.com/ssltest/analyze.html?d=example.com
TLS vs Other Secure Protocols
TLS isn't the only secure-transport game in town. Knowing where each one fits helps you pick the right tool.
| Protocol | Layer / Type | Best For |
|---|---|---|
| TLS | Application-layer security over TCP | Web, mail, most app traffic. Per-connection. Cert-based auth. |
| DTLS | TLS for UDP (Datagram TLS) | VoIP, WebRTC, QUIC predecessor, IoT. Same crypto as TLS but tolerates packet loss. |
| QUIC | New transport replacing TCP, with TLS 1.3 baked in | HTTP/3. 1-RTT or 0-RTT connection establishment. Always encrypted. |
| SSH | Custom protocol, not based on TLS | Remote shell, port forwarding, file transfer. Mutual auth via keys. |
| IPsec | Network-layer (Layer 3) encryption | Site-to-site VPNs, host-to-host. Encrypts ALL traffic between two endpoints. |
| WireGuard | Modern simple VPN, kernel-level | Personal + corporate VPNs. ChaCha20-Poly1305, Curve25519, ~4000 LoC. |
| Noise Protocol | Cryptographic framework | Used inside WireGuard, Lightning Network. Designed for forward-secret embedded use. |
TLS vs SSH — they're not competitors
SSH is for interactive admin: one user, one shell. TLS is for application data: any client, any server, often anonymous. SSH uses its own crypto and key types (RSA/ECDSA/Ed25519 keys, no certificates by default). TLS leans on PKI / X.509 for identity. Both use roughly the same algorithms underneath (ECDH, AES-GCM, etc.).
Quick Reference Cheat Sheet
One-liners and constants that come up constantly.
Common openssl one-liners
# Generate RSA key + CSR for new cert openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr # Generate ECDSA key + CSR openssl ecparam -name prime256v1 -genkey -noout -out server.key openssl req -new -key server.key -out server.csr # Self-signed cert (for testing only!) openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout self.key -out self.crt -subj '/CN=localhost' # Check cert/key match (modulus should match) openssl x509 -noout -modulus -in cert.pem | openssl md5 openssl rsa -noout -modulus -in key.pem | openssl md5 # Convert formats openssl x509 -in cert.crt -out cert.pem -outform PEM # DER → PEM openssl pkcs12 -in cert.pfx -out cert.pem -nodes # PFX → PEM openssl pkcs12 -export -in cert.pem -inkey key.pem -out out.pfx # PEM → PFX # Verify a cert chain openssl verify -CAfile chain.pem cert.pem # Show expiry date only openssl x509 -in cert.pem -noout -enddate # Get the SHA-256 fingerprint openssl x509 -in cert.pem -noout -fingerprint -sha256
Common TLS ports
| Port | Service |
|---|---|
| 443 | HTTPS |
| 465 | SMTPS (submission) |
| 587 | SMTP STARTTLS |
| 993 | IMAPS |
| 995 | POP3S |
| 990 | FTPS (implicit) |
| 21 | FTP STARTTLS (explicit) |
| 636 | LDAPS |
| 853 | DoT (DNS-over-TLS) |
| 5061 | SIP-TLS |
| 5223 | XMPP-TLS |
| 8883 | MQTTS |
Quick risk-rating of what you might see in the wild
| Grade | What that usually means |
|---|---|
| A+ | TLS 1.3 only, ECDHE, AEAD, HSTS preload, OCSP-stapling |
| A | TLS 1.2+1.3, ECDHE, AEAD, HSTS |
| B | TLS 1.2+, some non-AEAD ciphers |
| C | TLS 1.0/1.1 enabled, or no forward secrecy |
| F | SSLv3 / weak ciphers / expired cert |
Tool one-liners you'll use weekly
# Quickly see TLS version + cipher of a site curl -v https://example.com 2>&1 | grep 'SSL connection' # Check expiry of every cert in a directory for f in *.pem; do echo -n "$f: "; openssl x509 -enddate -noout -in "$f"; done # Test if cert was issued for a specific name openssl x509 -in cert.pem -noout -ext subjectAltName # Test STARTTLS on SMTP openssl s_client -starttls smtp -connect smtp.example.com:587 # All ciphers a server offers nmap --script ssl-enum-ciphers -p 443 example.com
Closing Thoughts
You now know more about TLS than 99% of people who deploy it. The protocol is huge and has 30 years of history, but every part of it boils down to: negotiate, authenticate, key-exchange, encrypt. Every famous attack abused one of these four phases — and every modern best practice is about making one of them harder to break.
Build a habit: every time you deploy a service, run testssl.sh against it. Every time you build a feature that handles tokens or sessions, ask yourself what an attacker on the same network could see. TLS is the foundation — set it up right, and most of the network-level attack surface disappears.