openentropy_core/sources/frontier/
keychain_timing.rs1use 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#[derive(Debug, Clone, Default)]
19pub struct KeychainTimingConfig {
20 pub use_write_path: bool,
26}
27
28#[derive(Default)]
58pub struct KeychainTimingSource {
59 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#[cfg(target_os = "macos")]
111fn collect_read_path(n_samples: usize) -> Vec<u8> {
112 #[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 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 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 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 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 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 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 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 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#[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 timings.push(t1.wrapping_sub(t0));
361 }
362
363 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] 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] 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}