1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[repr(u8)]
7pub enum BundleType {
8 Full = 0,
9 Delta = 1,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[repr(u8)]
15pub enum Completeness {
16 Partial = 0,
17 AuthoritativeComplete = 1,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[repr(u8)]
23pub enum ResponderIdType {
24 ByName = 0,
25 ByKey = 1,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ResponderId {
31 pub id_type: ResponderIdType,
32 pub value: Vec<u8>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct CaScope {
38 pub hash_algorithm: Vec<u8>,
39 pub issuer_name_hash: Vec<u8>,
40 pub issuer_key_hash: Vec<u8>,
41 pub epoch: u64,
42 pub responder_id: ResponderId,
43 pub responder_chain: Option<Vec<Vec<u8>>>,
44 pub signature_algorithm: Vec<u8>,
45 pub completeness: Completeness,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Window {
51 pub produced_at: u64,
52 pub this_update_min: u64,
53 pub next_update_min: u64,
54 pub next_update_max: u64,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct Integrity {
60 pub index_digest: [u8; 32],
61 pub data_digest: [u8; 32],
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct Continuity {
67 pub prev_manifest_digest: Option<[u8; 32]>,
68 pub base_manifest_digest: Option<[u8; 32]>,
69 pub chain_length: u64,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct Shard {
75 pub shard_index: u64,
76 pub shard_count: u64,
77 pub shard_fn: u64,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct Compression {
83 pub algorithm: CompressionAlgorithm,
84 pub dictionary_digest: Option<[u8; 32]>,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88#[repr(u8)]
89pub enum CompressionAlgorithm {
90 None = 0,
91 Zstd = 1,
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub struct Manifest {
97 pub format_version: u64,
98 pub bundle_id: Uuid,
99 pub producer_id: String,
100 pub created_at: u64,
101 pub bundle_type: BundleType,
102 pub ca_scopes: Vec<CaScope>,
103 pub window: Window,
104 pub integrity: Integrity,
105 pub entry_count: u64,
106 pub continuity: Continuity,
107 pub shard: Option<Shard>,
108 pub compression: Option<Compression>,
109 pub extensions: Option<Vec<(String, ciborium::Value)>>,
110}
111
112impl Manifest {
120 pub fn to_cbor(&self) -> Vec<u8> {
121 use ciborium::Value;
122
123 let responder_id_to_val = |rid: &ResponderId| -> Value {
124 Value::Map(vec![
125 (
126 Value::Integer(1.into()),
127 Value::Integer((rid.id_type as u8).into()),
128 ),
129 (Value::Integer(2.into()), Value::Bytes(rid.value.clone())),
130 ])
131 };
132
133 let ca_scopes: Vec<Value> = self
134 .ca_scopes
135 .iter()
136 .map(|s| {
137 let mut entries = vec![
138 (
139 Value::Integer(1.into()),
140 Value::Bytes(s.hash_algorithm.clone()),
141 ),
142 (
143 Value::Integer(2.into()),
144 Value::Bytes(s.issuer_name_hash.clone()),
145 ),
146 (
147 Value::Integer(3.into()),
148 Value::Bytes(s.issuer_key_hash.clone()),
149 ),
150 (Value::Integer(4.into()), Value::Integer((s.epoch).into())),
151 (
152 Value::Integer(5.into()),
153 responder_id_to_val(&s.responder_id),
154 ),
155 ];
156 if let Some(chain) = &s.responder_chain {
157 entries.push((
158 Value::Integer(6.into()),
159 Value::Array(chain.iter().map(|c| Value::Bytes(c.clone())).collect()),
160 ));
161 }
162 entries.push((
163 Value::Integer(7.into()),
164 Value::Bytes(s.signature_algorithm.clone()),
165 ));
166 entries.push((
167 Value::Integer(8.into()),
168 Value::Integer((s.completeness as u8).into()),
169 ));
170 Value::Map(entries)
171 })
172 .collect();
173
174 let window = Value::Map(vec![
175 (
176 Value::Integer(1.into()),
177 Value::Integer((self.window.produced_at).into()),
178 ),
179 (
180 Value::Integer(2.into()),
181 Value::Integer((self.window.this_update_min).into()),
182 ),
183 (
184 Value::Integer(3.into()),
185 Value::Integer((self.window.next_update_min).into()),
186 ),
187 (
188 Value::Integer(4.into()),
189 Value::Integer((self.window.next_update_max).into()),
190 ),
191 ]);
192
193 let integrity = Value::Map(vec![
194 (
195 Value::Integer(1.into()),
196 Value::Bytes(self.integrity.index_digest.to_vec()),
197 ),
198 (
199 Value::Integer(2.into()),
200 Value::Bytes(self.integrity.data_digest.to_vec()),
201 ),
202 ]);
203
204 let mut continuity_entries = Vec::new();
205 if let Some(prev) = &self.continuity.prev_manifest_digest {
206 continuity_entries.push((Value::Integer(1.into()), Value::Bytes(prev.to_vec())));
207 }
208 if let Some(base) = &self.continuity.base_manifest_digest {
209 continuity_entries.push((Value::Integer(2.into()), Value::Bytes(base.to_vec())));
210 }
211 continuity_entries.push((
212 Value::Integer(3.into()),
213 Value::Integer((self.continuity.chain_length).into()),
214 ));
215 let continuity = Value::Map(continuity_entries);
216
217 let mut root = vec![
218 (
219 Value::Integer(1.into()),
220 Value::Integer((self.format_version).into()),
221 ),
222 (
223 Value::Integer(2.into()),
224 Value::Bytes(self.bundle_id.as_bytes().to_vec()),
225 ),
226 (
227 Value::Integer(3.into()),
228 Value::Text(self.producer_id.clone()),
229 ),
230 (
231 Value::Integer(4.into()),
232 Value::Integer((self.created_at).into()),
233 ),
234 (
235 Value::Integer(5.into()),
236 Value::Integer((self.bundle_type as u8).into()),
237 ),
238 (Value::Integer(6.into()), Value::Array(ca_scopes)),
239 (Value::Integer(7.into()), window),
240 (Value::Integer(8.into()), integrity),
241 (
242 Value::Integer(9.into()),
243 Value::Integer((self.entry_count).into()),
244 ),
245 (Value::Integer(10.into()), continuity),
246 ];
247
248 if let Some(shard) = &self.shard {
249 root.push((
250 Value::Integer(11.into()),
251 Value::Map(vec![
252 (
253 Value::Integer(1.into()),
254 Value::Integer((shard.shard_index).into()),
255 ),
256 (
257 Value::Integer(2.into()),
258 Value::Integer((shard.shard_count).into()),
259 ),
260 (
261 Value::Integer(3.into()),
262 Value::Integer((shard.shard_fn).into()),
263 ),
264 ]),
265 ));
266 }
267
268 if let Some(comp) = &self.compression {
269 let mut comp_entries = vec![(
270 Value::Integer(1.into()),
271 Value::Integer((comp.algorithm as u8).into()),
272 )];
273 if let Some(dict) = &comp.dictionary_digest {
274 comp_entries.push((Value::Integer(2.into()), Value::Bytes(dict.to_vec())));
275 }
276 root.push((Value::Integer(12.into()), Value::Map(comp_entries)));
277 }
278
279 if let Some(exts) = &self.extensions {
280 let ext_map: Vec<(Value, Value)> = exts
281 .iter()
282 .map(|(k, v)| (Value::Text(k.clone()), v.clone()))
283 .collect();
284 root.push((Value::Integer(13.into()), Value::Map(ext_map)));
285 }
286
287 let root_val = Value::Map(root);
288 let mut buf = Vec::new();
289 ciborium::into_writer(&root_val, &mut buf).expect("CBOR encoding cannot fail for Value");
290 buf
291 }
292
293 pub fn from_cbor(data: &[u8]) -> crate::error::Result<Self> {
294 let val: ciborium::Value = ciborium::from_reader(data)
295 .map_err(|e| crate::error::AhuError::ManifestDecode(e.to_string()))?;
296
297 let map = val
298 .as_map()
299 .ok_or_else(|| crate::error::AhuError::ManifestDecode("root is not a map".into()))?;
300
301 fn get_uint(
302 map: &[(ciborium::Value, ciborium::Value)],
303 key: i128,
304 ) -> crate::error::Result<u64> {
305 for (k, v) in map {
306 if let Some(ki) = k.as_integer() {
307 if i128::from(ki) == key {
308 if let Some(vi) = v.as_integer() {
309 let n: i128 = vi.into();
310 return u64::try_from(n).map_err(|_| {
311 crate::error::AhuError::ManifestField(format!(
312 "key {key}: integer out of u64 range"
313 ))
314 });
315 }
316 return Err(crate::error::AhuError::ManifestField(format!(
317 "key {key}: expected integer"
318 )));
319 }
320 }
321 }
322 Err(crate::error::AhuError::ManifestField(format!(
323 "key {key}: missing"
324 )))
325 }
326
327 fn get_bytes(
328 map: &[(ciborium::Value, ciborium::Value)],
329 key: i128,
330 ) -> crate::error::Result<Vec<u8>> {
331 for (k, v) in map {
332 if let Some(ki) = k.as_integer() {
333 if i128::from(ki) == key {
334 if let Some(b) = v.as_bytes() {
335 return Ok(b.to_vec());
336 }
337 return Err(crate::error::AhuError::ManifestField(format!(
338 "key {key}: expected bytes"
339 )));
340 }
341 }
342 }
343 Err(crate::error::AhuError::ManifestField(format!(
344 "key {key}: missing"
345 )))
346 }
347
348 fn get_text(
349 map: &[(ciborium::Value, ciborium::Value)],
350 key: i128,
351 ) -> crate::error::Result<String> {
352 for (k, v) in map {
353 if let Some(ki) = k.as_integer() {
354 if i128::from(ki) == key {
355 if let Some(s) = v.as_text() {
356 return Ok(s.to_string());
357 }
358 return Err(crate::error::AhuError::ManifestField(format!(
359 "key {key}: expected text"
360 )));
361 }
362 }
363 }
364 Err(crate::error::AhuError::ManifestField(format!(
365 "key {key}: missing"
366 )))
367 }
368
369 fn get_map(
370 map: &[(ciborium::Value, ciborium::Value)],
371 key: i128,
372 ) -> crate::error::Result<&[(ciborium::Value, ciborium::Value)]> {
373 for (k, v) in map {
374 if let Some(ki) = k.as_integer() {
375 if i128::from(ki) == key {
376 if let Some(m) = v.as_map() {
377 return Ok(m);
378 }
379 return Err(crate::error::AhuError::ManifestField(format!(
380 "key {key}: expected map"
381 )));
382 }
383 }
384 }
385 Err(crate::error::AhuError::ManifestField(format!(
386 "key {key}: missing"
387 )))
388 }
389
390 fn get_array(
391 map: &[(ciborium::Value, ciborium::Value)],
392 key: i128,
393 ) -> crate::error::Result<&[ciborium::Value]> {
394 for (k, v) in map {
395 if let Some(ki) = k.as_integer() {
396 if i128::from(ki) == key {
397 if let Some(a) = v.as_array() {
398 return Ok(a);
399 }
400 return Err(crate::error::AhuError::ManifestField(format!(
401 "key {key}: expected array"
402 )));
403 }
404 }
405 }
406 Err(crate::error::AhuError::ManifestField(format!(
407 "key {key}: missing"
408 )))
409 }
410
411 fn get_optional_bytes(
412 map: &[(ciborium::Value, ciborium::Value)],
413 key: i128,
414 ) -> crate::error::Result<Option<Vec<u8>>> {
415 for (k, v) in map {
416 if let Some(ki) = k.as_integer() {
417 if i128::from(ki) == key {
418 if let Some(b) = v.as_bytes() {
419 return Ok(Some(b.to_vec()));
420 }
421 return Err(crate::error::AhuError::ManifestField(format!(
422 "key {key}: expected bytes"
423 )));
424 }
425 }
426 }
427 Ok(None)
428 }
429
430 fn get_optional_map(
431 map: &[(ciborium::Value, ciborium::Value)],
432 key: i128,
433 ) -> Option<&[(ciborium::Value, ciborium::Value)]> {
434 for (k, v) in map {
435 if let Some(ki) = k.as_integer() {
436 if i128::from(ki) == key {
437 return v.as_map().map(|v| v.as_slice());
438 }
439 }
440 }
441 None
442 }
443
444 fn to_fixed_32(v: &[u8]) -> crate::error::Result<[u8; 32]> {
445 v.try_into().map_err(|_| {
446 crate::error::AhuError::ManifestField("expected 32-byte digest".into())
447 })
448 }
449
450 let format_version = get_uint(map, 1)?;
451 let bundle_id_bytes = get_bytes(map, 2)?;
452 let bundle_id = Uuid::from_slice(&bundle_id_bytes)
453 .map_err(|e| crate::error::AhuError::ManifestField(format!("bundle_id: {e}")))?;
454 let producer_id = get_text(map, 3)?;
455 let created_at = get_uint(map, 4)?;
456 let bundle_type_raw = get_uint(map, 5)?;
457 let bundle_type = match bundle_type_raw {
458 0 => BundleType::Full,
459 1 => BundleType::Delta,
460 _ => {
461 return Err(crate::error::AhuError::ManifestField(format!(
462 "unknown bundle_type: {bundle_type_raw}"
463 )));
464 }
465 };
466
467 let ca_scope_arr = get_array(map, 6)?;
468 let mut ca_scopes = Vec::with_capacity(ca_scope_arr.len());
469 for scope_val in ca_scope_arr {
470 let scope_map = scope_val.as_map().ok_or_else(|| {
471 crate::error::AhuError::ManifestField("ca_scope entry is not a map".into())
472 })?;
473
474 let rid_map = get_map(scope_map, 5)?;
475 let responder_id = ResponderId {
476 id_type: match get_uint(rid_map, 1)? {
477 0 => ResponderIdType::ByName,
478 1 => ResponderIdType::ByKey,
479 n => {
480 return Err(crate::error::AhuError::ManifestField(format!(
481 "unknown responder_id type: {n}"
482 )));
483 }
484 },
485 value: get_bytes(rid_map, 2)?,
486 };
487
488 let responder_chain = {
489 let mut chain = None;
490 for (k, v) in scope_map {
491 if let Some(ki) = k.as_integer() {
492 if i128::from(ki) == 6 {
493 if let Some(arr) = v.as_array() {
494 let certs: crate::error::Result<Vec<Vec<u8>>> = arr
495 .iter()
496 .map(|c| {
497 c.as_bytes().map(|b| b.to_vec()).ok_or_else(|| {
498 crate::error::AhuError::ManifestField(
499 "responder chain entry is not bytes".into(),
500 )
501 })
502 })
503 .collect();
504 chain = Some(certs?);
505 }
506 }
507 }
508 }
509 chain
510 };
511
512 ca_scopes.push(CaScope {
513 hash_algorithm: get_bytes(scope_map, 1)?,
514 issuer_name_hash: get_bytes(scope_map, 2)?,
515 issuer_key_hash: get_bytes(scope_map, 3)?,
516 epoch: get_uint(scope_map, 4)?,
517 responder_id,
518 responder_chain,
519 signature_algorithm: get_bytes(scope_map, 7)?,
520 completeness: match get_uint(scope_map, 8)? {
521 0 => Completeness::Partial,
522 1 => Completeness::AuthoritativeComplete,
523 n => {
524 return Err(crate::error::AhuError::ManifestField(format!(
525 "unknown completeness: {n}"
526 )));
527 }
528 },
529 });
530 }
531
532 let window_map = get_map(map, 7)?;
533 let window = Window {
534 produced_at: get_uint(window_map, 1)?,
535 this_update_min: get_uint(window_map, 2)?,
536 next_update_min: get_uint(window_map, 3)?,
537 next_update_max: get_uint(window_map, 4)?,
538 };
539
540 let integrity_map = get_map(map, 8)?;
541 let integrity = Integrity {
542 index_digest: to_fixed_32(&get_bytes(integrity_map, 1)?)?,
543 data_digest: to_fixed_32(&get_bytes(integrity_map, 2)?)?,
544 };
545
546 let entry_count = get_uint(map, 9)?;
547
548 let continuity_map = get_map(map, 10)?;
549 let continuity = Continuity {
550 prev_manifest_digest: get_optional_bytes(continuity_map, 1)?
551 .map(|b| to_fixed_32(&b))
552 .transpose()?,
553 base_manifest_digest: get_optional_bytes(continuity_map, 2)?
554 .map(|b| to_fixed_32(&b))
555 .transpose()?,
556 chain_length: get_uint(continuity_map, 3)?,
557 };
558
559 let shard = get_optional_map(map, 11)
560 .map(|sm| -> crate::error::Result<Shard> {
561 Ok(Shard {
562 shard_index: get_uint(sm, 1)?,
563 shard_count: get_uint(sm, 2)?,
564 shard_fn: get_uint(sm, 3)?,
565 })
566 })
567 .transpose()?;
568
569 let compression = get_optional_map(map, 12)
570 .map(|cm| -> crate::error::Result<Compression> {
571 let algo = match get_uint(cm, 1)? {
572 0 => CompressionAlgorithm::None,
573 1 => CompressionAlgorithm::Zstd,
574 n => {
575 return Err(crate::error::AhuError::ManifestField(format!(
576 "unknown compression algorithm: {n}"
577 )));
578 }
579 };
580 let dict = get_optional_bytes(cm, 2)?
581 .map(|b| to_fixed_32(&b))
582 .transpose()?;
583 Ok(Compression {
584 algorithm: algo,
585 dictionary_digest: dict,
586 })
587 })
588 .transpose()?;
589
590 let extensions = get_optional_map(map, 13).map(|ext_map| {
591 ext_map
592 .iter()
593 .filter_map(|(k, v)| k.as_text().map(|key| (key.to_string(), v.clone())))
594 .collect::<Vec<_>>()
595 });
596
597 Ok(Manifest {
598 format_version,
599 bundle_id,
600 producer_id,
601 created_at,
602 bundle_type,
603 ca_scopes,
604 window,
605 integrity,
606 entry_count,
607 continuity,
608 shard,
609 compression,
610 extensions,
611 })
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 fn sample_manifest() -> Manifest {
620 Manifest {
621 format_version: 1,
622 bundle_id: Uuid::nil(),
623 producer_id: "test-producer".into(),
624 created_at: 1700000000,
625 bundle_type: BundleType::Full,
626 ca_scopes: vec![CaScope {
627 hash_algorithm: vec![0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01], issuer_name_hash: vec![0xAA; 32],
629 issuer_key_hash: vec![0xBB; 32],
630 epoch: 1,
631 responder_id: ResponderId {
632 id_type: ResponderIdType::ByKey,
633 value: vec![0xCC; 20],
634 },
635 responder_chain: None,
636 signature_algorithm: vec![0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02], completeness: Completeness::AuthoritativeComplete,
638 }],
639 window: Window {
640 produced_at: 1700000000,
641 this_update_min: 1700000000,
642 next_update_min: 1700086400,
643 next_update_max: 1700093600,
644 },
645 integrity: Integrity {
646 index_digest: [0x11; 32],
647 data_digest: [0x22; 32],
648 },
649 entry_count: 100,
650 continuity: Continuity {
651 prev_manifest_digest: None,
652 base_manifest_digest: None,
653 chain_length: 0,
654 },
655 shard: None,
656 compression: None,
657 extensions: None,
658 }
659 }
660
661 #[test]
662 fn cbor_round_trip() {
663 let manifest = sample_manifest();
664 let cbor = manifest.to_cbor();
665 let decoded = Manifest::from_cbor(&cbor).unwrap();
666
667 assert_eq!(manifest.format_version, decoded.format_version);
668 assert_eq!(manifest.bundle_id, decoded.bundle_id);
669 assert_eq!(manifest.producer_id, decoded.producer_id);
670 assert_eq!(manifest.bundle_type, decoded.bundle_type);
671 assert_eq!(manifest.entry_count, decoded.entry_count);
672 assert_eq!(manifest.ca_scopes.len(), decoded.ca_scopes.len());
673 assert_eq!(manifest.window, decoded.window);
674 assert_eq!(manifest.integrity, decoded.integrity);
675 assert_eq!(manifest.continuity, decoded.continuity);
676 }
677
678 #[test]
679 fn deterministic_encoding() {
680 let manifest = sample_manifest();
681 let cbor1 = manifest.to_cbor();
682 let cbor2 = manifest.to_cbor();
683 assert_eq!(cbor1, cbor2, "CBOR encoding must be deterministic");
684 }
685}