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