1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[repr(u8)]
14pub enum SecurityPolicy {
15 Permissive = 0x00,
17 WarnOnly = 0x01,
19 Strict = 0x02,
22 Paranoid = 0x03,
25}
26
27impl Default for SecurityPolicy {
28 fn default() -> Self {
29 Self::Strict
30 }
31}
32
33impl SecurityPolicy {
34 pub const fn requires_signature(&self) -> bool {
36 matches!(*self, Self::Strict | Self::Paranoid)
37 }
38
39 pub const fn verifies_content_hashes(&self) -> bool {
41 matches!(*self, Self::WarnOnly | Self::Strict | Self::Paranoid)
42 }
43
44 pub const fn verifies_level1(&self) -> bool {
46 matches!(*self, Self::Paranoid)
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub enum SecurityError {
57 UnsignedManifest {
59 manifest_offset: u64,
61 },
62
63 InvalidSignature {
65 manifest_offset: u64,
67 rejection_phase: &'static str,
69 },
70
71 UnknownSigner {
73 manifest_offset: u64,
75 actual_signer: [u8; 16],
77 expected_signer: Option<[u8; 16]>,
79 },
80
81 ContentHashMismatch {
83 pointer_name: &'static str,
85 expected_hash: [u8; 16],
87 actual_hash: [u8; 16],
89 seg_offset: u64,
91 },
92
93 EpochDriftExceeded {
95 epoch_drift: u32,
97 max_epoch_drift: u32,
99 },
100
101 Level1InvalidSignature {
103 manifest_offset: u64,
105 },
106}
107
108impl core::fmt::Display for SecurityError {
109 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110 match self {
111 Self::UnsignedManifest { manifest_offset } => {
112 write!(f, "unsigned manifest at offset 0x{manifest_offset:X}")
113 }
114 Self::InvalidSignature { manifest_offset, rejection_phase } => {
115 write!(
116 f,
117 "invalid signature at offset 0x{manifest_offset:X} \
118 (phase: {rejection_phase})"
119 )
120 }
121 Self::UnknownSigner { manifest_offset, .. } => {
122 write!(f, "unknown signer at offset 0x{manifest_offset:X}")
123 }
124 Self::ContentHashMismatch { pointer_name, seg_offset, .. } => {
125 write!(
126 f,
127 "content hash mismatch for {pointer_name} \
128 at offset 0x{seg_offset:X}"
129 )
130 }
131 Self::EpochDriftExceeded { epoch_drift, max_epoch_drift } => {
132 write!(
133 f,
134 "centroid epoch drift {epoch_drift} exceeds max {max_epoch_drift}"
135 )
136 }
137 Self::Level1InvalidSignature { manifest_offset } => {
138 write!(
139 f,
140 "Level 1 manifest invalid signature at offset 0x{manifest_offset:X}"
141 )
142 }
143 }
144 }
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153#[repr(C)]
154pub struct HardeningFields {
155 pub entrypoint_content_hash: [u8; 16],
157 pub toplayer_content_hash: [u8; 16],
159 pub centroid_content_hash: [u8; 16],
161 pub quantdict_content_hash: [u8; 16],
163 pub hot_cache_content_hash: [u8; 16],
165 pub centroid_epoch: u32,
167 pub max_epoch_drift: u32,
169 pub reserved: [u8; 8],
171}
172
173const _: () = assert!(core::mem::size_of::<HardeningFields>() == 96);
174
175impl HardeningFields {
176 pub const RESERVED_OFFSET: usize = 109;
180
181 pub const fn zeroed() -> Self {
183 Self {
184 entrypoint_content_hash: [0u8; 16],
185 toplayer_content_hash: [0u8; 16],
186 centroid_content_hash: [0u8; 16],
187 quantdict_content_hash: [0u8; 16],
188 hot_cache_content_hash: [0u8; 16],
189 centroid_epoch: 0,
190 max_epoch_drift: 64,
191 reserved: [0u8; 8],
192 }
193 }
194
195 pub fn to_bytes(&self) -> [u8; 96] {
197 let mut buf = [0u8; 96];
198 buf[0..16].copy_from_slice(&self.entrypoint_content_hash);
199 buf[16..32].copy_from_slice(&self.toplayer_content_hash);
200 buf[32..48].copy_from_slice(&self.centroid_content_hash);
201 buf[48..64].copy_from_slice(&self.quantdict_content_hash);
202 buf[64..80].copy_from_slice(&self.hot_cache_content_hash);
203 buf[80..84].copy_from_slice(&self.centroid_epoch.to_le_bytes());
204 buf[84..88].copy_from_slice(&self.max_epoch_drift.to_le_bytes());
205 buf[88..96].copy_from_slice(&self.reserved);
206 buf
207 }
208
209 pub fn from_bytes(buf: &[u8; 96]) -> Self {
211 let mut entrypoint_content_hash = [0u8; 16];
212 let mut toplayer_content_hash = [0u8; 16];
213 let mut centroid_content_hash = [0u8; 16];
214 let mut quantdict_content_hash = [0u8; 16];
215 let mut hot_cache_content_hash = [0u8; 16];
216 let mut reserved = [0u8; 8];
217
218 entrypoint_content_hash.copy_from_slice(&buf[0..16]);
219 toplayer_content_hash.copy_from_slice(&buf[16..32]);
220 centroid_content_hash.copy_from_slice(&buf[32..48]);
221 quantdict_content_hash.copy_from_slice(&buf[48..64]);
222 hot_cache_content_hash.copy_from_slice(&buf[64..80]);
223
224 let centroid_epoch = u32::from_le_bytes([buf[80], buf[81], buf[82], buf[83]]);
225 let max_epoch_drift = u32::from_le_bytes([buf[84], buf[85], buf[86], buf[87]]);
226 reserved.copy_from_slice(&buf[88..96]);
227
228 Self {
229 entrypoint_content_hash,
230 toplayer_content_hash,
231 centroid_content_hash,
232 quantdict_content_hash,
233 hot_cache_content_hash,
234 centroid_epoch,
235 max_epoch_drift,
236 reserved,
237 }
238 }
239
240 pub fn is_empty(&self) -> bool {
242 self.entrypoint_content_hash == [0u8; 16]
243 && self.toplayer_content_hash == [0u8; 16]
244 && self.centroid_content_hash == [0u8; 16]
245 && self.quantdict_content_hash == [0u8; 16]
246 && self.hot_cache_content_hash == [0u8; 16]
247 && self.centroid_epoch == 0
248 }
249
250 pub fn hash_for_pointer(&self, pointer_name: &str) -> Option<&[u8; 16]> {
252 match pointer_name {
253 "entrypoint" => Some(&self.entrypoint_content_hash),
254 "toplayer" => Some(&self.toplayer_content_hash),
255 "centroid" => Some(&self.centroid_content_hash),
256 "quantdict" => Some(&self.quantdict_content_hash),
257 "hot_cache" => Some(&self.hot_cache_content_hash),
258 _ => None,
259 }
260 }
261
262 pub fn epoch_drift(&self, manifest_epoch: u32) -> u32 {
264 manifest_epoch.saturating_sub(self.centroid_epoch)
265 }
266
267 pub fn is_epoch_drift_exceeded(&self, manifest_epoch: u32) -> bool {
269 self.epoch_drift(manifest_epoch) > self.max_epoch_drift
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn security_policy_default_is_strict() {
279 assert_eq!(SecurityPolicy::default(), SecurityPolicy::Strict);
280 }
281
282 #[test]
283 fn security_policy_signature_required() {
284 assert!(!SecurityPolicy::Permissive.requires_signature());
285 assert!(!SecurityPolicy::WarnOnly.requires_signature());
286 assert!(SecurityPolicy::Strict.requires_signature());
287 assert!(SecurityPolicy::Paranoid.requires_signature());
288 }
289
290 #[test]
291 fn security_policy_content_hashes() {
292 assert!(!SecurityPolicy::Permissive.verifies_content_hashes());
293 assert!(SecurityPolicy::WarnOnly.verifies_content_hashes());
294 assert!(SecurityPolicy::Strict.verifies_content_hashes());
295 assert!(SecurityPolicy::Paranoid.verifies_content_hashes());
296 }
297
298 #[test]
299 fn security_policy_level1() {
300 assert!(!SecurityPolicy::Strict.verifies_level1());
301 assert!(SecurityPolicy::Paranoid.verifies_level1());
302 }
303
304 #[test]
305 fn security_policy_repr() {
306 assert_eq!(SecurityPolicy::Permissive as u8, 0x00);
307 assert_eq!(SecurityPolicy::WarnOnly as u8, 0x01);
308 assert_eq!(SecurityPolicy::Strict as u8, 0x02);
309 assert_eq!(SecurityPolicy::Paranoid as u8, 0x03);
310 }
311
312 #[test]
313 fn hardening_fields_size() {
314 assert_eq!(core::mem::size_of::<HardeningFields>(), 96);
315 }
316
317 #[test]
318 fn hardening_fields_round_trip() {
319 let fields = HardeningFields {
320 entrypoint_content_hash: [1u8; 16],
321 toplayer_content_hash: [2u8; 16],
322 centroid_content_hash: [3u8; 16],
323 quantdict_content_hash: [4u8; 16],
324 hot_cache_content_hash: [5u8; 16],
325 centroid_epoch: 42,
326 max_epoch_drift: 64,
327 reserved: [0u8; 8],
328 };
329 let bytes = fields.to_bytes();
330 let decoded = HardeningFields::from_bytes(&bytes);
331 assert_eq!(fields, decoded);
332 }
333
334 #[test]
335 fn hardening_fields_zeroed() {
336 let fields = HardeningFields::zeroed();
337 assert!(fields.is_empty());
338 assert_eq!(fields.max_epoch_drift, 64);
339 }
340
341 #[test]
342 fn hardening_fields_hash_for_pointer() {
343 let mut fields = HardeningFields::zeroed();
344 fields.centroid_content_hash = [0xAB; 16];
345 assert_eq!(fields.hash_for_pointer("centroid"), Some(&[0xAB; 16]));
346 assert_eq!(fields.hash_for_pointer("unknown"), None);
347 }
348
349 #[test]
350 fn hardening_fields_epoch_drift() {
351 let fields = HardeningFields {
352 centroid_epoch: 10,
353 max_epoch_drift: 64,
354 ..HardeningFields::zeroed()
355 };
356 assert_eq!(fields.epoch_drift(50), 40);
357 assert!(!fields.is_epoch_drift_exceeded(50));
358 assert!(fields.is_epoch_drift_exceeded(100));
359 }
360
361 #[test]
362 fn security_error_display() {
363 let err = SecurityError::UnsignedManifest { manifest_offset: 0x1000 };
364 let s = alloc::format!("{err}");
365 assert!(s.contains("unsigned manifest"));
366
367 let err = SecurityError::ContentHashMismatch {
368 pointer_name: "centroid",
369 expected_hash: [0xAA; 16],
370 actual_hash: [0xBB; 16],
371 seg_offset: 0x2000,
372 };
373 let s = alloc::format!("{err}");
374 assert!(s.contains("centroid"));
375 assert!(s.contains("2000"));
376 }
377
378 #[test]
379 fn security_error_unknown_signer() {
380 let err = SecurityError::UnknownSigner {
381 manifest_offset: 0x3000,
382 actual_signer: [0x11; 16],
383 expected_signer: Some([0x22; 16]),
384 };
385 let s = alloc::format!("{err}");
386 assert!(s.contains("unknown signer"));
387 }
388
389 #[test]
390 fn reserved_offset_fits() {
391 assert!(HardeningFields::RESERVED_OFFSET + 96 <= 252);
393 }
394}