Skip to content
6 changes: 5 additions & 1 deletion cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,10 +794,14 @@
fragments_glob: Some(fg),
}) = &args.command
{
let stores = verify_fragmented(&args.path, fg)?;
let mut stores = verify_fragmented(&args.path, fg)?;

Check warning on line 797 in cli/src/main.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/main.rs#L797

Added line #L797 was not covered by tests
if stores.len() == 1 {
validate_cawg(&mut stores[0])?;

Check warning on line 799 in cli/src/main.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/main.rs#L799

Added line #L799 was not covered by tests
println!("{}", stores[0]);
} else {
for store in &mut stores {
validate_cawg(store)?;

Check warning on line 803 in cli/src/main.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/main.rs#L802-L803

Added lines #L802 - L803 were not covered by tests
}
println!("{} Init manifests validated", stores.len());
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ name = "v2show"
[[example]]
name = "v2api"

[[example]]
name = "fragmented_bmff"
required-features = ["file_io"]

[lib]
crate-type = ["lib"]

Expand Down
184 changes: 184 additions & 0 deletions sdk/examples/fragmented_bmff.rs
Original file line number Diff line number Diff line change
@@ -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<impl Signer> {
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<S: AsRef<Path>, G: AsRef<Path>, D: AsRef<Path>>(
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<String> = 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)
}
2 changes: 1 addition & 1 deletion sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ impl AssertionDefinition {
///
/// # Example: Building and signing a manifest:
///
/// ```
///
/// # use c2pa::Result;
/// use std::path::PathBuf;
///
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
//!
//! # Example: Adding a Manifest to a file
//!
//! ```
//!
//! # use c2pa::Result;
//! use std::path::PathBuf;
//!
Expand Down
69 changes: 62 additions & 7 deletions sdk/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2120,6 +2120,9 @@
pc.replace_assertion(assertion)?;
}

// clear the provenance claim data since the contents are now different
pc.clear_data();

Ok(true)
}

Expand Down Expand Up @@ -2204,27 +2207,78 @@
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)?;
}

Check warning on line 2268 in sdk/src/store.rs

View check run for this annotation

Codecov / codecov/patch

sdk/src/store.rs#L2268

Added line #L2268 was not covered by tests

// regenerate the jumbf because the cbor changed
jumbf_bytes = temp_store.to_jumbf_internal(signer.reserve_size())?;
}
_ => (),

Check warning on line 2273 in sdk/src/store.rs

View check run for this annotation

Codecov / codecov/patch

sdk/src/store.rs#L2273

Added line #L2273 was not covered by tests
};
}

Check warning on line 2275 in sdk/src/store.rs

View check run for this annotation

Codecov / codecov/patch

sdk/src/store.rs#L2275

Added line #L2275 was not covered by tests

// 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),
Expand Down Expand Up @@ -3866,7 +3920,7 @@
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},
},
};

Expand Down Expand Up @@ -6589,7 +6643,8 @@
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)
Expand Down
22 changes: 22 additions & 0 deletions sdk/src/utils/test_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ pub(crate) fn test_signer(alg: SigningAlg) -> Box<dyn Signer> {
))
}

pub(crate) fn test_cawg_signer(
alg: SigningAlg,
referenced_assertions: &[&str],
) -> Result<Box<dyn Signer>> {
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)]
Expand Down