Skip to main content

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