Skip to content

Commit 72cd346

Browse files
committed
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.
1 parent a357620 commit 72cd346

File tree

12 files changed

+198
-45
lines changed

12 files changed

+198
-45
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmov/src/slice.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const _: () = assert!(size_of::<usize>() <= WORD_SIZE, "unexpected word size");
1818
/// - if slices have unequal lengths
1919
impl Cmov for [u8] {
2020
#[inline]
21+
#[track_caller]
2122
fn cmovnz(&mut self, value: &Self, condition: Condition) {
2223
assert_eq!(
2324
self.len(),

ctutils/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Constant-time utility library with selection and equality testing support target
55
applications. Supports `const fn` where appropriate. Built on the `cmov` crate which provides
66
architecture-specific predication intrinsics. Heavily inspired by the `subtle` crate.
77
"""
8-
version = "0.3.2"
8+
version = "0.4.0-pre"
99
authors = ["RustCrypto Developers"]
1010
license = "Apache-2.0 OR MIT"
1111
homepage = "https://github.com/RustCrypto/utils/tree/master/ctselect"
@@ -17,7 +17,7 @@ edition = "2024"
1717
rust-version = "1.85"
1818

1919
[dependencies]
20-
cmov = "=0.5.0-pre.0"
20+
cmov = "0.5.0-pre.0"
2121

2222
# optional dependencies
2323
subtle = { version = "2", optional = true, default-features = false }

ctutils/src/bytes.rs

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,23 @@ use crate::Choice;
1313
use cmov::{Cmov, CmovEq};
1414

1515
#[cfg(doc)]
16-
use crate::{CtEq, CtSelect};
16+
use crate::{CtAssign, CtEq, CtSelect};
17+
18+
/// [`CtAssign`]-like trait impl'd for `[u8]` and `[u8; N]` providing optimized implementations
19+
/// which perform better than the generic impl of [`CtAssign`] for `[T]` and `[T; N]`
20+
/// where `T = u8`.
21+
///
22+
/// Ideally we would use [specialization] to provide more specific impls of these traits for these
23+
/// types, but it's unstable and unlikely to be stabilized soon.
24+
///
25+
/// [specialization]: https://rust-lang.github.io/rfcs/1210-impl-specialization.html
26+
pub trait BytesCtAssign: sealed::Sealed {
27+
/// Conditionally assign `other` to `self` if `choice` is [`Choice::TRUE`].
28+
fn bytes_ct_assign(&mut self, other: &Self, choice: Choice);
29+
}
1730

1831
/// [`CtEq`]-like trait impl'd for `[u8]` and `[u8; N]` providing optimized implementations which
19-
/// perform than the generic impl of [`CtEq`] for `[T; N]` where `T = u8`.
32+
/// perform better than the generic impl of [`CtEq`] for `[T; N]` where `T = u8`.
2033
///
2134
/// Ideally we would use [specialization] to provide more specific impls of these traits for these
2235
/// types, but it's unstable and unlikely to be stabilized soon.
@@ -33,16 +46,13 @@ pub trait BytesCtEq<Rhs: ?Sized = Self>: sealed::Sealed {
3346
}
3447

3548
/// [`CtSelect`]-like trait impl'd for `[u8]` and `[u8; N]` providing optimized implementations
36-
/// which perform than the generic impl of [`CtSelect`] for `[T; N]` where `T = u8`.
49+
/// which perform better than the generic impl of [`CtSelect`] for `[T; N]` where `T = u8`.
3750
///
3851
/// Ideally we would use [specialization] to provide more specific impls of these traits for these
3952
/// types, but it's unstable and unlikely to be stabilized soon.
4053
///
4154
/// [specialization]: https://rust-lang.github.io/rfcs/1210-impl-specialization.html
42-
pub trait BytesCtSelect: Sized + sealed::Sealed {
43-
/// Conditionally assign `other` to `self` if `choice` is [`Choice::TRUE`].
44-
fn bytes_ct_assign(&mut self, other: &Self, choice: Choice);
45-
55+
pub trait BytesCtSelect: BytesCtAssign + Sized {
4656
/// Select between `self` and `other` based on `choice`, returning a copy of the value.
4757
///
4858
/// # Returns
@@ -51,6 +61,22 @@ pub trait BytesCtSelect: Sized + sealed::Sealed {
5161
fn bytes_ct_select(&self, other: &Self, choice: Choice) -> Self;
5262
}
5363

64+
impl BytesCtAssign for [u8] {
65+
#[inline]
66+
#[track_caller]
67+
fn bytes_ct_assign(&mut self, other: &Self, choice: Choice) {
68+
assert_eq!(
69+
self.len(),
70+
other.len(),
71+
"source slice length ({}) does not match destination slice length ({})",
72+
other.len(),
73+
self.len()
74+
);
75+
76+
self.cmovnz(other, choice.into());
77+
}
78+
}
79+
5480
impl BytesCtEq for [u8] {
5581
#[inline]
5682
fn bytes_ct_eq(&self, other: &[u8]) -> Choice {
@@ -67,6 +93,13 @@ impl<const N: usize> BytesCtEq for [u8; N] {
6793
}
6894
}
6995

96+
impl<const N: usize> BytesCtAssign for [u8; N] {
97+
#[inline]
98+
fn bytes_ct_assign(&mut self, other: &Self, choice: Choice) {
99+
self.cmovnz(other, choice.into());
100+
}
101+
}
102+
70103
impl<const N: usize> BytesCtEq<[u8]> for [u8; N] {
71104
#[inline]
72105
fn bytes_ct_eq(&self, other: &[u8]) -> Choice {
@@ -77,11 +110,6 @@ impl<const N: usize> BytesCtEq<[u8]> for [u8; N] {
77110
}
78111

79112
impl<const N: usize> BytesCtSelect for [u8; N] {
80-
#[inline]
81-
fn bytes_ct_assign(&mut self, other: &Self, choice: Choice) {
82-
self.cmovnz(other, choice.into());
83-
}
84-
85113
#[inline]
86114
fn bytes_ct_select(&self, other: &Self, choice: Choice) -> Self {
87115
let mut ret = *self;
@@ -100,7 +128,7 @@ mod sealed {
100128

101129
#[cfg(test)]
102130
mod tests {
103-
use super::{BytesCtEq, BytesCtSelect, Choice};
131+
use super::{BytesCtAssign, BytesCtEq, BytesCtSelect, Choice};
104132

105133
mod array {
106134
use super::*;
@@ -137,6 +165,17 @@ mod tests {
137165
const EXAMPLE_B: &[u8] = &[2, 2, 3];
138166
const EXAMPLE_C: &[u8] = &[1, 2];
139167

168+
#[test]
169+
fn bytes_ct_assign() {
170+
let mut bytes = [0u8; 3];
171+
let slice = bytes.as_mut();
172+
173+
slice.bytes_ct_assign(EXAMPLE_A, Choice::FALSE);
174+
assert_eq!(slice, &[0u8; 3]);
175+
slice.bytes_ct_assign(EXAMPLE_A, Choice::TRUE);
176+
assert_eq!(slice, EXAMPLE_A);
177+
}
178+
140179
#[test]
141180
fn bytes_ct_eq() {
142181
assert!(EXAMPLE_A.bytes_ct_eq(EXAMPLE_A).to_bool());

ctutils/src/choice.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{CtEq, CtSelect};
1+
use crate::{CtAssign, CtEq, CtSelect};
22
use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not};
33

44
/// Bitwise less-than-or equal: returns `1` if `x <= y`, and otherwise returns `0`.
@@ -476,6 +476,13 @@ impl BitXorAssign for Choice {
476476
}
477477
}
478478

479+
impl CtAssign for Choice {
480+
#[inline]
481+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
482+
self.0.ct_assign(&other.0, choice);
483+
}
484+
}
485+
479486
impl CtEq for Choice {
480487
#[inline]
481488
fn ct_eq(&self, other: &Self) -> Self {

ctutils/src/ct_option.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Choice, CtEq, CtSelect};
1+
use crate::{Choice, CtAssign, CtEq, CtSelect};
22
use core::ops::{Deref, DerefMut};
33

44
/// Helper macro for providing behavior like the [`CtOption::map`] combinator that works in
@@ -532,6 +532,13 @@ impl<T> CtOption<&mut T> {
532532
}
533533
}
534534

535+
impl<T: CtAssign> CtAssign for CtOption<T> {
536+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
537+
self.value.ct_assign(&other.value, choice);
538+
self.is_some.ct_assign(&other.is_some, choice);
539+
}
540+
}
541+
535542
impl<T: CtEq> CtEq for CtOption<T> {
536543
#[inline]
537544
fn ct_eq(&self, other: &CtOption<T>) -> Choice {

ctutils/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ mod choice;
9191
mod ct_option;
9292
mod traits;
9393

94-
pub use bytes::{BytesCtEq, BytesCtSelect};
94+
pub use bytes::{BytesCtAssign, BytesCtEq, BytesCtSelect};
9595
pub use choice::Choice;
9696
pub use ct_option::CtOption;
9797
pub use traits::{
98-
ct_eq::CtEq, ct_find::CtFind, ct_gt::CtGt, ct_lookup::CtLookup, ct_lt::CtLt, ct_neg::CtNeg,
99-
ct_select::CtSelect,
98+
ct_assign::CtAssign, ct_eq::CtEq, ct_find::CtFind, ct_gt::CtGt, ct_lookup::CtLookup,
99+
ct_lt::CtLt, ct_neg::CtNeg, ct_select::CtSelect,
100100
};

ctutils/src/traits.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! These are each in their own module so we can also define tests for the core types they're impl'd
44
//! on in the same module.
55
6+
pub(crate) mod ct_assign;
67
pub(crate) mod ct_eq;
78
pub(crate) mod ct_find;
89
pub(crate) mod ct_gt;

ctutils/src/traits/ct_assign.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use crate::{Choice, CtSelect};
2+
use cmov::Cmov;
3+
use core::cmp;
4+
5+
/// Constant-time conditional assignment: assign a given value to another based on a [`Choice`].
6+
pub trait CtAssign {
7+
/// Conditionally assign `other` to `self` if `choice` is [`Choice::TRUE`].
8+
fn ct_assign(&mut self, other: &Self, choice: Choice);
9+
}
10+
11+
// Impl `CtAssign` using the `cmov::Cmov` trait
12+
macro_rules! impl_ct_assign_with_cmov {
13+
( $($ty:ty),+ ) => {
14+
$(
15+
impl CtAssign for $ty {
16+
#[inline]
17+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
18+
self.cmovnz(other, choice.into());
19+
}
20+
}
21+
)+
22+
};
23+
}
24+
25+
impl_ct_assign_with_cmov!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
26+
27+
#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))]
28+
impl CtAssign for isize {
29+
#[cfg(target_pointer_width = "32")]
30+
#[inline]
31+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
32+
*self = Self::ct_select(self, other, choice);
33+
}
34+
35+
#[cfg(target_pointer_width = "64")]
36+
#[allow(clippy::cast_possible_truncation)]
37+
#[inline]
38+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
39+
*self = Self::ct_select(self, other, choice);
40+
}
41+
}
42+
43+
#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))]
44+
impl CtAssign for usize {
45+
#[cfg(target_pointer_width = "32")]
46+
#[inline]
47+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
48+
*self = Self::ct_select(self, other, choice);
49+
}
50+
51+
#[cfg(target_pointer_width = "64")]
52+
#[allow(clippy::cast_possible_truncation)]
53+
#[inline]
54+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
55+
*self = Self::ct_select(self, other, choice);
56+
}
57+
}
58+
59+
impl CtAssign for cmp::Ordering {
60+
#[inline]
61+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
62+
*self = Self::ct_select(self, other, choice);
63+
}
64+
}
65+
66+
impl<T> CtAssign for [T]
67+
where
68+
T: CtAssign,
69+
{
70+
#[inline]
71+
#[track_caller]
72+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
73+
assert_eq!(
74+
self.len(),
75+
other.len(),
76+
"source slice length ({}) does not match destination slice length ({})",
77+
other.len(),
78+
self.len()
79+
);
80+
81+
for (a, b) in self.iter_mut().zip(other) {
82+
a.ct_assign(b, choice)
83+
}
84+
}
85+
}
86+
87+
impl<T, const N: usize> CtAssign for [T; N]
88+
where
89+
T: CtAssign,
90+
{
91+
#[inline]
92+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
93+
self.as_mut_slice().ct_assign(other, choice);
94+
}
95+
}
96+
97+
#[cfg(feature = "subtle")]
98+
impl CtAssign for subtle::Choice {
99+
#[inline]
100+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
101+
*self = Self::ct_select(self, other, choice);
102+
}
103+
}
104+
105+
#[cfg(feature = "subtle")]
106+
impl<T> CtAssign for subtle::CtOption<T>
107+
where
108+
T: Default + subtle::ConditionallySelectable,
109+
{
110+
#[inline]
111+
fn ct_assign(&mut self, other: &Self, choice: Choice) {
112+
use subtle::ConditionallySelectable as _;
113+
self.conditional_assign(other, choice.into());
114+
}
115+
}

ctutils/src/traits/ct_neg.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Choice, CtSelect};
1+
use crate::{Choice, CtAssign, CtSelect};
22

33
/// Constant-time conditional negation: negates a value when `choice` is [`Choice::TRUE`].
44
pub trait CtNeg: Sized {

0 commit comments

Comments
 (0)