Diffie-Hellman lets two parties who have never met agree on a shared secret while an eavesdropper watches every message. Neither side sends the secret, and nothing sent lets the eavesdropper compute it in practice. Published by Whitfield Diffie and Martin Hellman in 1976, it is still at the centre of almost every secure connection you make: TLS 1.3, SSH, WireGuard and the Signal protocol all run some form of it on every connection.

This page builds it from modular arithmetic, works a complete exchange by hand, shows real code with X25519 and a key derivation function, and then spends most of its length on what goes wrong: missing authentication, bad groups, unvalidated inputs and reused keys. If modular exponentiation is new to you, read Modular Exponentiation first; everything here is built on it.

The problem it solves

Symmetric ciphers such as AES-GCM are fast and well understood, but both ends need the same key. Before 1976 that meant a courier, a pre-shared password or a trusted third party. Diffie-Hellman replaces the courier with arithmetic that is easy in one direction and hard to reverse.

The idea in one picture is paint mixing. Both sides start with a public colour. Each adds a private colour and sends the mixture. Each then adds their own private colour to what they received. Both end up with public plus Alice's private plus Bob's private, and an observer who saw only the two mixtures cannot unmix them. The mathematics replaces mixing with exponentiation in a group where undoing it, the discrete logarithm, is believed to be infeasible.

The mathematics

Fix a large prime p and a generator g, a number whose powers modulo p cycle through a large subgroup. Both are public and are usually standard constants rather than chosen per connection. Then:

  1. Alice picks a random secret a and sends A = ga mod p.
  2. Bob picks a random secret b and sends B = gb mod p.
  3. Alice computes Ba = gba mod p.
  4. Bob computes Ab = gab mod p.

Since multiplication of exponents commutes, both get the same number Z = gab mod p. Computing A from a is fast: square-and-multiply needs about log2(a) squarings, around 2,000 for a 2,048-bit exponent. Going backwards, finding a from A, is the discrete logarithm problem. The best known classical algorithms for prime fields (the number field sieve family) are sub-exponential but still far too slow at 2,048 bits, which is why 2,048-bit groups are rated at roughly 112 bits of security.

Strictly, security rests on the computational Diffie-Hellman assumption: given g, ga and gb, computing gab is hard. For keys that must look random, a stronger assumption matters, decisional Diffie-Hellman, which says gab cannot be told apart from a random group element. That gap is one reason Z is never used directly as a key.

Worked example by hand

Use toy numbers so every step can be checked by hand: p = 23 and g = 5. The powers of 5 modulo 23 reach all 22 non-zero values, so 5 is a generator of the whole group.

StepAlice (a = 6)Bob (b = 15)
Public valueA = 56 mod 23 = 15625 mod 23 = 8B = 515 mod 23 = 19
Sent over the wire819
Shared secret196 mod 23 = 2815 mod 23 = 2

Check Bob's public value by repeated squaring: 52 = 25 = 2, 54 = 4, 58 = 16 (all mod 23). Then 515 = 58 times 54 times 52 times 5 = 16 times 4 times 2 times 5 = 640, and 640 mod 23 = 19. For Alice's secret, 19 is minus 4 modulo 23, and (minus 4)6 = 4096 = 2 mod 23. Both sides hold 2.

An eavesdropper sees 23, 5, 8 and 19. At this size they can try all 22 exponents in microseconds, which is exactly why real groups are 2,048 bits or more, or use elliptic curves where 256-bit values give about 128 bits of security.

# Toy finite-field DH. Teaching only: tiny group, no validation, no authentication.
import secrets

p, g = 23, 5
a = secrets.randbelow(p - 3) + 2          # private, in [2, p-2]
b = secrets.randbelow(p - 3) + 2
A, B = pow(g, a, p), pow(g, b, p)          # sent in the clear
assert pow(B, a, p) == pow(A, b, p)        # both sides derive the same Z

Real code: X25519 and a KDF

In production, use a vetted library and a modern curve. X25519, specified in RFC 7748, is Diffie-Hellman on Curve25519. Its public keys and outputs are 32 bytes, it runs in constant time in good implementations, and every 32-byte string is a valid public key, which removes a whole class of validation bugs. With Python's cryptography package:

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

def derive(shared: bytes, transcript: bytes) -> bytes:
    # Never use the raw DH output as a key: extract and expand it with context.
    return HKDF(algorithm=hashes.SHA256(), length=32,
                salt=None, info=b"demo v1 session key" + transcript).derive(shared)

alice = X25519PrivateKey.generate()        # fresh per session: ephemeral
bob = X25519PrivateKey.generate()

alice_pub = alice.public_key()             # these two go over the network
bob_pub = bob.public_key()

z_alice = alice.exchange(bob_pub)
z_bob = bob.exchange(alice_pub)
assert z_alice == z_bob

transcript = b"..."                        # hash of everything both sides sent
key = derive(z_alice, transcript)          # feed into AES-GCM or ChaCha20-Poly1305

Two lines carry the weight. Generating a new private key per session makes the exchange ephemeral, so a server key stolen next year cannot decrypt traffic recorded today; this property is forward secrecy. The key derivation step turns a group element with structure into uniformly random key material and binds it to the conversation, so the same Z can never be reused as a key in another context.

Man in the middle and authentication

