Appendix D: Solutions for Chapter 4
This page collects solutions and editorial notes for the exercises in Chapter 4: From classical to post-quantum. Compute and derivation exercises have worked solutions; open-ended exercises have an editorial note describing what a strong answer addresses.
The fuller versions of these routines are in the classical package under solutions/ch04-classical-to-pq. From a clone of the companion repository, pytest tests/ch04 runs its suite. Appendix C has the setup.
Exercise 1
Section titled “Exercise 1”The prime-counting theorem says the density of primes near is . A uniformly random 32-bit candidate is therefore prime with probability about 4.5%, which would put the expected count at about 22 candidates per prime.
That is the wrong regime for this implementation. _random_prime takes a random draw of the requested width and forces both its top bit and its bottom bit high. The top bit fixes the width at exactly 32 bits. The trailing | 1 forces every candidate odd, so no test is ever spent on an even number. Every prime above 2 is odd, so the density among the candidates actually drawn is twice the density among all integers, and the expected count halves to per prime. A keygen(bits=64) invocation draws two primes, so it makes about 22 Miller-Rabin calls in total, plus a small additional retry rate for the and checks.
Instrumenting the call and averaging over 200 seeds puts the implementation in the odd-forced regime, within a few percent of :
import random, syssys.path.insert(0, "solutions/ch04-classical-to-pq/src")import classical.rsa as rsa
calls = 0inner = rsa._miller_rabin
def counted(n, witnesses): global calls calls += 1 return inner(n, witnesses)
rsa._miller_rabin = counted
totals = []for seed in range(200): calls = 0 rsa.keygen(bits=64, rng=random.Random(seed)) totals.append(calls)
mean_per_keygen = sum(totals) / len(totals)print("calls per keygen:", round(mean_per_keygen, 1))print("calls per prime :", round(mean_per_keygen / 2, 1))# ==> calls per keygen: 22.9# ==> calls per prime : 11.5Any single seed scatters widely, because the count per prime is geometrically distributed; the average over many seeds is what should agree with the theory.
Exercise 2
Section titled “Exercise 2”For , the post-processing computes . Numerically , and . recover_factor returns a single integer, the first non-trivial candidate, so it returns . The cofactor is the other prime.
For , the order is even, but . The standard recovery requires . The case contradicts being the order. The case makes , because is odd, and , a trivial factor. Both branches give no useful factor. This is the documented retry condition. The classical Shor loop responds by drawing a fresh random base and starting over. Take a uniformly random unit modulo , with odd and carrying at least two distinct prime factors. The combined probability of an odd order or is then at most , so a constant number of retries succeeds in expectation. If at the outset, that gcd is already a factor and the quantum step is not needed at all.
n = 3233print(pow(10, 390, n))print(pow(2, 390, n))# ==> 2439# ==> 3232Exercise 3
Section titled “Exercise 3”The expected sequence: keygen() returns a (private_key, public_key) pair, private key first. sign(private_key, message, nonce) returns a signature pair , with the message passed as bytes. verify(public_key, message, signature) returns True. Flipping the first byte changes the SHA-256 digest and therefore the message representative , so the verification computation lands on a different point and . Verification returns False. Stated generally the guarantee is computational rather than absolute: absent a hash collision or a successful forgery, changing the signed bytes invalidates the signature. The surface is intentionally minimal and matches the verification equation in the chapter.
Exercise 4: recovering the ECDSA private key from a reused nonce
Section titled “Exercise 4: recovering the ECDSA private key from a reused nonce”Both signatures carry the same , because both come from with under the same nonce . Subtracting the two signing equations gives a linear relation in :
Subtracting eliminates :
Assume . Distinct messages do not by themselves guarantee this, since two messages can share a digest or reduce to the same representative, but it holds for any pair an attacker can actually use. Because is invertible modulo the prime order , the two signing equations then force , so is invertible modulo . Solving for :
That recovers from public data () alone. With in hand, substitute into the first signing equation and solve for . Multiplying both sides by :
Real ECDSA requires , and secp256k1 has prime group order , so is invertible modulo . Primality alone would not be enough: has no inverse for any modulus. Inverting :
The private key falls out in two modular operations once the nonce is known.
The PlayStation 3 code signing key was recovered this way in 2010, after fail0verflow showed that Sony’s ECDSA signer used a fixed value in place of a per-signature random nonce (fail0verflow, 2010). Bitcoin has leaked keys the same way, and at measurable scale: Bos and colleagues found 158 public keys on the chain that had signed more than once under the same nonce, each of them recoverable (Bos et al., 2014). Reuse is observable on-chain precisely because is published with every signature. The defense is deterministic nonce generation, which derives from the private key and the message hash with HMAC-DRBG and so removes any dependence on a signing-time randomness source (Pornin, 2013). It does not guarantee a distinct nonce for every distinct message: equal message-hash representatives feed the derivation identical input and produce identical . What it removes is the failure mode that caused both incidents above, a signer whose entropy source repeats or is absent.
N = 17 # toy stand-in for the secp256k1 group orderr = 5s1, s2 = 7, 11z1, z2 = 3, 9# nonce reuse: solve k = (z1 - z2)(s1 - s2)^{-1} mod Nk = ((z1 - z2) * pow(s1 - s2, -1, N)) % N# private key: d = (s1 k - z1) r^{-1} mod Nd = ((s1 * k - z1) * pow(r, -1, N)) % Nprint(k, d)# ==> 10 10The toy values above are illustrative. Real ECDSA operates over the secp256k1 group order, and the same algebraic identity holds.