From c8c9bb31310a0a6190f0591e25ea29c152f5a101 Mon Sep 17 00:00:00 2001 From: Adam Michalik Date: Sun, 16 Nov 2025 17:53:55 -0500 Subject: [PATCH] Add gungraun constant-time benchmarks and checker Introduce gungraun-based benches for Uint operations behind the ct_gungraun harness. Add a check_ct_gungraun helper binary that parses gungraun summary.json v6, and enforces constant-time instruction counts for non-vartime benchmark groups. --- Cargo.lock | 109 +++++++++++++++++++++++- Cargo.toml | 12 +++ benches/README.md | 69 +++++++++++++++ benches/gungraun/ct_uint_cmp_vartime.rs | 24 ++++++ benches/gungraun/ct_uint_gcd.rs | 18 ++++ benches/gungraun/ct_uint_modpow.rs | 43 ++++++++++ benches/gungraun/ct_uint_ops.rs | 57 +++++++++++++ benches/gungraun/mod.rs | 55 ++++++++++++ 8 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 benches/README.md create mode 100644 benches/gungraun/ct_uint_cmp_vartime.rs create mode 100644 benches/gungraun/ct_uint_gcd.rs create mode 100644 benches/gungraun/ct_uint_modpow.rs create mode 100644 benches/gungraun/ct_uint_ops.rs create mode 100644 benches/gungraun/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f376140a..94d9a2a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "2.0.1" @@ -240,10 +249,11 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" name = "crypto-bigint" version = "0.7.0-rc.10" dependencies = [ - "bincode", + "bincode 2.0.1", "chacha20", "criterion", "der", + "gungraun", "hex-literal", "hybrid-array", "num-bigint", @@ -253,8 +263,11 @@ dependencies = [ "proptest", "rand_core 0.10.0-rc-2", "rlp", + "serde", + "serde_json", "serdect", "subtle", + "walkdir", "zeroize", ] @@ -264,6 +277,26 @@ version = "0.8.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4" +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -304,6 +337,43 @@ dependencies = [ "wasi", ] +[[package]] +name = "gungraun" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b247b47ec86130ed355045982783d4666f32045e397a4547bbb6793cd53e940f" +dependencies = [ + "bincode 1.3.3", + "derive_more", + "gungraun-macros", + "gungraun-runner", +] + +[[package]] +name = "gungraun-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b605e561ccca36d68ebacd387751f2d58cf65202b781bb4a9029951dd3a66d" +dependencies = [ + "derive_more", + "proc-macro-error2", + "proc-macro2", + "quote", + "rustc_version", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "gungraun-runner" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ae6c9fe670d3d77f5576be41568ebf1733e5873c6ca85ff3b85848a31ac76" +dependencies = [ + "serde", +] + [[package]] name = "half" version = "2.6.0" @@ -467,6 +537,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -625,6 +717,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.0.8" @@ -671,6 +772,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.219" diff --git a/Cargo.toml b/Cargo.toml index ec4ad9cd..c900dfe7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,18 +32,24 @@ zeroize = { version = "1", optional = true, default-features = false } bincode = { version = "2", features = ["serde"] } chacha20 = { version = "0.10.0-rc.3", default-features = false, features = ["rng"] } criterion = { version = "0.7", features = ["html_reports"] } +gungraun = { version = "0.17.0"} hex-literal = "1" num-bigint = "0.4" num-integer = "0.1" num-modular = { version = "0.6", features = ["num-bigint", "num-integer", "num-traits"] } proptest = "1.9" rand_core = "0.10.0-rc-2" +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1" } +walkdir = "2" + [features] default = ["rand"] alloc = ["serdect?/alloc"] extra-sizes = [] +gungraun = [] rand = ["rand_core"] serde = ["dep:serdect"] @@ -84,5 +90,11 @@ harness = false name = "int" harness = false +[[bench]] +name = "ct_gungraun" +path = "benches/gungraun/mod.rs" +harness = false +required-features = ["gungraun", "rand"] + [profile.dev] opt-level = 2 diff --git a/benches/README.md b/benches/README.md new file mode 100644 index 00000000..841d40bb --- /dev/null +++ b/benches/README.md @@ -0,0 +1,69 @@ +# Gungraun-Based Constant-Time Benchmarks + +This directory contains the gungraun benchmarks and helper tooling used to +inspect the instruction-count behavior operations in +`crypto-bigint`. + +## Running the benchmarks + +The `ct_gungraun` bench is defined in `Cargo.toml` as a single bench +target which uses the gungraun harness in `benches/gungraun/mod.rs`: + +```bash +cargo bench --bench ct_gungraun --features "gungraun rand" +``` + +To collect Callgrind summaries (used by the checker in CI), enable +`GUNGRAUN_SAVE_SUMMARY`: + +```bash +GUNGRAUN_SAVE_SUMMARY=json \ +cargo bench --bench ct_gungraun --features "gungraun rand" +``` + +This populates `target/gungraun/` with per-case `summary.json` files. + +## Constant-time checker + +There is an integration test `tests/check_ct_gungraun.rs` which parses +the gungraun `summary.json` files, extracts Callgrind’s `Ir` (instruction +count) metric, and enforces constant-time behavior: + +- Groups whose module path contains `vartime` are **skipped** + (e.g. `uint_cmp_vartime`). +- All other groups are treated as constant-time and must have identical + `Ir` across all their benchmark cases. + +Usage (after running the bench with summaries enabled): + +```bash +cargo test --test check_ct_gungraun +``` + +If everything is as expected, the test passes and prints a summary like: + +```text +Constant-time check passed: N const-time groups, M vartime groups skipped. +``` + +If any constant-time group has differing `Ir` values across cases, the +test prints a diagnostic and fails, which is intended to fail CI. + +## Adding new benchmarks + +For a new constant-time operation: + +1. Create a new `ct_*` file (e.g. `ct_uint_foo.rs`) that: + - Uses `#[library_benchmark]` and `#[bench::...]` or + `#[benches::with_iter(...)]` to define cases. + - Keeps the function name and module path *without* `vartime` in the + name (so the checker will enforce constant-time behavior). +2. Re-export the function in `mod.rs` and add it to a + `library_benchmark_group!` that is included in the `main!` macro. +3. Re-run: + - `GUNGRAUN_SAVE_SUMMARY=json cargo bench --bench ct_gungraun --features "gungraun rand"` + - `cargo test --test check_ct_gungraun` + +For a new **variable-time** operation (for comparison only), include +`vartime` in the module path or function name so that the checker skips +it automatically.*** diff --git a/benches/gungraun/ct_uint_cmp_vartime.rs b/benches/gungraun/ct_uint_cmp_vartime.rs new file mode 100644 index 00000000..209001a8 --- /dev/null +++ b/benches/gungraun/ct_uint_cmp_vartime.rs @@ -0,0 +1,24 @@ +use std::hint::black_box; + +use crypto_bigint::{BitOps, U1024}; +use gungraun::library_benchmark; + +use super::utils::random_uint; + +fn high_bit_u1024() -> U1024 { + let mut x = U1024::ZERO; + x.set_bit_vartime(U1024::BITS - 1, true); + x +} + +// Benchmark variable-time comparison for U1024 with different operand patterns. +#[library_benchmark] +#[bench::equal(U1024::ZERO, U1024::ZERO)] +#[bench::diff_low(U1024::ONE, U1024::ZERO)] +#[bench::diff_high(high_bit_u1024(), U1024::ZERO)] +#[bench::random_1(random_uint(1), random_uint(2))] +#[bench::random_2(random_uint(3), random_uint(4))] +#[bench::random_3(random_uint(5), random_uint(6))] +pub fn bench_u1024_cmp_vartime(a: U1024, b: U1024) -> core::cmp::Ordering { + black_box(a.cmp_vartime(&b)) +} diff --git a/benches/gungraun/ct_uint_gcd.rs b/benches/gungraun/ct_uint_gcd.rs new file mode 100644 index 00000000..b332ea03 --- /dev/null +++ b/benches/gungraun/ct_uint_gcd.rs @@ -0,0 +1,18 @@ +use std::hint::black_box; + +use crypto_bigint::{Gcd, U1024}; +use gungraun::library_benchmark; + +use super::utils::random_uint; + +// Constant-time GCD via the `Gcd` trait. +#[library_benchmark] +#[bench::zeros(U1024::ZERO, U1024::ZERO)] +#[bench::one_and_zero(U1024::ONE, U1024::ZERO)] +#[bench::max_and_one(U1024::MAX, U1024::ONE)] +#[bench::random_1(random_uint(1), random_uint(2))] +#[bench::random_2(random_uint(3), random_uint(4))] +#[bench::random_3(random_uint(5), random_uint(6))] +pub fn bench_u1024_gcd(a: U1024, b: U1024) -> U1024 { + black_box(a.gcd(black_box(&b))) +} diff --git a/benches/gungraun/ct_uint_modpow.rs b/benches/gungraun/ct_uint_modpow.rs new file mode 100644 index 00000000..c45acd37 --- /dev/null +++ b/benches/gungraun/ct_uint_modpow.rs @@ -0,0 +1,43 @@ +use std::hint::black_box; + +use crypto_bigint::{Odd, U1024, modular::MontyForm}; +use gungraun::library_benchmark; + +use super::utils::random_uint; + +fn modpow_cases() -> Vec<(U1024, U1024, Odd)> { + let values = [U1024::ZERO, U1024::ONE, U1024::MAX, random_uint(1)]; + + // Reuse the same value candidates as potential moduli, but keep only + // odd, non-zero ones. + let mut moduli = Vec::>::new(); + for &n in &values { + let candidate = Odd::new(n); + if bool::from(candidate.is_some()) { + moduli.push(candidate.unwrap()); + } + } + + let mut cases = Vec::new(); + for &base in &values { + for &exp in &values { + for &m in &moduli { + cases.push((base, exp, m)); + } + } + } + + cases +} + +// Benchmark modular exponentiation using Montgomery form (`MontyForm::pow`) for U1024. +#[library_benchmark] +#[benches::with_iter(iter = modpow_cases())] +pub fn bench_u1024_modpow((base, exponent, modulus): (U1024, U1024, Odd)) -> U1024 { + let base = black_box(base); + let exponent = black_box(exponent); + let modulus = black_box(modulus); + let params = black_box(crypto_bigint::modular::MontyParams::new(modulus)); + let base_monty = black_box(MontyForm::new(&base, params)); + black_box(base_monty.pow(&exponent).retrieve()) +} diff --git a/benches/gungraun/ct_uint_ops.rs b/benches/gungraun/ct_uint_ops.rs new file mode 100644 index 00000000..e5d1062b --- /dev/null +++ b/benches/gungraun/ct_uint_ops.rs @@ -0,0 +1,57 @@ +use std::hint::black_box; + +use crypto_bigint::U1024; +use gungraun::library_benchmark; + +use super::utils::random_uint; + +// Benchmark wrapping addition for U1024 with different operand patterns. +#[library_benchmark] +#[bench::zeros(U1024::ZERO, U1024::ZERO)] +#[bench::one_plus_max(U1024::ONE, U1024::MAX)] +#[bench::max_plus_one(U1024::MAX, U1024::ONE)] +#[bench::max_plus_max(U1024::MAX, U1024::MAX)] +#[bench::low_high_pattern( + U1024::from_u128(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), + U1024::from_u128(0x0000_0000_0000_0000_0000_0000_0000_0002) +)] +#[bench::random_1(random_uint(1), random_uint(2))] +#[bench::random_2(random_uint(3), random_uint(4))] +#[bench::random_3(random_uint(5), random_uint(6))] +pub fn bench_u1024_wrapping_add(a: U1024, b: U1024) -> U1024 { + black_box(a.wrapping_add(black_box(&b))) +} + +// Benchmark wrapping subtraction for U1024 with different operand patterns. +#[library_benchmark] +#[bench::zeros(U1024::ZERO, U1024::ZERO)] +#[bench::one_minus_max(U1024::ONE, U1024::MAX)] +#[bench::max_minus_one(U1024::MAX, U1024::ONE)] +#[bench::max_minus_max(U1024::MAX, U1024::MAX)] +#[bench::low_high_pattern( + U1024::from_u128(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), + U1024::from_u128(0x0000_0000_0000_0000_0000_0000_0000_0002) +)] +#[bench::random_1(random_uint(1), random_uint(2))] +#[bench::random_2(random_uint(3), random_uint(4))] +#[bench::random_3(random_uint(5), random_uint(6))] +pub fn bench_u1024_wrapping_sub(a: U1024, b: U1024) -> U1024 { + black_box(a.wrapping_sub(&b)) +} + +// Benchmark wrapping multiplication for U1024 with different operand patterns. +#[library_benchmark] +#[bench::zeros(U1024::ZERO, U1024::ZERO)] +#[bench::one_times_max(U1024::ONE, U1024::MAX)] +#[bench::max_times_one(U1024::MAX, U1024::ONE)] +#[bench::max_times_max(U1024::MAX, U1024::MAX)] +#[bench::low_high_pattern( + U1024::from_u128(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), + U1024::from_u128(0x0000_0000_0000_0000_0000_0000_0000_0002) +)] +#[bench::random_1(random_uint(1), random_uint(2))] +#[bench::random_2(random_uint(3), random_uint(4))] +#[bench::random_3(random_uint(5), random_uint(6))] +pub fn bench_u1024_wrapping_mul(a: U1024, b: U1024) -> U1024 { + black_box(a.wrapping_mul(black_box(&b))) +} diff --git a/benches/gungraun/mod.rs b/benches/gungraun/mod.rs new file mode 100644 index 00000000..b4746ff9 --- /dev/null +++ b/benches/gungraun/mod.rs @@ -0,0 +1,55 @@ +use gungraun::{Callgrind, LibraryBenchmarkConfig, library_benchmark_group, main}; + +mod ct_uint_ops; +mod ct_uint_cmp_vartime; +mod ct_uint_gcd; +mod ct_uint_modpow; +mod utils; + +pub use ct_uint_ops::bench_u1024_wrapping_add; +pub use ct_uint_ops::bench_u1024_wrapping_sub; +pub use ct_uint_ops::bench_u1024_wrapping_mul; +pub use ct_uint_cmp_vartime::bench_u1024_cmp_vartime; +pub use ct_uint_gcd::bench_u1024_gcd; +pub use ct_uint_modpow::bench_u1024_modpow; + +library_benchmark_group!( + name = uint_mul_wrapping; + benchmarks = bench_u1024_wrapping_mul +); + +library_benchmark_group!( + name = uint_add_wrapping; + benchmarks = bench_u1024_wrapping_add +); + +library_benchmark_group!( + name = uint_sub_wrapping; + benchmarks = bench_u1024_wrapping_sub +); + +library_benchmark_group!( + name = uint_gcd; + benchmarks = bench_u1024_gcd +); + +library_benchmark_group!( + name = uint_modpow; + benchmarks = bench_u1024_modpow +); + +library_benchmark_group!( + name = uint_cmp_vartime; + benchmarks = bench_u1024_cmp_vartime +); + +main!( + config = LibraryBenchmarkConfig::default().tool(Callgrind::default()); + library_benchmark_groups = + uint_mul_wrapping, + uint_add_wrapping, + uint_sub_wrapping, + uint_gcd, + uint_modpow, + uint_cmp_vartime +);