From a09b013e805ccff785c99410ae0aa2a0cdb26dce Mon Sep 17 00:00:00 2001 From: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:05:56 -0400 Subject: [PATCH] Added support for external manifests (#101) * Treat 'meta' box as standard container Support for more BMFF container types Handle bad FourCC names Add bad signer unit test * clippy fixes * External manifest generation * More detailed unit test Bug fixes Slight speed up * test smaller chunk size for hashing * Only generate output upon success. Auto cleanup temp content * cleanup * WASM build fixes * code review fixes * Fix typo * Changes based on feedback * Support Info for constructor Put remote manifest behind xmp_write feature Split external manifest into two functions * One more suggestion * formatting * formatting --- sdk/Cargo.toml | 2 +- sdk/src/asset_handlers/c2pa_io.rs | 11 +- sdk/src/claim.rs | 75 +++++- sdk/src/store.rs | 384 +++++++++++++++++++++++++----- sdk/src/utils/hash_utils.rs | 2 +- 5 files changed, 410 insertions(+), 64 deletions(-) diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 7e5965d67..f3c4e1e62 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -79,13 +79,13 @@ tempfile = "3.1.0" thiserror = ">= 1.0.20, < 1.0.32" time = ">= 0.2.23" twoway = "0.2.1" +url = "2.2.2" uuid = { version = "0.8.1", features = ["serde", "v4", "wasm-bindgen"] } x509-parser = "0.11.0" x509-certificate = "0.12.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ring = "0.16.20" -url = "2.2.2" ureq = "2.4.0" image = { version = "0.24.2", optional = true } instant = "0.1.0" diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs index 8924ab88d..944c44a5d 100644 --- a/sdk/src/asset_handlers/c2pa_io.rs +++ b/sdk/src/asset_handlers/c2pa_io.rs @@ -14,10 +14,9 @@ use std::{fs::File, path::Path}; use crate::{ - asset_io::{AssetIO, CAILoader, CAIRead, HashObjectPositions}, + asset_io::{AssetIO, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions}, error::{Error, Result}, }; - /// Supports working with ".c2pa" files containing only manifest store data pub struct C2paIO {} @@ -53,7 +52,13 @@ impl AssetIO for C2paIO { &self, _asset_path: &std::path::Path, ) -> Result> { - Ok(Vec::new()) + let hop = HashObjectPositions { + offset: 0, + length: 0, + htype: HashBlockObjectType::Cai, + }; + + Ok(vec![hop]) } } diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index 0d3d30e7e..ec31f0311 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -156,6 +156,10 @@ impl fmt::Debug for ClaimAssertion { /// that hash is signed to produce the claim signature. #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct Claim { + // external manifest + #[serde(skip_deserializing, skip_serializing)] + remote_manifest: RemoteManifest, + // root of CAI store #[serde(skip_deserializing, skip_serializing)] update_manifest: bool, @@ -229,6 +233,20 @@ pub enum AssertionStoreJsonFormat { OrderedListNoBinary, // list of Assertions as json objects omitting binaries results } +/// Remote manifest options. Use 'set_remote_manifest' to generate external manifests. +#[derive(Clone, Debug, PartialEq)] +pub enum RemoteManifest { + NoRemote, // No external manifest (default) + SideCar, // Manifest will be saved as a side car file, output asset is untouched. + Remote(String), // Manifest will be saved as a side car file, output asset will contain remote reference +} + +impl Default for RemoteManifest { + fn default() -> Self { + Self::NoRemote + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct JsonOrderedAssertionData { label: String, @@ -247,8 +265,7 @@ impl Claim { /// Create a new claim. /// vendor: name used to label the claim (unique instance number is automatically calculated) /// claim_generator: User agent see c2pa spec for format - // #[cfg(not(target_arch = "wasm32"))] - pub fn new(claim_generator: &str, vendor: Option<&str>) -> Self { + pub fn new>(claim_generator: S, vendor: Option<&str>) -> Self { let urn = Uuid::new_v4(); let l = match vendor { Some(v) => format!( @@ -263,6 +280,7 @@ impl Claim { }; Claim { + remote_manifest: RemoteManifest::NoRemote, box_prefix: "self#jumbf".to_string(), root: jumbf::labels::MANIFEST_STORE.to_string(), signature_val: Vec::new(), @@ -270,7 +288,7 @@ impl Claim { label: l, signature: "".to_string(), - claim_generator: claim_generator.to_string(), + claim_generator: claim_generator.into(), assertion_store: Vec::new(), vc_store: Vec::new(), assertions: Vec::new(), @@ -288,6 +306,37 @@ impl Claim { } } + /// Create a new claim with a user supplied GUID. + /// user_guid: is user supplied guid conforming the C2PA spec for manifest names + /// claim_generator: User agent see c2pa spec for format + pub fn new_with_user_guid>(claim_generator: S, user_guid: S) -> Self { + Claim { + remote_manifest: RemoteManifest::NoRemote, + box_prefix: "self#jumbf".to_string(), + root: jumbf::labels::MANIFEST_STORE.to_string(), + signature_val: Vec::new(), + ingredients_store: HashMap::new(), + label: user_guid.into(), // todo figure out how to validate this + signature: "".to_string(), + + claim_generator: claim_generator.into(), + assertion_store: Vec::new(), + vc_store: Vec::new(), + assertions: Vec::new(), + original_bytes: None, + redacted_assertions: None, + alg: Some(BUILD_HASH_ALG.into()), + alg_soft: None, + claim_generator_hints: None, + + title: None, + format: "".to_string(), + instance_id: "".to_string(), + + update_manifest: false, + } + } + /// Build a claim and verify its integrity. pub fn build(&mut self) -> Result<()> { // A claim must have a signature box. @@ -381,6 +430,26 @@ impl Claim { self.update_manifest } + pub fn set_remote_manifest + AsRef>( + &mut self, + remote_url: S, + ) -> Result<()> { + url::Url::parse(remote_url.as_ref()) + .map_err(|_e| Error::BadParam("remote url is badly formed".to_string()))?; + self.remote_manifest = RemoteManifest::Remote(remote_url.into()); + + Ok(()) + } + + pub fn set_external_manifest(&mut self) { + self.remote_manifest = RemoteManifest::SideCar; + } + + #[cfg(feature = "file_io")] + pub(crate) fn remote_manifest(&self) -> RemoteManifest { + self.remote_manifest.clone() + } + pub(crate) fn set_update_manifest(&mut self, is_update_manifest: bool) { self.update_manifest = is_update_manifest; } diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 8853b45f2..9039c22cd 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -27,6 +27,7 @@ use crate::{ assertion::AssertionData, assertions::{BmffHash, DataHash, DataMap, ExclusionsMap, SubsetMap}, asset_io::{HashBlockObjectType, HashObjectPositions}, + claim::RemoteManifest, cose_sign::cose_sign, cose_validator::verify_cose, jumbf_io::{ @@ -53,6 +54,8 @@ use crate::{ ManifestStoreReport, }; +const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests + /// A `Store` maintains a list of `Claim` structs. /// /// Typically, this list of `Claim`s represents all of the claims in an asset. @@ -92,7 +95,7 @@ impl Default for Store { impl Store { /// Create a new, empty claims store. pub fn new() -> Self { - Self::new_with_label(jumbf::labels::MANIFEST_STORE) + Self::new_with_label(MANIFEST_STORE_EXT) } /// Create a new, empty claims store with a custom label. @@ -136,16 +139,14 @@ impl Store { &self.claims } - /// Add a new Claim to this Store. The claim label - /// may be updated to reflect is position in the Claim Store - /// if there are conflicting label names. The function - /// will return the label of the claim used + /// Add a new Claim to this Store. The function + /// will return the label of the claim. pub fn commit_claim(&mut self, mut claim: Claim) -> Result { // verify the claim is valid claim.build()?; // load the claim ingredients - // preparse first to make sure we can load them + // parse first to make sure we can load them let mut ingredient_claims: Vec = Vec::new(); for (pc, claims) in claim.claim_ingredient_store() { let mut valid_pc = false; @@ -1233,9 +1234,8 @@ impl Store { calc_hashes: bool, ) -> Result> { if block_locations.is_empty() { - return Err(Error::BadParam( - "No asset hash locations specified".to_owned(), - )); + let out: Vec = vec![]; + return Ok(out); } let metadata = asset_path.metadata().map_err(crate::error::wrap_io_err)?; @@ -1273,7 +1273,9 @@ impl Store { if found_jumbf { // add exclusion hash for bytes before and after jumbf let mut dh = DataHash::new("jumbf manifest", alg, None); - dh.add_exclusion(Exclusion::new(block_start, block_end - block_start)); + if block_end > block_start { + dh.add_exclusion(Exclusion::new(block_start, block_end - block_start)); + } if calc_hashes { dh.gen_hash(asset_path)?; } else { @@ -1386,26 +1388,76 @@ impl Store { Ok(hashes) } + + // move or copy data from source to dest + #[cfg(feature = "file_io")] + fn move_or_copy(source: &Path, dest: &Path) -> Result<()> { + // copy temp file to asset + std::fs::rename(source, dest) + // if rename fails, try to copy in case we are on different volumes or output does not exist + .or_else(|_| std::fs::copy(source, dest).and(Ok(()))) + .map_err(Error::IoError) + } + + // copy output and possibly the external manifest to final destination + #[cfg(feature = "file_io")] + fn copy_c2pa_to_output(source: &Path, dest: &Path, remote_type: RemoteManifest) -> Result<()> { + match remote_type { + crate::claim::RemoteManifest::NoRemote => Store::move_or_copy(source, dest)?, + crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => { + // make correct path names + let source_asset = source; + let source_cai = source_asset.with_extension(MANIFEST_STORE_EXT); + let dest_cai = dest.with_extension(MANIFEST_STORE_EXT); + + Store::move_or_copy(&source_cai, &dest_cai)?; // copy manifest + Store::move_or_copy(source_asset, dest)?; // copy asset + } + } + Ok(()) + } + /// Embed the claims store as jumbf into an asset. Updates XMP with provenance record. #[cfg(feature = "file_io")] pub fn save_to_asset( &mut self, asset_path: &Path, signer: &dyn Signer, - output_path: &Path, - ) -> Result<()> { - let jumbf_bytes = self.start_save(asset_path, output_path, signer.reserve_size())?; + dest_path: &Path, + ) -> Result> { + // set up temp dir, contents auto deleted + let td = tempfile::TempDir::new()?; + let temp_path = td.into_path(); + let temp_file = temp_path.join( + dest_path + .file_name() + .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?, + ); + + let jumbf_bytes = self.start_save(asset_path, &temp_file, signer.reserve_size())?; let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = self.sign_claim(pc, signer, signer.reserve_size())?; let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size()); - match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) { - Ok(v) => { + // get correct output path for remote manifest + let output_path = match pc.remote_manifest() { + crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(), + crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => { + temp_file.with_extension(MANIFEST_STORE_EXT) + } + }; + + match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) { + Ok((s, m)) => { // save sig so store is up to date let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - pc_mut.set_signature_val(v); - Ok(()) + pc_mut.set_signature_val(s); + + // copy the correct files upon completion + Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?; + + Ok(m) } Err(e) => Err(e), } @@ -1417,9 +1469,18 @@ impl Store { &mut self, asset_path: &Path, signer: &dyn AsyncSigner, - output_path: &Path, - ) -> Result<()> { - let jumbf_bytes = self.start_save(asset_path, output_path, signer.reserve_size())?; + dest_path: &Path, + ) -> Result> { + // set up temp dir, contents auto deleted + let td = tempfile::TempDir::new()?; + let temp_path = td.into_path(); + let temp_file = temp_path.join( + dest_path + .file_name() + .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?, + ); + + let jumbf_bytes = self.start_save(asset_path, &temp_file, signer.reserve_size())?; let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = self @@ -1427,12 +1488,24 @@ impl Store { .await?; let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size()); - match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) { - Ok(v) => { + // get correct output path for remote manifest + let output_path = match pc.remote_manifest() { + crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(), + crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => { + temp_file.with_extension(MANIFEST_STORE_EXT) + } + }; + + match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) { + Ok((s, m)) => { // save sig so store is up to date let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - pc_mut.set_signature_val(v); - Ok(()) + pc_mut.set_signature_val(s); + + // copy the correct files upon completion + Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?; + + Ok(m) } Err(e) => Err(e), } @@ -1444,21 +1517,42 @@ impl Store { &mut self, asset_path: &Path, remote_signer: &dyn crate::signer::RemoteSigner, - output_path: &Path, - ) -> Result<()> { - let jumbf_bytes = self.start_save(asset_path, output_path, remote_signer.reserve_size())?; + dest_path: &Path, + ) -> Result> { + // set up temp dir, contents auto deleted + let td = tempfile::TempDir::new()?; + let temp_path = td.into_path(); + let temp_file = temp_path.join( + dest_path + .file_name() + .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?, + ); + + let jumbf_bytes = self.start_save(asset_path, &temp_file, remote_signer.reserve_size())?; let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = remote_signer.sign_remote(&pc.data()?).await?; let sig_placeholder = self.sign_claim_placeholder(pc, remote_signer.reserve_size()); - match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) { - Ok(v) => { + // get correct output path for remote manifest + let output_path = match pc.remote_manifest() { + crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(), + crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => { + temp_file.with_extension(MANIFEST_STORE_EXT) + } + }; + + match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) { + Ok((s, m)) => { // save sig so store is up to date let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - pc_mut.set_signature_val(v); - Ok(()) + pc_mut.set_signature_val(s); + + // copy the correct files upon completion + Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?; + + Ok(m) } Err(e) => Err(e), } @@ -1468,39 +1562,67 @@ impl Store { fn start_save( &mut self, asset_path: &Path, - output_path: &Path, + dest_path: &Path, reserve_size: usize, ) -> Result> { // clone the source to working copy if requested + get_supported_file_extension(asset_path).ok_or(Error::UnsupportedType)?; // verify extensions - let ext = get_supported_file_extension(output_path).ok_or(Error::UnsupportedType)?; - if asset_path != output_path { - fs::copy(&asset_path, &output_path).map_err(Error::IoError)?; + let ext = get_supported_file_extension(dest_path).ok_or(Error::UnsupportedType)?; + if asset_path != dest_path { + fs::copy(&asset_path, &dest_path).map_err(Error::IoError)?; } // update file following the steps outlined in CAI spec // 1) Add DC provenance XMP - // update XMP info & add xmp hash to provenance claim - #[cfg(feature = "xmp_write")] - if let Some(provenance) = self.provenance_path() { - embedded_xmp::add_manifest_uri_to_file(output_path, &provenance)?; + let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; + let output_path = if cfg!(feature = "xmp_write") { + match pc.remote_manifest() { + crate::claim::RemoteManifest::NoRemote => { + // update XMP info & add xmp hash to provenance claim + #[cfg(feature = "xmp_write")] + if let Some(provenance) = self.provenance_path() { + embedded_xmp::add_manifest_uri_to_file(dest_path, &provenance)?; + } else { + return Err(Error::XmpWriteError); + } + dest_path.to_path_buf() + } + crate::claim::RemoteManifest::SideCar => { + dest_path.with_extension(MANIFEST_STORE_EXT) + } + crate::claim::RemoteManifest::Remote(url) => { + let d = dest_path.with_extension(MANIFEST_STORE_EXT); + embedded_xmp::add_manifest_uri_to_file(dest_path, &url)?; + d + } + } } else { - return Err(Error::XmpWriteError); - } + // only side car and embedded supported without feature "xmp_write" + match pc.remote_manifest() { + crate::claim::RemoteManifest::NoRemote => dest_path.to_path_buf(), + crate::claim::RemoteManifest::SideCar => { + dest_path.with_extension(MANIFEST_STORE_EXT) + } + crate::claim::RemoteManifest::Remote(_) => { + return Err(Error::BadParam("requires 'xmp_writte' feature".to_string())) + } + } + }; + + // get the provenance claim changing mutability + let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; let is_bmff = is_bmff_format(&ext); let mut data; let jumbf_size; - // get the provenance claim - let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - if is_bmff { // 2) Get hash ranges if needed, do not generate for update manifests if !pc.update_manifest() { - let bmff_hashes = Store::generate_bmff_data_hashes(output_path, pc.alg(), false)?; + let bmff_hashes = Store::generate_bmff_data_hashes(&output_path, pc.alg(), false)?; for hash in bmff_hashes { pc.add_assertion(&hash)?; } @@ -1511,7 +1633,7 @@ impl Store { // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned data = self.to_jumbf_internal(reserve_size)?; jumbf_size = data.len(); - save_jumbf_to_file(&data, output_path, Some(output_path))?; + save_jumbf_to_file(&data, &output_path, Some(&output_path))?; // generate actual hash values let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability @@ -1521,17 +1643,17 @@ impl Store { if !bmff_hashes.is_empty() { let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0])?; - bmff_hash.gen_hash(output_path)?; + bmff_hash.gen_hash(&output_path)?; pc.update_bmff_hash(bmff_hash)?; } } } else { // 2) Get hash ranges if needed, do not generate for update manifests - let mut hash_ranges = object_locations(output_path)?; + let mut hash_ranges = object_locations(&output_path)?; let hashes: Vec = if pc.update_manifest() { Vec::new() } else { - Store::generate_data_hashes(output_path, pc.alg(), &mut hash_ranges, false)? + Store::generate_data_hashes(dest_path, pc.alg(), &mut hash_ranges, false)? }; // add the placeholder data hashes to provenance claim so that the required space is reserved @@ -1548,23 +1670,22 @@ impl Store { // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned data = self.to_jumbf_internal(reserve_size)?; jumbf_size = data.len(); - save_jumbf_to_file(&data, output_path, Some(output_path))?; + save_jumbf_to_file(&data, &output_path, Some(&output_path))?; // 4) determine final object locations and patch the asset hashes with correct offset // replace the source with correct asset hashes so that the claim hash will be correct let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // get the final hash ranges, but not for update manifests - let mut new_hash_ranges = object_locations(output_path)?; + let mut new_hash_ranges = object_locations(&output_path)?; let updated_hashes = if pc.update_manifest() { Vec::new() } else { - Store::generate_data_hashes(output_path, pc.alg(), &mut new_hash_ranges, true)? + Store::generate_data_hashes(dest_path, pc.alg(), &mut new_hash_ranges, true)? }; // patch existing claim hash with updated data - for mut hash in updated_hashes { - hash.gen_hash(output_path)?; // generate + for hash in updated_hashes { pc.update_data_hash(hash)?; } } @@ -1585,7 +1706,7 @@ impl Store { output_path: &Path, sig: Vec, sig_placeholder: &[u8], - ) -> Result> { + ) -> Result<(Vec, Vec)> { if sig_placeholder.len() != sig.len() { return Err(Error::CoseSigboxTooSmall); } @@ -1596,7 +1717,7 @@ impl Store { // re-save to file save_jumbf_to_file(&jumbf_bytes, output_path, Some(output_path))?; - Ok(sig) + Ok((sig, jumbf_bytes)) } /// Verify Store from an existing asset @@ -2712,4 +2833,155 @@ pub mod tests { println!("store = {}", store); } + + #[test] + fn test_external_manifest_sidecar() { + // test adding to actual image + let ap = fixture_path("libpng-test.png"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); + + let sidecar = op.with_extension(MANIFEST_STORE_EXT); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let mut claim = create_test_claim().unwrap(); + + // set claim for side car generation + claim.set_external_manifest(); + + // Do we generate JUMBF? + let signer = temp_signer(); + + store.commit_claim(claim).unwrap(); + + let saved_manifest = store.save_to_asset(&ap, &signer, &op).unwrap(); + + assert!(sidecar.exists()); + + // load external manifest + let loaded_manifest = std::fs::read(sidecar).unwrap(); + + // compare returned to external + assert_eq!(saved_manifest, loaded_manifest); + + // load the jumbf back into a store + let mut validation_log = OneShotStatusTracker::default(); + let restored_store = Store::from_jumbf(&loaded_manifest, &mut validation_log).unwrap(); + let pc = restored_store.provenance_claim().unwrap(); + + // make sure this manifest goes with this asset + for dh_assertion in pc.data_hash_assertions() { + if dh_assertion.label_root() == DataHash::LABEL { + let dh = DataHash::from_assertion(dh_assertion).unwrap(); + + dh.verify_hash(&op.clone(), Some(pc.alg().to_string())) + .unwrap(); + } + } + } + + #[test] + fn test_external_manifest_embedded() { + // test adding to actual image + let ap = fixture_path("libpng-test.png"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); + + let sidecar = op.with_extension(MANIFEST_STORE_EXT); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let mut claim = create_test_claim().unwrap(); + + // Do we generate JUMBF? + let signer = temp_signer(); + + // start with base url + let fp = format!("file:/{}", sidecar.to_str().unwrap()); + let url = url::Url::parse(&fp).unwrap(); + + let url_string: String = url.into(); + + // set claim for side car with remote manifest embedding generation + claim.set_remote_manifest(url_string.clone()).unwrap(); + + store.commit_claim(claim).unwrap(); + + let saved_manifest = store.save_to_asset(&ap, &signer, &op).unwrap(); + + assert!(sidecar.exists()); + + // load external manifest + let loaded_manifest = std::fs::read(sidecar).unwrap(); + + // compare returned to external + assert_eq!(saved_manifest, loaded_manifest); + + // load the jumbf back into a store + let mut validation_log = OneShotStatusTracker::default(); + let restored_store = Store::from_jumbf(&loaded_manifest, &mut validation_log).unwrap(); + let pc = restored_store.provenance_claim().unwrap(); + + let mut asset_reader = std::fs::File::open(op.clone()).unwrap(); + let ext_ref = + crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, "png") + .provenance + .unwrap(); + + assert_eq!(ext_ref, url_string); + + // make sure this manifest goes with this asset + for dh_assertion in pc.data_hash_assertions() { + if dh_assertion.label_root() == DataHash::LABEL { + let dh = DataHash::from_assertion(dh_assertion).unwrap(); + + dh.verify_hash(&op.clone(), Some(pc.alg().to_string())) + .unwrap(); + } + } + } + + #[test] + fn test_user_guid_external_manifest_embedded() { + // test adding to actual image + let ap = fixture_path("libpng-test.png"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); + + let sidecar = op.with_extension(MANIFEST_STORE_EXT); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let mut claim = Claim::new_with_user_guid("store unit test", "my guid"); + + // Do we generate JUMBF? + let signer = temp_signer(); + + // start with base url + let fp = format!("file:/{}", sidecar.to_str().unwrap()); + let url = url::Url::parse(&fp).unwrap(); + + let url_string: String = url.into(); + // set claim for side car with remote manifest embedding generation + claim.set_remote_manifest(url_string).unwrap(); + + store.commit_claim(claim).unwrap(); + + let saved_manifest = store.save_to_asset(&ap, &signer, &op).unwrap(); + + assert!(sidecar.exists()); + + // load external manifest + let loaded_manifest = std::fs::read(sidecar).unwrap(); + + // compare returned to external + assert_eq!(saved_manifest, loaded_manifest); + } } diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs index ccded86f6..c5211466d 100644 --- a/sdk/src/utils/hash_utils.rs +++ b/sdk/src/utils/hash_utils.rs @@ -29,7 +29,7 @@ use sha2::{Digest, Sha256, Sha384, Sha512}; use crate::{Error, Result}; -const MAX_HASH_BUF: usize = 1024 * 1024 * 1024; // cap memory usage to 1GB +const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct Exclusion {