diff --git a/Cargo.lock b/Cargo.lock index 49682ad..0d2a027 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/ml-dsa/Cargo.toml b/ml-dsa/Cargo.toml index 55cac1b..b660cf5 100644 --- a/ml-dsa/Cargo.toml +++ b/ml-dsa/Cargo.toml @@ -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 } diff --git a/ml-dsa/src/algebra.rs b/ml-dsa/src/algebra.rs index d3a9b02..68bbc98 100644 --- a/ml-dsa/src/algebra.rs +++ b/ml-dsa/src/algebra.rs @@ -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::(); - 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(&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 diff --git a/ml-dsa/src/hint.rs b/ml-dsa/src/hint.rs index e1d497f..c03ad1f 100644 --- a/ml-dsa/src/hint.rs +++ b/ml-dsa/src/hint.rs @@ -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(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(h: bool, r: Elem) -> Elem { let m: u32 = (BaseField::Q - 1) / TwoGamma2::U32; let (r1, r0) = r.decompose::(); 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)]