Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
duyuefeng0708
GitHub Repository: duyuefeng0708/Cryptography-From-First-Principle
Path: blob/main/foundations/05-discrete-log-diffie-hellman/connect/dh-in-tls13.ipynb
483 views
unlisted
Kernel: SageMath 10.0

Connect: Diffie-Hellman in TLS 1.3

Module 05 | Real-World Connections

TLS 1.3 mandates ephemeral Diffie-Hellman for every connection, giving forward secrecy by default.

Introduction

Transport Layer Security (TLS) is the protocol that secures HTTPS, email, VPN, and most Internet communication. TLS 1.3 (RFC 8446, finalized 2018) made a landmark decision: every connection must use ephemeral Diffie-Hellman (or its elliptic curve variant, ECDHE).

This means:

  • Fresh DH keys are generated for every handshake

  • After the handshake, the DH secret is deleted

  • Even if the server's long-term private key is later compromised, past sessions remain secure (forward secrecy)

In this notebook, we simulate a simplified TLS 1.3-style handshake to see DH in action.

The TLS 1.3 Handshake (Simplified)

The key exchange portion of the TLS 1.3 handshake:

Client Server ------ ------ ClientHello + key_share: g^a mod p ----------> ServerHello <---------- + key_share: g^b mod p Both compute: shared_secret = g^(ab) mod p Derive: session_keys = HKDF(shared_secret, transcript_hash)

Key points:

  • The DH exchange happens in the first round trip (1-RTT)

  • Both aa and bb are ephemeral (new for each connection)

  • The shared secret is never transmitted; it's derived independently

# === Simulate a TLS 1.3-style DH handshake === import hashlib # Parameters (toy-sized for demonstration; TLS uses 2048+ bit primes or ECDH) p = 7919 # safe prime: (7919-1)/2 = 3959 is prime q = (p - 1) // 2 g = primitive_root(p) print(f'=== DH Parameters (toy-sized) ===') print(f'p = {p} (safe prime)') print(f'q = (p-1)/2 = {q}, is_prime(q) = {is_prime(q)}') print(f'g = {g} (generator)') print() # --- ClientHello --- a = ZZ.random_element(2, p - 2) # Client's ephemeral secret client_key_share = power_mod(g, a, p) print(f'--- ClientHello ---') print(f'Client ephemeral secret: a = {a}') print(f'Client key_share: g^a = {client_key_share}') print() # --- ServerHello --- b = ZZ.random_element(2, p - 2) # Server's ephemeral secret server_key_share = power_mod(g, b, p) print(f'--- ServerHello ---') print(f'Server ephemeral secret: b = {b}') print(f'Server key_share: g^b = {server_key_share}') print() # --- Both derive the shared secret --- client_shared = power_mod(server_key_share, a, p) # (g^b)^a server_shared = power_mod(client_key_share, b, p) # (g^a)^b print(f'--- Shared Secret Derivation ---') print(f'Client computes: (g^b)^a = {client_shared}') print(f'Server computes: (g^a)^b = {server_shared}') print(f'Match: {client_shared == server_shared}')
# === Derive a session key (simplified HKDF) === # In real TLS 1.3, HKDF-Expand-Label is used with the transcript hash. # We simulate this with a simple hash-based derivation. shared_secret = client_shared # Simulate transcript hash (in reality: hash of all handshake messages) transcript = f'ClientHello({client_key_share})||ServerHello({server_key_share})' # Derive session key ikm = f'{shared_secret}||{transcript}' session_key = hashlib.sha256(ikm.encode()).hexdigest()[:32] # 128-bit key print(f'Shared secret: {shared_secret}') print(f'Transcript hash: {hashlib.sha256(transcript.encode()).hexdigest()[:16]}...') print(f'Session key: {session_key}') print() print('Both client and server derive the SAME session key.') print('All subsequent traffic is encrypted with this key.')

Forward Secrecy: Why Ephemeral DH Matters

Forward secrecy means: if the server's long-term private key is compromised at some point in the future, past recorded sessions cannot be decrypted.

This is guaranteed because:

  1. Each session uses fresh DH keys (a,b)(a, b)

  2. After deriving the session key, the DH secrets (a,b)(a, b) are deleted

  3. The shared secret gabg^{ab} exists only in memory during the handshake

Without ephemeral DH (as in TLS 1.2 with RSA key exchange), an attacker who records ciphertext and later obtains the server's RSA private key can decrypt everything.

# === Demonstrate forward secrecy === # Simulate 3 connections, each with fresh ephemeral keys sessions = [] for i in range(3): a_i = ZZ.random_element(2, p - 2) b_i = ZZ.random_element(2, p - 2) A_i = power_mod(g, a_i, p) B_i = power_mod(g, b_i, p) s_i = power_mod(A_i, b_i, p) sessions.append({ 'session': i + 1, 'client_key': A_i, 'server_key': B_i, 'shared_secret': s_i }) print('=== Three independent sessions ===') for s in sessions: print(f" Session {s['session']}: " f"client_key={s['client_key']}, " f"server_key={s['server_key']}, " f"secret={s['shared_secret']}") print() print('Each session has a DIFFERENT shared secret.') print('Even if one session is compromised, the others remain secure.') print('The ephemeral secrets (a, b) are deleted after each handshake.') print() print('Contrast with static RSA key exchange:') print(' If the server\'s RSA key leaks, ALL recorded sessions are decryptable.') print(' This is why TLS 1.3 removed RSA key exchange entirely.')

Named Groups: Standardized DH Parameters

TLS 1.3 does not let servers choose arbitrary DH parameters. Instead, it uses named groups from RFC 7919:

GroupPrime sizeSecurity level
ffdhe20482048 bits~112 bits
ffdhe30723072 bits~128 bits
ffdhe40964096 bits~150 bits
ffdhe61446144 bits~175 bits
ffdhe81928192 bits~192 bits

All of these are safe primes (p=2q+1p = 2q + 1) with generator g=2g = 2.

Why standardize?

  • Prevents servers from using weak primes (smooth order, small subgroups)

  • The primes are generated from nothing-up-my-sleeve numbers (digits of π\pi), so no one can embed a backdoor

  • Enables precomputation for efficiency

# === Verify properties of the ffdhe2048 prime (first 10 hex digits shown) === # The actual ffdhe2048 prime from RFC 7919 (truncated for display) # Full prime is 2048 bits; we show the structure print('ffdhe2048 prime structure:') print(' p = 2^2048 - 2^1984 - 1 + 2^64 * (floor(2^1918 * pi) + 124476)') print(' Generator: g = 2') print() # Let's verify the safe-prime property on a smaller RFC-style prime # We'll use a known safe prime that mimics the structure p_demo = 7919 # our toy safe prime q_demo = (p_demo - 1) // 2 print(f'Our toy "named group":') print(f' p = {p_demo}') print(f' q = (p-1)/2 = {q_demo}') print(f' is_prime(p) = {is_prime(p_demo)}') print(f' is_prime(q) = {is_prime(q_demo)}') print(f' Safe prime: {is_prime(p_demo) and is_prime(q_demo)}') print() # What subgroups exist? print(f'Divisors of p-1 = {p_demo - 1}:') print(f' {divisors(p_demo - 1)}') print(f' Only 4 subgroup orders: 1, 2, {q_demo}, {p_demo - 1}') print(f' No small subgroups to exploit!')

Concept Map: Module 05 in TLS 1.3

Module 05 ConceptTLS 1.3 Application
DH key exchangeSession key establishment (ClientHello / ServerHello)
Ephemeral keysForward secrecy --- fresh (a,b)(a, b) per connection
DLP hardnessSecurity of key exchange (eavesdropper can't compute gabg^{ab})
Safe primesNamed groups (ffdhe2048-8192) prevent small-subgroup attacks
Pohlig-Hellman riskWhy TLS 1.3 mandates safe primes, not arbitrary primes
CDH assumptionThe core assumption: given gag^a and gbg^b, computing gabg^{ab} is hard

Summary

ConceptKey idea
Ephemeral DHEach connection gets fresh keys, so past sessions stay safe even if long-term keys are later compromised (forward secrecy).
DLP/CDH hardnessAn eavesdropper who sees gag^a and gbg^b cannot compute the shared secret gabg^{ab}.
Safe primesTLS 1.3 named groups use safe primes to prevent Pohlig-Hellman and small-subgroup attacks.
1-RTT handshakeDH key shares are sent in the very first messages, completing the exchange in a single round trip.
HKDF key derivationThe raw DH shared secret is expanded into a uniform session key via HKDF.
No static key exchangeTLS 1.3 removed static RSA and static DH entirely because forward secrecy is too important to leave optional.

TLS 1.3 removed all non-ephemeral key exchanges (static RSA, static DH) precisely because forward secrecy is too important to leave as optional.


Back to Module 05: Discrete Log and Diffie-Hellman