From 72cd346b15cb67a4a2fb20820f4e951e88c461f9 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Fri, 16 Jan 2026 15:29:18 -0700 Subject: [PATCH] ctutils: split `CtAssign` out of `CtSelect` [BREAKING] Note: version bumped to v0.4.0-pre to denote breaking change (not for release) Extracts the `ct_assign` method into its own trait. The main advantage of this is the `CtSelect` trait requires a `Sized` bound, where an independent `CtAssign` trait does not, which means it can be impl'd for slices as a sort of conditional `copy_from_slice`. The main disadvantage is we lose the default implementation, so it becomes one more trait to impl. There's no good way to blanket impl one in terms of the other that won't preclude efficient implementations in some way or another, so this opts not to do that for maximum flexibility. To preserve `subtle`-like ergonomics where `CtSelect` means we can `CtAssign` as well, this bounds `CtSelect` on `CtAssign`. Finally, the same treatment is given to the `Bytes*` traits, with a `BytesCtAssign` split out of `BytesCtSelect` for parity, and an impl of `BytesCtAssign` for `[u8]` has been added. --- Cargo.lock | 2 +- cmov/src/slice.rs | 1 + ctutils/Cargo.toml | 4 +- ctutils/src/bytes.rs | 65 ++++++++++++++---- ctutils/src/choice.rs | 9 ++- ctutils/src/ct_option.rs | 9 ++- ctutils/src/lib.rs | 6 +- ctutils/src/traits.rs | 1 + ctutils/src/traits/ct_assign.rs | 115 ++++++++++++++++++++++++++++++++ ctutils/src/traits/ct_neg.rs | 2 +- ctutils/src/traits/ct_select.rs | 27 ++------ ctutils/tests/proptests.rs | 2 +- 12 files changed, 198 insertions(+), 45 deletions(-) create mode 100644 ctutils/src/traits/ct_assign.rs 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! {