Skip to main content

openentropy_core/sources/
timing.rs

1//! Timing-based entropy sources: clock jitter, mach_absolute_time, and sleep jitter.
2//!
3//! **Raw output characteristics:** LSBs of timing deltas and clock differences.
4//! Shannon entropy ~2-5 bits/byte depending on source. Clock jitter has lowest
5//! entropy rate; mach_timing and sleep_jitter are higher due to pipeline effects.
6
7use std::thread;
8use std::time::{Duration, Instant, SystemTime};
9
10use crate::source::{EntropySource, SourceCategory, SourceInfo};
11
12// ---------------------------------------------------------------------------
13// ClockJitterSource
14// ---------------------------------------------------------------------------
15
16/// Measures phase noise between two independent clock oscillators
17/// (`Instant` vs `SystemTime`). Each clock is driven by a separate PLL on the
18/// SoC; thermal noise in the VCO causes random frequency drift. The LSBs of
19/// their difference are genuine analog entropy.
20pub struct ClockJitterSource;
21
22static CLOCK_JITTER_INFO: SourceInfo = SourceInfo {
23    name: "clock_jitter",
24    description: "Phase noise between Instant and SystemTime clocks",
25    physics: "Measures phase noise between two independent clock oscillators \
26              (perf_counter vs monotonic). Each clock is driven by a separate \
27              PLL (Phase-Locked Loop) on the SoC. Thermal noise in the PLL's \
28              voltage-controlled oscillator causes random frequency drift — \
29              the LSBs of their difference are genuine analog entropy from \
30              crystal oscillator physics.",
31    category: SourceCategory::Timing,
32    platform_requirements: &[],
33    entropy_rate_estimate: 0.5,
34    composite: false,
35};
36
37impl EntropySource for ClockJitterSource {
38    fn info(&self) -> &SourceInfo {
39        &CLOCK_JITTER_INFO
40    }
41
42    fn is_available(&self) -> bool {
43        true
44    }
45
46    fn collect(&self, n_samples: usize) -> Vec<u8> {
47        let mut output = Vec::with_capacity(n_samples);
48
49        for _ in 0..n_samples {
50            let mono = Instant::now();
51            let wall = SystemTime::now()
52                .duration_since(SystemTime::UNIX_EPOCH)
53                .unwrap_or_default();
54
55            let mono2 = Instant::now();
56            let mono_delta_ns = mono2.duration_since(mono).as_nanos() as u64;
57            let wall_ns = wall.as_nanos() as u64;
58
59            let delta = mono_delta_ns ^ wall_ns;
60            output.push(delta as u8);
61        }
62
63        output
64    }
65}
66
67// ---------------------------------------------------------------------------
68// MachTimingSource  (macOS only)
69// ---------------------------------------------------------------------------
70
71use super::helpers::mach_time;
72
73/// Reads the ARM system counter (`mach_absolute_time`) at sub-nanosecond
74/// resolution with variable micro-workloads between samples. Returns raw
75/// LSBs of timing deltas — no conditioning applied.
76pub struct MachTimingSource;
77
78static MACH_TIMING_INFO: SourceInfo = SourceInfo {
79    name: "mach_timing",
80    description: "mach_absolute_time() with micro-workload jitter (raw LSBs)",
81    physics: "Reads the ARM system counter (mach_absolute_time) at sub-nanosecond \
82              resolution with variable micro-workloads between samples. The timing \
83              jitter comes from CPU pipeline state: instruction reordering, branch \
84              prediction, cache state, interrupt coalescing, and power-state \
85              transitions.",
86    category: SourceCategory::Timing,
87    platform_requirements: &["macOS"],
88    entropy_rate_estimate: 0.3,
89    composite: false,
90};
91
92impl EntropySource for MachTimingSource {
93    fn info(&self) -> &SourceInfo {
94        &MACH_TIMING_INFO
95    }
96
97    fn is_available(&self) -> bool {
98        cfg!(target_os = "macos")
99    }
100
101    fn collect(&self, n_samples: usize) -> Vec<u8> {
102        // Collect raw LSBs from mach_absolute_time with micro-workloads.
103        let raw_count = n_samples * 2 + 64;
104        let mut raw = Vec::with_capacity(n_samples);
105
106        for i in 0..raw_count {
107            let t0 = mach_time();
108
109            // Variable micro-workload to perturb pipeline state.
110            let iterations = (i % 7) + 1;
111            let mut sink: u64 = t0;
112            for _ in 0..iterations {
113                sink = sink.wrapping_mul(6364136223846793005).wrapping_add(1);
114            }
115            std::hint::black_box(sink);
116
117            let t1 = mach_time();
118            let delta = t1.wrapping_sub(t0);
119
120            // Raw LSB of the delta — unconditioned
121            raw.push(delta as u8);
122
123            if raw.len() >= n_samples {
124                break;
125            }
126        }
127
128        raw.truncate(n_samples);
129        raw
130    }
131}
132
133// ---------------------------------------------------------------------------
134// SleepJitterSource
135// ---------------------------------------------------------------------------
136
137/// Requests zero-duration sleeps and measures the actual elapsed time.
138/// The jitter captures OS scheduler non-determinism: timer interrupt
139/// granularity, thread priority decisions, runqueue length, and DVFS.
140pub struct SleepJitterSource;
141
142static SLEEP_JITTER_INFO: SourceInfo = SourceInfo {
143    name: "sleep_jitter",
144    description: "OS scheduler jitter from zero-duration sleeps",
145    physics: "Requests zero-duration sleeps and measures actual wake time. The jitter \
146              captures OS scheduler non-determinism: timer interrupt granularity (1-4ms), \
147              thread priority decisions, runqueue length, and thermal-dependent clock \
148              frequency scaling (DVFS).",
149    category: SourceCategory::Timing,
150    platform_requirements: &[],
151    entropy_rate_estimate: 0.4,
152    composite: false,
153};
154
155impl EntropySource for SleepJitterSource {
156    fn info(&self) -> &SourceInfo {
157        &SLEEP_JITTER_INFO
158    }
159
160    fn is_available(&self) -> bool {
161        true
162    }
163
164    fn collect(&self, n_samples: usize) -> Vec<u8> {
165        let oversample = n_samples * 2 + 64;
166        let mut raw_timings = Vec::with_capacity(oversample);
167
168        for _ in 0..oversample {
169            let before = Instant::now();
170            thread::sleep(Duration::ZERO);
171            let elapsed_ns = before.elapsed().as_nanos() as u64;
172            raw_timings.push(elapsed_ns);
173        }
174
175        // Compute deltas and XOR adjacent pairs
176        let deltas: Vec<u64> = raw_timings
177            .windows(2)
178            .map(|w| w[1].wrapping_sub(w[0]))
179            .collect();
180
181        let mut raw = Vec::with_capacity(n_samples);
182        for pair in deltas.windows(2) {
183            let xored = pair[0] ^ pair[1];
184            raw.push(xored as u8);
185            if raw.len() >= n_samples {
186                break;
187            }
188        }
189
190        raw
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    #[ignore] // Run with: cargo test -- --ignored
200    fn clock_jitter_collects_bytes() {
201        let src = ClockJitterSource;
202        assert!(src.is_available());
203        let data = src.collect(128);
204        assert!(!data.is_empty());
205        assert!(data.len() <= 128);
206        let first = data[0];
207        assert!(data.iter().any(|&b| b != first), "all bytes were identical");
208    }
209
210    #[test]
211    #[cfg(target_os = "macos")]
212    #[ignore] // Run with: cargo test -- --ignored
213    fn mach_timing_collects_bytes() {
214        let src = MachTimingSource;
215        assert!(src.is_available());
216        let data = src.collect(64);
217        assert!(!data.is_empty());
218        assert!(data.len() <= 64);
219    }
220
221    #[test]
222    #[ignore] // Run with: cargo test -- --ignored
223    fn sleep_jitter_collects_bytes() {
224        let src = SleepJitterSource;
225        assert!(src.is_available());
226        let data = src.collect(64);
227        assert!(!data.is_empty());
228        assert!(data.len() <= 64);
229    }
230
231    #[test]
232    fn source_info_names() {
233        assert_eq!(ClockJitterSource.name(), "clock_jitter");
234        assert_eq!(MachTimingSource.name(), "mach_timing");
235        assert_eq!(SleepJitterSource.name(), "sleep_jitter");
236    }
237}