ml-dsa: use ctutils for constant-time selection; avoid branches (#1245)

This commit is contained in:
Scott Arciszewski
2026-03-26 15:19:57 -04:00
committed by GitHub
parent cdc95068ae
commit 7b0ffd01c1
4 changed files with 43 additions and 36 deletions
Generated
+1
View File
@@ -813,6 +813,7 @@ version = "0.1.0-rc.7"
dependencies = [
"const-oid 0.10.2",
"criterion",
"ctutils",
"getrandom 0.4.2",
"hex",
"hex-literal",
+1
View File
@@ -36,6 +36,7 @@ hybrid-array = { version = "0.4", features = ["extra-sizes"] }
module-lattice = "0.2.0-rc.0"
sha3 = { version = "0.11.0-rc.8", default-features = false }
signature = { version = "3.0.0-rc.10", default-features = false, features = ["digest"] }
ctutils = { version = "0.4", default-features = false }
# optional dependencies
const-oid = { version = "0.10", features = ["db"], optional = true }
+23 -23
View File
@@ -1,3 +1,4 @@
use ctutils::{CtEq, CtGt, CtLt, CtSelect};
use hybrid_array::{
ArraySize,
typenum::{Shleft, U1, U13, Unsigned},
@@ -28,11 +29,9 @@ pub(crate) trait BarrettReduce: Unsigned {
let quotient = (x * Self::MULTIPLIER) >> Self::SHIFT;
let remainder = x - quotient * m;
if remainder < m {
Truncate::truncate(remainder)
} else {
Truncate::truncate(remainder - m)
}
let r_small: u32 = Truncate::truncate(remainder);
let r_large: u32 = Truncate::truncate(remainder.wrapping_sub(m));
u32::ct_select(&r_large, &r_small, remainder.ct_lt(&m))
}
}
@@ -103,14 +102,17 @@ impl Decompose for Elem {
let r_plus = self.clone();
let r0 = r_plus.mod_plus_minus::<TwoGamma2>();
if r_plus - r0 == Elem::new(BaseField::Q - 1) {
(Elem::new(0), r0 - Elem::new(1))
} else {
let diff = r_plus - r0;
// Use constant-time division instead of hardware division
let r1 = Elem::new(TwoGamma2::ct_div(diff.0));
(r1, r0)
}
let diff = r_plus - r0;
let is_edge = diff.0.ct_eq(&(BaseField::Q - 1));
// Compute both branches unconditionally
let edge = (Elem::new(0), r0 - Elem::new(1));
let r1 = Elem::new(TwoGamma2::ct_div(diff.0));
let normal = (r1, r0);
let r1_out = Elem::new(u32::ct_select(&normal.0.0, &edge.0.0, is_edge));
let r0_out = Elem::new(u32::ct_select(&normal.1.0, &edge.1.0, is_edge));
(r1_out, r0_out)
}
}
@@ -126,11 +128,12 @@ pub(crate) trait AlgebraExt: Sized {
impl AlgebraExt for Elem {
fn mod_plus_minus<M: Unsigned>(&self) -> Self {
let raw_mod = Elem::new(M::reduce(self.0));
if raw_mod.0 <= M::U32 >> 1 {
raw_mod
} else {
raw_mod - Elem::new(M::U32)
}
let in_lower_half = !raw_mod.0.ct_gt(&(M::U32 >> 1));
Elem::new(u32::ct_select(
&(raw_mod - Elem::new(M::U32)).0,
&raw_mod.0,
in_lower_half,
))
}
// FIPS 204 defines the infinity norm differently for signed vs. unsigned integers:
@@ -142,11 +145,8 @@ impl AlgebraExt for Elem {
// the signed integers used in this crate, so we can safely use the unsigned version. However,
// since mod_plus_minus is also unsigned, we need to unwrap the "negative" values.
fn infinity_norm(&self) -> u32 {
if self.0 <= BaseField::Q >> 1 {
self.0
} else {
BaseField::Q - self.0
}
let in_lower_half = !self.0.ct_gt(&(BaseField::Q >> 1));
u32::ct_select(&(BaseField::Q - self.0), &self.0, in_lower_half)
}
// Algorithm 35 Power2Round
+18 -13
View File
@@ -2,6 +2,7 @@ use crate::{
algebra::{AlgebraExt, BaseField, Decompose, Elem, Polynomial, Vector},
param::{EncodedHint, SignatureParams},
};
use ctutils::{Choice, CtEq, CtGt, CtSelect};
use hybrid_array::{
Array,
typenum::{U256, Unsigned},
@@ -17,25 +18,29 @@ fn make_hint<TwoGamma2: Unsigned>(z: Elem, r: Elem) -> bool {
}
/// Algorithm 40 `UseHint`: returns the high bits of `r` adjusted according to hint `h`.
///
/// All branches are replaced with constant-time selection to avoid
/// leaking information about `r0` through branch timing.
#[allow(clippy::integer_division_remainder_used, reason = "params are public")]
fn use_hint<TwoGamma2: Unsigned>(h: bool, r: Elem) -> Elem {
let m: u32 = (BaseField::Q - 1) / TwoGamma2::U32;
let (r1, r0) = r.decompose::<TwoGamma2>();
let gamma2 = TwoGamma2::U32 / 2;
if h {
if r0.0 > 0 && r0.0 <= gamma2 {
Elem::new((r1.0 + 1) % m)
} else if (r0.0 == 0) || (r0.0 >= BaseField::Q - gamma2) {
Elem::new((r1.0 + m - 1) % m)
} else {
// We use the Elem encoding even for signed integers. Since r0 is computed
// mod+- 2*gamma2 (possibly minus 1), it is guaranteed to be in [-gamma2, gamma2].
unreachable!();
}
} else {
r1
}
// Compute both possible hint-adjusted results unconditionally
let r1_inc = Elem::new((r1.0 + 1) % m);
let r1_dec = Elem::new((r1.0 + m - 1) % m);
// r0 is "positive" when r0 > 0 AND r0 <= gamma2
let r0_positive = !r0.0.ct_eq(&0) & !r0.0.ct_gt(&gamma2);
let hinted = Elem::new(u32::ct_select(&r1_dec.0, &r1_inc.0, r0_positive));
// Apply hint only when h is set
Elem::new(u32::ct_select(
&r1.0,
&hinted.0,
Choice::from_u8_lsb(u8::from(h)),
))
}
#[derive(Clone, PartialEq, Debug)]