TLS Deep Dive: How HTTPS Actually Works
1. Why Should You Care?
Every day, you make hundreds of HTTPS requests. But what happens in that split second between typing a URL and seeing the padlock?
Understanding TLS helps you:
- Debug connection issues and certificate errors
- Configure servers securely
- Understand security advisories about protocol vulnerabilities
- Make informed decisions about cipher suites and versions
2. What TLS Provides
TLS (Transport Layer Security) provides three security properties:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Confidentiality โ
โ Data is encrypted, eavesdroppers see only ciphertext โ
โ โ Achieved with: AES-GCM, ChaCha20-Poly1305 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 2. Integrity โ
โ Any modification is detected โ
โ โ Achieved with: AEAD (built into AES-GCM) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 3. Authentication โ
โ Server is who it claims to be โ
โ โ Achieved with: Certificates, Digital Signatures โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ3. TLS 1.3 vs Earlier Versions
TLS 1.3 (2018) improvements over TLS 1.2:
Removed:
โ RSA key exchange (no forward secrecy)
โ CBC mode ciphers (padding oracle attacks)
โ MD5, SHA-1 for signatures
โ Compression (CRIME attack)
โ Renegotiation
Added:
โ Mandatory forward secrecy (ECDHE only)
โ 1-RTT handshake (faster)
โ 0-RTT resumption (optional, even faster)
โ Encrypted handshake messages
โ Simplified cipher suites
Performance:
TLS 1.2: 2 round trips before data
TLS 1.3: 1 round trip before data4. The TLS 1.3 Handshake
Overview
Client Server
โ โ
โโโโโโโโโโโโโ ClientHello โโโโโโโโโโโโโโโโโโโโโโ>โ
โ - Supported versions โ
โ - Cipher suites โ
โ - Key share (ECDH public key) โ
โ - Random โ
โ โ
โ<โโโโโโโโโโโ ServerHello โโโโโโโโโโโโโโโโโโโโโโโโ
โ - Selected version โ
โ - Selected cipher suite โ
โ - Key share (ECDH public key) โ
โ - Random โ
โ โ
โ [Both compute shared secret] โ
โ [Derive handshake keys] โ
โ โ
โ<โโโโโโโโโโโ {EncryptedExtensions} โโโโโโโโโโโโโโ
โ<โโโโโโโโโโโ {Certificate} โโโโโโโโโโโโโโโโโโโโโโ
โ<โโโโโโโโโโโ {CertificateVerify} โโโโโโโโโโโโโโโโ
โ<โโโโโโโโโโโ {Finished} โโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโโ {Finished} โโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ
โ<โโโโโโโโโโโโ Application Data โโโโโโโโโโโโโโโโ>โ
โ โ
{} = encrypted with handshake keysStep 1: ClientHello
# What the client sends (conceptual)
client_hello = {
'legacy_version': 0x0303, # TLS 1.2 for compatibility
'random': os.urandom(32),
'session_id': os.urandom(32), # For compatibility
'cipher_suites': [
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256',
'TLS_AES_128_GCM_SHA256',
],
'extensions': {
'supported_versions': ['TLS 1.3', 'TLS 1.2'],
'supported_groups': ['x25519', 'secp256r1'],
'signature_algorithms': ['ecdsa_secp256r1_sha256', 'rsa_pss_rsae_sha256'],
'key_share': {
'x25519': client_x25519_public_key
},
'server_name': 'example.com', # SNI
}
}Step 2: ServerHello
# What the server responds (conceptual)
server_hello = {
'legacy_version': 0x0303,
'random': os.urandom(32),
'session_id': client_session_id, # Echo back
'cipher_suite': 'TLS_AES_256_GCM_SHA384',
'extensions': {
'supported_versions': 'TLS 1.3',
'key_share': {
'x25519': server_x25519_public_key
}
}
}Step 3: Key Derivation
Both parties now have:
- Client's ECDH public key
- Server's ECDH public key
- Their own ECDH private key
They compute:
shared_secret = ECDH(my_private, peer_public)
TLS 1.3 uses HKDF to derive multiple keys:
shared_secret
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ HKDF-Extract โ
โ (with zeros) โ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Early Secret
โ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ HKDF-Expand โ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ HKDF-Extract โ
โ (with shared_secret)โ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Handshake Secret
โ โ
โผ โผ
client_handshake server_handshake
_traffic_secret _traffic_secret
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ HKDF-Extract โ
โ (with zeros) โ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Master Secret
โ โ
โผ โผ
client_application server_application
_traffic_secret _traffic_secretStep 4: Server Authentication
Server sends (encrypted with handshake keys):
1. EncryptedExtensions
- Additional parameters that aren't needed for key exchange
2. Certificate
- Server's X.509 certificate chain
- Now encrypted (privacy improvement over TLS 1.2)
3. CertificateVerify
- Signature over handshake transcript
- Proves server has private key for certificate
- signature = Sign(private_key, Hash(handshake_messages))
4. Finished
- HMAC of handshake transcript
- Proves no tampering with handshake
- finished = HMAC(finished_key, Hash(handshake_messages))Step 5: Client Finished
Client verifies:
1. Certificate chain is valid
2. Server name matches certificate
3. CertificateVerify signature is valid
4. Finished MAC is correct
Then sends its own Finished message:
- Proves client also saw the complete handshake
- Now both sides switch to application traffic keys5. Code: Inspecting TLS Connection
import ssl
import socket
import pprint
def inspect_tls_connection(hostname, port=443):
"""Inspect TLS connection details"""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
print(f"TLS Version: {ssock.version()}")
print(f"Cipher: {ssock.cipher()}")
print(f"Compression: {ssock.compression()}")
cert = ssock.getpeercert()
print(f"\nCertificate Subject: {cert['subject']}")
print(f"Issuer: {cert['issuer']}")
print(f"Valid From: {cert['notBefore']}")
print(f"Valid Until: {cert['notAfter']}")
print(f"SANs: {cert.get('subjectAltName', [])}")
# Example
inspect_tls_connection("www.google.com")
# Output:
# TLS Version: TLSv1.3
# Cipher: ('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256)
# Compression: None
# ...6. Cipher Suites in TLS 1.3
Available Suites
TLS 1.3 only has 5 cipher suites:
TLS_AES_128_GCM_SHA256 # Fast, secure
TLS_AES_256_GCM_SHA384 # Higher security margin
TLS_CHACHA20_POLY1305_SHA256 # Fast without AES-NI
TLS_AES_128_CCM_SHA256 # CCM mode (rare)
TLS_AES_128_CCM_8_SHA256 # Short tag (IoT)
Format: TLS_<AEAD>_<HASH>
- No key exchange algorithm (always ECDHE)
- No signature algorithm (negotiated separately)Choosing Cipher Suites
import ssl
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# TLS 1.3 cipher suites (set via ciphersuites, not set_ciphers)
context.set_ciphersuites('TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256')
# TLS 1.2 fallback ciphers (if needed)
context.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!MD5:!DSS')
# Minimum version
context.minimum_version = ssl.TLSVersion.TLSv1_27. Forward Secrecy
Why It Matters
Without forward secrecy (RSA key exchange):
1. Attacker records encrypted traffic
2. Years later, steals server's private key
3. Decrypts ALL historical traffic
With forward secrecy (ECDHE):
1. Each session uses ephemeral keys
2. Private keys deleted after session
3. Even if long-term key stolen:
- Cannot decrypt past sessions
- Each session's keys are goneHow ECDHE Provides Forward Secrecy
Session 1:
Client: generates ephemeral key pair (aโ, Aโ)
Server: generates ephemeral key pair (bโ, Bโ)
Shared: Kโ = aโรBโ = bโรAโ
After session: aโ, bโ deleted forever
Session 2:
Client: generates ephemeral key pair (aโ, Aโ)
Server: generates ephemeral key pair (bโ, Bโ)
Shared: Kโ = aโรBโ = bโรAโ
After session: aโ, bโ deleted forever
Attacker who compromises server later:
- Gets long-term signing key
- But Kโ, Kโ were never stored
- Cannot recover session keys8. Session Resumption
TLS 1.3 Session Tickets
First connection:
Client Server
โ โ
โโโโโ Full handshake โโโโโโโโโโโโโโโโโโ>โ
โ<โโโ Full handshake โโโโโโโโโโโโโโโโโโโโ
โ<โโโ NewSessionTicket โโโโโโโโโโโโโโโโโโ
โ (encrypted session state) โ
โ โ
Resumed connection:
Client Server
โ โ
โโโโโ ClientHello + pre_shared_key โโโโ>โ
โ (contains session ticket) โ
โ<โโโ ServerHello (selected PSK) โโโโโโโโ
โ โ
โ [Abbreviated handshake] โ
โ<โโโโ Application Data โโโโโโโโโโโโโโโโ>โ
Benefits:
- 1-RTT resumed handshake
- Server doesn't store session state
- Ticket encrypted with server's key0-RTT (Early Data)
Client can send data immediately:
Client Server
โ โ
โโโโโ ClientHello + early_data โโโโโโโโ>โ
โ (encrypted with PSK) โ
โ<โโโ ServerHello โโโโโโโโโโโโโโโโโโโโโโโ
โ โ
Risks:
- Replay attacks possible!
- Only use for idempotent requests
- Server can reject 0-RTT9. Common TLS Issues
Certificate Problems
import ssl
import socket
def diagnose_tls_issues(hostname, port=443):
"""Diagnose common TLS issues"""
# Try connection with verification
try:
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
print(f"โ Connection successful: {ssock.version()}")
return True
except ssl.SSLCertVerificationError as e:
print(f"โ Certificate verification failed: {e}")
# Try without verification to get more info
context_no_verify = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context_no_verify.check_hostname = False
context_no_verify.verify_mode = ssl.CERT_NONE
with socket.create_connection((hostname, port)) as sock:
with context_no_verify.wrap_socket(sock) as ssock:
cert = ssock.getpeercert(binary_form=True)
# Analyze cert...
except ssl.SSLError as e:
print(f"โ SSL error: {e}")
except socket.timeout:
print(f"โ Connection timeout")
except Exception as e:
print(f"โ Error: {e}")
return FalseVersion and Cipher Mismatches
# Check what versions/ciphers server supports
openssl s_client -connect example.com:443 -tls1_3
openssl s_client -connect example.com:443 -tls1_2
# List supported ciphers
openssl ciphers -v 'HIGH:!aNULL:!MD5'
# Test specific cipher
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'10. Server Configuration Best Practices
Modern TLS Configuration
# Nginx configuration
server {
listen 443 ssl http2;
# Certificates
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
# Protocol versions
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites (TLS 1.2)
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; # Let client choose in TLS 1.3
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# Session tickets
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Disable for forward secrecy
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}Testing Your Configuration
# Mozilla SSL Configuration Generator
# https://ssl-config.mozilla.org/
# SSL Labs test
# https://www.ssllabs.com/ssltest/
# testssl.sh
./testssl.sh https://example.com11. TLS in Code
Python HTTPS Client
import ssl
import urllib.request
# Default (secure) settings
response = urllib.request.urlopen('https://example.com')
# Custom context
context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_3 # Require TLS 1.3
context.check_hostname = True
context.verify_mode = ssl.CERT_REQUIRED
response = urllib.request.urlopen('https://example.com', context=context)Python HTTPS Server
import ssl
from http.server import HTTPServer, SimpleHTTPRequestHandler
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('server.crt', 'server.key')
context.minimum_version = ssl.TLSVersion.TLSv1_2
server = HTTPServer(('0.0.0.0', 443), SimpleHTTPRequestHandler)
server.socket = context.wrap_socket(server.socket, server_side=True)
server.serve_forever()Mutual TLS (mTLS)
import ssl
# Server requiring client certificate
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain('server.crt', 'server.key')
server_context.load_verify_locations('client_ca.crt')
server_context.verify_mode = ssl.CERT_REQUIRED # Require client cert
# Client with certificate
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.load_cert_chain('client.crt', 'client.key')
client_context.load_verify_locations('server_ca.crt')12. Summary
Three things to remember:
TLS 1.3 is simpler and faster. One round trip handshake, mandatory forward secrecy, fewer cipher suite choices (all good ones).
The handshake combines ECDH + signatures + certificates. ECDH for key exchange, signatures prove server identity, certificates prove who owns the signing key.
Forward secrecy protects past sessions. Even if the server is compromised later, recorded traffic cannot be decrypted because ephemeral keys were deleted.
13. Whatโs Next
TLS protects data in transit. But what about data at rest, like user passwords? You canโt encrypt passwords (you need to verify them), so how do you store them safely?
In the next article: Password Storageโwhy you should never encrypt passwords, and how bcrypt, Argon2, and scrypt protect user credentials.
