c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

commit ec8095b6d7eec2db6c1be80649fd2fa15536843f
parent e28dc875947251c956ca7dcfeaf5d571806a1e82
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Wed,  1 May 2024 14:56:15 -0700

Gpeacock/embed_remote_settings (#460)

* Change settings Manifest to Builder.
* Ensure sign to stream respects options.
* add missing writer for c2pa_io.
* Add streaming bmff and streaming remote_url support.
* PR feedback fixes.
Diffstat:
A2024_API_NOTES.md | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DV2_API_NOTES.md | 70----------------------------------------------------------------------
Msdk/src/assertions/bmff_hash.rs | 36++++++++++++++++--------------------
Msdk/src/asset_handlers/c2pa_io.rs | 4++++
Msdk/src/builder.rs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msdk/src/claim.rs | 2--
Msdk/src/jumbf_io.rs | 45++++++++++++++++++++++++++++++++++++++++++++-
Msdk/src/settings.rs | 42+++++++++++++++++++++---------------------
Msdk/src/store.rs | 335++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msdk/tests/test_builder.rs | 7+------
10 files changed, 451 insertions(+), 283 deletions(-)

diff --git a/2024_API_NOTES.md b/2024_API_NOTES.md @@ -0,0 +1,83 @@ +## 2024 API Notes + +### Goals +Provide a consistent flexible well tested API focusing on core functionality. + +- Move toward a JSON + binary resources model that ports well to multiple languages. +- Eliminate multiple variations of functions for file/memory/stream, sync/async & etc. +- Have one stream based version of each function that works sync and async. +- Design APIs keeping in mind support for multiple language bindings. +- Enable sign only/verify only and no openssl configuration. +- Support Box Hash and Data Hashed signing models. +- Enable builds for cameras and other embedded environments. +- Provide a consistent model for setting runtime options. +- Write unit tests, integration tests and documentation for all the v2 APIs. +- Keep porting as simple as possible. + + +### Resource References +A resource reference in the API is associated with a HashedUri as a superset. +- The c2pa spec refers to both a hashed-uri-map and a hashed-ext-uri-map +- In some cases either one can be used. +- The resource reference is a superset of both. +- It also adds local references to things like the filesystem or any abstracted storage. +I've been using the identifier field to distinguish from the url field, but they are really the same. However, the spec will only allow for JUMBF and http/https references, so if the external identifier is not http/https, it must be converted to +a JUMBF reference before embedding into a manifest. + +When defining a resource for the ManifestStoreBuilder, existing resources in other manifests may be identified via JUMBF urls. This allows a new manifest to inherit an existing thumbnail and is also used to reference parent ingredients. The API will generally do this resolution as needed so users do not need to know about JUMBF URL referencing on manifest creation. + +The spec will often require adding a hashed-uri to an assertion. Since the JUMBF uris for a new manifest are not known when defining the manifest, this creates a chicken and egg scenario. We resolve this with local resource references. When constructing the JUMBF for a manifest, the api will convert all local uri references into JUMBF references and fixup the associated cross references. + +URI schemes in a resource reference could take the following forms: +- self#jumbf= an internal JUMBF reference +- file:/// a local file reference +- app://contentauth/ a working store reference +- http:// remote uri +- https:// remote secure uri + +Note that the file: and app: schemes are only used in the context of ManifestStoreBuilder and will never be in JUMBF data. This is proposal, currently there is no implementation for file or app schemes and we do not yet handle http/https schemes this way. + +Lack of a scheme will be interpreted as a file:/// reference when file_io is enabled, otherwise as an app: reference. + +### Source asset vs Parent asset +- The source asset isn't always the parent asset. +The source asset is the asset that we will hash and sign. It can the output from an editing application that has not preserved the manifest store from the parent. In that case the application should have extracted a parent ingredient from the parent asset and added that to the manifest definition. + +- Parent asset: with a manifest store. +- Parent ingredient: generated from that parent asset (hashed and validated) +- Source asset: may be a generated rendition after edits from the parent asset (manifest?) +- Signed output which will include the source asset, and the new manifest store with parent ingredient. + +If there is no parent ingredient defined, and the source has a manifest store, the sdk will generate a parent ingredient from the parent. + +### Remote URLs and embedding +The default operation of c2pa signing is to embed a c2pa manifest store into an asset. +We also return the c2pa manifest store so that it can be written to a sidecar or uploaded to a remote service. +- The API supports embedding a remote url reference into the asset. +- The remote URL is stored in different ways depending on the asset, but is often stored in XMP data. +- The remote URL must be added to the asset before signing so that it can be hashed along with the asset. +- Not all file formats support embedding remote URLs or embedding manifests stores. +- If you embed a manifest or a remote URL, a new asset will be created with the new data embedded. +- If you don't embed, then the original asset is unmodified and there is no need to write one out. +- The remote url can be set with builder.remote_url. +- If embedding is not needed, set the builder.no_embed flag to true. + + +## Testing +We need a more comprehensive set of tests for the rust codebase. + +The plan is to build a solid set of tests on the new streams based API. +Then we will build everything else on top of that as stable base. +The current set of unit tests are helpful but many are out of date. +I've had a long standing issue to generate the test images from clean non-c2pa images. +When we check in images with manifests, they rapidly get out of date. +We do need some set of older manifests and third party images to test with +but I'm not sure if those need to be in the SDK. + +- A test assets folder with one public domain image in each asset format we support. +- A tool, like make_test_images, to generate different kinds of manifests for testing. +We should maintain an archive of the manifest_store json generated by the previous build +and compare the old build with the new ones for any significant deltas. +The tool needs to ignore changes due to new GUIDs, dates, and json object field order. + +The make_test_images crate has been updated to do this by default. We may make a policy to run the test comparison nightly. diff --git a/V2_API_NOTES.md b/V2_API_NOTES.md @@ -1,70 +0,0 @@ -## V2 API Notes - -### Goals -Provide a consistent flexible well tested API focusing on core functionality. - -- Move toward a JSON + binary resources model that ports well to multiple languages. -- Eliminate multiple variations of functions for file/memory/stream, sync/async & etc. -- Have one stream based version of each function that works sync and async. -- Design APIs keeping in mind support for multiple language bindings. -- Enable sign only/verify only and no openssl configuration. -- Support Box Hash and Data Hashed signing models. -- Enable builds for cameras and other embedded environments. -- Provide a consistent model for setting runtime options. -- Write unit tests, integration tests and documentation for all the v2 APIs. -- Keep v1 to v2 porting as simple as possible. - - -### Resource References -A resource reference in the API is associated with a HashedUri as a superset. -- The c2pa spec refers to both a hashed-uri-map and a hashed-ext-uri-map -- In some cases either one can be used. -- The resource reference is a superset of both. -- It also adds local references to things like the filesystem or any abstracted storage. -I've been using the identifier field to distinguish from the url field, but they are really the same. However, the spec will only allow for JUMBF and http/https references, so if the external identifier is not http/https, it must be converted to -a JUMBF reference before embedding into a manifest. - -When defining a resource for the ManifestStoreBuilder, existing resources in other manifests may be identified via JUMBF urls. This allows a new manifest to inherit an existing thumbnail and is also used to reference parent ingredients. The API will generally do this resolution as needed so users do not need to know about JUMBF URL referencing on manifest creation. - -The spec will often require adding a hashed-uri to an assertion. Since the JUMBF uris for a new manifest are not known when defining the manifest, this creates a chicken and egg scenario. We resolve this with local resource references. When constructing the JUMBF for a manifest, the api will convert all local uri references into JUMBF references and fixup the associated cross references. - -URI schemes in a resource reference could take the following forms: -- self#jumbf= an internal JUMBF reference -- file:/// a local file reference -- app://contentauth/ a working store reference -- http:// remote uri -- https:// remote secure uri - -Note that the file: and app: schemes are only used in the context of ManifestStoreBuilder and will never be in JUMBF data. This is proposal, currently there is no implementation for file or app schemes and we do not yet handle http/https schemes this way. - -Lack of a scheme will be interpreted as a file:/// reference when file_io is enabled, otherwise as an app: reference. - -### Source asset vs Parent asset -- The source asset isn't always the parent asset. -The source asset is the asset that we will hash and sign. It can the output from an editing application that has not preserved the manifest store from the parent. In that case the application should have extracted a parent ingredient from the parent asset and added that to the manifest definition. - -- Parent asset: with a manifest store. -- Parent ingredient: generated from that parent asset (hashed and validated) -- Source asset: may be a generated rendition after edits from the parent asset (manifest?) -- Signed output which will include the source asset, and the new manifest store with parent ingredient. - -If there is no parent ingredient defined, and the source has a manifest store, the sdk will generate a parent ingredient from the parent. - -## Testing -We need a more comprehensive set of tests for the rust codebase. - -The plan is to build a solid set of tests on the new streams based API. -Then we will build everything else on top of that as stable base. -The current set of unit tests are helpful but many are out of date. -I've had a long standing issue to generate the test images from clean non-c2pa images. -When we check in images with manifests, they rapidly get out of date. -We do need some set of older manifests and third party images to test with -but I'm not sure if those need to be in the SDK. - -- A test assets folder with one public domain image in each asset format we support. -- A tool, like make_test_images, to generate different kinds of manifests for testing. -We should maintain an archive of the manifest_store json generated by the previous build -and compare the old build with the new ones for any significant deltas. -The tool needs to ignore changes due to new GUIDs, dates, and json object field order. - -The make_test_images crate has been updated to do this by default. We may make a policy to run the test comparison nightly. diff --git a/sdk/src/assertions/bmff_hash.rs b/sdk/src/assertions/bmff_hash.rs @@ -17,7 +17,7 @@ use std::{ fmt, fs, io::{BufReader, Cursor, SeekFrom}, ops::Deref, - path::{Path, PathBuf}, + path::Path, }; use mp4::*; @@ -37,8 +37,8 @@ use crate::{ cbor_types::UriT, utils::{ hash_utils::{ - concat_and_hash, hash_asset_by_alg, hash_stream_by_alg, vec_compare, - verify_stream_by_alg, HashRange, Hasher, + concat_and_hash, hash_stream_by_alg, vec_compare, verify_stream_by_alg, HashRange, + Hasher, }, merkle::C2PAMerkleTree, }, @@ -254,9 +254,6 @@ pub struct BmffHash { url: Option<UriT>, // deprecated in V2 and not to be used #[serde(skip)] - pub path: PathBuf, - - #[serde(skip)] bmff_version: usize, } @@ -271,7 +268,6 @@ impl BmffHash { merkle: None, name: Some(name.to_string()), url, - path: PathBuf::new(), bmff_version: ASSERTION_CREATION_VERSION, } } @@ -326,22 +322,24 @@ impl BmffHash { } /// Generate the hash value for the asset using the range from the BmffHash. - pub fn gen_hash(&mut self, asset_path: &Path) -> crate::error::Result<()> { - self.hash = Some(ByteBuf::from(self.hash_from_asset(asset_path)?)); - self.path = PathBuf::from(asset_path); + pub fn gen_hash_from_stream(&mut self, stream: &mut dyn CAIRead) -> crate::error::Result<()> { + self.hash = Some(ByteBuf::from(self.hash_from_stream(stream)?)); + //self.path = PathBuf::from(asset_path); Ok(()) } - /// Generate the hash again. - pub fn regen_hash(&mut self) -> crate::error::Result<()> { - let p = self.path.clone(); - self.hash = Some(ByteBuf::from(self.hash_from_asset(p.as_path())?)); + /// Generate the hash value for the asset using the range from the BmffHash. + #[cfg(feature = "file_io")] + pub fn gen_hash(&mut self, asset_path: &Path) -> crate::error::Result<()> { + let mut file = std::fs::File::open(asset_path)?; + self.hash = Some(ByteBuf::from(self.hash_from_stream(&mut file)?)); + //self.path = PathBuf::from(asset_path); Ok(()) } - /// Generate the asset hash from a file asset using the constructed + /// Generate the asset hash from an asset stream using the constructed /// start and length values. - fn hash_from_asset(&mut self, asset_path: &Path) -> crate::error::Result<Vec<u8>> { + fn hash_from_stream(&mut self, stream: &mut dyn CAIRead) -> crate::error::Result<Vec<u8>> { if self.is_remote_hash() { return Err(Error::BadParam( "asset hash is remote, not yet supported".to_owned(), @@ -356,11 +354,9 @@ impl BmffHash { let bmff_exclusions = &self.exclusions; // convert BMFF exclusion map to flat exclusion list - let mut data = fs::File::open(asset_path)?; - let exclusions = - bmff_to_jumbf_exclusions(&mut data, bmff_exclusions, self.bmff_version > 1)?; + let exclusions = bmff_to_jumbf_exclusions(stream, bmff_exclusions, self.bmff_version > 1)?; - let hash = hash_asset_by_alg(&alg, asset_path, Some(exclusions))?; + let hash = hash_stream_by_alg(&alg, stream, Some(exclusions), true)?; if hash.is_empty() { Err(Error::BadParam("could not generate data hash".to_string())) diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs @@ -120,6 +120,10 @@ impl AssetIO for C2paIO { self } + fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> { + Some(Box::new(C2paIO::new(asset_type))) + } + fn supported_types(&self) -> &[&str] { &SUPPORTED_TYPES } diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs @@ -193,6 +193,12 @@ pub struct Builder { #[serde(flatten)] pub definition: ManifestDefinition, + /// Optional remote URL for the manifest + pub remote_url: Option<String>, + + // If true, the manifest store will not be embedded in the asset on sign + pub no_embed: bool, + /// container for binary assets (like thumbnails) #[serde(skip)] resources: ResourceStore, @@ -481,14 +487,15 @@ impl Builder { claim.add_claim_generator_info(claim_info); } - // if let Some(remote_op) = &self.remote_manifest { - // match remote_op { - // RemoteManifest::NoRemote => (), - // RemoteManifest::SideCar => claim.set_external_manifest(), - // RemoteManifest::Remote(r) => claim.set_remote_manifest(r)?, - // RemoteManifest::EmbedWithRemote(r) => claim.set_embed_remote_manifest(r)?, - // }; - // } + if let Some(remote_url) = &self.remote_url { + if self.no_embed { + claim.set_remote_manifest(remote_url)?; + } else { + claim.set_embed_remote_manifest(remote_url)?; + } + } else if self.no_embed { + claim.set_external_manifest() + } if let Some(title) = definition.title.as_ref() { claim.set_title(Some(title.to_owned())); @@ -657,7 +664,9 @@ impl Builder { where R: Read + Seek + ?Sized, { - if self.definition.thumbnail.is_none() { + // check settings to see if we should auto generate a thumbnail + let auto_thumbnail = crate::settings::get_settings_value::<bool>("builder.auto_thumbnail")?; + if self.definition.thumbnail.is_none() && auto_thumbnail { stream.rewind()?; if let Ok((format, image)) = crate::utils::thumbnail::make_thumbnail_from_stream(format, stream) @@ -778,7 +787,7 @@ mod tests { use wasm_bindgen_test::*; use super::*; - use crate::{manifest_store::ManifestStore, utils::test::temp_signer}; + use crate::{utils::test::temp_signer, Reader}; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -826,6 +835,8 @@ mod tests { .to_string() } + #[cfg(not(target_arch = "wasm32"))] + const TEST_IMAGE_CLEAN: &[u8] = include_bytes!("../tests/fixtures/IMG_0003.jpg"); const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); #[test] @@ -951,7 +962,7 @@ mod tests { zipped.rewind().unwrap(); let mut _builder = Builder::from_archive(&mut zipped).unwrap(); - // sign the ManifestStoreBuilder and write it to the output stream + // sign and write to the output stream let signer = temp_signer(); builder .sign(format, &mut source, &mut dest, signer.as_ref()) @@ -959,13 +970,12 @@ mod tests { // read and validate the signed manifest store dest.rewind().unwrap(); - let manifest_store = - ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes"); + let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); println!("{}", manifest_store); assert!(manifest_store.validation_status().is_none()); - assert!(manifest_store.get_active().is_some()); - let manifest = manifest_store.get_active().unwrap(); + assert!(manifest_store.active_manifest().is_some()); + let manifest = manifest_store.active_manifest().unwrap(); assert_eq!(manifest.title().unwrap(), "Test_Manifest"); let test_assertion: TestAssertion = manifest.find_assertion("org.life.meaning").unwrap(); assert_eq!(test_assertion.answer, 42); @@ -984,17 +994,17 @@ mod tests { .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE)) .unwrap(); - // sign the ManifestStoreBuilder and write it to the output stream + // sign and write to the output stream let signer = temp_signer(); builder.sign_file(source, &dest, signer.as_ref()).unwrap(); // read and validate the signed manifest store - let manifest_store = ManifestStore::from_file(&dest).expect("from_bytes"); + let manifest_store = Reader::from_file(&dest).expect("from_bytes"); println!("{}", manifest_store); assert!(manifest_store.validation_status().is_none()); assert_eq!( - manifest_store.get_active().unwrap().title().unwrap(), + manifest_store.active_manifest().unwrap().title().unwrap(), "Test_Manifest" ); } @@ -1008,15 +1018,14 @@ mod tests { "sample1.webp", "TUSCANY.TIF", "sample1.svg", - //"APC_0808.dng", "sample1.wav", "test.avi", - //"sample1.mp3", - //"sample1.avif", - //"sample1.heic", - //"sample1.heif", - //"video1.mp4", - //"cloud_manifest.c2pa", + "sample1.mp3", + "sample1.avif", + "sample1.heic", + "sample1.heif", + "video1.mp4", + "cloud_manifest.c2pa", ]; for file_name in TESTFILES { let extension = file_name.split('.').last().unwrap(); @@ -1036,7 +1045,7 @@ mod tests { .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE)) .unwrap(); - // sign the ManifestStoreBuilder and write it to the output stream + // sign and write to the output stream let signer = temp_signer(); builder .sign(format, &mut source, &mut dest, signer.as_ref()) @@ -1044,13 +1053,15 @@ mod tests { // read and validate the signed manifest store dest.rewind().unwrap(); - let manifest_store = - ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes"); + let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); println!("{}", manifest_store); - assert!(manifest_store.validation_status().is_none()); + if format != "c2pa" { + // c2pa files will not validate since they have no associated asset + assert!(manifest_store.validation_status().is_none()); + } assert_eq!( - manifest_store.get_active().unwrap().title().unwrap(), + manifest_store.active_manifest().unwrap().title().unwrap(), "Test_Manifest" ); @@ -1091,15 +1102,48 @@ mod tests { // read and validate the signed manifest store dest.rewind().unwrap(); - let manifest_store = - ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes"); + let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); println!("{}", manifest_store); #[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working assert!(manifest_store.validation_status().is_none()); assert_eq!( - manifest_store.get_active().unwrap().title().unwrap(), + manifest_store.active_manifest().unwrap().title().unwrap(), "Test_Manifest" ); } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn test_builder_remote_url() { + let mut source = Cursor::new(TEST_IMAGE_CLEAN); + let mut dest = Cursor::new(Vec::new()); + + let mut builder = Builder::from_json(&manifest_json()).unwrap(); + builder.remote_url = Some("http://my_remote_url".to_string()); + builder.no_embed = true; + + builder + .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE)) + .unwrap(); + + // sign the ManifestStoreBuilder and write it to the output stream + let signer = temp_signer(); + let manifest_data = builder + .sign("image/jpeg", &mut source, &mut dest, signer.as_ref()) + .unwrap(); + + // check to make sure we have a remote url and no manifest data + dest.set_position(0); + let _err = c2pa::Reader::from_stream("image/jpeg", &mut dest).expect_err("from_bytes"); + + // now validate the manifest against the written asset + dest.set_position(0); + let reader = + c2pa::Reader::from_manifest_data_and_stream(&manifest_data, "image/jpeg", &mut dest) + .expect("from_bytes"); + + println!("{}", reader.json()); + assert!(reader.validation_status().is_none()); + } } diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs @@ -938,7 +938,6 @@ impl Claim { } // Crate private function to allow for patching a BMFF hash with final contents. - #[cfg(feature = "file_io")] pub(crate) fn update_bmff_hash(&mut self, bmff_hash: BmffHash) -> Result<()> { self.replace_assertion(bmff_hash.to_assertion()?) } @@ -946,7 +945,6 @@ impl Claim { // Patch an existing assertion with new contents. // // `replace_with` should match in name and size of an existing assertion. - #[cfg(feature = "file_io")] pub(crate) fn replace_assertion(&mut self, replace_with: Assertion) -> Result<()> { self.update_assertion( replace_with, diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs @@ -90,7 +90,6 @@ lazy_static! { }; } -#[cfg(feature = "file_io")] pub(crate) fn is_bmff_format(asset_type: &str) -> bool { let bmff_io = BmffIO::new(""); bmff_io.supported_types().contains(&asset_type) @@ -319,6 +318,8 @@ pub mod tests { #![allow(clippy::unwrap_used)] use super::*; + use crate::utils::test::{create_test_store, temp_signer}; + #[test] fn test_get_assetio() { let handlers: Vec<Box<dyn AssetIO>> = vec![ @@ -406,4 +407,46 @@ pub mod tests { assert!(supported.iter().any(|s| s == "svg")); assert!(supported.iter().any(|s| s == "mp3")); } + + #[test] + fn test_streams() { + let files: Vec<(&str, &str)> = vec![ + ("IMG_0003.jpg", "jpeg"), + ("sample1.png", "png"), + //("sample1.webp", "webp"), // riff io deletion of manifest store isn't working. + ("TUSCANY.TIF", "tiff"), + ("sample1.svg", "svg"), + //("sample1.wav", "wav"), + //("test.avi", "avi"), + ("sample1.mp3", "mp3"), + ("sample1.avif", "avif"), + ("sample1.heic", "heic"), + ("sample1.heif", "heif"), + ("video1.mp4", "mp4"), + //("cloud_manifest.c2pa", "c2pa") + ]; + for (name, asset_type) in files { + println!("Testing {}", name); + let mut reader = std::fs::File::open(format!("tests/fixtures/{}", name)).unwrap(); + let mut writer = Cursor::new(Vec::new()); + let store = create_test_store().unwrap(); + let signer = temp_signer(); + let jumbf = store.to_jumbf(&*signer).unwrap(); + save_jumbf_to_stream(asset_type, &mut reader, &mut writer, &jumbf).unwrap(); + writer.set_position(0); + let jumbf2 = load_jumbf_from_stream(asset_type, &mut writer).unwrap(); + assert_eq!(jumbf, jumbf2); + + // test removing cai store + writer.set_position(0); + let handler = get_caiwriter_handler(asset_type).unwrap(); + let mut removed = Cursor::new(Vec::new()); + handler + .remove_cai_store_from_stream(&mut writer, &mut removed) + .unwrap(); + removed.set_position(0); + let result = load_jumbf_from_stream(asset_type, &mut removed); + assert!(matches!(result.err().unwrap(), Error::JumbfNotFound)); + } + } } diff --git a/sdk/src/settings.rs b/sdk/src/settings.rs @@ -175,14 +175,14 @@ impl Default for Verify { impl SettingsValidate for Verify {} -// Settings for manifest API options +// Settings for Builder API options #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[allow(unused)] -pub(crate) struct Manifest { +pub(crate) struct Builder { auto_thumbnail: bool, } -impl Default for Manifest { +impl Default for Builder { fn default() -> Self { Self { auto_thumbnail: true, @@ -190,7 +190,7 @@ impl Default for Manifest { } } -impl SettingsValidate for Manifest {} +impl SettingsValidate for Builder {} // Settings configuration for C2PA-RS. Default configuration values // are lazy loaded on first use. Values can also be loaded from a configuration @@ -202,7 +202,7 @@ pub(crate) struct Settings { trust: Trust, core: Core, verify: Verify, - manifest: Manifest, + builder: Builder, } impl Settings { @@ -270,7 +270,7 @@ impl SettingsValidate for Settings { self.trust.validate()?; self.core.validate()?; self.trust.validate()?; - self.manifest.validate() + self.builder.validate() } } @@ -415,7 +415,7 @@ pub mod tests { assert_eq!(settings.core, Core::default()); assert_eq!(settings.trust, Trust::default()); assert_eq!(settings.verify, Verify::default()); - assert_eq!(settings.manifest, Manifest::default()); + assert_eq!(settings.builder, Builder::default()); reset_default_settings().unwrap(); } @@ -430,8 +430,8 @@ pub mod tests { Core::default().hash_alg ); assert_eq!( - get_settings_value::<bool>("manifest.auto_thumbnail").unwrap(), - Manifest::default().auto_thumbnail + get_settings_value::<bool>("builder.auto_thumbnail").unwrap(), + Builder::default().auto_thumbnail ); assert_eq!( get_settings_value::<Option<String>>("trust.private_anchors").unwrap(), @@ -445,8 +445,8 @@ pub mod tests { Verify::default() ); assert_eq!( - get_settings_value::<Manifest>("manifest").unwrap(), - Manifest::default() + get_settings_value::<Builder>("builder").unwrap(), + Builder::default() ); assert_eq!( get_settings_value::<Trust>("trust").unwrap(), @@ -457,7 +457,7 @@ pub mod tests { let hash_alg: String = get_settings_value("core.hash_alg").unwrap(); let remote_manifest_fetch: bool = get_settings_value("verify.remote_manifest_fetch").unwrap(); - let auto_thumbnail: bool = get_settings_value("manifest.auto_thumbnail").unwrap(); + let auto_thumbnail: bool = get_settings_value("builder.auto_thumbnail").unwrap(); let private_anchors: Option<String> = get_settings_value("trust.private_anchors").unwrap(); assert_eq!(hash_alg, Core::default().hash_alg); @@ -465,18 +465,18 @@ pub mod tests { remote_manifest_fetch, Verify::default().remote_manifest_fetch ); - assert_eq!(auto_thumbnail, Manifest::default().auto_thumbnail); + assert_eq!(auto_thumbnail, Builder::default().auto_thumbnail); assert_eq!(private_anchors, Trust::default().private_anchors); // test implicit deserialization on objects let core: Core = get_settings_value("core").unwrap(); let verify: Verify = get_settings_value("verify").unwrap(); - let manifest: Manifest = get_settings_value("manifest").unwrap(); + let builder: Builder = get_settings_value("builder").unwrap(); let trust: Trust = get_settings_value("trust").unwrap(); assert_eq!(core, Core::default()); assert_eq!(verify, Verify::default()); - assert_eq!(manifest, Manifest::default()); + assert_eq!(builder, Builder::default()); assert_eq!(trust, Trust::default()); reset_default_settings().unwrap(); @@ -491,7 +491,7 @@ pub mod tests { // test updating values set_settings_value("core.hash_alg", "sha512").unwrap(); set_settings_value("verify.remote_manifest_fetch", false).unwrap(); - set_settings_value("manifest.auto_thumbnail", false).unwrap(); + set_settings_value("builder.auto_thumbnail", false).unwrap(); set_settings_value( "trust.private_anchors", Some(String::from_utf8(ts.to_vec()).unwrap()), @@ -503,7 +503,7 @@ pub mod tests { "sha512" ); assert!(!get_settings_value::<bool>("verify.remote_manifest_fetch").unwrap()); - assert!(!get_settings_value::<bool>("manifest.auto_thumbnail").unwrap()); + assert!(!get_settings_value::<bool>("builder.auto_thumbnail").unwrap()); assert_eq!( get_settings_value::<Option<String>>("trust.private_anchors").unwrap(), Some(String::from_utf8(ts.to_vec()).unwrap()) @@ -516,8 +516,8 @@ pub mod tests { Verify::default() ); assert_ne!( - get_settings_value::<Manifest>("manifest").unwrap(), - Manifest::default() + get_settings_value::<Builder>("builder").unwrap(), + Builder::default() ); assert_ne!( get_settings_value::<Trust>("trust").unwrap(), @@ -596,8 +596,8 @@ pub mod tests { // check a few defaults to make sure they are still there assert_eq!( - get_settings_value::<bool>("manifest.auto_thumbnail").unwrap(), - Manifest::default().auto_thumbnail + get_settings_value::<bool>("builder.auto_thumbnail").unwrap(), + Builder::default().auto_thumbnail ); assert_eq!( diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -22,18 +22,23 @@ use log::error; #[cfg(feature = "openssl")] use crate::cose_validator::{verify_cose, verify_cose_async}; +#[cfg(feature = "file_io")] +use crate::jumbf_io::{ + get_file_extension, get_supported_file_extension, load_jumbf_from_file, object_locations, + remove_jumbf_from_file, save_jumbf_to_file, +}; use crate::{ assertion::{ Assertion, AssertionBase, AssertionData, AssertionDecodeError, AssertionDecodeErrorCause, }, assertions::{ labels::{self, CLAIM}, - DataBox, DataHash, Ingredient, Relationship, + BmffHash, DataBox, DataHash, DataMap, ExclusionsMap, Ingredient, Relationship, SubsetMap, }, asset_io::{ CAIRead, CAIReadWrite, HashBlockObjectType, HashObjectPositions, RemoteRefEmbedType, }, - claim::{Claim, ClaimAssertion, ClaimAssetData}, + claim::{Claim, ClaimAssertion, ClaimAssetData, RemoteManifest}, cose_sign::{cose_sign, cose_sign_async}, cose_validator::check_ocsp_status, error::{Error, Result}, @@ -44,7 +49,7 @@ use crate::{ labels::{to_absolute_uri, ASSERTIONS, CREDENTIALS, DATABOXES, SIGNATURE}, }, jumbf_io::{ - get_assetio_handler, load_jumbf_from_stream, object_locations_from_stream, + get_assetio_handler, is_bmff_format, load_jumbf_from_stream, object_locations_from_stream, save_jumbf_to_memory, save_jumbf_to_stream, }, manifest_store_report::ManifestStoreReport, @@ -58,15 +63,6 @@ use crate::{ }, validation_status, AsyncSigner, RemoteSigner, Signer, }; -#[cfg(feature = "file_io")] -use crate::{ - assertions::{BmffHash, DataMap, ExclusionsMap, SubsetMap}, - claim::RemoteManifest, - jumbf_io::{ - get_file_extension, get_supported_file_extension, is_bmff_format, load_jumbf_from_file, - object_locations, remove_jumbf_from_file, save_jumbf_to_file, - }, -}; const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests @@ -1630,9 +1626,8 @@ impl Store { Ok(hashes) } - #[cfg(feature = "file_io")] fn generate_bmff_data_hashes( - asset_path: &Path, + asset_stream: &mut dyn CAIRead, alg: &str, calc_hashes: bool, ) -> Result<Vec<BmffHash>> { @@ -1726,7 +1721,7 @@ impl Store { */ if calc_hashes { - dh.gen_hash(asset_path)?; + dh.gen_hash_from_stream(asset_stream)?; } else { match alg { "sha256" => dh.set_hash([0u8; 32].to_vec()), @@ -1754,10 +1749,10 @@ impl Store { #[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(_) - | crate::claim::RemoteManifest::EmbedWithRemote(_) => { + RemoteManifest::NoRemote => Store::move_or_copy(source, dest)?, + RemoteManifest::SideCar + | RemoteManifest::Remote(_) + | RemoteManifest::EmbedWithRemote(_) => { // make correct path names let source_asset = source; let source_cai = source_asset.with_extension(MANIFEST_STORE_EXT); @@ -2054,6 +2049,7 @@ impl Store { signer.reserve_size(), )?; + intermediate_stream.set_position(0); let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = self.sign_claim(pc, signer, signer.reserve_size())?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); @@ -2108,6 +2104,7 @@ impl Store { }; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); + intermediate_stream.set_position(0); match self.finish_save_stream( jumbf_bytes, format, @@ -2195,9 +2192,10 @@ impl Store { // get correct output path for remote manifest let output_path = match pc.remote_manifest() { - crate::claim::RemoteManifest::NoRemote - | crate::claim::RemoteManifest::EmbedWithRemote(_) => temp_file.to_path_buf(), - crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => { + RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { + temp_file.to_path_buf() + } + RemoteManifest::SideCar | RemoteManifest::Remote(_) => { temp_file.with_extension(MANIFEST_STORE_EXT) } }; @@ -2209,9 +2207,7 @@ impl Store { pc_mut.set_signature_val(s); // do we need to make a C2PA file in addition to standard embedded output - if let crate::claim::RemoteManifest::EmbedWithRemote(_url) = - pc_mut.remote_manifest() - { + if let RemoteManifest::EmbedWithRemote(_url) = pc_mut.remote_manifest() { let c2pa = output_path.with_extension(MANIFEST_STORE_EXT); std::fs::write(c2pa, &m)?; } @@ -2252,9 +2248,10 @@ impl Store { // get correct output path for remote manifest let output_path = match pc.remote_manifest() { - crate::claim::RemoteManifest::NoRemote - | crate::claim::RemoteManifest::EmbedWithRemote(_) => temp_file.to_path_buf(), - crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => { + RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { + temp_file.to_path_buf() + } + RemoteManifest::SideCar | RemoteManifest::Remote(_) => { temp_file.with_extension(MANIFEST_STORE_EXT) } }; @@ -2266,9 +2263,7 @@ impl Store { pc_mut.set_signature_val(s); // do we need to make a C2PA file in addition to standard embedded output - if let crate::claim::RemoteManifest::EmbedWithRemote(_url) = - pc_mut.remote_manifest() - { + if let RemoteManifest::EmbedWithRemote(_url) = pc_mut.remote_manifest() { let c2pa = output_path.with_extension(MANIFEST_STORE_EXT); std::fs::write(c2pa, &m)?; } @@ -2308,9 +2303,10 @@ impl Store { // get correct output path for remote manifest let output_path = match pc.remote_manifest() { - crate::claim::RemoteManifest::NoRemote - | crate::claim::RemoteManifest::EmbedWithRemote(_) => temp_file.to_path_buf(), - crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => { + RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { + temp_file.to_path_buf() + } + RemoteManifest::SideCar | RemoteManifest::Remote(_) => { temp_file.with_extension(MANIFEST_STORE_EXT) } }; @@ -2322,9 +2318,7 @@ impl Store { pc_mut.set_signature_val(s); // do we need to make a C2PA file in addition to standard embedded output - if let crate::claim::RemoteManifest::EmbedWithRemote(_url) = - pc_mut.remote_manifest() - { + if let RemoteManifest::EmbedWithRemote(_url) = pc_mut.remote_manifest() { let c2pa = output_path.with_extension(MANIFEST_STORE_EXT); std::fs::write(c2pa, &m)?; } @@ -2345,117 +2339,188 @@ impl Store { output_stream: &mut dyn CAIReadWrite, reserve_size: usize, ) -> Result<Vec<u8>> { - let mut data; let intermediate_output: Vec<u8> = Vec::new(); let mut intermediate_stream = Cursor::new(intermediate_output); - // add remote reference XMP if needed let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - match pc.remote_manifest() { - crate::claim::RemoteManifest::Remote(url) => { - if let Some(h) = get_assetio_handler(format) { - if let Some(external_ref_writer) = h.remote_ref_writer_ref() { - // remove any previous c2pa manifest from the asset - let tmp_output: Vec<u8> = Vec::new(); - let mut tmp_stream = Cursor::new(tmp_output); - - if let Some(manifest_writer) = h.get_writer(format) { - manifest_writer - .remove_cai_store_from_stream(input_stream, &mut tmp_stream)?; - - // add external ref if possible - external_ref_writer.embed_reference_to_stream( - &mut tmp_stream, - &mut intermediate_stream, - RemoteRefEmbedType::Xmp(url), - )?; - } else { - return Err(Error::XmpNotSupported); - } - } else { - return Err(Error::XmpNotSupported); - } - } else { - return Err(Error::UnsupportedType); - } - } - _ => { - // just clone stream - input_stream.rewind()?; - std::io::copy(input_stream, &mut intermediate_stream)?; + + // Add remote reference XMP if needed and strip out existing manifest + // We don't need to strip manifests if we are replacing an exsiting one + let (url, remove_manifests) = match pc.remote_manifest() { + RemoteManifest::NoRemote => (None, false), + RemoteManifest::SideCar => (None, true), + RemoteManifest::Remote(url) => (Some(url), true), + RemoteManifest::EmbedWithRemote(url) => (Some(url), false), + }; + + let io_handler = get_assetio_handler(format).ok_or(Error::UnsupportedType)?; + + // Do not assume the handler supports XMP or removing manifests unless we need it to + if let Some(url) = url { + let external_ref_writer = io_handler + .remote_ref_writer_ref() + .ok_or(Error::XmpNotSupported)?; + + if remove_manifests { + let manifest_writer = io_handler + .get_writer(format) + .ok_or(Error::UnsupportedType)?; + + let tmp_output: Vec<u8> = Vec::new(); + let mut tmp_stream = Cursor::new(tmp_output); + manifest_writer.remove_cai_store_from_stream(input_stream, &mut tmp_stream)?; + + // add external ref if possible + tmp_stream.set_position(0); + external_ref_writer.embed_reference_to_stream( + &mut tmp_stream, + &mut intermediate_stream, + RemoteRefEmbedType::Xmp(url), + )?; + } else { + // add external ref if possible + external_ref_writer.embed_reference_to_stream( + input_stream, + &mut intermediate_stream, + RemoteRefEmbedType::Xmp(url), + )?; } + } else if remove_manifests { + let manifest_writer = io_handler + .get_writer(format) + .ok_or(Error::UnsupportedType)?; + + manifest_writer.remove_cai_store_from_stream(input_stream, &mut intermediate_stream)?; + } else { + // just clone stream + input_stream.rewind()?; + std::io::copy(input_stream, &mut intermediate_stream)?; } - // we will not do automatic hashing if we detect a box hash present - let mut needs_hashing = false; - if pc.hash_assertions().is_empty() { + let is_bmff = is_bmff_format(format); + + let mut data; + let jumbf_size; + + if is_bmff { // 2) Get hash ranges if needed, do not generate for update manifests - let mut hash_ranges = object_locations_from_stream(format, &mut intermediate_stream)?; - let hashes: Vec<DataHash> = if pc.update_manifest() { - Vec::new() + if !pc.update_manifest() { + intermediate_stream.rewind()?; + let bmff_hashes = + Store::generate_bmff_data_hashes(&mut intermediate_stream, pc.alg(), false)?; + for hash in bmff_hashes { + pc.add_assertion(&hash)?; + } + } + + // 3) Generate in memory CAI jumbf block + // and write preliminary jumbf store to file + // 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(); + // write the jumbf to the output stream if we are embedding the manifest + if !remove_manifests { + intermediate_stream.rewind()?; + save_jumbf_to_stream(format, &mut intermediate_stream, output_stream, &data)?; } else { - Store::generate_data_hashes_for_stream( - &mut intermediate_stream, - pc.alg(), - &mut hash_ranges, - false, - )? - }; + // just copy the asset to the output stream without an embedded manifest (may be stripping one out here) + intermediate_stream.rewind()?; + std::io::copy(&mut intermediate_stream, output_stream)?; + } - // add the placeholder data hashes to provenance claim so that the required space is reserved - for mut hash in hashes { - // add padding to account for possible cbor expansion of final DataHash - let padding: Vec<u8> = vec![0x0; 10]; - hash.add_padding(padding); + // generate actual hash values + let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability + + if !pc.update_manifest() { + let bmff_hashes = pc.bmff_hash_assertions(); - pc.add_assertion(&hash)?; + if !bmff_hashes.is_empty() { + let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0])?; + intermediate_stream.rewind()?; + output_stream.rewind()?; + std::io::copy(output_stream, &mut intermediate_stream)?; // remove this once we can get a CAIReader from CAIReadWrite safely + bmff_hash.gen_hash_from_stream(&mut intermediate_stream)?; + pc.update_bmff_hash(bmff_hash)?; + } } - needs_hashing = true; - } + } else { + // we will not do automatic hashing if we detect a box hash present + let mut needs_hashing = false; + if pc.hash_assertions().is_empty() { + // 2) Get hash ranges if needed, do not generate for update manifests + let mut hash_ranges = + object_locations_from_stream(format, &mut intermediate_stream)?; + let hashes: Vec<DataHash> = if pc.update_manifest() { + Vec::new() + } else { + Store::generate_data_hashes_for_stream( + &mut intermediate_stream, + pc.alg(), + &mut hash_ranges, + false, + )? + }; - // 3) Generate in memory CAI jumbf block - // and write preliminary jumbf store to file - // 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)?; - let jumbf_size = data.len(); - - intermediate_stream.rewind()?; - save_jumbf_to_stream(format, &mut intermediate_stream, output_stream, &data)?; - - // 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 - if needs_hashing { - let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - - // get the final hash ranges, but not for update manifests - intermediate_stream.rewind()?; - output_stream.rewind()?; - std::io::copy(output_stream, &mut intermediate_stream)?; // can remove this once we can get a CAIReader from CAIReadWrite safely - let mut new_hash_ranges = - object_locations_from_stream(format, &mut intermediate_stream)?; - let updated_hashes = if pc.update_manifest() { - Vec::new() - } else { - Store::generate_data_hashes_for_stream( - &mut intermediate_stream, - pc.alg(), - &mut new_hash_ranges, - true, - )? - }; + // add the placeholder data hashes to provenance claim so that the required space is reserved + for mut hash in hashes { + // add padding to account for possible cbor expansion of final DataHash + let padding: Vec<u8> = vec![0x0; 10]; + hash.add_padding(padding); - // patch existing claim hash with updated data - for hash in updated_hashes { - pc.update_data_hash(hash)?; + pc.add_assertion(&hash)?; + } + needs_hashing = true; } - // regenerate the jumbf because the cbor changed + // 3) Generate in memory CAI jumbf block data = self.to_jumbf_internal(reserve_size)?; - if jumbf_size != data.len() { - return Err(Error::JumbfCreationError); + jumbf_size = data.len(); + + // write the jumbf to the output stream if we are embedding the manifest + if !remove_manifests { + intermediate_stream.rewind()?; + save_jumbf_to_stream(format, &mut intermediate_stream, output_stream, &data)?; + } else { + // just copy the asset to the output stream without an embedded manifest (may be stripping one out here) + intermediate_stream.rewind()?; + std::io::copy(&mut intermediate_stream, output_stream)?; + } + + // 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 + if needs_hashing { + let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; + + // get the final hash ranges, but not for update manifests + intermediate_stream.rewind()?; + output_stream.rewind()?; + std::io::copy(output_stream, &mut intermediate_stream)?; // can remove this once we can get a CAIReader from CAIReadWrite safely + let mut new_hash_ranges = + object_locations_from_stream(format, &mut intermediate_stream)?; + let updated_hashes = if pc.update_manifest() { + Vec::new() + } else { + Store::generate_data_hashes_for_stream( + &mut intermediate_stream, + pc.alg(), + &mut new_hash_ranges, + true, + )? + }; + + // patch existing claim hash with updated data + for hash in updated_hashes { + pc.update_data_hash(hash)?; + } } } + // regenerate the jumbf because the cbor changed + data = self.to_jumbf_internal(reserve_size)?; + if jumbf_size != data.len() { + return Err(Error::JumbfCreationError); + } + Ok(data) // return JUMBF data } @@ -2476,7 +2541,16 @@ impl Store { .map_err(|_| Error::JumbfCreationError)?; // re-save to file - save_jumbf_to_stream(format, input_stream, output_stream, &jumbf_bytes)?; + let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; + match pc.remote_manifest() { + RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { + save_jumbf_to_stream(format, input_stream, output_stream, &jumbf_bytes)?; + } + RemoteManifest::SideCar | RemoteManifest::Remote(_) => { + // just copy the asset to the output stream without an embedded manifest (may be stripping one out here) + std::io::copy(input_stream, output_stream)?; + } + } Ok((sig, jumbf_bytes)) } @@ -2586,7 +2660,8 @@ impl Store { 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(dest_path, pc.alg(), false)?; + let mut file = std::fs::File::open(asset_path)?; + let bmff_hashes = Store::generate_bmff_data_hashes(&mut file, pc.alg(), false)?; for hash in bmff_hashes { pc.add_assertion(&hash)?; } diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs @@ -29,11 +29,6 @@ fn test_builder_ca_jpg() -> Result<()> { let mut dest = Cursor::new(Vec::new()); builder.sign(format, &mut source, &mut dest, &test_signer())?; - - // dest.set_position(0); - // let path = common::known_good_path("CA_test.json"); - // let reader = c2pa::Reader::from_stream(format, &mut dest)?; - // std::fs::write(path, reader.json())?; - + dest.set_position(0); compare_stream_to_known_good(&mut dest, format, "CA_test.json") }