Toolmingo
Guides10 min read

How HTTPS Works: SSL/TLS Explained

Learn how HTTPS secures web traffic. Covers TLS handshake, certificates, symmetric vs asymmetric encryption, certificate authorities, and how to implement HTTPS in Node.js, Python, Go, and nginx.

HTTPS is HTTP with a TLS layer on top. That layer encrypts every byte in transit, authenticates the server, and prevents tampering. This guide explains the mechanics step by step — from the handshake to certificate chains — plus how to set it up in code.

Quick reference

Concept Key fact
Protocol stack HTTP → TLS → TCP → IP
Default port 443 (HTTP uses 80)
Current version TLS 1.3 (2018), widely supported
Deprecated SSL 2.0, SSL 3.0, TLS 1.0, TLS 1.1
Key exchange ECDHE (ephemeral, forward secrecy)
Bulk cipher AES-128-GCM or AES-256-GCM
Certificate type X.509, signed by a Certificate Authority
Free certificates Let's Encrypt (90-day, auto-renewable)
HSTS Forces HTTPS in browser for a domain
Mixed content HTTP resources on HTTPS page — blocked by browsers

HTTP vs HTTPS

Feature HTTP HTTPS
Encryption None TLS (symmetric AES)
Authentication None Server cert signed by CA
Integrity None AEAD cipher (tamper-evident)
Port 80 443
Speed Slightly faster Negligible difference with TLS 1.3
SEO Lower ranking Google ranking boost
Browser indicator ⚠ Not secure 🔒
Required for Service Workers, HTTP/2, geolocation, camera

HTTPS is not optional for modern web apps. Browsers block Service Workers, HTTP/2, and many APIs (camera, geolocation, clipboard) on plain HTTP.

Symmetric vs asymmetric encryption

Symmetric encryption: the same key encrypts and decrypts. Fast. Problem: how do two strangers agree on a shared key without an eavesdropper learning it?

Asymmetric encryption: a key pair (public + private). Anything encrypted with the public key can only be decrypted with the private key. Slow for bulk data. Used to establish a shared secret.

HTTPS uses both:

  1. Asymmetric crypto during the handshake to agree on a shared secret.
  2. Symmetric AES for all subsequent data — fast and efficient.

The TLS 1.3 handshake

TLS 1.3 completes in one round trip (vs two for TLS 1.2).

