Skip to content

Commit b2a91f6

Browse files
authored
cmov: add initial cargo checkct driver (#1566)
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.
1 parent bd0015f commit b2a91f6

11 files changed

Lines changed: 445 additions & 0 deletions

File tree

‎.github/workflows/cmov.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ jobs:
4545
- run: cargo build --target ${{ matrix.target }} --all-features
4646
- run: cargo build --target ${{ matrix.target }} --all-features --release
4747

48+
checkct:
49+
runs-on: ubuntu-latest
50+
steps:
51+
- uses: actions/checkout@v7
52+
- uses: Ledger-Donjon/cargo-checkct@85271e5ce9eab9ba895c62c8b5d4129810a635a0
53+
with:
54+
directory: cmov
55+
timeout: 120
56+
4857
minimal-versions:
4958
uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master
5059
with:

‎cmov/checkct/.cargo/config.toml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[build]
2+
target = [
3+
"riscv32imac-unknown-none-elf",
4+
"thumbv6m-none-eabi",
5+
"thumbv7em-none-eabihf",
6+
"x86_64-unknown-linux-gnu",
7+
]
8+
9+
[target.x86_64-unknown-linux-gnu]
10+
rustflags = [
11+
"-C", "linker=rust-lld",
12+
"-C", "linker-flavor=ld.lld",
13+
"-C", "relocation-model=static",
14+
]
15+
16+
[unstable]
17+
unstable-options = true
18+
build-std = ["core", "panic_abort"]
19+
build-std-features = ["compiler-builtins-mem"]

‎cmov/checkct/Cargo.lock‎

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cmov/checkct/Cargo.toml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
cargo-features = ["panic-immediate-abort"]
2+
3+
[workspace]
4+
members = ["driver"]
5+
resolver = "2"
6+
7+
[profile.release]
8+
debug = true
9+
panic = "immediate-abort"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#----- AUTOGENERATED BY CARGO-CHECKCT: DO NOT MODIFY -----
2+
[package]
3+
name = "checkct_macros"
4+
version = "0.1.0"
5+
publish = false
6+
edition = "2021"
7+
8+
[lib]
9+
proc-macro = true
10+
11+
[dependencies]
12+
proc-macro2 = "1.0.86"
13+
quote = "1.0.37"
14+
syn = { version = "2.0.77", features = ["full"] }
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use proc_macro::TokenStream;
2+
use proc_macro2::{Literal, Span};
3+
use quote::quote;
4+
use syn::{
5+
parse::{Parse, ParseStream},
6+
parse_macro_input, Error, Ident, ItemFn, LitStr, Result, Token,
7+
};
8+
9+
struct AttributeArgs {
10+
descriptor_link_section: Option<String>,
11+
}
12+
13+
impl Parse for AttributeArgs {
14+
fn parse(input: ParseStream) -> Result<Self> {
15+
if input.is_empty() {
16+
return Ok(Self {
17+
descriptor_link_section: None,
18+
});
19+
}
20+
21+
let arg_name: Ident = input.parse()?;
22+
if arg_name != "descriptor_link_section" {
23+
return Err(Error::new(
24+
arg_name.span(),
25+
"expected `descriptor_link_section`",
26+
));
27+
}
28+
input.parse::<Token![=]>()?;
29+
let descriptor_link_section: LitStr = input.parse()?;
30+
31+
Ok(Self {
32+
descriptor_link_section: Some(descriptor_link_section.value()),
33+
})
34+
}
35+
}
36+
37+
#[proc_macro_attribute]
38+
pub fn checkct(args: TokenStream, input: TokenStream) -> TokenStream {
39+
let args = parse_macro_input!(args as AttributeArgs);
40+
// By default the descriptor will be stored in the .note.checkct section
41+
let descriptor_link_section = Literal::string(
42+
args.descriptor_link_section
43+
.as_deref()
44+
.unwrap_or(".note.checkct"),
45+
);
46+
47+
let item_fn = parse_macro_input!(input as ItemFn);
48+
let name = &item_fn.sig.ident;
49+
50+
let entrypoint_descriptor_name = Ident::new(
51+
&format!("__checkct_entrypoint_descriptor__{name}"),
52+
Span::call_site(),
53+
);
54+
let entrypoint_descriptor_type = Ident::new(
55+
&format!("__CheckctEntrypointDescriptor__{name}"),
56+
Span::call_site(),
57+
);
58+
59+
quote! {
60+
// The descriptor is laid out as an ELF note (which binsec parses), since it is stored
61+
// in a note section by default. It ends with the address of the entrypoint.
62+
#[doc(hidden)]
63+
#[allow(non_camel_case_types, dead_code)]
64+
#[repr(C, packed(4))]
65+
pub struct #entrypoint_descriptor_type {
66+
namesz: u32,
67+
descsz: u32,
68+
kind: u32,
69+
name: [u8; 8],
70+
entrypoint: fn(),
71+
}
72+
73+
// This variable is used to prevent the compiler from removing the code of the entrypoint
74+
// function if it is unused. Moreover this variable name has a special format which allows
75+
// cargo-checkct to find it.
76+
// Make sure that the descriptor link section is kept by the linker.
77+
#[unsafe(link_section = #descriptor_link_section)]
78+
#[used]
79+
pub static #entrypoint_descriptor_name: #entrypoint_descriptor_type =
80+
#entrypoint_descriptor_type {
81+
namesz: 8,
82+
descsz: ::core::mem::size_of::<fn()>() as u32,
83+
kind: 1,
84+
name: *b"checkct\0",
85+
entrypoint: #name,
86+
};
87+
88+
#item_fn
89+
}
90+
.into()
91+
}

