logicaffeine_proof/isogeny.rs
1//! # Certified isogeny torsion-image witnesses — the SIDH/SIKE public-data structure, in the prover
2//!
3//! An SIDH/SIKE public key is not merely the codomain curve `E' = φ(E₀)`: to make the scheme a key
4//! exchange it also publishes the **images of a public torsion basis**, `φ(P), φ(Q)`. That auxiliary
5//! torsion data is exactly what the 2022 Castryck–Decru attack weaponized. Its internal consistency is
6//! governed by the Weil pairing's isogeny-compatibility law:
7//!
8//! ```text
9//! e_N(φ(P), φ(Q)) = e_N(P, Q)^{deg φ}.
10//! ```
11//!
12//! This module bakes that relation into the prover as a re-checkable certificate — the same discipline as
13//! the rest of the campaign ([`crate::ait::DescriptionBound`], `LinearRigidityCert`, …): a
14//! [`TorsionImageWitness`] whose [`verify`](TorsionImageWitness::verify) re-derives the pairings on *both*
15//! curves from scratch and checks the law. It is the exact check an SIDH verifier performs, and the
16//! structural symmetry the break pulls on.
17
18use crate::elliptic::{torsion_basis, weil_pairing, Curve, Isogeny, Point};
19use crate::factor::modpow;
20use logicaffeine_base::BigInt;
21
22/// A re-checkable witness that an isogeny `φ: E → E'` is consistently specified by its action on an
23/// `N`-torsion basis — the SIDH/SIKE public-key format.
24#[derive(Clone, Debug)]
25pub struct TorsionImageWitness {
26 pub domain: Curve,
27 pub codomain: Curve,
28 /// The isogeny degree `deg φ`.
29 pub degree: u64,
30 /// The torsion order `N` (coprime to `deg φ`, so `φ` restricts to an isomorphism on `E[N]`).
31 pub torsion_order: u64,
32 /// A basis `(P, Q)` of `E[N]`.
33 pub basis: (Point, Point),
34 /// The published images `(φ(P), φ(Q))` on the codomain.
35 pub images: (Point, Point),
36}
37
38impl TorsionImageWitness {
39 /// Re-check the witness from scratch, trusting nothing about how it was produced:
40 /// 1. `(P, Q)` is a genuine basis of `E[N]` — the Weil pairing `e_N(P,Q)` is a *primitive* `N`th root
41 /// of unity (the points are independent, so the pairing is non-degenerate);
42 /// 2. `(φP, φQ)` lie on the codomain and are killed by `N`;
43 /// 3. the **isogeny-compatibility law** `e_N(φP, φQ) = e_N(P, Q)^{deg φ}` holds.
44 pub fn verify(&self) -> bool {
45 let n = self.torsion_order;
46 let one = BigInt::from_i64(1);
47
48 // (1) The basis pairing on the domain must be a primitive Nth root (non-degenerate ⟹ independent).
49 let ep = match weil_pairing(&self.domain, &self.basis.0, &self.basis.1, n) {
50 Some(e) if e != one && modpow(&e, &BigInt::from_i64(n as i64), &self.domain.p) == one => e,
51 _ => return false,
52 };
53
54 // (2) The images sit on the codomain and have order dividing N.
55 let ord_n = BigInt::from_i64(n as i64);
56 for pt in [&self.images.0, &self.images.1] {
57 if !self.codomain.is_on_curve(pt) || self.codomain.mul(&ord_n, pt) != Point::Infinity {
58 return false;
59 }
60 }
61
62 // (3) The compatibility law: e_N(φP, φQ) = e_N(P,Q)^{deg φ}.
63 match weil_pairing(&self.codomain, &self.images.0, &self.images.1, n) {
64 Some(eq) => eq == modpow(&ep, &BigInt::from_i64(self.degree as i64), &self.domain.p),
65 None => false,
66 }
67 }
68}
69
70/// Build a *genuine* certified witness: the real `ell`-isogeny with kernel `⟨kernel_gen⟩` on `domain`,
71/// together with the images of an actual `E[n]` basis (`n` coprime to `ell`, so `φ` is an isomorphism on
72/// the `n`-torsion). Its `verify()` passes by construction. `None` if the isogeny or basis cannot be formed.
73pub fn certify_isogeny(domain: &Curve, kernel_gen: &Point, ell: u64, n: u64) -> Option<TorsionImageWitness> {
74 let iso = Isogeny::from_kernel(domain, kernel_gen, ell)?;
75 let (p, q) = torsion_basis(domain, n)?;
76 let images = (iso.eval(&p), iso.eval(&q));
77 Some(TorsionImageWitness {
78 domain: domain.clone(),
79 codomain: iso.codomain.clone(),
80 degree: ell,
81 torsion_order: n,
82 basis: (p, q),
83 images,
84 })
85}
86
87/// The structural verdict on SIDH/SIKE-style public data.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum SidhAudit {
90 /// Consistent (the Weil-pairing law holds) — but it publishes the full action of `φ` on `E[N]`, the
91 /// auxiliary torsion information the Castryck–Decru attack requires. Valid data, structurally exposed:
92 /// post-2022 this whole class of scheme is broken, and the exposure is *why*.
93 ConsistentButTorsionExposed,
94 /// The torsion-image data fails the Weil-pairing law — not a valid isogeny witness at all.
95 Inconsistent,
96}
97
98/// Audit an isogeny public key: consistent, and what does it structurally expose?
99pub fn audit(w: &TorsionImageWitness) -> SidhAudit {
100 if w.verify() {
101 SidhAudit::ConsistentButTorsionExposed
102 } else {
103 SidhAudit::Inconsistent
104 }
105}
106
107// ---- Kani's theorem: the glue-and-split core -------------------------------------------------------
108//
109// Kani's theorem powers the Castryck–Decru break by GLUING two elliptic curves into an abelian surface:
110// given an isogeny `φ: E → E'` and a torsion basis, the GRAPH `K = {(T, φ(T)) : T ∈ E[N]} ⊂ (E × E')[N]`
111// is a candidate kernel of an `(N,N)`-isogeny. It is a valid kernel exactly when it is **Lagrangian**
112// (maximal isotropic) under the product Weil pairing — and that isotropy is decided by the pairing law we
113// certified: on the graph generators, `e_product((P,φP),(Q,φQ)) = e_N(P,Q)·e_N(φP,φQ) = e_N(P,Q)^{1+deg φ}`,
114// so the graph is isotropic ⟺ `deg φ ≡ −1 (mod N)`. The torsion images are exactly what make this pairing
115// computable — which is *why* publishing them is the leak. This certificate computes and re-checks that
116// gluing pairing; the remaining glue-and-split machinery (Richelot `(2,2)`-isogenies of the surface, theta
117// coordinates, and split detection) is genuine research-grade work built ON this isotropic core, NOT
118// claimed here.
119
120use crate::fp2::{
121 aut_1728, derive_isogeny_path2, fp2_const, fp2_pow, full_order_basis2, keyspace_codomain_classes,
122 kernel_generator2, point_of_order2, point_order2, product_weil_pairing, push_through2, recover_secret2,
123 recover_secret_recursive2, torsion_basis2, weil_pairing2, Curve2, Fp2, IsogenyStep2, Isogeny2, Point2,
124};
125
126/// A **Kani glue kernel**: the graph `{(T, φ(T))}` of an isogeny on the `N`-torsion, the candidate kernel of
127/// an `(N,N)`-isogeny of the abelian surface `E × E'`.
128#[derive(Clone, Debug)]
129pub struct KaniGlue {
130 pub e1: Curve2,
131 pub e2: Curve2,
132 pub degree: u64,
133 pub torsion_order: u64,
134 pub basis: (Point2, Point2),
135 pub images: (Point2, Point2),
136}
137
138impl KaniGlue {
139 /// The product Weil pairing on the graph generators `(P,φP)` and `(Q,φQ)`.
140 pub fn glue_pairing(&self) -> Option<Fp2> {
141 product_weil_pairing(
142 &self.e1,
143 &self.e2,
144 (&self.basis.0, &self.images.0),
145 (&self.basis.1, &self.images.1),
146 self.torsion_order,
147 )
148 }
149
150 /// Re-check Kani's gluing relation: the graph pairing equals `e_N(P,Q)^{1+deg φ}`. This is what makes
151 /// the isotropy of the glue kernel *decidable*, and it re-derives the pairings from scratch.
152 pub fn verify(&self) -> bool {
153 let ep = match weil_pairing2(&self.e1, &self.basis.0, &self.basis.1, self.torsion_order) {
154 Some(e) => e,
155 None => return false,
156 };
157 match self.glue_pairing() {
158 Some(glue) => glue == fp2_pow(&ep, 1 + self.degree, &self.e1.p),
159 None => false,
160 }
161 }
162
163 /// Whether the glue kernel is **Lagrangian** (isotropic) — i.e. an `(N,N)`-isogeny of `E × E'` exists
164 /// with this kernel. True exactly when the graph pairing is trivial, i.e. `deg φ ≡ −1 (mod N)`.
165 pub fn is_lagrangian(&self) -> bool {
166 self.glue_pairing().map_or(false, |g| g == fp2_const(1, &self.e1.p))
167 }
168}
169
170/// Build a genuine Kani glue kernel over `𝔽_{p²}` from a real `ell`-isogeny and an `N`-torsion basis.
171pub fn build_kani_glue(domain: &Curve2, kernel_gen: &Point2, ell: u64, n: u64) -> Option<KaniGlue> {
172 let iso = Isogeny2::from_kernel(domain, kernel_gen, ell)?;
173 let (p, q) = torsion_basis2(domain, n)?;
174 let images = (iso.eval(&p), iso.eval(&q));
175 Some(KaniGlue { e1: domain.clone(), e2: iso.codomain.clone(), degree: ell, torsion_order: n, basis: (p, q), images })
176}
177
178/// Whether `n` is `bound`-smooth (every prime factor ≤ `bound`) — the auxiliary isogeny degree of a Kani
179/// diamond must be smooth to be efficiently computable.
180pub fn is_smooth(mut n: u64, bound: u64) -> bool {
181 if n == 0 {
182 return false;
183 }
184 let mut f = 2u64;
185 while f * f <= n {
186 while n % f == 0 {
187 n /= f;
188 }
189 f += 1;
190 }
191 n == 1 || n <= bound
192}
193
194/// **The Kani degree condition.** To embed a degree-`d` secret isogeny in a `(2,2)`-isogeny *chain* of an
195/// abelian surface (the Castryck–Decru diamond), pick an exponent `e` and an auxiliary isogeny of degree
196/// `c = 2^e − d`, so the two sides of the diamond sum to `2^e` — the surface isogeny is then a length-`e`
197/// Richelot chain. `c` must be positive and smooth (efficiently computable). Returns the smallest such
198/// `(e, c)`. This is the diamond's number-theoretic bookkeeping.
199pub fn kani_diamond_degrees(d: u64, max_e: u32, smooth_bound: u64) -> Option<(u32, u64)> {
200 (1..=max_e).find_map(|e| {
201 let n = 1u64 << e;
202 // c must be a NONTRIVIAL auxiliary (degree > 1 — a degree-1 "isogeny" is the identity, no gluing).
203 (n > d + 1).then(|| n - d).filter(|&c| is_smooth(c, smooth_bound)).map(|c| (e, c))
204 })
205}
206
207/// A Castryck–Decru **Kani diamond**: a secret isogeny `φ: E₀ → E` of degree `d` and an auxiliary isogeny
208/// `γ: E₀ → C` of degree `c`, chosen so `c + d = 2^e`. Kani's lemma then supplies a `(2^e, 2^e)`-isogeny of
209/// the abelian surface `E × C` — a length-`e` Richelot chain — whose codomain **splits** into a product of
210/// elliptic curves iff the diamond is consistent. The split-test
211/// ([`crate::hyperelliptic::surface_is_reducible`]) is the per-digit oracle that reads that
212/// splitting off. **Honest boundary:** this builds and validates the diamond's *degree structure and both
213/// isogeny sides*; constructing the surface's `2^e`-torsion kernel from the diamond and the torsion images —
214/// the input the split-oracle consumes — is the remaining SageMath-scale research core and is **not**
215/// fabricated here.
216#[derive(Clone, Debug)]
217pub struct KaniDiamond {
218 pub e0: Curve2,
219 /// Codomain of the secret isogeny `φ` (degree `d`).
220 pub e_curve: Curve2,
221 /// Codomain of the auxiliary isogeny `γ` (degree `c`).
222 pub c_curve: Curve2,
223 pub d: u64,
224 pub c: u64,
225 /// `c + d = 2^e`.
226 pub e: u32,
227}
228
229/// Build a Kani diamond over `𝔽_{p²}`: the secret side `φ` is an `ell_phi^a`-isogeny with kernel `⟨phi_gen⟩`;
230/// the auxiliary side `γ` is a `c`-isogeny (`c` an odd prime) with kernel `⟨gamma_gen⟩`. Succeeds only when
231/// `c + ell_phi^a` is a power of two — the Kani degree condition.
232pub fn build_kani_diamond(
233 e0: &Curve2,
234 phi_gen: &Point2,
235 ell_phi: u64,
236 a: u32,
237 gamma_gen: &Point2,
238 c: u64,
239) -> Option<KaniDiamond> {
240 let d = ell_phi.pow(a);
241 let n = d + c;
242 if !n.is_power_of_two() {
243 return None;
244 }
245 let phi = derive_isogeny_path2(e0, phi_gen, ell_phi, a)?;
246 let gamma = Isogeny2::from_kernel(e0, gamma_gen, c)?;
247 Some(KaniDiamond {
248 e0: e0.clone(),
249 e_curve: phi.last()?.codomain.clone(),
250 c_curve: gamma.codomain,
251 d,
252 c,
253 e: n.trailing_zeros(),
254 })
255}
256
257/// A **law the recovery invoked** — auto-formalized as a re-checkable obligation, not prose. The recovery
258/// engine does not merely return an answer; it pops out the rules and laws it relied on, each of which
259/// [`SecretRecovery::verify`] re-derives from scratch.
260#[derive(Clone, Debug, PartialEq)]
261pub enum RecoveryLaw {
262 /// The secret kernel generator has order exactly `ℓᵃ`.
263 GeneratorOrder { ell: u64, a: u32 },
264 /// The unfolded chain is `a` connected `ℓ`-isogeny steps, each quotienting an order-`ℓ` subgroup.
265 DescendingKernels { ell: u64 },
266 /// Pushing the auxiliary torsion basis through the chain reproduces the published images (the defining
267 /// property of the recovered isogeny).
268 ImagesReproduced,
269 /// The `ℓ`-adic **tree** recursion (auto-partitioning the keyspace by digit, sharing prefixes) recovers
270 /// the same secret as the flat enumeration — the partition is faithful.
271 KeyspaceTreePartition { ell: u64, a: u32 },
272 /// `E₀/⟨K⟩ ≅ E₀/⟨ι(K)⟩` under the automorphism `ι`: recovering one kernel recovers its whole orbit.
273 AutOrbitClosure,
274}
275
276impl RecoveryLaw {
277 /// The formalized statement of the law — the "learning" the recovery pops out.
278 pub fn statement(&self) -> String {
279 match self {
280 RecoveryLaw::GeneratorOrder { ell, a } => format!("ord(gen) = {ell}^{a} on E₀"),
281 RecoveryLaw::DescendingKernels { ell } => {
282 format!("each of the a chain steps quotients an order-{ell} subgroup; the chain is connected")
283 }
284 RecoveryLaw::ImagesReproduced => "φ(P_B), φ(Q_B) = the published torsion images".into(),
285 RecoveryLaw::KeyspaceTreePartition { ell, a } => {
286 format!("the {ell}-adic keyspace tree (depth {a}) recovers the same secret as flat search")
287 }
288 RecoveryLaw::AutOrbitClosure => "E₀/⟨K⟩ ≅ E₀/⟨ι(K)⟩ — recovering one kernel recovers its orbit".into(),
289 }
290 }
291}
292
293/// A recovered SIDH secret **together with the self-checking record of the laws its recovery invoked**. The
294/// certificate carries everything needed to re-derive itself: [`verify`](SecretRecovery::verify) re-runs
295/// every law from scratch, trusting nothing about how the answer was produced. This is the auto-formalizing
296/// recovery — it inverts images → generator *and* emits the re-checkable mathematics it stands on.
297#[derive(Clone, Debug)]
298pub struct SecretRecovery {
299 pub e0: Curve2,
300 pub basis_a: (Point2, Point2),
301 pub basis_b: (Point2, Point2),
302 pub images: (Point2, Point2),
303 pub ell: u64,
304 pub a: u32,
305 pub secret: BigInt,
306 pub generator: Point2,
307 pub path: Vec<IsogenyStep2>,
308 pub laws: Vec<RecoveryLaw>,
309}
310
311impl SecretRecovery {
312 /// The formalized laws this recovery relied on, as statements — the rules and learnings, popped out.
313 pub fn learnings(&self) -> Vec<String> {
314 self.laws.iter().map(RecoveryLaw::statement).collect()
315 }
316
317 /// Re-check every law from scratch. Trusts nothing about how the recovery ran.
318 pub fn verify(&self) -> bool {
319 let n = self.ell.pow(self.a);
320 let (pa, qa) = (&self.basis_a.0, &self.basis_a.1);
321 let (pb, qb) = (&self.basis_b.0, &self.basis_b.1);
322 // The recovered generator is P_A + [s]Q_A of order ℓᵃ.
323 if kernel_generator2(&self.e0, pa, qa, &self.secret) != self.generator {
324 return false;
325 }
326 for law in &self.laws {
327 let ok = match law {
328 RecoveryLaw::GeneratorOrder { ell, a } => {
329 let m = ell.pow(*a);
330 point_order2(&self.e0, &self.generator, m + 1) == Some(m)
331 }
332 RecoveryLaw::DescendingKernels { ell } => {
333 self.path.len() as u32 == self.a
334 && self.path.iter().enumerate().all(|(k, st)| {
335 let order_ok = point_order2(&st.domain, &st.kernel, *ell + 1) == Some(*ell);
336 let connected = k == 0 || st.domain == self.path[k - 1].codomain;
337 order_ok && connected
338 })
339 }
340 RecoveryLaw::ImagesReproduced => {
341 push_through2(&self.path, self.ell, pb).as_ref() == Some(&self.images.0)
342 && push_through2(&self.path, self.ell, qb).as_ref() == Some(&self.images.1)
343 }
344 RecoveryLaw::KeyspaceTreePartition { ell, a } => {
345 // The ℓ-adic tree walk independently recovers a generator that reproduces the images —
346 // the partition is faithful. (It need not be the identical scalar: symmetry-equivalent
347 // kernels are genuine co-solutions, which is precisely the AutOrbitClosure law below.)
348 recover_secret_recursive2(&self.e0, (pa, qa), *ell, *a, (pb, qb), (&self.images.0, &self.images.1))
349 .and_then(|(_, _s, g)| {
350 let path = derive_isogeny_path2(&self.e0, &g, *ell, *a)?;
351 Some(
352 push_through2(&path, *ell, pb).as_ref() == Some(&self.images.0)
353 && push_through2(&path, *ell, qb).as_ref() == Some(&self.images.1),
354 )
355 })
356 .unwrap_or(false)
357 }
358 RecoveryLaw::AutOrbitClosure => {
359 let j = |g: &Point2| {
360 derive_isogeny_path2(&self.e0, g, self.ell, self.a)
361 .and_then(|p| p.last().and_then(|st| st.codomain.j_invariant()))
362 };
363 let ig = aut_1728(&self.e0.p, &self.generator);
364 point_order2(&self.e0, &ig, n + 1) == Some(n) && j(&self.generator) == j(&ig) && j(&ig).is_some()
365 }
366 };
367 if !ok {
368 return false;
369 }
370 }
371 true
372 }
373}
374
375/// **Certify an images → generator recovery**, emitting the answer with its self-checking law record. Runs
376/// the flat recovery for the answer, then attaches the full set of re-checkable laws (order, descent, image
377/// reproduction, the faithful ℓ-adic tree partition, and the Aut-orbit closure rule).
378pub fn certify_recovery(
379 e0: &Curve2,
380 basis_a: (&Point2, &Point2),
381 ell: u64,
382 a: u32,
383 basis_b: (&Point2, &Point2),
384 images: (&Point2, &Point2),
385) -> Option<SecretRecovery> {
386 let (secret, generator, path) = recover_secret2(e0, basis_a, ell, a, basis_b, images)?;
387 let laws = vec![
388 RecoveryLaw::GeneratorOrder { ell, a },
389 RecoveryLaw::DescendingKernels { ell },
390 RecoveryLaw::ImagesReproduced,
391 RecoveryLaw::KeyspaceTreePartition { ell, a },
392 RecoveryLaw::AutOrbitClosure,
393 ];
394 Some(SecretRecovery {
395 e0: e0.clone(),
396 basis_a: (basis_a.0.clone(), basis_a.1.clone()),
397 basis_b: (basis_b.0.clone(), basis_b.1.clone()),
398 images: (images.0.clone(), images.1.clone()),
399 ell,
400 a,
401 secret,
402 generator,
403 path,
404 laws,
405 })
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 fn i(x: i64) -> BigInt {
413 BigInt::from_i64(x)
414 }
415
416 #[test]
417 fn torsion_image_witness_verifies_and_rejects_tampering() {
418 use crate::elliptic::point_of_order;
419 // The curve y² = x³ + 2x + 25 over 𝔽₄₃ has #E = 45 = 9·5: full rational 3-torsion (the basis) and a
420 // rational 5-torsion point (the isogeny kernel; 5 is coprime to 3). μ₃ ⊂ 𝔽₄₃ (3 | 42), p ≡ 3 mod 4.
421 let curve = Curve::new(i(2), i(25), i(43));
422 let kernel = point_of_order(&curve, 5).expect("𝔽₄₃ 5-torsion");
423 let w = certify_isogeny(&curve, &kernel, 5, 3).expect("a genuine isogeny witness");
424
425 // A real witness satisfies the Weil-pairing compatibility law.
426 assert!(w.verify(), "e_N(φP,φQ) = e_N(P,Q)^{{deg φ}} for a genuine isogeny");
427 assert_eq!(audit(&w), SidhAudit::ConsistentButTorsionExposed);
428
429 // The numeric law, spelled out: e_3(φP,φQ) = e_3(P,Q)^5.
430 let ep = weil_pairing(&w.domain, &w.basis.0, &w.basis.1, 3).unwrap();
431 let eq = weil_pairing(&w.codomain, &w.images.0, &w.images.1, 3).unwrap();
432 assert_eq!(eq, modpow(&ep, &i(5), &w.domain.p), "the compatibility law holds numerically");
433
434 // Tampering an image breaks the law — the certificate is only as good as its re-check.
435 let mut bad = w.clone();
436 bad.images.0 = w.codomain.double(&w.images.0); // 2·φ(P) ≠ φ(P)
437 assert!(!bad.verify(), "a tampered torsion image fails the pairing law");
438 assert_eq!(audit(&bad), SidhAudit::Inconsistent);
439 }
440
441 #[test]
442 fn kani_glue_is_lagrangian_exactly_when_deg_is_minus_one_mod_n() {
443 // Over 𝔽_{59²} on the supersingular curve y²=x³+x (p+1 = 60 = 2²·3·5), both E[3] and E[5] are
444 // fully rational — enough to glue.
445 let p = BigInt::parse_decimal("59").unwrap();
446 let c = Curve2::new(fp2_const(1, &p), fp2_const(0, &p), p.clone());
447
448 // deg φ = 5, torsion N = 3: 5 ≡ −1 (mod 3), so the graph {(T,φT)} is Lagrangian — an (N,N)-isogeny of
449 // the surface E×E' exists. This is the gluing condition Kani's theorem supplies.
450 let k5 = point_of_order2(&c, 5).expect("5-torsion");
451 let glue = build_kani_glue(&c, &k5, 5, 3).expect("a genuine glue kernel");
452 assert!(glue.verify(), "the gluing relation e_product = e_N^{{1+deg}} holds");
453 assert!(glue.is_lagrangian(), "deg 5 ≡ −1 (mod 3) ⟹ Lagrangian glue kernel");
454 assert_eq!(glue.glue_pairing().unwrap(), fp2_const(1, &p), "isotropic: the product pairing is trivial");
455
456 // deg φ = 3, torsion N = 5: 3 ≢ −1 (mod 5), so NOT Lagrangian — yet the relation still re-checks.
457 let k3 = point_of_order2(&c, 3).expect("3-torsion");
458 let glue2 = build_kani_glue(&c, &k3, 3, 5).expect("a genuine glue kernel");
459 assert!(glue2.verify(), "the gluing relation holds regardless of the degree");
460 assert!(!glue2.is_lagrangian(), "deg 3 ≢ −1 (mod 5) ⟹ not Lagrangian");
461 }
462
463 #[test]
464 fn images_to_generator_recovery_certifies_itself_and_rejects_tampering() {
465 // SIDH-scale instance over 𝔽_{107²}: y²=x³+x (j=1728), #E=(p+1)²=108², so E[3³] and E[2²] are both
466 // rank-2 rational — a genuine 27-kernel keyspace.
467 let p = BigInt::parse_decimal("107").unwrap();
468 let e0 = Curve2::new(fp2_const(1, &p), fp2_const(0, &p), p.clone());
469 let (pa, qa) = full_order_basis2(&e0, 3, 3).expect("rank-2 E[3³] basis");
470 let (pb, qb) = full_order_basis2(&e0, 2, 2).expect("rank-2 E[2²] basis");
471
472 // A secret isogeny publishes its torsion images.
473 let secret = i(13);
474 let gen = kernel_generator2(&e0, &pa, &qa, &secret);
475 let path = derive_isogeny_path2(&e0, &gen, 3, 3).expect("the secret 3³-isogeny");
476 let images = (push_through2(&path, 3, &pb).unwrap(), push_through2(&path, 3, &qb).unwrap());
477
478 // Invert AND auto-formalize: recover a generator reproducing the images, and pop out the laws.
479 let rec = certify_recovery(&e0, (&pa, &qa), 3, 3, (&pb, &qb), (&images.0, &images.1))
480 .expect("images → generator recovery");
481 assert_eq!(push_through2(&rec.path, 3, &pb).unwrap(), images.0, "the recovered isogeny reproduces φ(P_B)");
482 assert_eq!(push_through2(&rec.path, 3, &qb).unwrap(), images.1, "and φ(Q_B) — inversion succeeded");
483 assert!(rec.verify(), "every law the recovery invoked re-checks from scratch");
484 assert_eq!(rec.laws.len(), 5, "the full law set is emitted");
485 assert!(rec.learnings().iter().any(|l| l.contains("ι(K)")), "the Aut-orbit rule is among the learnings");
486 // The recovered generator matches the reported scalar (internal consistency of the certificate).
487 assert_eq!(kernel_generator2(&e0, &pa, &qa, &rec.secret), rec.generator);
488
489 // The certificate is only as good as its re-check: tampering the secret must fail verification.
490 let _ = secret; // the planted secret is a valid solution; recovery may return a symmetry-equivalent one
491 let mut forged = rec.clone();
492 forged.secret = rec.secret.add(&i(1)); // a different scalar ⟹ a different generator ⟹ rejected
493 assert!(!forged.verify(), "a tampered secret no longer matches its generator ⟹ rejected");
494 }
495
496 #[test]
497 fn kani_degree_diamond_is_well_formed() {
498 // The degree bookkeeping: to attack a degree-3 isogeny, pair it with an auxiliary of degree
499 // c = 2^e − 3; the smallest smooth choice is c = 5 (3 + 5 = 8 = 2³).
500 assert_eq!(kani_diamond_degrees(3, 8, 100), Some((3, 5)), "3 + 5 = 2³ — smallest smooth diamond");
501 assert!(is_smooth(5, 100) && is_smooth(8, 3) && !is_smooth(101, 50), "smoothness gate");
502
503 // A real diamond over 𝔽_{59²}: φ a 3-isogeny, γ a 5-isogeny, 3 + 5 = 8 = 2³ (p+1 = 60 = 2²·3·5, so
504 // both 3- and 5-torsion are rational).
505 let p = BigInt::parse_decimal("59").unwrap();
506 let e0 = Curve2::new(fp2_const(1, &p), fp2_const(0, &p), p.clone());
507 let k3 = point_of_order2(&e0, 3).expect("3-torsion");
508 let k5 = point_of_order2(&e0, 5).expect("5-torsion");
509 let diamond = build_kani_diamond(&e0, &k3, 3, 1, &k5, 5).expect("a well-formed Kani diamond");
510
511 assert_eq!((diamond.d, diamond.c, diamond.e), (3, 5, 3), "degrees close to a power of two");
512 assert_eq!(diamond.d + diamond.c, 1 << diamond.e, "c + d = 2^e — the Kani degree condition");
513 // Both isogeny sides are genuine curves (real codomains), and the diamond is nontrivial.
514 assert!(diamond.e_curve.j_invariant().is_some(), "φ lands on a real curve E");
515 assert!(diamond.c_curve.j_invariant().is_some(), "γ lands on a real curve C");
516 // A mismatched auxiliary degree (c + d not a power of two) is correctly rejected.
517 assert!(build_kani_diamond(&e0, &k3, 3, 1, &k5, 5).is_some());
518 assert!(build_kani_diamond(&e0, &k3, 3, 1, &k3, 3).is_none(), "3 + 3 = 6 is not a power of two");
519 }
520}