Skip to main content

TimingOracle

Struct TimingOracle 

Source
pub struct TimingOracle { /* private fields */ }
Expand description

Main entry point for adaptive Bayesian timing analysis.

Use the builder pattern to configure and run timing tests. The oracle uses a two-phase approach:

  1. Calibration: Collect initial samples to estimate covariance and priors
  2. Adaptive loop: Collect batches until decision thresholds are reached

§Example

use tacet::{TimingOracle, AttackerModel, helpers::InputPair, Outcome};

let inputs = InputPair::new(
    || [0u8; 32],          // baseline: returns constant value
    || rand::random(),     // sample: generates varied values
);

// Choose attacker model based on your threat scenario
let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .test(inputs, |data| my_function(data));

match outcome {
    Outcome::Pass { leak_probability, .. } => {
        println!("No leak detected (P={:.1}%)", leak_probability * 100.0);
    }
    Outcome::Fail { leak_probability, exploitability, .. } => {
        println!("Leak detected! P={:.1}%, {:?}", leak_probability * 100.0, exploitability);
    }
    Outcome::Inconclusive { reason, .. } => {
        println!("Inconclusive: {:?}", reason);
    }
    Outcome::Unmeasurable { recommendation, .. } => {
        println!("Operation too fast: {}", recommendation);
    }
}

§Automatic PMU Detection

When running with sudo/root privileges, the library automatically uses cycle-accurate PMU timing (kperf on macOS, perf_event on Linux). No code changes needed - just run with sudo.

§Attacker Model Presets

Choose the appropriate attacker model for your threat scenario:

PresetthetaUse case
SharedHardware~0.6nsSGX, cross-VM, containers
AdjacentNetwork100nsLAN, HTTP/2 endpoints
RemoteNetwork50usPublic APIs, general internet
Research0Academic analysis (not for CI)

Implementations§

Source§

impl TimingOracle

Source

pub fn for_attacker(model: AttackerModel) -> Self

Create with an attacker model preset.

The attacker model determines the minimum effect threshold (theta) that is considered practically significant. Different attacker models represent different threat scenarios with varying capabilities.

§Example
use tacet::{TimingOracle, AttackerModel};

// For public APIs exposed to the internet
let oracle = TimingOracle::for_attacker(AttackerModel::RemoteNetwork);

// For internal LAN services or HTTP/2 endpoints
let oracle = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork);

// For SGX enclaves or shared hosting (most strict)
let oracle = TimingOracle::for_attacker(AttackerModel::SharedHardware);
§Presets
PresetthetaUse case
SharedHardware~0.6nsSGX, cross-VM, containers
AdjacentNetwork100nsLAN, HTTP/2 endpoints
RemoteNetwork50usPublic APIs, general internet
Research0Academic analysis (not for CI)
Source

pub fn timer_spec(self, spec: TimerSpec) -> Self

Set the timer specification.

Controls which timer implementation is used:

  • TimerSpec::Auto (default): Try cycle-accurate timer first, fall back to system timer
  • TimerSpec::SystemTimer: Always use system timer (rdtsc on x86_64, cntvct_el0 on ARM64)
  • TimerSpec::RequireCycleAccurate: Require cycle-accurate timing or panic
§Example
use tacet::{TimingOracle, AttackerModel, TimerSpec};

// Force system timer (no cycle-accurate timing)
let result = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .timer_spec(TimerSpec::SystemTimer)
    .test(...);
Source

pub fn system_timer(self) -> Self

Use the system timer only (no cycle-accurate timing).

Shorthand for .timer_spec(TimerSpec::SystemTimer).

Source

pub fn require_high_precision(self) -> Self

Require high-precision timing (≤2ns resolution).

Shorthand for .timer_spec(TimerSpec::RequireHighPrecision). Uses runtime detection: system timer if sufficient, else PMU timer. Panics if no high-precision timer is available.

Source

pub fn require_cycle_accurate(self) -> Self

Require cycle-accurate timing.

Shorthand for .timer_spec(TimerSpec::RequireCycleAccurate). Panics if cycle-accurate timing is unavailable.

Source

pub fn time_budget(self, duration: Duration) -> Self

Set the time budget for the adaptive sampling loop.

The oracle will stop and return Inconclusive if this time limit is reached without achieving a conclusive result.

Default: 60 seconds

§Example
use tacet::{TimingOracle, AttackerModel};
use std::time::Duration;

let oracle = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .time_budget(Duration::from_secs(30));
Source

