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 ef9b10e39b4ba6c5746c98d5eb83257d0224a6e9
parent 02e237dfe81e26bf743ba08a8c698b0dd7e1d7a0
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Wed, 15 Nov 2023 17:04:33 -0500

Add support for embeddable manifests with RemoteSigner (#344)

* Add support for embeddable manifests with RemoteSigner

* Fix clippy error
Diffstat:
Msdk/examples/data_hash.rs | 4++--
Msdk/src/manifest.rs | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msdk/src/store.rs | 136++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
3 files changed, 163 insertions(+), 68 deletions(-)

diff --git a/sdk/examples/data_hash.rs b/sdk/examples/data_hash.rs @@ -108,7 +108,7 @@ fn user_data_hash_with_sdk_hashing() { // 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") + .data_hash_placeholder(signer.reserve_size(), "jpg") .unwrap(); // Figure out where you want to put the manifest, let's put it at the beginning of the JPEG as first segment @@ -225,7 +225,7 @@ fn user_data_hash_with_user_hashing() { // 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") + .data_hash_placeholder(signer.reserve_size(), "jpg") .unwrap(); // Figure out where you want to put the manifest, let's put it at the beginning of the JPEG as first segment diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -1158,8 +1158,10 @@ 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 Signer, format: &str) -> Result<Vec<u8>> { + /// This is used to create a properly formatted file ready for signing. + /// The reserve_size is the amount of space to reserve for the signature box. This + /// value is fixed once set and must be sufficient to hold the completed signature + pub fn data_hash_placeholder(&mut self, reserve_size: usize, 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"); @@ -1170,8 +1172,7 @@ impl Manifest { } let mut store = self.to_store()?; - let placeholder = - store.get_data_hashed_manifest_placeholder(signer.reserve_size(), format)?; + let placeholder = store.get_data_hashed_manifest_placeholder(reserve_size, format)?; Ok(placeholder) } @@ -1197,6 +1198,29 @@ impl Manifest { Ok(cm) } + /// Generates an data hashed embeddable manifest for a file + /// + /// 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 a signed 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_remote( + &mut self, + dh: &DataHash, + signer: &dyn RemoteSigner, + format: &str, + mut asset_reader: Option<&mut dyn CAIRead>, + ) -> Result<Vec<u8>> { + let mut store = self.to_store()?; + if let Some(asset_reader) = asset_reader.as_deref_mut() { + asset_reader.rewind()?; + } + store + .get_data_hashed_embeddable_manifest_remote(dh, signer, format, asset_reader) + .await + } + /// Generates a signed box hashed manifest, optionally preformatted for embedding /// /// The manifest must include a box hash assertion with correct hashes @@ -2264,7 +2288,7 @@ pub(crate) mod tests { // get a placeholder the manifest let placeholder = manifest - .data_hash_placeholder(signer.as_ref(), "jpeg") + .data_hash_placeholder(signer.reserve_size(), "jpeg") .unwrap(); let temp_dir = tempfile::tempdir().unwrap(); @@ -2309,6 +2333,63 @@ pub(crate) mod tests { assert!(manifest_store.validation_status().is_none()); } + #[cfg(all(feature = "file_io", feature = "openssl_sign"))] + #[actix::test] + async fn test_data_hash_embeddable_manifest_remote_signed() { + let ap = fixture_path("cloud.jpg"); + + let signer = temp_remote_signer(); + + let mut manifest = Manifest::new("claim_generator"); + + // get a placeholder the manifest + let placeholder = manifest + .data_hash_placeholder(signer.reserve_size(), "jpeg") + .unwrap(); + + let temp_dir = tempfile::tempdir().unwrap(); + let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); + let mut output_file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&output) + .unwrap(); + + // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder) + let offset = + write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap(); + + // build manifest to insert in the hole + + // create an hash exclusion for the manifest + let exclusion = HashRange::new(offset, placeholder.len()); + let exclusions = vec![exclusion]; + + let mut dh = DataHash::new("source_hash", "sha256"); + dh.exclusions = Some(exclusions); + + let signed_manifest = manifest + .data_hash_embeddable_manifest_remote( + &dh, + signer.as_ref(), + "image/jpeg", + Some(&mut output_file), + ) + .await + .unwrap(); + + use std::io::{Seek, SeekFrom, Write}; + + // path in new composed manifest + output_file.seek(SeekFrom::Start(offset as u64)).unwrap(); + output_file.write_all(&signed_manifest).unwrap(); + + let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file"); + println!("{manifest_store}"); + assert!(manifest_store.validation_status().is_none()); + } + #[test] #[cfg(feature = "file_io")] fn test_box_hash_embeddable_manifest() { 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, Signer, + validation_status, AsyncSigner, ManifestStoreReport, RemoteSigner, Signer, }; #[cfg(feature = "file_io")] use crate::{ @@ -1659,22 +1659,10 @@ impl Store { Ok(composed) } - /// Returns a finalized, signed manifest. The manfiest are only supported - /// for cases when the client has provided a data hash content hash binding. Note, - /// this function will not work for cases like BMFF where the position - /// of the content is also encoded. This function is not compatible with - /// BMFF hash binding. If a BMFF data hash or box hash is detected that is - /// an error. The DataHash placeholder assertion will be adjusted to the contain - /// the correct values. If the asset_reader value is supplied it will also perform - /// the hash calulations, otherwise the function uses the caller supplied values. - /// 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 fn get_data_hashed_embeddable_manifest( + fn prep_embeddable_store( &mut self, + reserve_size: usize, dh: &DataHash, - signer: &dyn Signer, - format: &str, asset_reader: Option<&mut dyn CAIRead>, ) -> Result<Vec<u8>> { let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; @@ -1705,23 +1693,23 @@ impl Store { // update the placeholder hash pc.update_data_hash(adusted_dh)?; - // reborrow immuttable - let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; - let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?; - - // sign contents - let sig = self.sign_claim(pc, signer, signer.reserve_size())?; - - let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); + self.to_jumbf_internal(reserve_size) + } + fn finish_embeddable_store( + &mut self, + sig: &[u8], + sig_placeholder: &[u8], + jumbf_bytes: &mut Vec<u8>, + format: &str, + ) -> Result<Vec<u8>> { if sig_placeholder.len() != sig.len() { return Err(Error::CoseSigboxTooSmall); } - patch_bytes(&mut jumbf_bytes, &sig_placeholder, &sig) - .map_err(|_| Error::JumbfCreationError)?; + patch_bytes(jumbf_bytes, sig_placeholder, sig).map_err(|_| Error::JumbfCreationError)?; - self.get_composed_manifest(&jumbf_bytes, format) + self.get_composed_manifest(jumbf_bytes, format) } /// Returns a finalized, signed manifest. The manfiest are only supported @@ -1735,60 +1723,86 @@ 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_async( + pub fn get_data_hashed_embeddable_manifest( &mut self, dh: &DataHash, - signer: &dyn AsyncSigner, + signer: &dyn Signer, format: &str, asset_reader: Option<&mut dyn CAIRead>, ) -> Result<Vec<u8>> { - let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - - // make sure there are data hashes present before generating - if pc.hash_assertions().is_empty() { - return Err(Error::BadParam( - "Claim must have hash binding assertion".to_string(), - )); - } + let mut jumbf_bytes = + self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; - // don't allow BMFF assertions to be present - if !pc.bmff_hash_assertions().is_empty() { - return Err(Error::BadParam( - "BMFF assertions not supported in embeddable manifests".to_string(), - )); - } - - let mut adusted_dh = DataHash::new("jumbf manifest", pc.alg()); - adusted_dh.exclusions = dh.exclusions.clone(); - adusted_dh.hash = dh.hash.clone(); + // sign contents + let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; + let sig = self.sign_claim(pc, signer, signer.reserve_size())?; - if let Some(reader) = asset_reader { - // calc hashes - adusted_dh.gen_hash_from_stream(reader)?; - } + let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); - // update the placeholder hash - pc.update_data_hash(adusted_dh)?; + self.finish_embeddable_store(&sig, &sig_placeholder, &mut jumbf_bytes, format) + } - // reborrow immuttable - let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; - let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?; + /// Returns a finalized, signed manifest. The manfiest are only supported + /// for cases when the client has provided a data hash content hash binding. Note, + /// this function will not work for cases like BMFF where the position + /// of the content is also encoded. This function is not compatible with + /// BMFF hash binding. If a BMFF data hash or box hash is detected that is + /// an error. The DataHash placeholder assertion will be adjusted to the contain + /// the correct values. If the asset_reader value is supplied it will also perform + /// the hash calulations, otherwise the function uses the caller supplied values. + /// 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_async( + &mut self, + dh: &DataHash, + signer: &dyn AsyncSigner, + format: &str, + asset_reader: Option<&mut dyn CAIRead>, + ) -> Result<Vec<u8>> { + let mut jumbf_bytes = + self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; // sign contents + let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = self .sign_claim_async(pc, signer, signer.reserve_size()) .await?; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); - if sig_placeholder.len() != sig.len() { - return Err(Error::CoseSigboxTooSmall); - } + self.finish_embeddable_store(&sig, &sig_placeholder, &mut jumbf_bytes, format) + } - patch_bytes(&mut jumbf_bytes, &sig_placeholder, &sig) - .map_err(|_| Error::JumbfCreationError)?; + /// Returns a finalized, signed manifest. The manfiest are only supported + /// for cases when the client has provided a data hash content hash binding. Note, + /// this function will not work for cases like BMFF where the position + /// of the content is also encoded. This function is not compatible with + /// BMFF hash binding. If a BMFF data hash or box hash is detected that is + /// an error. The DataHash placeholder assertion will be adjusted to the contain + /// the correct values. If the asset_reader value is supplied it will also perform + /// the hash calulations, otherwise the function uses the caller supplied values. + /// 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_remote( + &mut self, + dh: &DataHash, + signer: &dyn RemoteSigner, + format: &str, + asset_reader: Option<&mut dyn CAIRead>, + ) -> Result<Vec<u8>> { + let mut jumbf_bytes = + self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; + + // sign contents + let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; + let claim_bytes = pc.data()?; + let sig = signer.sign_remote(&claim_bytes).await?; + + let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); - self.get_composed_manifest(&jumbf_bytes, format) + self.finish_embeddable_store(&sig, &sig_placeholder, &mut jumbf_bytes, format) } /// Returns a finalized, signed manifest. The client is required to have