Skip to main content

All weeks · Worksheet · Overview

Week 12 · Lecture slides

Week 12

Contents16 sections

Secure Transport / TLS (certificate-validation-bypass MITM)

Security & Cryptography · Nutthakorn Chalaemwongwan


Today

  • The TLS handshake: what it authenticates — and what it doesn't
  • X.509 certificates & the CA chain of trust
  • Hostname verification (SAN, RFC 6125)
  • Live MITM: a client that skips certificate validation
  • 🎭 Game: The Impostor's Certificate

Recap — Week 11

  • A digital signature lets anyone with the public key verify a claim — no shared secret needed
  • A signature is also a non-interactive zero-knowledge proof: it convinces you the signer knows the private key, without revealing it
  • This week: a certificate is a CA's signature over "this key belongs to this name" — watch what happens when nobody checks it

The TLS handshake — three jobs

  • Key exchange — negotiate a shared session key → confidentiality
  • Server authentication — the server presents a certificate claiming an identity
  • CertificateVerify — server proves it holds the private key matching that cert
  • Skip any one of the three and a machine-in-the-middle is back on the table

X.509 certificates & the chain of trust

  • A certificate is a claim: "this public key belongs to bob"
  • Anyone can generate a certificate that claims to be bob — self-signing costs nothing and needs no permission
  • A Certificate Authority's signature is what makes the claim checkable — your OS/browser already trusts the CA, so a CA-signed cert inherits that trust
  • No signature from a trusted CA → no chain → nothing backs the claim

Hostname verification

  • A valid chain of trust still isn't enough — the name in the cert must match the name you meant to connect to
  • Check the SAN (Subject Alternative Name) — RFC 6125 — not just the legacy CN field
  • In this week's lab, both the real and impostor certs carry CN=bob, SAN=DNS:bob — hostname matching alone cannot tell them apart
  • You need both: chain of trust and hostname match

The lab: 4 services, one Docker network

ServiceRoleWhat it does
gen_certs.pyinitgenerates a demo CA, Bob's CA-signed cert, and a self-signed impostor (also CN=bob)
bob.pylegit servermostly idle — Alice never dials it directly
mitm.pyattackerpresents the impostor cert; logs MITM INTERCEPTED: <secret> on success
alice.pyclientdials mitm, believing it's bob; behavior set by env var VERIFY

The break: CERT_NONE, check_hostname=False

ALICE: TLS handshake succeeded WITHOUT verifying the server cert
ALICE: sent encrypted message ('the vault code is 7731')
MITM INTERCEPTED: the vault code is 7731
  • CWE-295 — Improper Certificate Validation
  • CWE-300 — Channel Accessible by Non-Endpoint
  • The handshake genuinely succeeds — real crypto, real session key — just negotiated with the wrong party

"Encrypted" ≠ "encrypted to the right party"

  • Encryption protects a channel; only certificate validation ties that channel to a specific identity
  • CertificateVerify even passes — the MITM genuinely holds the impostor cert's private key
  • check_hostname=False + CERT_NONE = Alice never asks "is this actually bob?"
  • Whoever answers when Alice dials mitm:8443 gets treated as bob — no questions asked

The fix: VERIFY=1 — load the CA, keep hostname checking

ALICE: connecting to mitm:8443 as if it were 'bob' -- FIXED (VERIFY=1)
CERT VERIFICATION FAILED - ABORTING
ALICE: (reason: self-signed certificate)
  • mitm.py is byte-for-byte unchanged — same impostor cert, same attempt
  • Alice now trusts only the demo CA; the impostor has no chain to it
  • Fails during the handshake, before the secret is sent
  • Reason given: a trust/issuer error — not a hostname mismatch (the name was correct all along)

The real-world version of this bug

  • verify=False (Python requests) · CERT_NONE (Python ssl) · InsecureSkipVerify (Go)
  • Almost always copy-pasted to silence an SSL error — "just make it work"
  • "It's HTTPS, so it's encrypted" is true — and irrelevant if nobody checked who is on the other end
  • This is the single most common real-world TLS mistake — not a CA compromise, not a crypto break

Beyond the demo — this week's essay arm

  • Why SSL was retired, what TLS 1.3 changed
  • Forward secrecy — each session gets an ephemeral key, so a leaked long-term key can't decrypt past traffic (contrast: legacy static-RSA key exchange, removed in TLS 1.3 — see Heartbleed as the case for why that mattered)
  • 0-RTT — the performance-vs-replay trade-off
  • Mitigations for a compromised CA: pinning, revocation (CRL/OCSP), Certificate Transparency
  • TLS vs. the Noise Protocol Framework

🎭 Game — "The Impostor's Certificate"

  • You play the attacker: stand in for the server with nothing but a self-signed cert you generated yourself
  • Watch a careless client hand you its secret — "encrypted" the whole time
  • Flip on real certificate validation — the same impostor cert gets rejected before a single byte ships

Lab today — Worksheet 12

📋 Worksheet 12 — labs/week12-secure-transport/worksheet.md · kickoff: docker compose -f docker-compose.vulnerable.yml up --build

  • Part 1 (essays): SSL→TLS, handshake security roles, forward secrecy, 0-RTT, Web PKI, TLS vs Noise
  • Part 2a: capture vulnerable + fixed logs, then write the fix yourself in alice_student.py
  • 🤖 Audit the AI: critique a verify=False "fix" that an AI assistant swears is secure
  • EiPE + Prompt Problem + viva prep

Key takeaways

  • TLS's crypto is textbook-sound — key exchange and signatures both work exactly as designed
  • Textbook-secure primitive, real-system failure: the bug was skipping validation, not broken math
  • "Encrypted" and "encrypted to the right endpoint" are different claims — only certificate validation bridges them

Questions?

Next week: End-to-end encryption

All weeks in Security & Cryptography