Skip to content

Appendix D: Solutions for Chapter 3

This page collects solutions and editorial notes for the exercises in Chapter 3: Hard problems at a glance. Compute and derivation exercises have worked solutions; open-ended exercises have an editorial note describing what a strong answer addresses.

Every block below is self-contained and runs on the standard library alone. The fuller versions of these routines, with the lattice-membership congruence derived rather than asserted, are in the hard_problems package under solutions/ch03-hard-problems. From a clone of the companion repository, pytest tests/ch03 runs its suite. Appendix C has the setup.

Brute search. The brute search covers 7×7−1=487 \times 7 - 1 = 48 pairs. The minimum squared norm is 25, achieved by (a,b)=(1,−1)(a, b) = (1, -1), which gives v=(2−5,7−3)=(−3,4)v = (2 - 5, 7 - 3) = (-3, 4), and by (a,b)=(−1,1)(a, b) = (-1, 1), which gives v=(3,−4)v = (3, -4). The vectors (±3,∓4)(\pm 3, \mp 4) are negatives of each other, as expected: a lattice is closed under negation, so the shortest nonzero vector always comes in ±\pm pairs.

B = [(2, 7), (5, 3)] # the basis vectors b_1, b_2, the columns of B in the exercise
norms = []
for a in range(-3, 4):
for b in range(-3, 4):
if (a, b) == (0, 0):
continue
v = (a * B[0][0] + b * B[1][0], a * B[0][1] + b * B[1][1])
norms.append((v[0] * v[0] + v[1] * v[1], (a, b), v))
norms.sort()
print("pairs checked:", len(norms))
print("minimum squared norm:", norms[0][0])
print("minimizers:", [(ab, v) for sq, ab, v in norms if sq == norms[0][0]])
# ==> pairs checked: 48
# ==> minimum squared norm: 25
# ==> minimizers: [((-1, 1), (3, -4)), ((1, -1), (-3, 4))]

Closing the global-minimum gap. The brute search above only proves that 25 is the minimum squared norm over the chosen 7×77 \times 7 coefficient box. It does not by itself rule out a shorter lattice vector that lives outside the box.

The closure is a finite enumeration. Any nonzero v∈Lv \in L with ∥v∥<5\lVert v \rVert < 5 would be an integer point (x,y)≠(0,0)(x, y) \neq (0, 0) with x2+y2<25x^2 + y^2 < 25, and the lattice membership condition is a single residue check. A point (x,y)(x, y) lies in LL iff there exist integers a,ba, b with x=2a+5bx = 2a + 5b and y=7a+3by = 7a + 3b. Eliminating aa from the two equations gives 7x−2y=29b7x - 2y = 29 b, equivalently 7x≡2y(mod29)7x \equiv 2y \pmod{29}. That direction is only necessity. For the converse, solving the system gives a=(5y−3x)/29a = (5y - 3x)/29 and b=(7x−2y)/29b = (7x - 2y)/29. Both coefficients are integers exactly when 29 divides both numerators, and 29 divides 5y−3x5y - 3x whenever it divides 7x−2y7x - 2y, because 2(5y−3x)=29x−5(7x−2y)2(5y - 3x) = 29x - 5(7x - 2y). So the single congruence really is an iff, which is what lets one modulo stand in for solving the system.

The exercise asks for every interior candidate. There are 68 nonzero integer points with x2+y2<25x^2 + y^2 < 25:

  • x=0x = 0, with y∈{±1,±2,±3,±4}y \in \{\pm 1, \pm 2, \pm 3, \pm 4\}: 8 points
  • x∈{±1,±2}x \in \{\pm 1, \pm 2\}, with y∈{−4,…,4}y \in \{-4, \dots, 4\}: 36 points
  • x=±3x = \pm 3, with y∈{−3,…,3}y \in \{-3, \dots, 3\}: 14 points
  • x=±4x = \pm 4, with y∈{−2,…,2}y \in \{-2, \dots, 2\}: 10 points

Applying the residue check to all 68 returns the empty set.

# Strict-interior search: integer points (x, y) != (0, 0) with x^2 + y^2 < 25
# that satisfy the lattice membership condition 7x - 2y ≡ 0 (mod 29).
candidates_interior = []
for x in range(-4, 5):
for y in range(-4, 5):
if (x, y) == (0, 0):
continue
if x * x + y * y >= 25:
continue
if (7 * x - 2 * y) % 29 == 0:
candidates_interior.append((x, y, x * x + y * y))
print("short lattice points (sq norm < 25):", candidates_interior)
# ==> short lattice points (sq norm < 25): []

That settles strict shortness: no lattice vector has squared norm <25< 25. The boundary set x2+y2=25x^2 + y^2 = 25 contains exactly twelve integer points ((±5,0)(\pm 5, 0), (0,±5)(0, \pm 5), (±3,±4)(\pm 3, \pm 4), (±4,±3)(\pm 4, \pm 3)). The same residue check picks out which of those are in LL.

# Boundary search: integer points with x^2 + y^2 == 25 in the lattice.
candidates_boundary = []
for x in range(-5, 6):
for y in range(-5, 6):
if x * x + y * y != 25:
continue
if (7 * x - 2 * y) % 29 == 0:
candidates_boundary.append((x, y))
print("lattice points on sq norm 25:", candidates_boundary)
# ==> lattice points on sq norm 25: [(-3, 4), (3, -4)]

The boundary check returns exactly the two vectors found in the brute search, so λ1(L)=5\lambda_1(L) = 5 globally, with minimizers (±3,∓4)(\pm 3, \mp 4). Minkowski’s first theorem (Chapter 7) is unnecessary for this finite-search proof. It gives a different statement, an existence bound on some short nonzero lattice vector rather than a uniform bound on every lattice vector, and is left for Chapter 7’s general treatment.

The syndrome (1,0,1)⊤(1, 0, 1)^\top read as a binary integer (most significant bit first) is 55, so the error is at position 5. The weight-1 error vector is e=(0,0,0,0,1,0,0)e = (0, 0, 0, 0, 1, 0, 0), with the 1 in the fifth coordinate.

For any parity-check matrix HH, a weight-1 error in coordinate jj has syndrome equal to column jj of HH. What the standard Hamming ordering adds is that those three bits can be read directly as the coordinate number, because column jj is the binary representation of jj. For a generic matrix one must instead search for the matching column, and repeated or zero columns can leave a weight-1 error ambiguous or invisible. A full-rank 3×73 \times 7 matrix whose seven columns are the seven distinct nonzero 3-bit vectors is a Hamming code up to a column permutation and a change of row basis. The only thing genuinely lost is the convenient indexing.

That indexing convenience is not why the McEliece family, in Chapter 20, uses Goppa codes. McEliece needs a secret structured family that admits an efficient algebraic decoder, while publishing a disguised generator or parity-check matrix that is meant to look like a random one. The decoder is the asset. The assumption the submission states is one-wayness of decoding for the published key itself, and, as the chapter says, it does not require the disguised matrix to be indistinguishable from a random one. Neither is about reading a syndrome as an index.

H = [[int(b) for b in format(j, "03b")] for j in range(1, 8)] # columns
H = list(zip(*H)) # rows: 3 rows x 7 cols
s = (1, 0, 1)
position = int("".join(str(b) for b in s), 2)
e = [0] * 7
e[position - 1] = 1
print("error position (1-based):", position)
print("error vector:", e)
# ==> error position (1-based): 5
# ==> error vector: [0, 0, 0, 0, 1, 0, 0]

Exact problems. A reference statement of all three (compare your own version to this, then to Regev’s paper):

  • Shortest vector problem (SVP). Input: a basis BB of a lattice L⊂RnL \subset \mathbb{R}^n. Output: a nonzero lattice vector v∈Lv \in L of minimum Euclidean norm.
  • Closest vector problem (CVP). Input: a basis BB of a lattice LL and a target t∈Rnt \in \mathbb{R}^n. Output: a lattice vector v∈Lv \in L that minimizes ∥v−t∥\lVert v - t \rVert.
  • Learning with errors (LWE), search. Input: a list of pairs (ai,bi)(a_i, b_i) where each ai∈Zqna_i \in \mathbb{Z}_q^n is uniform and each bi=⟨ai,s⟩+ei(modq)b_i = \langle a_i, s \rangle + e_i \pmod q for a fixed secret s∈Zqns \in \mathbb{Z}_q^n and small noise eie_i drawn from a narrow distribution (typically a discrete Gaussian). Output: the secret ss.

Decision and gap variants. The exact problems above are not all of what the cryptographic reductions target. Search LWE in particular is the average-case problem Regev’s reduction lands on. GapSVPγ\mathrm{GapSVP}_\gamma and GapCVPγ\mathrm{GapCVP}_\gamma are promise decision variants of SVP and CVP. Decisional LWE is a distribution-distinguishing variant, not a gap problem, so the three below are not three of a kind.

  • GapSVPγ\mathrm{GapSVP}_\gamma. Input: a basis BB of a lattice L⊂RnL \subset \mathbb{R}^n and a threshold d>0d > 0, under the promise that λ1(L)≤d\lambda_1(L) \leq d or λ1(L)>γd\lambda_1(L) > \gamma d. Output: which of the two cases holds.
  • GapCVPγ\mathrm{GapCVP}_\gamma. Input: a basis BB, a target tt, and a threshold d>0d > 0, under the promise that dist(t,L)≤d\mathrm{dist}(t, L) \leq d or dist(t,L)>γd\mathrm{dist}(t, L) > \gamma d. Output: which of the two cases holds.
  • Decisional LWE. Input: mm samples of the form (ai,bi)(a_i, b_i) with aia_i uniform in Zqn\mathbb{Z}_q^n. Output: decide whether the samples come from the LWE distribution (bi=⟨ai,s⟩+ei(modq)b_i = \langle a_i, s \rangle + e_i \pmod q for a fixed secret ss and noise eie_i) or from the uniform distribution on Zqn×Zq\mathbb{Z}_q^n \times \mathbb{Z}_q.

A complete answer also notes that the worst-case-to-average-case reduction (Regev, 2009) targets GapSVPγ\mathrm{GapSVP}_\gamma and SIVPγ\mathrm{SIVP}_\gamma, at approximation factors γ\gamma polynomial in nn, and reaches them through average-case search LWE. It does not target GapCVP\mathrm{GapCVP}. Regev’s decision-to-search lemma assumes a prime qq polynomial in nn and holds for any fixed error distribution χ\chi. The noise-rate condition belongs to the separate worst-case lattice reduction, not to this one.

(a) SIDH publishes the images of fixed torsion-point bases under the secret isogeny as part of the public protocol. The Castryck-Decru attack uses those auxiliary torsion-point images to build an isogeny out of a product of two elliptic curves, the public curve and a second curve the attacker computes. Kani’s criterion says whether that isogeny lands on a product again, which lets the attacker test guesses about the secret isogeny and recover it piece by piece. The running time is heuristically polynomial and classical, assuming the endomorphism ring of SIDH’s starting curve is known, and apart from factoring a small number of parameter-dependent integers. The attack is specifically against the SIDH transcript (smooth-degree path-finding plus published torsion images), not against the general supersingular isogeny problem with no auxiliary data.

(b) SQIsign signs by exhibiting a single isogeny between two specific supersingular curves. The procedure has no SIDH-style images of publicly fixed torsion bases under the secret isogeny to feed into Kani’s criterion, so the attack does not transfer.

The claim has to be that specific. Current SQIsign signatures do carry interpolation point images and compact basis-generation hints; what they lack is the particular secret-isogeny leakage SIDH exposed. SQIsign’s central hardness assumption is the supersingular endomorphism-ring problem (recover End(E)\mathrm{End}(E) from EE). It is equivalent under probabilistic polynomial-time reductions to the OneEnd problem (Theorem 1.1 in Page & Wesolowski, 2024) and to finding an arbitrary isogeny in efficient representation, the latter with no unproved hypothesis only since 2025 (Theorem 1 in Herlédan Le Merdy & Wesolowski, 2025). It is not the smooth-degree torsion-leakage form SIDH was instantiated against. Chapter 23 defines OneEnd and an efficient representation, and states both equivalences.

Herlédan Le Merdy, A., & Wesolowski, B. (2025). Unconditional foundations for supersingular isogeny-based cryptography. Cryptology ePrint Archive, Paper 2025/271. https://eprint.iacr.org/2025/271
Page, A., & Wesolowski, B. (2024). The Supersingular Endomorphism Ring and One Endomorphism Problems are Equivalent. In M. Joye & G. Leander (Eds.), Advances in Cryptology – EUROCRYPT 2024, Part VI (Vol. 14656, pp. 388–417). Springer, Cham. https://doi.org/10.1007/978-3-031-58751-1_14
Regev, O. (2009). On lattices, learning with errors, random linear codes, and cryptography. Journal of the ACM, 56(6), 34:1-34:40. https://doi.org/10.1145/1568318.1568324