24. Cryptographic Mathematics
Level: Expert
You'll use this for: actually understanding why RSA, Diffie-Hellman, and elliptic-curve crypto are secure — not just calling openssl.
Modular exponentiation (the core operation of public-key crypto)
Computing a^b mod n for huge b naively (multiply a by itself b
times) is infeasible for cryptographic-sized numbers. Fast (modular)
exponentiation ("square and multiply") does it in O(log b)
multiplications instead of O(b).
def fast_pow_mod(base, exp, mod):
result = 1
base = base % mod
while exp > 0:
if exp & 1: # if current bit is 1
result = (result * base) % mod
exp >>= 1 # shift to next bit
base = (base * base) % mod # square the base
return result
print(fast_pow_mod(7, 128, 13)) # matches pow(7, 128, 13)
print(pow(7, 128, 13)) # Python's built-in does this internally
Tracing "square and multiply" by hand for 3^13 mod 7. Write the
exponent 13 in binary (1101), then process it one bit at a time from
the least significant end, squaring the base every step and folding it
into the result only when the current bit is 1:
exp = 13 = 1101 (binary) result starts at 1, base starts at 3
bit 1 (rightmost, value 1): result = 1×3 mod 7 = 3 base = 3² mod 7 = 2
bit 0 (value 0): result unchanged = 3 base = 2² mod 7 = 4
bit 1 (value 1): result = 3×4 mod 7 = 12 mod 7 = 5 base = 4² mod 7 = 2
bit 1 (leftmost, value 1): result = 5×2 mod 7 = 10 mod 7 = 3 (last bit, base unused after)
Final result: 3
Only 4 squarings and 3 multiplications were needed for exponent 13 —
compare that to naively multiplying 3 by itself 13 times. For a
2048-bit RSA exponent, that's the difference between roughly 2,048
operations and 2^2048 operations — the entire reason this algorithm, not
the exponent's size, determines whether modern cryptography is
computationally feasible at all.
This is the actual algorithm running every time a server performs an RSA or Diffie-Hellman operation — without it, cryptography using 2048+ bit numbers would be computationally impossible.
RSA, derived end to end
- Pick two large primes
p,q. Computen = p·q. - Compute
φ(n) = (p-1)(q-1)(Euler's totient, Ch. 19). - Pick public exponent
e(commonly 65537) such thatgcd(e, φ(n)) = 1. - Compute private exponent
d = e⁻¹ mod φ(n)(modular inverse, Ch. 19). - Public key:
(n, e). Private key:(n, d). - Encrypt:
ciphertext = message^e mod n. - Decrypt:
message = ciphertext^d mod n.
# Toy RSA with small primes (NEVER do this with small primes in real life)
p, q = 61, 53
n = p * q # 3233
phi = (p - 1) * (q - 1) # 3120
e = 17 # public exponent, gcd(17, 3120) = 1
d = pow(e, -1, phi) # private exponent (modular inverse)
message = 65
ciphertext = pow(message, e, n)
decrypted = pow(ciphertext, d, n)
print(ciphertext, decrypted) # decrypted == 65 again
Why this is secure with large primes: anyone can see the public key
(n, e), but recovering the private key d requires knowing φ(n),
which requires factoring n back into p and q — and factoring a
2048-bit product of two large primes is (currently) computationally
infeasible. RSA's core hardness rests on this number-theoretic assumption
(Ch. 19), plus correct padding, parameter choices, and implementation.
Diffie-Hellman key exchange — agreeing on a secret over a public channel
Two parties, Alice and Bob, agree on a public prime p and generator g.
p = 23 # public prime (toy size — real DH uses 2048+ bit primes)
g = 5 # public generator
a = 6 # Alice's private key (secret, never transmitted)
b = 15 # Bob's private key (secret, never transmitted)
A = pow(g, a, p) # Alice's public value, sent to Bob openly
B = pow(g, b, p) # Bob's public value, sent to Alice openly
alice_shared_secret = pow(B, a, p) # Bob's public value ^ Alice's private key
bob_shared_secret = pow(A, b, p) # Alice's public value ^ Bob's private key
print(alice_shared_secret == bob_shared_secret) # True — both derive the same secret!
Why an eavesdropper who sees p, g, A, B can't compute the shared
secret: doing so requires solving the discrete logarithm problem
(recovering a from g^a mod p), which is believed computationally
infeasible for large primes — a different hardness assumption from RSA's
factoring problem, but the same flavor: an operation that's easy in one
direction (exponentiate) and (believed) hard to reverse (take the discrete
log).
Elliptic curve cryptography (ECC) — same idea, smaller keys
ECC replaces modular exponentiation with point addition on an elliptic
curve (y² = x³ + ax + b mod p). "Multiplying" a point G by a scalar
k (i.e., adding G to itself k times using the curve's geometric
group law) is easy; recovering k from k·G and G (the elliptic
curve discrete logarithm problem) is believed even harder than the
classic discrete log problem relative to key size — which is why a
256-bit ECC key (P-256, Curve25519) offers security roughly comparable
to a 3072-bit RSA key, with much smaller keys and faster operations. This
is why modern TLS and SSH default to ECC (ECDHE, Ed25519) over classic
RSA/DH.
The birthday attack — hash collision math
The birthday paradox: in a room of just 23 people, there's a >50% chance two share a birthday — far fewer than the 366 you might intuitively guess, because you're comparing all pairs, which grows quadratically.
P(collision) ≈ 1 - e^(-k²/2N) for k items into N buckets
Applied to hash functions: to find any collision in an n-bit hash
function, an attacker needs only about 2^(n/2) attempts (not 2^n) —
this is why a hash function needs double the bit-length of its
intended collision-resistance security level. A 128-bit hash only offers
~64 bits of collision resistance, which is why SHA-256 (256-bit output,
~128-bit collision resistance) is the practical minimum for
collision-critical uses today, and why MD5 (128-bit, ~64-bit collision
resistance, further weakened by structural cryptanalytic attacks) is
considered broken.
def birthday_bound(hash_bits):
return 2 ** (hash_bits / 2)
print(f"{birthday_bound(128):.2e}") # MD5-scale: ~1.8 * 10^19 attempts to find a collision
print(f"{birthday_bound(256):.2e}") # SHA-256-scale: ~1.3 * 10^38 attempts
Why RSA decryption gives back the message: the proof
| Symbol | Meaning | Toy value |
|---|---|---|
p, q |
the two secret primes | 61, 53 |
n = p·q |
public modulus | 3233 |
φ(n) |
Euler's totient, (p-1)(q-1), secret |
3120 |
e |
public exponent, coprime to φ(n) |
17 |
d |
private exponent, e·d ≡ 1 (mod φ(n)) |
2753 (extended Euclid, Ch. 19) |
m, c |
message and ciphertext, both in ℤₙ |
65, 2790 |
Because e·d ≡ 1 (mod φ(n)), there's some integer k with e·d = 1 + k·φ(n).
Then, using Euler's theorem (m^φ(n) ≡ 1, Ch. 19):
c^d = (m^e)^d = m^(e·d) = m^(1 + k·φ(n)) = m · (m^φ(n))^k ≡ m · 1^k = m (mod n)
Decryption undoes encryption because the exponent wraps around at
φ(n). Only someone who knows φ(n), which means knowing p and q, can
compute the d that makes the wrap land exactly on 1. (The proof above
assumes gcd(m, n) = 1. The CRT from Ch. 19 extends it to every m.)
Why "textbook RSA" is broken: malleability and padding
Raw RSA is multiplicative: Enc(m₁) · Enc(m₂) = (m₁m₂)ᵉ = Enc(m₁·m₂). An
attacker who sees c = Enc(65) = 2790 can compute c · 2ᵉ mod n without any
key, and that decrypts to 2 · 65 = 130. If the message were an amount of
money, the attacker just doubled it.
Real RSA therefore pads the message with structured randomness first (OAEP
for encryption, PSS for signatures). Padding has its own maths trap. In 1998
Bleichenbacher showed that a server which merely reveals whether the
padding was valid (an error message, or a timing difference) acts as an
oracle. Each answer leaks a little information about m, and roughly a
million adaptive queries decrypt any ciphertext. The 2017 ROBOT scan found
the same flaw still live on major sites. That's why TLS 1.3 removed RSA key
transport entirely.
Side channels: square-and-multiply leaks the key
Look at the fast_pow_mod loop above. It multiplies only when the current
exponent bit is 1. When the exponent is the private key d, the running
time (or the power trace, or the cache footprint) of each step reveals each
bit of d. Kocher's 1996 timing attack recovered RSA and DH keys from
response timings alone, and later remote attacks did it across a LAN (Brumley
& Boneh, 2003).
The fix is mathematical, not cryptographic. A Montgomery ladder performs
exactly one multiply and one square per bit, whatever the bit is, and
blinding randomizes the input (c · rᵉ, decrypt, then divide out r).
That's the same multiplicative property that made textbook RSA malleable,
used here as a defence. "Constant-time code" in crypto libraries is
fundamentally about making a program's resource usage independent of
secret bits.
Diffie–Hellman, symbol by symbol, and why parameters matter
| Symbol | Public? | Meaning |
|---|---|---|
p |
public | a large prime, the modulus |
g |
public | a generator: its powers g¹, g², … cycle through a large subgroup of ℤₚ* |
a, b |
secret | each side's random private exponent |
A = gᵃ mod p, B = gᵇ mod p |
public | the values sent over the wire |
K = gᵃᵇ mod p |
secret | the shared key, computed as Bᵃ = Aᵇ |
Security depends on more than the size of p:
- Small subgroups. If
p - 1has only small prime factors, the discrete log can be solved separately in each small subgroup and recombined with the CRT (the Pohlig–Hellman algorithm). That's why DH uses safe primesp = 2q + 1withqalso prime, which leaves no small subgroups to hide in. It's also why TLS 1.3 only allows a fixed list of vetted groups (RFC 7919). - Precomputation. The best discrete-log algorithm (the number field sieve)
spends most of its effort on work that depends only on
p, not onaorb. The 2015 Logjam paper showed that 512-bit "export" DH could be broken in minutes once that precomputation was done. It also estimated that a nation-state budget could precompute one widely shared 1024-bit prime, enough to passively decrypt a large fraction of VPN and SSH traffic. Shared parameters plus precomputation turned a per-connection cost into a one-time cost.
Elliptic curves, by hand
An elliptic curve over a prime field is the set of points (x, y) with
y² ≡ x³ + a·x + b (mod p)
plus a special "point at infinity" O, which acts like zero. Points can be
added. Geometrically, you draw the line through P and Q, find where
it hits the curve a third time, and reflect that point. Algebraically:
slope: λ = (y₂ - y₁) / (x₂ - x₁) if P ≠ Q (the chord)
λ = (3x₁² + a) / (2y₁) if P = Q (the tangent: implicit differentiation, Ch. 12!)
sum: x₃ = λ² - x₁ - x₂
y₃ = λ(x₁ - x₃) - y₁
Every "division" here means multiplying by a modular inverse (Ch. 19).
A toy curve: y² = x³ + 2x + 2 (mod 17), base point G = (5, 1).
Compute 2G (doubling, so use the tangent):
λ = (3·5² + 2) / (2·1) = 77 / 2 ≡ 77 · 9 ≡ 9 · 9 = 81 ≡ 13 (mod 17) [2⁻¹ ≡ 9, since 2·9 = 18 ≡ 1; 77 ≡ 9]
x₃ = 13² - 5 - 5 = 159 ≡ 6 (mod 17)
y₃ = 13·(5 - 6) - 1 = -14 ≡ 3 (mod 17)
2G = (6, 3) check: 3² = 9, 6³ + 2·6 + 2 = 230 = 13·17 + 9 ≡ 9 ✓
Keep adding G and the points go (5,1), (6,3), (10,6), (3,1), (9,16), ….
At 19G you reach O, so the group has order 19, and 20G = G again.
That cycle is the "clock" ECC works on.
ECDH with toy numbers. Alice picks secret a = 3 and sends A = 3G = (10, 6).
Bob picks b = 7 and sends B = 7G = (0, 6). Alice computes 3·B and Bob
computes 7·A. Both get 21G = 2G = (6, 3), because 21 mod 19 = 2. An
eavesdropper sees G, (10,6) and (0,6) and must solve the elliptic
curve discrete log problem to recover 3 or 7. With 19 points that takes a
moment. With about 2²⁵⁶ points (Curve25519, P-256), the best known attack
(Pollard's rho) needs about √(2²⁵⁶) = 2¹²⁸ steps. That square root is the
birthday bound again (below), and it's why a 256-bit curve gives "128-bit
security".
ECDSA nonce reuse: the PS3 hack, in algebra
An ECDSA signature on message hash z with private key d uses a fresh
secret nonce k, over a curve whose group has order n:
r = x-coordinate of (k·G) mod n
s = k⁻¹ · (z + r·d) mod n
| Symbol | Meaning |
|---|---|
d |
the private key (a number) |
k |
the per-signature nonce. It must be secret and unique |
z |
hash of the message being signed |
(r, s) |
the signature |
Sign two different messages z₁, z₂ with the same k. Then r is
identical in both, and subtracting the two s equations eliminates d:
s₁ - s₂ = k⁻¹·(z₁ - z₂) -> k = (z₁ - z₂) / (s₁ - s₂) (mod n)
then from either signature: d = (s₁·k - z₁) / r (mod n)
Two signatures and two lines of algebra give you the private key. On the toy
curve (n = 19) with d = 7, k = 10, z₁ = 5, z₂ = 12, the signatures
are (7, 13) and (7, 8). Then k = (5-12)/(13-8) ≡ 10 and
d = (13·10 - 5)/7 ≡ 7. Sony's PS3 used a constant k (2010). Android
Bitcoin wallets with a broken SecureRandom repeated k (2013) and had funds
stolen. Even a few biased bits of k are enough when many signatures are
available, using lattice attacks (the 2019 Minerva and TPM-Fail attacks
recovered keys from timing-leaked nonce bits). That's why modern
implementations derive k deterministically from the key and the message
(RFC 6979), and why Ed25519 was designed that way from the start.
The birthday bound, derived
Put k items into N equally likely buckets one at a time. Item i+1
avoids all i earlier ones with probability 1 - i/N. So:
P(no collision) = (1 - 1/N)(1 - 2/N)…(1 - (k-1)/N)
≈ e^(-1/N) · e^(-2/N) · … · e^(-(k-1)/N) using 1 - x ≈ e^(-x) (Taylor, Ch. 12)
= e^(-(1 + 2 + … + (k-1))/N)
= e^(-k(k-1)/(2N)) (arithmetic series, Ch. 6)
≈ e^(-k²/(2N))
Set P(collision) = 1/2: k²/(2N) = ln 2, so k ≈ √(2 ln 2) · √N ≈ 1.18·√N.
For birthdays (N = 365) that gives 1.18 · 19.1 ≈ 22.5, so 23 people. ✓
| Where it bites | Numbers | Result |
|---|---|---|
| 64-bit random IDs | 1.18 · 2³² |
50% collision chance after 5.1 billion IDs, a real risk at scale |
| UUIDv4 (122 random bits) | 10¹² IDs |
P ≈ k²/2N ≈ 10⁻¹³, which is safe |
| AES-GCM random 96-bit nonces | 2³² messages per key |
P ≈ 2⁶⁴/2⁹⁷ = 2⁻³³. This is why NIST caps a key at 2³² random-nonce messages, since a repeated GCM nonce leaks the authentication key |
| SHA-1 (160-bit) | generic bound 2⁸⁰ |
the 2017 SHAttered collision needed only about 2⁶³ by exploiting structure |
How many bits of security? One table to calibrate everything
| Primitive | Best classical attack | Security (bits) | After a large quantum computer |
|---|---|---|---|
| AES-128 | brute force 2¹²⁸ |
128 | ~64 (Grover's square root), so use AES-256 for long-term data |
| SHA-256 collision | birthday 2¹²⁸ |
128 | still about 128 in practice |
| RSA-2048 | number field sieve | ~112 | 0 (Shor's algorithm) |
| RSA-3072 | number field sieve | ~128 | 0 |
| P-256 / X25519 | Pollard rho √n |
~128 | 0 (Shor's algorithm) |
| ML-KEM-768 | lattice reduction | ~192 (NIST category 3, equivalent to AES-192) | about the same (no known large quantum speed-up) |
Bits of security means "the attacker needs about 2ᵇ operations". Each extra
bit doubles the work, so 128 bits is about 3.4·10³⁸ operations, which is not
reachable with any conceivable classical computer.
Post-quantum: Learning With Errors, the maths behind ML-KEM
Shor's algorithm breaks RSA, DH and ECC because they all hide a secret in the period of modular exponentiation, and quantum computers find periods efficiently. Lattice cryptography hides the secret in noise instead:
b = A · s + e (mod q)
| Symbol | Public? | Meaning |
|---|---|---|
A |
public | a random matrix, m × n |
s |
secret | the secret vector (the key) |
e |
secret | a small random error vector, entries like -2 … 2 |
b |
public | the noisy product |
q |
public | the modulus (ML-KEM uses q = 3329) |
Without e, this is Ch. 10's A·x = b, which Gaussian elimination solves in
milliseconds. With a little noise added, elimination amplifies the errors
until the answer is garbage. The best known attacks (lattice reduction) take
exponential time, even on quantum computers. That's the Learning With
Errors problem (Regev, 2005). ML-KEM (FIPS 203, 2024) uses a structured
version over polynomials of degree 256, and Chrome, Cloudflare and OpenSSH
already negotiate hybrid X25519 + ML-KEM key exchange by default, so
"harvest now, decrypt later" recordings of today's traffic stay safe.
Real-life engineering ties
- Security: this chapter is the mathematical grounding for evaluating cryptographic choices — key sizes, algorithm deprecation decisions (why MD5/SHA-1 are banned for security use), TLS cipher suite selection.
- Network: every TLS handshake performs (EC)DHE key exchange and RSA or ECDSA signature verification live, on every HTTPS connection — this math runs billions of times a second across the internet.
- AI: less direct, but cryptographic hashing underlies content deduplication in training data pipelines, and differential privacy (adding calibrated noise to protect individual data points) borrows concepts from this same hardness-assumption tradition.
- SWE: understanding why you should never write your own crypto — the security rests entirely on decades of cryptanalysis against specific, standardized constructions, not on the general idea being sound.
More real-life examples:
- In 2010, Sony's PlayStation 3 signing system was broken because its
ECDSA implementation reused the same "random" nonce
kfor every signature instead of generating a fresh one each time — with two signatures sharing a nonce, simple algebra recovers the private key directly, a real demonstration of how fragile these assumptions are to implementation mistakes. - A 2008 bug in Debian's OpenSSL package (CVE-2008-0166) accidentally removed almost all sources of entropy from key generation, meaning the "random" keys it generated came from a pool of only about 32,768 possible values — every key generated on affected systems for nearly two years was trivially guessable, despite using "correct" RSA/DSA math.
- The 2017 ROCA vulnerability (CVE-2017-15361) found that a widely used Infineon smart-card/TPM chip generated RSA primes with a subtle structural weakness, making factoring dramatically faster — millions of real government ID cards and TPM-backed keys were affected.
- Modern TLS 1.3 mandates (EC)DHE for every handshake specifically to guarantee forward secrecy — even if a server's long-term private key is stolen later, past recorded traffic can't be decrypted, because each session's shared secret was never derived from that long-term key.
Common mistake
Believing "hard to reverse" cryptographic assumptions are mathematically proven — they are not (this echoes Ch. 19's point). RSA, DH, and ECC security all rest on unproven computational hardness assumptions that have simply resisted decades of attack. This is precisely why quantum computing (Shor's algorithm) is such a big deal: it would break the factoring and discrete-log assumptions efficiently, which is why NIST finalized post-quantum standards (ML-KEM for key exchange, ML-DSA for signatures) in 2024.