pub fn time_budget_secs(self, secs: u64) -> Self

Set the time budget in seconds.

Convenience method for .time_budget(Duration::from_secs(secs)).

Source

pub fn max_samples(self, n: usize) -> Self

Set the maximum number of samples per class.

The oracle will stop and return Inconclusive if this limit is reached without achieving a conclusive result.

Default: 1,000,000

§Panics

Panics if n is 0.

Source

pub fn batch_size(self, n: usize) -> Self

Set the batch size for adaptive sampling.

Larger batches are more efficient but less responsive to early stopping.

Default: 1,000

§Panics

Panics if n is 0.

Source

pub fn calibration_samples(self, n: usize) -> Self

Set the number of calibration samples.

These samples are collected at the start to estimate the covariance matrix and set Bayesian priors. This is a fixed overhead.

Default: 5,000

§Panics

Panics if n is 0.

Source

pub fn pass_threshold(self, threshold: f64) -> Self

Set the pass threshold for leak probability.

If the posterior probability of a timing leak falls below this threshold, the test passes. Default: 0.05 (5%).

Lower values require more confidence to pass (more conservative).

§Panics

Panics if threshold is not in (0, 1) or >= fail_threshold.

Source

pub fn fail_threshold(self, threshold: f64) -> Self

Set the fail threshold for leak probability.

If the posterior probability of a timing leak exceeds this threshold, the test fails. Default: 0.95 (95%).

Higher values require more confidence to fail (more conservative).

§Panics

Panics if threshold is not in (0, 1) or <= pass_threshold.

Source

pub fn warmup(self, n: usize) -> Self

Set warmup iterations.

Warmup iterations warm CPU caches, stabilize frequency scaling, and trigger any JIT compilation before measurement begins.

Default: 1,000

Source

pub fn cov_bootstrap_iterations(self, n: usize) -> Self

Set bootstrap iterations for covariance estimation.

Used during calibration to estimate the noise covariance matrix. More iterations give better estimates but take longer.

Default: 2,000

§Panics

Panics if n is 0.

Source

pub fn outlier_percentile(self, p: f64) -> Self

Set outlier filtering percentile.

Must be in the range (0, 1]. Set to 1.0 to disable filtering.

§Panics

Panics if p is not in the range (0, 1].

Source

pub fn prior_no_leak(self, p: f64) -> Self

Set prior probability of no leak.

Must be in the range (0, 1).

§Panics

Panics if p is not in the range (0, 1).

Source

pub fn seed(self, seed: u64) -> Self

Set deterministic measurement seed.

Source

pub fn force_discrete_mode(self, force: bool) -> Self

Force discrete mode for testing.

When set to true, the oracle uses discrete mode (m-out-of-n bootstrap with mid-quantiles) regardless of actual timer resolution. This is primarily useful for testing the discrete mode code path on machines with high-resolution timers.

§Example
use tacet::{TimingOracle, AttackerModel};

let oracle = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .force_discrete_mode(true)  // Force discrete mode for testing
    .test(inputs, |data| operation(data));
Source

pub fn cpu_affinity(self, enabled: bool) -> Self

Enable or disable CPU affinity pinning.

When enabled (default), the measurement thread is pinned to its current CPU to reduce noise from thread migration between cores.

  • Linux: Enforced via sched_setaffinity (no privileges needed)
  • macOS: Advisory hint via thread_policy_set (kernel may ignore)
§Example
use tacet::{TimingOracle, AttackerModel};

// Disable CPU affinity if it causes issues
let oracle = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .cpu_affinity(false)
    .test(inputs, |data| operation(data));
Source

pub fn thread_priority(self, enabled: bool) -> Self

Enable or disable thread priority elevation.

When enabled (default), attempts to raise thread priority to reduce preemption during measurement. This is best-effort and fails silently if privileges are insufficient.

  • Linux: Lowers nice value and sets SCHED_BATCH policy
  • macOS: Lowers nice value and sets thread precedence hint
§Example
use tacet::{TimingOracle, AttackerModel};

// Disable priority elevation if it causes issues
let oracle = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .thread_priority(false)
    .test(inputs, |data| operation(data));
Source

pub fn frequency_stabilization_ms(self, ms: u64) -> Self

Set the frequency stabilization duration in milliseconds.

Before measurement begins, a brief spin-wait loop runs to let the CPU frequency ramp up and stabilize. Many CPUs start in low-power mode and take several milliseconds to reach their turbo/boost frequency.

