1use crate::signing::{BundleSignature, SignaturePolicy, SigningKey, TrustStore};
2use crate::validation::{BundleParts, ModuleRecord, validate_archive_parts};
3use crate::{
4 BundleError, CEDAR_VERSION, Diagnostic, FORMAT_VERSION, LabelSet, Result, TREETOP_CORE_VERSION,
5};
6use flate2::bufread::GzDecoder;
7use flate2::{Compression, GzBuilder};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use sha2::{Digest, Sha256};
11use std::collections::HashSet;
12use std::fs::File;
13use std::io::{self, Cursor, Read, Write};
14use std::path::{Component, Path};
15use tar::{Archive, Builder, EntryType, Header};
16use treetop_core::{LabelRegistryBuilder, PolicyEngine, PolicyStoreConfig, PolicyStoreLayout};
17
18const MANIFEST_PATH: &str = "manifest.json";
19const SIGNATURE_PATH: &str = "signature.json";
20const POLICIES_PATH: &str = "policies.cedar";
21const SCHEMA_PATH: &str = "schema.json";
22const LABELS_PATH: &str = "labels.json";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ArchiveLimits {
27 max_compressed_bytes: usize,
28 max_uncompressed_bytes: usize,
29}
30
31impl ArchiveLimits {
32 pub const DEFAULT_MAX_COMPRESSED_BYTES: usize = 10 * 1024 * 1024;
33 pub const DEFAULT_MAX_UNCOMPRESSED_BYTES: usize = 50 * 1024 * 1024;
34
35 pub fn new(max_compressed_bytes: usize, max_uncompressed_bytes: usize) -> Result<Self> {
36 if max_compressed_bytes == 0 || max_uncompressed_bytes == 0 {
37 return Err(BundleError::Archive(
38 "archive size limits must be greater than zero".to_string(),
39 ));
40 }
41 limit_plus_one(max_compressed_bytes, "compressed")?;
42 limit_plus_one(max_uncompressed_bytes, "uncompressed")?;
43 Ok(Self {
44 max_compressed_bytes,
45 max_uncompressed_bytes,
46 })
47 }
48
49 pub fn max_compressed_bytes(&self) -> usize {
50 self.max_compressed_bytes
51 }
52
53 pub fn max_uncompressed_bytes(&self) -> usize {
54 self.max_uncompressed_bytes
55 }
56}
57
58impl Default for ArchiveLimits {
59 fn default() -> Self {
60 Self {
61 max_compressed_bytes: Self::DEFAULT_MAX_COMPRESSED_BYTES,
62 max_uncompressed_bytes: Self::DEFAULT_MAX_UNCOMPRESSED_BYTES,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69struct GeneratorRecord {
70 treetop_bundle: String,
71 treetop_core: String,
72 cedar: String,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(deny_unknown_fields)]
77struct ArtifactRecord {
78 path: String,
79 size: usize,
80 sha256: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85struct ArchiveManifest {
86 format_version: u32,
87 bundle_id: String,
88 name: String,
89 generator: GeneratorRecord,
90 modules: Vec<ModuleRecord>,
91 policy_ids: Vec<String>,
92 artifacts: Vec<ArtifactRecord>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct VerifiedSignature {
99 signed: bool,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 key_id: Option<String>,
102}
103
104impl VerifiedSignature {
105 pub fn is_signed(&self) -> bool {
106 self.signed
107 }
108
109 pub fn key_id(&self) -> Option<&str> {
110 self.key_id.as_deref()
111 }
112}
113
114pub struct ValidatedBundle {
116 format_version: u32,
117 bundle_id: String,
118 name: String,
119 modules: Vec<ModuleRecord>,
120 policies: String,
121 schema_json: Option<Value>,
122 labels: LabelSet,
123 policy_ids: Vec<String>,
124 diagnostics: Vec<Diagnostic>,
125 archive_sha256: String,
126 compressed_size: usize,
127 signature: VerifiedSignature,
128}
129
130impl ValidatedBundle {
131 pub fn format_version(&self) -> u32 {
132 self.format_version
133 }
134
135 pub fn bundle_id(&self) -> &str {
136 &self.bundle_id
137 }
138
139 pub fn name(&self) -> &str {
140 &self.name
141 }
142
143 pub fn module_count(&self) -> usize {
144 self.modules.len()
145 }
146
147 pub fn policies(&self) -> &str {
148 &self.policies
149 }
150
151 pub fn schema_json(&self) -> Option<&Value> {
152 self.schema_json.as_ref()
153 }
154
155 pub fn schema_json_string(&self) -> Result<Option<String>> {
156 self.schema_json
157 .as_ref()
158 .map(canonical_json_bytes)
159 .transpose()
160 .and_then(|value| {
161 value
162 .map(|bytes| {
163 String::from_utf8(bytes)
164 .map_err(|error| BundleError::Serialization(error.to_string()))
165 })
166 .transpose()
167 })
168 }
169
170 pub fn labels(&self) -> &LabelSet {
171 &self.labels
172 }
173
174 pub fn labels_json(&self) -> Result<String> {
175 let bytes = canonical_json_bytes(&self.labels)?;
176 String::from_utf8(bytes).map_err(|error| BundleError::Serialization(error.to_string()))
177 }
178
179 pub fn policy_ids(&self) -> &[String] {
180 &self.policy_ids
181 }
182
183 pub fn diagnostics(&self) -> &[Diagnostic] {
184 &self.diagnostics
185 }
186
187 pub fn archive_sha256(&self) -> &str {
188 &self.archive_sha256
189 }
190
191 pub fn compressed_size(&self) -> usize {
192 self.compressed_size
193 }
194
195 pub fn verified_signature(&self) -> &VerifiedSignature {
196 &self.signature
197 }
198
199 pub fn prepare_engine(&self) -> Result<PolicyEngine> {
201 self.prepare_engine_with_layout(None)
202 }
203
204 pub fn prepare_engine_with_policy_stores(&self) -> Result<PolicyEngine> {
214 let stores = self
215 .modules
216 .iter()
217 .filter(|module| module.role == crate::ModuleRole::Ordinary)
218 .map(|module| PolicyStoreConfig::new(&module.name, &module.namespace))
219 .collect::<std::result::Result<Vec<_>, _>>()
220 .map_err(policy_engine_error)?;
221 let global_policy_ids = self
222 .modules
223 .iter()
224 .filter(|module| module.role == crate::ModuleRole::Global)
225 .flat_map(|module| module.policy_ids.iter());
226 let layout = PolicyStoreLayout::new(stores)
227 .and_then(|layout| layout.with_global_policy_ids(global_policy_ids))
228 .map_err(policy_engine_error)?;
229
230 self.prepare_engine_with_layout(Some(layout))
231 }
232
233 fn prepare_engine_with_layout(
234 &self,
235 layout: Option<PolicyStoreLayout>,
236 ) -> Result<PolicyEngine> {
237 let mut engine = match (&self.schema_json, layout) {
238 (Some(schema), layout) => {
239 let schema =
240 cedar_policy::Schema::from_json_value(schema.clone()).map_err(|error| {
241 BundleError::Validation(vec![Diagnostic::error(
242 "schema.aggregate_invalid",
243 error.to_string(),
244 )])
245 })?;
246 match layout {
247 Some(layout) => PolicyEngine::new_from_str_with_schema_and_policy_stores(
248 &self.policies,
249 schema,
250 layout,
251 ),
252 None => PolicyEngine::new_from_str_with_schema(&self.policies, schema),
253 }
254 .map_err(policy_engine_error)?
255 }
256 (None, Some(layout)) => {
257 PolicyEngine::new_from_str_with_policy_stores(&self.policies, layout)
258 .map_err(policy_engine_error)?
259 }
260 (None, None) => {
261 PolicyEngine::new_from_str(&self.policies).map_err(policy_engine_error)?
262 }
263 };
264 let labelers = self.labels.to_labelers();
265 if !labelers.is_empty() {
266 let mut builder = LabelRegistryBuilder::new();
267 for labeler in labelers {
268 builder = builder.add_labeler(labeler);
269 }
270 engine = engine.with_label_registry(builder.build());
271 }
272 Ok(engine)
273 }
274}
275
276fn policy_engine_error(error: impl ToString) -> BundleError {
277 BundleError::Validation(vec![Diagnostic::error(
278 "policy.engine_prepare",
279 error.to_string(),
280 )])
281}
282
283#[derive(Debug, Clone)]
285pub struct BundleArchive {
286 bytes: Vec<u8>,
287}
288
289impl BundleArchive {
290 pub fn from_bytes(bytes: Vec<u8>) -> Self {
291 Self { bytes }
292 }
293
294 pub fn read(path: impl AsRef<Path>, max_compressed_bytes: usize) -> Result<Self> {
295 let path = path.as_ref();
296 let file = File::open(path).map_err(|error| BundleError::io(path, error))?;
297 let metadata = file
298 .metadata()
299 .map_err(|error| BundleError::io(path, error))?;
300 if !metadata.is_file() {
301 return Err(BundleError::io(
302 path,
303 io::Error::new(io::ErrorKind::InvalidInput, "archive is not a regular file"),
304 ));
305 }
306 if metadata.len() > max_compressed_bytes as u64 {
307 return Err(BundleError::SizeLimit {
308 kind: "compressed",
309 limit: max_compressed_bytes,
310 });
311 }
312 let mut bytes = Vec::with_capacity(
313 usize::try_from(metadata.len())
314 .unwrap_or(max_compressed_bytes)
315 .min(max_compressed_bytes),
316 );
317 file.take(limit_plus_one(max_compressed_bytes, "compressed")?)
318 .read_to_end(&mut bytes)
319 .map_err(|error| BundleError::io(path, error))?;
320 if bytes.len() > max_compressed_bytes {
321 return Err(BundleError::SizeLimit {
322 kind: "compressed",
323 limit: max_compressed_bytes,
324 });
325 }
326 Ok(Self { bytes })
327 }
328
329 pub fn as_bytes(&self) -> &[u8] {
330 &self.bytes
331 }
332
333 pub fn into_bytes(self) -> Vec<u8> {
334 self.bytes
335 }
336
337 pub fn sha256(&self) -> String {
338 sha256_hex(&self.bytes)
339 }
340
341 pub fn validate(
342 &self,
343 signature_policy: SignaturePolicy,
344 trust_store: &TrustStore,
345 limits: ArchiveLimits,
346 ) -> Result<ValidatedBundle> {
347 self.validate_inner(signature_policy, trust_store, limits, true)
348 }
349
350 pub fn resign(&self, key: &SigningKey, limits: ArchiveLimits) -> Result<Self> {
352 let decoded = decode_archive(&self.bytes, limits)?;
353 validate_decoded(
354 &decoded,
355 SignaturePolicy::AllowUnsigned,
356 &TrustStore::new(),
357 false,
358 )?;
359 let signature = key.sign_manifest(&decoded.manifest);
360 let signature_bytes = canonical_json_bytes(&signature)?;
361 let bytes = encode_archive(
362 &decoded.manifest,
363 Some(&signature_bytes),
364 artifact_entries(&decoded),
365 )?;
366 Ok(Self { bytes })
367 }
368
369 fn validate_inner(
370 &self,
371 signature_policy: SignaturePolicy,
372 trust_store: &TrustStore,
373 limits: ArchiveLimits,
374 verify_signature: bool,
375 ) -> Result<ValidatedBundle> {
376 let decoded = decode_archive(&self.bytes, limits)?;
377 let (manifest, signature, parts) =
378 validate_decoded(&decoded, signature_policy, trust_store, verify_signature)?;
379 Ok(ValidatedBundle {
380 format_version: manifest.format_version,
381 bundle_id: manifest.bundle_id,
382 name: parts.name,
383 modules: parts.modules,
384 policies: parts.policies,
385 schema_json: parts.schema_json,
386 labels: parts.labels,
387 policy_ids: parts.policy_ids,
388 diagnostics: parts.diagnostics,
389 archive_sha256: sha256_hex(&self.bytes),
390 compressed_size: self.bytes.len(),
391 signature,
392 })
393 }
394
395 pub(crate) fn build(parts: BundleParts, key: Option<&SigningKey>) -> Result<Self> {
396 let policies = parts.policies.as_bytes().to_vec();
397 let schema = parts
398 .schema_json
399 .as_ref()
400 .map(canonical_json_bytes)
401 .transpose()?;
402 let labels = canonical_json_bytes(&parts.labels)?;
403
404 let mut artifact_data = vec![(POLICIES_PATH.to_string(), policies)];
405 if let Some(schema) = schema {
406 artifact_data.push((SCHEMA_PATH.to_string(), schema));
407 }
408 artifact_data.push((LABELS_PATH.to_string(), labels));
409 let artifacts = artifact_data
410 .iter()
411 .map(|(path, bytes)| ArtifactRecord {
412 path: path.clone(),
413 size: bytes.len(),
414 sha256: sha256_hex(bytes),
415 })
416 .collect();
417 let mut manifest = ArchiveManifest {
418 format_version: FORMAT_VERSION,
419 bundle_id: String::new(),
420 name: parts.name,
421 generator: GeneratorRecord {
422 treetop_bundle: env!("CARGO_PKG_VERSION").to_string(),
423 treetop_core: TREETOP_CORE_VERSION.to_string(),
424 cedar: CEDAR_VERSION.to_string(),
425 },
426 modules: parts.modules,
427 policy_ids: parts.policy_ids,
428 artifacts,
429 };
430 manifest.bundle_id = compute_bundle_id(&manifest)?;
431 let manifest_bytes = canonical_json_bytes(&manifest)?;
432 let signature_bytes = key
433 .map(|key| canonical_json_bytes(&key.sign_manifest(&manifest_bytes)))
434 .transpose()?;
435 let bytes = encode_archive(
436 &manifest_bytes,
437 signature_bytes.as_deref(),
438 artifact_data
439 .iter()
440 .map(|(path, contents)| (path.as_str(), contents.as_slice())),
441 )?;
442 Ok(Self { bytes })
443 }
444}
445
446struct DecodedArchive {
447 manifest: Vec<u8>,
448 signature: Option<Vec<u8>>,
449 policies: Vec<u8>,
450 schema: Option<Vec<u8>>,
451 labels: Vec<u8>,
452}
453
454fn decode_archive(bytes: &[u8], limits: ArchiveLimits) -> Result<DecodedArchive> {
455 if bytes.len() > limits.max_compressed_bytes {
456 return Err(BundleError::SizeLimit {
457 kind: "compressed",
458 limit: limits.max_compressed_bytes,
459 });
460 }
461 let cursor = Cursor::new(bytes);
462 let decoder = GzDecoder::new(cursor);
463 let limited = decoder.take(limit_plus_one(
464 limits.max_uncompressed_bytes,
465 "uncompressed",
466 )?);
467 let mut archive = Archive::new(limited);
468 let mut entries = Vec::new();
469 {
470 let archive_entries = archive
471 .entries()
472 .map_err(|error| BundleError::Archive(format!("tar decoding failed: {error}")))?;
473 for entry in archive_entries {
474 let mut entry = entry
475 .map_err(|error| BundleError::Archive(format!("tar entry is invalid: {error}")))?;
476 if entries.len() == 5 {
477 return Err(BundleError::Archive(
478 "bundle archive contains more than five entries".to_string(),
479 ));
480 }
481 if entry.header().entry_type() != EntryType::Regular {
482 return Err(BundleError::Archive(
483 "only regular tar entries are allowed".to_string(),
484 ));
485 }
486 let path = entry
487 .path()
488 .map_err(|error| BundleError::Archive(format!("invalid tar path: {error}")))?
489 .into_owned();
490 if path.is_absolute()
491 || path.components().count() != 1
492 || path.components().any(|component| {
493 matches!(
494 component,
495 Component::ParentDir | Component::RootDir | Component::Prefix(_)
496 )
497 })
498 {
499 return Err(BundleError::Archive(format!(
500 "unsafe tar entry path {}",
501 path.display()
502 )));
503 }
504 let name = path
505 .to_str()
506 .ok_or_else(|| BundleError::Archive("tar path is not UTF-8".to_string()))?
507 .to_string();
508 let mut contents = Vec::new();
509 entry
510 .read_to_end(&mut contents)
511 .map_err(|error| BundleError::Archive(format!("cannot read tar entry: {error}")))?;
512 entries.push((name, contents));
513 }
514 }
515 let mut limited = archive.into_inner();
516 io::copy(&mut limited, &mut io::sink())
517 .map_err(|error| BundleError::Archive(format!("gzip decoding failed: {error}")))?;
518 if limited.limit() == 0 {
519 return Err(BundleError::SizeLimit {
520 kind: "uncompressed",
521 limit: limits.max_uncompressed_bytes,
522 });
523 }
524 let cursor = limited.into_inner().into_inner();
525 if cursor.position() != bytes.len() as u64 {
526 return Err(BundleError::Archive(
527 "concatenated gzip members or trailing bytes are not allowed".to_string(),
528 ));
529 }
530
531 let names = entries
532 .iter()
533 .map(|(name, _)| name.as_str())
534 .collect::<Vec<_>>();
535 let valid = matches!(
536 names.as_slice(),
537 [MANIFEST_PATH, POLICIES_PATH, LABELS_PATH]
538 | [MANIFEST_PATH, POLICIES_PATH, SCHEMA_PATH, LABELS_PATH]
539 | [MANIFEST_PATH, SIGNATURE_PATH, POLICIES_PATH, LABELS_PATH]
540 | [
541 MANIFEST_PATH,
542 SIGNATURE_PATH,
543 POLICIES_PATH,
544 SCHEMA_PATH,
545 LABELS_PATH
546 ]
547 );
548 if !valid {
549 return Err(BundleError::Archive(format!(
550 "archive entries are missing, unknown, duplicated, or out of order: {names:?}"
551 )));
552 }
553
554 let mut by_name = entries
555 .into_iter()
556 .collect::<std::collections::BTreeMap<_, _>>();
557 Ok(DecodedArchive {
558 manifest: by_name
559 .remove(MANIFEST_PATH)
560 .expect("validated entry order includes manifest"),
561 signature: by_name.remove(SIGNATURE_PATH),
562 policies: by_name
563 .remove(POLICIES_PATH)
564 .expect("validated entry order includes policies"),
565 schema: by_name.remove(SCHEMA_PATH),
566 labels: by_name
567 .remove(LABELS_PATH)
568 .expect("validated entry order includes labels"),
569 })
570}
571
572fn validate_decoded(
573 decoded: &DecodedArchive,
574 signature_policy: SignaturePolicy,
575 trust_store: &TrustStore,
576 verify_signature: bool,
577) -> Result<(ArchiveManifest, VerifiedSignature, BundleParts)> {
578 let manifest: ArchiveManifest = parse_json(MANIFEST_PATH, &decoded.manifest)?;
579 let signature: Option<BundleSignature> = decoded
580 .signature
581 .as_ref()
582 .map(|bytes| parse_json(SIGNATURE_PATH, bytes))
583 .transpose()?;
584 if let Some(signature) = &signature {
585 signature.validate_format()?;
586 }
587
588 let verified_signature = match signature {
589 Some(signature) if verify_signature => VerifiedSignature {
590 signed: true,
591 key_id: Some(trust_store.verify(&decoded.manifest, &signature)?),
592 },
593 Some(signature) => VerifiedSignature {
594 signed: true,
595 key_id: Some(signature.key_id().to_string()),
596 },
597 None if signature_policy == SignaturePolicy::Required => {
598 return Err(BundleError::Archive("signature_missing".to_string()));
599 }
600 None => VerifiedSignature {
601 signed: false,
602 key_id: None,
603 },
604 };
605
606 validate_manifest(&manifest)?;
607
608 let expected = artifact_entries(decoded);
609 if manifest.artifacts.len() != expected.len() {
610 return Err(BundleError::Archive(
611 "manifest artifact list does not match archive entries".to_string(),
612 ));
613 }
614 for (record, (path, contents)) in manifest.artifacts.iter().zip(&expected) {
615 if record.path != *path
616 || record.size != contents.len()
617 || record.sha256 != sha256_hex(contents)
618 {
619 return Err(BundleError::Archive(format!(
620 "artifact hash or size mismatch for {path}"
621 )));
622 }
623 }
624 if manifest.bundle_id != compute_bundle_id(&manifest)? {
625 return Err(BundleError::Archive(
626 "manifest bundle_id does not match its canonical payload".to_string(),
627 ));
628 }
629
630 let policies = utf8(POLICIES_PATH, &decoded.policies)?;
631 let schema_json = decoded
632 .schema
633 .as_ref()
634 .map(|bytes| parse_json(SCHEMA_PATH, bytes))
635 .transpose()?;
636 let labels_source = utf8(LABELS_PATH, &decoded.labels)?;
637 let labels = LabelSet::from_json_str(&labels_source)?;
638 let parts = validate_archive_parts(
639 manifest.name.clone(),
640 manifest.modules.clone(),
641 policies,
642 schema_json,
643 labels,
644 &manifest.policy_ids,
645 )?;
646 Ok((manifest, verified_signature, parts))
647}
648
649fn validate_manifest(manifest: &ArchiveManifest) -> Result<()> {
650 if manifest.format_version != FORMAT_VERSION {
651 return Err(BundleError::Archive(format!(
652 "unsupported bundle format version {}",
653 manifest.format_version
654 )));
655 }
656 if manifest.name.trim().is_empty() {
657 return Err(BundleError::Archive(
658 "bundle manifest name must not be empty".to_string(),
659 ));
660 }
661 if manifest.generator.treetop_bundle != env!("CARGO_PKG_VERSION")
662 || manifest.generator.treetop_core != TREETOP_CORE_VERSION
663 || manifest.generator.cedar != CEDAR_VERSION
664 {
665 return Err(BundleError::Archive(
666 "bundle generator dependency versions are unsupported".to_string(),
667 ));
668 }
669 let mut module_names = HashSet::new();
670 let mut namespaces: Vec<&str> = Vec::new();
671 let mut assigned_policy_ids = HashSet::new();
672 if manifest.modules.is_empty() {
673 return Err(BundleError::Archive(
674 "manifest must contain at least one module".to_string(),
675 ));
676 }
677 if !manifest
678 .modules
679 .windows(2)
680 .all(|pair| pair[0].name < pair[1].name)
681 {
682 return Err(BundleError::Archive(
683 "manifest modules are not ordered by name".to_string(),
684 ));
685 }
686 if !manifest.policy_ids.windows(2).all(|pair| pair[0] < pair[1]) {
687 return Err(BundleError::Archive(
688 "manifest policy IDs must be sorted and unique".to_string(),
689 ));
690 }
691 let selected_namespaces = manifest
692 .modules
693 .iter()
694 .map(|module| module.namespace.as_str())
695 .collect::<HashSet<_>>();
696 for module in &manifest.modules {
697 if module.name.trim().is_empty()
698 || module.namespace.trim().is_empty()
699 || module
700 .namespace
701 .parse::<cedar_policy::EntityTypeName>()
702 .is_err()
703 {
704 return Err(BundleError::Archive(
705 "manifest contains an invalid module name or namespace".to_string(),
706 ));
707 }
708 if !module_names.insert(module.name.as_str()) {
709 return Err(BundleError::Archive(
710 "manifest contains duplicate module names".to_string(),
711 ));
712 }
713 namespaces.push(module.namespace.as_str());
714 let mut imports = HashSet::new();
715 if module.imports.iter().any(|import| {
716 !selected_namespaces.contains(import.as_str())
717 || import == &module.namespace
718 || !imports.insert(import)
719 }) {
720 return Err(BundleError::Archive(
721 "manifest contains an unresolved, self, or duplicate module import".to_string(),
722 ));
723 }
724 if module
725 .policy_ids
726 .iter()
727 .any(|policy_id| !assigned_policy_ids.insert(policy_id.as_str()))
728 {
729 return Err(BundleError::Archive(
730 "manifest assigns a policy ID more than once".to_string(),
731 ));
732 }
733 }
734 namespaces.sort_unstable();
735 if namespaces.windows(2).any(|pair| {
736 crate::manifest::namespace_owns(pair[0], pair[1])
737 || crate::manifest::namespace_owns(pair[1], pair[0])
738 }) {
739 return Err(BundleError::Archive(
740 "manifest contains overlapping module namespaces".to_string(),
741 ));
742 }
743 if assigned_policy_ids
744 != manifest
745 .policy_ids
746 .iter()
747 .map(String::as_str)
748 .collect::<HashSet<_>>()
749 {
750 return Err(BundleError::Archive(
751 "manifest module policy assignments do not match policy_ids".to_string(),
752 ));
753 }
754 Ok(())
755}
756
757fn artifact_entries(decoded: &DecodedArchive) -> Vec<(&'static str, &[u8])> {
758 let mut entries = vec![(POLICIES_PATH, decoded.policies.as_slice())];
759 if let Some(schema) = &decoded.schema {
760 entries.push((SCHEMA_PATH, schema.as_slice()));
761 }
762 entries.push((LABELS_PATH, decoded.labels.as_slice()));
763 entries
764}
765
766fn encode_archive<'a>(
767 manifest: &[u8],
768 signature: Option<&[u8]>,
769 artifacts: impl IntoIterator<Item = (&'a str, &'a [u8])>,
770) -> Result<Vec<u8>> {
771 let encoder = GzBuilder::new()
772 .mtime(0)
773 .write(Vec::new(), Compression::default());
774 let mut builder = Builder::new(encoder);
775 append_tar_file(&mut builder, MANIFEST_PATH, manifest)?;
776 if let Some(signature) = signature {
777 append_tar_file(&mut builder, SIGNATURE_PATH, signature)?;
778 }
779 for (path, contents) in artifacts {
780 append_tar_file(&mut builder, path, contents)?;
781 }
782 builder
783 .finish()
784 .map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))?;
785 let encoder = builder
786 .into_inner()
787 .map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))?;
788 encoder
789 .finish()
790 .map_err(|error| BundleError::Archive(format!("gzip encoding failed: {error}")))
791}
792
793fn append_tar_file<W: Write>(builder: &mut Builder<W>, path: &str, contents: &[u8]) -> Result<()> {
794 let mut header = Header::new_gnu();
795 header.set_entry_type(EntryType::Regular);
796 header.set_mode(0o644);
797 header.set_uid(0);
798 header.set_gid(0);
799 header.set_mtime(0);
800 header.set_size(contents.len() as u64);
801 header.set_cksum();
802 builder
803 .append_data(&mut header, path, Cursor::new(contents))
804 .map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))
805}
806
807fn compute_bundle_id(manifest: &ArchiveManifest) -> Result<String> {
808 let mut payload = serde_json::to_value(manifest)
809 .map_err(|error| BundleError::Serialization(error.to_string()))?;
810 payload
811 .as_object_mut()
812 .expect("ArchiveManifest always serializes as an object")
813 .remove("bundle_id");
814 Ok(sha256_hex(&canonical_json_bytes(&payload)?))
815}
816
817pub(crate) fn canonical_json_bytes(value: &impl Serialize) -> Result<Vec<u8>> {
818 let value = serde_json::to_value(value)
819 .map_err(|error| BundleError::Serialization(error.to_string()))?;
820 let value = sort_json(value);
821 let mut bytes = serde_json::to_vec(&value)
822 .map_err(|error| BundleError::Serialization(error.to_string()))?;
823 bytes.push(b'\n');
824 Ok(bytes)
825}
826
827fn sort_json(value: Value) -> Value {
828 match value {
829 Value::Object(object) => {
830 let sorted = object
831 .into_iter()
832 .map(|(key, value)| (key, sort_json(value)))
833 .collect::<std::collections::BTreeMap<_, _>>();
834 Value::Object(sorted.into_iter().collect::<Map<_, _>>())
835 }
836 Value::Array(values) => Value::Array(values.into_iter().map(sort_json).collect()),
837 other => other,
838 }
839}
840
841fn parse_json<T: for<'de> Deserialize<'de>>(path: &str, bytes: &[u8]) -> Result<T> {
842 let source = std::str::from_utf8(bytes)
843 .map_err(|error| BundleError::Archive(format!("{path} is not UTF-8: {error}")))?;
844 serde_json::from_str(source)
845 .map_err(|error| BundleError::Archive(format!("{path} is invalid JSON: {error}")))
846}
847
848fn utf8(path: &str, bytes: &[u8]) -> Result<String> {
849 String::from_utf8(bytes.to_vec())
850 .map_err(|error| BundleError::Archive(format!("{path} is not UTF-8: {error}")))
851}
852
853fn sha256_hex(bytes: &[u8]) -> String {
854 Sha256::digest(bytes)
855 .iter()
856 .map(|byte| format!("{byte:02x}"))
857 .collect()
858}
859
860fn limit_plus_one(limit: usize, kind: &'static str) -> Result<u64> {
861 u64::try_from(limit)
862 .ok()
863 .and_then(|limit| limit.checked_add(1))
864 .ok_or_else(|| BundleError::Archive(format!("{kind} size limit is too large to enforce")))
865}