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