Ephemeral Diffie-Hellman inside a TLS 1.3 handshakeClientsecret a, share A = g^aServersecret b, share B = g^bClientHello: key_share AServerHello: key_share BCertificate + signature over transcriptZ = B^asame value on both sidesZ = A^ba, b never sentHKDF key scheduleZ + transcript hashTraffic keysAEAD, per directionDH gives secrecy; the signature binds B to the server. Without it, a man in the middle runs two exchanges.
Figure 1. Where Diffie-Hellman sits in TLS 1.3. Key shares travel in the two Hello messages; the server's signature over the transcript authenticates its share, and HKDF turns the shared secret into traffic keys.

Plain Diffie-Hellman authenticates nobody. Mallory sitting on the path can answer Alice with her own share and open a second exchange with Bob. Alice shares a key with Mallory, Bob shares a different key with Mallory, and Mallory decrypts, reads and re-encrypts everything. Both sides see a successful exchange.

Every real protocol therefore binds the shares to an identity. TLS 1.3 has the server sign a hash of the handshake transcript, including both key shares, with the private key behind its certificate. SSH signs with the host key that the client pinned on first connection. WireGuard mixes long-term static keys into the handshake so that only the holder of the right static key can complete it. The Signal protocol combines several DH computations between identity keys and ephemeral keys. The rule underneath is the same: DH gives you a shared secret with someone; authentication tells you who.

For deeper walkthroughs, SSL/TLS covers the full handshake, WireGuard shows a static-plus-ephemeral design, and the Signal double ratchet runs a fresh DH step on almost every reply.

Groups, validation and Logjam

With finite-field DH, the group matters as much as the algorithm. Use a safe prime p = 2q + 1 with q also prime, and a generator of the subgroup of order q. Named groups do this for you: RFC 7919 defines ffdhe2048 through ffdhe8192 for TLS, and RFC 3526 defines the older MODP groups used by IPsec. Do not generate your own parameters for a protocol, and do not accept arbitrary parameters from a peer.

Validate the peer's public value. For finite-field DH with a safe prime, require 1 < Y < p - 1 and that Y lies in the prime-order subgroup, i.e. Yq mod p = 1. Without that check, an attacker can send a value from a tiny subgroup, confining the shared secret to a handful of possibilities; against a party that reuses its private key, repeated small-subgroup probes leak that key piece by piece. On elliptic curves the analogue is checking that a point is on the curve. X25519 sidesteps most of this by design, but a few low-order inputs produce an all-zero output. Protocols are expected to reject an all-zero shared secret; many libraries do it for you, but check that yours does rather than assume.

History shows why sizes and sharing matter. The 2015 Logjam research showed that TLS servers could be downgraded to 512-bit export-grade groups, and that because a small number of 1,024-bit primes were shared by millions of servers, a well-funded adversary's one-time precomputation on one prime would break every connection using it. The fixes were to remove export groups, move to 2,048 bits or more, use standard groups that are large enough, and, increasingly, use elliptic curves.

Failure modes

  • Unauthenticated exchange. Encrypted but trivially intercepted. Always sign the transcript or mix in a verified static key.
  • Raw Z used as a key. Non-uniform, and the same secret may end up in two roles. Run it through HKDF with context.
  • Static keys where ephemeral were intended. Losing forward secrecy, and in finite fields enabling small-subgroup key recovery. Generate per session and erase after use.
  • Weak randomness. A predictable private exponent is a published one. Use the operating system generator, such as secrets in Python, never random.
  • Timing leaks. A naive square-and-multiply branches on secret bits. Use constant-time library code; never hand-roll this for production.
  • Downgrade. A negotiation that lets an attacker strip strong groups. TLS 1.3 signs the whole transcript, which is why it fixed this class.
  • Quantum risk. Shor's algorithm would solve discrete logarithms in both finite fields and elliptic curves on a large enough quantum computer, so recorded traffic is at risk later. Major browsers and CDNs now offer a hybrid TLS group, X25519MLKEM768, that combines X25519 with the ML-KEM post-quantum key encapsulation mechanism so the session stays safe if either one holds.

Trade-offs

OptionKey size for about 128-bit securityNotes
Finite-field DH (ffdhe3072)3,072-bit public valuesSlow, large; needs group validation
ECDH on P-25665-byte uncompressed pointsWidely mandated (FIPS); must validate points
X2551932 bytesFast, simple, misuse-resistant; the TLS 1.3 default in practice
Hybrid X25519 + ML-KEM-768About 1.2 KB per directionPost-quantum protection; larger handshakes

For new designs, the practical answer is X25519 inside an established protocol such as TLS 1.3 or the Noise framework, with the hybrid post-quantum group enabled where your stack supports it. Finite-field DH remains in legacy VPNs and compliance-bound systems; if you must run it, use a named group of 2,048 bits or more. For the number theory behind safe-prime generation, see Miller-Rabin primality testing.

What to do next

  1. Work the p = 23, g = 5 exchange yourself with different secrets, then run the toy Python and check your hand result.
  2. Run the X25519 example, change one byte of a public key and confirm the derived keys no longer match.
  3. Scan your own endpoints with a TLS scanner and confirm only TLS 1.2 or later is offered, with no finite-field group below 2,048 bits and no export ciphers.
  4. Audit any custom protocol in your codebase: is every exchange authenticated, ephemeral, validated and followed by a KDF?
  5. Check whether your TLS library and load balancers support the hybrid X25519MLKEM768 group, and plan to enable it.
  6. Read the Signal and WireGuard handshakes to see how static and ephemeral DH are combined in practice.
Key takeaway: Diffie-Hellman turns public messages into a shared secret because exponentiation is cheap and the discrete logarithm is not. On its own it is secret but anonymous: authenticate the shares, use fresh keys per session, validate inputs, derive keys with HKDF, prefer X25519, and move toward hybrid post-quantum groups.