Aeneas-compat: make vartime_double_base::mul extraction-clean

Pure refactors (cargo check green under both feature sets), semantics of
mul unchanged:
- dsm_top_index / dsm_loop / dsm_step_p / dsm_step_b helpers: the main
  double-and-add loop becomes a strictly-decreasing while with a
  single-assignment body and parameter-rooted borrows (the original
  loop/break shape with match-updates fails Aeneas' loop fixed point);
- the starting-index scan always returns 255: leading zero NAF digits
  double the identity (a no-op), so the result is unchanged - only the
  variable-time skip is dropped (constant-time behavior improves);
- the downward break-scan (which failed Aeneas' symbolic join) is gone.

With these, Charon+Aeneas extract the complete path - non_adjacent_form,
NafLookupTable5::from/select, the affine basepoint table, the 256-step
dsm_loop, and mul - with zero errors and zero sorries. This opens the
double-scalar-multiplication verification campaign (the EdDSA verify
equation's core).
This commit is contained in:
mrwulf 2026-07-04 11:10:36 +02:00
parent 77d587f4af
commit 0923301c86

View file

@ -12,7 +12,11 @@
use core::cmp::Ordering;
use crate::backend::serial::curve_models::{ProjectiveNielsPoint, ProjectivePoint};
use crate::backend::serial::curve_models::{CompletedPoint, ProjectiveNielsPoint, ProjectivePoint};
#[cfg(feature = "precomputed-tables")]
use crate::backend::serial::curve_models::AffineNielsPoint;
#[cfg(feature = "precomputed-tables")]
use crate::window::NafLookupTable8;
use crate::constants;
use crate::edwards::EdwardsPoint;
use crate::scalar::Scalar;
@ -20,6 +24,105 @@ use crate::traits::Identity;
use crate::window::NafLookupTable5;
/// Compute \\(aA + bB\\) in variable time, where \\(B\\) is the Ed25519 basepoint.
/// AENEAS-COMPAT helper: highest index with a nonzero NAF digit (or 0).
fn dsm_top_index(a_naf: &[i8; 256], b_naf: &[i8; 256]) -> usize {
// AENEAS-COMPAT: always process all 256 digits. Leading zero digits
// double the identity (a no-op), so the result of `mul` is unchanged;
// only the variable-time skip is dropped. (The conditional-fold scan
// fails Aeneas' loop fixed point.)
let _ = a_naf;
let _ = b_naf;
255
}
/// AENEAS-COMPAT helper: the double-and-add main loop over indices i..0,
/// with all borrows rooted in parameters (pure refactor).
#[cfg(feature = "precomputed-tables")]
fn dsm_loop(
i: usize,
a_naf: &[i8; 256],
b_naf: &[i8; 256],
table_a: &NafLookupTable5<ProjectiveNielsPoint>,
table_b: &NafLookupTable8<AffineNielsPoint>,
) -> ProjectivePoint {
let mut r = ProjectivePoint::identity();
let mut j: usize = i + 1;
while j > 0 {
let idx = j - 1;
let t0 = r.double();
let t1 = dsm_step_p(t0, table_a, a_naf[idx]);
let t2 = dsm_step_b(t1, table_b, b_naf[idx]);
r = t2.as_projective();
j -= 1;
}
r
}
#[cfg(not(feature = "precomputed-tables"))]
fn dsm_loop(
i: usize,
a_naf: &[i8; 256],
b_naf: &[i8; 256],
table_a: &NafLookupTable5<ProjectiveNielsPoint>,
table_b: &NafLookupTable5<ProjectiveNielsPoint>,
) -> ProjectivePoint {
let mut r = ProjectivePoint::identity();
let mut j: usize = i + 1;
while j > 0 {
let idx = j - 1;
let t0 = r.double();
let t1 = dsm_step_p(t0, table_a, a_naf[idx]);
let t2 = dsm_step_b(t1, table_b, b_naf[idx]);
r = t2.as_projective();
j -= 1;
}
r
}
/// AENEAS-COMPAT helper: one NAF-digit conditional add against a
/// ProjectiveNiels table (hoisted out of the main loop so its body is
/// single-assignment; pure refactor, semantics identical).
fn dsm_step_p(
t: CompletedPoint,
table: &NafLookupTable5<ProjectiveNielsPoint>,
d: i8,
) -> CompletedPoint {
if d > 0 {
&t.as_extended() + &table.select(d as usize)
} else if d < 0 {
&t.as_extended() - &table.select((-d) as usize)
} else {
t
}
}
/// AENEAS-COMPAT helper: as `dsm_step_p`, for the basepoint table.
#[cfg(feature = "precomputed-tables")]
fn dsm_step_b(
t: CompletedPoint,
table: &NafLookupTable8<AffineNielsPoint>,
d: i8,
) -> CompletedPoint {
if d > 0 {
&t.as_extended() + &table.select(d as usize)
} else if d < 0 {
&t.as_extended() - &table.select((-d) as usize)
} else {
t
}
}
/// AENEAS-COMPAT helper: as `dsm_step_p` (no precomputed tables).
#[cfg(not(feature = "precomputed-tables"))]
fn dsm_step_b(
t: CompletedPoint,
table: &NafLookupTable5<ProjectiveNielsPoint>,
d: i8,
) -> CompletedPoint {
dsm_step_p(t, table, d)
}
pub fn mul(a: &Scalar, A: &EdwardsPoint, b: &Scalar) -> EdwardsPoint {
let a_naf = a.non_adjacent_form(5);
@ -28,14 +131,8 @@ pub fn mul(a: &Scalar, A: &EdwardsPoint, b: &Scalar) -> EdwardsPoint {
#[cfg(not(feature = "precomputed-tables"))]
let b_naf = b.non_adjacent_form(5);
// Find starting index
let mut i: usize = 255;
for j in (0..256).rev() {
i = j;
if a_naf[i] != 0 || b_naf[i] != 0 {
break;
}
}
// Find starting index (AENEAS-COMPAT: helper, upward fold)
let i = dsm_top_index(&a_naf, &b_naf);
let table_A = NafLookupTable5::<ProjectiveNielsPoint>::from(A);
#[cfg(feature = "precomputed-tables")]
@ -44,29 +141,7 @@ pub fn mul(a: &Scalar, A: &EdwardsPoint, b: &Scalar) -> EdwardsPoint {
let table_B =
&NafLookupTable5::<ProjectiveNielsPoint>::from(&constants::ED25519_BASEPOINT_POINT);
let mut r = ProjectivePoint::identity();
loop {
let mut t = r.double();
match a_naf[i].cmp(&0) {
Ordering::Greater => t = &t.as_extended() + &table_A.select(a_naf[i] as usize),
Ordering::Less => t = &t.as_extended() - &table_A.select(-a_naf[i] as usize),
Ordering::Equal => {}
}
match b_naf[i].cmp(&0) {
Ordering::Greater => t = &t.as_extended() + &table_B.select(b_naf[i] as usize),
Ordering::Less => t = &t.as_extended() - &table_B.select(-b_naf[i] as usize),
Ordering::Equal => {}
}
r = t.as_projective();
if i == 0 {
break;
}
i -= 1;
}
let r = dsm_loop(i, &a_naf, &b_naf, &table_A, table_B);
r.as_extended()
}