1use crate::{
2 constants::DIAGNOSTIC_STRING_MAX_BYTES, key::StableKey, slot::AllocationSlotDescriptor,
3};
4use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
5
6#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
21#[serde(deny_unknown_fields)]
22pub struct PolicyIdentity {
23 name: Box<str>,
24 version: u32,
25 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
26 configuration_digest: Option<[u8; 32]>,
27}
28
29#[derive(Deserialize)]
30#[serde(deny_unknown_fields)]
31struct PolicyIdentityRepresentation {
32 name: String,
33 version: u32,
34 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
35 configuration_digest: Option<[u8; 32]>,
36}
37
38impl<'de> Deserialize<'de> for PolicyIdentity {
39 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
40 where
41 D: Deserializer<'de>,
42 {
43 let representation = PolicyIdentityRepresentation::deserialize(deserializer)?;
44 let mut identity =
45 Self::new(representation.name, representation.version).map_err(D::Error::custom)?;
46 identity.configuration_digest = representation.configuration_digest;
47 Ok(identity)
48 }
49}
50
51impl PolicyIdentity {
52 pub fn new(name: impl Into<String>, version: u32) -> Result<Self, PolicyIdentityError> {
54 let name = name.into();
55 validate_policy_identity_name(&name)?;
56 if version == 0 {
57 return Err(PolicyIdentityError::ZeroVersion);
58 }
59 Ok(Self {
60 name: name.into_boxed_str(),
61 version,
62 configuration_digest: None,
63 })
64 }
65
66 #[must_use]
68 pub const fn with_configuration_digest(mut self, digest: [u8; 32]) -> Self {
69 self.configuration_digest = Some(digest);
70 self
71 }
72
73 #[must_use]
75 pub fn name(&self) -> &str {
76 &self.name
77 }
78
79 #[must_use]
81 pub const fn version(&self) -> u32 {
82 self.version
83 }
84
85 #[must_use]
87 pub const fn configuration_digest(&self) -> Option<&[u8; 32]> {
88 self.configuration_digest.as_ref()
89 }
90}
91
92#[non_exhaustive]
99#[derive(Clone, Copy, Debug, Eq, thiserror::Error, PartialEq)]
100pub enum PolicyIdentityError {
101 #[error("runtime bootstrap policy identity name must not be empty")]
103 EmptyName,
104 #[error("runtime bootstrap policy identity name is {length} bytes; maximum is {maximum} bytes")]
106 NameTooLong {
107 length: usize,
109 maximum: usize,
111 },
112 #[error("runtime bootstrap policy identity name must be ASCII")]
114 NonAsciiName,
115 #[error("runtime bootstrap policy identity name must not contain ASCII control characters")]
117 ControlCharacterName,
118 #[error("runtime bootstrap policy identity version must be greater than zero")]
120 ZeroVersion,
121}
122
123fn validate_policy_identity_name(name: &str) -> Result<(), PolicyIdentityError> {
124 if name.is_empty() {
125 return Err(PolicyIdentityError::EmptyName);
126 }
127 if name.len() > DIAGNOSTIC_STRING_MAX_BYTES {
128 return Err(PolicyIdentityError::NameTooLong {
129 length: name.len(),
130 maximum: DIAGNOSTIC_STRING_MAX_BYTES,
131 });
132 }
133 if !name.is_ascii() {
134 return Err(PolicyIdentityError::NonAsciiName);
135 }
136 if name.bytes().any(|byte| byte.is_ascii_control()) {
137 return Err(PolicyIdentityError::ControlCharacterName);
138 }
139 Ok(())
140}
141
142pub trait AllocationPolicy {
161 type Error;
163
164 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error>;
166
167 fn validate_slot(
169 &self,
170 key: &StableKey,
171 slot: &AllocationSlotDescriptor,
172 ) -> Result<(), Self::Error>;
173
174 fn validate_reserved_slot(
176 &self,
177 key: &StableKey,
178 slot: &AllocationSlotDescriptor,
179 ) -> Result<(), Self::Error>;
180}
181
182pub trait RuntimeBootstrapPolicy: AllocationPolicy {
196 fn prepare_bootstrap(
203 &self,
204 _admission: &mut crate::BootstrapAdmission<'_>,
205 ) -> Result<(), Self::Error> {
206 Ok(())
207 }
208
209 fn runtime_bootstrap_identity(&self) -> Result<PolicyIdentity, PolicyIdentityError>;
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn policy_identity_validates_name_version_and_digest() {
219 let digest = [0xA5; 32];
220 let identity = PolicyIdentity::new("canic.memory-bootstrap-policy", 1)
221 .expect("valid identity")
222 .with_configuration_digest(digest);
223
224 assert_eq!(identity.name(), "canic.memory-bootstrap-policy");
225 assert_eq!(identity.version(), 1);
226 assert_eq!(identity.configuration_digest(), Some(&digest));
227 }
228
229 #[test]
230 fn policy_identity_rejects_unbounded_or_noncanonical_metadata() {
231 assert_eq!(
232 PolicyIdentity::new("", 1).expect_err("empty name"),
233 PolicyIdentityError::EmptyName
234 );
235 assert!(matches!(
236 PolicyIdentity::new("x".repeat(DIAGNOSTIC_STRING_MAX_BYTES + 1), 1),
237 Err(PolicyIdentityError::NameTooLong { .. })
238 ));
239 assert_eq!(
240 PolicyIdentity::new("policy\nname", 1).expect_err("control character"),
241 PolicyIdentityError::ControlCharacterName
242 );
243 assert_eq!(
244 PolicyIdentity::new("policé", 1).expect_err("non-ASCII"),
245 PolicyIdentityError::NonAsciiName
246 );
247 assert_eq!(
248 PolicyIdentity::new("policy", 0).expect_err("zero version"),
249 PolicyIdentityError::ZeroVersion
250 );
251 }
252
253 #[test]
254 fn policy_identity_deserialization_revalidates_invariants() {
255 #[derive(Serialize)]
256 struct UncheckedPolicyIdentity<'a> {
257 name: &'a str,
258 version: u32,
259 configuration_digest: Option<[u8; 32]>,
260 }
261
262 let bytes = crate::test_cbor::to_vec(&UncheckedPolicyIdentity {
263 name: "",
264 version: 1,
265 configuration_digest: None,
266 })
267 .expect("invalid diagnostic bytes");
268 let error = crate::test_cbor::from_slice::<PolicyIdentity>(&bytes)
269 .expect_err("deserialization must revalidate identity");
270 assert!(error.to_string().contains("must not be empty"));
271 }
272}