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 {
115 manifest_offset,
116 rejection_phase,
117 } => {
118 write!(
119 f,
120 "invalid signature at offset 0x{manifest_offset:X} \
121 (phase: {rejection_phase})"
122 )
123 }
124 Self::UnknownSigner {
125 manifest_offset, ..
126 } => {
127 write!(f, "unknown signer at offset 0x{manifest_offset:X}")
128 }
129 Self::ContentHashMismatch {
130 pointer_name,
131 seg_offset,
132 ..
133 } => {
134 write!(
135 f,
136 "content hash mismatch for {pointer_name} \
137 at offset 0x{seg_offset:X}"
138 )
139 }
140 Self::EpochDriftExceeded {
141 epoch_drift,
142 max_epoch_drift,
143 } => {
144 write!(
145 f,
146 "centroid epoch drift {epoch_drift} exceeds max {max_epoch_drift}"
147 )
148 }
149 Self::Level1InvalidSignature { manifest_offset } => {
150 write!(
151 f,
152 "Level 1 manifest invalid signature at offset 0x{manifest_offset:X}"
153 )
154 }
155 }
156 }
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
165#[repr(C)]
166pub struct HardeningFields {
167 pub entrypoint_content_hash: [u8; 16],
169 pub toplayer_content_hash: [u8; 16],
171 pub centroid_content_hash: [u8; 16],
173 pub quantdict_content_hash: [u8; 16],
175 pub hot_cache_content_hash: [u8; 16],
177 pub centroid_epoch: u32,
179 pub max_epoch_drift: u32,
181 pub reserved: [u8; 8],
183}
184
185const _: () = assert!(core::mem::size_of::<HardeningFields>() == 96);
186
187impl HardeningFields {
188 pub const RESERVED_OFFSET: usize = 109;
192
193 pub const fn zeroed() -> Self {
195 Self {
196 entrypoint_content_hash: [0u8; 16],
197 toplayer_content_hash: [0u8; 16],
198 centroid_content_hash: [0u8; 16],
199 quantdict_content_hash: [0u8; 16],
200 hot_cache_content_hash: [0u8; 16],
201 centroid_epoch: 0,
202 max_epoch_drift: 64,
203 reserved: [0u8; 8],
204 }
205 }
206
207 pub fn to_bytes(&self) -> [u8; 96] {
209 let mut buf = [0u8; 96];
210 buf[0..16].copy_from_slice(&self.entrypoint_content_hash);
211 buf[16..32].copy_from_slice(&self.toplayer_content_hash);
212 buf[32..48].copy_from_slice(&self.centroid_content_hash);
213 buf[48..64].copy_from_slice(&self.quantdict_content_hash);
214 buf[64..80].copy_from_slice(&self.hot_cache_content_hash);
215 buf[80..84].copy_from_slice(&self.centroid_epoch.to_le_bytes());
216 buf[84..88].copy_from_slice(&self.max_epoch_drift.to_le_bytes());
217 buf[88..96].copy_from_slice(&self.reserved);
218 buf
219 }
220
221 pub fn from_bytes(buf: &[u8; 96]) -> Self {
223 let mut entrypoint_content_hash = [0u8; 16];
224 let mut toplayer_content_hash = [0u8; 16];
225 let mut centroid_content_hash = [0u8; 16];
226 let mut quantdict_content_hash = [0u8; 16];
227 let mut hot_cache_content_hash = [0u8; 16];
228 let mut reserved = [0u8; 8];
229
230 entrypoint_content_hash.copy_from_slice(&buf[0..16]);
231 toplayer_content_hash.copy_from_slice(&buf[16..32]);
232 centroid_content_hash.copy_from_slice(&buf[32..48]);
233 quantdict_content_hash.copy_from_slice(&buf[48..64]);
234 hot_cache_content_hash.copy_from_slice(&buf[64..80]);
235
236 let centroid_epoch = u32::from_le_bytes([buf[80], buf[81], buf[82], buf[83]]);
237 let max_epoch_drift = u32::from_le_bytes([buf[84], buf[85], buf[86], buf[87]]);
238 reserved.copy_from_slice(&buf[88..96]);
239
240 Self {
241 entrypoint_content_hash,
242 toplayer_content_hash,
243 centroid_content_hash,
244 quantdict_content_hash,
245 hot_cache_content_hash,
246 centroid_epoch,
247 max_epoch_drift,
248 reserved,
249 }
250 }
251
252 pub fn is_empty(&self) -> bool {
254 self.entrypoint_content_hash == [0u8; 16]
255 && self.toplayer_content_hash == [0u8; 16]
256 && self.centroid_content_hash == [0u8; 16]
257 && self.quantdict_content_hash == [0u8; 16]
258 && self.hot_cache_content_hash == [0u8; 16]
259 && self.centroid_epoch == 0
260 }
261
262 pub fn hash_for_pointer(&self, pointer_name: &str) -> Option<&[u8; 16]> {
264 match pointer_name {
265 "entrypoint" => Some(&self.entrypoint_content_hash),
266 "toplayer" => Some(&self.toplayer_content_hash),
267 "centroid" => Some(&self.centroid_content_hash),
268 "quantdict" => Some(&self.quantdict_content_hash),
269 "hot_cache" => Some(&self.hot_cache_content_hash),
270 _ => None,
271 }
272 }
273
274 pub fn epoch_drift(&self, manifest_epoch: u32) -> u32 {
276 manifest_epoch.saturating_sub(self.centroid_epoch)
277 }
278
279 pub fn is_epoch_drift_exceeded(&self, manifest_epoch: u32) -> bool {
281 self.epoch_drift(manifest_epoch) > self.max_epoch_drift
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn security_policy_default_is_strict() {
291 assert_eq!(SecurityPolicy::default(), SecurityPolicy::Strict);
292 }
293
294 #[test]
295 fn security_policy_signature_required() {
296 assert!(!SecurityPolicy::Permissive.requires_signature());
297 assert!(!SecurityPolicy::WarnOnly.requires_signature());
298 assert!(SecurityPolicy::Strict.requires_signature());
299 assert!(SecurityPolicy::Paranoid.requires_signature());
300 }
301
302 #[test]
303 fn security_policy_content_hashes() {
304 assert!(!SecurityPolicy::Permissive.verifies_content_hashes());
305 assert!(SecurityPolicy::WarnOnly.verifies_content_hashes());
306 assert!(SecurityPolicy::Strict.verifies_content_hashes());
307 assert!(SecurityPolicy::Paranoid.verifies_content_hashes());
308 }
309
310 #[test]
311 fn security_policy_level1() {
312 assert!(!SecurityPolicy::Strict.verifies_level1());
313 assert!(SecurityPolicy::Paranoid.verifies_level1());
314 }
315
316 #[test]
317 fn security_policy_repr() {
318 assert_eq!(SecurityPolicy::Permissive as u8, 0x00);
319 assert_eq!(SecurityPolicy::WarnOnly as u8, 0x01);
320 assert_eq!(SecurityPolicy::Strict as u8, 0x02);
321 assert_eq!(SecurityPolicy::Paranoid as u8, 0x03);
322 }
323
324 #[test]
325 fn hardening_fields_size() {
326 assert_eq!(core::mem::size_of::<HardeningFields>(), 96);
327 }
328
329 #[test]
330 fn hardening_fields_round_trip() {
331 let fields = HardeningFields {
332 entrypoint_content_hash: [1u8; 16],
333 toplayer_content_hash: [2u8; 16],
334 centroid_content_hash: [3u8; 16],
335 quantdict_content_hash: [4u8; 16],
336 hot_cache_content_hash: [5u8; 16],
337 centroid_epoch: 42,
338 max_epoch_drift: 64,
339 reserved: [0u8; 8],
340 };
341 let bytes = fields.to_bytes();
342 let decoded = HardeningFields::from_bytes(&bytes);
343 assert_eq!(fields, decoded);
344 }
345
346 #[test]
347 fn hardening_fields_zeroed() {
348 let fields = HardeningFields::zeroed();
349 assert!(fields.is_empty());
350 assert_eq!(fields.max_epoch_drift, 64);
351 }
352
353 #[test]
354 fn hardening_fields_hash_for_pointer() {
355 let mut fields = HardeningFields::zeroed();
356 fields.centroid_content_hash = [0xAB; 16];
357 assert_eq!(fields.hash_for_pointer("centroid"), Some(&[0xAB; 16]));
358 assert_eq!(fields.hash_for_pointer("unknown"), None);
359 }
360
361 #[test]
362 fn hardening_fields_epoch_drift() {
363 let fields = HardeningFields {
364 centroid_epoch: 10,
365 max_epoch_drift: 64,
366 ..HardeningFields::zeroed()
367 };
368 assert_eq!(fields.epoch_drift(50), 40);
369 assert!(!fields.is_epoch_drift_exceeded(50));
370 assert!(fields.is_epoch_drift_exceeded(100));
371 }
372
373 #[test]
374 fn security_error_display() {
375 let err = SecurityError::UnsignedManifest {
376 manifest_offset: 0x1000,
377 };
378 let s = alloc::format!("{err}");
379 assert!(s.contains("unsigned manifest"));
380
381 let err = SecurityError::ContentHashMismatch {
382 pointer_name: "centroid",
383 expected_hash: [0xAA; 16],
384 actual_hash: [0xBB; 16],
385 seg_offset: 0x2000,
386 };
387 let s = alloc::format!("{err}");
388 assert!(s.contains("centroid"));
389 assert!(s.contains("2000"));
390 }
391
392 #[test]
393 fn security_error_unknown_signer() {
394 let err = SecurityError::UnknownSigner {
395 manifest_offset: 0x3000,
396 actual_signer: [0x11; 16],
397 expected_signer: Some([0x22; 16]),
398 };
399 let s = alloc::format!("{err}");
400 assert!(s.contains("unknown signer"));
401 }
402
403 #[test]
404 fn reserved_offset_fits() {
405 assert!(HardeningFields::RESERVED_OFFSET + 96 <= 252);
407 }
408}