Diffie-Hellman Key Exchange Protocol: Theory and Python Implementation

The Diffie-Hellman key exchange protocol explained through the discrete logarithm problem, the computational Diffie-Hellman problem, and forward secrecy. We reproduce man-in-the-middle, small-subgroup, and Pohlig-Hellman attacks in Python and show numerically why safe primes are required.

Overview

The Diffie-Hellman key exchange, published in 1976 by Whitfield Diffie and Martin Hellman, was the first concrete implementation of the concept of public-key cryptography. Using this protocol, parties can securely establish a shared secret key through a public communication channel susceptible to eavesdropping, without needing to share any secret information in advance.

This protocol is important for ensuring Forward Secrecy. Forward secrecy is the property that ensures past communications cannot be decrypted even if the secret key is compromised in the future. In DH key exchange, a new shared key is generated for each session, achieving this property.

This article is an introductory treatment of DH: we derive the protocol, examine the basis of its security, and reproduce in Python the three classic attacks — man-in-the-middle, small-subgroup, and Pohlig-Hellman. For safe primes, ECDH, X25519, and Signal’s X3DH, see the deeper sequel Diffie-Hellman Key Exchange in Depth . Within the same public-key framework, RSA relies on the hardness of integer factorization instead. For the broader landscape, see the Cryptography Roadmap .

Algorithm Principles

The Diffie-Hellman key exchange bases its security on the difficulty of the discrete logarithm problem.

  1. Agreement on Public Parameters: Alice and Bob, who wish to communicate, first agree on two public parameters:

    • A large prime number \(p\)
    • A primitive root (or generator) \(g\) modulo \(p\)
  2. Secret Key Generation:

    • Alice randomly chooses a secret integer \(a\) .
    • Bob randomly chooses a secret integer \(b\) . These \(a\) and \(b\) become Alice’s and Bob’s secret keys, respectively.
  3. Public Key Computation and Exchange:

    • Alice computes her public key \(A = g^a \pmod{p}\) and sends it to Bob.
    • Bob computes his public key \(B = g^b \pmod{p}\) and sends it to Alice. Since \(A\) and \(B\) travel through the public communication channel, an eavesdropper can also learn them.
  4. Shared Key Computation:

    • Alice uses Bob’s public key \(B\) and her own secret key \(a\) to compute the shared key \(S_A = B^a \pmod{p}\) .
    • Bob uses Alice’s public key \(A\) and his own secret key \(b\) to compute the shared key \(S_B = A^b \pmod{p}\) .

Here, \(S_A = (g^b)^a \pmod{p} = g^{ba} \pmod{p}\) and \(S_B = (g^a)^b \pmod{p} = g^{ab} \pmod{p}\) , so \(S_A = S_B\) , meaning Alice and Bob can share the same secret shared key. This equality follows purely from the commutativity of exponents, \(ba = ab\) — a remarkably simple mechanism.

Why It’s Secure: The Discrete Log Problem vs. the Computational Diffie-Hellman Problem

An eavesdropper Eve observes all of \(g, p, A, B\) . What she ultimately wants is the shared secret \(K = g^{ab} \bmod p\) , but this splits into two related, yet distinct, problems.

Discrete Logarithm Problem (DLP): given \(A = g^a \bmod p\) , find \(a\) .

\[ \text{Given } A \equiv g^{a} \pmod{p}, \; \text{find } a \]

Computational Diffie-Hellman problem (CDH): given \(g, p, A=g^a \bmod p, B=g^b \bmod p\) , find \(g^{ab} \bmod p\) directly, without going through \(a\) or \(b\) .

\[ \text{Given } g^{a} \bmod p \text{ and } g^{b} \bmod p, \; \text{find } g^{ab} \bmod p \]

If DLP can be solved (i.e., \(a\) can be recovered from \(A\) ), then CDH is trivially solved too — just compute \(K = B^a \bmod p\) . So solving DLP implies solving CDH. The converse — whether there is a shortcut to CDH that bypasses DLP entirely — is an open problem, and every known attack effectively solves DLP (or something of comparable computational cost)1. DH’s security formally rests on the assumption that CDH is hard (the CDH assumption), which is a slightly weaker (i.e., potentially easier to break) assumption than DLP hardness.

A worked numeric example

Using the textbook-small \(p=23, g=5\) , we run the full protocol and Eve’s most direct attack — brute-forcing the discrete log. The random seed is fixed for reproducibility.

import random
import time

random.seed(20230907)  # fixed seed for reproducibility

