From f6d0374d79e80eb984f13a3f7d30fd94e4a5c3e4 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 6 Dec 2023 09:58:43 -0700 Subject: [PATCH] BoxedUint: add constant-time division implementation (#398) Adapts the implementation originally from #277 to `BoxedUint`, adding the following methods: - `BoxedUint::div_rem` - `BoxedUint::rem` Additionally, `wrapping_div` and `checked_div` have been changed to use the constant-time versions, rather than `*_vartime`. --- src/modular/boxed_residue.rs | 6 +-- src/uint/boxed/div.rs | 84 +++++++++++++++++++++++++++++------ src/uint/boxed/shl.rs | 12 +++++ tests/boxed_uint_proptests.rs | 43 ++++++++++++++++++ 4 files changed, 127 insertions(+), 18 deletions(-) diff --git a/src/modular/boxed_residue.rs b/src/modular/boxed_residue.rs index 1b0f3646..933e155b 100644 --- a/src/modular/boxed_residue.rs +++ b/src/modular/boxed_residue.rs @@ -58,20 +58,18 @@ impl BoxedResidueParams { .expect("modulus ensured non-zero"); let r = BoxedUint::max(bits_precision) - .rem_vartime(&modulus_nz) + .rem(&modulus_nz) .wrapping_add(&BoxedUint::one()); let r2 = r .square() - .rem_vartime(&modulus_nz.widen(bits_precision * 2)) // TODO(tarcieri): constant time + .rem(&modulus_nz.widen(bits_precision * 2)) .shorten(bits_precision); // Since we are calculating the inverse modulo (Word::MAX+1), // we can take the modulo right away and calculate the inverse of the first limb only. let modulus_lo = BoxedUint::from(modulus.limbs.get(0).copied().unwrap_or_default()); - let mod_neg_inv = Limb(Word::MIN.wrapping_sub(modulus_lo.inv_mod2k(Word::BITS).limbs[0].0)); - let r3 = montgomery_reduction_boxed(&mut r2.square(), &modulus, mod_neg_inv); let params = Self { diff --git a/src/uint/boxed/div.rs b/src/uint/boxed/div.rs index c9712cf3..39379cc5 100644 --- a/src/uint/boxed/div.rs +++ b/src/uint/boxed/div.rs @@ -2,13 +2,25 @@ use crate::{BoxedUint, CheckedDiv, Limb, NonZero, Wrapping}; use core::ops::{Div, DivAssign, Rem, RemAssign}; -use subtle::{Choice, ConstantTimeEq, CtOption}; +use subtle::{Choice, ConstantTimeEq, ConstantTimeLess, CtOption}; impl BoxedUint { + /// Computes self / rhs, returns the quotient, remainder. + pub fn div_rem(&self, rhs: &NonZero) -> (Self, Self) { + // Since `rhs` is nonzero, this should always hold. + self.div_rem_unchecked(rhs.as_ref()) + } + + /// Computes self % rhs, returns the remainder. + pub fn rem(&self, rhs: &NonZero) -> Self { + self.div_rem(rhs).1 + } + /// Computes self / rhs, returns the quotient, remainder. /// /// Variable-time with respect to `rhs` pub fn div_rem_vartime(&self, rhs: &NonZero) -> (Self, Self) { + // Since `rhs` is nonzero, this should always hold. self.div_rem_vartime_unchecked(rhs.as_ref()) } @@ -45,23 +57,60 @@ impl BoxedUint { /// /// Panics if `rhs == 0`. pub fn wrapping_div(&self, rhs: &NonZero) -> Self { - self.div_rem_vartime(rhs).0 + self.div_rem(rhs).0 } /// Perform checked division, returning a [`CtOption`] which `is_some` /// only if the rhs != 0 pub fn checked_div(&self, rhs: &Self) -> CtOption { - CtOption::new(self.div_rem_vartime_unchecked(rhs).0, rhs.is_zero()) + let q = self.div_rem_unchecked(rhs).0; + CtOption::new(q, !rhs.is_zero()) } - /// Compute divison and remainder without checking `rhs` is zero. - fn div_rem_vartime_unchecked(&self, rhs: &Self) -> (Self, Self) { + /// Computes `self` / `rhs`, returns the quotient (q), remainder (r) without checking if `rhs` + /// is zero. + /// + /// This function is constant-time with respect to both `self` and `rhs`. + fn div_rem_unchecked(&self, rhs: &Self) -> (Self, Self) { debug_assert_eq!(self.bits_precision(), rhs.bits_precision()); let mb = rhs.bits(); + let bits_precision = self.bits_precision(); + let mut rem = self.clone(); + let mut quo = Self::zero_with_precision(bits_precision); + let mut c = rhs.shl(bits_precision - mb); + let mut i = bits_precision; + let mut done = Choice::from(0u8); + + loop { + let (mut r, borrow) = rem.sbb(&c, Limb::ZERO); + rem = Self::conditional_select(&r, &rem, Choice::from((borrow.0 & 1) as u8) | done); + r = quo.bitor(&Self::one()); + quo = Self::conditional_select(&r, &quo, Choice::from((borrow.0 & 1) as u8) | done); + if i == 0 { + break; + } + i -= 1; + // when `i < mb`, the computation is actually done, so we ensure `quo` and `rem` + // aren't modified further (but do the remaining iterations anyway to be constant-time) + done = i.ct_lt(&mb); + c.shr1_assign(); + quo = Self::conditional_select(&quo.shl1(), &quo, done); + } + + (quo, rem) + } + + /// Computes `self` / `rhs`, returns the quotient (q), remainder (r) without checking if `rhs` + /// is zero. + /// + /// This function operates in variable-time. + fn div_rem_vartime_unchecked(&self, rhs: &Self) -> (Self, Self) { + debug_assert_eq!(self.bits_precision(), rhs.bits_precision()); + let mb = rhs.bits_vartime(); let mut bd = self.bits_precision() - mb; let mut remainder = self.clone(); let mut quotient = Self::zero_with_precision(self.bits_precision()); - let mut c = rhs.shl(bd); + let mut c = rhs.shl_vartime(bd); loop { let (mut r, borrow) = remainder.sbb(&c, Limb::ZERO); @@ -74,7 +123,7 @@ impl BoxedUint { } bd -= 1; c.shr1_assign(); - quotient = quotient.shl(1); + quotient.shl1_assign(); } (quotient, remainder) @@ -125,7 +174,7 @@ impl Div> for BoxedUint { type Output = BoxedUint; fn div(self, rhs: NonZero) -> Self::Output { - self.div_rem_vartime(&rhs).0 + self.div_rem(&rhs).0 } } @@ -190,7 +239,7 @@ impl Rem<&NonZero> for &BoxedUint { #[inline] fn rem(self, rhs: &NonZero) -> Self::Output { - self.rem_vartime(rhs) + self.rem(rhs) } } @@ -199,7 +248,7 @@ impl Rem<&NonZero> for BoxedUint { #[inline] fn rem(self, rhs: &NonZero) -> Self::Output { - self.rem_vartime(rhs) + Self::rem(&self, rhs) } } @@ -208,7 +257,7 @@ impl Rem> for &BoxedUint { #[inline] fn rem(self, rhs: NonZero) -> Self::Output { - self.rem_vartime(&rhs) + self.rem(&rhs) } } @@ -217,19 +266,19 @@ impl Rem> for BoxedUint { #[inline] fn rem(self, rhs: NonZero) -> Self::Output { - self.rem_vartime(&rhs) + self.rem(&rhs) } } impl RemAssign<&NonZero> for BoxedUint { fn rem_assign(&mut self, rhs: &NonZero) { - *self = self.rem_vartime(rhs) + *self = Self::rem(self, rhs) } } impl RemAssign> for BoxedUint { fn rem_assign(&mut self, rhs: NonZero) { - *self = self.rem_vartime(&rhs) + *self = Self::rem(self, &rhs) } } @@ -237,6 +286,13 @@ impl RemAssign> for BoxedUint { mod tests { use super::{BoxedUint, NonZero}; + #[test] + fn rem() { + let n = BoxedUint::from(0xFFEECCBBAA99887766u128); + let p = NonZero::new(BoxedUint::from(997u128)).unwrap(); + assert_eq!(BoxedUint::from(648u128), n.rem(&p)); + } + #[test] fn rem_vartime() { let n = BoxedUint::from(0xFFEECCBBAA99887766u128); diff --git a/src/uint/boxed/shl.rs b/src/uint/boxed/shl.rs index bc7b7b63..7fa9d4af 100644 --- a/src/uint/boxed/shl.rs +++ b/src/uint/boxed/shl.rs @@ -76,6 +76,18 @@ impl BoxedUint { (Self { limbs }, Limb(carry)) } + + /// Computes `self >> 1` in constant-time. + pub(crate) fn shl1(&self) -> Self { + // TODO(tarcieri): optimized implementation + self.shl_vartime(1) + } + + /// Computes `self >> 1` in-place in constant-time. + pub(crate) fn shl1_assign(&mut self) { + // TODO(tarcieri): optimized implementation + *self = self.shl1(); + } } impl Shl for BoxedUint { diff --git a/tests/boxed_uint_proptests.rs b/tests/boxed_uint_proptests.rs index c95c3352..4fcb99d6 100644 --- a/tests/boxed_uint_proptests.rs +++ b/tests/boxed_uint_proptests.rs @@ -94,6 +94,20 @@ proptest! { } } + #[test] + fn checked_div((a, b) in uint_pair()) { + let actual = a.checked_div(&b); + + if b.is_zero().into() { + prop_assert!(bool::from(actual.is_none())); + } else { + let a_bi = to_biguint(&a); + let b_bi = to_biguint(&b); + let expected = &a_bi / &b_bi; + prop_assert_eq!(expected, to_biguint(&actual.unwrap())); + } + } + #[test] fn div_rem((a, mut b) in uint_pair()) { if b.is_zero().into() { @@ -105,6 +119,22 @@ proptest! { let expected_quotient = &a_bi / &b_bi; let expected_remainder = a_bi % b_bi; + let (actual_quotient, actual_remainder) = a.div_rem(&NonZero::new(b).unwrap()); + prop_assert_eq!(expected_quotient, to_biguint(&actual_quotient)); + prop_assert_eq!(expected_remainder, to_biguint(&actual_remainder)); + } + + #[test] + fn div_rem_vartime((a, mut b) in uint_pair()) { + if b.is_zero().into() { + b = b.wrapping_add(&BoxedUint::one()); + } + + let a_bi = to_biguint(&a); + let b_bi = to_biguint(&b); + let expected_quotient = &a_bi / &b_bi; + let expected_remainder = a_bi % b_bi; + let (actual_quotient, actual_remainder) = a.div_rem_vartime(&NonZero::new(b).unwrap()); prop_assert_eq!(expected_quotient, to_biguint(&actual_quotient)); prop_assert_eq!(expected_remainder, to_biguint(&actual_remainder)); @@ -157,6 +187,19 @@ proptest! { prop_assert_eq!(expected, to_biguint(&actual)); } + #[test] + fn rem((a, b) in uint_pair()) { + if bool::from(!b.is_zero()) { + let a_bi = to_biguint(&a); + let b_bi = to_biguint(&b); + + let expected = a_bi % b_bi; + let actual = a.rem(&NonZero::new(b).unwrap()); + + prop_assert_eq!(expected, to_biguint(&actual)); + } + } + #[test] fn rem_vartime((a, b) in uint_pair()) { if bool::from(!b.is_zero()) {