Every time you see the padlock in your browser, a sophisticated cryptographic handshake has just happened. Here is every step — with runnable JavaScript.
HTTP sends every byte — your passwords, cookies, credit card numbers — as raw readable text across a network. Anyone with access to any router between you and the server can read it. This isn't theoretical: coffee shop Wi-Fi, ISPs, and government surveillance systems all routinely capture HTTP traffic.
// Simulating what a passive network observer captures // In reality this is exactly what tools like Wireshark show on HTTP traffic function simulateHTTP(request) { // HTTP sends headers and body as raw text over TCP const rawPacket = [ `POST /login HTTP/1.1`, `Host: ${request.host}`, `Content-Type: application/x-www-form-urlencoded`, `Cookie: session_id=${request.cookie}`, ``, `username=${request.username}&password=${request.password}`, ].join("\r\n"); return rawPacket; // every router sees this } function simulateHTTPS(request) { // After TLS, the network only sees encrypted opaque bytes // The attacker can see: server IP, approximate data size, timing // The attacker CANNOT see: headers, path, cookies, body, anything meaningful const recordHeader = { contentType: 0x17, // 23 = Application Data version: 0x0303, // TLS 1.2 record layer (TLS 1.3 uses this for compat) length: 512, // encrypted + padded to hide true size }; // The payload is AEAD-encrypted — looks like random bytes to attacker const encryptedPayload = Array.from({length: 32}, () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0') ).join(' '); return { recordHeader, encryptedPayload, visibleToAttacker: "destination IP only" }; } const request = { host: "mybank.com", username: "alice", password: "hunter2", cookie: "abc123def456", }; console.log("=== HTTP (what the attacker sees) ==="); console.log(simulateHTTP(request)); console.log("\n=== HTTPS (what the attacker sees) ==="); const tls = simulateHTTPS(request); console.log("TLS Record Header:", JSON.stringify(tls.recordHeader)); console.log("Encrypted payload: ", tls.encryptedPayload); console.log("Visible to attacker:", tls.visibleToAttacker); console.log(`Password "${request.password}" is: completely hidden`);
TLS (Transport Layer Security) is a protocol that wraps any stream-based connection — most commonly HTTP — in three distinct security guarantees. Each one addresses a different attack.
| Property | What it means | Attack it prevents | Mechanism |
|---|---|---|---|
| Confidentiality | Data can only be read by sender and receiver | Passive eavesdropping (wiretapping, packet capture) | Symmetric encryption (AES-GCM) |
| Integrity | Data cannot be modified in transit undetected | Active manipulation (bit-flipping, injection) | Message authentication codes (HMAC / AEAD) |
| Authentication | Server's identity is verified | Impersonation, MITM (man-in-the-middle) | X.509 certificates + PKI trust chain |
Without confidentiality: attacker reads your data. Without integrity: attacker modifies it silently (inject malware into a downloaded file). Without authentication: attacker pretends to be your bank — even with encryption, you'd be securely talking to the wrong server.
Two completely different types of cryptography work together in TLS. Understanding why both are needed is the key to understanding the whole protocol.
Both parties use the same secret key to encrypt and decrypt. It's fast — AES can encrypt gigabytes per second in hardware. The problem: how do you share the key safely before you have a secure channel? You can't encrypt the key to send it — you'd need a key to do that.
Each party has a public key (freely shared) and a private key (secret). Anything encrypted with the public key can only be decrypted with the private key. Crucially, knowing the public key tells an attacker essentially nothing about the private key — it's a mathematical one-way function (modular exponentiation or elliptic curve multiplication).
// Illustrating the core ideas: symmetric uses one key, asymmetric uses a pair // Real TLS uses AES-256-GCM (symmetric) and X25519 (asymmetric DH) // === Symmetric: XOR cipher (simplified — real systems use AES) === function xorEncrypt(plaintext, key) { return [...plaintext].map((c, i) => String.fromCharCode(c.charCodeAt(0) ^ key.charCodeAt(i % key.length)) ).join(""); } const xorDecrypt = xorEncrypt; // XOR is its own inverse — same operation const sharedKey = "secret42"; const message = "Hello, server!"; const encrypted = xorEncrypt(message, sharedKey); const decrypted = xorDecrypt(encrypted, sharedKey); console.log("=== Symmetric ==="); console.log(`Plaintext: "${message}"`); console.log(`Ciphertext: ${[...encrypted].map(c=>c.charCodeAt(0).toString(16).padStart(2,'0')).join(' ')}`); console.log(`Decrypted: "${decrypted}" ✓`); // === Asymmetric: RSA-like modular exponentiation (toy parameters) === // Real RSA uses 2048-4096 bit numbers; ECDH uses elliptic curves function modPow(base, exp, mod) { // Fast modular exponentiation (square and multiply) let result = 1n; base = base % mod; while (exp > 0n) { if (exp % 2n === 1n) result = (result * base) % mod; exp = exp / 2n; base = (base * base) % mod; } return result; } // Toy RSA: p=61, q=53 → n=3233, e=17, d=2753 const n = 3233n, e = 17n, d = 2753n; const publicKey = { n, e }; // shared openly const privateKey = { n, d }; // kept secret const plainNum = 42n; const cipherNum = modPow(plainNum, publicKey.e, publicKey.n); // encrypt with public const recovered = modPow(cipherNum, privateKey.d, privateKey.n); // decrypt with private console.log("\n=== Asymmetric (toy RSA) ==="); console.log(`Public key: (n=${n}, e=${e})`); console.log(`Private key: (n=${n}, d=${d})`); console.log(`Plaintext: ${plainNum}`); console.log(`Encrypted: ${cipherNum} ← only private key can undo this`); console.log(`Decrypted: ${recovered} ✓`); // Why asymmetric is slow: const t0 = Date.now(); for (let i = 0; i < 1000; i++) modPow(42n, e, n); const asymMs = Date.now() - t0; const t1 = Date.now(); for (let i = 0; i < 1000000; i++) xorEncrypt("hello", "key"); const symMs = Date.now() - t1; console.log(`\n1000 asymmetric ops: ${asymMs}ms`); console.log(`1,000,000 symmetric ops: ${symMs}ms`); console.log(`Symmetric is ~${Math.max(1, Math.round((1000 * asymMs) / Math.max(1, symMs)))}× faster (even with toy params)`);
Diffie-Hellman (1976) solved the fundamental problem: how can two parties who have never met before agree on a shared secret over a public channel, where an eavesdropper hears everything they say?
The answer relies on a mathematical one-way function: modular exponentiation. Computing g^a mod p is easy. Reversing it — finding a given only g^a mod p, g, and p — is computationally infeasible for large numbers. This is the discrete logarithm problem.
// Diffie-Hellman Key Exchange — the full algorithm // Using small numbers for readability; TLS uses 2048-bit primes or elliptic curves function modPow(base, exp, mod) { let result = 1n; base = base % mod; while (exp > 0n) { if (exp & 1n) result = result * base % mod; exp >>= 1n; base = base * base % mod; } return result; } function dhKeyExchange(p, g) { // In TLS 1.3 this uses X25519 (elliptic curve DH with Curve25519) // Conceptually identical — just a different one-way function // Step 1: each party picks a random private key const randBigInt = (max) => BigInt(Math.floor(Math.random() * Number(max - 2n)) + 2); const a = randBigInt(p); // Alice's private key — NEVER shared const b = randBigInt(p); // Bob's private key — NEVER shared // Step 2: compute public keys (the one-way function) const A = modPow(g, a, p); // Alice's public key: g^a mod p const B = modPow(g, b, p); // Bob's public key: g^b mod p // Step 3: exchange public keys (attacker sees A and B — that's fine) console.log(`Public params: p=${p}, g=${g}`); console.log(`Alice private: a=${a} ← secret`); console.log(`Bob private: b=${b} ← secret`); console.log(`Alice public: A=${A} ← sent over network`); console.log(`Bob public: B=${B} ← sent over network`); // Step 4: each party computes the shared secret independently const aliceSecret = modPow(B, a, p); // Alice computes B^a mod p = g^(ba) mod p const bobSecret = modPow(A, b, p); // Bob computes A^b mod p = g^(ab) mod p console.log(`\nAlice computes: B^a mod p = ${aliceSecret}`); console.log(`Bob computes: A^b mod p = ${bobSecret}`); console.log(`Shared secret matches: ${aliceSecret === bobSecret} ✓`); // What the attacker knows: p, g, A, B — but NOT a, b, or the secret // To break this: solve the discrete logarithm problem — find a from A=g^a mod p // With 2048-bit primes this takes ~2^112 operations — computationally infeasible return aliceSecret; } // Small prime (real TLS uses RFC 3526 Group 14: a 2048-bit prime) const p = 23n; // prime modulus const g = 5n; // generator (primitive root mod 23) dhKeyExchange(p, g); // ECDH (Elliptic Curve DH) — TLS 1.3 default is X25519 // Instead of g^a mod p, uses point multiplication on an elliptic curve: a·G // Provides same security with much smaller key sizes (256 bits vs 3072 bits) console.log("\n--- Key size comparison for equivalent security ---"); console.log("RSA/DH (finite field): 3072 bits for 128-bit security"); console.log("ECDH (elliptic curve): 256 bits for 128-bit security"); console.log("Curve25519 key exchange: ~100µs on modern hardware");
Modern TLS 1.3 uses Elliptic Curve Diffie-Hellman (ECDH), specifically the X25519 curve. The mathematics is the same idea but operates on points on an elliptic curve rather than integers modulo a prime. This gives equivalent security with keys that are 12× smaller, and is resistant to certain attacks that affect finite-field DH.
TLS 1.3 reduced the handshake to a single round-trip (1-RTT). Here is every message, what it contains, and why it's there. The client sends its key share in the very first message, so the server can derive the session key immediately and start encrypting from its second message onward.
// TLS 1.3 key derivation schedule (simplified) // Real TLS uses HKDF-Extract and HKDF-Expand with SHA-256 // We simulate the structure to show how multiple keys derive from one shared secret function simpleHash(data) { // Toy hash — real TLS uses SHA-256 let h = 0x811c9dc5; for (const c of data) h = Math.imul(h ^ c.charCodeAt(0), 0x01000193) >>> 0; return h.toString(16).padStart(8, '0'); } function hkdfExtract(salt, ikm) { // HKDF-Extract: combine salt and input key material return simpleHash(salt + "|" + ikm); } function hkdfExpand(prk, label, length) { // HKDF-Expand: derive a specific sub-key with a label return simpleHash(prk + "|tls13 " + label).repeat(Math.ceil(length/8)).slice(0, length); } // === TLS 1.3 Key Schedule === const dhSharedSecret = "abc123def456"; // result of ECDH key exchange const clientRandom = "client_rand_32bytes"; const serverRandom = "server_rand_32bytes"; const transcript = clientRandom + serverRandom; // hash of all handshake messages console.log("=== TLS 1.3 Key Derivation Schedule ==="); // 1. Early secret (from pre-shared key, or zeros for fresh handshake) const earlySecret = hkdfExtract("0000", "0000"); console.log(`Early secret: ${earlySecret}`); // 2. Handshake secret — derived from DH shared secret const handshakeSecret = hkdfExtract(earlySecret, dhSharedSecret); console.log(`Handshake secret: ${handshakeSecret}`); // 3. Derive client and server handshake traffic keys const clientHsKey = hkdfExpand(handshakeSecret, "c hs traffic" + transcript, 32); const serverHsKey = hkdfExpand(handshakeSecret, "s hs traffic" + transcript, 32); console.log(`Client hs key: ${clientHsKey} ← encrypts client Finished`); console.log(`Server hs key: ${serverHsKey} ← encrypts Certificate, CertVerify, Finished`); // 4. Master secret — mixed with zeros (no further secret input in basic handshake) const masterSecret = hkdfExtract(handshakeSecret, "0000"); console.log(`Master secret: ${masterSecret}`); // 5. Application traffic keys — used for all HTTP data after handshake const clientAppKey = hkdfExpand(masterSecret, "c ap traffic" + transcript, 32); const serverAppKey = hkdfExpand(masterSecret, "s ap traffic" + transcript, 32); console.log(`Client app key: ${clientAppKey} ← encrypts all outgoing HTTP`); console.log(`Server app key: ${serverAppKey} ← encrypts all incoming HTTP`); // 6. Exporter master secret (for channel binding, QUIC, etc.) const exporterSecret = hkdfExpand(masterSecret, "exp master" + transcript, 32); console.log(`Exporter secret: ${exporterSecret}`); console.log("\nAll keys derived from one DH shared secret + handshake transcript."); console.log("Separate keys for client→server and server→client prevent reflection attacks.");
DH key exchange protects against passive eavesdroppers — but what if an attacker intercepts the connection and acts as a "man in the middle", performing DH with both sides separately? You'd be talking to the attacker, and they'd relay everything to the real server, seeing all your data.
Certificates solve this by binding an authentication public key to a verified identity. A Certificate Authority (CA) checks that you actually own the domain, then digitally signs a certificate asserting "this public key belongs to example.com". In TLS 1.3 that certificate key authenticates the server via signatures; the session secret still comes from the separate ephemeral Diffie-Hellman key shares. Your browser ships with ~150 trusted root CA certificates pre-installed.
Root CAs don't sign individual website certificates — the private key is kept offline. Instead they sign intermediate certificates, which in turn sign the website's leaf certificate. This limits exposure: if an intermediate is compromised, only revoke that intermediate.
// Certificate verification — what the browser does during the TLS handshake // The signature scheme here is deliberately toy-level: just enough structure to show // that verification recomputes a signature from cert contents + issuer public key. function simpleHash(data) { let h = 0x811c9dc5; for (const c of data) h = Math.imul(h ^ c.charCodeAt(0), 0x01000193) >>> 0; return h.toString(16).padStart(8, '0'); } class Certificate { constructor({subject, issuer, publicKey, validFrom, validTo, signature}) { this.subject = subject; this.issuer = issuer; this.publicKey = publicKey; // the server's authentication public key this.validFrom = new Date(validFrom); this.validTo = new Date(validTo); this.signature = signature; // CA's digital signature over this cert's fields } signedData() { return [ this.subject, this.issuer, this.publicKey, this.validFrom.toISOString().slice(0, 10), this.validTo.toISOString().slice(0, 10) ].join("|"); } isExpired(now = new Date()) { return now < this.validFrom || now > this.validTo; } matchesDomain(hostname) { // Wildcard matching: *.example.com matches sub.example.com if (this.subject === hostname) return true; if (this.subject.startsWith("*.")) { const base = this.subject.slice(2); return hostname.endsWith("." + base) && !hostname.slice(0, -base.length-1).includes("."); } return false; } } function toySign(cert, issuerPublicKey) { return "sig:" + simpleHash(cert.signedData() + "|" + issuerPublicKey); } function verifyCertChain(chain, hostname, trustedRoots, now = new Date()) { console.log(`Verifying chain for: ${hostname}`); const results = []; for (let i = 0; i < chain.length; i++) { const cert = chain[i]; const issuer = chain[i+1] || trustedRoots.find(r => r.subject === cert.issuer); // Check 1: is the certificate expired? if (cert.isExpired(now)) results.push(` ✗ [${cert.subject}] EXPIRED`); else results.push(` ✓ [${cert.subject}] validity period OK`); // Check 2: does the leaf cert match the hostname? if (i === 0) { if (cert.matchesDomain(hostname)) results.push(` ✓ [${cert.subject}] hostname matches`); else results.push(` ✗ [${cert.subject}] hostname mismatch!`); } // Check 3: does the signature verify under the issuer's public key? if (!issuer) { results.push(` ✗ [${cert.subject}] issuer "${cert.issuer}" not found in trust store`); } else if (cert.signature === toySign(cert, issuer.publicKey)) { results.push(` ✓ [${cert.subject}] signature verifies against ${issuer.subject}`); } else { results.push(` ✗ [${cert.subject}] invalid signature!`); } } results.forEach(r => console.log(r)); return !results.some(r => r.includes("✗")); } // Simulate a certificate chain const rootCA = new Certificate({ subject: "DigiCert Global Root CA", issuer: "DigiCert Global Root CA", publicKey: "rootPublicKey_abc123", validFrom: "2006-11-10", validTo: "2031-11-10", signature: "", }); rootCA.signature = toySign(rootCA, rootCA.publicKey); const intermediate = new Certificate({ subject: "DigiCert TLS RSA 2020 CA1", issuer: "DigiCert Global Root CA", publicKey: "interPublicKey_xyz789", validFrom: "2021-04-14", validTo: "2030-04-13", signature: "", }); intermediate.signature = toySign(intermediate, rootCA.publicKey); const leaf = new Certificate({ subject: "*.example.com", issuer: "DigiCert TLS RSA 2020 CA1", publicKey: "leafPublicKey_pqr456", validFrom: "2025-01-01", validTo: "2027-01-01", signature: "", }); leaf.signature = toySign(leaf, intermediate.publicKey); const chain = [leaf, intermediate]; const trusted = [rootCA]; console.log("=== Valid chain ==="); const ok = verifyCertChain(chain, "api.example.com", trusted); console.log(`Result: ${ok ? "TRUSTED ✓" : "UNTRUSTED ✗"}`); console.log("\n=== Hostname mismatch ==="); verifyCertChain(chain, "attacker.com", trusted);
HTTPS is not a separate protocol — it's the same HTTP/1.1 or HTTP/2 running inside a TLS tunnel. From the application's perspective, it just writes to a socket; TLS handles the encryption and authentication transparently.
The URL scheme (https://) and default port (443 instead of 80) tell the client to perform a TLS handshake before sending any HTTP. The server's Content-Security-Policy and Strict-Transport-Security headers can then enforce that future connections also use HTTPS.
// HTTPS protocol stack: each layer wraps the layer above it // Real implementations are event-driven (Node.js tls.connect → https.request) class TLSRecord { // TLS wraps arbitrary data into "records" of up to 16KB static wrap(data, contentType = "application_data") { return { type: contentType, // 20=change_cipher, 21=alert, 22=handshake, 23=app_data version: "TLS 1.2", // record layer stays 1.2 for compatibility; TLS 1.3 in extension length: data.length, payload: "[ENCRYPTED:" + data.slice(0, 20) + "...]", // AEAD encrypted authTag: "[16-byte GCM tag]", }; } static unwrap(record, sessionKey) { // Verify auth tag first — reject if tampered, before decrypting return "[decrypted plaintext]"; // real: AES-256-GCM decrypt } } // Simulate a full HTTPS request/response cycle function simulateHTTPS(url, method = "GET", body = null) { const { hostname, pathname } = new URL(url); console.log(`=== HTTPS ${method} ${url} ===\n`); // Layer 1: TCP (handled by OS) console.log("[TCP] SYN → SYN-ACK → ACK (1 round trip, ~20ms)"); // Layer 2: TLS (1 round trip in TLS 1.3) console.log("[TLS] ClientHello →"); console.log(" ← ServerHello + Certificate + Finished"); console.log("[TLS] → Finished (handshake complete, ~1 round trip)"); console.log("[TLS] Session established. Keys derived."); // Layer 3: HTTP (now runs inside the TLS tunnel) const httpRequest = [ `${method} ${pathname} HTTP/1.1`, `Host: ${hostname}`, `User-Agent: Mozilla/5.0`, `Accept: application/json`, body ? `Content-Length: ${body.length}` : null, ``, body, ].filter(Boolean).join("\r\n"); console.log("\n[HTTP] Plaintext request (only visible inside TLS tunnel):"); console.log(httpRequest); const tlsRecord = TLSRecord.wrap(httpRequest); console.log("\n[TLS] Encrypted record sent on wire:"); console.log(` type: ${tlsRecord.type}`); console.log(` payload: ${tlsRecord.payload}`); console.log(` authTag: ${tlsRecord.authTag}`); const httpResponse = `HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{"status":"ok"}`; console.log("\n[TLS] Server response (decrypted, delivered to HTTP layer):"); console.log(httpResponse); // HSTS: server can tell browser to ALWAYS use HTTPS for this domain console.log("\n[HTTP Header] Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"); console.log(" → Browser will refuse to connect via HTTP for the next 365 days"); } simulateHTTPS("https://api.example.com/user/profile"); console.log("\nTotal overhead vs plain HTTP:"); console.log(" First request: +1 RTT (TLS handshake) = ~50ms on typical connection"); console.log(" Subsequent: ~0ms (TLS session resumption / 0-RTT)");
Imagine an attacker records all encrypted TLS traffic today, then waits. Three years later they steal the server's private key. With older TLS (pre-1.3, RSA key exchange), they could now decrypt all that captured traffic retroactively — the session keys were derived from the server's long-term private key.
Perfect Forward Secrecy (PFS) prevents this. TLS 1.3 mandates ephemeral Diffie-Hellman: both sides generate fresh, random DH key pairs for every single session. The shared secret is derived from these ephemeral keys, not from the server's long-term certificate key. After the session ends, the ephemeral private keys are destroyed.
// Demonstrating forward secrecy: each session gets fresh ephemeral keys // Old TLS (RSA): session key derived from server's long-term key → NOT forward-secret // TLS 1.3 (ECDHE): session key derived from ephemeral keys → forward-secret function modPow(b, e, m) { let r = 1n; b %= m; while (e > 0n) { if (e & 1n) r = r*b%m; e >>= 1n; b = b*b%m; } return r; } class Session { constructor(id, p, g) { this.id = id; // Ephemeral DH key pair — generated fresh for this session only this.ephemeralPrivate = BigInt(Math.floor(Math.random() * 1000) + 2); this.ephemeralPublic = modPow(g, this.ephemeralPrivate, p); this.sessionKey = null; this.p = p; this.g = g; } deriveSharedSecret(peerPublic) { this.sessionKey = modPow(peerPublic, this.ephemeralPrivate, this.p); return this.sessionKey; } destroy() { // Critical: wipe ephemeral keys from memory after session ends console.log(` Session ${this.id}: wiping ephemeral private key ${this.ephemeralPrivate} from memory`); this.ephemeralPrivate = 0n; // gone forever this.sessionKey = null; } } const p = 23n, g = 5n; const serverLongTermKey = "server_cert_private_key"; // NEVER used for key derivation in TLS 1.3 console.log("=== Three independent sessions (same server, different times) ==="); const sessionKeys = []; for (let i = 1; i <= 3; i++) { const server = new Session(`server-${i}`, p, g); const client = new Session(`client-${i}`, p, g); const sk = client.deriveSharedSecret(server.ephemeralPublic); server.deriveSharedSecret(client.ephemeralPublic); console.log(`Session ${i}: key=${sk} (server ephem pub=${server.ephemeralPublic})`); sessionKeys.push(sk); server.destroy(); client.destroy(); } console.log("\nAll session keys are different (fresh ephemeral keys each time):"); console.log(` ${sessionKeys.join(", ")}`); console.log(` Same? ${sessionKeys.every(k => k === sessionKeys[0])}`); console.log(`\n=== Server's long-term key is compromised ===`); console.log(`Long-term key: "${serverLongTermKey}"`); console.log("Can attacker now decrypt past sessions? NO — ephemeral keys were destroyed."); console.log("Attacker can impersonate the server going forward, but past is safe."); console.log("\n=== OLD TLS (RSA key exchange, no PFS) ==="); console.log("Client encrypts a random PreMasterSecret using server's certificate public key."); console.log("Session key is derived from PreMasterSecret."); console.log("If attacker records traffic NOW and steals server key LATER:"); console.log(" → Decrypt the PreMasterSecret → derive session key → decrypt all traffic. ✗");
In TLS 1.2 and earlier, a cipher suite named most of the handshake and record-protection choices together. TLS 1.3 simplifies this sharply: the cipher suite now specifies only the record-protection AEAD and the HKDF hash. The key exchange group (such as X25519) and the authentication algorithm (such as RSA-PSS or ECDSA) are negotiated separately.
| TLS 1.3 cipher suite | Record protection (AEAD) | HKDF hash | Typical note |
|---|---|---|---|
| TLS_AES_256_GCM_SHA384 | AES-256-GCM | SHA-384 | High-security GCM option |
| TLS_AES_128_GCM_SHA256 | AES-128-GCM | SHA-256 | Common default on AES-accelerated CPUs |
| TLS_CHACHA20_POLY1305_SHA256 | ChaCha20-Poly1305 | SHA-256 | Often preferred on mobile / no AES acceleration |
| TLS_AES_128_CCM_SHA256 | AES-128-CCM | SHA-256 | Niche constrained-device profile |
| TLS_AES_128_CCM_8_SHA256 | AES-128-CCM-8 | SHA-256 | Very constrained environments; shorter tag |
X25519 for the ephemeral DH share and rsa_pss_rsae_sha256 or ecdsa_secp256r1_sha256 for certificate signatures.| Legacy TLS 1.2 suites | Key exchange | Auth | Encryption | Status |
|---|---|---|---|---|
| TLS_RSA_WITH_AES_128_GCM_SHA256 | RSA (no PFS) | RSA | AES-128-GCM | Deprecated — no forward secrecy |
| TLS_RSA_WITH_RC4_128_MD5 | RSA | RSA | RC4 (broken) | Prohibited — RC4 cryptographically broken |
| TLS_DHE_RSA_WITH_AES_128_CBC_SHA | DHE | RSA | CBC (BEAST/POODLE risk) | Avoid — CBC padding oracle attacks |
Since 2018, Chrome requires all certificates to be logged in public Certificate Transparency (CT) logs. Every CA must submit every issued certificate to an append-only, cryptographically verifiable log. This means any certificate misissued by a rogue or compromised CA is publicly visible within seconds — enabling rapid detection and revocation.
HSTS tells browsers to always use HTTPS for a domain, even if the user types http://. The preload directive goes further — the domain is hardcoded into browser source code, preventing even the first HTTP connection.
// Real-world TLS features: AEAD, session resumption (0-RTT), HSTS // === AEAD: Authenticated Encryption with Associated Data === // AES-GCM provides confidentiality + integrity in one operation // "Associated data" = TLS record header (authenticated but not encrypted) function simulateAEAD(key, nonce, plaintext, associatedData) { // Real: AES-256-GCM. Simulated with XOR + simple auth tag const keyStream = [...plaintext].map((_,i) => String.fromCharCode(key.charCodeAt(i%key.length) ^ nonce.charCodeAt(i%nonce.length))); const ciphertext = [...plaintext].map((c,i) => (c.charCodeAt(0) ^ keyStream[i].charCodeAt(0)).toString(16).padStart(2,'0')).join(''); // Auth tag covers BOTH ciphertext AND associated data (header) // Any modification to either invalidates the tag → rejected before decryption let tag = 0; [...ciphertext, ...(associatedData||'')].forEach(c => tag = (tag*31 + c.charCodeAt(0)) & 0xffff); return { ciphertext, tag: tag.toString(16).padStart(4,'0'), nonce }; } const sessionKey = "derived-session-key"; const seqNum = "000000000001"; // TLS uses sequence number as nonce to prevent replay const tlsHeader = "17 0303 0028"; // content-type, version, length — NOT encrypted but authenticated const httpData = "GET /secret HTTP/1.1\r\nHost: bank.com"; const { ciphertext, tag } = simulateAEAD(sessionKey, seqNum, httpData, tlsHeader); console.log("=== AEAD Encryption ==="); console.log(`Plaintext: "${httpData}"`); console.log(`Ciphertext: ${ciphertext.slice(0,40)}...`); console.log(`Auth tag: ${tag} ← covers ciphertext + TLS header`); console.log("If attacker flips ANY bit in ciphertext or header: tag mismatch → REJECTED"); // === Session Resumption / 0-RTT === console.log("\n=== TLS Session Resumption ==="); const sessionTicket = { // Server sends this encrypted ticket at end of handshake // Client presents it on next connection — server can skip full handshake resumptionSecret: "res_master_secret_xyz", maxEarlyData: 16384, // 0-RTT: client can send this many bytes before server Finished lifetime: 7200, // seconds }; console.log(`Session ticket issued: lifetime=${sessionTicket.lifetime}s, 0-RTT allowed=${sessionTicket.maxEarlyData}B`); console.log("Next connection: client sends early data (0-RTT) with first packet — saves 1 RTT"); console.log("Warning: 0-RTT is NOT forward secret and is replay-vulnerable → use for idempotent GET only"); // === HSTS === console.log("\n=== HTTP Strict Transport Security ==="); const hstsHeader = "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload"; console.log(`Header: ${hstsHeader}`); console.log(`max-age=63072000 → HTTPS required for next ${63072000/86400/365} years`); console.log("includeSubDomains → applies to *.example.com too"); console.log("preload → submit to hstspreload.org → hardcoded into Chrome/Firefox source"); console.log("Effect: browser converts http:// → https:// BEFORE making any network connection");