p = 23  # large prime (kept small here for a textbook example)
g = 5   # primitive root modulo p

alice_private_key = random.randint(1, p - 2)
bob_private_key = random.randint(1, p - 2)

alice_public_key = pow(g, alice_private_key, p)  # A = g^a mod p
bob_public_key = pow(g, bob_private_key, p)      # B = g^b mod p

shared_secret_alice = pow(bob_public_key, alice_private_key, p)  # S_A = B^a mod p
shared_secret_bob = pow(alice_public_key, bob_private_key, p)    # S_B = A^b mod p

print(f"Public parameters: p = {p}, g = {g}")
print(f"Alice's secret key a = {alice_private_key}, public key A = {alice_public_key}")
print(f"Bob's secret key   b = {bob_private_key}, public key B = {bob_public_key}")
print(f"Alice's shared key S_A = B^a mod p = {shared_secret_alice}")
print(f"Bob's shared key   S_B = A^b mod p = {shared_secret_bob}")
assert shared_secret_alice == shared_secret_bob
print(f"Match check: S_A == S_B -> {shared_secret_alice}")


def brute_force_dlog(g, A, p):
    """Brute-force search for the discrete log a from g, A, p (educational; only realistic for small p)"""
    for x in range(1, p - 1):
        if pow(g, x, p) == A:
            return x
    return None


t0 = time.perf_counter()
recovered_a = brute_force_dlog(g, alice_public_key, p)
t1 = time.perf_counter()
print(f"\nEve recovered a = {recovered_a} by brute force (correct value: {alice_private_key})")
print(f"Time taken: {(t1 - t0) * 1e6:.2f} us (search space: p-2 = {p - 2} candidates)")

# Once a is known, the CDH problem (computing g^{ab} mod p) is trivial too
eve_shared_secret = pow(bob_public_key, recovered_a, p)
print(f"Shared key computed by Eve: {eve_shared_secret} (matches Alice/Bob: {eve_shared_secret == shared_secret_alice})")

Output:

Public parameters: p = 23, g = 5
Alice's secret key a = 4, public key A = 4
Bob's secret key   b = 8, public key B = 16
Alice's shared key S_A = B^a mod p = 9
Bob's shared key   S_B = A^b mod p = 9
Match check: S_A == S_B -> 9

Eve recovered a = 4 by brute force (correct value: 4)
Time taken: 1.83 us (search space: p-2 = 21 candidates)
Shared key computed by Eve: 9 (matches Alice/Bob: True)

With a search space of only \(p-2=21\) candidates, brute force finishes in microseconds — Eve instantly recovers \(a\) and then the shared key (the exact timing will vary by machine, but it is always on the order of microseconds here). The search space grows roughly proportionally to \(p\) , while real-world DH uses \(p\) of 2048–3072 bits, i.e., more than \(2^{2048}\) candidates — brute force would not finish within the age of the universe. This exponential blow-up in candidate count is the intuitive basis for the hardness of DLP and CDH.

Man-in-the-Middle: Without Authentication, Two Separate Keys Get Established

DH by itself has no way to verify that a received public key truly belongs to “the other party.” An active attacker Eve sitting on the wire can exploit this to independently establish one shared secret with Alice and a completely different one with Bob, transparently relaying (and reading) all traffic in plaintext.

