TLS & Certificates How HTTPS works and how ProxyServer decrypts it

This guide explains TLS from first principles — what problem it solves, how certificates create trust, and how ProxyServer intercepts encrypted traffic by becoming a trusted man-in-the-middle.

The Problem HTTPS Solves

HTTP is plaintext. Anyone on the same network (a coffee shop, your ISP, a corporate firewall) can read your traffic:

HTTP (insecure): Browser ──── "POST /login password=secret" ────► Server │ Anyone can read this

HTTPS wraps HTTP inside TLS (Transport Layer Security), which provides two things:

  1. Encryption — The data is scrambled so only the intended recipient can read it
  2. Authentication — You can verify you're talking to the real server, not an impersonator
HTTPS (secure): Browser ══ encrypted tunnel ═════════════════════► Server │ Looks like random noise to anyone watching

How TLS Certificates Work

What Is a Certificate?

A TLS certificate is an electronic document (X.509 format) that binds a public key to a hostname. When you connect to example.com, the server sends its certificate which says "I am example.com, and here is my public key." Your browser uses the public key to set up an encrypted channel.

A certificate contains:

FieldExamplePurpose
SubjectCN=example.comWho this certificate belongs to
IssuerCN=DigiCertWho vouches for this certificate
Public Key(2048-bit RSA)Used to establish encrypted channel
SANDNS:example.com, DNS:www.example.comAll hostnames this cert is valid for
Validity2024-01-01 to 2026-01-01Expiration dates
Signature(SHA-256 with RSA)Cryptographic proof the issuer approved this

The Chain of Trust

How does your browser know to trust a certificate? Through a chain of trust:

