From 7681f9e3e6d0e1910eb0d4fcaa11e20630ad4ab0 Mon Sep 17 00:00:00 2001 From: Edwards Date: Mon, 2 Feb 2026 14:52:42 -0800 Subject: [PATCH 01/13] First changes attempt to refactor for issue 72 --- src/manifest/common.rs | 485 +++++++++++++++++++++++++---------------- 1 file changed, 301 insertions(+), 184 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index d965be5..ac835a1 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -232,93 +232,103 @@ fn generate_c2pa_claim(config: &ManifestCreationConfig, asset_kind: AssetKind) - } /// Creates a manifest for a model, dataset, software, or evaluation + pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { - let claim = generate_c2pa_claim(&config, asset_kind)?; + let format = ManifestFormat::Standalone; + format.create(config, asset_kind) +} - // Create the manifest - let mut manifest = Manifest { - claim_generator: CLAIM_GENERATOR.to_string(), - title: config.name.clone(), - instance_id: format!("urn:c2pa:{}", Uuid::new_v4()), - claim: claim.clone(), - ingredients: vec![], - created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()), - cross_references: vec![], - claim_v2: Some(claim), - is_active: true, - }; +// pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { +// let claim = generate_c2pa_claim(&config, asset_kind)?; + +// // Create the manifest +// let mut manifest = Manifest { +// claim_generator: CLAIM_GENERATOR.to_string(), +// title: config.name.clone(), +// instance_id: format!("urn:c2pa:{}", Uuid::new_v4()), +// claim: claim.clone(), +// ingredients: vec![], +// created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()), +// cross_references: vec![], +// claim_v2: Some(claim), +// is_active: true, +// }; + +// // Sign if key is provided +// if let Some(key_file) = &config.key_path { +// manifest.sign(key_file.to_path_buf(), config.hash_alg)?; +// } + +// if let Some(manifest_ids) = &config.linked_manifests { +// if let Some(storage_backend) = &config.storage { +// for linked_id in manifest_ids { +// match storage_backend.retrieve_manifest(linked_id) { +// Ok(linked_manifest) => { +// // Create a JSON representation of the linked manifest +// let linked_json = serde_json::to_string(&linked_manifest) +// .map_err(|e| Error::Serialization(e.to_string()))?; + +// // Create a hash of the linked manifest +// let linked_hash = hash::calculate_hash(linked_json.as_bytes()); + +// // Create a cross-reference +// let cross_ref = CrossReference { +// manifest_url: linked_id.clone(), +// manifest_hash: linked_hash, +// media_type: Some("application/json".to_string()), +// }; + +// // Add the cross-reference to the manifest +// manifest.cross_references.push(cross_ref); + +// println!("Added link to manifest: {linked_id}"); +// } +// Err(e) => { +// println!("Warning: Could not link to manifest {linked_id}: {e}"); +// } +// } +// } +// } else { +// println!("Warning: Cannot link manifests without a storage backend"); +// } +// } + +// // Output manifest if requested +// if config.print || config.storage.is_none() { +// match config.output_encoding.to_lowercase().as_str() { +// "json" => { +// let manifest_json = +// to_string_pretty(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; +// println!("{manifest_json}"); +// } +// "cbor" => { +// let manifest_cbor = serde_cbor::to_vec(&manifest) +// .map_err(|e| Error::Serialization(e.to_string()))?; +// println!("{}", hex::encode(&manifest_cbor)); +// } +// _ => { +// return Err(Error::Validation(format!( +// "Invalid output encoding '{}'. Valid options are: json, cbor", +// config.output_encoding +// ))); +// } +// } +// } + +// // Store manifest if storage is provided +// if let Some(storage) = &config.storage { +// if !config.print { +// let id = storage.store_manifest(&manifest)?; +// println!("Manifest stored successfully with ID: {id}"); +// } +// } + +// Ok(()) +// } - // Sign if key is provided - if let Some(key_file) = &config.key_path { - manifest.sign(key_file.to_path_buf(), config.hash_alg)?; - } - if let Some(manifest_ids) = &config.linked_manifests { - if let Some(storage_backend) = &config.storage { - for linked_id in manifest_ids { - match storage_backend.retrieve_manifest(linked_id) { - Ok(linked_manifest) => { - // Create a JSON representation of the linked manifest - let linked_json = serde_json::to_string(&linked_manifest) - .map_err(|e| Error::Serialization(e.to_string()))?; - - // Create a hash of the linked manifest - let linked_hash = hash::calculate_hash(linked_json.as_bytes()); - - // Create a cross-reference - let cross_ref = CrossReference { - manifest_url: linked_id.clone(), - manifest_hash: linked_hash, - media_type: Some("application/json".to_string()), - }; - - // Add the cross-reference to the manifest - manifest.cross_references.push(cross_ref); - - println!("Added link to manifest: {linked_id}"); - } - Err(e) => { - println!("Warning: Could not link to manifest {linked_id}: {e}"); - } - } - } - } else { - println!("Warning: Cannot link manifests without a storage backend"); - } - } - - // Output manifest if requested - if config.print || config.storage.is_none() { - match config.output_encoding.to_lowercase().as_str() { - "json" => { - let manifest_json = - to_string_pretty(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; - println!("{manifest_json}"); - } - "cbor" => { - let manifest_cbor = serde_cbor::to_vec(&manifest) - .map_err(|e| Error::Serialization(e.to_string()))?; - println!("{}", hex::encode(&manifest_cbor)); - } - _ => { - return Err(Error::Validation(format!( - "Invalid output encoding '{}'. Valid options are: json, cbor", - config.output_encoding - ))); - } - } - } - // Store manifest if storage is provided - if let Some(storage) = &config.storage { - if !config.print { - let id = storage.store_manifest(&manifest)?; - println!("Manifest stored successfully with ID: {id}"); - } - } - Ok(()) -} /// Creates an OpenSSF Model Signing (OMS) compliant C2PA manifest for a model. /// @@ -372,114 +382,128 @@ pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> /// /// create_oms_manifest(config).unwrap(); /// ``` + pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> { - let claim = generate_c2pa_claim(&config, AssetKind::Model)?; + let format = ManifestFormat::OMS; + format.create(config, AssetKind::Model) +} - // Create the manifest - let mut manifest = Manifest { - claim_generator: "".to_string(), - title: "".to_string(), - instance_id: format!("urn:c2pa:{}", Uuid::new_v4()), - claim: claim.clone(), - ingredients: vec![], - created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()), - cross_references: vec![], - claim_v2: None, - is_active: true, - }; - if let Some(manifest_ids) = &config.linked_manifests { - if let Some(storage_backend) = &config.storage { - for linked_id in manifest_ids { - match storage_backend.retrieve_manifest(linked_id) { - Ok(linked_manifest) => { - // Create a JSON representation of the linked manifest - let linked_json = serde_json::to_string(&linked_manifest) - .map_err(|e| Error::Serialization(e.to_string()))?; - - // Create a hash of the linked manifest - let linked_hash = hash::calculate_hash(linked_json.as_bytes()); - - // Create a cross-reference - let cross_ref = CrossReference { - manifest_url: linked_id.clone(), - manifest_hash: linked_hash, - media_type: Some("application/json".to_string()), - }; - - // Add the cross-reference to the manifest - manifest.cross_references.push(cross_ref); - - println!("Added link to manifest: {linked_id}"); - } - Err(e) => { - println!("Warning: Could not link to manifest {linked_id}: {e}"); - } - } - } - } else { - println!("Warning: Cannot link manifests without a storage backend"); - } - } - // Generate the in-toto format Statement and sign the DSSE - - // we need to convert this into a string to serialize into the Struct proto expected by in-toto - let manifest_json = to_string(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; - let manifest_proto = in_toto::json_to_struct_proto(&manifest_json)?; - - let subject_hash = generate_oms_subject_hash(&manifest, &config.hash_alg)?; - - let subject = in_toto::make_minimal_resource_descriptor( - &config.name, - hash::algorithm_to_string(&config.hash_alg), - &subject_hash, - ); - - let key_path = config - .key_path - .ok_or_else(|| Error::Validation("OMS format requires a signing key".to_string()))?; - - let envelope = in_toto::generate_signed_statement_v1( - &[subject], - "https://spec.c2pa.org/specifications/specifications/2.2", - &manifest_proto, - key_path.to_path_buf(), - config.hash_alg, - )?; - - // Output manifest if requested - if config.print || config.storage.is_none() { - match config.output_encoding.to_lowercase().as_str() { - "json" => { - let envelope_json = - to_string_pretty(&envelope).map_err(|e| Error::Serialization(e.to_string()))?; - println!("{envelope_json}"); - } - "cbor" => { - let envelope_cbor = serde_cbor::to_vec(&envelope) - .map_err(|e| Error::Serialization(e.to_string()))?; - println!("{}", hex::encode(&envelope_cbor)); - } - _ => { - return Err(Error::Validation(format!( - "Invalid output encoding '{}'. Valid options are: json, cbor", - config.output_encoding - ))); - } - } - } - // Store manifest if storage is provided - if let Some(storage) = &config.storage { - if !config.print { - let id = storage.store_manifest(&manifest)?; - println!("Manifest stored successfully with ID: {id}"); - } - } - - Ok(()) -} +// ############################################################################################### + +// pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> { +// let claim = generate_c2pa_claim(&config, AssetKind::Model)?; + +// // Create the manifest +// let mut manifest = Manifest { +// claim_generator: "".to_string(), +// title: "".to_string(), +// instance_id: format!("urn:c2pa:{}", Uuid::new_v4()), +// claim: claim.clone(), +// ingredients: vec![], +// created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()), +// cross_references: vec![], +// claim_v2: None, +// is_active: true, +// }; + +// if let Some(manifest_ids) = &config.linked_manifests { +// if let Some(storage_backend) = &config.storage { +// for linked_id in manifest_ids { +// match storage_backend.retrieve_manifest(linked_id) { +// Ok(linked_manifest) => { +// // Create a JSON representation of the linked manifest +// let linked_json = serde_json::to_string(&linked_manifest) +// .map_err(|e| Error::Serialization(e.to_string()))?; + +// // Create a hash of the linked manifest +// let linked_hash = hash::calculate_hash(linked_json.as_bytes()); + +// // Create a cross-reference +// let cross_ref = CrossReference { +// manifest_url: linked_id.clone(), +// manifest_hash: linked_hash, +// media_type: Some("application/json".to_string()), +// }; + +// // Add the cross-reference to the manifest +// manifest.cross_references.push(cross_ref); + +// println!("Added link to manifest: {linked_id}"); +// } +// Err(e) => { +// println!("Warning: Could not link to manifest {linked_id}: {e}"); +// } +// } +// } +// } else { +// println!("Warning: Cannot link manifests without a storage backend"); +// } +// } + +// // Generate the in-toto format Statement and sign the DSSE + +// // we need to convert this into a string to serialize into the Struct proto expected by in-toto +// let manifest_json = to_string(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; +// let manifest_proto = in_toto::json_to_struct_proto(&manifest_json)?; + +// let subject_hash = generate_oms_subject_hash(&manifest, &config.hash_alg)?; + +// let subject = in_toto::make_minimal_resource_descriptor( +// &config.name, +// hash::algorithm_to_string(&config.hash_alg), +// &subject_hash, +// ); + +// let key_path = config +// .key_path +// .ok_or_else(|| Error::Validation("OMS format requires a signing key".to_string()))?; + +// let envelope = in_toto::generate_signed_statement_v1( +// &[subject], +// "https://spec.c2pa.org/specifications/specifications/2.2", +// &manifest_proto, +// key_path.to_path_buf(), +// config.hash_alg, +// )?; + +// // Output manifest if requested +// if config.print || config.storage.is_none() { +// match config.output_encoding.to_lowercase().as_str() { +// "json" => { +// let envelope_json = +// to_string_pretty(&envelope).map_err(|e| Error::Serialization(e.to_string()))?; +// println!("{envelope_json}"); +// } +// "cbor" => { +// let envelope_cbor = serde_cbor::to_vec(&envelope) +// .map_err(|e| Error::Serialization(e.to_string()))?; +// println!("{}", hex::encode(&envelope_cbor)); +// } +// _ => { +// return Err(Error::Validation(format!( +// "Invalid output encoding '{}'. Valid options are: json, cbor", +// config.output_encoding +// ))); +// } +// } +// } + +// // Store manifest if storage is provided +// if let Some(storage) = &config.storage { +// if !config.print { +// let id = storage.store_manifest(&manifest)?; +// println!("Manifest stored successfully with ID: {id}"); +// } +// } + +// Ok(()) +// } + + +// ############################################################################################### /// Lists manifests from storage, optionally filtered by asset type. /// @@ -1038,6 +1062,99 @@ fn generate_oms_subject_hash(manifest: &Manifest, hash_alg: &HashAlgorithm) -> R )) } +// Refactor start here + + + +enum ManifestFormat { + Standalone, + OMS, + // Future formats... +} + +impl ManifestFormat { + fn create(&self, config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { + match (self, asset_kind) { + (Self::Standalone, AssetKind) => { + let manifest = create_c2pa_manifest(config, asset_kind); + if let Some(key_file) = &config.key_path { + manifest.sign(key_file.to_path_buf(), config.hash_alg)?; + } + let metadata_container = manifest; + }, + (Self::OMS, AssetKind::Model) => { + let manifest = create_c2pa_manifest(config, AssetKind::Model); + + // Generate the in-toto format Statement and sign the DSSE + + // we need to convert this into a string to serialize into the Struct proto expected by in-toto + let manifest_json = to_string(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; + let manifest_proto = in_toto::json_to_struct_proto(&manifest_json)?; + + let subject_hash = generate_oms_subject_hash(&manifest, &config.hash_alg)?; + + let subject = in_toto::make_minimal_resource_descriptor( + &config.name, + hash::algorithm_to_string(&config.hash_alg), + &subject_hash, + ); + + let key_path = config + .key_path + .ok_or_else(|| Error::Validation("OMS format requires a signing key".to_string()))?; + + let envelope = in_toto::generate_signed_statement_v1( + &[subject], + "https://spec.c2pa.org/specifications/specifications/2.2", + &manifest_proto, + key_path.to_path_buf(), + config.hash_alg, + )?; + let metadata_container = envelope; + } + } + // Output metadata_container if requested + if config.print || config.storage.is_none() { + match config.output_encoding.to_lowercase().as_str() { + "json" => { + let metadata_container_json = + to_string_pretty(&metadata_container).map_err(|e| Error::Serialization(e.to_string()))?; + println!("{metadata_container_json}"); + } + "cbor" => { + let metadata_container_cbor = serde_cbor::to_vec(&metadata_container) + .map_err(|e| Error::Serialization(e.to_string()))?; + println!("{}", hex::encode(&metadata_container_cbor)); + } + _ => { + return Err(Error::Validation(format!( + "Invalid output encoding '{}'. Valid options are: json, cbor", + config.output_encoding + ))); + } + } + } + // TODO: Below probably doesn't work as-is since I am asking storage.store_manifest to work for both metadata containers + // Before refactoring, it saved the manifest in both cases. I thought this was a bug. If it is not + // I will change back to doing that. If I was correct this is preferred I either need + // to confirm the method works on both containers or implement storage separetely for each. + + // Store metadata_container if storage is provided + if let Some(storage) = &config.storage { + if !config.print { + let id = storage.store_manifest(&metadata_container)?; + println!("Metadata container stored successfully with ID: {id}"); + } + } + + Ok(()) + } +} + + +// Refactor end here + + #[cfg(test)] mod tests { use super::*; From a49e2393005edd416518b06c1ad0fd7f67eeff3b Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Mon, 2 Feb 2026 16:58:22 -0800 Subject: [PATCH 02/13] Including some tests that were previously commented out. --- src/manifest/common.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index ac835a1..3a8fd26 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -1199,23 +1199,23 @@ mod tests { assert_eq!(claim.claim_generator_info, "atlas-cli:0.2.0"); } - // #[test] - // fn test_create_manifest() -> Result<()>{ - // let config = make_test_manifest_config(); - // let result = create_manifest(config, AssetKind::Model); - // assert!(result.is_ok()); // Should succeed even with no ingredients - - // Ok(()) - // } - - // #[test] - // fn test_create_oms_manifest() -> Result<()> { - // let config = make_test_manifest_config(); - // let result = create_oms_manifest(config); - // assert!(result.is_ok()); // Should succeed with the provided key - - // Ok(()) - // } + #[test] + fn test_create_manifest() -> Result<()>{ + let config = make_test_manifest_config(); + let result = create_manifest(config, AssetKind::Model); + assert!(result.is_ok()); // Should succeed even with no ingredients + + Ok(()) + } + + #[test] + fn test_create_oms_manifest() -> Result<()> { + let config = make_test_manifest_config(); + let result = create_oms_manifest(config); + assert!(result.is_ok()); // Should succeed with the provided key + + Ok(()) + } #[test] fn test_create_oms_manifest_no_key() { From 65e3157df64544d2fe7c6b5f4159e859073abb71 Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Thu, 5 Feb 2026 16:13:43 -0800 Subject: [PATCH 03/13] tests passing, but still need to make the asset file clean up after --- src/manifest/common.rs | 179 +++++++++++++++++++++++++++++++++-------- 1 file changed, 146 insertions(+), 33 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index 3a8fd26..5783422 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -2,6 +2,7 @@ use crate::cc_attestation; use crate::error::{Error, Result}; use crate::hash; use crate::in_toto; +use in_toto::dsse::Envelope; use crate::manifest::config::ManifestCreationConfig; use crate::manifest::utils::{ determine_dataset_type, determine_format, determine_model_type, determine_software_type, @@ -19,6 +20,7 @@ use atlas_c2pa_lib::datetime_wrapper::OffsetDateTimeWrapper; use atlas_c2pa_lib::ingredient::{Ingredient, IngredientData}; use atlas_c2pa_lib::manifest::Manifest; use serde_json::{to_string, to_string_pretty}; +use serde::Serialize; use std::path::{Path, PathBuf}; use tdx_workload_attestation::get_platform_name; use time::OffsetDateTime; @@ -231,11 +233,70 @@ fn generate_c2pa_claim(config: &ManifestCreationConfig, asset_kind: AssetKind) - }) } + +/// Creates a C2PA manifest for a model, dataset, software, or evaluation +// TODO: Find better naming, this is the core creation after the refactor +fn create_c2pa_manifest(config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result { + let claim = generate_c2pa_claim(&config, asset_kind)?; + + // Create the manifest + let mut manifest = Manifest { + claim_generator: CLAIM_GENERATOR.to_string(), + title: config.name.clone(), + instance_id: format!("urn:c2pa:{}", Uuid::new_v4()), + claim: claim.clone(), + ingredients: vec![], + created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()), + cross_references: vec![], + claim_v2: Some(claim), + is_active: true, + }; + + if let Some(manifest_ids) = &config.linked_manifests { + if let Some(storage_backend) = &config.storage { + for linked_id in manifest_ids { + match storage_backend.retrieve_manifest(linked_id) { + Ok(linked_manifest) => { + // Create a JSON representation of the linked manifest + let linked_json = serde_json::to_string(&linked_manifest) + .map_err(|e| Error::Serialization(e.to_string()))?; + + // Create a hash of the linked manifest + let linked_hash = hash::calculate_hash(linked_json.as_bytes()); + + // Create a cross-reference + let cross_ref = CrossReference { + manifest_url: linked_id.clone(), + manifest_hash: linked_hash, + media_type: Some("application/json".to_string()), + }; + + // Add the cross-reference to the manifest + manifest.cross_references.push(cross_ref); + + println!("Added link to manifest: {linked_id}"); + } + Err(e) => { + println!("Warning: Could not link to manifest {linked_id}: {e}"); + } + } + } + } else { + println!("Warning: Cannot link manifests without a storage backend"); + } + } + Ok(manifest) +} + + + + + /// Creates a manifest for a model, dataset, software, or evaluation pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { let format = ManifestFormat::Standalone; - format.create(config, asset_kind) + format.create(&config, asset_kind) } // pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { @@ -385,7 +446,7 @@ pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> { let format = ManifestFormat::OMS; - format.create(config, AssetKind::Model) + format.create(&config, AssetKind::Model) } @@ -1072,18 +1133,26 @@ enum ManifestFormat { // Future formats... } +#[derive(Serialize)] +pub enum MetadataContainer { + C2PAManifest(Manifest), + OMSInTotoEnvelope(Envelope), +} + impl ManifestFormat { - fn create(&self, config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { - match (self, asset_kind) { - (Self::Standalone, AssetKind) => { - let manifest = create_c2pa_manifest(config, asset_kind); + fn create(&self, config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { + // TODO: below we may return different things in second position if storage of envelope + // is preferred to storage of the manifest + let (metadata_container, manifest) = match (self, &asset_kind) { + (Self::Standalone, _) => { + let mut manifest = create_c2pa_manifest(&config, asset_kind)?; if let Some(key_file) = &config.key_path { - manifest.sign(key_file.to_path_buf(), config.hash_alg)?; + manifest.sign(key_file.to_path_buf(), config.hash_alg.clone())?; } - let metadata_container = manifest; + (MetadataContainer::C2PAManifest(manifest.clone()), manifest) }, (Self::OMS, AssetKind::Model) => { - let manifest = create_c2pa_manifest(config, AssetKind::Model); + let manifest = create_c2pa_manifest(&config, AssetKind::Model)?; // Generate the in-toto format Statement and sign the DSSE @@ -1098,9 +1167,9 @@ impl ManifestFormat { hash::algorithm_to_string(&config.hash_alg), &subject_hash, ); - let key_path = config .key_path + .as_ref() .ok_or_else(|| Error::Validation("OMS format requires a signing key".to_string()))?; let envelope = in_toto::generate_signed_statement_v1( @@ -1108,12 +1177,18 @@ impl ManifestFormat { "https://spec.c2pa.org/specifications/specifications/2.2", &manifest_proto, key_path.to_path_buf(), - config.hash_alg, + config.hash_alg.clone(), )?; - let metadata_container = envelope; - } - } + (MetadataContainer::OMSInTotoEnvelope(envelope), manifest) + }, + (Self::OMS, _) => { + return Err(Error::Validation( + "OMS format is only supported for AssetKind::Model".to_string(), + )); + }, + }; // Output metadata_container if requested + // TODO: Or should this stay as output of the manifest only? if config.print || config.storage.is_none() { match config.output_encoding.to_lowercase().as_str() { "json" => { @@ -1134,19 +1209,15 @@ impl ManifestFormat { } } } - // TODO: Below probably doesn't work as-is since I am asking storage.store_manifest to work for both metadata containers - // Before refactoring, it saved the manifest in both cases. I thought this was a bug. If it is not - // I will change back to doing that. If I was correct this is preferred I either need - // to confirm the method works on both containers or implement storage separetely for each. + // TODO: Below work is needed possibly. We currently store the manifest in both cases (OMS and standalone) - // Store metadata_container if storage is provided + // Store manifest if storage is provided if let Some(storage) = &config.storage { if !config.print { - let id = storage.store_manifest(&metadata_container)?; - println!("Metadata container stored successfully with ID: {id}"); + let id = storage.store_manifest(&manifest)?; + println!("Manifest stored successfully with ID: {id}"); } } - Ok(()) } } @@ -1159,11 +1230,21 @@ impl ManifestFormat { mod tests { use super::*; use crate::signing::test_utils::generate_temp_key; + use std::fs::File; + + const TEST_ASSET_FILENAME: &str = "empty_test_model_file.onnx"; - fn make_test_manifest_config() -> ManifestCreationConfig { - let (_secure_key, tmp_dir) = generate_temp_key().unwrap(); + // Helper function to get the module directory (for creating test files) + fn module_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("src") + } - ManifestCreationConfig { + fn make_test_manifest_config() -> (tempfile::TempDir, ManifestCreationConfig) { + let (_secure_key, tmp_key_dir) = generate_temp_key().unwrap(); + let key_path = tmp_key_dir.path().join("test_key.pem"); + // must return the temp_key_dir to ensure it lives long enough for the tests that use the config, otherwise the temp dir (and key) will be deleted immediately after this function returns + (tmp_key_dir, ManifestCreationConfig { name: "test-model".to_string(), description: Some("A test model".to_string()), author_name: Some("Test Author".to_string()), @@ -1171,7 +1252,7 @@ mod tests { paths: vec![], ingredient_names: vec![], hash_alg: HashAlgorithm::Sha384, - key_path: Some(tmp_dir.path().join("test_key.pem")), + key_path: Some(key_path), output_encoding: "json".to_string(), print: false, storage: None, @@ -1180,12 +1261,39 @@ mod tests { custom_fields: None, software_type: None, version: None, - } + }) + } + + fn make_test_oms_manifest_config() -> (tempfile::TempDir, PathBuf, ManifestCreationConfig) { + let (_secure_key, tmp_key_dir) = generate_temp_key().unwrap(); + let key_path = tmp_key_dir.path().join("test_key.pem"); + let asset_dirpath = module_dir(); + let path = asset_dirpath.join(TEST_ASSET_FILENAME); + println!("Using test asset file for config {:#?}", path.to_str()); + // return the temp_dir so that it doesn't get deleted immediately + (tmp_key_dir, asset_dirpath, ManifestCreationConfig { + name: "test-model".to_string(), + description: Some("A test model".to_string()), + author_name: Some("Test Author".to_string()), + author_org: Some("Test Org".to_string()), + paths: vec![path], + ingredient_names: vec!["Test model ingredient".to_string()], // OMS requires at least one ingredient + hash_alg: HashAlgorithm::Sha384, + key_path: Some(key_path), + output_encoding: "json".to_string(), + print: false, + storage: None, + with_cc: false, + linked_manifests: None, + custom_fields: None, + software_type: None, + version: None, + }) } #[test] fn test_generate_c2pa_assertions() { - let config = make_test_manifest_config(); + let (_tmp_key_dir, config) = make_test_manifest_config(); let assertions = generate_c2pa_assertions(&config, AssetKind::Model).unwrap(); assert!(!assertions.is_empty()); // Should have at least the CreativeWork assertion @@ -1193,7 +1301,7 @@ mod tests { #[test] fn test_generate_c2pa_claim() { - let config = make_test_manifest_config(); + let (_tmp_key_dir, config) = make_test_manifest_config(); let claim = generate_c2pa_claim(&config, AssetKind::Model).unwrap(); assert!(claim.instance_id.starts_with("urn:c2pa:")); assert_eq!(claim.claim_generator_info, "atlas-cli:0.2.0"); @@ -1201,7 +1309,7 @@ mod tests { #[test] fn test_create_manifest() -> Result<()>{ - let config = make_test_manifest_config(); + let (_tmp_key_dir, config) = make_test_manifest_config(); let result = create_manifest(config, AssetKind::Model); assert!(result.is_ok()); // Should succeed even with no ingredients @@ -1210,16 +1318,21 @@ mod tests { #[test] fn test_create_oms_manifest() -> Result<()> { - let config = make_test_manifest_config(); + let (_tmp_key_dir, asset_dirpath, config) = make_test_oms_manifest_config(); + let asset_path = asset_dirpath.join(TEST_ASSET_FILENAME); + let _file = File::create(&asset_path)?; + assert!(asset_path.exists(), "Test asset file does not exist at {:#?}", asset_path.to_str()); let result = create_oms_manifest(config); - assert!(result.is_ok()); // Should succeed with the provided key + assert!(result.is_ok(), + "create_oms_manifest failed with error: {:#?}", + result.err()); // Should succeed with the provided key Ok(()) } #[test] fn test_create_oms_manifest_no_key() { - let mut config = make_test_manifest_config(); + let (_dir, _asset_dirpath, mut config) = make_test_oms_manifest_config(); config.key_path = None; // Remove the key path to simulate missing key let result = create_oms_manifest(config); assert!(result.is_err()); // Should fail because OMS requires a signing key From a6cba25de66367cafc1641e9fac7a4e3ec265a74 Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Fri, 6 Feb 2026 09:23:17 -0800 Subject: [PATCH 04/13] Some cleanup. --- src/manifest/common.rs | 232 +---------------------------------------- 1 file changed, 4 insertions(+), 228 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index 5783422..02dca92 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -233,7 +233,6 @@ fn generate_c2pa_claim(config: &ManifestCreationConfig, asset_kind: AssetKind) - }) } - /// Creates a C2PA manifest for a model, dataset, software, or evaluation // TODO: Find better naming, this is the core creation after the refactor fn create_c2pa_manifest(config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result { @@ -288,109 +287,12 @@ fn create_c2pa_manifest(config: &ManifestCreationConfig, asset_kind: AssetKind) Ok(manifest) } - - - - /// Creates a manifest for a model, dataset, software, or evaluation - pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { let format = ManifestFormat::Standalone; format.create(&config, asset_kind) } -// pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { -// let claim = generate_c2pa_claim(&config, asset_kind)?; - -// // Create the manifest -// let mut manifest = Manifest { -// claim_generator: CLAIM_GENERATOR.to_string(), -// title: config.name.clone(), -// instance_id: format!("urn:c2pa:{}", Uuid::new_v4()), -// claim: claim.clone(), -// ingredients: vec![], -// created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()), -// cross_references: vec![], -// claim_v2: Some(claim), -// is_active: true, -// }; - -// // Sign if key is provided -// if let Some(key_file) = &config.key_path { -// manifest.sign(key_file.to_path_buf(), config.hash_alg)?; -// } - -// if let Some(manifest_ids) = &config.linked_manifests { -// if let Some(storage_backend) = &config.storage { -// for linked_id in manifest_ids { -// match storage_backend.retrieve_manifest(linked_id) { -// Ok(linked_manifest) => { -// // Create a JSON representation of the linked manifest -// let linked_json = serde_json::to_string(&linked_manifest) -// .map_err(|e| Error::Serialization(e.to_string()))?; - -// // Create a hash of the linked manifest -// let linked_hash = hash::calculate_hash(linked_json.as_bytes()); - -// // Create a cross-reference -// let cross_ref = CrossReference { -// manifest_url: linked_id.clone(), -// manifest_hash: linked_hash, -// media_type: Some("application/json".to_string()), -// }; - -// // Add the cross-reference to the manifest -// manifest.cross_references.push(cross_ref); - -// println!("Added link to manifest: {linked_id}"); -// } -// Err(e) => { -// println!("Warning: Could not link to manifest {linked_id}: {e}"); -// } -// } -// } -// } else { -// println!("Warning: Cannot link manifests without a storage backend"); -// } -// } - -// // Output manifest if requested -// if config.print || config.storage.is_none() { -// match config.output_encoding.to_lowercase().as_str() { -// "json" => { -// let manifest_json = -// to_string_pretty(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; -// println!("{manifest_json}"); -// } -// "cbor" => { -// let manifest_cbor = serde_cbor::to_vec(&manifest) -// .map_err(|e| Error::Serialization(e.to_string()))?; -// println!("{}", hex::encode(&manifest_cbor)); -// } -// _ => { -// return Err(Error::Validation(format!( -// "Invalid output encoding '{}'. Valid options are: json, cbor", -// config.output_encoding -// ))); -// } -// } -// } - -// // Store manifest if storage is provided -// if let Some(storage) = &config.storage { -// if !config.print { -// let id = storage.store_manifest(&manifest)?; -// println!("Manifest stored successfully with ID: {id}"); -// } -// } - -// Ok(()) -// } - - - - - /// Creates an OpenSSF Model Signing (OMS) compliant C2PA manifest for a model. /// /// This function generates a manifest that conforms to the OpenSSF Model Signing specification, @@ -442,130 +344,12 @@ pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> /// }; /// /// create_oms_manifest(config).unwrap(); -/// ``` - +/// pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> { let format = ManifestFormat::OMS; format.create(&config, AssetKind::Model) } - - - -// ############################################################################################### - -// pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> { -// let claim = generate_c2pa_claim(&config, AssetKind::Model)?; - -// // Create the manifest -// let mut manifest = Manifest { -// claim_generator: "".to_string(), -// title: "".to_string(), -// instance_id: format!("urn:c2pa:{}", Uuid::new_v4()), -// claim: claim.clone(), -// ingredients: vec![], -// created_at: OffsetDateTimeWrapper(OffsetDateTime::now_utc()), -// cross_references: vec![], -// claim_v2: None, -// is_active: true, -// }; - -// if let Some(manifest_ids) = &config.linked_manifests { -// if let Some(storage_backend) = &config.storage { -// for linked_id in manifest_ids { -// match storage_backend.retrieve_manifest(linked_id) { -// Ok(linked_manifest) => { -// // Create a JSON representation of the linked manifest -// let linked_json = serde_json::to_string(&linked_manifest) -// .map_err(|e| Error::Serialization(e.to_string()))?; - -// // Create a hash of the linked manifest -// let linked_hash = hash::calculate_hash(linked_json.as_bytes()); - -// // Create a cross-reference -// let cross_ref = CrossReference { -// manifest_url: linked_id.clone(), -// manifest_hash: linked_hash, -// media_type: Some("application/json".to_string()), -// }; - -// // Add the cross-reference to the manifest -// manifest.cross_references.push(cross_ref); - -// println!("Added link to manifest: {linked_id}"); -// } -// Err(e) => { -// println!("Warning: Could not link to manifest {linked_id}: {e}"); -// } -// } -// } -// } else { -// println!("Warning: Cannot link manifests without a storage backend"); -// } -// } - -// // Generate the in-toto format Statement and sign the DSSE - -// // we need to convert this into a string to serialize into the Struct proto expected by in-toto -// let manifest_json = to_string(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; -// let manifest_proto = in_toto::json_to_struct_proto(&manifest_json)?; - -// let subject_hash = generate_oms_subject_hash(&manifest, &config.hash_alg)?; - -// let subject = in_toto::make_minimal_resource_descriptor( -// &config.name, -// hash::algorithm_to_string(&config.hash_alg), -// &subject_hash, -// ); - -// let key_path = config -// .key_path -// .ok_or_else(|| Error::Validation("OMS format requires a signing key".to_string()))?; - -// let envelope = in_toto::generate_signed_statement_v1( -// &[subject], -// "https://spec.c2pa.org/specifications/specifications/2.2", -// &manifest_proto, -// key_path.to_path_buf(), -// config.hash_alg, -// )?; - -// // Output manifest if requested -// if config.print || config.storage.is_none() { -// match config.output_encoding.to_lowercase().as_str() { -// "json" => { -// let envelope_json = -// to_string_pretty(&envelope).map_err(|e| Error::Serialization(e.to_string()))?; -// println!("{envelope_json}"); -// } -// "cbor" => { -// let envelope_cbor = serde_cbor::to_vec(&envelope) -// .map_err(|e| Error::Serialization(e.to_string()))?; -// println!("{}", hex::encode(&envelope_cbor)); -// } -// _ => { -// return Err(Error::Validation(format!( -// "Invalid output encoding '{}'. Valid options are: json, cbor", -// config.output_encoding -// ))); -// } -// } -// } - -// // Store manifest if storage is provided -// if let Some(storage) = &config.storage { -// if !config.print { -// let id = storage.store_manifest(&manifest)?; -// println!("Manifest stored successfully with ID: {id}"); -// } -// } - -// Ok(()) -// } - - -// ############################################################################################### - /// Lists manifests from storage, optionally filtered by asset type. /// /// This function retrieves all manifests from the provided storage backend and optionally @@ -1123,10 +907,6 @@ fn generate_oms_subject_hash(manifest: &Manifest, hash_alg: &HashAlgorithm) -> R )) } -// Refactor start here - - - enum ManifestFormat { Standalone, OMS, @@ -1188,7 +968,7 @@ impl ManifestFormat { }, }; // Output metadata_container if requested - // TODO: Or should this stay as output of the manifest only? + // TODO: Not tested. Also, should this stay as output of the manifest only? if config.print || config.storage.is_none() { match config.output_encoding.to_lowercase().as_str() { "json" => { @@ -1209,7 +989,7 @@ impl ManifestFormat { } } } - // TODO: Below work is needed possibly. We currently store the manifest in both cases (OMS and standalone) + // TODO: We currently store the manifest in both cases (OMS and standalone) regardless of the fact we are creating an envelope in the OMS case. // Store manifest if storage is provided if let Some(storage) = &config.storage { @@ -1222,10 +1002,6 @@ impl ManifestFormat { } } - -// Refactor end here - - #[cfg(test)] mod tests { use super::*; @@ -1270,7 +1046,7 @@ mod tests { let asset_dirpath = module_dir(); let path = asset_dirpath.join(TEST_ASSET_FILENAME); println!("Using test asset file for config {:#?}", path.to_str()); - // return the temp_dir so that it doesn't get deleted immediately + // return the temp_dir so that it doesn't get deleted due to coming out of scope (tmp_key_dir, asset_dirpath, ManifestCreationConfig { name: "test-model".to_string(), description: Some("A test model".to_string()), From 01b4c3b1635f27e13668668a66b4b235e93e5077 Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Fri, 6 Feb 2026 10:05:36 -0800 Subject: [PATCH 05/13] addressed cleanup of test file --- src/manifest/common.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index 02dca92..91847e2 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -990,6 +990,7 @@ impl ManifestFormat { } } // TODO: We currently store the manifest in both cases (OMS and standalone) regardless of the fact we are creating an envelope in the OMS case. + // TODO: Not tested. // Store manifest if storage is provided if let Some(storage) = &config.storage { @@ -1006,9 +1007,10 @@ impl ManifestFormat { mod tests { use super::*; use crate::signing::test_utils::generate_temp_key; - use std::fs::File; + use tempfile::TempPath; + use std::fs::{File, OpenOptions}; - const TEST_ASSET_FILENAME: &str = "empty_test_model_file.onnx"; + const TEST_ASSET_FILENAME: &str = "empty_test_model_file_not_expected_to_persist.onnx"; // Helper function to get the module directory (for creating test files) fn module_dir() -> PathBuf { @@ -1045,8 +1047,7 @@ mod tests { let key_path = tmp_key_dir.path().join("test_key.pem"); let asset_dirpath = module_dir(); let path = asset_dirpath.join(TEST_ASSET_FILENAME); - println!("Using test asset file for config {:#?}", path.to_str()); - // return the temp_dir so that it doesn't get deleted due to coming out of scope + // return the temp_dir (avoid deletion due to coming out of scope) (tmp_key_dir, asset_dirpath, ManifestCreationConfig { name: "test-model".to_string(), description: Some("A test model".to_string()), @@ -1096,8 +1097,11 @@ mod tests { fn test_create_oms_manifest() -> Result<()> { let (_tmp_key_dir, asset_dirpath, config) = make_test_oms_manifest_config(); let asset_path = asset_dirpath.join(TEST_ASSET_FILENAME); - let _file = File::create(&asset_path)?; - assert!(asset_path.exists(), "Test asset file does not exist at {:#?}", asset_path.to_str()); + let _file = OpenOptions::new() + .write(true) + .create_new(true) // Ensure if file already exists that we do not overwrite it + .open(&asset_path)?; + let _temp_path = TempPath::from_path(asset_path); // Ensure the file is deleted after the test let result = create_oms_manifest(config); assert!(result.is_ok(), "create_oms_manifest failed with error: {:#?}", From c4b3feac08def085e590a8812696c87bf6d746bf Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Fri, 6 Feb 2026 10:08:35 -0800 Subject: [PATCH 06/13] cleanup --- src/manifest/common.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index 91847e2..4d5423f 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -234,7 +234,6 @@ fn generate_c2pa_claim(config: &ManifestCreationConfig, asset_kind: AssetKind) - } /// Creates a C2PA manifest for a model, dataset, software, or evaluation -// TODO: Find better naming, this is the core creation after the refactor fn create_c2pa_manifest(config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result { let claim = generate_c2pa_claim(&config, asset_kind)?; From 27c2b5b533765152f78407d1cb862e43defbb4bc Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Fri, 6 Feb 2026 10:13:05 -0800 Subject: [PATCH 07/13] unused import --- src/manifest/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index 4d5423f..e2989fc 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -1007,7 +1007,7 @@ mod tests { use super::*; use crate::signing::test_utils::generate_temp_key; use tempfile::TempPath; - use std::fs::{File, OpenOptions}; + use std::fs::OpenOptions; const TEST_ASSET_FILENAME: &str = "empty_test_model_file_not_expected_to_persist.onnx"; From eb7a7eccd922c3c3c5f09f385449482cc3067050 Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Fri, 6 Feb 2026 10:33:42 -0800 Subject: [PATCH 08/13] clean up comments --- src/manifest/common.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index e2989fc..d30d4a6 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -234,6 +234,7 @@ fn generate_c2pa_claim(config: &ManifestCreationConfig, asset_kind: AssetKind) - } /// Creates a C2PA manifest for a model, dataset, software, or evaluation +// TODO: Should I make this public? fn create_c2pa_manifest(config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result { let claim = generate_c2pa_claim(&config, asset_kind)?; @@ -343,7 +344,7 @@ pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> /// }; /// /// create_oms_manifest(config).unwrap(); -/// +///``` pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> { let format = ManifestFormat::OMS; format.create(&config, AssetKind::Model) @@ -1011,7 +1012,7 @@ mod tests { const TEST_ASSET_FILENAME: &str = "empty_test_model_file_not_expected_to_persist.onnx"; - // Helper function to get the module directory (for creating test files) + // Helper function to get the module directory (for a predetermined directory in which to create temporary test files) fn module_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("src") @@ -1020,7 +1021,7 @@ mod tests { fn make_test_manifest_config() -> (tempfile::TempDir, ManifestCreationConfig) { let (_secure_key, tmp_key_dir) = generate_temp_key().unwrap(); let key_path = tmp_key_dir.path().join("test_key.pem"); - // must return the temp_key_dir to ensure it lives long enough for the tests that use the config, otherwise the temp dir (and key) will be deleted immediately after this function returns + // must return the temp_key_dir (temp dirs get deleted when out of scope) (tmp_key_dir, ManifestCreationConfig { name: "test-model".to_string(), description: Some("A test model".to_string()), From 8283afc08314a84440c91d0b0be78f805cf55114 Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Fri, 6 Feb 2026 10:35:51 -0800 Subject: [PATCH 09/13] simplifying diff --- src/manifest/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index d30d4a6..c6f4d07 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -344,7 +344,7 @@ pub fn create_manifest(config: ManifestCreationConfig, asset_kind: AssetKind) -> /// }; /// /// create_oms_manifest(config).unwrap(); -///``` +/// ``` pub fn create_oms_manifest(config: ManifestCreationConfig) -> Result<()> { let format = ManifestFormat::OMS; format.create(&config, AssetKind::Model) From 88350e6a8a1b91e82b0d59824fa3b39319f305ee Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Tue, 10 Feb 2026 14:49:43 -0800 Subject: [PATCH 10/13] cargo fmt --- src/manifest/common.rs | 131 ++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 60 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index c6f4d07..dd14b27 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -2,7 +2,6 @@ use crate::cc_attestation; use crate::error::{Error, Result}; use crate::hash; use crate::in_toto; -use in_toto::dsse::Envelope; use crate::manifest::config::ManifestCreationConfig; use crate::manifest::utils::{ determine_dataset_type, determine_format, determine_model_type, determine_software_type, @@ -19,8 +18,9 @@ use atlas_c2pa_lib::cross_reference::CrossReference; use atlas_c2pa_lib::datetime_wrapper::OffsetDateTimeWrapper; use atlas_c2pa_lib::ingredient::{Ingredient, IngredientData}; use atlas_c2pa_lib::manifest::Manifest; -use serde_json::{to_string, to_string_pretty}; +use in_toto::dsse::Envelope; use serde::Serialize; +use serde_json::{to_string, to_string_pretty}; use std::path::{Path, PathBuf}; use tdx_workload_attestation::get_platform_name; use time::OffsetDateTime; @@ -235,7 +235,10 @@ fn generate_c2pa_claim(config: &ManifestCreationConfig, asset_kind: AssetKind) - /// Creates a C2PA manifest for a model, dataset, software, or evaluation // TODO: Should I make this public? -fn create_c2pa_manifest(config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result { +fn create_c2pa_manifest( + config: &ManifestCreationConfig, + asset_kind: AssetKind, +) -> Result { let claim = generate_c2pa_claim(&config, asset_kind)?; // Create the manifest @@ -922,7 +925,7 @@ pub enum MetadataContainer { impl ManifestFormat { fn create(&self, config: &ManifestCreationConfig, asset_kind: AssetKind) -> Result<()> { // TODO: below we may return different things in second position if storage of envelope - // is preferred to storage of the manifest + // is preferred to storage of the manifest let (metadata_container, manifest) = match (self, &asset_kind) { (Self::Standalone, _) => { let mut manifest = create_c2pa_manifest(&config, asset_kind)?; @@ -930,14 +933,15 @@ impl ManifestFormat { manifest.sign(key_file.to_path_buf(), config.hash_alg.clone())?; } (MetadataContainer::C2PAManifest(manifest.clone()), manifest) - }, + } (Self::OMS, AssetKind::Model) => { let manifest = create_c2pa_manifest(&config, AssetKind::Model)?; - + // Generate the in-toto format Statement and sign the DSSE // we need to convert this into a string to serialize into the Struct proto expected by in-toto - let manifest_json = to_string(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; + let manifest_json = + to_string(&manifest).map_err(|e| Error::Serialization(e.to_string()))?; let manifest_proto = in_toto::json_to_struct_proto(&manifest_json)?; let subject_hash = generate_oms_subject_hash(&manifest, &config.hash_alg)?; @@ -947,10 +951,9 @@ impl ManifestFormat { hash::algorithm_to_string(&config.hash_alg), &subject_hash, ); - let key_path = config - .key_path - .as_ref() - .ok_or_else(|| Error::Validation("OMS format requires a signing key".to_string()))?; + let key_path = config.key_path.as_ref().ok_or_else(|| { + Error::Validation("OMS format requires a signing key".to_string()) + })?; let envelope = in_toto::generate_signed_statement_v1( &[subject], @@ -960,20 +963,20 @@ impl ManifestFormat { config.hash_alg.clone(), )?; (MetadataContainer::OMSInTotoEnvelope(envelope), manifest) - }, + } (Self::OMS, _) => { return Err(Error::Validation( "OMS format is only supported for AssetKind::Model".to_string(), - )); - }, + )); + } }; // Output metadata_container if requested // TODO: Not tested. Also, should this stay as output of the manifest only? if config.print || config.storage.is_none() { match config.output_encoding.to_lowercase().as_str() { "json" => { - let metadata_container_json = - to_string_pretty(&metadata_container).map_err(|e| Error::Serialization(e.to_string()))?; + let metadata_container_json = to_string_pretty(&metadata_container) + .map_err(|e| Error::Serialization(e.to_string()))?; println!("{metadata_container_json}"); } "cbor" => { @@ -991,7 +994,7 @@ impl ManifestFormat { } // TODO: We currently store the manifest in both cases (OMS and standalone) regardless of the fact we are creating an envelope in the OMS case. // TODO: Not tested. - + // Store manifest if storage is provided if let Some(storage) = &config.storage { if !config.print { @@ -1007,39 +1010,41 @@ impl ManifestFormat { mod tests { use super::*; use crate::signing::test_utils::generate_temp_key; - use tempfile::TempPath; use std::fs::OpenOptions; + use tempfile::TempPath; const TEST_ASSET_FILENAME: &str = "empty_test_model_file_not_expected_to_persist.onnx"; // Helper function to get the module directory (for a predetermined directory in which to create temporary test files) fn module_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("src") + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src") } fn make_test_manifest_config() -> (tempfile::TempDir, ManifestCreationConfig) { let (_secure_key, tmp_key_dir) = generate_temp_key().unwrap(); let key_path = tmp_key_dir.path().join("test_key.pem"); // must return the temp_key_dir (temp dirs get deleted when out of scope) - (tmp_key_dir, ManifestCreationConfig { - name: "test-model".to_string(), - description: Some("A test model".to_string()), - author_name: Some("Test Author".to_string()), - author_org: Some("Test Org".to_string()), - paths: vec![], - ingredient_names: vec![], - hash_alg: HashAlgorithm::Sha384, - key_path: Some(key_path), - output_encoding: "json".to_string(), - print: false, - storage: None, - with_cc: false, - linked_manifests: None, - custom_fields: None, - software_type: None, - version: None, - }) + ( + tmp_key_dir, + ManifestCreationConfig { + name: "test-model".to_string(), + description: Some("A test model".to_string()), + author_name: Some("Test Author".to_string()), + author_org: Some("Test Org".to_string()), + paths: vec![], + ingredient_names: vec![], + hash_alg: HashAlgorithm::Sha384, + key_path: Some(key_path), + output_encoding: "json".to_string(), + print: false, + storage: None, + with_cc: false, + linked_manifests: None, + custom_fields: None, + software_type: None, + version: None, + }, + ) } fn make_test_oms_manifest_config() -> (tempfile::TempDir, PathBuf, ManifestCreationConfig) { @@ -1048,24 +1053,28 @@ mod tests { let asset_dirpath = module_dir(); let path = asset_dirpath.join(TEST_ASSET_FILENAME); // return the temp_dir (avoid deletion due to coming out of scope) - (tmp_key_dir, asset_dirpath, ManifestCreationConfig { - name: "test-model".to_string(), - description: Some("A test model".to_string()), - author_name: Some("Test Author".to_string()), - author_org: Some("Test Org".to_string()), - paths: vec![path], - ingredient_names: vec!["Test model ingredient".to_string()], // OMS requires at least one ingredient - hash_alg: HashAlgorithm::Sha384, - key_path: Some(key_path), - output_encoding: "json".to_string(), - print: false, - storage: None, - with_cc: false, - linked_manifests: None, - custom_fields: None, - software_type: None, - version: None, - }) + ( + tmp_key_dir, + asset_dirpath, + ManifestCreationConfig { + name: "test-model".to_string(), + description: Some("A test model".to_string()), + author_name: Some("Test Author".to_string()), + author_org: Some("Test Org".to_string()), + paths: vec![path], + ingredient_names: vec!["Test model ingredient".to_string()], // OMS requires at least one ingredient + hash_alg: HashAlgorithm::Sha384, + key_path: Some(key_path), + output_encoding: "json".to_string(), + print: false, + storage: None, + with_cc: false, + linked_manifests: None, + custom_fields: None, + software_type: None, + version: None, + }, + ) } #[test] @@ -1085,7 +1094,7 @@ mod tests { } #[test] - fn test_create_manifest() -> Result<()>{ + fn test_create_manifest() -> Result<()> { let (_tmp_key_dir, config) = make_test_manifest_config(); let result = create_manifest(config, AssetKind::Model); assert!(result.is_ok()); // Should succeed even with no ingredients @@ -1103,9 +1112,11 @@ mod tests { .open(&asset_path)?; let _temp_path = TempPath::from_path(asset_path); // Ensure the file is deleted after the test let result = create_oms_manifest(config); - assert!(result.is_ok(), - "create_oms_manifest failed with error: {:#?}", - result.err()); // Should succeed with the provided key + assert!( + result.is_ok(), + "create_oms_manifest failed with error: {:#?}", + result.err() + ); // Should succeed with the provided key Ok(()) } From 9aa466fe7b192deadced79c483a6d358f54e0bc4 Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Wed, 11 Feb 2026 10:08:14 -0800 Subject: [PATCH 11/13] Updating info regarding storage testing into TODO comment. --- src/manifest/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index dd14b27..c71ef9e 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -971,7 +971,7 @@ impl ManifestFormat { } }; // Output metadata_container if requested - // TODO: Not tested. Also, should this stay as output of the manifest only? + // TODO: Lightly tested with file system storage. Is there anything to output regarding the envelope, separate from the manifest? if config.print || config.storage.is_none() { match config.output_encoding.to_lowercase().as_str() { "json" => { From 1bab818528693eed8197baf38b5af58416c4efcb Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Wed, 11 Feb 2026 10:12:01 -0800 Subject: [PATCH 12/13] more comments --- src/manifest/common.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index c71ef9e..2659f0e 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -971,7 +971,7 @@ impl ManifestFormat { } }; // Output metadata_container if requested - // TODO: Lightly tested with file system storage. Is there anything to output regarding the envelope, separate from the manifest? + // TODO: Is there anything to output regarding the envelope, separate from the manifest? if config.print || config.storage.is_none() { match config.output_encoding.to_lowercase().as_str() { "json" => { @@ -992,8 +992,8 @@ impl ManifestFormat { } } } - // TODO: We currently store the manifest in both cases (OMS and standalone) regardless of the fact we are creating an envelope in the OMS case. - // TODO: Not tested. + // TODO: Is there anything to store regarding the envelope, separate from the manifest? + // TODO: Lightly tested with filesystem storage. // Store manifest if storage is provided if let Some(storage) = &config.storage { From 50c144023f4365c03ba65620758284b9a36ce012 Mon Sep 17 00:00:00 2001 From: Brandon Edwards Date: Wed, 11 Feb 2026 10:13:31 -0800 Subject: [PATCH 13/13] more comments --- src/manifest/common.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/manifest/common.rs b/src/manifest/common.rs index 2659f0e..1be5f08 100644 --- a/src/manifest/common.rs +++ b/src/manifest/common.rs @@ -971,7 +971,6 @@ impl ManifestFormat { } }; // Output metadata_container if requested - // TODO: Is there anything to output regarding the envelope, separate from the manifest? if config.print || config.storage.is_none() { match config.output_encoding.to_lowercase().as_str() { "json" => {