‎cmov/checkct/driver/Cargo.toml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "driver"
3+
version = "0.0.0"
4+
publish = false
5+
edition = "2024"
6+
7+
[dependencies]
8+
checkct_macros = { path = "../checkct_macros" }
9+
cmov = { path = "../.." }
10+
# RNG traits supplied by cargo-checkct's generated rng.rs.
11+
rand_core = "0.10"
12+
rand_core_09 = { package = "rand_core", version = "0.9" }
13+
rand_core_06 = { package = "rand_core", version = "0.6" }

‎cmov/checkct/driver/src/driver.rs‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! Initial constant-time checks for the u32 and u64 primitives.
2+
//!
3+
//! Operands, conditions, and initial outputs are secret.
4+
//! Keep the result live without asserting a predicate on a secret result.
5+
use crate::rng::{PrivateRng, PublicRng};
6+
use checkct_macros::checkct;
7+
use cmov::{Cmov, CmovEq};
8+
use core::hint::black_box;
9+
10+
#[inline]
11+
fn private_byte() -> u8 {
12+
// The generated BINSEC script requires both RNG symbols, even though these
13+
// checks only need secret inputs. Keep the public hook linked as well.
14+
black_box(PublicRng.next_u32());
15+
let mut bytes = [0u8; 1];
16+
PrivateRng.fill_bytes(&mut bytes);
17+
bytes[0]
18+
}
19+
20+
macro_rules! private_integer {
21+
($ty:ty) => {{
22+
let mut bytes = [0u8; size_of::<$ty>()];
23+
PrivateRng.fill_bytes(&mut bytes);
24+
<$ty>::from_le_bytes(bytes)
25+
}};
26+
}
27+
28+
macro_rules! scalar_checks {
29+
($module:ident, $ty:ty) => {
30+
mod $module {
31+
use super::*;
32+
33+
#[checkct]
34+
pub fn cmovnz() {
35+
let mut a = private_integer!($ty);
36+
let b = private_integer!($ty);
37+
a.cmovnz(&b, private_byte());
38+
black_box(a);
39+
}
40+
41+
#[checkct]
42+
pub fn cmovz() {
43+
let mut a = private_integer!($ty);
44+
let b = private_integer!($ty);
45+
a.cmovz(&b, private_byte());
46+
black_box(a);
47+
}
48+
49+
#[checkct]
50+
pub fn cmoveq() {
51+
let a = private_integer!($ty);
52+
let b = private_integer!($ty);
53+
let mut output = private_byte();
54+
a.cmoveq(&b, private_byte(), &mut output);
55+
black_box(output);
56+
}
57+
58+
#[checkct]
59+
pub fn cmovne() {
60+
let a = private_integer!($ty);
61+
let b = private_integer!($ty);
62+
let mut output = private_byte();
63+
a.cmovne(&b, private_byte(), &mut output);
64+
black_box(output);
65+
}
66+
}
67+
};
68+
}
69+
70+
scalar_checks!(u32_ops, u32);
71+
scalar_checks!(u64_ops, u64);

‎cmov/checkct/driver/src/main.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//----- AUTOGENERATED BY CARGO-CHECKCT: DO NOT MODIFY -----
2+
//
3+
#![no_std]
4+
#![no_main]
5+
6+
mod driver;
7+
mod rng;
8+
9+
#[unsafe(no_mangle)]
10+
pub extern "C" fn _start() -> ! {
11+
panic!()
12+
}
13+
14+
#[panic_handler]
15+
fn panic(_info: &core::panic::PanicInfo) -> ! {
16+
__checkct_panic()
17+
}
18+
19+
/// The analysis of a path stops when it reaches this function.
20+
#[unsafe(no_mangle)]
21+
#[inline(never)]
22+
pub extern "C" fn __checkct_panic() -> ! {
23+
loop {}
24+
}

0 commit comments

Comments
 (0)