Concretely, Eve substitutes her own values for the public keys sent to each side:

  • To Alice, she sends \(B' = g^{e_B} \bmod p\) (with her own secret exponent \(e_B\) ), claiming it is Bob’s public key.
  • To Bob, she sends \(A' = g^{e_A} \bmod p\) (with her own secret exponent \(e_A\) ), claiming it is Alice’s public key.

Alice, believing \(B'\) is Bob’s real public key, computes \(K_{AE} = (B')^{a} \bmod p = g^{e_B a} \bmod p\) . Eve can compute the same value from Alice’s real public key \(A\) and her own \(e_B\) : \(A^{e_B} \bmod p = g^{a e_B} \bmod p\) . So Eve now shares \(K_{AE}\) with Alice. Symmetrically, Eve and Bob end up sharing \(K_{EB} = g^{e_A b} \bmod p\) . In general \(K_{AE} \neq K_{EB}\) : Alice and Bob each believe they established a key directly with the other, but in reality each shares a different key with Eve.

import random

random.seed(20230907)

p = 23
g = 5

# The legitimate parties
alice_private = random.randint(1, p - 2)
bob_private = random.randint(1, p - 2)
alice_public = pow(g, alice_private, p)  # A = g^a mod p
bob_public = pow(g, bob_private, p)      # B = g^b mod p

# Eve prepares two independent secret exponents, one per victim
eve_secret_vs_alice = random.randint(1, p - 2)  # used to impersonate Bob
eve_secret_vs_bob = random.randint(1, p - 2)    # used to impersonate Alice
fake_B = pow(g, eve_secret_vs_alice, p)  # sent to Alice as "Bob's public key"
fake_A = pow(g, eve_secret_vs_bob, p)    # sent to Bob as "Alice's public key"

print(f"Public parameters: p = {p}, g = {g}")
print(f"Alice: a = {alice_private}, A = {alice_public}")
print(f"Bob  : b = {bob_private}, B = {bob_public}")
print(f"Eve  : e_B(vs Alice) = {eve_secret_vs_alice} -> forged B' = {fake_B}")
print(f"Eve  : e_A(vs Bob)   = {eve_secret_vs_bob} -> forged A' = {fake_A}")

# Alice thinks fake_B is Bob's public key
K_Alice_Eve = pow(fake_B, alice_private, p)                   # (g^{e_B})^a mod p
K_Eve_with_Alice = pow(alice_public, eve_secret_vs_alice, p)  # (g^a)^{e_B} mod p (Eve's computation)

# Bob thinks fake_A is Alice's public key
K_Bob_Eve = pow(fake_A, bob_private, p)                # (g^{e_A})^b mod p
K_Eve_with_Bob = pow(bob_public, eve_secret_vs_bob, p)  # (g^b)^{e_A} mod p (Eve's computation)

print(f"\nShared key Alice believes she established, K_AE = {K_Alice_Eve}")
print(f"Shared key Eve computed with Alice           = {K_Eve_with_Alice} (match: {K_Alice_Eve == K_Eve_with_Alice})")
print(f"Shared key Bob believes he established, K_EB = {K_Bob_Eve}")
print(f"Shared key Eve computed with Bob             = {K_Eve_with_Bob} (match: {K_Bob_Eve == K_Eve_with_Bob})")
print(f"\nIs K_AE == K_EB? {K_Alice_Eve == K_Bob_Eve}")

Output:

Public parameters: p = 23, g = 5
Alice: a = 4, A = 4
Bob  : b = 8, B = 16
Eve  : e_B(vs Alice) = 17 -> forged B' = 15
Eve  : e_A(vs Bob)   = 1 -> forged A' = 5

Shared key Alice believes she established, K_AE = 2
Shared key Eve computed with Alice           = 2 (match: True)
Shared key Bob believes he established, K_EB = 16
Shared key Eve computed with Bob             = 16 (match: True)

Is K_AE == K_EB? False

Alice’s “shared key” is 2 and Bob’s is 16 — genuinely different values — and Eve knows both exactly. This is the essence of MITM: DH exchanges a key, but guarantees nothing about the other party’s identity. The fix is to authenticate the public keys through a separate mechanism: signed DH (TLS’s ECDHE-RSA / ECDHE-ECDSA), PAKE, or the STS protocol. See the deeper sequel for authenticated key exchange in practice.

Small-Subgroup Attacks: What Happens If You Skip Public-Key Validation

Another practical pitfall is failing to check that a received public key actually belongs to the expected group (the cyclic group of order \(p-1\) , or an appropriate subgroup of it). If an attacker sends an element \(h\) of small order \(t\) (where \(t\) divides \(p-1\) ) as “their public key,” then no matter what secret exponent \(b\) the victim uses, the resulting shared secret \(S_B = h^b \bmod p\) can only ever land inside the order-\(t\) subgroup generated by \(h\) . A search space that should have been \(p-1\) candidates collapses to just \(t\) .

We demonstrate this with \(p=2003\) (so \(p-1 = 2 \times 7 \times 11 \times 13 = 2002\) ), sending an element of order \(t=7\) .

import random
from sympy import isprime, factorint, primitive_root

random.seed(20230907)

# a prime whose p-1 has several small prime factors
p = 2003
assert isprime(p)
print(f"p = {p}, factorization of p-1: {factorint(p - 1)}")

g = primitive_root(p)
print(f"primitive root g = {g}")

# the attacker sends an element of order t = 7 as their "public key"
t = 7
assert (p - 1) % t == 0
h = pow(g, (p - 1) // t, p)  # an element of order exactly t
print(f"element of order {t}: h = g^((p-1)/{t}) mod p = {h}")
order_check = [pow(h, i, p) for i in range(1, t + 1)]
print(f"h, h^2, ..., h^{t} mod p = {order_check} (h^{t} should be 1)")

# Bob receives h as "the other party's public key" and, without subgroup checks,
# computes a shared secret as usual
distinct_secrets = set()
trials = 2000
for _ in range(trials):
    b = random.randint(1, p - 2)  # Bob's real secret key (re-randomized each trial)
    S_B = pow(h, b, p)
    distinct_secrets.add(S_B)

print(f"\nComputed S_B = h^b mod p over {trials} random choices of Bob's secret b")
print(f"Distinct S_B values observed: {sorted(distinct_secrets)}")
print(f"Count: {len(distinct_secrets)} (bounded by t={t})")

Output:

p = 2003, factorization of p-1: {2: 1, 7: 1, 11: 1, 13: 1}
primitive root g = 5
element of order 7: h = g^((p-1)/7) mod p = 874
h, h^2, ..., h^7 mod p = [874, 733, 1685, 485, 1257, 974, 1] (h^7 should be 1)

Computed S_B = h^b mod p over 2000 random choices of Bob's secret b
Distinct S_B values observed: [1, 485, 733, 874, 974, 1257, 1685]
Count: 7 (bounded by t=7)

Even across 2000 different random choices of Bob’s secret key, the computed shared secret only ever takes one of these 7 values. If the attacker can precompute these 7 candidates and use some oracle (e.g., whether decryption with the derived key later succeeds — a side channel) to determine which one Bob actually used, they learn \(b \bmod t\) . Repeating this with several different small subgroups and combining the results via the Chinese Remainder Theorem can recover the full secret key — this is the multiplicative-group analogue of the “invalid curve attack” in elliptic curve cryptography.

The figure below shows how the number of possible shared-secret values scales with the chosen subgroup order \(t\) .

Bar chart on a log scale showing the number of distinct shared-secret values obtained when an attacker sends an element of order t (t = 2, 7, 11, 13, 14, 22) and Bob’s secret key is re-randomized 2000 times. Every t collapses the search space to exactly t values, far below the honest search space of p-1 = 2002

Larger \(t\) weakens the attack, but for every \(t\) tested, the observed number of candidates is exactly \(t\) — dramatically smaller than the honest search space \(p-1=2002\) . The fix is to validate that a received public key lies in the correct order subgroup (typically the large prime-order subgroup under a safe prime). Production DH libraries such as cryptography perform this check automatically.

Safe Primes and the Pohlig-Hellman Attack

The standard defense that prevents small-subgroup attacks from being possible in the first place is using a safe prime. A prime \(p\) of the form

\[ p = 2q + 1 \quad (q \text{ also prime}) \]

is called a safe prime, and \(q\) a Sophie Germain prime. Since \(p - 1 = 2q\) has only the prime factors \(2\) and \(q\) , there are no exploitable small subgroups (aside from the order-2 one).

Conversely, if \(p-1\) is composed only of small prime factors (i.e., it is smooth), the Pohlig-Hellman algorithm solves the entire discrete log problem efficiently. The idea: factor \(p - 1 = q_1^{e_1} q_2^{e_2} \cdots q_k^{e_k}\) , then:

  1. For each prime-power factor \(q_i^{e_i}\) , project \(g\) and \(h\) down into the order-\(q_i^{e_i}\) subgroup (compute \(g^{(p-1)/q_i^{e_i}}\) and \(h^{(p-1)/q_i^{e_i}}\) ).
  2. Solve the discrete log \(x \bmod q_i^{e_i}\) within that small subgroup (brute force or baby-step giant-step).
  3. Recombine the \(x \bmod q_i^{e_i}\) results via the Chinese Remainder Theorem (CRT) to recover \(x \bmod (p-1)\) .

The overall cost is governed by the largest prime-power factor \(\max_i q_i^{e_i}\) — so if every factor of \(p-1\) is small, the discrete log is easy no matter how large \(p\) itself is.

A hand-computable toy example

With \(p=41\) (so \(p-1 = 40 = 2^3 \times 5\) ), we run Pohlig-Hellman step by step to recover the secret exponent \(x=15\) .

from sympy import factorint
from sympy.ntheory.modular import crt

# p-1 = 40 = 2^3 * 5 (smooth: only small prime factors)
p = 41
g = 6  # primitive root
x_true = 15  # the secret discrete log we will recover via Pohlig-Hellman
h = pow(g, x_true, p)
print(f"p = {p}, g = {g}, factorization of p-1 = {p - 1}: {factorint(p - 1)}")
print(f"We want x such that g^x = h mod p, h = {h}")

factors = factorint(p - 1)
remainders = []
moduli = []
for q, e in factors.items():
    qe = q**e
    g_sub = pow(g, (p - 1) // qe, p)  # g projected into the order-qe subgroup
    h_sub = pow(h, (p - 1) // qe, p)  # h projected into the order-qe subgroup
    x_i = None
    for candidate in range(qe):  # small subgroup, so brute force is fine
        if pow(g_sub, candidate, p) == h_sub:
            x_i = candidate
            break
    remainders.append(x_i)
    moduli.append(qe)
    print(f"  prime power {qe:>3d}: discrete log in this subgroup x ≡ {x_i} (mod {qe})")

x_recovered, mod_total = crt(moduli, remainders)
print(f"\nx recovered via CRT = {x_recovered} (mod {mod_total})")
print(f"Check: g^x_recovered mod p = {pow(g, int(x_recovered), p)} (h = {h})")
assert pow(g, int(x_recovered), p) == h
print(f"Matches the true secret exponent: {int(x_recovered) == x_true}")

Output:

p = 41, g = 6, factorization of p-1 = 40: {2: 3, 5: 1}
We want x such that g^x = h mod p, h = 3

  prime power   8: discrete log in this subgroup x ≡ 7 (mod 8)
  prime power   5: discrete log in this subgroup x ≡ 0 (mod 5)

x recovered via CRT = 15 (mod 40)
Check: g^x_recovered mod p = 3 (h = 3)
Matches the true secret exponent: True

The order-8 subgroup gives \(x \equiv 7 \pmod 8\) and the order-5 subgroup gives \(x \equiv 0 \pmod 5\) ; combining these two small subproblems with CRT recovers the true \(x=15\) exactly. There was never any need to brute-force all of \(p-1=40\) — only subgroups of size at most 8 — and that reduction in search space is the heart of Pohlig-Hellman.

Measuring smooth \(p-1\) vs. safe prime

We generate primes of similar bit length, one with smooth \(p-1\) and one a safe prime, and time how long sympy.discrete_log (which internally runs Pohlig-Hellman plus baby-step giant-step) takes to solve the discrete log in each.

import time
import random
from sympy import isprime, factorint, primitive_root, discrete_log, primerange

random.seed(20230907)

small_primes = list(primerange(2, 100))  # pool of small primes to keep p-1 smooth


def find_smooth_prime(target_bits, tries=20000):
    """Find a prime p whose p-1 factors are all below 100"""
    for _ in range(tries):
        random.shuffle(small_primes)
        prod = 1
        for q in small_primes:
            if prod.bit_length() >= target_bits:
                break
            prod *= q
        p = prod + 1
        if prod.bit_length() >= target_bits - 2 and isprime(p):
            return p
    raise RuntimeError("not found")


def find_safe_prime(bits):
    while True:
        q = random.getrandbits(bits - 1) | 1 | (1 << (bits - 2))
        if not isprime(q):
            continue
        p = 2 * q + 1
        if isprime(p):
            return p


for bits in [24, 32, 40, 48]:
    p_smooth = find_smooth_prime(bits)
    p_safe = find_safe_prime(bits)

    g_smooth = primitive_root(p_smooth)
    g_safe = primitive_root(p_safe)

    x_smooth = random.randint(2, p_smooth - 2)
    h_smooth = pow(g_smooth, x_smooth, p_smooth)
    x_safe = random.randint(2, p_safe - 2)
    h_safe = pow(g_safe, x_safe, p_safe)

    t0 = time.perf_counter()
    xr_smooth = discrete_log(p_smooth, h_smooth, g_smooth)
    t_smooth = time.perf_counter() - t0
    assert xr_smooth == x_smooth

    t0 = time.perf_counter()
    xr_safe = discrete_log(p_safe, h_safe, g_safe)
    t_safe = time.perf_counter() - t0
    assert xr_safe == x_safe

    max_factor_smooth = max(factorint(p_smooth - 1).keys())
    max_factor_safe_bits = max(factorint(p_safe - 1).keys()).bit_length()

    print(
        f"~{bits} bit: smooth p={p_smooth} (largest factor {max_factor_smooth}) -> {t_smooth * 1000:.3f} ms | "
        f"safe p={p_safe} (largest factor {max_factor_safe_bits}bit) -> {t_safe * 1000:.3f} ms | "
        f"ratio {t_safe / t_smooth:.1f}x"
    )

Output:

~24 bit: smooth p=16297859 (largest factor 89) -> 0.107 ms | safe p=11600867 (largest factor 23bit) -> 0.365 ms | ratio 3.4x
~32 bit: smooth p=10747150679 (largest factor 89) -> 0.194 ms | safe p=3978555587 (largest factor 31bit) -> 9.649 ms | ratio 49.8x
~40 bit: smooth p=1401201149063 (largest factor 67) -> 0.262 ms | safe p=837601172687 (largest factor 39bit) -> 173.684 ms | ratio 664.1x
~48 bit: smooth p=212996770326583 (largest factor 71) -> 0.353 ms | safe p=253901762228183 (largest factor 47bit) -> 16301.000 ms | ratio 46200.3x

Bar chart comparing discrete-log solve time in milliseconds (log scale) for primes with smooth p-1 versus safe primes, at bit lengths 24/32/40/48. The smooth case always solves in under 1 millisecond, while the safe-prime case grows exponentially with bit length, exceeding 16 seconds at 48 bits

The smooth case (largest factor under 100) solves in under a millisecond regardless of how large \(p\) ’s bit length grows. The safe-prime case, on the other hand, gets exponentially slower as the largest factor (essentially \(q\) itself) gets longer — reaching 16.3 seconds at 48 bits, roughly 46,200 times slower than the smooth case. This is the direct consequence of baby-step giant-step’s \(O(\sqrt{q})\) cost being exponential in the bit length of \(q\) . In production-grade safe primes of 2048–3072 bits, \(q\) itself exceeds 2000 bits, making \(\sqrt{q}\) astronomically large — that is precisely where the security comes from. These measurements make the conclusion concrete: if \(p-1\) has only small prime factors, Pohlig-Hellman effectively neutralizes the discrete log problem regardless of how many bits \(p\) has.

Summary

  • The shared key \(K=g^{ab} \bmod p\) follows purely from the commutativity of exponents, but its security rests on the CDH problem, an assumption possibly slightly weaker than DLP.
  • With the small example \(p=23\) , we confirmed that brute-forcing the discrete log finishes in microseconds; real deployments push \(p\) to 2048+ bits to make the search space exponentially larger.
  • Man-in-the-middle is DH’s fundamental weakness in the absence of authentication — an attacker can establish separate shared secrets with each party. Signed DH, PAKE, or STS-style authentication is required to close this gap.
  • Small-subgroup attacks exploit missing order validation: sending a low-order element collapses the shared secret to just a handful of candidates.
  • The Pohlig-Hellman attack efficiently solves the discrete log whenever \(p-1\) is smooth; our measurements showed roughly a 46,200x solve-time gap between a 48-bit smooth prime and a 48-bit safe prime.
  • The standard defense against all of the above is using a safe prime (\(p=2q+1\) ) and validating the order of any received public key. For production implementations (cryptography.hazmat, ECDH, X25519), see the deeper sequel .

References

  • Diffie, W., & Hellman, M. (1976). “New Directions in Cryptography.” IEEE Transactions on Information Theory, 22(6), 644–654.
  • Pohlig, S., & Hellman, M. (1978). “An Improved Algorithm for Computing Logarithms over GF(p) and Its Cryptographic Significance.” IEEE Transactions on Information Theory, 24(1), 106–110.
  • Pfeiffer, S., & Tihanyi, N. (2024). “D(HE)at: A Practical Denial-of-Service Attack on the Finite Field Diffie–Hellman Key Exchange.” IEEE Access, 12, 957–980. (Shows how missing public-key order validation enables the “D(HE)at” DoS attack / CVE-2024-41996.)
  • Tang, K. F., Wu, K. L., & Chau, S. Y. (2024). “Investigating TLS Version Downgrade in Enterprise Software.” Proceedings of the 14th ACM Conference on Data and Application Security and Privacy (CODASPY 2024). (A 2024 study of Logjam-style TLS downgrade attacks in enterprise software.)
  • Adrian, D., et al. (2015). “Imperfect Forward Secrecy: How Diffie-Hellman Fails in Practice” (Logjam attack). CCS 2015.

  1. In general groups, CDH can provably be strictly easier than DLP, but no such separation is known for the multiplicative group \(\mathbb{Z}_p^*\) that DH actually uses — in practice we treat “CDH ≈ DLP hardness.” ↩︎