diff --git a/.github/workflows/checkct.yml b/.github/workflows/checkct.yml new file mode 100644 index 00000000..fec4e4c8 --- /dev/null +++ b/.github/workflows/checkct.yml @@ -0,0 +1,25 @@ +name: cargo-checkct + +on: + pull_request: + paths-ignore: + - README.md + push: + branches: + - master + paths-ignore: + - README.md + +permissions: + contents: read + +jobs: + checkct: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: Ledger-Donjon/cargo-checkct@85271e5ce9eab9ba895c62c8b5d4129810a635a0 + with: + directory: . + timeout: 120 + checks: control-flow,memory-access diff --git a/Cargo.toml b/Cargo.toml index 99d3937b..956ed484 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["rsa", "encryption", "security", "crypto"] categories = ["cryptography"] readme = "README.md" rust-version = "1.87" -exclude = ["marvin_toolkit/", "thirdparty/"] +exclude = ["checkct/", "marvin_toolkit/", "thirdparty/"] [dependencies] const-oid = { version = "0.10", default-features = false } diff --git a/checkct/.cargo/config.toml b/checkct/.cargo/config.toml new file mode 100644 index 00000000..28ffcac0 --- /dev/null +++ b/checkct/.cargo/config.toml @@ -0,0 +1,19 @@ +[build] +# NOTE: can't test on Thumb-1 due to use of `Arc` +target = [ + "riscv32imac-unknown-none-elf", + "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", "alloc", "panic_abort"] +build-std-features = ["compiler-builtins-mem"] diff --git a/checkct/.gitignore b/checkct/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/checkct/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/checkct/Cargo.lock b/checkct/Cargo.lock new file mode 100644 index 00000000..9b5ef0f3 --- /dev/null +++ b/checkct/Cargo.lock @@ -0,0 +1,263 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "checkct_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "crypto-bigint" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb08299a6046833a879ace115697f12fd2bd6c8d3e93c909ffc9e0738e7cae0" +dependencies = [ + "cpubits", + "ctutils", + "num-traits", + "rand_core 0.10.1", + "serdect", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "crypto-primes" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" +dependencies = [ + "crypto-bigint", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" +dependencies = [ + "cmov", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "const-oid", + "crypto-common", +] + +[[package]] +name = "driver" +version = "0.0.0" +dependencies = [ + "checkct_macros", + "crypto-bigint", + "rand_core 0.10.1", + "rand_core 0.6.4", + "rand_core 0.9.5", + "rsa", + "zeroize", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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 = "rsa" +version = "0.10.0-rc.19" +dependencies = [ + "const-oid", + "crypto-bigint", + "crypto-primes", + "digest", + "rand_core 0.10.1", + "signature", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest", + "rand_core 0.10.1", +] + +[[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 = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" diff --git a/checkct/Cargo.toml b/checkct/Cargo.toml new file mode 100644 index 00000000..2c89b753 --- /dev/null +++ b/checkct/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +members = ["driver"] +resolver = "2" + +[profile.release] +debug = true +panic = "abort" diff --git a/checkct/checkct_macros/Cargo.toml b/checkct/checkct_macros/Cargo.toml new file mode 100644 index 00000000..559aaf47 --- /dev/null +++ b/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/checkct/checkct_macros/src/lib.rs b/checkct/checkct_macros/src/lib.rs new file mode 100644 index 00000000..2051302e --- /dev/null +++ b/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/checkct/driver/Cargo.toml b/checkct/driver/Cargo.toml new file mode 100644 index 00000000..00437a00 --- /dev/null +++ b/checkct/driver/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "driver" +version = "0.0.0" +publish = false +edition = "2024" + +[dependencies] +checkct_macros = { path = "../checkct_macros" } +rsa = { path = "../..", default-features = false } +crypto-bigint = { version = "0.7", default-features = false, features = ["alloc", "zeroize"] } +zeroize = { version = "1.8", default-features = false, features = ["alloc"] } +# 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/checkct/driver/src/allocator.rs b/checkct/driver/src/allocator.rs new file mode 100644 index 00000000..4380dfca --- /dev/null +++ b/checkct/driver/src/allocator.rs @@ -0,0 +1,55 @@ +//! Single-threaded bump allocator for these short, independent analyses. + +use core::alloc::{GlobalAlloc, Layout}; +use core::ptr::{addr_of_mut, null_mut}; + +const HEAP_SIZE: usize = 4096; + +#[repr(align(64))] +struct Heap([u8; HEAP_SIZE]); + +static mut HEAP: Heap = Heap([0; HEAP_SIZE]); +static mut NEXT: usize = 0; + +struct Allocator; + +#[global_allocator] +static ALLOCATOR: Allocator = Allocator; + +// SAFETY: Each analysis is single-threaded. Allocations are disjoint, aligned, +// and remain valid until reset, which is called before any allocation. +unsafe impl GlobalAlloc for Allocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + if layout.align() > align_of::() { + return null_mut(); + } + + // SAFETY: Only the current single-threaded analysis accesses NEXT. + let start = unsafe { (NEXT + layout.align() - 1) & !(layout.align() - 1) }; + let Some(end) = start.checked_add(layout.size()) else { + return null_mut(); + }; + if end > HEAP_SIZE { + return null_mut(); + } + + // SAFETY: The allocation is aligned and within HEAP; advancing NEXT + // prevents overlap with earlier allocations. + unsafe { + NEXT = end; + addr_of_mut!(HEAP.0).cast::().add(start) + } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} + +/// Initialize the cursor explicitly: BINSEC starts at each checkct entrypoint, +/// without running startup code or initializing .bss. +/// +/// # Safety +/// No allocation may be live, and no other thread may access the allocator. +pub unsafe fn reset() { + // SAFETY: Guaranteed by the caller. + unsafe { NEXT = 0 }; +} diff --git a/checkct/driver/src/driver.rs b/checkct/driver/src/driver.rs new file mode 100644 index 00000000..20ec2b42 --- /dev/null +++ b/checkct/driver/src/driver.rs @@ -0,0 +1,41 @@ +//! Small fixed-width checks for I2OSP, including zeroization on drop. + +use crate::rng::{PrivateRng, PublicRng}; +use checkct_macros::checkct; +use core::hint::black_box; +use crypto_bigint::BoxedUint; + +fn i2osp() { + // SAFETY: This is the start of an independent, single-threaded analysis; + // no allocations are live yet. + unsafe { crate::allocator::reset() }; + + // The generated BINSEC script requires both RNG symbols. + black_box(PublicRng.next_u32()); + let mut bytes = [0u8; INPUT_LEN]; + PrivateRng.fill_bytes(&mut bytes); + + // Storage precision and both byte lengths are public and fixed. Leading + // zero bytes in the secret input remain unconstrained. + let input = black_box(BoxedUint::from_be_slice(&bytes, 64).unwrap()); + let output = crate::pad::i2osp_zeroizing(input, OUTPUT_LEN); + black_box(output.as_slice()); + drop(output); +} + +#[checkct] +pub fn i2osp_zeroizing_equal_width() { + i2osp::<8, 8>(); +} + +#[checkct] +pub fn i2osp_zeroizing_left_pad() { + i2osp::<8, 16>(); +} + +#[checkct] +pub fn i2osp_zeroizing_truncate_storage() { + // Seven secret bytes fit in seven output bytes, while the input occupies + // eight storage bytes on every target. Only a known-zero byte is removed. + i2osp::<7, 7>(); +} diff --git a/checkct/driver/src/main.rs b/checkct/driver/src/main.rs new file mode 100644 index 00000000..5a889d9b --- /dev/null +++ b/checkct/driver/src/main.rs @@ -0,0 +1,32 @@ +#![no_std] +#![no_main] + +#[macro_use] +extern crate alloc; + +mod allocator; +mod driver; + +// Compile the actual private helper without adding a public API for the harness. +pub use rsa::errors; +#[allow(dead_code)] +#[path = "../../../src/algorithms/pad.rs"] +mod pad; +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/checkct/driver/src/rng.rs b/checkct/driver/src/rng.rs new file mode 100644 index 00000000..2af279c9 --- /dev/null +++ b/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/checkct/rust-toolchain.toml b/checkct/rust-toolchain.toml new file mode 100644 index 00000000..3e71075b --- /dev/null +++ b/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"]