Set to 0 to disable frequency stabilization.

Default: 5 ms.

§Example
use tacet::{TimingOracle, AttackerModel};

// Increase stabilization time for laptops with aggressive power management
let oracle = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .frequency_stabilization_ms(10)
    .test(inputs, |data| operation(data));
Source

pub fn config(&self) -> &Config

Get the current configuration.

Source

pub fn from_env(self) -> Self

Merge configuration from environment variables.

Reads the following environment variables to override settings:

  • TO_TIME_BUDGET_SECS: Time budget in seconds
  • TO_MAX_SAMPLES: Maximum samples per class
  • TO_BATCH_SIZE: Batch size for adaptive sampling
  • TO_CALIBRATION_SAMPLES: Number of calibration samples
  • TO_PASS_THRESHOLD: Pass threshold (e.g., “0.05”)
  • TO_FAIL_THRESHOLD: Fail threshold (e.g., “0.95”)
  • TO_MIN_EFFECT_NS: Minimum effect of concern in nanoseconds
  • TO_SEED: Deterministic measurement seed
§Example
use tacet::{TimingOracle, AttackerModel};

// In CI, set TO_TIME_BUDGET_SECS=120 to increase time budget
let oracle = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork).from_env();
Source

pub fn test<T, F1, F2, F>( self, inputs: InputPair<T, F1, F2>, operation: F, ) -> Outcome
where T: Clone + Hash, F1: FnMut() -> T, F2: FnMut() -> T, F: FnMut(&T),

Run a timing test with pre-generated inputs.

This is the primary API for timing tests. It handles input pre-generation internally to ensure accurate measurements without generator overhead.

§Example
use tacet::{TimingOracle, AttackerModel, helpers::InputPair};

let inputs = InputPair::new([0u8; 32], || rand::random());
let result = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .test(inputs, |data| {
        my_crypto_function(data);
    });
§How It Works
  1. Pre-generates all baseline and sample inputs before measurement
  2. Runs warmup iterations
  3. Calibration phase: collects samples to estimate covariance
  4. Adaptive phase: collects batches until a decision is reached
§Arguments
  • inputs - An InputPair containing the baseline and sample generators
  • operation - Closure that performs the operation under test
§Returns

An Outcome which is one of:

  • Pass: No timing leak detected
  • Fail: Timing leak confirmed
  • Inconclusive: Cannot reach a definitive conclusion
  • Unmeasurable: Operation too fast to measure reliably
Source

pub fn analyze_raw_samples( &self, baseline_ns: &[f64], test_ns: &[f64], ) -> Outcome

Analyze pre-collected timing samples in a single pass.

This method computes the posterior probability of a timing leak given fixed sets of baseline and test samples. Unlike the test method, it does not collect new samples - it works with what it has.

Useful for:

  • Analyzing data from external tools (SILENT, dudect, etc.)
  • Replaying historical measurements
  • Testing with synthetic or simulated data
§Arguments
  • baseline_ns - Baseline timing samples in nanoseconds
  • test_ns - Test timing samples in nanoseconds
§Example
use tacet::{TimingOracle, AttackerModel, Outcome};

// Load pre-collected samples
let baseline_ns: Vec<f64> = load_samples("baseline.csv");
let test_ns: Vec<f64> = load_samples("test.csv");

let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .analyze_raw_samples(&baseline_ns, &test_ns);

match outcome {
    Outcome::Pass { .. } => println!("No leak detected"),
    Outcome::Fail { .. } => println!("Leak detected!"),
    _ => {}
}
Source

pub fn analyze_timing_data( &self, data: &TimingData, cpu_freq_ghz: Option<f64>, ) -> Outcome

Analyze timing data loaded from a file or external source.

This is a convenience wrapper around analyze_raw_samples that accepts TimingData loaded via the data module.

§Arguments
  • data - Timing data with baseline and test samples
  • cpu_freq_ghz - CPU frequency in GHz (for cycle-to-ns conversion, optional)
§Example
use tacet::{TimingOracle, AttackerModel, data::load_silent_csv};
use std::path::Path;

let data = load_silent_csv(Path::new("measurements.csv")).unwrap();
let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
    .analyze_timing_data(&data, Some(3.0)); // 3 GHz CPU

Trait Implementations§

Source§

impl Clone for TimingOracle

Source§

fn clone(&self) -> TimingOracle

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for TimingOracle

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

Source§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
Source§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
Source§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
Source§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> EventData for T
where T: Send + Sync,