diff --git a/cli/src/main.rs b/cli/src/main.rs index e0b1f08be..c2d6c2747 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -794,10 +794,14 @@ fn main() -> Result<()> { fragments_glob: Some(fg), }) = &args.command { - let stores = verify_fragmented(&args.path, fg)?; + let mut stores = verify_fragmented(&args.path, fg)?; if stores.len() == 1 { + validate_cawg(&mut stores[0])?; println!("{}", stores[0]); } else { + for store in &mut stores { + validate_cawg(store)?; + } println!("{} Init manifests validated", stores.len()); } } else { diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 87af78a1c..0d5dd3c15 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -68,6 +68,10 @@ name = "v2show" [[example]] name = "v2api" +[[example]] +name = "fragmented_bmff" +required-features = ["file_io"] + [lib] crate-type = ["lib"] diff --git a/sdk/examples/fragmented_bmff.rs b/sdk/examples/fragmented_bmff.rs new file mode 100644 index 000000000..a91588234 --- /dev/null +++ b/sdk/examples/fragmented_bmff.rs @@ -0,0 +1,184 @@ +// Copyright 2025 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + +//! Example App that generates a CAWG manifest for a given file +//! and validates the identity assertion. +//! +//! This example is not supported on WASI targets. +//! +//! ```bash +//! cargo run --example fragmented_cawg -- /path/to/source/init_segment_or_glob fragment_glob_pattern path/to/destination/folder +//! ``` + +mod cawg { + use std::path::{Path, PathBuf}; + + use anyhow::{anyhow, bail, Context, Result}; + use c2pa::{ + crypto::raw_signature, + identity::{ + builder::{IdentityAssertionBuilder, IdentityAssertionSigner}, + x509::X509CredentialHolder, + }, + Builder, Signer, SigningAlg, + }; + use serde_json::json; + + const CERTS: &[u8] = include_bytes!("../../sdk/tests/fixtures/certs/es256.pub"); + const PRIVATE_KEY: &[u8] = include_bytes!("../../sdk/tests/fixtures/certs/es256.pem"); + + const CAWG_CERTS: &[u8] = include_bytes!("../../sdk/tests/fixtures/certs/ed25519.pub"); + const CAWG_PRIVATE_KEY: &[u8] = include_bytes!("../../sdk/tests/fixtures/certs/ed25519.pem"); + + fn manifest_def() -> String { + json!({ + "claim_version": 2, + "claim_generator_info": [ + { + "name": "c2pa cawg test", + "version": env!("CARGO_PKG_VERSION") + } + ], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": " http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture" + } + ] + } + }, + { + "label": "cawg.training-mining", + "data": { + "entries": { + "cawg.ai_inference": { + "use": "notAllowed" + }, + "cawg.ai_generative_training": { + "use": "notAllowed" + } + } + } + } + ] + }) + .to_string() + } + + /// Creates a CAWG signer from a certificate chains and private keys. + fn cawg_signer(referenced_assertions: &[&str]) -> Result { + let c2pa_raw_signer = raw_signature::signer_from_cert_chain_and_private_key( + CERTS, + PRIVATE_KEY, + SigningAlg::Es256, + None, + )?; + + let cawg_raw_signer = raw_signature::signer_from_cert_chain_and_private_key( + CAWG_CERTS, + CAWG_PRIVATE_KEY, + SigningAlg::Ed25519, + None, + )?; + + let mut ia_signer = IdentityAssertionSigner::new(c2pa_raw_signer); + + let x509_holder = X509CredentialHolder::from_raw_signer(cawg_raw_signer); + let mut iab = IdentityAssertionBuilder::for_credential_holder(x509_holder); + iab.add_referenced_assertions(referenced_assertions); + + ia_signer.add_identity_assertion(iab); + Ok(ia_signer) + } + + pub fn run, G: AsRef, D: AsRef>( + source: S, + glob_pattern: G, + dest: D, + ) -> Result<()> { + let source = source.as_ref(); + let glob_pattern = glob_pattern.as_ref().to_path_buf(); + let dest = dest.as_ref(); + + let mut builder = Builder::from_json(&manifest_def())?; + builder.definition.claim_version = Some(2); // CAWG should only be used on v2 claims + + // This example will generate a CAWG manifest referencing the training-mining + // assertion. + let signer = cawg_signer(&["cawg.training-mining"])?; + + sign_fragmented(&mut builder, &signer, source, &glob_pattern, dest) + } + + fn sign_fragmented( + builder: &mut Builder, + signer: &dyn Signer, + init_pattern: &Path, + frag_pattern: &PathBuf, + output_path: &Path, + ) -> Result<()> { + // search folders for init segments + let ip = init_pattern + .to_str() + .ok_or(anyhow!("could not parse source pattern"))?; + let inits = glob::glob(ip).context("could not process glob pattern")?; + let mut count = 0; + for init in inits { + match init { + Ok(p) => { + let mut fragments = Vec::new(); + let init_dir = p.parent().context("init segment had no parent dir")?; + let seg_glob = init_dir.join(frag_pattern); // segment match pattern + + // grab the fragments that go with this init segment + let seg_glob_str = seg_glob.to_str().context("fragment path not valid")?; + let seg_paths = glob::glob(seg_glob_str).context("fragment glob not valid")?; + for seg in seg_paths { + match seg { + Ok(f) => fragments.push(f), + Err(_) => return Err(anyhow!("fragment path not valid")), + } + } + + println!("Adding manifest to: {:?}", p); + let new_output_path = + output_path.join(init_dir.file_name().context("invalid file name")?); + builder.sign_fragmented_files(signer, &p, &fragments, &new_output_path)?; + + count += 1; + } + Err(_) => bail!("bad path to init segment"), + } + } + if count == 0 { + println!("No files matching pattern: {}", ip); + } + Ok(()) + } +} + +fn main() -> anyhow::Result<()> { + let args: Vec = std::env::args().collect(); + if args.len() < 4 { + eprintln!("Creates a CAWG manifest (requires source path to init segment or init segment glob, glob pattern for fragments, and destination folder paths)"); + std::process::exit(1); + } + let source: std::path::PathBuf = std::path::PathBuf::from(&args[1]); + let glob_pattern: std::path::PathBuf = std::path::PathBuf::from(&args[2]); + let dest: std::path::PathBuf = std::path::PathBuf::from(&args[3]); + cawg::run(source, glob_pattern, dest) +} diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index e6ceed8fa..4cc2e0b30 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -162,7 +162,7 @@ impl AssertionDefinition { /// /// # Example: Building and signing a manifest: /// -/// ``` +/// /// # use c2pa::Result; /// use std::path::PathBuf; /// diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index dfc891f09..336466d91 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -47,7 +47,7 @@ //! //! # Example: Adding a Manifest to a file //! -//! ``` +//! //! # use c2pa::Result; //! use std::path::PathBuf; //! diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 4fcab5af8..258cdf353 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -2120,6 +2120,9 @@ impl Store { pc.replace_assertion(assertion)?; } + // clear the provenance claim data since the contents are now different + pc.clear_data(); + Ok(true) } @@ -2204,27 +2207,78 @@ impl Store { None => return Err(Error::UnsupportedType), } + let output_filename = asset_path.file_name().ok_or(Error::NotFound)?; + let dest_path = output_path.join(output_filename); + let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); + + // add dynamic assertions to the store + let dynamic_assertions = signer.dynamic_assertions(); + let da_uris = self.add_dynamic_assertion_placeholders(&dynamic_assertions)?; + + // get temp store as JUMBF let jumbf = self.to_jumbf(signer)?; - // use temp store so mulitple calls will work (the Store is not finalized this way) + // use temp store so mulitple calls across renditions will work (the Store is not finalized this way) let mut temp_store = Store::from_jumbf(&jumbf, &mut validation_log)?; - let jumbf_bytes = temp_store.start_save_bmff_fragmented( + let mut jumbf_bytes = temp_store.start_save_bmff_fragmented( asset_path, fragments, output_path, signer.reserve_size(), )?; + let mut preliminary_claim = PartialClaim::default(); + { + let pc = temp_store.provenance_claim().ok_or(Error::ClaimEncoding)?; + for assertion in pc.assertions() { + preliminary_claim.add_assertion(assertion); + } + } + + // Now add the dynamic assertions and update the JUMBF. + let modified = temp_store.write_dynamic_assertions( + &dynamic_assertions, + &da_uris, + &mut preliminary_claim, + )?; + + // update the JUMBF if modified with dynamic assertions + if modified { + let pc = temp_store.provenance_claim().ok_or(Error::ClaimEncoding)?; + match pc.remote_manifest() { + RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { + jumbf_bytes = temp_store.to_jumbf_internal(signer.reserve_size())?; + + // save the jumbf to the output path + save_jumbf_to_file(&jumbf_bytes, &dest_path, Some(&dest_path))?; + + let pc = temp_store + .provenance_claim_mut() + .ok_or(Error::ClaimEncoding)?; + // generate actual hash values + let bmff_hashes = pc.bmff_hash_assertions(); + + if !bmff_hashes.is_empty() { + let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0])?; + bmff_hash.update_fragmented_inithash(&dest_path)?; + pc.update_bmff_hash(bmff_hash)?; + } + + // regenerate the jumbf because the cbor changed + jumbf_bytes = temp_store.to_jumbf_internal(signer.reserve_size())?; + } + _ => (), + }; + } + + // sign the claim let pc = temp_store.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = temp_store.sign_claim(pc, signer, signer.reserve_size())?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); - let output_filename = asset_path.file_name().ok_or(Error::NotFound)?; - let dest_path = output_path.join(output_filename); - match temp_store.finish_save(jumbf_bytes, &dest_path, sig, &sig_placeholder) { Ok(_) => Ok(()), Err(e) => Err(e), @@ -3866,7 +3920,7 @@ pub mod tests { create_test_claim, fixture_path, temp_dir_path, temp_fixture_path, write_jpeg_placeholder_file, }, - test_signer::{async_test_signer, test_signer}, + test_signer::{async_test_signer, test_cawg_signer, test_signer}, }, }; @@ -6589,7 +6643,8 @@ pub mod tests { store.commit_claim(claim).unwrap(); // Do we generate JUMBF? - let signer = test_signer(SigningAlg::Ps256); + let signer = + test_cawg_signer(SigningAlg::Ps256, &[labels::SCHEMA_ORG]).unwrap(); // Use Tempdir for automatic cleanup let new_subdir = tempfile::TempDir::new_in(output_path) diff --git a/sdk/src/utils/test_signer.rs b/sdk/src/utils/test_signer.rs index e903b589d..da65925de 100644 --- a/sdk/src/utils/test_signer.rs +++ b/sdk/src/utils/test_signer.rs @@ -33,6 +33,28 @@ pub(crate) fn test_signer(alg: SigningAlg) -> Box { )) } +pub(crate) fn test_cawg_signer( + alg: SigningAlg, + referenced_assertions: &[&str], +) -> Result> { + let (cert_chain, private_key) = cert_chain_and_private_key_for_alg(alg); + + let c2pa_raw_signer = + signer_from_cert_chain_and_private_key(&cert_chain, &private_key, alg, None).unwrap(); + let cawg_raw_signer = + signer_from_cert_chain_and_private_key(&cert_chain, &private_key, alg, None).unwrap(); + + let mut ia_signer = crate::identity::builder::IdentityAssertionSigner::new(c2pa_raw_signer); + + let x509_holder = crate::identity::x509::X509CredentialHolder::from_raw_signer(cawg_raw_signer); + let mut iab = + crate::identity::builder::IdentityAssertionBuilder::for_credential_holder(x509_holder); + iab.add_referenced_assertions(referenced_assertions); + + ia_signer.add_identity_assertion(iab); + Ok(Box::new(ia_signer)) +} + /// Creates an [`AsyncSigner`] instance for testing purposes using test credentials. #[cfg(not(target_arch = "wasm32"))] #[allow(dead_code)]