From 635f3cde3ee788374a3b497a250689c815e35fd3 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Thu, 22 Oct 2026 17:25:57 -0600 Subject: [PATCH] cmov: add initial `cargo checkct` driver Adds an initial `cargo checkct` harness to check compiled release binaries for secret-dependent branches and memory accesses using BINSEC. Covers `cmovnz`, `cmovz`, `cmoveq`, and `cmovne` for `u32` and `u64` on x86-64, Thumb-1, Thumb-2, and RV32, with operands and conditions treated as secret. Also adds a CI job to run these checks. --- .github/workflows/cmov.yml | 9 ++ cmov/checkct/.cargo/config.toml | 19 +++++ cmov/checkct/Cargo.lock | 80 ++++++++++++++++++ cmov/checkct/Cargo.toml | 9 ++ cmov/checkct/checkct_macros/Cargo.toml | 14 ++++ cmov/checkct/checkct_macros/src/lib.rs | 91 ++++++++++++++++++++ cmov/checkct/driver/Cargo.toml | 13 +++ cmov/checkct/driver/src/driver.rs | 71 ++++++++++++++++ cmov/checkct/driver/src/main.rs | 24 ++++++ cmov/checkct/driver/src/rng.rs | 110 +++++++++++++++++++++++++ cmov/checkct/rust-toolchain.toml | 5 ++ 11 files changed, 445 insertions(+) create mode 100644 cmov/checkct/.cargo/config.toml create mode 100644 cmov/checkct/Cargo.lock create mode 100644 cmov/checkct/Cargo.toml create mode 100644 cmov/checkct/checkct_macros/Cargo.toml create mode 100644 cmov/checkct/checkct_macros/src/lib.rs create mode 100644 cmov/checkct/driver/Cargo.toml create mode 100644 cmov/checkct/driver/src/driver.rs create mode 100644 cmov/checkct/driver/src/main.rs create mode 100644 cmov/checkct/driver/src/rng.rs create mode 100644 cmov/checkct/rust-toolchain.toml diff --git a/.github/workflows/cmov.yml b/.github/workflows/cmov.yml index 53f3f6b2..ddca6dc2 100644 --- a/.github/workflows/cmov.yml +++ b/.github/workflows/cmov.yml @@ -45,6 +45,15 @@ jobs: - run: cargo build --target ${{ matrix.target }} --all-features - run: cargo build --target ${{ matrix.target }} --all-features --release + checkct: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: Ledger-Donjon/cargo-checkct@85271e5ce9eab9ba895c62c8b5d4129810a635a0 + with: + directory: cmov + timeout: 120 + minimal-versions: uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master with: diff --git a/cmov/checkct/.cargo/config.toml b/cmov/checkct/.cargo/config.toml new file mode 100644 index 00000000..aeb3543b --- /dev/null +++ b/cmov/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/cmov/checkct/Cargo.lock b/cmov/checkct/Cargo.lock new file mode 100644 index 00000000..fac6c2a1 --- /dev/null +++ b/cmov/checkct/Cargo.lock @@ -0,0 +1,80 @@ +# 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 = "driver" +version = "0.0.0" +dependencies = [ + "checkct_macros", + "cmov", + "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/cmov/checkct/Cargo.toml b/cmov/checkct/Cargo.toml new file mode 100644 index 00000000..b98258d2 --- /dev/null +++ b/cmov/checkct/Cargo.toml @@ -0,0 +1,9 @@ +cargo-features = ["panic-immediate-abort"] + +[workspace] +members = ["driver"] +resolver = "2" + +[profile.release] +debug = true +panic = "immediate-abort" diff --git a/cmov/checkct/checkct_macros/Cargo.toml b/cmov/checkct/checkct_macros/Cargo.toml new file mode 100644 index 00000000..559aaf47 --- /dev/null +++ b/cmov/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/cmov/checkct/checkct_macros/src/lib.rs b/cmov/checkct/checkct_macros/src/lib.rs new file mode 100644 index 00000000..2051302e --- /dev/null +++ b/cmov/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/cmov/checkct/driver/Cargo.toml b/cmov/checkct/driver/Cargo.toml new file mode 100644 index 00000000..fa9d6cd1 --- /dev/null +++ b/cmov/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" } +cmov = { 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/cmov/checkct/driver/src/driver.rs b/cmov/checkct/driver/src/driver.rs new file mode 100644 index 00000000..e0fca4f9 --- /dev/null +++ b/cmov/checkct/driver/src/driver.rs @@ -0,0 +1,71 @@ +//! Initial constant-time checks for the u32 and u64 primitives. +//! +//! Operands, conditions, and initial outputs are secret. +//! Keep the result live without asserting a predicate on a secret result. +use crate::rng::{PrivateRng, PublicRng}; +use checkct_macros::checkct; +use cmov::{Cmov, CmovEq}; +use core::hint::black_box; + +#[inline] +fn private_byte() -> u8 { + // The generated BINSEC script requires both RNG symbols, even though these + // checks only need secret inputs. Keep the public hook linked as well. + black_box(PublicRng.next_u32()); + let mut bytes = [0u8; 1]; + PrivateRng.fill_bytes(&mut bytes); + bytes[0] +} + +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) => { + mod $module { + use super::*; + + #[checkct] + pub fn cmovnz() { + let mut a = private_integer!($ty); + let b = private_integer!($ty); + a.cmovnz(&b, private_byte()); + black_box(a); + } + + #[checkct] + pub fn cmovz() { + let mut a = private_integer!($ty); + let b = private_integer!($ty); + a.cmovz(&b, private_byte()); + black_box(a); + } + + #[checkct] + pub fn cmoveq() { + let a = private_integer!($ty); + let b = private_integer!($ty); + let mut output = private_byte(); + a.cmoveq(&b, private_byte(), &mut output); + black_box(output); + } + + #[checkct] + pub fn cmovne() { + let a = private_integer!($ty); + let b = private_integer!($ty); + let mut output = private_byte(); + a.cmovne(&b, private_byte(), &mut output); + black_box(output); + } + } + }; +} + +scalar_checks!(u32_ops, u32); +scalar_checks!(u64_ops, u64); diff --git a/cmov/checkct/driver/src/main.rs b/cmov/checkct/driver/src/main.rs new file mode 100644 index 00000000..b1b07dd7 --- /dev/null +++ b/cmov/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/cmov/checkct/driver/src/rng.rs b/cmov/checkct/driver/src/rng.rs new file mode 100644 index 00000000..2af279c9 --- /dev/null +++ b/cmov/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/cmov/checkct/rust-toolchain.toml b/cmov/checkct/rust-toolchain.toml new file mode 100644 index 00000000..3e71075b --- /dev/null +++ b/cmov/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"]