diff --git a/Cargo.lock b/Cargo.lock index 00a0163c..635a25f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,7 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.3.2" +version = "0.4.0-pre" dependencies = [ "cmov", "proptest", diff --git a/cmov/src/slice.rs b/cmov/src/slice.rs index a4b10a4f..89b95539 100644 --- a/cmov/src/slice.rs +++ b/cmov/src/slice.rs @@ -18,6 +18,7 @@ const _: () = assert!(size_of::() <= WORD_SIZE, "unexpected word size"); /// - if slices have unequal lengths impl Cmov for [u8] { #[inline] + #[track_caller] fn cmovnz(&mut self, value: &Self, condition: Condition) { assert_eq!( self.len(), diff --git a/ctutils/Cargo.toml b/ctutils/Cargo.toml index 9f781c02..cc8a76f3 100644 --- a/ctutils/Cargo.toml +++ b/ctutils/Cargo.toml @@ -5,7 +5,7 @@ Constant-time utility library with selection and equality testing support target applications. Supports `const fn` where appropriate. Built on the `cmov` crate which provides architecture-specific predication intrinsics. Heavily inspired by the `subtle` crate. """ -version = "0.3.2" +version = "0.4.0-pre" authors = ["RustCrypto Developers"] license = "Apache-2.0 OR MIT" homepage = "https://github.com/RustCrypto/utils/tree/master/ctselect" @@ -17,7 +17,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -cmov = "=0.5.0-pre.0" +cmov = "0.5.0-pre.0" # optional dependencies subtle = { version = "2", optional = true, default-features = false } diff --git a/ctutils/src/bytes.rs b/ctutils/src/bytes.rs index 1df96a98..a83cc56f 100644 --- a/ctutils/src/bytes.rs +++ b/ctutils/src/bytes.rs @@ -13,10 +13,23 @@ use crate::Choice; use cmov::{Cmov, CmovEq}; #[cfg(doc)] -use crate::{CtEq, CtSelect}; +use crate::{CtAssign, CtEq, CtSelect}; + +/// [`CtAssign`]-like trait impl'd for `[u8]` and `[u8; N]` providing optimized implementations +/// which perform better than the generic impl of [`CtAssign`] for `[T]` and `[T; N]` +/// where `T = u8`. +/// +/// Ideally we would use [specialization] to provide more specific impls of these traits for these +/// types, but it's unstable and unlikely to be stabilized soon. +/// +/// [specialization]: https://rust-lang.github.io/rfcs/1210-impl-specialization.html +pub trait BytesCtAssign: sealed::Sealed { + /// Conditionally assign `other` to `self` if `choice` is [`Choice::TRUE`]. + fn bytes_ct_assign(&mut self, other: &Self, choice: Choice); +} /// [`CtEq`]-like trait impl'd for `[u8]` and `[u8; N]` providing optimized implementations which -/// perform than the generic impl of [`CtEq`] for `[T; N]` where `T = u8`. +/// perform better than the generic impl of [`CtEq`] for `[T; N]` where `T = u8`. /// /// Ideally we would use [specialization] to provide more specific impls of these traits for these /// types, but it's unstable and unlikely to be stabilized soon. @@ -33,16 +46,13 @@ pub trait BytesCtEq: sealed::Sealed { } /// [`CtSelect`]-like trait impl'd for `[u8]` and `[u8; N]` providing optimized implementations -/// which perform than the generic impl of [`CtSelect`] for `[T; N]` where `T = u8`. +/// which perform better than the generic impl of [`CtSelect`] for `[T; N]` where `T = u8`. /// /// Ideally we would use [specialization] to provide more specific impls of these traits for these /// types, but it's unstable and unlikely to be stabilized soon. /// /// [specialization]: https://rust-lang.github.io/rfcs/1210-impl-specialization.html -pub trait BytesCtSelect: Sized + sealed::Sealed { - /// Conditionally assign `other` to `self` if `choice` is [`Choice::TRUE`]. - fn bytes_ct_assign(&mut self, other: &Self, choice: Choice); - +pub trait BytesCtSelect: BytesCtAssign + Sized { /// Select between `self` and `other` based on `choice`, returning a copy of the value. /// /// # Returns @@ -51,6 +61,22 @@ pub trait BytesCtSelect: Sized + sealed::Sealed { fn bytes_ct_select(&self, other: &Self, choice: Choice) -> Self; } +impl BytesCtAssign for [u8] { + #[inline] + #[track_caller] + fn bytes_ct_assign(&mut self, other: &Self, choice: Choice) { + assert_eq!( + self.len(), + other.len(), + "source slice length ({}) does not match destination slice length ({})", + other.len(), + self.len() + ); + + self.cmovnz(other, choice.into()); + } +} + impl BytesCtEq for [u8] { #[inline] fn bytes_ct_eq(&self, other: &[u8]) -> Choice { @@ -67,6 +93,13 @@ impl BytesCtEq for [u8; N] { } } +impl BytesCtAssign for [u8; N] { + #[inline] + fn bytes_ct_assign(&mut self, other: &Self, choice: Choice) { + self.cmovnz(other, choice.into()); + } +} + impl BytesCtEq<[u8]> for [u8; N] { #[inline] fn bytes_ct_eq(&self, other: &[u8]) -> Choice { @@ -77,11 +110,6 @@ impl BytesCtEq<[u8]> for [u8; N] { } impl BytesCtSelect for [u8; N] { - #[inline] - fn bytes_ct_assign(&mut self, other: &Self, choice: Choice) { - self.cmovnz(other, choice.into()); - } - #[inline] fn bytes_ct_select(&self, other: &Self, choice: Choice) -> Self { let mut ret = *self; @@ -100,7 +128,7 @@ mod sealed { #[cfg(test)] mod tests { - use super::{BytesCtEq, BytesCtSelect, Choice}; + use super::{BytesCtAssign, BytesCtEq, BytesCtSelect, Choice}; mod array { use super::*; @@ -137,6 +165,17 @@ mod tests { const EXAMPLE_B: &[u8] = &[2, 2, 3]; const EXAMPLE_C: &[u8] = &[1, 2]; + #[test] + fn bytes_ct_assign() { + let mut bytes = [0u8; 3]; + let slice = bytes.as_mut(); + + slice.bytes_ct_assign(EXAMPLE_A, Choice::FALSE); + assert_eq!(slice, &[0u8; 3]); + slice.bytes_ct_assign(EXAMPLE_A, Choice::TRUE); + assert_eq!(slice, EXAMPLE_A); + } + #[test] fn bytes_ct_eq() { assert!(EXAMPLE_A.bytes_ct_eq(EXAMPLE_A).to_bool()); diff --git a/ctutils/src/choice.rs b/ctutils/src/choice.rs index 4c6b0104..319daa20 100644 --- a/ctutils/src/choice.rs +++ b/ctutils/src/choice.rs @@ -1,4 +1,4 @@ -use crate::{CtEq, CtSelect}; +use crate::{CtAssign, CtEq, CtSelect}; use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; /// Bitwise less-than-or equal: returns `1` if `x <= y`, and otherwise returns `0`. @@ -476,6 +476,13 @@ impl BitXorAssign for Choice { } } +impl CtAssign for Choice { + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + self.0.ct_assign(&other.0, choice); + } +} + impl CtEq for Choice { #[inline] fn ct_eq(&self, other: &Self) -> Self { diff --git a/ctutils/src/ct_option.rs b/ctutils/src/ct_option.rs index a633bbd7..aa14d5f5 100644 --- a/ctutils/src/ct_option.rs +++ b/ctutils/src/ct_option.rs @@ -1,4 +1,4 @@ -use crate::{Choice, CtEq, CtSelect}; +use crate::{Choice, CtAssign, CtEq, CtSelect}; use core::ops::{Deref, DerefMut}; /// Helper macro for providing behavior like the [`CtOption::map`] combinator that works in @@ -532,6 +532,13 @@ impl CtOption<&mut T> { } } +impl CtAssign for CtOption { + fn ct_assign(&mut self, other: &Self, choice: Choice) { + self.value.ct_assign(&other.value, choice); + self.is_some.ct_assign(&other.is_some, choice); + } +} + impl CtEq for CtOption { #[inline] fn ct_eq(&self, other: &CtOption) -> Choice { diff --git a/ctutils/src/lib.rs b/ctutils/src/lib.rs index 69628019..9d362b8d 100644 --- a/ctutils/src/lib.rs +++ b/ctutils/src/lib.rs @@ -91,10 +91,10 @@ mod choice; mod ct_option; mod traits; -pub use bytes::{BytesCtEq, BytesCtSelect}; +pub use bytes::{BytesCtAssign, BytesCtEq, BytesCtSelect}; pub use choice::Choice; pub use ct_option::CtOption; pub use traits::{ - ct_eq::CtEq, ct_find::CtFind, ct_gt::CtGt, ct_lookup::CtLookup, ct_lt::CtLt, ct_neg::CtNeg, - ct_select::CtSelect, + ct_assign::CtAssign, ct_eq::CtEq, ct_find::CtFind, ct_gt::CtGt, ct_lookup::CtLookup, + ct_lt::CtLt, ct_neg::CtNeg, ct_select::CtSelect, }; diff --git a/ctutils/src/traits.rs b/ctutils/src/traits.rs index 82567304..42bc79a5 100644 --- a/ctutils/src/traits.rs +++ b/ctutils/src/traits.rs @@ -3,6 +3,7 @@ //! These are each in their own module so we can also define tests for the core types they're impl'd //! on in the same module. +pub(crate) mod ct_assign; pub(crate) mod ct_eq; pub(crate) mod ct_find; pub(crate) mod ct_gt; diff --git a/ctutils/src/traits/ct_assign.rs b/ctutils/src/traits/ct_assign.rs new file mode 100644 index 00000000..7d868c9d --- /dev/null +++ b/ctutils/src/traits/ct_assign.rs @@ -0,0 +1,115 @@ +use crate::{Choice, CtSelect}; +use cmov::Cmov; +use core::cmp; + +/// Constant-time conditional assignment: assign a given value to another based on a [`Choice`]. +pub trait CtAssign { + /// Conditionally assign `other` to `self` if `choice` is [`Choice::TRUE`]. + fn ct_assign(&mut self, other: &Self, choice: Choice); +} + +// Impl `CtAssign` using the `cmov::Cmov` trait +macro_rules! impl_ct_assign_with_cmov { + ( $($ty:ty),+ ) => { + $( + impl CtAssign for $ty { + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + self.cmovnz(other, choice.into()); + } + } + )+ + }; +} + +impl_ct_assign_with_cmov!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128); + +#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))] +impl CtAssign for isize { + #[cfg(target_pointer_width = "32")] + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + *self = Self::ct_select(self, other, choice); + } + + #[cfg(target_pointer_width = "64")] + #[allow(clippy::cast_possible_truncation)] + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + *self = Self::ct_select(self, other, choice); + } +} + +#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))] +impl CtAssign for usize { + #[cfg(target_pointer_width = "32")] + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + *self = Self::ct_select(self, other, choice); + } + + #[cfg(target_pointer_width = "64")] + #[allow(clippy::cast_possible_truncation)] + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + *self = Self::ct_select(self, other, choice); + } +} + +impl CtAssign for cmp::Ordering { + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + *self = Self::ct_select(self, other, choice); + } +} + +impl CtAssign for [T] +where + T: CtAssign, +{ + #[inline] + #[track_caller] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + assert_eq!( + self.len(), + other.len(), + "source slice length ({}) does not match destination slice length ({})", + other.len(), + self.len() + ); + + for (a, b) in self.iter_mut().zip(other) { + a.ct_assign(b, choice) + } + } +} + +impl CtAssign for [T; N] +where + T: CtAssign, +{ + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + self.as_mut_slice().ct_assign(other, choice); + } +} + +#[cfg(feature = "subtle")] +impl CtAssign for subtle::Choice { + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + *self = Self::ct_select(self, other, choice); + } +} + +#[cfg(feature = "subtle")] +impl CtAssign for subtle::CtOption +where + T: Default + subtle::ConditionallySelectable, +{ + #[inline] + fn ct_assign(&mut self, other: &Self, choice: Choice) { + use subtle::ConditionallySelectable as _; + self.conditional_assign(other, choice.into()); + } +} diff --git a/ctutils/src/traits/ct_neg.rs b/ctutils/src/traits/ct_neg.rs index ca9f5ab2..03e7885a 100644 --- a/ctutils/src/traits/ct_neg.rs +++ b/ctutils/src/traits/ct_neg.rs @@ -1,4 +1,4 @@ -use crate::{Choice, CtSelect}; +use crate::{Choice, CtAssign, CtSelect}; /// Constant-time conditional negation: negates a value when `choice` is [`Choice::TRUE`]. pub trait CtNeg: Sized { diff --git a/ctutils/src/traits/ct_select.rs b/ctutils/src/traits/ct_select.rs index 5f7e3c65..af7d42dd 100644 --- a/ctutils/src/traits/ct_select.rs +++ b/ctutils/src/traits/ct_select.rs @@ -1,12 +1,11 @@ -use crate::Choice; -use cmov::Cmov; +use crate::{Choice, CtAssign}; use core::cmp; #[cfg(feature = "subtle")] use crate::CtOption; /// Constant-time selection: pick between two values based on a given [`Choice`]. -pub trait CtSelect: Sized { +pub trait CtSelect: CtAssign + Sized { /// Select between `self` and `other` based on `choice`, returning a copy of the value. /// /// # Returns @@ -14,11 +13,6 @@ pub trait CtSelect: Sized { /// - `other` if `choice` is [`Choice::TRUE`]. fn ct_select(&self, other: &Self, choice: Choice) -> Self; - /// Conditionally assign `other` to `self` if `choice` is [`Choice::TRUE`]. - fn ct_assign(&mut self, other: &Self, choice: Choice) { - *self = Self::ct_select(self, other, choice); - } - /// Conditionally swap `self` and `other` if `choice` is [`Choice::TRUE`]. fn ct_swap(&mut self, other: &mut Self, choice: Choice) { let tmp = self.ct_select(other, choice); @@ -27,8 +21,8 @@ pub trait CtSelect: Sized { } } -// Impl `CtSelect` using the `cmov::Cmov` trait -macro_rules! impl_ct_select_with_cmov { +// Impl `CtSelect` using the `CtAssign` trait, which in turn calls `cmov::Cmov` +macro_rules! impl_ct_select_with_ct_assign { ( $($ty:ty),+ ) => { $( impl CtSelect for $ty { @@ -38,17 +32,12 @@ macro_rules! impl_ct_select_with_cmov { ret.ct_assign(other, choice); ret } - - #[inline] - fn ct_assign(&mut self, other: &Self, choice: Choice) { - self.cmovnz(other, choice.into()); - } } )+ }; } -impl_ct_select_with_cmov!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128); +impl_ct_select_with_ct_assign!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128); #[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))] impl CtSelect for isize { @@ -111,12 +100,6 @@ where fn ct_select(&self, other: &Self, choice: Choice) -> Self { core::array::from_fn(|i| T::ct_select(&self[i], &other[i], choice)) } - - fn ct_assign(&mut self, other: &Self, choice: Choice) { - for (a, b) in self.iter_mut().zip(other) { - a.ct_assign(b, choice) - } - } } #[cfg(feature = "subtle")] diff --git a/ctutils/tests/proptests.rs b/ctutils/tests/proptests.rs index 52bee384..b6768480 100644 --- a/ctutils/tests/proptests.rs +++ b/ctutils/tests/proptests.rs @@ -3,7 +3,7 @@ macro_rules! int_proptests { ( $($int:ident),+ ) => { $( mod $int { - use ctutils::{CtSelect, CtEq, Choice}; + use ctutils::{CtAssign, CtSelect, CtEq, Choice}; use proptest::prelude::*; proptest! {