Skip to content

Commit 1658a0f

Browse files
authored
ctutils: add BytesCtEq and BytesCtSelect traits (#1359)
The generic implementations of `CtEq` and `CtSelect` for `[T]` and `[T; N]` are suboptimal for `[u8]` and `[u8; N]`, where there are now optimized implementations in the `cmov` crate that perform operations on such types a word-at-a-time instead of a byte-at-a-time. Lacking specialization, this adds some redundant sealed traits that are impl'd for `[u8; N]` and `[u8]` which provide access to the optimized implementations for these types in the `cmov` crate, without exposing `cmov` in the public API of `ctutils`.
1 parent 5038c79 commit 1658a0f

File tree

4 files changed

+161
-3
lines changed

4 files changed

+161
-3
lines changed

ctutils/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//! Traits which provide access to the optimized implementations of `cmov::{Cmov, CmovEq}` for
2+
//! `[u8]` and `[u8; N]`.
3+
//!
4+
//! The trait impls for these types in the `cmov` crate coalesce word-sized chunks of bytes and then
5+
//! use the underlying `Cmov`/`CmovEq` operation a word-at-a-time, for as many word-sized chunks
6+
//! exist a given byte array/slice. The remainder are compared a byte-at-a-time in the event that
7+
//! the array/slice length isn't a multiple of the word size.
8+
//!
9+
//! This is much more efficient than the generic impl which first converts each individual `u8` into
10+
//! a word, then compares those, performing one CMOV-like operation per byte instead of per word.
11+
12+
use crate::Choice;
13+
use cmov::{Cmov, CmovEq};
14+
15+
#[cfg(doc)]
16+
use crate::{CtEq, CtSelect};
17+
18+
/// [`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`.
20+
///
21+
/// Ideally we would use [specialization] to provide more specific impls of these traits for these
22+
/// types, but it's unstable and unlikely to be stabilized soon.
23+
///
24+
/// [specialization]: https://rust-lang.github.io/rfcs/1210-impl-specialization.html
25+
pub trait BytesCtEq<Rhs: ?Sized = Self>: sealed::Sealed {
26+
/// Determine if `self` is equal to the provided `value`.
27+
fn bytes_ct_eq(&self, other: &Rhs) -> Choice;
28+
29+
/// Determine if `self` is NOT equal to the provided `value`.
30+
fn bytes_ct_ne(&self, other: &Rhs) -> Choice {
31+
!self.bytes_ct_eq(other)
32+
}
33+
}
34+
35+
/// [`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`.
37+
///
38+
/// Ideally we would use [specialization] to provide more specific impls of these traits for these
39+
/// types, but it's unstable and unlikely to be stabilized soon.
40+
///
41+
/// [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+
46+
/// Select between `self` and `other` based on `choice`, returning a copy of the value.
47+
///
48+
/// # Returns
49+
/// - `self` if `choice` is [`Choice::FALSE`].
50+
/// - `other` if `choice` is [`Choice::TRUE`].
51+
fn bytes_ct_select(&self, other: &Self, choice: Choice) -> Self;
52+
}
53+
54+
impl BytesCtEq for [u8] {
55+
#[inline]
56+
fn bytes_ct_eq(&self, other: &[u8]) -> Choice {
57+
let mut ret = Choice::FALSE;
58+
self.cmoveq(other, 1, &mut ret.0);
59+
ret
60+
}
61+
}
62+
63+
impl<const N: usize> BytesCtEq for [u8; N] {
64+
#[inline]
65+
fn bytes_ct_eq(&self, other: &[u8; N]) -> Choice {
66+
self.bytes_ct_eq(other.as_slice())
67+
}
68+
}
69+
70+
impl<const N: usize> BytesCtEq<[u8]> for [u8; N] {
71+
#[inline]
72+
fn bytes_ct_eq(&self, other: &[u8]) -> Choice {
73+
let mut ret = Choice::FALSE;
74+
self.as_slice().cmoveq(other, 1, &mut ret.0);
75+
ret
76+
}
77+
}
78+
79+
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+
85+
#[inline]
86+
fn bytes_ct_select(&self, other: &Self, choice: Choice) -> Self {
87+
let mut ret = *self;
88+
ret.cmovnz(other, choice.into());
89+
ret
90+
}
91+
}
92+
93+
mod sealed {
94+
/// Sealed trait to prevent others from adding impls of [`BytesExt`]. Instead, impls of the
95+
/// `Ct*` traits should be used.
96+
pub trait Sealed {}
97+
impl Sealed for [u8] {}
98+
impl<const N: usize> Sealed for [u8; N] {}
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use super::{BytesCtEq, BytesCtSelect, Choice};
104+
105+
mod array {
106+
use super::*;
107+
108+
const EXAMPLE_A: [u8; 3] = [1, 2, 3];
109+
const EXAMPLE_B: [u8; 3] = [2, 2, 3];
110+
111+
#[test]
112+
fn bytes_ct_eq() {
113+
assert!(EXAMPLE_A.bytes_ct_eq(&EXAMPLE_A).to_bool());
114+
assert!(!EXAMPLE_A.bytes_ct_eq(&EXAMPLE_B).to_bool());
115+
}
116+
117+
#[test]
118+
fn bytes_ct_ne() {
119+
assert!(!EXAMPLE_A.bytes_ct_ne(&EXAMPLE_A).to_bool());
120+
assert!(EXAMPLE_A.bytes_ct_ne(&EXAMPLE_B).to_bool());
121+
}
122+
123+
#[test]
124+
fn bytes_ct_select() {
125+
let should_be_a = EXAMPLE_A.bytes_ct_select(&EXAMPLE_B, Choice::FALSE);
126+
assert_eq!(EXAMPLE_A, should_be_a);
127+
128+
let should_be_b = EXAMPLE_A.bytes_ct_select(&EXAMPLE_B, Choice::TRUE);
129+
assert_eq!(EXAMPLE_B, should_be_b);
130+
}
131+
}
132+
133+
mod slice {
134+
use super::*;
135+
136+
const EXAMPLE_A: &[u8] = &[1, 2, 3];
137+
const EXAMPLE_B: &[u8] = &[2, 2, 3];
138+
const EXAMPLE_C: &[u8] = &[1, 2];
139+
140+
#[test]
141+
fn bytes_ct_eq() {
142+
assert!(EXAMPLE_A.bytes_ct_eq(EXAMPLE_A).to_bool());
143+
assert!(!EXAMPLE_A.bytes_ct_eq(EXAMPLE_B).to_bool());
144+
// different lengths
145+
assert!(!EXAMPLE_A.bytes_ct_eq(EXAMPLE_C).to_bool());
146+
}
147+
148+
#[test]
149+
fn bytes_ct_ne() {
150+
assert!(!EXAMPLE_A.bytes_ct_ne(EXAMPLE_A).to_bool());
151+
assert!(EXAMPLE_A.bytes_ct_ne(EXAMPLE_B).to_bool());
152+
// different lengths
153+
assert!(EXAMPLE_A.bytes_ct_ne(EXAMPLE_C).to_bool());
154+
}
155+
}
156+
}

ctutils/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,12 @@
8686
//! This makes it possible to use `ctutils` in a codebase where other dependencies are using
8787
//! `subtle`.
8888
89+
mod bytes;
8990
mod choice;
9091
mod ct_option;
9192
mod traits;
9293

94+
pub use bytes::{BytesCtEq, BytesCtSelect};
9395
pub use choice::Choice;
9496
pub use ct_option::CtOption;
9597
pub use traits::{ct_eq::CtEq, ct_gt::CtGt, ct_lt::CtLt, ct_neg::CtNeg, ct_select::CtSelect};

ctutils/src/traits/ct_eq.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ pub trait CtEq<Rhs = Self>
1313
where
1414
Rhs: ?Sized,
1515
{
16-
/// Equality
16+
/// Determine if `self` is equal to `other` in constant-time.
1717
fn ct_eq(&self, other: &Rhs) -> Choice;
1818

19-
/// Inequality
19+
/// Determine if `self` is NOT equal to `other` in constant-time.
2020
fn ct_ne(&self, other: &Rhs) -> Choice {
2121
!self.ct_eq(other)
2222
}

0 commit comments

Comments
 (0)