Skip to main content

openentropy_core/sources/frontier/
keychain_timing.rs

1//! Keychain/securityd IPC timing — entropy from the Security framework's
2//! multi-domain round-trip through securityd, SEP, and APFS.
3
4use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
5#[cfg(target_os = "macos")]
6use crate::sources::helpers::mach_time;
7
8#[cfg(target_os = "macos")]
9use super::extract_timing_entropy_variance;
10
11/// Configuration for keychain timing entropy collection.
12///
13/// # Example
14/// ```
15/// # use openentropy_core::sources::frontier::KeychainTimingConfig;
16/// let config = KeychainTimingConfig::default();
17/// ```
18#[derive(Debug, Clone, Default)]
19pub struct KeychainTimingConfig {
20    /// Use SecItemAdd/Delete (write path) instead of SecItemCopyMatching (read path).
21    ///
22
23    ///
24    /// **Default:** `false` (use read path for speed)
25    pub use_write_path: bool,
26}
27
28/// Harvests timing jitter from Security.framework keychain operations.
29///
30/// # What it measures
31/// Nanosecond timing of keychain operations (SecItemCopyMatching or SecItemAdd/Delete)
32/// that traverse the full Apple security stack.
33///
34/// # Why it's entropic
35/// Every keychain operation travels through multiple independent physical domains:
36/// 1. **XPC IPC** to securityd — scheduling/dispatch jitter
37/// 2. **securityd processing** — SQLite database lookup, access control evaluation
38/// 3. **Kernel scheduling** — context switches between our process and securityd
39/// 4. **Database I/O** — SQLite page reads from the keychain database file
40///
41/// The write path additionally involves APFS copy-on-write and NVMe controller
42/// timing. The read path may or may not traverse the Secure Enclave depending
43/// on the item's access control policy.
44///
45/// # What makes it unique
46/// No prior work has used keychain operation timing as an entropy source.
47/// The round-trip through XPC IPC, securityd scheduling, and database I/O
48/// aggregates jitter from multiple independent domains in a single measurement.
49///
50/// # Caveats
51/// - High autocorrelation at lag-1 (~0.43): variance extraction mitigates this
52/// - Warm-up effect: first ~500 reads are slower due to securityd cold caches
53/// - Slow: ~0.6ms per sample, not suitable for high-throughput collection
54///
55/// # Configuration
56/// See [`KeychainTimingConfig`] for tunable parameters.
57#[derive(Default)]
58pub struct KeychainTimingSource {
59    /// Source configuration. Use `Default::default()` for recommended settings.
60    pub config: KeychainTimingConfig,
61}
62
63static KEYCHAIN_TIMING_INFO: SourceInfo = SourceInfo {
64    name: "keychain_timing",
65    description: "Keychain/securityd round-trip timing jitter",
66    physics: "Times keychain operations that traverse: XPC IPC to securityd → SQLite \
67              database lookup → access control evaluation → return. Each domain (IPC \
68              scheduling, securityd process scheduling, database page I/O, kernel context \
69              switches) contributes independent jitter. Variance extraction removes serial \
70              correlation (lag-1 autocorrelation ~0.43 in raw timings). First 500 samples \
71              discarded to avoid warm-up transient from securityd cold caches.",
72    category: SourceCategory::IPC,
73    platform: Platform::MacOS,
74    requirements: &[Requirement::SecurityFramework],
75    entropy_rate_estimate: 6500.0,
76    composite: false,
77};
78
79impl EntropySource for KeychainTimingSource {
80    fn info(&self) -> &SourceInfo {
81        &KEYCHAIN_TIMING_INFO
82    }
83
84    fn is_available(&self) -> bool {
85        cfg!(target_os = "macos")
86    }
87
88    fn collect(&self, n_samples: usize) -> Vec<u8> {
89        #[cfg(target_os = "macos")]
90        {
91            if self.config.use_write_path {
92                collect_write_path(n_samples)
93            } else {
94                collect_read_path(n_samples)
95            }
96        }
97        #[cfg(not(target_os = "macos"))]
98        {
99            let _ = n_samples;
100            Vec::new()
101        }
102    }
103}
104
105#[cfg(target_os = "macos")]
106const WARMUP_SAMPLES: usize = 100;
107
108/// Collect entropy via the keychain read path (SecItemCopyMatching).
109/// Faster (~0.6ms/sample) with excellent entropy (H∞ ≈ 6.5–7.0).
110#[cfg(target_os = "macos")]
111fn collect_read_path(n_samples: usize) -> Vec<u8> {
112    // Bind Security framework symbols.
113    #[link(name = "Security", kind = "framework")]
114    unsafe extern "C" {
115        static kSecClass: CFStringRef;
116        static kSecClassGenericPassword: CFStringRef;
117        static kSecAttrLabel: CFStringRef;
118        static kSecReturnData: CFStringRef;
119        static kCFBooleanTrue: CFBooleanRef;
120        static kSecValueData: CFStringRef;
121        static kSecAttrAccessible: CFStringRef;
122        static kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: CFStringRef;
123
124        fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
125        fn SecItemDelete(query: CFDictionaryRef) -> i32;
126        fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
127    }
128
129    #[link(name = "CoreFoundation", kind = "framework")]
130    unsafe extern "C" {
131        fn CFStringCreateWithCString(
132            alloc: CFAllocatorRef,
133            cStr: *const i8,
134            encoding: u32,
135        ) -> CFStringRef;
136        fn CFDataCreate(alloc: CFAllocatorRef, bytes: *const u8, length: isize) -> CFDataRef;
137        fn CFDictionaryCreateMutable(
138            alloc: CFAllocatorRef,
139            capacity: isize,
140            keyCallBacks: *const std::ffi::c_void,
141            valueCallBacks: *const std::ffi::c_void,
142        ) -> CFMutableDictionaryRef;
143        fn CFDictionarySetValue(
144            dict: CFMutableDictionaryRef,
145            key: *const std::ffi::c_void,
146            value: *const std::ffi::c_void,
147        );
148        fn CFRelease(cf: CFTypeRef);
149
150        static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
151        static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
152    }
153
154    // Opaque CF types (we only use them as pointers).
155    type CFAllocatorRef = *const std::ffi::c_void;
156    type CFStringRef = *const std::ffi::c_void;
157    type CFDataRef = *const std::ffi::c_void;
158    type CFBooleanRef = *const std::ffi::c_void;
159    type CFDictionaryRef = *const std::ffi::c_void;
160    type CFMutableDictionaryRef = *mut std::ffi::c_void;
161    type CFTypeRef = *const std::ffi::c_void;
162
163    #[repr(C)]
164    struct CFDictionaryKeyCallBacks {
165        _data: [u8; 40],
166    }
167    #[repr(C)]
168    struct CFDictionaryValueCallBacks {
169        _data: [u8; 40],
170    }
171
172    const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
173
174    let label_cstr = b"openentropy-timing-probe\0";
175
176    unsafe {
177        // Create a keychain item to query.
178        let label_cf = CFStringCreateWithCString(
179            std::ptr::null(),
180            label_cstr.as_ptr() as *const i8,
181            K_CF_STRING_ENCODING_UTF8,
182        );
183        let secret: [u8; 16] = [0x42; 16];
184        let secret_cf = CFDataCreate(std::ptr::null(), secret.as_ptr(), 16);
185
186        // Create the item.
187        let add_dict = CFDictionaryCreateMutable(
188            std::ptr::null(),
189            0,
190            &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
191            &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
192        );
193        CFDictionarySetValue(add_dict, kSecClass as _, kSecClassGenericPassword as _);
194        CFDictionarySetValue(add_dict, kSecAttrLabel as _, label_cf as _);
195        CFDictionarySetValue(add_dict, kSecValueData as _, secret_cf as _);
196        CFDictionarySetValue(
197            add_dict,
198            kSecAttrAccessible as _,
199            kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as _,
200        );
201
202        // Delete any existing item, then add.
203        let del_dict = CFDictionaryCreateMutable(
204            std::ptr::null(),
205            0,
206            &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
207            &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
208        );
209        CFDictionarySetValue(del_dict, kSecClass as _, kSecClassGenericPassword as _);
210        CFDictionarySetValue(del_dict, kSecAttrLabel as _, label_cf as _);
211        SecItemDelete(del_dict as _);
212        SecItemAdd(add_dict as _, std::ptr::null_mut());
213
214        // Build query dictionary.
215        let query = CFDictionaryCreateMutable(
216            std::ptr::null(),
217            0,
218            &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
219            &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
220        );
221        CFDictionarySetValue(query, kSecClass as _, kSecClassGenericPassword as _);
222        CFDictionarySetValue(query, kSecAttrLabel as _, label_cf as _);
223        CFDictionarySetValue(query, kSecReturnData as _, kCFBooleanTrue as _);
224
225        // Warm-up: discard first samples to avoid securityd cold cache transient.
226        for _ in 0..WARMUP_SAMPLES {
227            let mut result: CFTypeRef = std::ptr::null();
228            SecItemCopyMatching(query as _, &mut result);
229            if !result.is_null() {
230                CFRelease(result);
231            }
232        }
233
234        // Collect timings.
235        let raw_count = n_samples * 4 + 64;
236        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
237
238        for _ in 0..raw_count {
239            let mut result: CFTypeRef = std::ptr::null();
240            let t0 = mach_time();
241            SecItemCopyMatching(query as _, &mut result);
242            let t1 = mach_time();
243            timings.push(t1.wrapping_sub(t0));
244            if !result.is_null() {
245                CFRelease(result);
246            }
247        }
248
249        // Cleanup.
250        SecItemDelete(del_dict as _);
251        CFRelease(del_dict as CFTypeRef);
252        CFRelease(add_dict as CFTypeRef);
253        CFRelease(query as CFTypeRef);
254        CFRelease(secret_cf);
255        CFRelease(label_cf);
256
257        extract_timing_entropy_variance(&timings, n_samples)
258    }
259}
260
261/// Collect entropy via the keychain write path (SecItemAdd/Delete).
262/// Slower (~5ms/sample) but highest entropy (H∞ ≈ 7.4).
263#[cfg(target_os = "macos")]
264fn collect_write_path(n_samples: usize) -> Vec<u8> {
265    #[link(name = "Security", kind = "framework")]
266    unsafe extern "C" {
267        static kSecClass: CFStringRef;
268        static kSecClassGenericPassword: CFStringRef;
269        static kSecAttrLabel: CFStringRef;
270        static kSecValueData: CFStringRef;
271        static kSecAttrAccessible: CFStringRef;
272        static kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: CFStringRef;
273
274        fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> i32;
275        fn SecItemDelete(query: CFDictionaryRef) -> i32;
276    }
277
278    #[link(name = "CoreFoundation", kind = "framework")]
279    unsafe extern "C" {
280        fn CFStringCreateWithCString(
281            alloc: CFAllocatorRef,
282            cStr: *const i8,
283            encoding: u32,
284        ) -> CFStringRef;
285        fn CFDataCreate(alloc: CFAllocatorRef, bytes: *const u8, length: isize) -> CFDataRef;
286        fn CFDictionaryCreateMutable(
287            alloc: CFAllocatorRef,
288            capacity: isize,
289            keyCallBacks: *const std::ffi::c_void,
290            valueCallBacks: *const std::ffi::c_void,
291        ) -> CFMutableDictionaryRef;
292        fn CFDictionarySetValue(
293            dict: CFMutableDictionaryRef,
294            key: *const std::ffi::c_void,
295            value: *const std::ffi::c_void,
296        );
297        fn CFRelease(cf: CFTypeRef);
298
299        static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
300        static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
301    }
302
303    type CFAllocatorRef = *const std::ffi::c_void;
304    type CFStringRef = *const std::ffi::c_void;
305    type CFDataRef = *const std::ffi::c_void;
306    type CFDictionaryRef = *const std::ffi::c_void;
307    type CFMutableDictionaryRef = *mut std::ffi::c_void;
308    type CFTypeRef = *const std::ffi::c_void;
309
310    #[repr(C)]
311    struct CFDictionaryKeyCallBacks {
312        _data: [u8; 40],
313    }
314    #[repr(C)]
315    struct CFDictionaryValueCallBacks {
316        _data: [u8; 40],
317    }
318
319    const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
320
321    let raw_count = n_samples * 4 + 64;
322    let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
323
324    unsafe {
325        for i in 0..raw_count {
326            let mut label_buf = [0u8; 64];
327            let label_str = format!("oe-ent-{}\0", i);
328            label_buf[..label_str.len()].copy_from_slice(label_str.as_bytes());
329
330            let label_cf = CFStringCreateWithCString(
331                std::ptr::null(),
332                label_buf.as_ptr() as *const i8,
333                K_CF_STRING_ENCODING_UTF8,
334            );
335
336            let secret: [u8; 16] = [i as u8; 16];
337            let secret_cf = CFDataCreate(std::ptr::null(), secret.as_ptr(), 16);
338
339            let attrs = CFDictionaryCreateMutable(
340                std::ptr::null(),
341                0,
342                &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
343                &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
344            );
345            CFDictionarySetValue(attrs, kSecClass as _, kSecClassGenericPassword as _);
346            CFDictionarySetValue(attrs, kSecAttrLabel as _, label_cf as _);
347            CFDictionarySetValue(attrs, kSecValueData as _, secret_cf as _);
348            CFDictionarySetValue(
349                attrs,
350                kSecAttrAccessible as _,
351                kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as _,
352            );
353
354            let t0 = mach_time();
355            let status = SecItemAdd(attrs as _, std::ptr::null_mut());
356            let t1 = mach_time();
357
358            if status == 0 {
359                // errSecSuccess
360                timings.push(t1.wrapping_sub(t0));
361            }
362
363            // Delete.
364            let del = CFDictionaryCreateMutable(
365                std::ptr::null(),
366                0,
367                &kCFTypeDictionaryKeyCallBacks as *const _ as *const std::ffi::c_void,
368                &kCFTypeDictionaryValueCallBacks as *const _ as *const std::ffi::c_void,
369            );
370            CFDictionarySetValue(del, kSecClass as _, kSecClassGenericPassword as _);
371            CFDictionarySetValue(del, kSecAttrLabel as _, label_cf as _);
372            SecItemDelete(del as _);
373
374            CFRelease(del as CFTypeRef);
375            CFRelease(attrs as CFTypeRef);
376            CFRelease(secret_cf);
377            CFRelease(label_cf);
378        }
379    }
380
381    extract_timing_entropy_variance(&timings, n_samples)
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn info() {
390        let src = KeychainTimingSource::default();
391        assert_eq!(src.name(), "keychain_timing");
392        assert_eq!(src.info().category, SourceCategory::IPC);
393        assert!(!src.info().composite);
394    }
395
396    #[test]
397    fn default_config() {
398        let config = KeychainTimingConfig::default();
399        assert!(!config.use_write_path);
400    }
401
402    #[test]
403    #[ignore] // Requires macOS keychain access
404    fn collects_bytes_read_path() {
405        let src = KeychainTimingSource::default();
406        if src.is_available() {
407            let data = src.collect(64);
408            assert!(!data.is_empty());
409            assert!(data.len() <= 64);
410        }
411    }
412
413    #[test]
414    #[ignore] // Requires macOS keychain access, slow
415    fn collects_bytes_write_path() {
416        let src = KeychainTimingSource {
417            config: KeychainTimingConfig {
418                use_write_path: true,
419            },
420        };
421        if src.is_available() {
422            let data = src.collect(32);
423            assert!(!data.is_empty());
424            assert!(data.len() <= 32);
425        }
426    }
427}