commit db03f1f59fb00d850285de13693d109187f6e09a
parent 4dd6b596bc4d28b390238ceeb82cf21b0567c309
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Thu, 19 Oct 2023 14:10:26 -0400
Restore async versions of embedd functions (#327)
* Initial box hash support
* Full box hash implementation for JPG
* Box hash fixes
* More unit tests and safety checks
* Formatting fix
* Another format fix
* More error checking
* Add API to get embeddable manifest
* PR feedback
* Add missing test file
* Change timestamp message imprint to include entire protected header
* Format fixes
* Fix for mixing cbor and json in a single object
* cleanup
* Simplify fix to use existing code
* Rename test file to independent of other uses.
* Initial support for new external hashing support
* New external hashing support
* Switch new functions to be async
* Allow users to set data hash
* Disable unit test that is not ready to be enabled
* Fix clippy issue
* Add async version of embed functions
* Revert accidental change
---------
Co-authored-by: Gavin Peacock <gpeacock@adobe.com>
Diffstat:
2 files changed, 257 insertions(+), 7 deletions(-)
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -1170,7 +1170,8 @@ impl Manifest {
}
let mut store = self.to_store()?;
- let placeholder = store.get_data_hashed_manifest_placeholder(signer, format)?;
+ let placeholder =
+ store.get_data_hashed_manifest_placeholder(signer.reserve_size(), format)?;
Ok(placeholder)
}
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -1627,13 +1627,13 @@ impl Store {
/// This function is used to pre-generate a manifest with place holders for the final
/// DataHash and Manifest Signature. The DataHash will reserve space for at least 10
- /// Exclusion ranges. The Signature box reserved size is based on the size returned by
- /// the Signer. This function is not needed when using Box Hash. This function is used
+ /// Exclusion ranges. The Signature box reserved size is based on the size required by
+ /// the Signer you plan to use. This function is not needed when using Box Hash. This function is used
/// in conjunction with `get_data_hashed_embeddable_manifest`. The manifest returned
/// 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 Signer,
+ reserve_size: usize,
format: &str,
) -> Result<Vec<u8>> {
let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
@@ -1652,7 +1652,7 @@ impl Store {
pc.add_assertion_with_salt(&ph, &DefaultSalt::default())?;
}
- let jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?;
+ let jumbf_bytes = self.to_jumbf_internal(reserve_size)?;
let composed = self.get_composed_manifest(&jumbf_bytes, format)?;
@@ -1724,6 +1724,73 @@ impl Store {
self.get_composed_manifest(&jumbf_bytes, format)
}
+ /// 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 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(),
+ ));
+ }
+
+ // 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();
+
+ if let Some(reader) = asset_reader {
+ // calc hashes
+ adusted_dh.gen_hash_from_stream(reader)?;
+ }
+
+ // 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_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);
+ }
+
+ patch_bytes(&mut jumbf_bytes, &sig_placeholder, &sig)
+ .map_err(|_| Error::JumbfCreationError)?;
+
+ self.get_composed_manifest(&jumbf_bytes, format)
+ }
+
/// Returns a finalized, signed manifest. The client is required to have
/// included the necessary box hash assertion with the pregenerated hashes.
pub fn get_box_hashed_embeddable_manifest(&mut self, signer: &dyn Signer) -> Result<Vec<u8>> {
@@ -1757,6 +1824,44 @@ impl Store {
Ok(jumbf_bytes)
}
+ /// 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_async(
+ &mut self,
+ signer: &dyn AsyncSigner,
+ ) -> Result<Vec<u8>> {
+ let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
+
+ // make sure there is only one
+ if pc.hash_assertions().len() != 1 {
+ return Err(Error::BadParam(
+ "Claim must have exactly one hash binding assertion".to_string(),
+ ));
+ }
+
+ // only allow box hash assertions to be present
+ if pc.box_hash_assertions().is_empty() {
+ return Err(Error::BadParam("Missing box hash assertion".to_string()));
+ }
+
+ let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?;
+
+ // sign contents
+ 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);
+ }
+
+ patch_bytes(&mut jumbf_bytes, &sig_placeholder, &sig)
+ .map_err(|_| Error::JumbfCreationError)?;
+
+ Ok(jumbf_bytes)
+ }
+
/// Returns the supplied manifest composed to be directly compatibile with the desired format.
/// For example, if format is JPEG funtion will return the set of APP11 segments that contains
/// the manifest. Similarly for PNG it would be the PNG chunk complete with header and CRC.
@@ -4454,6 +4559,88 @@ pub mod tests {
}
}
}
+
+ #[actix::test]
+ #[cfg(feature = "file_io")]
+ async fn test_boxhash_embeddable_manifest_async() {
+ // test adding to actual image
+ let ap = fixture_path("boxhash.jpg");
+ let box_hash_path = fixture_path("boxhash.json");
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let mut claim = create_test_claim().unwrap();
+
+ // add box hash for CA.jpg
+ let box_hash_data = std::fs::read(box_hash_path).unwrap();
+ let assertion = Assertion::from_data_json(BOX_HASH, &box_hash_data).unwrap();
+ let box_hash = BoxHash::from_json_assertion(&assertion).unwrap();
+ claim.add_assertion(&box_hash).unwrap();
+
+ store.commit_claim(claim).unwrap();
+
+ // Do we generate JUMBF?
+ let signer = crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256);
+
+ // get the embeddable manifest
+ let em = store
+ .get_box_hashed_embeddable_manifest_async(&signer)
+ .await
+ .unwrap();
+
+ // get composed version for embedding to JPEG
+ let cm = store.get_composed_manifest(&em, "jpg").unwrap();
+
+ // insert manifest into ouput asset
+ let jpeg_io = get_assetio_handler_from_path(&ap).unwrap();
+ let ol = jpeg_io.get_object_locations(&ap).unwrap();
+
+ let cai_loc = ol
+ .iter()
+ .find(|o| o.htype == HashBlockObjectType::Cai)
+ .unwrap();
+
+ // remove any existing manifest
+ jpeg_io.read_cai_store(&ap).unwrap();
+
+ // build new asset in memory inserting new manifest
+ let outbuf = Vec::new();
+ let mut out_stream = Cursor::new(outbuf);
+ let mut input_file = std::fs::File::open(&ap).unwrap();
+
+ // write before
+ let mut before = vec![0u8; cai_loc.offset];
+ input_file.read_exact(before.as_mut_slice()).unwrap();
+ out_stream.write_all(&before).unwrap();
+
+ // write composed bytes
+ out_stream.write_all(&cm).unwrap();
+
+ // write bytes after
+ let mut after_buf = Vec::new();
+ input_file.read_to_end(&mut after_buf).unwrap();
+ out_stream.write_all(&after_buf).unwrap();
+
+ // save to output file
+ 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();
+ output_file.write_all(&out_stream.into_inner()).unwrap();
+
+ let mut report = DetailedStatusTracker::new();
+ let _new_store = Store::load_from_asset(&output, true, &mut report).unwrap();
+
+ let errors = report_split_errors(report.get_log_mut());
+ assert!(errors.is_empty());
+ }
+
#[test]
#[cfg(feature = "file_io")]
fn test_boxhash_embeddable_manifest() {
@@ -4534,6 +4721,68 @@ pub mod tests {
assert!(errors.is_empty());
}
+ #[actix::test]
+ #[cfg(feature = "file_io")]
+ async fn test_datahash_embeddable_manifest_async() {
+ // test adding to actual image
+ let ap = fixture_path("cloud.jpg");
+
+ // Do we generate JUMBF?
+ let signer = crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256);
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let claim = create_test_claim().unwrap();
+
+ store.commit_claim(claim).unwrap();
+
+ // get a placeholder the manifest
+ let placeholder = store
+ .get_data_hashed_manifest_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);
+
+ // get the embeddable manifest, letting API do the hashing
+ output_file.rewind().unwrap();
+ let cm = store
+ .get_data_hashed_embeddable_manifest_async(&dh, &signer, "jpeg", Some(&mut output_file))
+ .await
+ .unwrap();
+
+ // path in new composed manifest
+ output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
+ output_file.write_all(&cm).unwrap();
+
+ let mut report = DetailedStatusTracker::new();
+ let _new_store = Store::load_from_asset(&output, true, &mut report).unwrap();
+
+ let errors = report_split_errors(report.get_log_mut());
+ assert!(errors.is_empty());
+ }
+
#[test]
#[cfg(feature = "file_io")]
fn test_datahash_embeddable_manifest() {
@@ -4553,7 +4802,7 @@ pub mod tests {
// get a placeholder the manifest
let placeholder = store
- .get_data_hashed_manifest_placeholder(signer.as_ref(), "jpeg")
+ .get_data_hashed_manifest_placeholder(signer.reserve_size(), "jpeg")
.unwrap();
let temp_dir = tempfile::tempdir().unwrap();
@@ -4621,7 +4870,7 @@ pub mod tests {
// get a placeholder for the manifest
let placeholder = store
- .get_data_hashed_manifest_placeholder(signer.as_ref(), "jpeg")
+ .get_data_hashed_manifest_placeholder(signer.reserve_size(), "jpeg")
.unwrap();
let temp_dir = tempfile::tempdir().unwrap();