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:
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