1use crate::cc_attestation;
2use crate::error::{Error, Result};
3use crate::hash;
4use crate::in_toto;
5use crate::manifest::config::ManifestCreationConfig;
6use crate::manifest::utils::{
7 determine_dataset_type, determine_format, determine_model_type, determine_software_type,
8};
9use crate::signing::signable::Signable;
10use crate::storage::traits::{ArtifactLocation, StorageBackend};
11use atlas_c2pa_lib::assertion::{
12 Action, ActionAssertion, Assertion, Author, CreativeWorkAssertion, CustomAssertion,
13};
14use atlas_c2pa_lib::asset_type::AssetType;
15use atlas_c2pa_lib::claim::ClaimV2;
16use atlas_c2pa_lib::cose::HashAlgorithm;
17use atlas_c2pa_lib::cross_reference::CrossReference;
18use atlas_c2pa_lib::datetime_wrapper::OffsetDateTimeWrapper;
19use atlas_c2pa_lib::ingredient::{Ingredient, IngredientData};
20use atlas_c2pa_lib::manifest::Manifest;
21use serde_json::{to_string, to_string_pretty};
22use std::path::{Path, PathBuf};
23use tdx_workload_attestation::get_platform_name;
24use time::OffsetDateTime;
25use uuid::Uuid;
26
27const CLAIM_GENERATOR: &str = "atlas-cli:0.2.0";
28
29pub enum AssetKind {
31 Model,
32 Dataset,
33 Software,
34 Evaluation,
35}
36
37fn generate_c2pa_assertions(
43 config: &ManifestCreationConfig,
44 asset_kind: AssetKind,
45) -> Result<Vec<Assertion>> {
46 let (creative_type, digital_source_type) = match asset_kind {
48 AssetKind::Model => (
49 "Model".to_string(),
50 "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia".to_string(),
51 ),
52 AssetKind::Dataset => (
53 "Dataset".to_string(),
54 "http://cv.iptc.org/newscodes/digitalsourcetype/dataset".to_string(),
55 ),
56 AssetKind::Software => (
57 "Software".to_string(),
58 "http://cv.iptc.org/newscodes/digitalsourcetype/software".to_string(),
59 ),
60 AssetKind::Evaluation => (
61 "EvaluationResult".to_string(),
62 "http://cv.iptc.org/newscodes/digitalsourcetype/evaluationResult".to_string(),
63 ),
64 };
65
66 let mut assertions = vec![
68 Assertion::CreativeWork(CreativeWorkAssertion {
69 context: "http://schema.org/".to_string(),
70 creative_type,
71 author: vec![
72 Author {
73 author_type: "Organization".to_string(),
74 name: config
75 .author_org
76 .clone()
77 .unwrap_or_else(|| "Organization".to_string()),
78 },
79 Author {
80 author_type: "Person".to_string(),
81 name: config
82 .author_name
83 .clone()
84 .unwrap_or_else(|| "Unknown".to_string()),
85 },
86 ],
87 }),
88 Assertion::Action(ActionAssertion {
89 actions: vec![Action {
90 action: match asset_kind {
91 AssetKind::Evaluation => "c2pa.evaluation".to_string(),
92 _ => "c2pa.created".to_string(),
93 },
94 software_agent: Some(CLAIM_GENERATOR.to_string()),
95 parameters: Some(match asset_kind {
96 AssetKind::Evaluation => {
97 let mut params = serde_json::json!({
99 "name": config.name,
100 "description": config.description,
101 "author": {
102 "organization": config.author_org,
103 "name": config.author_name
104 }
105 });
106
107 if let Some(config_params) = &config.custom_fields {
109 if let Some(eval_params) = config_params.get("evaluation") {
110 if let Some(obj) = params.as_object_mut() {
111 obj.insert(
112 "model_id".to_string(),
113 eval_params
114 .get("model_id")
115 .cloned()
116 .unwrap_or(serde_json::Value::Null),
117 );
118 obj.insert(
119 "dataset_id".to_string(),
120 eval_params
121 .get("dataset_id")
122 .cloned()
123 .unwrap_or(serde_json::Value::Null),
124 );
125 obj.insert(
126 "metrics".to_string(),
127 eval_params
128 .get("metrics")
129 .cloned()
130 .unwrap_or(serde_json::Value::Null),
131 );
132 }
133 }
134 }
135 params
136 }
137 AssetKind::Software => {
138 let mut params = serde_json::json!({
139 "name": config.name,
140 "description": config.description,
141 "author": {
142 "organization": config.author_org,
143 "name": config.author_name
144 }
145 });
146
147 if let Some(software_type) = &config.software_type {
148 params.as_object_mut().unwrap().insert(
149 "software_type".to_string(),
150 serde_json::Value::String(software_type.clone()),
151 );
152 }
153 if let Some(version) = &config.version {
154 params.as_object_mut().unwrap().insert(
155 "version".to_string(),
156 serde_json::Value::String(version.clone()),
157 );
158 }
159 params
160 }
161 _ => serde_json::json!({}),
164 }),
165 digital_source_type: Some(digital_source_type),
166 instance_id: None,
167 }],
168 }),
169 ];
170
171 if config.with_cc {
174 let cc_assertion = get_cc_attestation_assertion().unwrap();
176
177 assertions.push(Assertion::CustomAssertion(cc_assertion));
178 }
179
180 Ok(assertions)
181}
182
183fn generate_c2pa_claim(config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result<ClaimV2> {
190 let mut ingredients = Vec::new();
192
193 for (path, ingredient_name) in config.paths.iter().zip(config.ingredient_names.iter()) {
194 let format = determine_format(path)?;
196 let asset_type = match asset_kind {
197 AssetKind::Model => determine_model_type(path)?,
198 AssetKind::Dataset => determine_dataset_type(path)?,
199 AssetKind::Software => determine_software_type(path)?,
200 AssetKind::Evaluation => AssetType::Dataset, };
202
203 let ingredient = create_ingredient_from_path_with_algorithm(
205 path,
206 ingredient_name,
207 asset_type,
208 format,
209 &config.hash_alg,
210 )?;
211 ingredients.push(ingredient);
212 }
213
214 ingredients.sort_by_key(|ingredient| ingredient.title.to_lowercase());
220
221 let assertions = generate_c2pa_assertions(config, asset_kind)?;
222
223 Ok(ClaimV2 {
225 instance_id: format!("urn:c2pa:{}", Uuid::new_v4()),
226 ingredients: ingredients.clone(),
227 created_assertions: assertions,
228 claim_generator_info: CLAIM_GENERATOR.to_string(),
229 signature: None,
230 created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()),
231 })
232}
233
234pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> {
236 let claim = generate_c2pa_claim(&config, asset_kind)?;
237
238 let mut manifest = Manifest {
240 claim_generator: CLAIM_GENERATOR.to_string(),
241 title: config.name.clone(),
242 instance_id: format!("urn:c2pa:{}", Uuid::new_v4()),
243 claim: claim.clone(),
244 ingredients: vec![],
245 created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()),
246 cross_references: vec![],
247 claim_v2: Some(claim),
248 is_active: true,
249 };
250
251 if let Some(key_file) = &config.key_path {
253 manifest.sign(key_file.to_path_buf(), config.hash_alg)?;
254 }
255
256 if let Some(manifest_ids) = &config.linked_manifests {
257 if let Some(storage_backend) = &config.storage {
258 for linked_id in manifest_ids {
259 match storage_backend.retrieve_manifest(linked_id) {
260 Ok(linked_manifest) => {
261 let linked_json = serde_json::to_string(&linked_manifest)
263 .map_err(|e| Error::Serialization(e.to_string()))?;
264
265 let linked_hash = hash::calculate_hash(linked_json.as_bytes());
267
268 let cross_ref = CrossReference {
270 manifest_url: linked_id.clone(),
271 manifest_hash: linked_hash,
272 media_type: Some("application/json".to_string()),
273 };
274
275 manifest.cross_references.push(cross_ref);
277
278 println!("Added link to manifest: {linked_id}");
279 }
280 Err(e) => {
281 println!("Warning: Could not link to manifest {linked_id}: {e}");
282 }
283 }
284 }
285 } else {
286 println!("Warning: Cannot link manifests without a storage backend");
287 }
288 }
289
290 if config.print || config.storage.is_none() {
292 match config.output_encoding.to_lowercase().as_str() {
293 "json" => {
294 let manifest_json =
295 to_string_pretty(&manifest).map_err(|e| Error::Serialization(e.to_string()))?;
296 println!("{manifest_json}");
297 }
298 "cbor" => {
299 let manifest_cbor = serde_cbor::to_vec(&manifest)
300 .map_err(|e| Error::Serialization(e.to_string()))?;
301 println!("{}", hex::encode(&manifest_cbor));
302 }
303 _ => {
304 return Err(Error::Validation(format!(
305 "Invalid output encoding '{}'. Valid options are: json, cbor",
306 config.output_encoding
307 )));
308 }
309 }
310 }
311
312 if let Some(storage) = &config.storage {
314 if !config.print {
315 let id = storage.store_manifest(&manifest)?;
316 println!("Manifest stored successfully with ID: {id}");
317 }
318 }
319
320 Ok(())
321}
322
323pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> {
376 let claim = generate_c2pa_claim(&config, AssetKind::Model)?;
377
378 let mut manifest = Manifest {
380 claim_generator: "".to_string(),
381 title: "".to_string(),
382 instance_id: format!("urn:c2pa:{}", Uuid::new_v4()),
383 claim: claim.clone(),
384 ingredients: vec![],
385 created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()),
386 cross_references: vec![],
387 claim_v2: None,
388 is_active: true,
389 };
390
391 if let Some(manifest_ids) = &config.linked_manifests {
392 if let Some(storage_backend) = &config.storage {
393 for linked_id in manifest_ids {
394 match storage_backend.retrieve_manifest(linked_id) {
395 Ok(linked_manifest) => {
396 let linked_json = serde_json::to_string(&linked_manifest)
398 .map_err(|e| Error::Serialization(e.to_string()))?;
399
400 let linked_hash = hash::calculate_hash(linked_json.as_bytes());
402
403 let cross_ref = CrossReference {
405 manifest_url: linked_id.clone(),
406 manifest_hash: linked_hash,
407 media_type: Some("application/json".to_string()),
408 };
409
410 manifest.cross_references.push(cross_ref);
412
413 println!("Added link to manifest: {linked_id}");
414 }
415 Err(e) => {
416 println!("Warning: Could not link to manifest {linked_id}: {e}");
417 }
418 }
419 }
420 } else {
421 println!("Warning: Cannot link manifests without a storage backend");
422 }
423 }
424
425 let manifest_json = to_string(&manifest).map_err(|e| Error::Serialization(e.to_string()))?;
429 let manifest_proto = in_toto::json_to_struct_proto(&manifest_json)?;
430
431 let subject_hash = generate_oms_subject_hash(&manifest, &config.hash_alg)?;
432
433 let subject = in_toto::make_minimal_resource_descriptor(
434 &config.name,
435 hash::algorithm_to_string(&config.hash_alg),
436 &subject_hash,
437 );
438
439 let key_path = config
440 .key_path
441 .ok_or_else(|| Error::Validation("OMS format requires a signing key".to_string()))?;
442
443 let envelope = in_toto::generate_signed_statement_v1(
444 &[subject],
445 "https://spec.c2pa.org/specifications/specifications/2.2",
446 &manifest_proto,
447 key_path.to_path_buf(),
448 config.hash_alg,
449 )?;
450
451 if config.print || config.storage.is_none() {
453 match config.output_encoding.to_lowercase().as_str() {
454 "json" => {
455 let envelope_json =
456 to_string_pretty(&envelope).map_err(|e| Error::Serialization(e.to_string()))?;
457 println!("{envelope_json}");
458 }
459 "cbor" => {
460 let envelope_cbor = serde_cbor::to_vec(&envelope)
461 .map_err(|e| Error::Serialization(e.to_string()))?;
462 println!("{}", hex::encode(&envelope_cbor));
463 }
464 _ => {
465 return Err(Error::Validation(format!(
466 "Invalid output encoding '{}'. Valid options are: json, cbor",
467 config.output_encoding
468 )));
469 }
470 }
471 }
472
473 if let Some(storage) = &config.storage {
475 if !config.print {
476 let id = storage.store_manifest(&manifest)?;
477 println!("Manifest stored successfully with ID: {id}");
478 }
479 }
480
481 Ok(())
482}
483
484pub fn list_manifests(storage: &dyn StorageBackend, asset_kind: Option<AssetKind>) -> Result<()> {
516 let manifests = storage.list_manifests()?;
517
518 let filtered_manifests = if let Some(kind) = asset_kind {
520 manifests
521 .into_iter()
522 .filter(|m| match kind {
523 AssetKind::Model => {
524 matches!(m.manifest_type, crate::storage::traits::ManifestType::Model)
525 }
526 AssetKind::Dataset => matches!(
527 m.manifest_type,
528 crate::storage::traits::ManifestType::Dataset
529 ),
530 AssetKind::Software => matches!(
531 m.manifest_type,
532 crate::storage::traits::ManifestType::Software
533 ),
534 AssetKind::Evaluation => {
535 m.name.contains("Evaluation") || m.name.contains("evaluation")
537 }
538 })
539 .collect::<Vec<_>>()
540 } else {
541 manifests
542 };
543
544 for metadata in filtered_manifests {
546 println!(
547 "Manifest: {} (ID: {}, Type: {:?}, Created: {})",
548 metadata.name, metadata.id, metadata.manifest_type, metadata.created_at
549 );
550 }
551
552 Ok(())
553}
554
555pub fn verify_manifest(id: &str, storage: &dyn StorageBackend) -> Result<()> {
599 let manifest = storage.retrieve_manifest(id)?;
600
601 atlas_c2pa_lib::manifest::validate_manifest(&manifest)
603 .map_err(|e| crate::error::Error::Validation(e.to_string()))?;
604
605 println!("Verifying manifest with ID: {id}");
606
607 for ingredient in &manifest.ingredients {
609 println!("Verifying ingredient: {}", ingredient.title);
610
611 if ingredient.data.url.starts_with("file://") {
612 let path = PathBuf::from(ingredient.data.url.trim_start_matches("file://"));
613
614 let location = ArtifactLocation {
616 url: ingredient.data.url.clone(),
617 file_path: Some(path),
618 hash: ingredient.data.hash.clone(),
619 };
620
621 match location.verify() {
623 Ok(true) => {
624 println!(
625 "✓ Successfully verified hash for component: {}",
626 ingredient.title
627 );
628 }
629 Ok(false) => {
630 return Err(Error::Validation(format!(
631 "Hash verification failed for component: {}. The file may have been modified.",
632 ingredient.title
633 )));
634 }
635 Err(e) => {
636 return Err(Error::Validation(format!(
637 "Error verifying component {}: {}. The file may be missing or inaccessible.",
638 ingredient.title, e
639 )));
640 }
641 }
642 } else {
643 match hash::calculate_file_hash(PathBuf::from(&ingredient.data.url)) {
645 Ok(calculated_hash) => {
646 if calculated_hash != ingredient.data.hash {
647 return Err(Error::Validation(format!(
648 "Hash mismatch for ingredient: {}",
649 ingredient.title
650 )));
651 }
652 println!(
653 "✓ Successfully verified hash for component: {}",
654 ingredient.title
655 );
656 }
657 Err(_) => {
658 println!(
659 "⚠ Warning: Component {} does not use file:// URL scheme and could not be verified directly",
660 ingredient.title
661 );
662 }
663 }
664 }
665 }
666
667 if !manifest.cross_references.is_empty() {
669 println!("Verifying cross-references...");
670
671 for cross_ref in &manifest.cross_references {
672 let linked_manifest = storage.retrieve_manifest(&cross_ref.manifest_url)?;
673 let manifest_json = serde_json::to_string(&linked_manifest)
674 .map_err(|e| Error::Serialization(e.to_string()))?;
675 let algorithm = hash::detect_hash_algorithm(&cross_ref.manifest_hash);
676 let calculated_hash =
677 hash::calculate_hash_with_algorithm(manifest_json.as_bytes(), &algorithm);
678
679 if calculated_hash != cross_ref.manifest_hash {
680 return Err(Error::Validation(format!(
681 "Cross-reference verification failed for linked manifest: {}. Hash mismatch: stored={}, calculated={}",
682 cross_ref.manifest_url, cross_ref.manifest_hash, calculated_hash
683 )));
684 }
685 println!(
686 "✓ Verified cross-reference to manifest: {}",
687 cross_ref.manifest_url
688 );
689 }
690 }
691
692 verify_asset_specific_requirements(&manifest)?;
694
695 println!("✓ Manifest verification successful");
696 Ok(())
697}
698
699fn verify_asset_specific_requirements(manifest: &Manifest) -> Result<()> {
701 let is_dataset = is_dataset_manifest(manifest);
703 let is_model = is_model_manifest(manifest);
704 let is_software = is_software_manifest(manifest);
705 let is_evaluation = is_evaluation_manifest(manifest);
706
707 if !is_evaluation && manifest.ingredients.is_empty() {
709 return Err(Error::Validation(
710 "Manifest must contain at least one ingredient".to_string(),
711 ));
712 }
713
714 if let Some(claim) = &manifest.claim_v2 {
716 if is_dataset {
717 let has_dataset_assertion = claim.created_assertions.iter().any(|assertion| {
718 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Dataset")
719 });
720
721 let has_dataset_assertion_in_claim = if !has_dataset_assertion {
722 manifest.claim.created_assertions.iter().any(|assertion| {
723 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Dataset")
724 })
725 } else {
726 false
727 };
728
729 if !has_dataset_assertion && !has_dataset_assertion_in_claim {
730 println!(
731 "WARNING: Dataset manifest doesn't contain a Dataset creative work assertion"
732 );
733
734 return Err(Error::Validation(
735 "Dataset manifest must contain a Dataset creative work assertion".to_string(),
736 ));
737 }
738 }
739
740 if is_model {
741 let has_model_assertion = claim.created_assertions.iter().any(|assertion| {
742 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Model")
743 });
744
745 let has_model_assertion_in_claim = if !has_model_assertion {
746 manifest.claim.created_assertions.iter().any(|assertion| {
747 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Model")
748 })
749 } else {
750 false
751 };
752
753 if !has_model_assertion && !has_model_assertion_in_claim {
754 println!("WARNING: Model manifest doesn't contain a Model creative work assertion");
755
756 return Err(Error::Validation(
757 "Model manifest must contain a Model creative work assertion".to_string(),
758 ));
759 }
760 }
761
762 if is_software {
763 let has_software_assertion = claim.created_assertions.iter().any(|assertion| {
764 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Software")
765 });
766
767 let has_software_parameters = claim.created_assertions.iter().any(|assertion| {
768 if let Assertion::Action(action_assertion) = assertion {
769 action_assertion.actions.iter().any(|action| {
770 if let Some(params) = &action.parameters {
771 params.get("software_type").is_some()
772 } else {
773 false
774 }
775 })
776 } else {
777 false
778 }
779 });
780
781 if !has_software_assertion && !has_software_parameters {
782 println!(
783 "WARNING: Software manifest doesn't contain a Software creative work assertion or software_type parameter"
784 );
785
786 return Err(Error::Validation(
787 "Software manifest must contain a Software creative work assertion or software_type parameter".to_string(),
788 ));
789 }
790 }
791
792 if is_evaluation {
793 let has_evaluation_assertion = claim.created_assertions.iter().any(|assertion| {
794 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "EvaluationResult")
795 });
796
797 if !has_evaluation_assertion {
798 println!(
799 "WARNING: Evaluation manifest doesn't contain an EvaluationResult creative work assertion"
800 );
801
802 return Err(Error::Validation(
803 "Evaluation manifest must contain an EvaluationResult creative work assertion"
804 .to_string(),
805 ));
806 }
807 }
808 }
809
810 Ok(())
811}
812
813fn is_dataset_manifest(manifest: &Manifest) -> bool {
815 if is_evaluation_manifest(manifest) {
817 return false;
818 }
819
820 let has_dataset_ingredients = manifest.ingredients.iter().any(|ingredient| {
822 ingredient.data.data_types.iter().any(|t| {
823 matches!(
824 t,
825 AssetType::Dataset
826 | AssetType::DatasetOnnx
827 | AssetType::DatasetTensorFlow
828 | AssetType::DatasetPytorch
829 )
830 })
831 });
832
833 let has_dataset_assertion = if let Some(claim) = &manifest.claim_v2 {
834 claim.created_assertions.iter().any(|assertion| {
835 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Dataset")
836 })
837 } else {
838 false
839 };
840
841 has_dataset_ingredients || has_dataset_assertion
842}
843
844fn is_model_manifest(manifest: &Manifest) -> bool {
846 let has_model_ingredients = manifest.ingredients.iter().any(|ingredient| {
848 ingredient.data.data_types.iter().any(|t| {
849 matches!(
850 t,
851 AssetType::Model
852 | AssetType::ModelOnnx
853 | AssetType::ModelTensorFlow
854 | AssetType::ModelPytorch
855 | AssetType::ModelOpenVino
856 )
857 })
858 });
859
860 let has_model_assertion = if let Some(claim) = &manifest.claim_v2 {
862 claim.created_assertions.iter().any(|assertion| {
863 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Model")
864 })
865 } else if let Some(Assertion::CreativeWork(creative_work)) = manifest
866 .claim
867 .created_assertions
868 .iter()
869 .find(|a| matches!(a, Assertion::CreativeWork(_)))
870 {
871 creative_work.creative_type == "Model"
873 } else {
874 false
875 };
876
877 has_model_ingredients || has_model_assertion
879}
880
881fn is_software_manifest(manifest: &Manifest) -> bool {
883 let has_software_ingredients = manifest.ingredients.iter().any(|ingredient| {
885 ingredient
886 .data
887 .data_types
888 .iter()
889 .any(|t| matches!(t, AssetType::Generator))
890 });
891
892 let has_software_assertion = if let Some(claim) = &manifest.claim_v2 {
894 claim.created_assertions.iter().any(|assertion| {
895 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "Software")
896 })
897 } else {
898 false
899 };
900
901 let has_software_parameters = if let Some(claim) = &manifest.claim_v2 {
903 claim.created_assertions.iter().any(|assertion| {
904 if let Assertion::Action(action_assertion) = assertion {
905 action_assertion.actions.iter().any(|action| {
906 if let Some(params) = &action.parameters {
907 params.get("software_type").is_some()
908 } else {
909 false
910 }
911 })
912 } else {
913 false
914 }
915 })
916 } else {
917 false
918 };
919
920 has_software_ingredients || has_software_assertion || has_software_parameters
921}
922
923fn is_evaluation_manifest(manifest: &Manifest) -> bool {
925 if let Some(claim) = &manifest.claim_v2 {
926 claim.created_assertions.iter().any(|assertion| {
927 matches!(assertion, Assertion::CreativeWork(creative_work) if creative_work.creative_type == "EvaluationResult")
928 })
929 } else {
930 false
931 }
932}
933
934pub fn create_ingredient_from_path(
936 path: &Path,
937 name: &str,
938 asset_type: AssetType,
939 format: String,
940) -> Result<Ingredient> {
941 create_ingredient_from_path_with_algorithm(
942 path,
943 name,
944 asset_type,
945 format,
946 &HashAlgorithm::Sha384,
947 )
948}
949
950pub fn create_ingredient_from_path_with_algorithm(
952 path: &Path,
953 name: &str,
954 asset_type: AssetType,
955 format: String,
956 algorithm: &HashAlgorithm,
957) -> Result<Ingredient> {
958 let ingredient_data = IngredientData {
959 url: format!("file://{}", path.to_string_lossy()),
960 alg: algorithm.as_str().to_string(),
961 hash: hash::calculate_file_hash_with_algorithm(path, algorithm)?,
962 data_types: vec![asset_type],
963 linked_ingredient_url: None,
964 linked_ingredient_hash: None,
965 };
966
967 Ok(Ingredient {
968 title: name.to_string(),
969 format,
970 relationship: "componentOf".to_string(),
971 document_id: format!("uuid:{}", Uuid::new_v4()),
972 instance_id: format!("uuid:{}", Uuid::new_v4()),
973 data: ingredient_data,
974 linked_ingredient: None,
975 public_key: None,
976 })
977}
978
979fn get_cc_attestation_assertion() -> Result<CustomAssertion> {
981 let report = match cc_attestation::get_report(false) {
982 Ok(r) => r,
983 Err(e) => {
984 return Err(Error::CCAttestationError(format!(
985 "Failed to get attestation: {e}"
986 )));
987 }
988 };
989
990 let platform = match get_platform_name() {
992 Ok(p) => p,
993 Err(e) => {
994 return Err(Error::CCAttestationError(format!(
995 "Error detecting attestation platform: {e}"
996 )));
997 }
998 };
999
1000 let cc_assertion = CustomAssertion {
1001 label: platform,
1002 data: serde_json::Value::String(report),
1003 };
1004
1005 Ok(cc_assertion)
1006}
1007
1008fn generate_oms_subject_hash(manifest: &Manifest, hash_alg: &HashAlgorithm) -> Result<String> {
1010 if manifest.claim.ingredients.is_empty() {
1012 return Err(Error::Validation(
1013 "OMS requires at least one ingredient".to_string(),
1014 ));
1015 }
1016
1017 let mut ingredients_to_hash = manifest.claim.ingredients.clone();
1022 ingredients_to_hash.sort_by_key(|ingredient| ingredient.title.to_lowercase());
1023
1024 let mut ingredient_hashes: Vec<u8> = Vec::new();
1025 for ingredient in &ingredients_to_hash {
1026 let raw_bytes = hex::decode(&ingredient.data.hash).map_err(|e| {
1027 Error::Validation(format!(
1028 "Invalid hash for ingredient {}: {}",
1029 ingredient.title, e
1030 ))
1031 })?;
1032 ingredient_hashes.extend_from_slice(&raw_bytes);
1033 }
1034
1035 Ok(hash::calculate_hash_with_algorithm(
1036 &ingredient_hashes,
1037 hash_alg,
1038 ))
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043 use super::*;
1044 use crate::signing::test_utils::generate_temp_key;
1045
1046 fn make_test_manifest_config() -> ManifestCreationConfig {
1047 let (_secure_key, tmp_dir) = generate_temp_key().unwrap();
1048
1049 ManifestCreationConfig {
1050 name: "test-model".to_string(),
1051 description: Some("A test model".to_string()),
1052 author_name: Some("Test Author".to_string()),
1053 author_org: Some("Test Org".to_string()),
1054 paths: vec![],
1055 ingredient_names: vec![],
1056 hash_alg: HashAlgorithm::Sha384,
1057 key_path: Some(tmp_dir.path().join("test_key.pem")),
1058 output_encoding: "json".to_string(),
1059 print: false,
1060 storage: None,
1061 with_cc: false,
1062 linked_manifests: None,
1063 custom_fields: None,
1064 software_type: None,
1065 version: None,
1066 }
1067 }
1068
1069 #[test]
1070 fn test_generate_c2pa_assertions() {
1071 let config = make_test_manifest_config();
1072
1073 let assertions = generate_c2pa_assertions(&config, AssetKind::Model).unwrap();
1074 assert!(!assertions.is_empty()); }
1076
1077 #[test]
1078 fn test_generate_c2pa_claim() {
1079 let config = make_test_manifest_config();
1080 let claim = generate_c2pa_claim(&config, AssetKind::Model).unwrap();
1081 assert!(claim.instance_id.starts_with("urn:c2pa:"));
1082 assert_eq!(claim.claim_generator_info, "atlas-cli:0.1.1");
1083 }
1084
1085 #[test]
1104 fn test_create_oms_manifest_no_key() {
1105 let mut config = make_test_manifest_config();
1106 config.key_path = None; let result = create_oms_manifest(config);
1108 assert!(result.is_err()); }
1110}