Frontend Labs
⋯

Chapter 18 — Diffie–Hellman key exchange

In one sentence

Diffie–Hellman lets two parties who have never met derive an identical shared secret by exchanging public values, such that an eavesdropper who sees everything still cannot compute it.

The idea: mixing paint

The standard analogy, and it's a good one:

   1. Alice and Bob publicly agree on a common paint colour   (yellow).
   2. Each secretly picks a private colour and mixes it with yellow.
        Alice:  yellow + red    = orange       (she sends orange to Bob)
        Bob:    yellow + blue   = green         (he sends green to Alice)
   3. Each adds their OWN secret colour to the paint they RECEIVED.
        Alice:  green  + red    = brown
        Bob:    orange + blue   = brown
   4. Both now hold BROWN. The eavesdropper saw yellow, orange, and green --
      and cannot un-mix them to find red or blue, so cannot make brown.

"Un-mixing paint is hard" stands in for a real mathematical one-way operation.

How it actually works (the maths, kept small)

Everything happens in modular arithmetic (§0.5) — clock arithmetic — which makes the operations easy to do forwards and infeasible to reverse.

   PUBLIC PARAMETERS (everyone knows these, they can be baked into a standard):
     p = a large prime          (toy value: 23)
     g = a generator            (toy value: 5)

   ALICE                                   BOB
     picks secret a = 6                      picks secret b = 15
     A = g^a mod p                           B = g^b mod p
       = 5^6  mod 23 = 8                       = 5^15 mod 23 = 19
     --- sends A = 8 ------------------->
                            <------------- sends B = 19 ---
     s = B^a mod p                           s = A^b mod p
       = 19^6 mod 23 = 2                       = 8^15 mod 23 = 2

   BOTH now share s = 2.  It works because  (g^a)^b = g^(ab) = (g^b)^a  mod p.

The eavesdropper has p, g, A=8, B=19. To find s they must recover a from A = g^a mod p — the discrete logarithm problem. For a 2048-bit prime, nobody knows how to do this in less than astronomical time.

Real parameters: p is 2048–4096 bits (a standardised "MODP group" or, far better, an elliptic curve — Chapter 20). The secrets are similarly large random numbers.

The property that makes DH special: forward secrecy

If Alice and Bob generate a fresh a and b for every session and throw them away afterwards, then:

   An attacker who records all the encrypted traffic today, and YEARS LATER
   steals Alice's long-term private key, STILL cannot decrypt that traffic.

   The session secret depended on ephemeral values that no longer exist.

This is forward secrecy (sometimes "perfect forward secrecy", PFS). It's why modern TLS always uses ephemeral Diffie–Hellman (ECDHE) and why RSA key exchange was removed entirely in TLS 1.3. The letter E on the end — DHE, ECDHE — means "ephemeral" and is the part that matters.

   DH   -- static, keys reused        -> NO forward secrecy   (don't)
   DHE  -- ephemeral, fresh per       -> forward secrecy       (finite-field)
           session
   ECDHE-- ephemeral, elliptic curve  -> forward secrecy, fast (USE THIS)

What DH does NOT give you

   DH gives you a shared secret with SOMEONE.
   It does not tell you WHO.

Raw DH is wide open to a man-in-the-middle: Mallory does a separate DH exchange with each of Alice and Bob, sits in the middle, and relays (re-encrypted) traffic. Both think they have a private channel; Mallory reads everything.

The fix is authentication: one or both parties sign their DH public value with a long-term key whose owner you can verify (via a certificate — Part 7, or a known SSH host key — Part 8). DH + signatures is the actual handshake.

Worked example (run it)

cd ~/sec-lab/scratch

python3 - <<'PY'
# Toy DH by hand
p, g = 23, 5
a, b = 6, 15                                  # secrets
A, B = pow(g, a, p), pow(g, b, p)            # public values, sent on the wire
s_alice = pow(B, a, p)
s_bob   = pow(A, b, p)
print(f"A (Alice sends) = {A}")
print(f"B (Bob sends)   = {B}")
print(f"Alice computes s = {s_alice}")
print(f"Bob computes   s = {s_bob}")
assert s_alice == s_bob
print("shared secret:", s_alice)

# The eavesdropper's task: find 'a' such that 5^a mod 23 == 8  (discrete log)
for guess in range(p):
    if pow(g, guess, p) == A:
        print(f"eavesdropper brute-forced a = {guess} (trivial at p=23,")
        print(" utterly infeasible at p = 2^2048)")
        break
PY

Real X25519 (the modern, elliptic-curve DH):

cd ~/sec-lab/scratch
python3 -m venv .venv 2>/dev/null; .venv/bin/pip install -q cryptography

.venv/bin/python - <<'PY'
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey

alice = X25519PrivateKey.generate()
bob   = X25519PrivateKey.generate()

# each sends the other their PUBLIC key, then:
s_alice = alice.exchange(bob.public_key())
s_bob   = bob.exchange(alice.public_key())

print("Alice's shared secret:", s_alice.hex())
print("Bob's   shared secret:", s_bob.hex())
print("identical:", s_alice == s_bob, " length:", len(s_alice), "bytes")
# You'd now run this 32-byte secret through a KDF (HKDF) to get AEAD keys.
PY

Note: LibreSSL (the openssl shipped with macOS) does not support X25519 via the CLI, and its genpkey output can trip up pkeyutl. This is the §0.4 trap. Use the Python cryptography library, or install real OpenSSL 3.x (brew install openssl@3) and put it first on your PATH.

Practice (30 min)

  1. Run the toy DH above. Change a and b; confirm both sides still agree.
  2. Add a Mallory who runs DH separately with each side. Show that Alice and Bob end up with different secrets (Mallory's with each), and that Mallory can read both. This is the MITM — feel how easy it is without authentication.
  3. Run the X25519 example. Feed the shared secret through HKDF (cryptography.hazmat.primitives.kdf.hkdf) to derive a 32-byte AES key.
  4. In notes.md, explain in your own words why recording traffic today and stealing a key tomorrow doesn't break an ECDHE session.

Common confusions

  • "DH encrypts my data." It does not. It produces a shared secret. You then run it through a KDF and encrypt with AEAD.
  • "DH proves who I'm talking to." No. Unauthenticated DH is MITM-able. It needs signatures on top.
  • "Static DH is fine." It works, but you lose forward secrecy. Always use the ephemeral variant (DHE/ECDHE).
  • "The shared secret is the AES key." Use it as input to a KDF, don't use it directly. Raw DH output isn't uniformly random and may have small subgroup quirks.

Check yourself

  1. In the paint analogy, what can't the eavesdropper do?
  2. What hard problem protects finite-field Diffie–Hellman?
  3. What does the "E" in ECDHE mean, and why does it matter?
  4. What attack is raw, unauthenticated DH vulnerable to, and what's the fix?
  5. What must you do to the shared secret before using it as an encryption key?

Further reading

  • Video: Computerphile, "Diffie Hellman — the Mathematics bit" (~8 min) and "Key Exchange Problems".
  • RFC 7748 — Curve25519 and Curve448. The spec behind X25519.
  • Article: "Weak Diffie-Hellman and the Logjam Attack" (weakdh.org) — why small/shared/512-bit DH groups were a real-world disaster in 2015.
  • Interactive: cryptohack.org — the Diffie–Hellman section has excellent hands-on challenges.

Generated from real-life-guide.md · 8 Oct 2026 16:36