diff --git a/.github/workflows/ctutils.yml b/.github/workflows/ctutils.yml index 997b3a07..cc8633dc 100644 --- a/.github/workflows/ctutils.yml +++ b/.github/workflows/ctutils.yml @@ -48,6 +48,15 @@ jobs: - run: cargo build --target ${{ matrix.target }} - run: cargo build --target ${{ matrix.target }} --features alloc + checkct: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: Ledger-Donjon/cargo-checkct@85271e5ce9eab9ba895c62c8b5d4129810a635a0 + with: + directory: ctutils + timeout: 120 + minimal-versions: uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master with: diff --git a/ctutils/checkct/.cargo/config.toml b/ctutils/checkct/.cargo/config.toml new file mode 100644 index 00000000..aeb3543b --- /dev/null +++ b/ctutils/checkct/.cargo/config.toml @@ -0,0 +1,19 @@ +[build] +target = [ + "riscv32imac-unknown-none-elf", + "thumbv6m-none-eabi", + "thumbv7em-none-eabihf", + "x86_64-unknown-linux-gnu", +] + +[target.x86_64-unknown-linux-gnu] +rustflags = [ + "-C", "linker=rust-lld", + "-C", "linker-flavor=ld.lld", + "-C", "relocation-model=static", +] + +[unstable] +unstable-options = true +build-std = ["core", "panic_abort"] +build-std-features = ["compiler-builtins-mem"] diff --git a/ctutils/checkct/Cargo.lock b/ctutils/checkct/Cargo.lock new file mode 100644 index 00000000..fc3ab105 --- /dev/null +++ b/ctutils/checkct/Cargo.lock @@ -0,0 +1,87 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "checkct_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cmov" +version = "0.5.4" + +[[package]] +name = "ctutils" +version = "0.4.3" +dependencies = [ + "cmov", +] + +[[package]] +name = "driver" +version = "0.0.0" +dependencies = [ + "checkct_macros", + "ctutils", + "rand_core 0.10.1", + "rand_core 0.6.4", + "rand_core 0.9.5", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" diff --git a/ctutils/checkct/Cargo.toml b/ctutils/checkct/Cargo.toml new file mode 100644 index 00000000..d9436b8a --- /dev/null +++ b/ctutils/checkct/Cargo.toml @@ -0,0 +1,12 @@ +cargo-features = ["panic-immediate-abort"] + +[workspace] +members = ["driver"] +resolver = "2" + +[profile.release] +debug = true +panic = "immediate-abort" + +[patch.crates-io] +cmov = { path = "../../cmov" } diff --git a/ctutils/checkct/checkct_macros/Cargo.toml b/ctutils/checkct/checkct_macros/Cargo.toml new file mode 100644 index 00000000..559aaf47 --- /dev/null +++ b/ctutils/checkct/checkct_macros/Cargo.toml @@ -0,0 +1,14 @@ +#----- AUTOGENERATED BY CARGO-CHECKCT: DO NOT MODIFY ----- +[package] +name = "checkct_macros" +version = "0.1.0" +publish = false +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.86" +quote = "1.0.37" +syn = { version = "2.0.77", features = ["full"] } diff --git a/ctutils/checkct/checkct_macros/src/lib.rs b/ctutils/checkct/checkct_macros/src/lib.rs new file mode 100644 index 00000000..2051302e --- /dev/null +++ b/ctutils/checkct/checkct_macros/src/lib.rs @@ -0,0 +1,91 @@ +use proc_macro::TokenStream; +use proc_macro2::{Literal, Span}; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, Error, Ident, ItemFn, LitStr, Result, Token, +}; + +struct AttributeArgs { + descriptor_link_section: Option, +} + +impl Parse for AttributeArgs { + fn parse(input: ParseStream) -> Result { + if input.is_empty() { + return Ok(Self { + descriptor_link_section: None, + }); + } + + let arg_name: Ident = input.parse()?; + if arg_name != "descriptor_link_section" { + return Err(Error::new( + arg_name.span(), + "expected `descriptor_link_section`", + )); + } + input.parse::()?; + let descriptor_link_section: LitStr = input.parse()?; + + Ok(Self { + descriptor_link_section: Some(descriptor_link_section.value()), + }) + } +} + +#[proc_macro_attribute] +pub fn checkct(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as AttributeArgs); + // By default the descriptor will be stored in the .note.checkct section + let descriptor_link_section = Literal::string( + args.descriptor_link_section + .as_deref() + .unwrap_or(".note.checkct"), + ); + + let item_fn = parse_macro_input!(input as ItemFn); + let name = &item_fn.sig.ident; + + let entrypoint_descriptor_name = Ident::new( + &format!("__checkct_entrypoint_descriptor__{name}"), + Span::call_site(), + ); + let entrypoint_descriptor_type = Ident::new( + &format!("__CheckctEntrypointDescriptor__{name}"), + Span::call_site(), + ); + + quote! { + // The descriptor is laid out as an ELF note (which binsec parses), since it is stored + // in a note section by default. It ends with the address of the entrypoint. + #[doc(hidden)] + #[allow(non_camel_case_types, dead_code)] + #[repr(C, packed(4))] + pub struct #entrypoint_descriptor_type { + namesz: u32, + descsz: u32, + kind: u32, + name: [u8; 8], + entrypoint: fn(), + } + + // This variable is used to prevent the compiler from removing the code of the entrypoint + // function if it is unused. Moreover this variable name has a special format which allows + // cargo-checkct to find it. + // Make sure that the descriptor link section is kept by the linker. + #[unsafe(link_section = #descriptor_link_section)] + #[used] + pub static #entrypoint_descriptor_name: #entrypoint_descriptor_type = + #entrypoint_descriptor_type { + namesz: 8, + descsz: ::core::mem::size_of::() as u32, + kind: 1, + name: *b"checkct\0", + entrypoint: #name, + }; + + #item_fn + } + .into() +} diff --git a/ctutils/checkct/driver/Cargo.toml b/ctutils/checkct/driver/Cargo.toml new file mode 100644 index 00000000..7532ab03 --- /dev/null +++ b/ctutils/checkct/driver/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "driver" +version = "0.0.0" +publish = false +edition = "2024" + +[dependencies] +checkct_macros = { path = "../checkct_macros" } +ctutils = { path = "../.." } +# RNG traits supplied by cargo-checkct's generated rng.rs. +rand_core = "0.10" +rand_core_09 = { package = "rand_core", version = "0.9" } +rand_core_06 = { package = "rand_core", version = "0.6" } diff --git a/ctutils/checkct/driver/src/driver.rs b/ctutils/checkct/driver/src/driver.rs new file mode 100644 index 00000000..6a507fb9 --- /dev/null +++ b/ctutils/checkct/driver/src/driver.rs @@ -0,0 +1,74 @@ +use crate::rng::{PrivateRng, PublicRng}; +use checkct_macros::checkct; +use core::hint::black_box; +use ctutils::{Choice, CtSelect}; + +macro_rules! private_integer { + ($ty:ty) => {{ + let mut bytes = [0u8; size_of::<$ty>()]; + PrivateRng.fill_bytes(&mut bytes); + <$ty>::from_le_bytes(bytes) + }}; +} + +macro_rules! scalar_checks { + ($module:ident, $ty:ty, $from_lsb:ident, $from_lt:ident, $select:ident) => { + mod $module { + use super::*; + + fn inputs() -> ($ty, $ty, $ty) { + // Keep both RNG hooks linked: the BINSEC script uses both. + black_box(PublicRng.next_u32()); + ( + private_integer!($ty), + private_integer!($ty), + private_integer!($ty), + ) + } + + #[checkct] + pub fn choice_select() { + let (a, b, condition) = inputs(); + black_box(select(a, b, condition)); + } + + #[inline(never)] + fn select(a: $ty, b: $ty, condition: $ty) -> $ty { + Choice::$from_lsb(condition).$select(a, b) + } + + /// A single shift-and-subtract division step, as used in crypto-bigint. + /// Keep the comparison and selection together to test their optimization. + #[checkct] + pub fn choice_select_after_lt() { + let (a, b, _) = inputs(); + black_box(subtract_if_ge(a, b)); + } + + #[inline(never)] + fn subtract_if_ge(a: $ty, b: $ty) -> $ty { + let choice = Choice::$from_lt(a, b); + choice.$select(a.wrapping_sub(b), a) + } + + /// Compare the const helper with the cmov-backed trait on the same expression. + #[checkct] + pub fn ct_select_after_lt() { + let (a, b, _) = inputs(); + black_box(subtract_if_ge_with_ct_select(a, b)); + } + + #[inline(never)] + fn subtract_if_ge_with_ct_select(a: $ty, b: $ty) -> $ty { + let choice = Choice::$from_lt(a, b); + a.wrapping_sub(b).ct_select(&a, choice) + } + } + }; +} + +scalar_checks!(u8_ops, u8, from_u8_lsb, from_u8_lt, select_u8); +scalar_checks!(u16_ops, u16, from_u16_lsb, from_u16_lt, select_u16); +scalar_checks!(u32_ops, u32, from_u32_lsb, from_u32_lt, select_u32); +scalar_checks!(u64_ops, u64, from_u64_lsb, from_u64_lt, select_u64); +// TODO: scalar_checks!(u128_ops, u128, from_u128_lsb, from_u128_lt, select_u128); diff --git a/ctutils/checkct/driver/src/main.rs b/ctutils/checkct/driver/src/main.rs new file mode 100644 index 00000000..b1b07dd7 --- /dev/null +++ b/ctutils/checkct/driver/src/main.rs @@ -0,0 +1,24 @@ +//----- AUTOGENERATED BY CARGO-CHECKCT: DO NOT MODIFY ----- +// +#![no_std] +#![no_main] + +mod driver; +mod rng; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + panic!() +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + __checkct_panic() +} + +/// The analysis of a path stops when it reaches this function. +#[unsafe(no_mangle)] +#[inline(never)] +pub extern "C" fn __checkct_panic() -> ! { + loop {} +} diff --git a/ctutils/checkct/driver/src/rng.rs b/ctutils/checkct/driver/src/rng.rs new file mode 100644 index 00000000..2af279c9 --- /dev/null +++ b/ctutils/checkct/driver/src/rng.rs @@ -0,0 +1,110 @@ +//----- AUTOGENERATED BY CARGO-CHECKCT: DO NOT MODIFY ----- +// +//! `PrivateRng` produces secret values, and `PublicRng` produces public values. +//! Both provide `next_u32`, `next_u64` and `fill_bytes` methods, and implement +//! the traits of rand_core 0.10, 0.9 and 0.6. +#![allow(dead_code)] + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn __checkct_private_rand() -> u8 { + unsafe { core::ptr::read_volatile(0xcafe as *const u8) } +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn __checkct_public_rand() -> u8 { + unsafe { core::ptr::read_volatile(0xf00d as *const u8) } +} + +#[allow(unused_imports)] +pub use rand_core::{CryptoRng, Rng}; +// Drivers generated by older versions of cargo-checkct import `RngCore` +#[allow(unused_imports)] +pub use rand_core_06::RngCore; + +pub struct PrivateRng; +pub struct PublicRng; + +macro_rules! impl_rng { + ($rng:ident, $rand:ident) => { + impl $rng { + pub const fn new() -> Self { + Self + } + + pub fn next_u32(&mut self) -> u32 { + u32::from_be_bytes([$rand(), $rand(), $rand(), $rand()]) + } + + pub fn next_u64(&mut self) -> u64 { + (self.next_u32() as u64) << 32 | (self.next_u32() as u64) + } + + pub fn fill_bytes(&mut self, dest: &mut [u8]) { + for d in dest { + *d = $rand(); + } + } + } + + impl rand_core::TryRng for $rng { + type Error = core::convert::Infallible; + + fn try_next_u32(&mut self) -> Result { + Ok($rng::next_u32(self)) + } + + fn try_next_u64(&mut self) -> Result { + Ok($rng::next_u64(self)) + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> { + $rng::fill_bytes(self, dest); + Ok(()) + } + } + + impl rand_core::TryCryptoRng for $rng {} + + impl rand_core_09::RngCore for $rng { + fn next_u32(&mut self) -> u32 { + $rng::next_u32(self) + } + + fn next_u64(&mut self) -> u64 { + $rng::next_u64(self) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + $rng::fill_bytes(self, dest) + } + } + + impl rand_core_09::CryptoRng for $rng {} + + impl rand_core_06::RngCore for $rng { + fn next_u32(&mut self) -> u32 { + $rng::next_u32(self) + } + + fn next_u64(&mut self) -> u64 { + $rng::next_u64(self) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + $rng::fill_bytes(self, dest) + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core_06::Error> { + $rng::fill_bytes(self, dest); + Ok(()) + } + } + + impl rand_core_06::CryptoRng for $rng {} + }; +} + +impl_rng!(PrivateRng, __checkct_private_rand); +impl_rng!(PublicRng, __checkct_public_rand); diff --git a/ctutils/checkct/rust-toolchain.toml b/ctutils/checkct/rust-toolchain.toml new file mode 100644 index 00000000..3e71075b --- /dev/null +++ b/ctutils/checkct/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# Pinned so that driver builds are reproducible, since they rely on the unstable `-Zbuild-std` +channel = "nightly-2026-10-06" +profile = "minimal" +components = ["rust-src"] diff --git a/ctutils/src/choice.rs b/ctutils/src/choice.rs index d6a8415b..b858e438 100644 --- a/ctutils/src/choice.rs +++ b/ctutils/src/choice.rs @@ -429,7 +429,7 @@ impl Choice { #[inline] #[must_use] pub const fn to_u8_mask(self) -> u8 { - self.0.wrapping_neg() + self.to_u8().wrapping_neg() } /// Create a `u16` bitmask. @@ -440,7 +440,7 @@ impl Choice { #[inline] #[must_use] pub const fn to_u16_mask(self) -> u16 { - (self.0 as u16).wrapping_neg() + (self.to_u8() as u16).wrapping_neg() } /// Create a `u32` bitmask. @@ -451,7 +451,7 @@ impl Choice { #[inline] #[must_use] pub const fn to_u32_mask(self) -> u32 { - (self.0 as u32).wrapping_neg() + (self.to_u8() as u32).wrapping_neg() } /// Create a `u64` bitmask. @@ -462,7 +462,7 @@ impl Choice { #[inline] #[must_use] pub const fn to_u64_mask(self) -> u64 { - (self.0 as u64).wrapping_neg() + (self.to_u8() as u64).wrapping_neg() } /// Create a `u128` bitmask. @@ -473,7 +473,7 @@ impl Choice { #[inline] #[must_use] pub const fn to_u128_mask(self) -> u128 { - (self.0 as u128).wrapping_neg() + (self.to_u8() as u128).wrapping_neg() } }