Skip to main content

tacet/
lib.rs

1//! # tacet
2//!
3//! Detect timing side channels in cryptographic code.
4//!
5//! This crate provides adaptive Bayesian methodology for detecting timing variations
6//! between two input classes (baseline vs sample), outputting:
7//! - Posterior probability of timing leak (0.0-1.0)
8//! - Effect size estimates in nanoseconds (shift and tail components)
9//! - Pass/Fail/Inconclusive decisions with bounded FPR
10//! - Exploitability assessment
11//!
12//! ## Common Pitfall: Side-Effects in Closures
13//!
14//! The closures you provide must execute **identical code paths**.
15//! Only the input *data* should differ - not the operations performed.
16//!
17//! ```ignore
18//! // WRONG - Sample closure has extra RNG/allocation overhead
19//! TimingOracle::for_attacker(AttackerModel::AdjacentNetwork).test(
20//!     InputPair::new(|| my_op(&[0u8; 32]), || my_op(&rand::random())),
21//!     |_| {},  // RNG called during measurement!
22//! );
23//!
24//! // CORRECT - Pre-generate inputs, both closures identical
25//! use tacet::{TimingOracle, AttackerModel, helpers::InputPair};
26//! let inputs = InputPair::new(|| [0u8; 32], || rand::random());
27//! TimingOracle::for_attacker(AttackerModel::AdjacentNetwork).test(inputs, |data| {
28//!     my_op(data);
29//! });
30//! ```
31//!
32//! See the `helpers` module for utilities that make this pattern easier.
33//!
34//! ## Quick Start
35//!
36//! ```ignore
37//! use tacet::{TimingOracle, AttackerModel, helpers::InputPair, Outcome};
38//!
39//! // Builder API with InputPair
40//! let inputs = InputPair::new(|| [0u8; 32], || rand::random());
41//! let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
42//!     .test(inputs, |data| {
43//!         my_function(data);
44//!     });
45//!
46//! match outcome {
47//!     Outcome::Pass { leak_probability, .. } => {
48//!         println!("No leak detected: P={:.1}%", leak_probability * 100.0);
49//!     }
50//!     Outcome::Fail { leak_probability, exploitability, .. } => {
51//!         println!("Leak detected: P={:.1}%, {:?}", leak_probability * 100.0, exploitability);
52//!     }
53//!     Outcome::Inconclusive { reason, .. } => {
54//!         println!("Inconclusive: {:?}", reason);
55//!     }
56//!     Outcome::Unmeasurable { recommendation, .. } => {
57//!         println!("Skipping: {}", recommendation);
58//!     }
59//! }
60//! ```
61
62#![warn(missing_docs)]
63#![warn(clippy::all)]
64
65// Core modules
66pub mod adaptive;
67mod config;
68mod constants;
69mod oracle;
70pub mod result;
71mod thread_pool;
72mod types;
73
74// Functional modules
75pub mod analysis;
76pub mod data;
77pub mod helpers;
78pub mod measurement;
79pub mod output;
80pub mod preflight;
81pub mod statistics;
82
83// Power analysis module (feature-gated)
84#[cfg(feature = "power")]
85pub mod power;
86
87// Re-exports for public API
88pub use config::{Config, IterationsPerSample};
89pub use constants::{DECILES, LOG_2PI};
90pub use measurement::{BoxedTimer, Timer, TimerError, TimerSpec};
91pub use oracle::{compute_min_uniqueness_ratio, TimingOracle};
92pub use result::{
93    BatchingInfo, Diagnostics, EffectEstimate, Exploitability, InconclusiveReason, IssueCode,
94    MeasurementQuality, Metadata, MinDetectableEffect, Outcome, QualityIssue, TopQuantile,
95    UnmeasurableInfo, UnreliablePolicy,
96};
97pub use types::{AttackerModel, Class, TimingSample};
98
99// Re-export helpers for convenience
100pub use helpers::InputPair;
101
102// Re-export effect injection utilities for benchmarking
103pub use helpers::effect::{
104    busy_wait_ns, counter_frequency_hz, global_max_delay_ns, set_global_max_delay_ns,
105    timer_backend_name, timer_resolution_ns, using_precise_timer, BenchmarkEffect, EffectInjector,
106};
107
108// ============================================================================
109// Assertion Macros
110// ============================================================================
111
112/// Assert that the result indicates constant-time behavior.
113/// Panics on Fail or Inconclusive with detailed diagnostic output.
114///
115/// # Example
116///
117/// ```ignore
118/// use tacet::{TimingOracle, AttackerModel, helpers::InputPair, assert_constant_time};
119///
120/// let inputs = InputPair::new(|| [0u8; 32], || rand::random());
121/// let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
122///     .test(inputs, |data| my_crypto_function(data));
123/// assert_constant_time!(outcome);
124/// ```
125#[macro_export]
126macro_rules! assert_constant_time {
127    ($outcome:expr) => {
128        match &$outcome {
129            $crate::Outcome::Pass { .. } => {}
130            $crate::Outcome::Fail { .. } => {
131                let summary = $crate::output::format_debug_summary(&$outcome);
132                panic!("Timing leak detected!\n\n{}", summary,);
133            }
134            $crate::Outcome::Inconclusive {
135                reason,
136                leak_probability,
137                ..
138            } => {
139                let summary = $crate::output::format_debug_summary(&$outcome);
140                panic!(
141                    "Could not confirm constant-time (P={:.1}%): {}\n\n{}",
142                    leak_probability * 100.0,
143                    reason,
144                    summary,
145                );
146            }
147            $crate::Outcome::Unmeasurable { recommendation, .. } => {
148                let summary = $crate::output::format_debug_summary(&$outcome);
149                panic!(
150                    "Cannot measure operation: {}\n\n{}",
151                    recommendation, summary
152                );
153            }
154            $crate::Outcome::Research(research) => {
155                let summary = $crate::output::format_debug_summary(&$outcome);
156                panic!(
157                    "Research mode result (use research mode assertions): {:?}\n\n{}",
158                    research.status, summary
159                );
160            }
161        }
162    };
163}
164
165/// Assert that no timing leak was detected.
166/// Panics only on Fail (lenient - allows Inconclusive and Pass).
167/// Includes detailed diagnostic output on failure.
168///
169/// # Example
170///
171/// ```ignore
172/// use tacet::{TimingOracle, AttackerModel, helpers::InputPair, assert_no_timing_leak};
173///
174/// let inputs = InputPair::new(|| [0u8; 32], || rand::random());
175/// let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
176///     .test(inputs, |data| my_crypto_function(data));
177/// assert_no_timing_leak!(outcome);
178/// ```
179#[macro_export]
180macro_rules! assert_no_timing_leak {
181    ($outcome:expr) => {
182        if let $crate::Outcome::Fail { .. } = &$outcome {
183            let summary = $crate::output::format_debug_summary(&$outcome);
184            panic!("Timing leak detected!\n\n{}", summary,);
185        }
186    };
187}
188
189/// Assert that a timing leak WAS detected (for testing known-leaky code).
190/// Panics on Pass with detailed diagnostic output showing why no leak was found.
191///
192/// # Example
193///
194/// ```ignore
195/// use tacet::{TimingOracle, AttackerModel, helpers::InputPair, assert_leak_detected};
196///
197/// let inputs = InputPair::new(|| [0u8; 32], || rand::random());
198/// let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
199///     .test(inputs, |data| leaky_function(data));
200/// assert_leak_detected!(outcome);
201/// ```
202#[macro_export]
203macro_rules! assert_leak_detected {
204    ($outcome:expr) => {
205        match &$outcome {
206            $crate::Outcome::Fail { .. } => {}
207            $crate::Outcome::Pass {
208                leak_probability, ..
209            } => {
210                let summary = $crate::output::format_debug_summary(&$outcome);
211                panic!(
212                    "Expected timing leak but got Pass (P={:.1}%)\n\n{}",
213                    leak_probability * 100.0,
214                    summary,
215                );
216            }
217            $crate::Outcome::Inconclusive {
218                reason,
219                leak_probability,
220                ..
221            } => {
222                // Accept Inconclusive with high leak probability (≥90%) as a detected leak.
223                // This handles cases where the oracle found strong evidence of a leak
224                // but a quality gate (e.g., WouldTakeTooLong) prevented formal confirmation.
225                if *leak_probability >= 0.90 {
226                    // Leak detected with high confidence - pass the assertion
227                } else {
228                    let summary = $crate::output::format_debug_summary(&$outcome);
229                    panic!(
230                        "Expected timing leak but got Inconclusive (P={:.1}%): {}\n\n{}",
231                        leak_probability * 100.0,
232                        reason,
233                        summary,
234                    );
235                }
236            }
237            $crate::Outcome::Unmeasurable { recommendation, .. } => {
238                let summary = $crate::output::format_debug_summary(&$outcome);
239                panic!(
240                    "Expected timing leak but operation unmeasurable: {}\n\n{}",
241                    recommendation, summary,
242                );
243            }
244            $crate::Outcome::Research(research) => {
245                let summary = $crate::output::format_debug_summary(&$outcome);
246                panic!(
247                    "Expected timing leak but got Research mode result: {:?}\n\n{}",
248                    research.status, summary,
249                );
250            }
251        }
252    };
253}
254
255// ============================================================================
256// Reliability Handling Macros
257// ============================================================================
258
259/// Skip test if measurement is unreliable (fail-open).
260///
261/// Prints `[SKIPPED]` message and returns early if unreliable.
262/// Returns `TestResult` if reliable.
263///
264/// # Example
265/// ```ignore
266/// use tacet::{TimingOracle, InputPair, skip_if_unreliable};
267///
268/// #[test]
269/// fn test_aes() {
270///     let inputs = InputPair::new(|| [0u8; 16], || rand::random());
271///     let outcome = TimingOracle::new().test(inputs, |data| encrypt(data));
272///     let result = skip_if_unreliable!(outcome, "test_aes");
273///     assert!(result.leak_probability < 0.1);
274/// }
275/// ```
276#[macro_export]
277macro_rules! skip_if_unreliable {
278    ($outcome:expr, $name:expr) => {
279        match $outcome.handle_unreliable($name, $crate::UnreliablePolicy::FailOpen) {
280            Some(result) => result,
281            None => return,
282        }
283    };
284}
285
286/// Require measurement to be reliable (fail-closed).
287///
288/// Panics if unreliable. Returns `TestResult` if reliable.
289///
290/// # Example
291/// ```ignore
292/// use tacet::{TimingOracle, InputPair, require_reliable};
293///
294/// #[test]
295/// fn test_aes_critical() {
296///     let inputs = InputPair::new(|| [0u8; 16], || rand::random());
297///     let outcome = TimingOracle::new().test(inputs, |data| encrypt(data));
298///     let result = require_reliable!(outcome, "test_aes_critical");
299///     assert!(result.leak_probability < 0.1);
300/// }
301/// ```
302#[macro_export]
303macro_rules! require_reliable {
304    ($outcome:expr, $name:expr) => {
305        match $outcome.handle_unreliable($name, $crate::UnreliablePolicy::FailClosed) {
306            Some(result) => result,
307            None => unreachable!(),
308        }
309    };
310}
311
312// Re-export the timing_test! and timing_test_checked! proc macros when the macros feature is enabled
313#[cfg(feature = "macros")]
314pub use tacet_macros::{timing_test, timing_test_checked};