Skip to content

Commit

Permalink
BoxedUint: add cond_map and cond_and_then
Browse files Browse the repository at this point in the history
Unfortunately `BoxedUint` can't impl `subtle::ConditionallySelectable`
due to a supertrait bound on `Copy`. See dalek-cryptography/subtle#94

This bound is required by `CtOption::map` and `CtOption::and_then` which
are important for writing constant-time code.

As a workaround which makes it still possible to leverate `CtOption`,
this adds special `BoxedUint`-specialized combinators that are able to
work around this issue by generating a placeholder (zero) value to pass
to the provided callbacks in the event `CtOption` is none.

This requires branching on the output of `CtOption` (which is
unavoidable without an upstream fix in `subtle` itself), but still
ensures that the provided callback function is called with a `BoxedUint`
of a matching number of limbs regardless of whether the `CtOption` is
some or none, which is the best we can do for now (and really quite
close to what `subtle` is doing under the hood anyway).
  • Loading branch information
tarcieri committed Nov 29, 2023
1 parent 68777f5 commit cd90fbe
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/uint/boxed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod bit_and;
mod bit_or;
mod bits;
mod cmp;
mod ct;
mod div;
pub(crate) mod encoding;
mod inv_mod;
Expand Down
120 changes: 120 additions & 0 deletions src/uint/boxed/ct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! Constant-time helper functions.
//!
//! These largely exist as a workaround for the `Copy` bound on [`subtle::ConditionallySelectable`].

use super::BoxedUint;
use subtle::CtOption;

impl BoxedUint {
/// Conditional `map`: workaround which provides a [`CtOption::map`]-like API.
///
/// Ensures both functions are called regardless of whether the first returns some/none with an
/// argument whose precision matches `self`.
///
/// Workaround due to `Copy` in [`subtle::ConditionallySelectable`] supertrait bounds.
pub fn cond_map<C, F, T>(&self, condition: C, f: F) -> CtOption<T>
where
C: Fn(&Self) -> CtOption<Self>,
F: Fn(Self) -> T,
{
let placeholder = Self::zero_with_precision(self.bits_precision());
let cond_val = condition(self);
let is_some = cond_val.is_some();

let value = Option::<Self>::from(cond_val).unwrap_or(placeholder);
debug_assert_eq!(self.bits_precision(), value.bits_precision());
CtOption::new(f(value), is_some)
}

/// Conditional `and_then`: workaround which provides a [`CtOption::and_then`]-like API.
///
/// Ensures both functions are called regardless of whether they return some/none with an
/// argument whose precision matches `self`.
///
/// Workaround due to `Copy` in [`subtle::ConditionallySelectable`] supertrait bounds.
pub fn cond_and_then<C, F>(&self, condition: C, f: F) -> CtOption<Self>
where
C: Fn(&Self) -> CtOption<Self>,
F: Fn(Self) -> CtOption<Self>,
{
let cond_val = condition(self);
let mut is_some = cond_val.is_some();

let placeholder = Self::zero_with_precision(self.bits_precision());
let value = Option::<Self>::from(cond_val).unwrap_or(placeholder);
debug_assert_eq!(self.bits_precision(), value.bits_precision());

let cond_val = f(value);
is_some &= cond_val.is_some();

let placeholder = Self::zero_with_precision(self.bits_precision());
let value = Option::from(cond_val).unwrap_or(placeholder);
CtOption::new(value, is_some)
}
}

#[cfg(test)]
mod tests {
use super::BoxedUint;
use subtle::CtOption;

#[test]
fn cond_map_some() {
let n = BoxedUint::one();

let ret = n
.cond_map(
|n| CtOption::new(n.clone(), 1u8.into()),
|n| n.wrapping_add(&BoxedUint::one()),
)
.unwrap();

assert_eq!(ret, BoxedUint::from(2u8));
}

#[test]
fn cond_map_none() {
let n = BoxedUint::one();

let ret = n.cond_map(
|n| CtOption::new(n.clone(), 0u8.into()),
|n| n.wrapping_add(&BoxedUint::one()),
);

assert!(bool::from(ret.is_none()));
}

#[test]
fn cond_and_then_all_some() {
let n = BoxedUint::one();

let ret = n
.cond_and_then(
|n| CtOption::new(n.clone(), 1u8.into()),
|n| CtOption::new(n.wrapping_add(&BoxedUint::one()), 1u8.into()),
)
.unwrap();

assert_eq!(ret, BoxedUint::from(2u8));
}

macro_rules! cond_and_then_none_test {
($name:ident, $a:expr, $b:expr) => {
#[test]
fn $name() {
let n = BoxedUint::one();

let ret = n.cond_and_then(
|n| CtOption::new(n.clone(), $a.into()),
|n| CtOption::new(n.wrapping_add(&BoxedUint::one()), $b.into()),
);

assert!(bool::from(ret.is_none()));
}
};
}

cond_and_then_none_test!(cond_and_then_none_some, 0, 1);
cond_and_then_none_test!(cond_and_then_some_none, 1, 0);
cond_and_then_none_test!(cond_and_then_none_none, 0, 0);
}

0 comments on commit cd90fbe

Please sign in to comment.