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 1c12e1c8cb3293226b4b53e1c50faa6f22414300
parent 8dc217b93bbbee643a8d5aa6b4a0f28122c6ab6d
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Fri,  8 Sep 2023 11:43:56 -0400

(MINOR) Expose HashRange (#300)

* Make HashRange public

* Change embedding manifest functions to be synchronous.
Create example of using embedding functions.

* Fix formatting

* format fix

* Disable example for WASM

* Disable more WASM stuff

* Clippy fixes

* wasm fix
Diffstat:
Asdk/examples/data_hash.rs | 268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdk/src/lib.rs | 3++-
Msdk/src/manifest.rs | 45+++++++++++++++++----------------------------
Msdk/src/manifest_store.rs | 37+++++++++++++++++++++++++++++++++++++
Msdk/src/store.rs | 65++++++++++++++++++++---------------------------------------------
5 files changed, 344 insertions(+), 74 deletions(-)

diff --git a/sdk/examples/data_hash.rs b/sdk/examples/data_hash.rs @@ -0,0 +1,268 @@ +// Copyright 2023 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 code (in unit test) for how you might use client DataHash values. This allows clients +// to perform the manifest embedding and optionally the hashing + +#[cfg(not(target_arch = "wasm32"))] +use std::{ + io::{Read, Seek, Write}, + path::PathBuf, +}; + +#[cfg(not(target_arch = "wasm32"))] +use c2pa::{ + assertions::{c2pa_action, Action, Actions, CreativeWork, DataHash, Exif, SchemaDotOrgPerson}, + create_signer, hash_stream_by_alg, HashRange, Ingredient, Manifest, ManifestStore, SigningAlg, +}; + +fn main() { + println!("DataHash demo"); + + #[cfg(not(target_arch = "wasm32"))] + user_data_hash_with_sdk_hashing(); + + #[cfg(not(target_arch = "wasm32"))] + user_data_hash_with_user_hashing(); +} + +#[cfg(not(target_arch = "wasm32"))] +fn user_data_hash_with_sdk_hashing() { + const GENERATOR: &str = "test_app/0.1"; + + // You will often implement your own Signer trait to perform on device signing + let signcert_path = "sdk/tests/fixtures/certs/es256.pub"; + let pkey_path = "sdk/tests/fixtures/certs/es256.pem"; + let signer = + create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None).unwrap(); + + let src = "sdk/tests/fixtures/earth_apollo17.jpg"; + let dst = "target/tmp/output.jpg"; + + let source = PathBuf::from(src); + let dest = PathBuf::from(dst); + + let mut input_file = std::fs::OpenOptions::new() + .read(true) + .open(&source) + .unwrap(); + + let mut output_file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&dest) + .unwrap(); + + let parent = Ingredient::from_file(source.as_path()).unwrap(); + + // create an action assertion stating that we imported this file + let actions = Actions::new().add_action( + Action::new(c2pa_action::PLACED) + .set_parameter("identifier", parent.instance_id().to_owned()) + .unwrap(), + ); + + // build a creative work assertion + let creative_work = CreativeWork::new() + .add_author(SchemaDotOrgPerson::new().set_name("me").unwrap()) + .unwrap(); + + let exif = Exif::from_json_str( + r#"{ + "@context" : { + "exif": "http://ns.adobe.com/exif/1.0/" + }, + "exif:GPSVersionID": "2.2.0.0", + "exif:GPSLatitude": "39,21.102N", + "exif:GPSLongitude": "74,26.5737W", + "exif:GPSAltitudeRef": 0, + "exif:GPSAltitude": "100963/29890", + "exif:GPSTimeStamp": "2019-09-22T18:22:57Z" + }"#, + ) + .unwrap(); + + // create a new Manifest + let mut manifest = Manifest::new(GENERATOR.to_owned()); + // add parent and assertions + manifest + .set_parent(parent) + .unwrap() + .add_assertion(&actions) + .unwrap() + .add_assertion(&creative_work) + .unwrap() + .add_assertion(&exif) + .unwrap(); + + // get the composed manifest ready to insert into a file (returns manifest of same length as finished manifest) + let unfinished_manifest = manifest + .data_hash_placeholder(signer.as_ref(), "jpg") + .unwrap(); + + // Figure out where you want to put the manifest, let's put it at the beginning of the JPEG as first segment + // generate new file inserting unfinished manifest into file + input_file.rewind().unwrap(); + let mut before = vec![0u8; 2]; + input_file.read_exact(before.as_mut_slice()).unwrap(); + + output_file.write_all(&before).unwrap(); + + // write completed final manifest + output_file.write_all(&unfinished_manifest).unwrap(); + + // write bytes after + let mut after_buf = Vec::new(); + input_file.read_to_end(&mut after_buf).unwrap(); + output_file.write_all(&after_buf).unwrap(); + + // we need to add a data hash that excludes the manifest + let mut dh = DataHash::new("my_manifest", "sha265"); + let hr = HashRange::new(2, unfinished_manifest.len()); + dh.add_exclusion(hr); + + // tell SDK to fill in the hash and sign to complete the manifest + output_file.rewind().unwrap(); + let final_manifest = manifest + .data_hash_embeddable_manifest(&dh, signer.as_ref(), "jpg", Some(&mut output_file)) + .unwrap(); + + // replace temporary manifest with final signed manifest + // move to location where we inserted manifest, + // note: temporary manifest and final manifest will be the same size + output_file.seek(std::io::SeekFrom::Start(2)).unwrap(); + + // write completed final manifest bytes over temporary bytes + output_file.write_all(&final_manifest).unwrap(); + + // make sure the output file is correct + let manifest_store = ManifestStore::from_file(&dest).unwrap(); + + // example of how to print out the whole manifest as json + println!("{manifest_store}\n"); +} + +#[cfg(not(target_arch = "wasm32"))] +fn user_data_hash_with_user_hashing() { + const GENERATOR: &str = "test_app/0.1"; + + // You will often implement your own Signer trait to perform on device signing + let signcert_path = "sdk/tests/fixtures/certs/es256.pub"; + let pkey_path = "sdk/tests/fixtures/certs/es256.pem"; + let signer = + create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None).unwrap(); + + let src = "sdk/tests/fixtures/earth_apollo17.jpg"; + let dst = "target/tmp/output.jpg"; + + let source = PathBuf::from(src); + let dest = PathBuf::from(dst); + + let mut input_file = std::fs::OpenOptions::new() + .read(true) + .open(&source) + .unwrap(); + + let mut output_file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&dest) + .unwrap(); + + let parent = Ingredient::from_file(source.as_path()).unwrap(); + + // create an action assertion stating that we imported this file + let actions = Actions::new().add_action( + Action::new(c2pa_action::PLACED) + .set_parameter("identifier", parent.instance_id().to_owned()) + .unwrap(), + ); + + // build a creative work assertion + let creative_work = CreativeWork::new() + .add_author(SchemaDotOrgPerson::new().set_name("me").unwrap()) + .unwrap(); + + let exif = Exif::from_json_str( + r#"{ + "@context" : { + "exif": "http://ns.adobe.com/exif/1.0/" + }, + "exif:GPSVersionID": "2.2.0.0", + "exif:GPSLatitude": "39,21.102N", + "exif:GPSLongitude": "74,26.5737W", + "exif:GPSAltitudeRef": 0, + "exif:GPSAltitude": "100963/29890", + "exif:GPSTimeStamp": "2019-09-22T18:22:57Z" + }"#, + ) + .unwrap(); + + // create a new Manifest + let mut manifest = Manifest::new(GENERATOR.to_owned()); + // add parent and assertions + manifest + .set_parent(parent) + .unwrap() + .add_assertion(&actions) + .unwrap() + .add_assertion(&creative_work) + .unwrap() + .add_assertion(&exif) + .unwrap(); + + // get the composed manifest ready to insert into a file (returns manifest of same length as finished manifest) + let unfinished_manifest = manifest + .data_hash_placeholder(signer.as_ref(), "jpg") + .unwrap(); + + // Figure out where you want to put the manifest, let's put it at the beginning of the JPEG as first segment + // we will need to add a data hash that excludes the manifest + let mut dh = DataHash::new("my_manifest", "sha265"); + let hr = HashRange::new(2, unfinished_manifest.len()); + dh.add_exclusion(hr); + + // since the only thing we are excluding in this example is the manifest we can just hash all the bytes + // if you have additional exclusions you can add them to the DataHash and pass them to this function to be ' + // excluded from the hash generation + let hash = hash_stream_by_alg("sha256", &mut input_file, None, true).unwrap(); + dh.set_hash(hash); + + // tell SDK to fill we will provide the hash and sign to complete the manifest + let final_manifest = manifest + .data_hash_embeddable_manifest(&dh, signer.as_ref(), "jpg", None) + .unwrap(); + + // generate new file inserting final manifest into file + input_file.rewind().unwrap(); + let mut before = vec![0u8; 2]; + input_file.read_exact(before.as_mut_slice()).unwrap(); + + output_file.write_all(&before).unwrap(); + + // write completed final manifest + output_file.write_all(&final_manifest).unwrap(); + + // write bytes after + let mut after_buf = Vec::new(); + input_file.read_to_end(&mut after_buf).unwrap(); + output_file.write_all(&after_buf).unwrap(); + + // make sure the output file is correct + let manifest_store = ManifestStore::from_file(&dest).unwrap(); + + // example of how to print out the whole manifest as json + println!("{manifest_store}\n"); +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs @@ -145,9 +145,10 @@ pub(crate) mod status_tracker; pub(crate) mod store; pub(crate) mod time_stamp; pub(crate) mod utils; -pub use utils::cbor_types::DateT; pub mod validation_status; +pub use hash_utils::HashRange; pub(crate) use utils::{cbor_types, hash_utils}; +pub use utils::{cbor_types::DateT, hash_utils::hash_stream_by_alg}; pub(crate) mod validator; #[cfg(target_arch = "wasm32")] pub mod wasm; diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -32,12 +32,12 @@ use crate::{ asset_io::CAIRead, claim::{Claim, RemoteManifest}, error::{Error, Result}, - hash_utils::HashRange, jumbf, resource_store::{skip_serializing_resources, ResourceRef, ResourceStore}, salt::DefaultSalt, store::Store, - ClaimGeneratorInfo, Ingredient, ManifestAssertion, ManifestAssertionKind, RemoteSigner, Signer, + ClaimGeneratorInfo, HashRange, Ingredient, ManifestAssertion, ManifestAssertionKind, + RemoteSigner, Signer, }; /// A Manifest represents all the information in a c2pa manifest @@ -1147,22 +1147,16 @@ impl Manifest { /// The return value is pre-formatted for insertion into a file of the given format /// For JPEG it is a series of App11 JPEG segments containing space for a manifest /// This is used to create a properly formatted file ready for signing - pub fn data_hash_placeholder( - &mut self, - signer: &dyn RemoteSigner, - format: &str, - ) -> Result<Vec<u8>> { + pub fn data_hash_placeholder(&mut self, signer: &dyn Signer, format: &str) -> Result<Vec<u8>> { let dh: Result<DataHash> = self.find_assertion(DataHash::LABEL); if dh.is_err() { let mut ph = DataHash::new("jumbf manifest", "sha256"); for _ in 0..10 { ph.add_exclusion(HashRange::new(0, 2)); } - let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let mut stream = std::io::Cursor::new(data); - ph.gen_hash_from_stream(&mut stream)?; self.add_assertion(&ph)?; } + let mut store = self.to_store()?; let placeholder = store.get_data_hashed_manifest_placeholder(signer, format)?; Ok(placeholder) @@ -1175,10 +1169,10 @@ impl Manifest { /// This can directly replace a placeholder manifest to create a properly signed asset /// The data hash must contain exclusions and may contain pre-calculated hashes /// if an asset reader is provided, it will be used to calculate the data hash - pub async fn data_hash_embeddable_manifest( + pub fn data_hash_embeddable_manifest( &mut self, dh: &DataHash, - signer: &dyn RemoteSigner, + signer: &dyn Signer, format: &str, mut asset_reader: Option<&mut dyn CAIRead>, ) -> Result<Vec<u8>> { @@ -1186,22 +1180,20 @@ impl Manifest { if let Some(asset_reader) = asset_reader.as_deref_mut() { asset_reader.rewind()?; } - let cm = store - .get_data_hashed_embeddable_manifest(dh, signer, format, asset_reader) - .await?; + let cm = store.get_data_hashed_embeddable_manifest(dh, signer, format, asset_reader)?; Ok(cm) } /// Generates a signed box hashed manifest, optionally preformatted for embedding /// /// The manifest must include a box hash assertion with correct hashes - pub async fn box_hash_embeddable_manifest( + pub fn box_hash_embeddable_manifest( &mut self, - signer: &dyn RemoteSigner, + signer: &dyn Signer, format: Option<&str>, ) -> Result<Vec<u8>> { let mut store = self.to_store()?; - let mut cm = store.get_box_hashed_embeddable_manifest(signer).await?; + let mut cm = store.get_box_hashed_embeddable_manifest(signer)?; if let Some(format) = format { cm = store.get_composed_manifest(&cm, format)?; } @@ -2217,12 +2209,12 @@ pub(crate) mod tests { assert!(active_manifest.thumbnail().is_none()); } - #[actix::test] + #[test] #[cfg(feature = "file_io")] - async fn test_data_hash_embeddable_manifest() { + fn test_data_hash_embeddable_manifest() { let ap = fixture_path("cloud.jpg"); - let signer = temp_remote_signer(); + let signer = temp_signer(); let mut manifest = Manifest::new("claim_generator"); @@ -2260,7 +2252,6 @@ pub(crate) mod tests { "image/jpeg", Some(&mut output_file), ) - .await .unwrap(); use std::io::{Seek, SeekFrom, Write}; @@ -2274,9 +2265,9 @@ pub(crate) mod tests { assert!(manifest_store.validation_status().is_none()); } - #[actix::test] + #[test] #[cfg(feature = "file_io")] - async fn test_box_hash_embeddable_manifest() { + fn test_box_hash_embeddable_manifest() { let asset_bytes = include_bytes!("../tests/fixtures/boxhash.jpg"); let box_hash_data = include_bytes!("../tests/fixtures/boxhash.json"); let box_hash: crate::assertions::BoxHash = serde_json::from_slice(box_hash_data).unwrap(); @@ -2288,20 +2279,18 @@ pub(crate) mod tests { .add_labeled_assertion(crate::assertions::labels::BOX_HASH, &box_hash) .unwrap(); - let signer = temp_remote_signer(); + let signer = temp_signer(); let embeddable = manifest .box_hash_embeddable_manifest(signer.as_ref(), None) - .await .expect("embeddable_manifest"); // Validate the embeddable manifest against the asset bytes - let manifest_store = crate::ManifestStore::from_manifest_and_asset_bytes_async( + let manifest_store = crate::ManifestStore::from_manifest_and_asset_bytes( &embeddable, "image/jpeg", asset_bytes, ) - .await .unwrap(); println!("{manifest_store}"); assert!(!manifest_store.manifests().is_empty()); diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs @@ -283,6 +283,43 @@ impl ManifestStore { Ok(Self::from_store(&store, &mut validation_log)) } + + /// Synchronously loads a manifest from a buffer holding a binary manifest (.c2pa) and validates against an asset buffer + /// + /// # Example: Creating a manifest store from a .c2pa manifest and validating it against an asset + /// ``` + /// use c2pa::{Result, ManifestStore}; + /// + /// # fn main() -> Result<()> { + /// # async { + /// let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg"); + /// let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa"); + /// + /// let manifest_store = ManifestStore::from_manifest_and_asset_bytes(manifest_bytes, "image/jpg", asset_bytes) + /// .unwrap(); + /// + /// println!("{}", manifest_store); + /// # }; + /// # + /// # Ok(()) + /// } + /// ``` + pub fn from_manifest_and_asset_bytes( + manifest_bytes: &[u8], + format: &str, + asset_bytes: &[u8], + ) -> Result<ManifestStore> { + let mut validation_log = DetailedStatusTracker::new(); + let store = Store::from_jumbf(manifest_bytes, &mut validation_log)?; + + Store::verify_store( + &store, + &mut ClaimAssetData::Bytes(asset_bytes, format), + &mut validation_log, + )?; + + Ok(Self::from_store(&store, &mut validation_log)) + } } impl Default for ManifestStore { diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -51,7 +51,7 @@ use crate::{ hash_utils::{hash256, HashRange}, patch::patch_bytes, }, - validation_status, AsyncSigner, ManifestStoreReport, RemoteSigner, Signer, + validation_status, AsyncSigner, ManifestStoreReport, Signer, }; #[cfg(feature = "file_io")] use crate::{ @@ -1628,7 +1628,7 @@ impl Store { /// from `get_data_hashed_embeddable_manifest` will have a size that matches this function. pub fn get_data_hashed_manifest_placeholder( &mut self, - signer: &dyn RemoteSigner, + signer: &dyn Signer, format: &str, ) -> Result<Vec<u8>> { let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; @@ -1665,10 +1665,10 @@ impl Store { /// It is an error if `get_data_hashed_manifest_placeholder` was not called first /// as this call inserts the DataHash placeholder assertion to reserve space for the /// actual hash values not required when using BoxHashes. - pub async fn get_data_hashed_embeddable_manifest( + pub fn get_data_hashed_embeddable_manifest( &mut self, dh: &DataHash, - signer: &dyn RemoteSigner, + signer: &dyn Signer, format: &str, asset_reader: Option<&mut dyn CAIRead>, ) -> Result<Vec<u8>> { @@ -1705,8 +1705,7 @@ impl Store { let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?; // sign contents - let claim_bytes = pc.data()?; - let sig = signer.sign_remote(&claim_bytes).await?; + let sig = self.sign_claim(pc, signer, signer.reserve_size())?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); @@ -1722,10 +1721,7 @@ impl Store { /// Returns a finalized, signed manifest. The client is required to have /// included the necessary box hash assertion with the pregenerated hashes. - pub async fn get_box_hashed_embeddable_manifest( - &mut self, - signer: &dyn RemoteSigner, - ) -> Result<Vec<u8>> { + pub fn get_box_hashed_embeddable_manifest(&mut self, signer: &dyn Signer) -> Result<Vec<u8>> { let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; // make sure there is only one @@ -1743,8 +1739,7 @@ impl Store { let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?; // sign contents - let claim_bytes = pc.data()?; - let sig = signer.sign_remote(&claim_bytes).await?; + let sig = self.sign_claim(pc, signer, signer.reserve_size())?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); if sig_placeholder.len() != sig.len() { @@ -2895,8 +2890,8 @@ pub mod tests { hash_utils::Hasher, patch::patch_file, test::{ - create_test_claim, fixture_path, temp_dir_path, temp_fixture_path, - temp_remote_signer, temp_signer, write_jpeg_placeholder_file, + create_test_claim, fixture_path, temp_dir_path, temp_fixture_path, temp_signer, + write_jpeg_placeholder_file, }, }, AssertionJson, SigningAlg, @@ -3988,24 +3983,6 @@ pub mod tests { } #[test] - fn test_modify_xmp() { - // modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP) - let mut report = patch_and_report( - "cloud.jpg", - b"W5M0MpCehiHzreSzNTczkc9d", - b"W5M0MpCehiHzreSzNTczkXXX", - ); - assert!(!report.get_log().is_empty()); - let errors = report_split_errors(report.get_log_mut()); - - assert!(errors[0].error_str().unwrap().starts_with("HashMismatch")); - assert_eq!( - errors[0].validation_status.as_deref(), - Some(validation_status::ASSERTION_DATAHASH_MISMATCH) - ); // what validation status should we have for this? - } - - #[test] fn test_claim_modified() { // replace the title that is inside the claim data - should cause signature to not match let mut report = patch_and_report("C.jpg", b"C.jpg", b"X.jpg"); @@ -4472,10 +4449,9 @@ pub mod tests { } } } - - #[actix::test] + #[test] #[cfg(feature = "file_io")] - async fn test_boxhash_embeddable_manifest() { + fn test_boxhash_embeddable_manifest() { // test adding to actual image let ap = fixture_path("boxhash.jpg"); let box_hash_path = fixture_path("boxhash.json"); @@ -4495,12 +4471,11 @@ pub mod tests { store.commit_claim(claim).unwrap(); // Do we generate JUMBF? - let signer = temp_remote_signer(); + let signer = temp_signer(); // get the embeddable manifest let em = store .get_box_hashed_embeddable_manifest(signer.as_ref()) - .await .unwrap(); // get composed version for embedding to JPEG @@ -4554,13 +4529,14 @@ pub mod tests { assert!(errors.is_empty()); } - #[actix::test] - async fn test_datahash_embeddable_manifest() { + #[test] + #[cfg(feature = "file_io")] + fn test_datahash_embeddable_manifest() { // test adding to actual image let ap = fixture_path("cloud.jpg"); // Do we generate JUMBF? - let signer = temp_remote_signer(); + let signer = temp_signer(); // Create claims store. let mut store = Store::new(); @@ -4606,7 +4582,6 @@ pub mod tests { "jpeg", Some(&mut output_file), ) - .await .unwrap(); // path in new composed manifest @@ -4620,15 +4595,16 @@ pub mod tests { assert!(errors.is_empty()); } - #[actix::test] - async fn test_datahash_embeddable_manifest_user_hashed() { + #[test] + #[cfg(feature = "file_io")] + fn test_datahash_embeddable_manifest_user_hashed() { // test adding to actual image let ap = fixture_path("cloud.jpg"); let mut hasher = Hasher::SHA256(Sha256::new()); // Do we generate JUMBF? - let signer = temp_remote_signer(); + let signer = temp_signer(); // Create claims store. let mut store = Store::new(); @@ -4670,7 +4646,6 @@ pub mod tests { // get the embeddable manifest, using user hashing let cm = store .get_data_hashed_embeddable_manifest(&dh, signer.as_ref(), "jpeg", None) - .await .unwrap(); // path in new composed manifest