openentropy_core/sources/microarch/
commoncrypto_aes_timing.rs1use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
60
61static COMMONCRYPTO_AES_TIMING_INFO: SourceInfo = SourceInfo {
62 name: "commoncrypto_aes_timing",
63 description: "CommonCrypto AES-128-CBC warm/cold key schedule bimodal timing",
64 physics: "Times CCCrypt(AES-128-CBC) calls with rotating keys. Framework dispatch \
65 shows bimodal distribution: ~50 ticks (warm, key schedule cached) vs \
66 ~120 ticks (cold, key reload via system fabric). CV=155.4%; warm/cold \
67 transition driven by other processes' AES load (FileVault, HTTPS), AES \
68 coprocessor power management, and thermal state. Distinct from direct \
69 AESE instruction timing (aes_exec_timing): captures framework overhead \
70 and key schedule management layer. Cross-process sensitivity: Time Machine \
71 / FileVault bursts visibly shift distribution toward cold path.",
72 category: SourceCategory::Microarch,
73 platform: Platform::MacOS,
74 requirements: &[],
75 entropy_rate_estimate: 2.0,
76 composite: false,
77 is_fast: false,
78};
79
80pub struct CommonCryptoAesTimingSource;
82
83#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
84mod imp {
85 use super::*;
86 use crate::sources::helpers::extract_timing_entropy_debiased;
87 use crate::sources::helpers::mach_time;
88
89 const CC_ENCRYPT: u32 = 0;
91 const CC_AES: u32 = 0;
92 const CC_CBC_MODE: u32 = 2;
93 const CC_SUCCESS: i32 = 0;
94 const AES_KEY_SIZE: usize = 16;
95 const AES_BLOCK_SIZE: usize = 16;
96
97 unsafe extern "C" {
99 fn CCCrypt(
100 operation: u32,
101 algorithm: u32,
102 options: u32,
103 key: *const u8,
104 key_length: usize,
105 iv: *const u8,
106 data_in: *const u8,
107 data_in_len: usize,
108 data_out: *mut u8,
109 data_out_available: usize,
110 data_out_moved: *mut usize,
111 ) -> i32;
112 }
113
114 unsafe fn time_cccrypt(
116 key: &[u8; AES_KEY_SIZE],
117 iv: &[u8; AES_BLOCK_SIZE],
118 plaintext: &[u8; AES_BLOCK_SIZE],
119 ) -> Option<u64> {
120 let mut ciphertext = [0u8; AES_BLOCK_SIZE];
121 let mut out_moved: usize = 0;
122
123 let t0 = mach_time();
124 let status = unsafe {
125 CCCrypt(
126 CC_ENCRYPT,
127 CC_AES,
128 CC_CBC_MODE,
129 key.as_ptr(),
130 AES_KEY_SIZE,
131 iv.as_ptr(),
132 plaintext.as_ptr(),
133 AES_BLOCK_SIZE,
134 ciphertext.as_mut_ptr(),
135 AES_BLOCK_SIZE,
136 &mut out_moved,
137 )
138 };
139 let t1 = mach_time();
140
141 if status == CC_SUCCESS {
142 Some(t1.wrapping_sub(t0))
143 } else {
144 None
145 }
146 }
147
148 fn cccrypt_probe() -> bool {
150 let key = [0u8; AES_KEY_SIZE];
151 let iv = [0u8; AES_BLOCK_SIZE];
152 let plaintext = [0u8; AES_BLOCK_SIZE];
153 unsafe { time_cccrypt(&key, &iv, &plaintext).is_some() }
155 }
156
157 impl EntropySource for CommonCryptoAesTimingSource {
158 fn info(&self) -> &SourceInfo {
159 &COMMONCRYPTO_AES_TIMING_INFO
160 }
161
162 fn is_available(&self) -> bool {
163 static CCCRYPT_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
164 *CCCRYPT_AVAILABLE.get_or_init(cccrypt_probe)
165 }
166
167 fn collect(&self, n_samples: usize) -> Vec<u8> {
168 let raw_count = n_samples * 8 + 128;
170 let mut timings = Vec::with_capacity(raw_count);
171
172 let base_key: [u8; AES_KEY_SIZE] = [
174 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
175 0x4f, 0x3c,
176 ];
177 let base_iv: [u8; AES_BLOCK_SIZE] = [
179 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
180 0x0e, 0x0f,
181 ];
182 let plaintext: [u8; AES_BLOCK_SIZE] = [
184 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
185 0x17, 0x2a,
186 ];
187
188 for i in 0..32_usize {
190 let mut key = base_key;
191 for j in 0..16 {
192 key[j] = base_key[j].wrapping_add(i as u8);
193 }
194 let _ = unsafe { time_cccrypt(&key, &base_iv, &plaintext) };
195 }
196
197 for i in 0..raw_count {
198 let mut key = [0u8; AES_KEY_SIZE];
200 for j in 0..16 {
201 key[j] = base_key[(j + i) & 15].wrapping_add((i >> 4) as u8);
202 }
203
204 if let Some(t) = unsafe { time_cccrypt(&key, &base_iv, &plaintext) } {
205 if t < 50_000 {
207 timings.push(t);
208 }
209 }
210 }
211
212 let shifted: Vec<u64> = timings.iter().map(|&t| t >> 1).collect();
215 extract_timing_entropy_debiased(&shifted, n_samples)
216 }
217 }
218}
219
220#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
221impl EntropySource for CommonCryptoAesTimingSource {
222 fn info(&self) -> &SourceInfo {
223 &COMMONCRYPTO_AES_TIMING_INFO
224 }
225 fn is_available(&self) -> bool {
226 false
227 }
228 fn collect(&self, _n_samples: usize) -> Vec<u8> {
229 Vec::new()
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn info() {
239 let src = CommonCryptoAesTimingSource;
240 assert_eq!(src.info().name, "commoncrypto_aes_timing");
241 assert!(matches!(src.info().category, SourceCategory::Microarch));
242 assert_eq!(src.info().platform, Platform::MacOS);
243 assert!(!src.info().composite);
244 }
245
246 #[test]
247 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
248 fn is_available_on_macos() {
249 assert!(CommonCryptoAesTimingSource.is_available());
250 }
251
252 #[test]
253 #[ignore] fn collects_bimodal_variation() {
255 let src = CommonCryptoAesTimingSource;
256 if !src.is_available() {
257 return;
258 }
259 let data = src.collect(32);
260 assert!(!data.is_empty());
261 let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
262 assert!(
263 unique.len() > 2,
264 "expected bimodal variation from CCCrypt warm/cold paths"
265 );
266 }
267}