#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum SecurityPolicy {
Permissive = 0x00,
WarnOnly = 0x01,
Strict = 0x02,
Paranoid = 0x03,
}
impl Default for SecurityPolicy {
fn default() -> Self {
Self::Strict
}
}
impl SecurityPolicy {
pub const fn requires_signature(&self) -> bool {
matches!(*self, Self::Strict | Self::Paranoid)
}
pub const fn verifies_content_hashes(&self) -> bool {
matches!(*self, Self::WarnOnly | Self::Strict | Self::Paranoid)
}
pub const fn verifies_level1(&self) -> bool {
matches!(*self, Self::Paranoid)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SecurityError {
UnsignedManifest {
manifest_offset: u64,
},
InvalidSignature {
manifest_offset: u64,
rejection_phase: &'static str,
},
UnknownSigner {
manifest_offset: u64,
actual_signer: [u8; 16],
expected_signer: Option<[u8; 16]>,
},
ContentHashMismatch {
pointer_name: &'static str,
expected_hash: [u8; 16],
actual_hash: [u8; 16],
seg_offset: u64,
},
EpochDriftExceeded {
epoch_drift: u32,
max_epoch_drift: u32,
},
Level1InvalidSignature {
manifest_offset: u64,
},
}
impl core::fmt::Display for SecurityError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnsignedManifest { manifest_offset } => {
write!(f, "unsigned manifest at offset 0x{manifest_offset:X}")
}
Self::InvalidSignature {
manifest_offset,
rejection_phase,
} => {
write!(
f,
"invalid signature at offset 0x{manifest_offset:X} \
(phase: {rejection_phase})"
)
}
Self::UnknownSigner {
manifest_offset, ..
} => {
write!(f, "unknown signer at offset 0x{manifest_offset:X}")
}
Self::ContentHashMismatch {
pointer_name,
seg_offset,
..
} => {
write!(
f,
"content hash mismatch for {pointer_name} \
at offset 0x{seg_offset:X}"
)
}
Self::EpochDriftExceeded {
epoch_drift,
max_epoch_drift,
} => {
write!(
f,
"centroid epoch drift {epoch_drift} exceeds max {max_epoch_drift}"
)
}
Self::Level1InvalidSignature { manifest_offset } => {
write!(
f,
"Level 1 manifest invalid signature at offset 0x{manifest_offset:X}"
)
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct HardeningFields {
pub entrypoint_content_hash: [u8; 16],
pub toplayer_content_hash: [u8; 16],
pub centroid_content_hash: [u8; 16],
pub quantdict_content_hash: [u8; 16],
pub hot_cache_content_hash: [u8; 16],
pub centroid_epoch: u32,
pub max_epoch_drift: u32,
pub reserved: [u8; 8],
}
const _: () = assert!(core::mem::size_of::<HardeningFields>() == 96);
impl HardeningFields {
pub const RESERVED_OFFSET: usize = 109;
pub const fn zeroed() -> Self {
Self {
entrypoint_content_hash: [0u8; 16],
toplayer_content_hash: [0u8; 16],
centroid_content_hash: [0u8; 16],
quantdict_content_hash: [0u8; 16],
hot_cache_content_hash: [0u8; 16],
centroid_epoch: 0,
max_epoch_drift: 64,
reserved: [0u8; 8],
}
}
pub fn to_bytes(&self) -> [u8; 96] {
let mut buf = [0u8; 96];
buf[0..16].copy_from_slice(&self.entrypoint_content_hash);
buf[16..32].copy_from_slice(&self.toplayer_content_hash);
buf[32..48].copy_from_slice(&self.centroid_content_hash);
buf[48..64].copy_from_slice(&self.quantdict_content_hash);
buf[64..80].copy_from_slice(&self.hot_cache_content_hash);
buf[80..84].copy_from_slice(&self.centroid_epoch.to_le_bytes());
buf[84..88].copy_from_slice(&self.max_epoch_drift.to_le_bytes());
buf[88..96].copy_from_slice(&self.reserved);
buf
}
pub fn from_bytes(buf: &[u8; 96]) -> Self {
let mut entrypoint_content_hash = [0u8; 16];
let mut toplayer_content_hash = [0u8; 16];
let mut centroid_content_hash = [0u8; 16];
let mut quantdict_content_hash = [0u8; 16];
let mut hot_cache_content_hash = [0u8; 16];
let mut reserved = [0u8; 8];
entrypoint_content_hash.copy_from_slice(&buf[0..16]);
toplayer_content_hash.copy_from_slice(&buf[16..32]);
centroid_content_hash.copy_from_slice(&buf[32..48]);
quantdict_content_hash.copy_from_slice(&buf[48..64]);
hot_cache_content_hash.copy_from_slice(&buf[64..80]);
let centroid_epoch = u32::from_le_bytes([buf[80], buf[81], buf[82], buf[83]]);
let max_epoch_drift = u32::from_le_bytes([buf[84], buf[85], buf[86], buf[87]]);
reserved.copy_from_slice(&buf[88..96]);
Self {
entrypoint_content_hash,
toplayer_content_hash,
centroid_content_hash,
quantdict_content_hash,
hot_cache_content_hash,
centroid_epoch,
max_epoch_drift,
reserved,
}
}
pub fn is_empty(&self) -> bool {
self.entrypoint_content_hash == [0u8; 16]
&& self.toplayer_content_hash == [0u8; 16]
&& self.centroid_content_hash == [0u8; 16]
&& self.quantdict_content_hash == [0u8; 16]
&& self.hot_cache_content_hash == [0u8; 16]
&& self.centroid_epoch == 0
}
pub fn hash_for_pointer(&self, pointer_name: &str) -> Option<&[u8; 16]> {
match pointer_name {
"entrypoint" => Some(&self.entrypoint_content_hash),
"toplayer" => Some(&self.toplayer_content_hash),
"centroid" => Some(&self.centroid_content_hash),
"quantdict" => Some(&self.quantdict_content_hash),
"hot_cache" => Some(&self.hot_cache_content_hash),
_ => None,
}
}
pub fn epoch_drift(&self, manifest_epoch: u32) -> u32 {
manifest_epoch.saturating_sub(self.centroid_epoch)
}
pub fn is_epoch_drift_exceeded(&self, manifest_epoch: u32) -> bool {
self.epoch_drift(manifest_epoch) > self.max_epoch_drift
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn security_policy_default_is_strict() {
assert_eq!(SecurityPolicy::default(), SecurityPolicy::Strict);
}
#[test]
fn security_policy_signature_required() {
assert!(!SecurityPolicy::Permissive.requires_signature());
assert!(!SecurityPolicy::WarnOnly.requires_signature());
assert!(SecurityPolicy::Strict.requires_signature());
assert!(SecurityPolicy::Paranoid.requires_signature());
}
#[test]
fn security_policy_content_hashes() {
assert!(!SecurityPolicy::Permissive.verifies_content_hashes());
assert!(SecurityPolicy::WarnOnly.verifies_content_hashes());
assert!(SecurityPolicy::Strict.verifies_content_hashes());
assert!(SecurityPolicy::Paranoid.verifies_content_hashes());
}
#[test]
fn security_policy_level1() {
assert!(!SecurityPolicy::Strict.verifies_level1());
assert!(SecurityPolicy::Paranoid.verifies_level1());
}
#[test]
fn security_policy_repr() {
assert_eq!(SecurityPolicy::Permissive as u8, 0x00);
assert_eq!(SecurityPolicy::WarnOnly as u8, 0x01);
assert_eq!(SecurityPolicy::Strict as u8, 0x02);
assert_eq!(SecurityPolicy::Paranoid as u8, 0x03);
}
#[test]
fn hardening_fields_size() {
assert_eq!(core::mem::size_of::<HardeningFields>(), 96);
}
#[test]
fn hardening_fields_round_trip() {
let fields = HardeningFields {
entrypoint_content_hash: [1u8; 16],
toplayer_content_hash: [2u8; 16],
centroid_content_hash: [3u8; 16],
quantdict_content_hash: [4u8; 16],
hot_cache_content_hash: [5u8; 16],
centroid_epoch: 42,
max_epoch_drift: 64,
reserved: [0u8; 8],
};
let bytes = fields.to_bytes();
let decoded = HardeningFields::from_bytes(&bytes);
assert_eq!(fields, decoded);
}
#[test]
fn hardening_fields_zeroed() {
let fields = HardeningFields::zeroed();
assert!(fields.is_empty());
assert_eq!(fields.max_epoch_drift, 64);
}
#[test]
fn hardening_fields_hash_for_pointer() {
let mut fields = HardeningFields::zeroed();
fields.centroid_content_hash = [0xAB; 16];
assert_eq!(fields.hash_for_pointer("centroid"), Some(&[0xAB; 16]));
assert_eq!(fields.hash_for_pointer("unknown"), None);
}
#[test]
fn hardening_fields_epoch_drift() {
let fields = HardeningFields {
centroid_epoch: 10,
max_epoch_drift: 64,
..HardeningFields::zeroed()
};
assert_eq!(fields.epoch_drift(50), 40);
assert!(!fields.is_epoch_drift_exceeded(50));
assert!(fields.is_epoch_drift_exceeded(100));
}
#[test]
fn security_error_display() {
let err = SecurityError::UnsignedManifest {
manifest_offset: 0x1000,
};
let s = alloc::format!("{err}");
assert!(s.contains("unsigned manifest"));
let err = SecurityError::ContentHashMismatch {
pointer_name: "centroid",
expected_hash: [0xAA; 16],
actual_hash: [0xBB; 16],
seg_offset: 0x2000,
};
let s = alloc::format!("{err}");
assert!(s.contains("centroid"));
assert!(s.contains("2000"));
}
#[test]
fn security_error_unknown_signer() {
let err = SecurityError::UnknownSigner {
manifest_offset: 0x3000,
actual_signer: [0x11; 16],
expected_signer: Some([0x22; 16]),
};
let s = alloc::format!("{err}");
assert!(s.contains("unknown signer"));
}
#[test]
fn reserved_offset_fits() {
assert!(HardeningFields::RESERVED_OFFSET + 96 <= 252);
}
}