Luke a Pro

Luke Sun

Developer & Marketer

๐Ÿ‡บ๐Ÿ‡ฆ

TLS Deep Dive: How HTTPS Actually Works

| , 13 minutes reading.

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 data

4. 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 keys

Step 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_secret

Step 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 keys

5. 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_2

7. 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 gone

How 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 keys

8. 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 key

0-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-RTT

9. 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 False

Version 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.com

11. 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:

  1. TLS 1.3 is simpler and faster. One round trip handshake, mandatory forward secrecy, fewer cipher suite choices (all good ones).

  2. The handshake combines ECDH + signatures + certificates. ECDH for key exchange, signatures prove server identity, certificates prove who owns the signing key.

  3. 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.