diff --git a/.github/workflows/ctutils.yml b/.github/workflows/ctutils.yml index cc8633dc..5789ebbe 100644 --- a/.github/workflows/ctutils.yml +++ b/.github/workflows/ctutils.yml @@ -50,6 +50,11 @@ jobs: checkct: runs-on: ubuntu-latest + strategy: + matrix: + overflow-checks: ["false", "true"] + env: + CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS: ${{ matrix.overflow-checks }} steps: - uses: actions/checkout@v7 - uses: Ledger-Donjon/cargo-checkct@85271e5ce9eab9ba895c62c8b5d4129810a635a0 @@ -80,6 +85,9 @@ jobs: - run: cargo test --features subtle - run: cargo test --all-features - run: cargo test --all-features --release + - run: cargo test --all-features --release + env: + CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS: true # Test using `cargo careful` test-careful: diff --git a/ctutils/checkct/Cargo.lock b/ctutils/checkct/Cargo.lock index 36956081..c1d32636 100644 --- a/ctutils/checkct/Cargo.lock +++ b/ctutils/checkct/Cargo.lock @@ -17,7 +17,7 @@ version = "0.5.4" [[package]] name = "ctutils" -version = "0.4.4" +version = "0.4.6" dependencies = [ "cmov", ] diff --git a/ctutils/checkct/driver/src/driver.rs b/ctutils/checkct/driver/src/driver.rs index ca2a809d..b02d1393 100644 --- a/ctutils/checkct/driver/src/driver.rs +++ b/ctutils/checkct/driver/src/driver.rs @@ -11,6 +11,9 @@ macro_rules! private_integer { }}; } +mod comparisons; +mod composition; +mod negation; mod predicates; mod primitives; @@ -56,7 +59,7 @@ macro_rules! scalar_checks { /// Compare the const helper with the cmov-backed trait on the same expression. #[checkct] - pub fn ct_select_after_lt() { + pub fn ct_select_after_choice_lt() { let (a, b, _) = inputs(); black_box(select_min_with_ct_select(a, b)); } diff --git a/ctutils/checkct/driver/src/driver/comparisons.rs b/ctutils/checkct/driver/src/driver/comparisons.rs new file mode 100644 index 00000000..ec92530b --- /dev/null +++ b/ctutils/checkct/driver/src/driver/comparisons.rs @@ -0,0 +1,112 @@ +//! Comparison operations. + +use super::*; +use ctutils::{CtGt, CtLt}; + +macro_rules! comparison_checks { + ($module:ident, $ty:ty) => { + mod $module { + use super::*; + + fn inputs() -> ($ty, $ty) { + // Keep both RNG hooks linked: the BINSEC script uses both. + black_box(PublicRng.next_u32()); + (private_integer!($ty), private_integer!($ty)) + } + + #[checkct] + pub fn ct_lt() { + let (a, b) = inputs(); + black_box(a.ct_lt(&b).to_u8()); + } + + #[checkct] + pub fn ct_gt() { + let (a, b) = inputs(); + black_box(a.ct_gt(&b).to_u8()); + } + + #[checkct] + pub fn ct_select_after_lt() { + let (a, b) = inputs(); + black_box(b.ct_select(&a, a.ct_lt(&b))); + } + + #[checkct] + pub fn ct_select_after_gt() { + let (a, b) = inputs(); + black_box(b.ct_select(&a, a.ct_gt(&b))); + } + + #[checkct] + pub fn ct_select_independent_after_lt() { + let (a, b) = inputs(); + let (x, y) = inputs(); + black_box(x.ct_select(&y, a.ct_lt(&b))); + } + + #[checkct] + pub fn ct_select_independent_after_gt() { + let (a, b) = inputs(); + let (x, y) = inputs(); + black_box(x.ct_select(&y, a.ct_gt(&b))); + } + + #[checkct] + pub fn ct_select_after_lt_constant() { + let (value, _) = inputs(); + let value = value & 63; + black_box(value.ct_select(&2, (2 as $ty).ct_lt(&value))); + } + + #[checkct] + pub fn ct_select_after_gt_constant() { + let (value, _) = inputs(); + let value = value & 63; + black_box(value.ct_select(&2, (2 as $ty).ct_gt(&value))); + } + } + }; +} + +comparison_checks!(u8_ops, u8); +comparison_checks!(u16_ops, u16); +comparison_checks!(u32_ops, u32); +comparison_checks!(u64_ops, u64); +comparison_checks!(u128_ops, u128); +comparison_checks!(usize_ops, usize); + +macro_rules! constructor_checks { + ($module:ident, $ty:ty, $from_lt:ident) => { + mod $module { + use super::*; + + fn private() -> $ty { + black_box(PublicRng.next_u32()); + private_integer!($ty) + } + + #[checkct] + pub fn constant_lhs() { + black_box(Choice::$from_lt(2, private() & 63).to_u8()); + } + + #[checkct] + pub fn constant_rhs() { + black_box(Choice::$from_lt(private() & 63, 2).to_u8()); + } + + #[checkct] + pub fn select_constant() { + let value = private() & 63; + black_box(value.ct_select(&2, Choice::$from_lt(2, value))); + } + } + }; +} + +constructor_checks!(u8_choice, u8, from_u8_lt); +constructor_checks!(u16_choice, u16, from_u16_lt); +constructor_checks!(u32_choice, u32, from_u32_lt); +constructor_checks!(u64_choice, u64, from_u64_lt); +constructor_checks!(u128_choice, u128, from_u128_lt); diff --git a/ctutils/checkct/driver/src/driver/composition.rs b/ctutils/checkct/driver/src/driver/composition.rs new file mode 100644 index 00000000..25a7a9e4 --- /dev/null +++ b/ctutils/checkct/driver/src/driver/composition.rs @@ -0,0 +1,127 @@ +//! Scalar predicates composed with selection, containers, and wrappers. + +use super::*; +use ctutils::{CtEq, CtFind, CtGt, CtLookup, CtLt, CtOption}; + +fn condition() -> Choice { + Choice::from_u8_lsb(private_integer!(u8)) +} + +macro_rules! composition_checks { + ($module:ident, $ty:ty) => { + mod $module { + use super::*; + + fn private() -> $ty { + black_box(PublicRng.next_u32()); + private_integer!($ty) + } + + fn array() -> [$ty; 4] { + core::array::from_fn(|_| private()) + } + + fn option() -> CtOption<$ty> { + CtOption::new(private(), condition()) + } + + #[checkct] + pub fn equality_select() { + let (a, b) = (private(), private()); + black_box(a.ct_select(&b, a.ct_eq(&b))); + black_box(a.ct_ne(&b).to_u8()); + } + + #[checkct] + pub fn array_select() { + black_box(array().ct_select(&array(), condition())); + } + + #[checkct] + pub fn array_equality() { + black_box(array().ct_eq(&array()).to_u8()); + } + + #[checkct] + pub fn option_unwrap_or() { + black_box(option().unwrap_or(private())); + } + + #[checkct] + pub fn option_or() { + black_box(option().or(option()).unwrap_or(private())); + } + + #[checkct] + pub fn option_and_then() { + let other = option(); + black_box(option().and_then(|_| other).unwrap_or(private())); + } + + #[checkct] + pub fn option_equality() { + black_box(option().ct_eq(&option()).to_u8()); + } + + #[checkct] + pub fn option_select() { + let selected = option().ct_select(&option(), condition()); + black_box(selected.is_some().to_u8()); + black_box(selected.unwrap_or(private())); + } + + #[checkct] + pub fn lookup() { + // An unrestricted secret index includes every hit and out-of-range miss. + let index = private_integer!(usize); + let found = array().ct_lookup(index); + black_box(found.is_some().to_u8()); + black_box(found.unwrap_or(private())); + } + + #[checkct] + pub fn find() { + // Includes no match, multiple matches, and first/last matches. + let needle = private(); + let found = array().ct_find(|value| value.ct_eq(&needle)); + black_box(found.is_some().to_u8()); + black_box(found.unwrap_or(private())); + } + } + }; +} + +composition_checks!(u32_ops, u32); +composition_checks!(u64_ops, u64); + +macro_rules! nonzero_comparison_checks { + ($module:ident, $ty:ident, $int:ty) => { + mod $module { + use super::*; + + fn private() -> core::num::$ty { + black_box(PublicRng.next_u32()); + core::num::$ty::new(private_integer!($int) | 1).expect("low bit is set") + } + + #[checkct] + pub fn select_after_lt() { + let (a, b) = (private(), private()); + black_box(b.ct_select(&a, a.ct_lt(&b))); + } + + #[checkct] + pub fn select_after_gt() { + let (a, b) = (private(), private()); + black_box(b.ct_select(&a, a.ct_gt(&b))); + } + } + }; +} + +nonzero_comparison_checks!(nonzero_u8, NonZeroU8, u8); +nonzero_comparison_checks!(nonzero_u16, NonZeroU16, u16); +nonzero_comparison_checks!(nonzero_u32, NonZeroU32, u32); +nonzero_comparison_checks!(nonzero_u64, NonZeroU64, u64); +nonzero_comparison_checks!(nonzero_u128, NonZeroU128, u128); +nonzero_comparison_checks!(nonzero_usize, NonZeroUsize, usize); diff --git a/ctutils/checkct/driver/src/driver/negation.rs b/ctutils/checkct/driver/src/driver/negation.rs new file mode 100644 index 00000000..37de0953 --- /dev/null +++ b/ctutils/checkct/driver/src/driver/negation.rs @@ -0,0 +1,73 @@ +//! Negation operations. + +// Run with and without `CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS=true`. Secret inputs include MIN. +// A public false condition must not trigger a secret-dependent overflow check while eagerly +// computing the other operand. + +use super::*; +use ctutils::CtNeg; + +macro_rules! negation_checks { + ($module:ident, $ty:ty, $input:expr) => { + mod $module { + use super::*; + + fn inputs() -> ($ty, Choice) { + black_box(PublicRng.next_u32()); + ($input, Choice::from_u8_lsb(private_integer!(u8))) + } + + #[checkct] + pub fn ct_neg() { + let (value, choice) = inputs(); + black_box(value.ct_neg(choice)); + } + + #[checkct] + pub fn ct_neg_assign() { + let (mut value, choice) = inputs(); + value.ct_neg_assign(choice); + black_box(value); + } + + #[checkct] + pub fn ct_neg_false() { + let (value, _) = inputs(); + black_box(value.ct_neg(Choice::FALSE)); + } + + #[checkct] + pub fn ct_neg_assign_false() { + let (mut value, _) = inputs(); + value.ct_neg_assign(Choice::FALSE); + black_box(value); + } + } + }; +} + +negation_checks!(i8_ops, i8, private_integer!(i8)); +negation_checks!(i16_ops, i16, private_integer!(i16)); +negation_checks!(i32_ops, i32, private_integer!(i32)); +negation_checks!(i64_ops, i64, private_integer!(i64)); +negation_checks!(i128_ops, i128, private_integer!(i128)); +negation_checks!(isize_ops, isize, private_integer!(isize)); + +// Setting the sign bit guarantees NonZero's invariant without excluding MIN or introducing a +// secret-dependent branch in the harness. +macro_rules! nonzero_checks { + ($module:ident, $ty:ident, $int:ty) => { + negation_checks!( + $module, + core::num::$ty, + core::num::$ty::new(private_integer!($int) | <$int>::MIN).expect("sign bit is set") + ); + }; +} + +nonzero_checks!(nonzero_i8, NonZeroI8, i8); +nonzero_checks!(nonzero_i16, NonZeroI16, i16); +nonzero_checks!(nonzero_i32, NonZeroI32, i32); +nonzero_checks!(nonzero_i64, NonZeroI64, i64); +nonzero_checks!(nonzero_i128, NonZeroI128, i128); +nonzero_checks!(nonzero_isize, NonZeroIsize, isize); diff --git a/ctutils/src/choice.rs b/ctutils/src/choice.rs index 7b0be369..340ccef4 100644 --- a/ctutils/src/choice.rs +++ b/ctutils/src/choice.rs @@ -1,4 +1,4 @@ -use crate::primitives::{bitle, bitlt, bitnz}; +use crate::primitives::{bitle, bitnz}; use crate::{CtAssign, CtAssignSlice, CtEq, CtEqSlice, CtSelectUsingCtAssign}; use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; @@ -123,7 +123,15 @@ impl Choice { #[inline] #[must_use] pub const fn from_u8_lt(x: u8, y: u8) -> Self { - Self::from_u8_lsb(bitlt!(x, y, u8::BITS)) + // On Thumb-1, constant specialization can turn a narrow comparison into a branch. + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + { + Self::from_u32_lt(x as u32, y as u32) + } + #[cfg(not(all(target_arch = "arm", not(target_has_atomic = "ptr"))))] + { + Self::from_u8_lsb(crate::primitives::bitlt!(x, y, u8::BITS)) + } } /// Returns [`Choice::TRUE`] if `value != 0`, and [`Choice::FALSE`] otherwise. @@ -160,7 +168,15 @@ impl Choice { #[inline] #[must_use] pub const fn from_u16_lt(x: u16, y: u16) -> Self { - Self::from_u16_lsb(bitlt!(x, y, u16::BITS)) + // Use the same hardening as the word-sized comparison on Thumb-1. + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + { + Self::from_u32_lt(x as u32, y as u32) + } + #[cfg(not(all(target_arch = "arm", not(target_has_atomic = "ptr"))))] + { + Self::from_u16_lsb(crate::primitives::bitlt!(x, y, u16::BITS)) + } } /// Returns [`Choice::TRUE`] if `value != 0`, and [`Choice::FALSE`] otherwise. @@ -284,7 +300,7 @@ impl Choice { } #[cfg(not(target_pointer_width = "32"))] { - Self::from_u128_lsb(bitlt!(x, y, u128::BITS)) + Self::from_u128_lsb(crate::primitives::bitlt!(x, y, u128::BITS)) } } diff --git a/ctutils/src/primitives.rs b/ctutils/src/primitives.rs index ccc933cf..beb8febc 100644 --- a/ctutils/src/primitives.rs +++ b/ctutils/src/primitives.rs @@ -69,7 +69,7 @@ macro_rules! comparison_primitives { inline(always) )] pub(crate) const fn $lt(lhs: $ty, rhs: $ty) -> Choice { - let bit = bitlt!(lhs, rhs, <$ty>::BITS); + let bit = $crate::primitives::bitlt!(lhs, rhs, <$ty>::BITS); #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] let bit = black_box(bit); Choice::$from_lsb(bit) diff --git a/ctutils/src/traits/ct_gt.rs b/ctutils/src/traits/ct_gt.rs index 29d17960..bcad7154 100644 --- a/ctutils/src/traits/ct_gt.rs +++ b/ctutils/src/traits/ct_gt.rs @@ -1,4 +1,4 @@ -use crate::Choice; +use crate::{Choice, CtLt}; use core::{ cmp, num::{NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize}, @@ -11,29 +11,21 @@ pub trait CtGt { fn ct_gt(&self, other: &Self) -> Choice; } -// Impl `CtGt` using overflowing subtraction +// Share the target-specific comparison hardening with `CtLt`. macro_rules! impl_unsigned_ct_gt { ( $($uint:ty),+ ) => { $( impl CtGt for $uint { #[inline] fn ct_gt(&self, other: &Self) -> Choice { - let (_, overflow) = other.overflowing_sub(*self); - Choice(overflow.into()) + other.ct_lt(self) } } )+ }; } -impl_unsigned_ct_gt!(u8, u16, u32, u64, usize); - -impl CtGt for u128 { - #[inline] - fn ct_gt(&self, other: &Self) -> Choice { - Choice::from_u128_lt(*other, *self) - } -} +impl_unsigned_ct_gt!(u8, u16, u32, u64, u128, usize); /// Impl `CtGt` for `NonZero` by calling `NonZero::get`. macro_rules! impl_ct_gt_for_nonzero_integer { diff --git a/ctutils/src/traits/ct_lt.rs b/ctutils/src/traits/ct_lt.rs index 66c983eb..75380087 100644 --- a/ctutils/src/traits/ct_lt.rs +++ b/ctutils/src/traits/ct_lt.rs @@ -11,22 +11,58 @@ pub trait CtLt { fn ct_lt(&self, other: &Self) -> Choice; } -// Impl `CtLt` using overflowing subtraction macro_rules! impl_unsigned_ct_lt { - ( $($uint:ty),+ ) => { + ( $(($uint:ty, $from_lt:ident)),+ ) => { $( impl CtLt for $uint { #[inline] fn ct_lt(&self, other: &Self) -> Choice { - let (_, overflow) = self.overflowing_sub(*other); - Choice(overflow.into()) + // Thumb-1 can branch when converting an overflowing subtraction's borrow flag + // into a byte, before `Choice`'s barrier or selection is reached. + // Reuse the hardened constructors there. + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + { + Choice::$from_lt(*self, *other) + } + // Retain native subtraction on other targets. + #[cfg(not(all(target_arch = "arm", not(target_has_atomic = "ptr"))))] + { + let (_, overflow) = self.overflowing_sub(*other); + Choice(overflow.into()) + } } } )+ }; } -impl_unsigned_ct_lt!(u8, u16, u32, u64, usize); +impl_unsigned_ct_lt!((u8, from_u8_lt), (u16, from_u16_lt), (u32, from_u32_lt)); + +#[cfg(not(target_arch = "riscv32"))] +impl_unsigned_ct_lt!((u64, from_u64_lt)); + +#[cfg(target_arch = "riscv32")] +impl CtLt for u64 { + #[inline] + fn ct_lt(&self, other: &Self) -> Choice { + // Native wide comparison can short-circuit after caller specialization. + Choice::from_u64_lt(*self, *other) + } +} + +impl CtLt for usize { + #[inline] + #[allow(clippy::cast_possible_truncation, reason = "matches pointer width")] + fn ct_lt(&self, other: &Self) -> Choice { + #[cfg(target_pointer_width = "16")] + let (a, b) = (*self as u16, *other as u16); + #[cfg(target_pointer_width = "32")] + let (a, b) = (*self as u32, *other as u32); + #[cfg(target_pointer_width = "64")] + let (a, b) = (*self as u64, *other as u64); + a.ct_lt(&b) + } +} impl CtLt for u128 { #[inline] diff --git a/ctutils/src/traits/ct_neg.rs b/ctutils/src/traits/ct_neg.rs index e920cecb..019d721e 100644 --- a/ctutils/src/traits/ct_neg.rs +++ b/ctutils/src/traits/ct_neg.rs @@ -5,6 +5,9 @@ use core::num::{ }; /// Constant-time conditional negation: negates a value when `choice` is [`Choice::TRUE`]. +/// +/// Integer implementations use wrapping negation, including signed and non-zero integers. +/// Negating a signed integer's minimum value therefore returns that value. pub trait CtNeg: Sized { /// Conditionally negate `self`, returning `-self` if `choice` is [`Choice::TRUE`], or `self` /// otherwise. @@ -24,15 +27,13 @@ macro_rules! impl_signed_ct_neg { $( impl CtNeg for $int { #[inline] - #[allow(clippy::arithmetic_side_effects)] fn ct_neg(&self, choice: Choice) -> Self { - self.ct_select(&-*self, choice) + self.ct_select(&self.wrapping_neg(), choice) } #[inline] - #[allow(clippy::arithmetic_side_effects)] fn ct_neg_assign(&mut self, choice: Choice) { - self.ct_assign(&-*self, choice) + self.ct_assign(&self.wrapping_neg(), choice) } } )+ @@ -128,6 +129,17 @@ mod tests { x.ct_neg_assign(Choice::TRUE); assert_eq!(-n, x); } + + #[test] + fn ct_neg_min() { + let min = <$int>::MIN; + for choice in [Choice::FALSE, Choice::TRUE] { + assert_eq!(min.ct_neg(choice), min); + let mut value = min; + value.ct_neg_assign(choice); + assert_eq!(value, min); + } + } } )+ }; @@ -164,4 +176,31 @@ mod tests { signed_ct_neg_tests!(i8, i16, i32, i64, i128, isize); unsigned_ct_neg_tests!(u8, u16, u32, u64, u128, usize); + + macro_rules! nonzero_signed_ct_neg_tests { + ($(($test:ident, $ty:ty, $int:ty)),+ $(,)?) => { + $( + #[test] + fn $test() { + use crate::{Choice, CtNeg}; + let min = <$ty>::new(<$int>::MIN).expect("MIN is non-zero"); + for choice in [Choice::FALSE, Choice::TRUE] { + assert_eq!(min.ct_neg(choice), min); + let mut value = min; + value.ct_neg_assign(choice); + assert_eq!(value, min); + } + } + )+ + }; + } + + nonzero_signed_ct_neg_tests!( + (nonzero_i8_min, core::num::NonZeroI8, i8), + (nonzero_i16_min, core::num::NonZeroI16, i16), + (nonzero_i32_min, core::num::NonZeroI32, i32), + (nonzero_i64_min, core::num::NonZeroI64, i64), + (nonzero_i128_min, core::num::NonZeroI128, i128), + (nonzero_isize_min, core::num::NonZeroIsize, isize), + ); }