Client                              Server
  |                                   |
  |-- ClientHello ------------------>|
  |   (supported ciphers, key share) |
  |                                   |
  |<-- ServerHello ------------------|
  |    Certificate                    |
  |    CertificateVerify              |
  |    Finished                       |
  |   (server's key share)           |
  |                                   |
  |-- Finished -------------------->|
  |                                   |
  |<====== Encrypted data ===========>|

Step by step

1. ClientHello — browser sends:

  • TLS version (1.3)
  • List of supported cipher suites (e.g., TLS_AES_128_GCM_SHA256)
  • Key share: a temporary public key for ECDHE key exchange
  • Random nonce

2. ServerHello + Certificate — server responds with:

  • Chosen cipher suite
  • Server's ECDHE public key (its half of the key exchange)
  • Its X.509 certificate (containing the server's RSA/EC public key, domain name, validity dates, CA signature)

At this point, both sides can compute the same shared secret using ECDHE — without it ever crossing the wire.

3. CertificateVerify — server signs the handshake transcript with its private key, proving it owns the certificate.

4. Finished — both sides send a MAC over the handshake, confirming nothing was tampered with.

5. Application data — all subsequent bytes are encrypted with AES-GCM using session keys derived from the shared secret.

How ECDHE key exchange works

ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) lets two parties compute the same number without transmitting it.

# Conceptual (simplified)
# Both agree on a public curve and base point G

Server: picks random s → computes S = s × G  → sends S
Client: picks random c → computes C = c × G  → sends C

Server computes: s × C = s × c × G
Client computes: c × S = c × s × G  ← same value!

# Eavesdropper sees G, S, C — cannot compute s×c×G
# (discrete log problem on elliptic curves)

"Ephemeral" means a fresh key pair is generated per connection. Even if the server's long-term private key is later stolen, past sessions are safe — this property is called forward secrecy.

X.509 certificates

A certificate is a signed document that binds a public key to a domain name.

Certificate fields:
  Subject:    CN=example.com, O=Example Inc
  Issuer:     CN=Let's Encrypt R3
  Valid from: 2026-01-01
  Valid until:2026-04-01
  Public key: EC (prime256v1) — server's public key
  SANs:       example.com, www.example.com
  Signature:  SHA256withECDSA (signed by Let's Encrypt R3)

The browser trusts it because:

  1. Let's Encrypt R3 signed it → verify with Let's Encrypt's public key.
  2. Let's Encrypt R3's cert is signed by ISRG Root X1 (a root CA).
  3. ISRG Root X1 is in the browser's trust store (pre-installed root CAs).

This chain of trust is called the certificate chain or PKI (Public Key Infrastructure).

ISRG Root X1 (root CA — self-signed, in browser trust store)
  └── Let's Encrypt R3 (intermediate CA)
        └── example.com (end-entity certificate)

Certificate types

Type Validation Use case
DV (Domain Validated) Proves domain control only Most websites, Let's Encrypt
OV (Organization Validated) Domain + org identity checked Corporate sites
EV (Extended Validation) Deep org vetting Banks, payment processors
Wildcard *.example.com covers subdomains Multi-subdomain setups
SAN Multiple domains in one cert example.com + api.example.com
Self-signed No CA, browser shows error Local dev only

How to get a certificate

Let's Encrypt with Certbot (nginx)

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Obtain and install certificate
sudo certbot --nginx -d example.com -d www.example.com

# Auto-renewal (certbot sets up a systemd timer or cron)
sudo certbot renew --dry-run

# Check renewal timer
sudo systemctl status certbot.timer

Certbot edits your nginx config to enable HTTPS and redirect HTTP → HTTPS.

Manual certificate (nginx config)

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Modern TLS settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # OCSP stapling (faster cert validation)
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 1.0.0.1 valid=300s;

    # HSTS: tell browsers to always use HTTPS for 1 year
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Redirect HTTP → HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

HTTPS in code

Node.js (built-in https module)

import https from 'https';
import fs from 'fs';
import { createServer } from 'http';

// HTTPS server
const options = {
  cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem'),
  key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
};

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('Hello HTTPS\n');
}).listen(443);

// Redirect HTTP → HTTPS
createServer((req, res) => {
  res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
  res.end();
}).listen(80);

In production, use a reverse proxy (nginx, Caddy) for TLS termination and run Node.js on a plain HTTP internal port.

Node.js — making HTTPS requests

// fetch uses HTTPS automatically for https:// URLs
const response = await fetch('https://api.example.com/data');
const data = await response.json();

// Self-signed cert in dev (Node.js)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // dev only, never production

Python (ssl module + requests)

import ssl
import http.server

# HTTPS server
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('/path/to/fullchain.pem', '/path/to/privkey.pem')

server = http.server.HTTPServer(('', 443), http.server.SimpleHTTPRequestHandler)
server.socket = context.wrap_socket(server.socket, server_side=True)
server.serve_forever()

# Making HTTPS requests
import requests

# Normal HTTPS (verified by default)
response = requests.get('https://api.example.com/data')

# Custom CA bundle
response = requests.get('https://internal.corp', verify='/path/to/ca-bundle.pem')

# Self-signed cert in dev (don't use in production)
response = requests.get('https://localhost', verify=False)

Go (standard library)

package main

import (
    "crypto/tls"
    "net/http"
    "log"
)

func main() {
    // HTTPS server
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello HTTPS"))
    })

    server := &http.Server{
        Addr:    ":443",
        Handler: mux,
        TLSConfig: &tls.Config{
            MinVersion: tls.VersionTLS12,
            CurvePreferences: []tls.CurveID{
                tls.X25519,
                tls.CurveP256,
            },
        },
    }

    log.Fatal(server.ListenAndServeTLS(
        "/etc/letsencrypt/live/example.com/fullchain.pem",
        "/etc/letsencrypt/live/example.com/privkey.pem",
    ))
}

// Making HTTPS requests (Go verifies certs by default)
resp, err := http.Get("https://api.example.com/data")

HSTS (HTTP Strict Transport Security)

HSTS tells browsers to always use HTTPS for a domain, even if the user types http://.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Directive Meaning
max-age=31536000 Remember for 1 year
includeSubDomains Also applies to *.example.com
preload Submit to browser preload list (hardcoded HTTPS)