Root CA (pre-installed in your OS/browser — DigiCert, Let's Encrypt, etc.) │ │ signs ▼ Intermediate CA (optional — the issuing CA) │ │ signs ▼ Server Certificate (example.com) ← the one the server presents
  1. Your OS/browser ships with ~150 Root CAs pre-trusted
  2. The server's certificate says "DigiCert signed me"
  3. The browser finds DigiCert in its trusted root store
  4. It verifies the cryptographic signature to confirm DigiCert actually signed it
  5. If the chain is valid and the hostname matches, the browser shows the lock icon
Key insight: Trust flows downward from root CAs. If you add a new root CA to your system, everything signed by that CA becomes trusted. This is exactly how ProxyServer works.

The TLS Handshake (Simplified)

Browser Server │ │ │── ClientHello (TLS version, ciphers) ────────►│ │ │ │◄── ServerHello (chosen cipher) + Certificate ─│ │ │ │ [Browser verifies certificate chain] │ │ [Browser checks hostname matches SAN] │ │ [Browser checks certificate not expired] │ │ │ │── Key Exchange (encrypted with server's ──────►│ │ public key) │ │ │ │◄═══════ Encrypted Channel Established ═══════►│ │ │ │── GET /api/data (encrypted) ──────────────────►│ │◄── 200 OK + JSON body (encrypted) ────────────│

After the handshake, both sides have agreed on a shared secret key. All subsequent data is encrypted with this key.

How MITM Interception Works

ProxyServer intercepts HTTPS by creating two separate TLS sessions:

Browser ProxyServer Real Server │ │ │ │── CONNECT host:443 ─►│ │ │◄── 200 Established ──│ │ │ │ │ │ ═══ TLS Session 1 ═══ │ │ │ (Browser ↔ Proxy) │ │ │ Proxy presents a │ │ │ fake cert for "host" │ │ │ signed by our CA │ │ │ │ ═══ TLS Session 2 ═══════ │ │ │ (Proxy ↔ Real Server) │ │ │ Uses real server's cert │ │ │ │ │── GET /path ────────►│ │ │ (encrypted in S1) │── GET /path ─────────────►│ │ │ (encrypted in S2) │ │ │ │ │ │◄── 200 OK ───────────────│ │◄── 200 OK ──────────│ │ │ │ │ │ Proxy sees plaintext between the two sessions

Step by Step

  1. CONNECT tunnel

    The browser sends CONNECT example.com:443. This is the HTTP method used to establish a tunnel through a proxy. ProxyServer responds with 200 Connection Established — the tunnel is open.

  2. Proxy generates a fake certificate

    Instead of blindly tunneling bytes, ProxyServer intercepts the TLS handshake. It generates a certificate for example.com on the fly, signed by its own CA (certs/ca.crt). Because you've trusted this CA, the browser accepts the fake cert.

  3. Client-side TLS session

    The browser completes its TLS handshake with the proxy, thinking it's talking to example.com. Data flows encrypted between browser and proxy.

  4. Server-side TLS session

    The proxy opens a separate TLS connection to the real example.com. It tries HTTP/2 first (via ALPN negotiation), falling back to HTTP/1.1.

  5. Plaintext in the middle

    The proxy decrypts data from the browser, inspects it, logs it to TrafficStore, then re-encrypts and sends it to the real server (and vice versa for responses).

ProxyServer's Certificate System

CA Certificate Generation

On first run, CertManager generates a self-signed root CA:

PropertyValue
AlgorithmRSA 2048-bit
Common NameProxyServer CA
Validity10 years
ExtensionsbasicConstraints: CA=true, keyUsage: keyCertSign
Filescerts/ca.key (private key), certs/ca.crt (certificate)
Protect your CA private key. Anyone with certs/ca.key can generate certificates trusted by your system. Never share it. If compromised, remove the CA from your trust store and delete the certs/ directory — a new CA will be generated on next run.

Per-Host Certificate Generation

When you visit example.com through the proxy for the first time:

  1. CertManager.getHostCert("example.com") is called
  2. It checks the memory cache (Map) — cache miss
  3. It checks the disk cache (certs/hosts/example.com.key / .crt) — cache miss
  4. It generates a new 2048-bit RSA key pair
  5. Creates a certificate with:
    • Subject: CN=example.com
    • Issuer: the CA certificate's subject
    • SAN (Subject Alternative Name): DNS:example.com
    • Validity: 2 years
  6. Signs it with the CA's private key
  7. Caches to disk and memory
Why SAN matters: Modern browsers (Chrome 58+, Firefox, Safari) require the Subject Alternative Name extension. They no longer check the Common Name (CN) field for hostname validation. Without a proper SAN, the browser will reject the certificate even if the CN matches.

Certificate Caching

Request for example.com cert: ┌─────────────┐ hit ┌──────────────┐ │ Memory Map │ ───────────► Return cached │ └──────┬──────┘ └──────────────┘ │ miss ▼ ┌─────────────┐ hit ┌──────────────┐ │ Disk Cache │ ───────────► Load → memory │ │ certs/hosts/│ │ → return │ └──────┬──────┘ └──────────────┘ │ miss ▼ ┌─────────────┐ ┌──────────────┐ │ Generate │ ───────────► Save to disk │ │ new cert │ │ + memory │ │ (~200-500ms)│ │ → return │ └─────────────┘ └──────────────┘

The first request to a new host takes 200-500ms due to RSA key generation (done in pure JavaScript by node-forge). Subsequent requests are instant.

Trusting the CA Certificate

macOS

# Add to system keychain (covers Chrome + Safari + most apps)
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain certs/ca.crt

# Verify it was added
security find-certificate -a -c "ProxyServer CA" /Library/Keychains/System.keychain

Firefox

Firefox uses its own certificate store and ignores the system keychain:

  1. Open Preferences > Privacy & Security
  2. Scroll to Certificates > click View Certificates
  3. Go to the Authorities tab
  4. Click Import and select certs/ca.crt
  5. Check "Trust this CA to identify websites"

Linux (Chrome/Chromium)

# Ubuntu/Debian
sudo cp certs/ca.crt /usr/local/share/ca-certificates/proxyserver-ca.crt
sudo update-ca-certificates

# For Chrome specifically
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," \
  -n "ProxyServer CA" -i certs/ca.crt

Removing the CA

# macOS — remove from system keychain
sudo security remove-trusted-cert -d certs/ca.crt

# Or delete everything and regenerate
rm -rf certs/
node server.js  # generates a fresh CA

Troubleshooting

ProblemCauseFix
SEC_ERROR_UNKNOWN_ISSUER in Firefox Firefox uses its own cert store Import ca.crt via Firefox preferences (see above)
ERR_CERT_AUTHORITY_INVALID in Chrome CA not trusted in system keychain Run the security add-trusted-cert command
Certificate errors after deleting certs/ Old CA was trusted; new CA is different Remove old CA from trust store, re-trust new one
Slow first HTTPS request per host RSA key generation in pure JS Normal — subsequent requests use cached cert
Certificate pinning errors (mobile apps) App verifies specific cert, not just CA chain MITM proxy cannot bypass pinning without app modification