1use crate::access_policy::{MmioResource, ResourceCeiling, ResourceId, ResourceKind, WIDTH32};
15use sha2::{Digest as _, Sha256};
16use std::collections::BTreeMap;
17use std::fmt;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct Sha256Digest([u8; 32]);
22impl Sha256Digest {
23 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
25 Self(bytes)
26 }
27
28 pub fn as_bytes(&self) -> &[u8; 32] {
30 &self.0
31 }
32}
33
34impl fmt::Display for Sha256Digest {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(f, "sha256:")?;
37 for byte in self.0 {
38 write!(f, "{byte:02x}")?;
39 }
40 Ok(())
41 }
42}
43
44#[derive(Clone, Copy, Debug)]
46pub struct DigestInputs<'a> {
47 pub node_identity: &'a str,
50 pub provider: &'a str,
52 pub id: ResourceId,
54 pub resource: &'a MmioResource,
58 pub ceiling: Option<&'a ResourceCeiling>,
61}
62
63const RESOURCE_DOMAIN: &str = "fuchsia.driver.lab resource digest v1";
64const COMBINED_DOMAIN: &str = "fuchsia.driver.lab combined digest v1";
65const POLICY_DOMAIN: &str = "fuchsia.driver.lab policy digest v1";
66
67fn put_bytes(hasher: &mut Sha256, tag: &str, bytes: &[u8]) {
68 hasher.update((tag.len() as u64).to_le_bytes());
69 hasher.update(tag.as_bytes());
70 hasher.update((bytes.len() as u64).to_le_bytes());
71 hasher.update(bytes);
72}
73
74fn put_u64(hasher: &mut Sha256, tag: &str, value: u64) {
75 put_bytes(hasher, tag, &value.to_le_bytes());
76}
77
78fn finish(hasher: Sha256) -> Sha256Digest {
79 let output = hasher.finalize();
80 Sha256Digest(output.as_slice().try_into().expect("sha256 output is 32 bytes"))
81}
82
83pub fn resource_digest(inputs: &DigestInputs<'_>) -> Sha256Digest {
86 let mut hasher = Sha256::new();
87 put_bytes(&mut hasher, "domain", RESOURCE_DOMAIN.as_bytes());
88 put_bytes(&mut hasher, "node", inputs.node_identity.as_bytes());
89 put_bytes(&mut hasher, "provider", inputs.provider.as_bytes());
90 put_u64(&mut hasher, "id", u64::from(inputs.id));
91 put_bytes(&mut hasher, "name", inputs.resource.name.as_bytes());
92 put_bytes(
93 &mut hasher,
94 "kind",
95 match inputs.resource.kind {
96 ResourceKind::Mmio => b"mmio",
97 ResourceKind::Gpio => b"gpio",
98 ResourceKind::I2c => b"i2c",
99 ResourceKind::Spi => b"spi",
100 ResourceKind::Interrupt => b"interrupt",
101 ResourceKind::Clock => b"clock",
102 ResourceKind::Reset => b"reset",
103 ResourceKind::Serial => b"serial",
104 },
105 );
106 put_u64(&mut hasher, "logical_size", inputs.resource.logical_size);
107 put_u64(&mut hasher, "width", u64::from(WIDTH32));
108 match inputs.ceiling {
109 None => put_u64(&mut hasher, "ceiling", 0),
110 Some(ceiling) => {
111 put_u64(&mut hasher, "ceiling", 1);
112 put_u64(&mut hasher, "allow_unknown_reads", u64::from(ceiling.allow_unknown_reads));
113 put_u64(&mut hasher, "allow_poll", u64::from(ceiling.allow_poll));
114 put_u64(&mut hasher, "allow_interrupt", u64::from(ceiling.allow_interrupt));
115 let mut denied: Vec<(u64, u64)> =
116 ceiling.hard_denied.iter().map(|range| (range.start, range.end)).collect();
117 denied.sort_unstable();
118 put_u64(&mut hasher, "hard_denied_count", denied.len() as u64);
119 for (start, end) in denied {
120 put_u64(&mut hasher, "deny_start", start);
121 put_u64(&mut hasher, "deny_end", end);
122 }
123 if let Some(proto) = &ceiling.protocol {
124 put_u64(&mut hasher, "protocol_ceiling", 1);
125 put_u64(&mut hasher, "max_transfer_size", u64::from(proto.max_transfer_size));
126 put_u64(&mut hasher, "allow_mutating", u64::from(proto.allow_mutating));
127 for method in &proto.allowed_methods {
128 put_bytes(&mut hasher, "method", method.as_bytes());
129 }
130 } else {
131 put_u64(&mut hasher, "protocol_ceiling", 0);
132 }
133 }
134 }
135 finish(hasher)
136}
137
138pub fn combined_digest(digests: impl IntoIterator<Item = Sha256Digest>) -> Sha256Digest {
142 let mut hasher = Sha256::new();
143 put_bytes(&mut hasher, "domain", COMBINED_DOMAIN.as_bytes());
144 for digest in digests {
145 put_bytes(&mut hasher, "resource", digest.as_bytes());
146 }
147 finish(hasher)
148}
149
150pub fn per_resource_digests(
153 node_identity: &str,
154 provider: &str,
155 resources: &BTreeMap<ResourceId, MmioResource>,
156 ceiling: &BTreeMap<ResourceId, ResourceCeiling>,
157) -> BTreeMap<ResourceId, Sha256Digest> {
158 resources
159 .iter()
160 .map(|(id, resource)| {
161 let digest = resource_digest(&DigestInputs {
162 node_identity,
163 provider,
164 id: *id,
165 resource,
166 ceiling: ceiling.get(id),
167 });
168 (*id, digest)
169 })
170 .collect()
171}
172
173pub fn policy_digest(ceiling: &BTreeMap<ResourceId, ResourceCeiling>) -> Sha256Digest {
177 let mut hasher = Sha256::new();
178 put_bytes(&mut hasher, "domain", POLICY_DOMAIN.as_bytes());
179 put_u64(&mut hasher, "resource_count", ceiling.len() as u64);
180 for (id, entry) in ceiling {
181 put_u64(&mut hasher, "id", u64::from(*id));
182 put_u64(&mut hasher, "allow_unknown_reads", u64::from(entry.allow_unknown_reads));
183 put_u64(&mut hasher, "allow_poll", u64::from(entry.allow_poll));
184 put_u64(&mut hasher, "allow_interrupt", u64::from(entry.allow_interrupt));
185 let mut denied: Vec<(u64, u64)> =
186 entry.hard_denied.iter().map(|range| (range.start, range.end)).collect();
187 denied.sort_unstable();
188 put_u64(&mut hasher, "hard_denied_count", denied.len() as u64);
189 for (start, end) in denied {
190 put_u64(&mut hasher, "deny_start", start);
191 put_u64(&mut hasher, "deny_end", end);
192 }
193 }
194 finish(hasher)
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn resource() -> MmioResource {
202 MmioResource {
203 name: "ctrl".to_string(),
204 kind: ResourceKind::Mmio,
205 logical_size: 0x100,
206 mapped_size: 0x1000,
207 }
208 }
209
210 fn ceiling() -> ResourceCeiling {
211 ResourceCeiling {
212 hard_denied: vec![0x40..0x44, 0x80..0x90],
213 allow_unknown_reads: true,
214 allow_poll: false,
215 writable_registers: vec![],
216 protocol: None,
217 allow_interrupt: false,
218 }
219 }
220
221 fn digest_of(resource: &MmioResource, ceiling: Option<&ResourceCeiling>) -> Sha256Digest {
222 resource_digest(&DigestInputs {
223 node_identity: "dev.sys.platform.example",
224 provider: "platform",
225 id: 1,
226 resource,
227 ceiling,
228 })
229 }
230
231 #[test]
232 fn digest_is_deterministic() {
233 assert_eq!(
234 digest_of(&resource(), Some(&ceiling())),
235 digest_of(&resource(), Some(&ceiling()))
236 );
237 }
238
239 #[test]
240 fn display_format() {
241 let text = digest_of(&resource(), Some(&ceiling())).to_string();
242 assert!(text.starts_with("sha256:"));
243 assert_eq!(text.len(), "sha256:".len() + 64);
244 assert!(text["sha256:".len()..].chars().all(|c| c.is_ascii_hexdigit()));
245 }
246
247 #[test]
248 fn every_stable_field_changes_the_digest() {
249 let base = digest_of(&resource(), Some(&ceiling()));
250
251 let mut renamed = resource();
252 renamed.name = "ctrl2".to_string();
253 assert_ne!(digest_of(&renamed, Some(&ceiling())), base);
254
255 let mut resized = resource();
256 resized.logical_size = 0x200;
257 assert_ne!(digest_of(&resized, Some(&ceiling())), base);
258
259 let other_node = resource_digest(&DigestInputs {
260 node_identity: "dev.sys.platform.other",
261 provider: "platform",
262 id: 1,
263 resource: &resource(),
264 ceiling: Some(&ceiling()),
265 });
266 assert_ne!(other_node, base);
267
268 let other_provider = resource_digest(&DigestInputs {
269 node_identity: "dev.sys.platform.example",
270 provider: "pci",
271 id: 1,
272 resource: &resource(),
273 ceiling: Some(&ceiling()),
274 });
275 assert_ne!(other_provider, base);
276
277 let other_id = resource_digest(&DigestInputs {
278 node_identity: "dev.sys.platform.example",
279 provider: "platform",
280 id: 2,
281 resource: &resource(),
282 ceiling: Some(&ceiling()),
283 });
284 assert_ne!(other_id, base);
285 }
286
287 #[test]
288 fn ceiling_changes_the_digest() {
289 let base = digest_of(&resource(), Some(&ceiling()));
290
291 let mut no_unknown = ceiling();
292 no_unknown.allow_unknown_reads = false;
293 assert_ne!(digest_of(&resource(), Some(&no_unknown)), base);
294
295 let mut different_denies = ceiling();
296 different_denies.hard_denied = vec![0x40..0x48];
297 assert_ne!(digest_of(&resource(), Some(&different_denies)), base);
298
299 assert_ne!(digest_of(&resource(), None), base);
300 let empty = ResourceCeiling::default();
301 assert_ne!(digest_of(&resource(), Some(&empty)), digest_of(&resource(), None));
302 }
303
304 #[test]
305 fn hard_denied_order_does_not_matter() {
306 let reordered = ResourceCeiling {
307 hard_denied: vec![0x80..0x90, 0x40..0x44],
308 allow_unknown_reads: true,
309 allow_poll: false,
310 writable_registers: vec![],
311 protocol: None,
312 allow_interrupt: false,
313 };
314 assert_eq!(
315 digest_of(&resource(), Some(&reordered)),
316 digest_of(&resource(), Some(&ceiling()))
317 );
318 }
319
320 #[test]
321 fn mapped_size_is_excluded() {
322 let mut remapped = resource();
323 remapped.mapped_size = 0x4000;
324 assert_eq!(
325 digest_of(&remapped, Some(&ceiling())),
326 digest_of(&resource(), Some(&ceiling()))
327 );
328 }
329
330 #[test]
331 fn combined_digest_reflects_members() {
332 let a = digest_of(&resource(), Some(&ceiling()));
333 let b = digest_of(&resource(), None);
334 assert_eq!(combined_digest([a, b]), combined_digest([a, b]));
335 assert_ne!(combined_digest([a, b]), combined_digest([a]));
336 assert_ne!(combined_digest([a, b]), combined_digest([b, a]));
337 assert_eq!(combined_digest([]), combined_digest([]));
338 }
339
340 #[test]
341 fn per_resource_digests_match_direct_computation() {
342 let resources = BTreeMap::from([(1, resource()), (2, resource())]);
343 let ceilings = BTreeMap::from([(1, ceiling())]);
344 let digests = per_resource_digests("node", "fake", &resources, &ceilings);
345 assert_eq!(digests.len(), 2);
346 assert_eq!(
347 digests[&1],
348 resource_digest(&DigestInputs {
349 node_identity: "node",
350 provider: "fake",
351 id: 1,
352 resource: &resources[&1],
353 ceiling: Some(&ceilings[&1]),
354 })
355 );
356 assert_ne!(digests[&1], digests[&2]);
358 }
359
360 #[test]
361 fn policy_digest_is_domain_separated_and_content_sensitive() {
362 let empty = BTreeMap::new();
363 assert_ne!(policy_digest(&empty).to_string(), combined_digest([]).to_string());
366
367 let one = BTreeMap::from([(1, ceiling())]);
368 assert_ne!(policy_digest(&one), policy_digest(&empty));
369
370 let mut narrowed = ceiling();
371 narrowed.allow_unknown_reads = false;
372 let changed = BTreeMap::from([(1, narrowed)]);
373 assert_ne!(policy_digest(&changed), policy_digest(&one));
374
375 let reordered_denies = BTreeMap::from([(
376 1,
377 ResourceCeiling {
378 hard_denied: vec![0x80..0x90, 0x40..0x44],
379 allow_unknown_reads: true,
380 allow_poll: false,
381 writable_registers: vec![],
382 protocol: None,
383 allow_interrupt: false,
384 },
385 )]);
386 assert_eq!(policy_digest(&reordered_denies), policy_digest(&one));
387
388 let mut poll_allowed = ceiling();
389 poll_allowed.allow_poll = true;
390 let poll_changed = BTreeMap::from([(1, poll_allowed)]);
391 assert_ne!(policy_digest(&poll_changed), policy_digest(&one));
392
393 let mut interrupt_allowed = ceiling();
394 interrupt_allowed.allow_interrupt = true;
395 let interrupt_changed = BTreeMap::from([(1, interrupt_allowed)]);
396 assert_ne!(policy_digest(&interrupt_changed), policy_digest(&one));
397 }
398}