Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Boxed uintlike coverage #436

Closed
wants to merge 21 commits into from

Conversation

xuganyu96
Copy link
Contributor

This is an attempt to implement for BoxedUint the missing arithmetic needed for crypto-primes (see entropyxyz/crypto-primes#37 (comment)).

This is the continuation of #428.

@xuganyu96 xuganyu96 marked this pull request as ready for review December 16, 2023 06:46
@xuganyu96
Copy link
Contributor Author

@tarcieri @fjarri this PR it ready for review. Thank you!

pub const fn const_new(n: BoxedUint) -> (Self, ConstChoice) {
let is_nonzero = n.is_nonzero();
(Self(n), is_nonzero)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BoxedUint can't be instantiated in a const fn context because it's heap-allocated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When should a function return (Value, Choice) instead of CtChoice(Value)?

In boxed/sqrt.rs the implementation of sqrt uses a NonZero::<BoxedUint>::new(x) whose value needs to be used whether x is zero or not, but BoxedUint is not ConditionallySelectable so I can't use CtChoice's map and unwrap_or. It seems like having a NonZero::<BoxedUint>::new that returns (NonZero<BoxedUint>, Choice) would be necessary, but the current new's signature doesn't work this way, and such a new is definitely not const_new.

Copy link
Member

@tarcieri tarcieri Dec 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The (hopefully temporary) workaround for that sort of thing is BoxedUint::conditional_map.

I'm trying to find an upstream solution in subtle but it may be necessary to define traits shaped like the ones in that PR in crypto-bigint as a stopgap (and a CtOption-alike type, possibly ConstCtOption) /cc @fjarri

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw the ConstantTimeSelect trait and will use ct_select and ct_assign where appropriate. Thank you for the implementation!

Comment on lines 16 to 29
pub(crate) fn select(a: &Self, b: &Self, c: ConstChoice) -> Self {
debug_assert_eq!(a.limbs.len(), b.limbs.len());
let mut limbs = vec![Limb::ZERO; a.limbs.len()];

let mut i = 0;
while i < limbs.len() {
limbs[i] = Limb::select(a.limbs[i], b.limbs[i], c);
i += 1;
}

Self {
limbs: limbs.into(),
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already impl'd as BoxedUint::conditional_select. You can use Into to convert from ConstChoice to Choice.

Comment on lines 31 to 36
/// Returns the truthy value if `self >= rhs` and the falsy value otherwise.
#[inline]
pub(crate) fn gt(lhs: &Self, rhs: &Self) -> ConstChoice {
let (_res, borrow) = rhs.sbb(lhs, Limb::ZERO);
ConstChoice::from_word_mask(borrow.0)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already impl'd as BoxedUint::ct_gt. Really there's not a lot of point in supporting ConstChoice with BoxedUint, and the least-common-denominator trait API should use Choice instead (which provides an optimization barrier in non-const fn contexts, which is the only place BoxedUint can be used)

@@ -0,0 +1,295 @@
//! [`BoxedUint`] square root operations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine to get this implemented, but in a followup PR (which I'm happy to do) it should probably get a similar macro treatment to the multiply functions so Uint and BoxedUint can share a common implementation.

@xuganyu96
Copy link
Contributor Author

xuganyu96 commented Dec 17, 2023

Note to myself:

  • use conditional_map in BoxedUint::sqrt

Edit: this is no longer needed; we can now use <BoxedUint as ConstantTimeSelect>::ct_select

Comment on lines 243 to 245
pub fn div_by_2(&self) -> Self {
Self {
montgomery_form: div_by_2(&self.montgomery_form, &self.residue_params.modulus),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little confusing due to the name clash which makes it almost look like it's going to recurse, although it's hard to suggest something better

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using fully qualified namespace like div_by_2::boxed::div_by_2?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, sounds good

@xuganyu96
Copy link
Contributor Author

Newest commits:

  • Used ConstantTimeSelect::{ct_assign, ct_select} where appropriate (see boxed/sqrt.rs and div_by_2)
  • Added ConstCtOption<NonZero<Limb>>::expect (for use in crypto-primes)

Comment on lines +99 to +105
let mut b = 0;
let mut i = 0;
while i < self.limbs.len() {
b |= self.limbs[i].0;
i += 1;
}
Limb(b).is_nonzero().into()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut b = 0;
let mut i = 0;
while i < self.limbs.len() {
b |= self.limbs[i].0;
i += 1;
}
Limb(b).is_nonzero().into()
let choice = Choice::from(1u8);
for limb in &self.limbs {
choice &= limb.ct_eq(&Limb::ZERO);
}
choice

Copy link
Contributor Author

@xuganyu96 xuganyu96 Dec 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just !self.is_zero()? The logic suggested above is basically what is_zero uses but backwards.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, we can do that, but other instances can be replaced with !x.is_zero() too. It makes less sense when const fn isn't required

Comment on lines +261 to +266
/// TODO: this is not really "const", but I need a way to return (value, choice) since
/// BoxedUint is not [`ConditionallySelectable`] so `CtChoice::map` and such does not work
pub fn const_new(n: BoxedUint) -> (Self, Choice) {
let nonzero = n.is_nonzero();
(Self(n), nonzero)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I factored these into methods on e.g. in this case BoxedUint: #484

You can use ConstCtChoice now.

@@ -131,6 +131,40 @@ impl BoxedUint {

out.into()
}

/// Create a new [`BoxedUint`] from the provided big endian hex string.
pub fn from_be_hex(hex: &str, bits_precision: u32) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these can't be constructed at compile-time, it would probably be good to make it fallible

Comment on lines -114 to -125
/// Computes `self << shift`.
/// Returns `None` if `shift >= self.bits_precision()`.
///
/// NOTE: this operation is variable time with respect to `shift` *ONLY*.
///
/// When used with a fixed `shift`, this function is constant-time with respect to `self`.
#[inline(always)]
pub fn shl_vartime(&self, shift: u32) -> Option<Self> {
let mut result = Self::zero_with_precision(self.bits_precision());
let success = self.shl_vartime_into(&mut result, shift);
success.map(|_| result)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally made them to accommodate the requirement from crypto-primes, but now I realized I can use overflowing_shl/overflowing_shr to achieve the same thing, so I felt no need to add shl_vartime/shr_vartime for BoxedUint.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err, I added shl_vartime in #330. The reason we have *_vartime equivalents of the sh* methods is because they can be quite a bit faster.

Copy link
Contributor

@fjarri fjarri Dec 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh now I see the reason for all the replaced _vartime methods previously. Yes, we should keep these.

Comment on lines -118 to -129
/// Computes `self >> shift`.
/// Returns `None` if `shift >= self.bits_precision()`.
///
/// NOTE: this operation is variable time with respect to `shift` *ONLY*.
///
/// When used with a fixed `shift`, this function is constant-time with respect to `self`.
#[inline(always)]
pub fn shr_vartime(&self, shift: u32) -> Option<Self> {
let mut result = Self::zero_with_precision(self.bits_precision());
let success = self.shr_vartime_into(&mut result, shift);
success.map(|_| result)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

@tarcieri
Copy link
Member

@xuganyu96 sorry about the rebase you're gonna have to do after #485, but we'll try to get this merged ASAP once you do!

@xuganyu96
Copy link
Contributor Author

@xuganyu96 sorry about the rebase you're gonna have to do after #485, but we'll try to get this merged ASAP once you do!

Totally not an issue. Thank you for being so thorough with my PR!

@@ -11,7 +11,7 @@ fn bench_shifts(c: &mut Criterion) {
group.bench_function("shl_vartime", |b| {
b.iter_batched(
|| BoxedUint::random(&mut OsRng, UINT_BITS),
|x| black_box(x.shl_vartime(UINT_BITS / 2 + 10)),
|x| black_box(x.overflowing_shl(UINT_BITS / 2 + 10).0),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change here? And if you're benchmarking overflowing_shl now, the string label should be changed too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sorry I think I got really confused between the various shl/shr functions.

pub fn div_by_2(&self) -> Self {
Self {
montgomery_form: div_by_2::boxed::div_by_2(&self.montgomery_form, &self.params.modulus),
params: self.params.clone(), // TODO: avoid clone?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There would be no need for clone if it was an inplace method, but if it creates a new number, I think cloning is unavoidable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When std is available they're stored as an Arc, so cloning is cheap (incrementing an atomic reference counter)

@@ -28,3 +28,23 @@ pub(crate) fn div_by_2<const LIMBS: usize>(a: &Uint<LIMBS>, modulus: &Uint<LIMBS

Uint::<LIMBS>::select(&if_even, &if_odd, is_odd)
}

#[cfg(feature = "alloc")]
pub(crate) mod boxed {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think putting different variants of this function in modules is a bit of an overcomplication, perhaps just different suffixes would suffice.

@@ -84,7 +140,7 @@ mod tests {
fn uint_with_bits_at(positions: &[u32]) -> BoxedUint {
let mut result = BoxedUint::zero_with_precision(256);
for &pos in positions {
result |= BoxedUint::one_with_precision(256).shl_vartime(pos).unwrap();
result |= BoxedUint::one_with_precision(256).overflowing_shl(pos).0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason for the change here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another instance of me being confused. Will restore back to its original state.

@@ -10,6 +10,28 @@ use subtle::{
Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeGreater, ConstantTimeLess,
};

impl BoxedUint {
/// Returns the Ordering between `self` and `rhs` in variable time.
pub fn cmp_vartime(&self, rhs: &Self) -> Ordering {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling that using comparisons between limbs instead of subtraction may be faster. Put a TODO perhaps?

@@ -38,7 +52,7 @@ impl BoxedUint {
let mut bd = self.bits_precision() - mb;
let mut rem = self.clone();
// Will not overflow since `bd < bits_precision`
let mut c = rhs.shl_vartime(bd).expect("shift within range");
let mut c = rhs.overflowing_shl(bd).0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a vartime method, no need to use a constant-time overflowing_shl here

@@ -112,7 +135,7 @@ impl BoxedUint {
let mut remainder = self.clone();
let mut quotient = Self::zero_with_precision(self.bits_precision());
// Will not overflow since `bd < bits_precision`
let mut c = rhs.shl_vartime(bd).expect("shift within range");
let mut c = rhs.overflowing_shl(bd).0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, no need for a constant-time shift here.

@@ -0,0 +1,208 @@
//! Reciprocal, shared across Uint and BoxedUint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should reference the paper the algorithm was taken from.

use super::{div2by1, Reciprocal};
use crate::{Limb, NonZero, Word};
#[test]
fn div2by1_overflow() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this test tests a function from uint::reciprocal, perhaps it should be located there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is actually an exact copy of the same test in uint::div_limb. I will delete this one in boxed/div_limb.rs.


/// Returns `u32::MAX` if `a < b` and `0` otherwise.
#[inline]
const fn lt(a: u32, b: u32) -> u32 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lt and select should really be moved to ConstChoice - forgot to do that in #458

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added them as TODO's.

@xuganyu96
Copy link
Contributor Author

@tarcieri @fjarri sorry but I felt I lost track of where I made changes that overrode your work (e.g. shl_vartime and shr_vartime). I've opened another PR #489.

@xuganyu96 xuganyu96 closed this Dec 21, 2023
tarcieri pushed a commit that referenced this pull request Dec 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants