Appendix D: Solutions for Chapter 20
This page collects solutions and editorial notes for the exercises in Chapter 20: McEliece: the original PQC. 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 mceliece package under solutions/ch20-mceliece. From a clone of the companion repository, pytest tests/ch20 runs its suite. Appendix C has the setup.
Exercise 1
Section titled “Exercise 1”The exercise is a roundtrip sanity check. With the encrypter adds a single error, and the chapter’s warm-up recovers it by syndrome lookup rather than by Patterson. The table has only entries at these parameters, and every nonzero syndrome equals exactly one column of . The underlying code has minimum distance and corrects 1 error, so every weight-1 error is correctable and every roundtrip succeeds. The expected output is 16/16 successful roundtrips with no decryption failures. If a roundtrip fails, the cause is almost always a bug in the GF(8) arithmetic or the key randomization, not a structural decoding problem.
Running it over all 16 messages and all 7 error positions, rather than one error position per message, is the stronger check and costs nothing:
G = [[0,1,1,1,0,0,0],[1,1,0,0,1,0,0],[1,1,1,0,0,1,0],[1,0,1,0,0,0,1]]S = [[1,0,0,0],[1,1,0,0],[0,1,1,0],[0,0,1,1]]S_inv = [[1,0,0,0],[1,1,0,0],[1,1,1,0],[1,1,1,1]]H = [[0,1,1,0,1,0,1],[1,0,0,0,1,1,1],[1,1,0,1,0,0,1]]perm = [2, 0, 4, 1, 6, 3, 5]
perm_inv = [0] * 7for i, j in enumerate(perm): perm_inv[j] = i
# G_pub = S * G * P.SG = [[sum(S[r][i] * G[i][c] for i in range(4)) % 2 for c in range(7)] for r in range(4)]G_pub = [[SG[r][perm_inv[c]] for c in range(7)] for r in range(4)]
ok = 0for code in range(16): msg = [(code >> b) & 1 for b in range(4)] for pos in range(7): c = [0] * 7 for i, mi in enumerate(msg): if mi: c = [x ^ y for x, y in zip(c, G_pub[i])] ct = list(c) ct[pos] ^= 1 unperm = [0] * 7 for i in range(7): unperm[perm_inv[i]] = ct[i] s = [sum(a * x for a, x in zip(row, unperm)) % 2 for row in H] for j in range(7): if [H[r][j] for r in range(3)] == s: unperm[j] ^= 1 break scrambled = unperm[3:] rec = [sum(scrambled[i] * S_inv[i][j] for i in range(4)) % 2 for j in range(4)] ok += (rec == msg)
print(f"{ok} of {16 * 7} roundtrips recovered the message")# ==> 112 of 112 roundtrips recovered the messageExercise 2
Section titled “Exercise 2”Expected Prange iterations: (equivalently , the form the chapter’s prange_exponent uses), far below the vectors a brute-force search enumerates. On iteration count alone Prange essentially always wins: , so the expected iteration count never exceeds the brute-force count.
That comparison is misleading because the two units of work differ. One brute-force trial is a single weight- syndrome check, bit operations. One Prange iteration is a Gaussian elimination on an matrix to test one information set, on the order of bit operations (here against the of a brute-force check). The honest comparison multiplies the iteration count by the per-iteration cost. ISD’s real advantage is asymptotic, the iteration-count term shrinking exponentially faster than the per-iteration polynomial grows, not “fewer iterations” on a 16-bit toy. At the mceliece348864 regime (, , ) the iteration term is the the chapter computes, with the Gaussian-elimination cost a polynomial factor on top.
import mathprint(round(math.comb(16, 8) / math.comb(14, 8), 2))print(math.comb(16, 2))# ==> 4.29# ==> 120Exercise 3
Section titled “Exercise 3”Systematic-form key: bits. For mceliece348864: bits bytes (255 KiB). Raw ciphertext: bits bytes. Ratio: .
Key size dominates because public keys are transmitted once per protocol session (TLS, IKEv2), and 255 KiB does not merely strain that transport. It exceeds what a single TLS extension can carry: an extension’s extension_data is length-prefixed with two bytes, capping it at 65,535. A raw McEliece ciphertext is ~436 bytes, and Classic McEliece’s Niederreiter syndrome is 96, either of which fits any modern protocol with room to spare. The bottleneck is the one-time key transport, not per-message bandwidth.
n, k = 3488, 2720pk = k * (n - k) // 8ct = n // 8print(pk, ct, round(pk / ct, 1))# ==> 261120 436 598.9Exercise 4
Section titled “Exercise 4”For the toy parameters , the parity-check matrix has rows. There are distinct weight-2 error vectors and syndromes, so counting alone leaves injectivity possible without establishing it. The minimum distance establishes it. Two distinct weight-2 vectors differ by a nonzero vector of weight at most 4, the code has , so that difference is not a codeword and the two syndromes cannot coincide. The same argument covers weight in general, which is what makes the syndrome a faithful ciphertext for a weight-2 message.
Enumerate to confirm it:
import sys, pathlib, random, itertoolssys.path.insert(0, str(pathlib.Path("solutions/ch20-mceliece/src")))from mceliece.gf2m import poly_evalfrom mceliece.goppa import goppa_parity_check, find_irreducible_goppa_poly, full_supportfrom mceliece.gf2 import random_invertible_matrix, random_permutation_matrix, mat_mul
m, irred, t, n = 4, 0b10011, 2, 16rng = random.Random(42)g = find_irreducible_goppa_poly(m, irred, t, rng)support = [a for a in full_support(m) if poly_eval(g, a, m, irred) != 0]H = goppa_parity_check(m, irred, g, support)
# H_pub = M * H * P, the Niederreiter public key.M = random_invertible_matrix(len(H), rng)P, _perm = random_permutation_matrix(n, rng)H_pub = mat_mul(mat_mul(M, H), P)
seen = {}for combo in itertools.chain.from_iterable( itertools.combinations(range(n), w) for w in range(t + 1)): e = [0] * n for c in combo: e[c] = 1 s = tuple(sum(H_pub[r][c] * e[c] for c in range(n)) % 2 for r in range(len(H_pub))) seen.setdefault(s, []).append(combo)
weight2 = sum(1 for v in seen.values() for c in v if len(c) == 2)print(f"weight-2 patterns: {weight2}")print(f"patterns of weight <= {t}: {sum(len(v) for v in seen.values())}")print(f"distinct syndromes: {len(seen)}")print(f"injective: {all(len(v) == 1 for v in seen.values())}")# ==> weight-2 patterns: 120# ==> patterns of weight <= 2: 137# ==> distinct syndromes: 137# ==> injective: TrueNiederreiter security is identical to McEliece’s: the disguising matrix and permutation hide the algebraic structure of the underlying Goppa code, and recovering from without the secret key reduces to syndrome decoding of a random-looking binary code. The advantage of Niederreiter is smaller ciphertexts: the message is a weight- error vector of length , carrying bits, far less than the -bit codewords McEliece transmits. This is why the NIST submission, Classic McEliece, uses the Niederreiter syndrome form (with a CCA-secure KEM transform: ciphertext verification and implicit rejection), giving the 96-byte mceliece348864 ciphertext. The original generator-matrix McEliece form is the one this chapter’s toy implements.