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
5use std::thread;
6use std::time::{Duration, Instant, SystemTime};
7
8use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
9
10// ---------------------------------------------------------------------------
11// ClockJitterSource
12// ---------------------------------------------------------------------------
13
14/// Measures phase noise between two independent clock oscillators
15/// (`Instant` vs `SystemTime`). Each clock is driven by a separate PLL on the
16/// SoC; thermal noise in the VCO causes random frequency drift. The LSBs of
17/// their difference are genuine analog entropy.
18pub struct ClockJitterSource;
19
20static CLOCK_JITTER_INFO: SourceInfo = SourceInfo {
21    name: "clock_jitter",
22    description: "Phase noise between Instant and SystemTime clocks",
23    physics: "Measures phase noise between two independent clock oscillators \
24              (perf_counter vs monotonic). Each clock is driven by a separate \
25              PLL (Phase-Locked Loop) on the SoC. Thermal noise in the PLL's \
26              voltage-controlled oscillator causes random frequency drift — \
27              the LSBs of their difference are genuine analog entropy from \
28              crystal oscillator physics.",
29    category: SourceCategory::Timing,
30    platform: Platform::Any,
31    requirements: &[],
32    entropy_rate_estimate: 0.5,
33    composite: false,
34};
35
36impl EntropySource for ClockJitterSource {
37    fn info(&self) -> &SourceInfo {
38        &CLOCK_JITTER_INFO
39    }
40
41    fn is_available(&self) -> bool {
42        true
43    }
44
45    fn collect(&self, n_samples: usize) -> Vec<u8> {
46        let mut output = Vec::with_capacity(n_samples);
47
48        for _ in 0..n_samples {
49            let mono = Instant::now();
50            let wall = SystemTime::now()
51                .duration_since(SystemTime::UNIX_EPOCH)
52                .unwrap_or_default();
53
54            let mono2 = Instant::now();
55            let mono_delta_ns = mono2.duration_since(mono).as_nanos() as u64;
56            let wall_ns = wall.as_nanos() as u64;
57
58            let delta = mono_delta_ns ^ wall_ns;
59            output.push(delta as u8);
60        }
61
62        output
63    }
64}
65
66// ---------------------------------------------------------------------------
67// MachTimingSource  (macOS only)
68// ---------------------------------------------------------------------------
69
70use super::helpers::mach_time;
71
72/// Reads the ARM system counter (`mach_absolute_time`) at sub-nanosecond
73/// resolution with variable micro-workloads between samples. Returns raw
74/// LSBs of timing deltas — no conditioning applied.
75pub struct MachTimingSource;
76
77static MACH_TIMING_INFO: SourceInfo = SourceInfo {
78    name: "mach_timing",
79    description: "mach_absolute_time() with micro-workload jitter (raw LSBs)",
80    physics: "Reads the ARM system counter (mach_absolute_time) at sub-nanosecond \
81              resolution with variable micro-workloads between samples. The timing \
82              jitter comes from CPU pipeline state: instruction reordering, branch \
83              prediction, cache state, interrupt coalescing, and power-state \
84              transitions.",
85    category: SourceCategory::Timing,
86    platform: Platform::MacOS,
87    requirements: &[],
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::Scheduling,
150    platform: Platform::Any,
151    requirements: &[],
152    entropy_rate_estimate: 0.4,
153    composite: false,
154};
155
156impl EntropySource for SleepJitterSource {
157    fn info(&self) -> &SourceInfo {
158        &SLEEP_JITTER_INFO
159    }
160
161    fn is_available(&self) -> bool {
162        true
163    }
164
165    fn collect(&self, n_samples: usize) -> Vec<u8> {
166        let oversample = n_samples * 2 + 64;
167        let mut raw_timings = Vec::with_capacity(oversample);
168
169        for _ in 0..oversample {
170            let before = Instant::now();
171            thread::sleep(Duration::ZERO);
172            let elapsed_ns = before.elapsed().as_nanos() as u64;
173            raw_timings.push(elapsed_ns);
174        }
175
176        // Compute deltas and XOR adjacent pairs
177        let deltas: Vec<u64> = raw_timings
178            .windows(2)
179            .map(|w| w[1].wrapping_sub(w[0]))
180            .collect();
181
182        let mut raw = Vec::with_capacity(n_samples);
183        for pair in deltas.windows(2) {
184            let xored = pair[0] ^ pair[1];
185            raw.push(xored as u8);
186            if raw.len() >= n_samples {
187                break;
188            }
189        }
190
191        raw
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    #[ignore] // Run with: cargo test -- --ignored
201    fn clock_jitter_collects_bytes() {
202        let src = ClockJitterSource;
203        assert!(src.is_available());
204        let data = src.collect(128);
205        assert!(!data.is_empty());
206        assert!(data.len() <= 128);
207        let first = data[0];
208        assert!(data.iter().any(|&b| b != first), "all bytes were identical");
209    }
210
211    #[test]
212    #[cfg(target_os = "macos")]
213    #[ignore] // Run with: cargo test -- --ignored
214    fn mach_timing_collects_bytes() {
215        let src = MachTimingSource;
216        assert!(src.is_available());
217        let data = src.collect(64);
218        assert!(!data.is_empty());
219        assert!(data.len() <= 64);
220    }
221
222    #[test]
223    #[ignore] // Run with: cargo test -- --ignored
224    fn sleep_jitter_collects_bytes() {
225        let src = SleepJitterSource;
226        assert!(src.is_available());
227        let data = src.collect(64);
228        assert!(!data.is_empty());
229        assert!(data.len() <= 64);
230    }
231
232    #[test]
233    fn source_info_names() {
234        assert_eq!(ClockJitterSource.name(), "clock_jitter");
235        assert_eq!(MachTimingSource.name(), "mach_timing");
236        assert_eq!(SleepJitterSource.name(), "sleep_jitter");
237    }
238
239    #[test]
240    fn source_info_categories() {
241        assert_eq!(ClockJitterSource.info().category, SourceCategory::Timing);
242        assert_eq!(MachTimingSource.info().category, SourceCategory::Timing);
243        assert_eq!(
244            SleepJitterSource.info().category,
245            SourceCategory::Scheduling
246        );
247        assert!(!ClockJitterSource.info().composite);
248        assert!(!MachTimingSource.info().composite);
249        assert!(!SleepJitterSource.info().composite);
250    }
251}