HSTS preload: submit your domain to hstspreload.org and it's baked into Chrome, Firefox, Safari. Even the very first request is HTTPS. Irreversible for ~1 year — only add if you're committed to HTTPS forever.

Certificate validation by the browser

When a browser receives a server certificate, it checks:

  1. Chain of trust: every certificate up the chain is signed by the next, ending at a trusted root CA.
  2. Hostname match: the cert's Subject Alternative Names (SANs) include the domain being visited.
  3. Validity period: current date is between notBefore and notAfter.
  4. Revocation: OCSP (Online Certificate Status Protocol) or CRL — has the CA revoked this cert?
  5. Signature algorithm: not MD5 or SHA-1 (deprecated).

If any check fails, the browser shows an error and blocks the page.

OCSP stapling

Without stapling, the browser queries the CA's OCSP server on every page load — slow and privacy-leaking (CA learns which sites you visit).

With OCSP stapling, the web server periodically fetches its own OCSP response and "staples" it to the TLS handshake. The browser gets fresh revocation status without a round trip to the CA.

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;
resolver_timeout 5s;

Common mistakes

Mistake Problem Fix
Expired certificate Browser error, site down Auto-renew with Certbot, monitor expiry dates
Self-signed cert in production Users see security warning Get a DV cert (free via Let's Encrypt)
Missing intermediate cert Some clients fail Serve fullchain.pem, not just cert.pem
TLS 1.0/1.1 enabled Vulnerable to POODLE, BEAST Allow only TLS 1.2+
HTTP resources on HTTPS page Mixed content blocked Update all URLs to HTTPS, use CSP
No HSTS header Downgrade attacks possible Add Strict-Transport-Security header
Storing private key insecurely Key theft → impersonation chmod 600, owned by root, never commit to git
NODE_TLS_REJECT_UNAUTHORIZED=0 in production Disables cert verification Only in dev, remove before deploy

Debugging HTTPS

# Check certificate details
openssl s_client -connect example.com:443 -servername example.com

# View cert expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

# Test TLS version and cipher support
nmap --script ssl-enum-ciphers -p 443 example.com

# Test from command line (follows redirects, shows cert info)
curl -vI https://example.com 2>&1 | grep -E "subject|issuer|expire|TLS|SSL"

# Check HSTS header
curl -sI https://example.com | grep -i strict

# Test HTTP → HTTPS redirect
curl -sI http://example.com | grep -i location

# Online tools
# https://www.ssllabs.com/ssltest/ — full TLS configuration audit
# https://securityheaders.com — check security headers

FAQ

What's the difference between SSL and TLS?
SSL (Secure Sockets Layer) was the original protocol. It was deprecated because of serious vulnerabilities (SSLv3/POODLE). TLS (Transport Layer Security) is its successor. TLS 1.0 and 1.1 are also deprecated. Today you should use TLS 1.2 or 1.3. Despite this, people still say "SSL certificate" colloquially — they mean a TLS certificate.

Does HTTPS hide the URL from my ISP?
The domain name is visible (in DNS queries and in the TLS SNI field). The path, query string, request body, and headers are encrypted. Your ISP sees example.com but not /login?user=alice. Use a VPN or Tor to hide the domain too.

What is a wildcard certificate?
A cert for *.example.com covers any single-level subdomain: api.example.com, www.example.com, app.example.com. It does not cover deep.api.example.com or example.com itself (though you can add example.com as a SAN).

Can I get HTTPS for localhost?
Yes. Use mkcert — a tool that creates locally trusted certs:

brew install mkcert  # or choco install mkcert on Windows
mkcert -install       # installs a local root CA into your system trust store
mkcert localhost 127.0.0.1  # creates localhost.pem + localhost-key.pem

How long does a TLS certificate last?
As of September 2020, browser-trusted certificates are valid for a maximum of 398 days. Let's Encrypt issues 90-day certs and recommends auto-renewal every 60 days. Apple and Google are pushing for 47-day max certs starting 2026.

What is certificate pinning?
Mobile apps can "pin" a specific cert or public key and refuse connections to any other cert — even a valid one. It defeats CA compromise attacks but breaks when the cert rotates. Modern recommendation: pin the public key (HPKP is deprecated; use TrustKit or similar library), not the cert itself.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools