Chapter 22: Isogenies for programmers
Chapters 19 through 21 built cryptosystems from linear codes over . McEliece’s 1978 scheme hides a structured code behind a random-looking generator matrix; HQC keeps a random-looking code but gives it quasi-cyclic structure, so a single row describes a whole circulant block. Both rest on the hardness of decoding a random linear code.
This chapter moves to a different mathematical object: maps between elliptic curves. An isogeny is a group homomorphism from one elliptic curve to another that is also a rational map. Given a finite subgroup of a curve , there is a unique isogeny with kernel . Computing is efficient (Velu gave explicit formulas in 1971). In the supersingular graph at cryptographic parameters, recovering a low-degree isogeny path between two given curves, without auxiliary information about the connecting kernel, is believed to be hard.
SIDH (Supersingular Isogeny Diffie-Hellman) built a key exchange on this hardness assumption in 2011 (De Feo et al., 2014). In July 2022, Castryck and Decru broke it by exploiting auxiliary information that the protocol published: heuristic polynomial time when the starting curve’s endomorphism ring is known, which holds for SIKE’s starting curve (Castryck & Decru, 2023). The break made SIKE non-viable as a NIST candidate. The SIKE team’s final postscript states that SIKE and SIDH are insecure and should not be used (Jao et al., 2022). The attack does not apply to CSIDH or SQIsign (Chapter 23): they are not SIDH-style protocols and do not publish the secret isogeny’s auxiliary torsion-point images, the data Castryck-Decru exploit. That separates them from the SIDH break; it is not a blanket proof of their security.
This chapter builds a toy SIDH from scratch at , then explains why the break works.
A first isogeny
Section titled “A first isogeny”The curve over has rational points. The point satisfies (it is a 2-torsion point, since forces the tangent line to be vertical). The kernel is , so the Velu sum has one nontrivial term.
This formula is equivalent to Velu’s original (Vélu, 1971). See also (Silverman, 2009). For a kernel and a point ,
This requires only point addition on the source curve.
p = 431a, b = 1, 0 # E_0: y^2 = x^3 + x
def point_add(P, Q, a, p): if P is None: return Q if Q is None: return P x1, y1 = P; x2, y2 = Q if x1 == x2: if (y1 + y2) % p == 0: return None lam = (3*x1*x1 + a) * pow(2*y1, -1, p) % p else: lam = (y2 - y1) * pow(x2 - x1, -1, p) % p x3 = (lam*lam - x1 - x2) % p y3 = (lam*(x1 - x3) - y1) % p return (x3, y3)
def scalar_mul(k, P, a, p): R = None while k: if k & 1: R = point_add(R, P, a, p) P = point_add(P, P, a, p) k >>= 1 return R
# The 2-torsion point T = (0, 0): 2T = O since y_T = 0.T = (0, 0)print(scalar_mul(2, T, a, p))# ==> None
# Velu evaluation formula with kernel {O, T}.G = (13, 290) # generator of E_0(F_431), order 432GpT = point_add(G, T, a, p)phi_G = ((G[0] + GpT[0] - T[0]) % p, (G[1] + GpT[1] - T[1]) % p)print(phi_G)# ==> (212, 426)
# A second image to recover the codomain y^2 = x^3 + a'x + b'.P2 = scalar_mul(5, G, a, p)P2pT = point_add(P2, T, a, p)phi_P2 = ((P2[0] + P2pT[0] - T[0]) % p, (P2[1] + P2pT[1] - T[1]) % p)
# Two points on the codomain give two equations:# y_i^2 - x_i^3 = a'*x_i + b'# Subtract to solve for a'.x1, y1 = phi_Gx2, y2 = phi_P2lhs1 = (y1*y1 - x1**3) % plhs2 = (y2*y2 - x2**3) % pa_new = (lhs1 - lhs2) * pow(x1 - x2, -1, p) % pb_new = (lhs1 - a_new*x1) % p
def j_inv(a, b, p): num = 4 * pow(a, 3, p) % p denom = (num + 27 * pow(b, 2, p)) % p return 1728 * num * pow(denom, -1, p) % p
# j = 1728 * 4a^3 / (4a^3 + 27b^2). Over F_431, 1728 mod 431 = 4.print(f"j(E_0) = {j_inv(1, 0, p)}")# ==> j(E_0) = 4print(f"j(E_0/<T>) = {j_inv(a_new, b_new, p)}")# ==> j(E_0/<T>) = 4Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch22/, one file per block. Appendix C covers the clone and the environment they run on.
The j-invariant did not change: . This is not a bug. The curve has , which admits extra automorphisms (the map is an automorphism over where ). The degree-2 isogeny with this specific kernel produces a curve isomorphic to the original. A degree-3 isogeny moves to a different j-invariant:
p = 431a, b = 1, 0
def point_add(P, Q, a, p): if P is None: return Q if Q is None: return P x1, y1 = P; x2, y2 = Q if x1 == x2: if (y1 + y2) % p == 0: return None lam = (3*x1*x1 + a) * pow(2*y1, -1, p) % p else: lam = (y2 - y1) * pow(x2 - x1, -1, p) % p x3 = (lam*lam - x1 - x2) % p y3 = (lam*(x1 - x3) - y1) % p return (x3, y3)
def scalar_mul(k, P, a, p): R = None while k: if k & 1: R = point_add(R, P, a, p) P = point_add(P, P, a, p) k >>= 1 return R
def j_inv(a, b, p): num = 4 * pow(a, 3, p) % p denom = (num + 27 * pow(b, 2, p)) % p return 1728 * num * pow(denom, -1, p) % p
G = (13, 290)
# 3-torsion point: 144*G has order 3 (since 432/3 = 144).P3 = scalar_mul(144, G, a, p)print(f"P3 = {P3}")# ==> P3 = (261, 309)print(f"3*P3 = {scalar_mul(3, P3, a, p)}")# ==> 3*P3 = None
# Degree-3 isogeny via the Velu evaluation formula.kernel_3 = [None, P3, scalar_mul(2, P3, a, p)]x_new, y_new = G[0], G[1]for R in kernel_3: if R is None: continue GpR = point_add(G, R, a, p) x_new = (x_new + GpR[0] - R[0]) % p y_new = (y_new + GpR[1] - R[1]) % pphi3_G = (x_new, y_new)print(f"phi_3(G) = {phi3_G}")# ==> phi_3(G) = (412, 94)
# Recover the codomain's j-invariant.P2 = scalar_mul(7, G, a, p)x2n, y2n = P2[0], P2[1]for R in kernel_3: if R is None: continue P2R = point_add(P2, R, a, p) x2n = (x2n + P2R[0] - R[0]) % p y2n = (y2n + P2R[1] - R[1]) % px1, y1 = phi3_G; x2, y2 = x2n, y2nlhs1 = (y1*y1 - x1**3) % plhs2 = (y2*y2 - x2**3) % pa3 = (lhs1 - lhs2) * pow(x1 - x2, -1, p) % pb3 = (lhs1 - a3*x1) % pprint(f"j(E_0/<P3>) = {j_inv(a3, b3, p)}")# ==> j(E_0/<P3>) = 319The degree-3 isogeny sends to . At the supersingular isogeny graph has 37 vertices. This computation traversed one edge.
The mathematics of isogenies
Section titled “The mathematics of isogenies”Isogenies and kernels
Section titled “Isogenies and kernels”An isogeny is a nonconstant rational map between elliptic curves that sends the identity to the identity. Because it is a rational map satisfying , it is automatically a group homomorphism: for all (Theorem III.4.8 in Silverman, 2009).
The kernel is a finite subgroup of . The converse holds: for every finite subgroup , there exists a unique (up to isomorphism of the target) separable isogeny with (Proposition III.4.12 in Silverman, 2009). The subgroup determines the isogeny.
Degree and the dual
Section titled “Degree and the dual”The degree of a separable isogeny equals the size of its kernel: (Theorem III.4.10c in Silverman, 2009). For every isogeny of degree , there exists a unique dual isogeny satisfying , the multiplication-by- map on (Theorem III.6.1 in Silverman, 2009). The dual has the same degree: .
The j-invariant
Section titled “The j-invariant”Two elliptic curves over an algebraically closed field are isomorphic if and only if they have the same j-invariant (Proposition III.1.4b in Silverman, 2009). For a short Weierstrass curve , the j-invariant is
The j-invariant classifies curves up to isomorphism over an algebraically closed field. Two curves with different j-invariants are non-isomorphic. Two curves with the same j-invariant become isomorphic after passing to the algebraic closure. Over a finite base field they may still be non-isomorphic twists (Proposition X.5.4 in Silverman, 2009).
Velu’s formulas
Section titled “Velu’s formulas”Velu’s 1971 paper (Vélu, 1971) gives explicit rational formulas for computing given a finite subgroup . An equivalent evaluation formula computes the image of a point by summing correction terms from each kernel element (Vélu, 1971):
where . This requires point additions on the source curve. For a kernel of order , the cost is field operations.
Supersingular curves and their graph
Section titled “Supersingular curves and their graph”For , an elliptic curve over is supersingular if and only if the trace of Frobenius is zero, equivalently (Exercise V.5.10a and V.5.10b in Silverman, 2009) (see Chapter 3). The chapter works at , so this criterion applies directly.
Over , the number of supersingular j-invariants is , where depends on (Theorem V.4.1c in Silverman, 2009). For , and , giving 37 total.
Fix a prime . The -isogeny graph has supersingular j-invariants over as vertices and -isogenies as edges. The torsion subgroup has exactly cyclic subgroups of order (one for each point of ), so each vertex has outgoing edges (Corollary III.6.4 in Silverman, 2009). For , the graph is 3-regular; for , it is 4-regular.
Figure 22.1 draws the local 2-isogeny structure at the chapter’s starting vertex . This vertex is not generic. The curve has extra automorphisms, so its three order-2 kernels do not reach three distinct neighbors. The kernel returns a curve isomorphic to , a self-loop at . The two kernels over both reach , a double edge.
The supersingular -isogeny graph is therefore regular only when edges are counted with multiplicity. Vertices with extra automorphisms, such as and , can produce loops or parallel edges. A generic vertex has three distinct neighbors. The degeneracy is not confined to degree 2. The same vertex has four order-3 kernels reaching only two distinct neighbors, so a breadth-first search from here expands more slowly than a branching factor of four would predict (Appendix D, Exercise 3).
Pizer showed that this graph is a Ramanujan graph: its spectral gap is optimal, which means random walks mix rapidly to their stationary distribution (Pizer, 1990). That distribution is uniform up to the vertices with extra automorphisms. For the natural walk that picks one of the kernels uniformly, a vertex’s stationary weight is proportional to its out-degree divided by the order of its reduced automorphism group, so carries half a generic vertex’s weight and a third (Theorem 4.9 in Florit & Smith, 2021). Both exceptional vertices are present at , so “uniform over all -invariants” is a statement about the generic ones. Walking the graph is efficient. Finding the path someone walked, given only the start and end vertices, is believed to be hard: the best classical algorithm ran in time for three decades (Delfs & Galbraith, 2016), and the heuristic July 2026 algorithm that reaches , at the cost of memory, is still exponential in (Wesolowski, 2026).
Why F_{p^2}?
Section titled “Why F_{p^2}?”The curve has points over . But the full -torsion subgroup has elements, most of which live over the extension field . Over alone, the 2-torsion is , cyclic of order 2 (because has no roots mod 431, since ).
SIDH needs independent generators for the full -torsion to define a key exchange. This forces the arithmetic into . We represent with elements as pairs meaning .
p = 431
def fp2_mul(x, y, p): return ((x[0]*y[0] - x[1]*y[1]) % p, (x[0]*y[1] + x[1]*y[0]) % p)
def fp2_inv(x, p): norm = (x[0]*x[0] + x[1]*x[1]) % p inv_n = pow(norm, -1, p) return ((x[0]*inv_n) % p, ((-x[1])*inv_n) % p)
# Verify: i^2 = -1 in F_{431^2}.i = (0, 1)print(fp2_mul(i, i, p))# ==> (430, 0)
# Verify: (3 + 5i)*(3 + 5i)^{-1} = 1.z = (3, 5)z_inv = fp2_inv(z, p)print(fp2_mul(z, z_inv, p))# ==> (1, 0)The result is , confirming .
Endomorphism rings (preview)
Section titled “Endomorphism rings (preview)”The endomorphism ring of a supersingular curve over is a maximal order in the quaternion algebra ramified at and . Deuring’s theorem makes the reverse direction exact: conjugacy classes of maximal orders in correspond to supersingular -invariants taken up to Galois conjugacy, that is to the pairs rather than to single -invariants (Deuring, 1941; Lemma 42.4.1 in Voight, 2021). Chapter 23 states the theorem in the form SQIsign needs and builds key generation and signing on it.
Building SIDH from scratch
Section titled “Building SIDH from scratch”Elliptic curve arithmetic over F_{p^2}
Section titled “Elliptic curve arithmetic over F_{p^2}”Chapter 4 implemented point addition for secp256k1 ( over a 256-bit prime field). The arithmetic generalizes to an arbitrary short Weierstrass curve with coordinates in .
p = 431
def fp2_add(x, y, p): return ((x[0]+y[0]) % p, (x[1]+y[1]) % p)def fp2_sub(x, y, p): return ((x[0]-y[0]) % p, (x[1]-y[1]) % p)def fp2_mul(x, y, p): return ((x[0]*y[0]-x[1]*y[1]) % p, (x[0]*y[1]+x[1]*y[0]) % p)def fp2_inv(x, p): norm = (x[0]*x[0]+x[1]*x[1]) % p return ((x[0]*pow(norm,-1,p)) % p, ((-x[1])*pow(norm,-1,p)) % p)def fp2_neg(x, p): return ((-x[0]) % p, (-x[1]) % p)def fp2_sqr(x, p): return ((x[0]*x[0]-x[1]*x[1]) % p, (2*x[0]*x[1]) % p)
def ec_add(P, Q, a, p): if P is None: return Q if Q is None: return P x1, y1 = P; x2, y2 = Q if x1[0]%p == x2[0]%p and x1[1]%p == x2[1]%p: ny2 = fp2_neg(y2, p) if y1[0]%p == ny2[0]%p and y1[1]%p == ny2[1]%p: return None num = fp2_add(fp2_mul((3,0), fp2_sqr(x1,p), p), a, p) den = fp2_mul((2,0), y1, p) lam = fp2_mul(num, fp2_inv(den,p), p) else: lam = fp2_mul(fp2_sub(y2,y1,p), fp2_inv(fp2_sub(x2,x1,p),p), p) x3 = fp2_sub(fp2_sub(fp2_sqr(lam,p), x1, p), x2, p) y3 = fp2_sub(fp2_mul(lam, fp2_sub(x1,x3,p), p), y1, p) return (x3, y3)
def ec_mul(k, P, a, p): R = None while k: if k & 1: R = ec_add(R, P, a, p) P = ec_add(P, P, a, p) k >>= 1 return R
# E_0: y^2 = x^3 + x, with a = (1,0), b = (0,0) in F_{p^2}.a0 = (1, 0)G = ((13, 0), (290, 0))print(ec_mul(432, G, a0, p))# ==> NoneVelu’s formulas over F_{p^2}
Section titled “Velu’s formulas over F_{p^2}”Velu’s evaluation formula is field-agnostic: substitute operations throughout.
p = 431
def fp2_add(x, y, p): return ((x[0]+y[0]) % p, (x[1]+y[1]) % p)def fp2_sub(x, y, p): return ((x[0]-y[0]) % p, (x[1]-y[1]) % p)def fp2_mul(x, y, p): return ((x[0]*y[0]-x[1]*y[1]) % p, (x[0]*y[1]+x[1]*y[0]) % p)def fp2_inv(x, p): norm = (x[0]*x[0]+x[1]*x[1]) % p return ((x[0]*pow(norm,-1,p)) % p, ((-x[1])*pow(norm,-1,p)) % p)def fp2_neg(x, p): return ((-x[0]) % p, (-x[1]) % p)def fp2_sqr(x, p): return ((x[0]*x[0]-x[1]*x[1]) % p, (2*x[0]*x[1]) % p)
def ec_add(P, Q, a, p): if P is None: return Q if Q is None: return P x1, y1 = P; x2, y2 = Q if x1[0]%p == x2[0]%p and x1[1]%p == x2[1]%p: ny2 = fp2_neg(y2, p) if y1[0]%p == ny2[0]%p and y1[1]%p == ny2[1]%p: return None num = fp2_add(fp2_mul((3,0), fp2_sqr(x1,p), p), a, p) den = fp2_mul((2,0), y1, p) lam = fp2_mul(num, fp2_inv(den,p), p) else: lam = fp2_mul(fp2_sub(y2,y1,p), fp2_inv(fp2_sub(x2,x1,p),p), p) x3 = fp2_sub(fp2_sub(fp2_sqr(lam,p), x1, p), x2, p) y3 = fp2_sub(fp2_mul(lam, fp2_sub(x1,x3,p), p), y1, p) return (x3, y3)
def ec_mul(k, P, a, p): R = None while k: if k & 1: R = ec_add(R, P, a, p) P = ec_add(P, P, a, p) k >>= 1 return R
def velu_eval(Q, kernel, a, p): if Q is None: return None xQ, yQ = Q x_new, y_new = xQ, yQ for R in kernel: if R is None: continue xR, yR = R if xQ[0]%p == xR[0]%p and xQ[1]%p == xR[1]%p: return None QpR = ec_add(Q, R, a, p) if QpR is None: return None x_new = fp2_add(x_new, fp2_sub(QpR[0], xR, p), p) y_new = fp2_add(y_new, fp2_sub(QpR[1], yR, p), p) return (x_new, y_new)
a0 = (1, 0)G = ((13, 0), (290, 0))P3 = ec_mul(144, G, a0, p) # order 3P3_2 = ec_mul(2, P3, a0, p)kernel_3 = [None, P3, P3_2]
phi_G = velu_eval(G, kernel_3, a0, p)print(phi_G)# ==> ((412, 0), (94, 0))
print(velu_eval(P3, kernel_3, a0, p))# ==> NoneThe SIDH protocol
Section titled “The SIDH protocol”SIDH (De Feo et al., 2014) is a Diffie-Hellman-style key exchange on the supersingular isogeny graph. The setup:
- Fix a prime , often with a small cofactor (in this toy chapter , so ), and a supersingular curve over .
- Fix torsion bases generating and generating .
Alice picks a secret and computes:
- Kernel generator (order ).
- Isogeny .
- Torsion images and on .
Alice publishes .
Bob picks a secret and does the same with the 3-power torsion, publishing
To derive the shared secret, Alice uses Bob’s published data to compute . Bob computes . Both arrive at the same j-invariant. Writing and , Alice’s codomain is
and Bob’s is isomorphic to the same . The shared quotient is independent of the order in which the two kernels are quotiented out (De Feo et al., 2014), which is what makes the Diffie-Hellman analogy hold.
Walking the isogeny chain
Section titled “Walking the isogeny chain”A degree- isogeny is too expensive to compute in one step (Velu’s formulas cost ). Instead, decompose it into steps of degree . For Alice’s degree- isogeny:
- Start with kernel generator of order 16.
- Step 0: compute (order 2). Apply the degree-2 Velu isogeny with this kernel. Push and the torsion points through.
- Step 1: the pushed now has order 8. Compute (order 2). Apply degree-2 Velu. Push everything through.
- Steps 2 and 3: repeat.
After 4 steps, the accumulated isogeny has degree .
p = 431
def fp2_add(x, y, p): return ((x[0]+y[0]) % p, (x[1]+y[1]) % p)
def fp2_sub(x, y, p): return ((x[0]-y[0]) % p, (x[1]-y[1]) % p)
def fp2_mul(x, y, p): return ((x[0]*y[0]-x[1]*y[1]) % p, (x[0]*y[1]+x[1]*y[0]) % p)
def fp2_inv(x, p): norm = (x[0]*x[0]+x[1]*x[1]) % p inv_n = pow(norm, -1, p) return ((x[0]*inv_n) % p, ((-x[1])*inv_n) % p)
def fp2_neg(x, p): return ((-x[0]) % p, (-x[1]) % p)
def fp2_sqr(x, p): return ((x[0]*x[0]-x[1]*x[1]) % p, (2*x[0]*x[1]) % p)
def ec_add(P, Q, a, p): if P is None: return Q if Q is None: return P x1, y1 = P x2, y2 = Q if x1[0]%p == x2[0]%p and x1[1]%p == x2[1]%p: ny2 = fp2_neg(y2, p) if y1[0]%p == ny2[0]%p and y1[1]%p == ny2[1]%p: return None num = fp2_add(fp2_mul((3, 0), fp2_sqr(x1, p), p), a, p) den = fp2_mul((2, 0), y1, p) lam = fp2_mul(num, fp2_inv(den, p), p) else: num = fp2_sub(y2, y1, p) den = fp2_sub(x2, x1, p) lam = fp2_mul(num, fp2_inv(den, p), p) x3 = fp2_sub(fp2_sub(fp2_sqr(lam, p), x1, p), x2, p) y3 = fp2_sub(fp2_mul(lam, fp2_sub(x1, x3, p), p), y1, p) return (x3, y3)
def ec_mul(k, P, a, p): R = None while k: if k & 1: R = ec_add(R, P, a, p) P = ec_add(P, P, a, p) k >>= 1 return R
def velu_eval(Q, kernel, a, p): if Q is None: return None xQ, yQ = Q x_new, y_new = xQ, yQ for R in kernel: if R is None: continue xR, yR = R if (xQ[0]%p == xR[0]%p and xQ[1]%p == xR[1]%p): return None QpR = ec_add(Q, R, a, p) if QpR is None: return None x_new = fp2_add(x_new, fp2_sub(QpR[0], xR, p), p) y_new = fp2_add(y_new, fp2_sub(QpR[1], yR, p), p) return (x_new, y_new)
def compute_kernel(gen, order, a, p): pts = [None] cur = gen for _ in range(1, order): pts.append(cur) cur = ec_add(cur, gen, a, p) return pts
def recover_curve(img1, img2, p): """Recover a', b' from two points on y^2 = x^3 + a'x + b'.""" x1, y1 = img1 x2, y2 = img2 lhs1 = fp2_sub(fp2_sqr(y1, p), fp2_mul(fp2_sqr(x1, p), x1, p), p) lhs2 = fp2_sub(fp2_sqr(y2, p), fp2_mul(fp2_sqr(x2, p), x2, p), p) num = fp2_sub(lhs1, lhs2, p) den = fp2_sub(x1, x2, p) a_new = fp2_mul(num, fp2_inv(den, p), p) b_new = fp2_sub(lhs1, fp2_mul(a_new, x1, p), p) return a_new, b_new
def velu_step(kernel_gen, l, a, b, p, aux): kernel = compute_kernel(kernel_gen, l, a, p) pushed = [velu_eval(pt, kernel, a, p) for pt in aux] probes = [img for img in pushed if img is not None] # Need two points with distinct x-coords for curve recovery. # Remove duplicates by x-coordinate. unique = [] seen_x = set() for pr in probes: key = (pr[0][0] % p, pr[0][1] % p) if key not in seen_x: seen_x.add(key) unique.append(pr) probes = unique if len(probes) < 2: for x_int in range(2, p): x = (x_int, 0) x3 = fp2_mul(fp2_sqr(x, p), x, p) rhs = fp2_add(fp2_add(x3, fp2_mul(a, x, p), p), b, p) if rhs[1] != 0: continue if rhs[0] == 0: continue if pow(rhs[0], (p-1)//2, p) != 1: continue y_int = pow(rhs[0], (p+1)//4, p) probe = ((x_int, 0), (y_int, 0)) img = velu_eval(probe, kernel, a, p) if img is None: continue key = (img[0][0] % p, img[0][1] % p) if key in seen_x: continue seen_x.add(key) probes.append(img) if len(probes) >= 2: break a_new, b_new = recover_curve(probes[0], probes[1], p) return a_new, b_new, pushed
def walk_chain(kernel_gen, l, e, a, b, p, aux, probes=None): """Walk an l^e isogeny as e steps of degree l.
*probes* are extra points pushed through for curve recovery only; they are not returned. """ gen = kernel_gen a_cur, b_cur = a, b cur_aux = list(aux) if probes is None: probes = [] cur_probes = list(probes) for step in range(e): remaining = e - step step_gen = ec_mul(l**(remaining-1), gen, a_cur, p) push = [gen] + cur_aux + cur_probes a_new, b_new, pushed = velu_step( step_gen, l, a_cur, b_cur, p, push) gen = pushed[0] cur_aux = pushed[1:1+len(cur_aux)] cur_probes = pushed[1+len(cur_aux):] a_cur, b_cur = a_new, b_new return a_cur, b_cur, cur_aux
# --- SIDH at p = 431 = 2^4 * 3^3 - 1 ---a0 = (1, 0)b0 = (0, 0)
# Torsion bases (precomputed, verified in test suite).PA = ((372, 0), (48, 0)) # order 16QA = ((178, 168), (190, 428)) # order 16, independent of PAPB = ((123, 0), (396, 0)) # order 27QB = ((128, 133), (47, 6)) # order 27, independent of PB
# Alice: alpha = 3.alpha = 3RA = ec_add(PA, ec_mul(alpha, QA, a0, p), a0, p)a_A, b_A, aux_A = walk_chain(RA, 2, 4, a0, b0, p, [PB, QB])phiA_PB, phiA_QB = aux_A
# Bob: beta = 5.beta = 5RB = ec_add(PB, ec_mul(beta, QB, a0, p), a0, p)a_B, b_B, aux_B = walk_chain(RB, 3, 3, a0, b0, p, [PA, QA])phiB_PA, phiB_QA = aux_B
# Alice derives the shared secret.# Pass torsion images as probes for curve recovery.kernel_alice = ec_add( phiB_PA, ec_mul(alpha, phiB_QA, a_B, p), a_B, p)a_AB_a, b_AB_a, _ = walk_chain( kernel_alice, 2, 4, a_B, b_B, p, [], probes=[phiB_PA, phiB_QA])
def j_inv_fp2(a, b, p): a3 = fp2_mul(fp2_sqr(a, p), a, p) four_a3 = fp2_mul((4, 0), a3, p) b2 = fp2_sqr(b, p) denom = fp2_add(four_a3, fp2_mul((27, 0), b2, p), p) return fp2_mul(fp2_mul((1728, 0), four_a3, p), fp2_inv(denom, p), p)
j_alice = j_inv_fp2(a_AB_a, b_AB_a, p)
# Bob derives the shared secret.kernel_bob = ec_add( phiA_PB, ec_mul(beta, phiA_QB, a_A, p), a_A, p)a_AB_b, b_AB_b, _ = walk_chain( kernel_bob, 3, 3, a_A, b_A, p, [], probes=[phiA_PB, phiA_QB])j_bob = j_inv_fp2(a_AB_b, b_AB_b, p)
print(f"j_alice = {j_alice}")# ==> j_alice = (315, 132)print(f"j_bob = {j_bob}")# ==> j_bob = (315, 132)print(f"Match: {j_alice == j_bob}")# ==> Match: TrueAlice and Bob arrive at the same j-invariant . The shared secret is this j-invariant.
The public data each party sends is a curve (two elements ) and two torsion images (four elements). The secret is one integer ( or ). At real SIKE parameters (SIKEp434, NIST level 1), the public key was 330 bytes (Jao et al., 2022).
The 2022 break
Section titled “The 2022 break”What SIDH leaks
Section titled “What SIDH leaks”SIDH publishes more than just the destination curve . It also publishes and : the images of Bob’s torsion basis under Alice’s secret isogeny. This auxiliary data is necessary for the protocol (Bob needs it to compute the shared secret), but it encodes Alice’s secret redundantly.
Alice’s secret is the coefficient in . Once the attacker knows , they can reconstruct and compute the shared secret. The published torsion images and encode how acts on the -torsion. Because is determined by its kernel , and the kernel is determined by , the torsion images are functions of over the public -torsion group.
The Castryck-Decru attack
Section titled “The Castryck-Decru attack”In July 2022, Castryck and Decru showed that this auxiliary data enables a key recovery in heuristic polynomial time when the endomorphism ring of the starting curve is known, apart from the factorization of a few integers that depend only on the system parameters (Castryck & Decru, 2023). Robert then removed the starting-curve condition and the heuristic: his attack runs in polynomial time from the torsion images and the factored smooth degrees alone (Robert, 2022). The attack uses three ingredients:
Kani’s theorem. An abelian surface is the two-dimensional analogue of an elliptic curve (an elliptic curve is a one-dimensional abelian variety). Given two elliptic curves and isogenies between them, the Kani-Frey gluing construction stitches and into a product abelian surface and builds a -isogeny on it (Kani, 1997). The published torsion images are the gluing data this construction consumes.
Richelot isogenies. Isogenies between abelian surfaces (genus-2 curves or products of elliptic curves) can be computed explicitly. The Richelot isogeny is the genus-2 analog of Velu’s formula.
Decomposition. The product surface isogeny, constructed from the published torsion data, factors through a chain of Richelot isogenies. Each step of this chain reveals partial information about Alice’s kernel. After enough steps, the attacker recovers completely.
Under either analysis the attack runs in polynomial time in . It does not require quantum computation.
The torsion images amount to evaluations of Alice’s secret isogeny at two independent generators of Bob’s -torsion subgroup. Together they pin down the kernel more tightly than the bare isogeny problem would. The Kani-Frey lifting trick converts these constraints into a system that recovers in polynomial time.
What does not break
Section titled “What does not break”The Castryck-Decru attack targets SIDH’s protocol design. It does not apply to isogeny problems that omit torsion-image disclosure.
CSIDH (Castryck et al., 2018) uses supersingular curves defined over (not ) with a commutative class group action on the -rational endomorphism ring. The public key is a single curve. No torsion images are published. The attack does not apply.
SQIsign (Chapter 23) is a signature scheme, not a key exchange. The signature is an isogeny itself, not a pair of torsion images taken under a secret isogeny. Round-2 SQIsign does publish torsion-point images, but only of the public response isogeny , so the verifier can evaluate it (The SQIsign Team, 2025, sec. 1.3). It never publishes the secret isogeny’s action on a torsion basis, which is the data the Castryck-Decru attack needs, so the attack has no input here (Chapter 23 gives the full Round-2 protocol geometry). SQIsign’s security rests on the endomorphism ring problem for supersingular curves, in a variant that hands the adversary hints (The SQIsign Team, 2025, sec. 1.1).
The supersingular isogeny problem without auxiliary torsion data remains hard. The best known classical algorithm ran in for the general supersingular graph for three decades (Delfs & Galbraith, 2016); in July 2026 Wesolowski gave a heuristic algorithm in time and memory, still exponential in , which the SQIsign team’s round-3 specification prices for its own parameters (The SQIsign Team, 2026, sec. 8.2; Wesolowski, 2026). Curves with -invariant in admit a faster structure-exploiting walk, again exponential. The best known quantum algorithm gives only a square-root speedup to (Biasse et al., 2014), also exponential in . No polynomial-time (Shor-style) quantum algorithm is known as of September 2026.
Timeline
Section titled “Timeline”- 1997: Couveignes formulates the first cryptographic use of isogenies (Couveignes, 1997). The manuscript is not published until 2006.
- 2006: Rostovtsev and Stolbunov independently rediscover the group-action approach on ordinary curves (Rostovtsev & Stolbunov, 2006). Castryck et al. later reformulate it over supersingular curves as CSIDH (Castryck et al., 2018).
- 2011: De Feo, Jao, and Plut propose SIDH, the first efficient isogeny-based key exchange (published in journal form in 2014) (De Feo et al., 2014).
- 2017: SIKE (Supersingular Isogeny Key Encapsulation), based on SIDH, is submitted to NIST’s PQC standardization process.
- July to August 2022: Castryck and Decru publish a key recovery attack on SIDH, heuristic polynomial time when the starting curve’s endomorphism ring is known (Castryck & Decru, 2023). Maino and Martindale independently find a subexponential attack with no condition on the starting curve (Maino & Martindale, 2022). Robert gives a polynomial-time attack for any starting curve, proved without heuristics (Robert, 2022).
- September 2022: The SIKE team submits a fourth-round proposal dated 15 September 2022 whose only change is a postscript stating that SIKE and SIDH are insecure and should not be used (Jao et al., 2022). NIST does not formally withdraw the submission, and the team’s stated reason for submitting rather than withdrawing is that the closing record should reflect the break where an unwitting user would see it. SIKE is no longer a viable standardization candidate.
- March 2025: NIST IR 8545 closes the fourth round: HQC is selected for standardization as a code-based backup KEM; SIKE, the only isogeny-based fourth-round candidate, is not selected (National Institute of Standards and Technology, 2025).
Eleven years separated SIDH’s publication from the Castryck-Decru attack. SIKE had survived five years of NIST evaluation before the attack landed.
Tradeoffs: isogenies, lattices, and codes
Section titled “Tradeoffs: isogenies, lattices, and codes”| Property | SIDH/SIKE | SQIsign (level 1) | ML-KEM-512 | HQC-1 |
|---|---|---|---|---|
| Type | KEM | Signature | KEM | KEM |
| NIST status | Broken (2022) | Additional signatures Round 3 candidate | FIPS 203 | Selected (2025) |
| Public key | 330 B | 83 B | 800 B | 2,241 B |
| Ciphertext/sig | 346 B | 200 B | 768 B | 4,433 B |
| pk + ct/sig | 676 B | 283 B | 1,568 B | 6,674 B |
| Assumption | SIDH torsion-aided isogeny (broken) | EndRing (hint variant) | Module-LWE | QCSD |
| First proposed | 2011 | 2020 | 2017 (as Kyber) | 2017 |
| Quantum status | Classically broken (2022) | No known poly-time quantum attack | No known poly-time quantum attack | No known poly-time quantum attack |
Sources: SIDH/SIKE (Jao et al., 2022), SQIsign (Table 1 in The SQIsign Team, 2026), ML-KEM (National Institute of Standards and Technology, 2024), HQC (Gaborit et al., 2025). The ML-KEM column’s first-proposed date is CRYSTALS-Kyber’s, from the original paper (Bos et al., 2018). FIPS 203 §1.1 records only that ML-KEM is derived from the round-three Kyber submission, and gives no date.
Isogeny-based designs can produce very small public keys and signatures. Among current NIST additional-signature candidates, SQIsign has the smallest combined public-key-plus-signature size: 283 bytes at level 1, smaller than any lattice or code-based signature alternative. The tradeoff is assumption maturity: isogeny-based cryptography has the shortest cryptographic track record among the major PQC families, and SIDH’s break shows that protocol-specific auxiliary data can invalidate an otherwise plausible hardness assumption. CSIDH remains standing but has not been submitted to NIST. SQIsign is a Round 3 candidate in NIST’s additional-signatures evaluation track (National Institute of Standards and Technology, 2026). The sizes and timings here are from the round-3 specification, version 3.0 of 1 September 2026 (The SQIsign Team, 2026); the round-2 specification of July 2025 had a 65-byte key and a 148-byte signature, and the increase prices the algorithm above (The SQIsign Team, 2025). SQIsign’s security rests on the endomorphism ring problem, in the hint variant its EUF-CMA proof reduces to. Chapter 23 develops the details.
Signing latency is a second tradeoff: the round-3 SQIsign specification reports about 28 ms per signature and about 3.6 ms per verification for the optimized 64-bit Intel implementation on an Intel Core i7-13700K at its 3.4 GHz nominal clock (Table 2 in The SQIsign Team, 2026). Per-transaction signing on a high-throughput Layer-1 blockchain operates in the microsecond class, so 28 ms makes SQIsign unattractive for high-throughput per-transaction signing without categorically excluding it: a wallet’s signing demand, its parallelism and the verifier’s workload are different constraints, and Chapter 23 draws the same line. Chapter 23 covers the governance-key context where the latency is acceptable.
Chapter 23 builds SQIsign from scratch, using this chapter’s isogeny graph together with quaternion algebra. It picks up both threads the cryptanalysis section left open. Deuring’s correspondence, previewed above as a bijection between conjugacy classes of maximal orders and Galois-conjugate pairs of supersingular -invariants, becomes the machinery that lets a signer connect two curves while revealing neither the secret one nor its endomorphism ring. And the distinction that lets SQIsign survive Castryck-Decru, publishing point images of the public response isogeny but never of the secret isogeny, becomes the commitment-challenge-response square the verifier checks.
Exercises
Section titled “Exercises”Exercise 1. Find all points of order 2 on over . (Hint: 2-torsion points have , so solve .) How many 2-torsion points does have over ?
Exercise 2. Starting from , walk the 3-isogeny graph for 4 steps. At each step, choose a 3-torsion point on the current curve, compute the degree-3 Velu isogeny, and record the j-invariant of the target. Does the walk revisit any j-invariant within 4 steps?
Exercise 3. Compute the number of supersingular j-invariants over using the formula . The correction depends on : it is 0 for , 1 for or , and 2 for . Verify by enumerating all j-invariants reachable from via repeated 3-isogenies.
Exercise 4. Explain in two sentences why the Castryck-Decru attack does not apply to CSIDH.
Exercise 5. (Harder) Verify that is prime. The curve over has points. How many steps would Alice’s isogeny chain have at this prime? How many for Bob?
Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 22. A separate track, for rebuilding rather than reading: the package exercises/ch22-isogenies has every function the chapter teaches replaced by a stub. Run PQC_IMPL=exercises pytest tests/ch22 to grade your version against the suite that proves the reference one.
References
Section titled “References”Last updated: