commit ffa1d553a7e2a5e64bd7fcfaac9b4af40d622f5f
parent 29bb633d12d6a100f478216a1c279474e171535d
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Thu, 21 Jul 2022 17:49:48 -0400
Add support for remotely generated CoseSign1 signatures (#87)
* Fix bug in verify_from_buffer
* make_test fixes
adjust signature box reserved size to account for large timestamps
* change tsa to tsa_url
* Fix make images in makefile
* remove debug printlns
* Async claim signing support
* fix formatting
* allow async validation with OpenSSL validators
* simplify login in cose_pad
* clippy fixes
* cargo fmt fix
* Remove old comment to resolve PR request
* Restore `bmff` feature
* Add remote signing capability
* Make RemoteSigner accessible via manifest.
* Add manifest unit tests for embed_async_signed and embed_remote_signed
* All embed method have source and dest paths
* Rename function save_to_asset_remotely_signed to save_to_asset_remote_signed
* Edit API docs slightly
* Edit API docs
* Fix up use statements to match new API doc syntax
Co-authored-by: Gavin Peacock <gpeacock@adobe.com>
Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
4 files changed, 220 insertions(+), 28 deletions(-)
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -110,10 +110,10 @@ mod openssl;
#[cfg(feature = "file_io")]
mod signer;
-#[cfg(feature = "async_signer")]
-pub use signer::AsyncSigner;
#[cfg(feature = "file_io")]
pub use signer::Signer;
+#[cfg(feature = "async_signer")]
+pub use signer::{AsyncSigner, RemoteSigner};
/// crate private declarations
#[allow(dead_code, clippy::enum_variant_names)]
pub(crate) mod asn1;
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -24,6 +24,9 @@ use crate::{
#[cfg(feature = "file_io")]
use crate::Signer;
+#[cfg(all(feature = "async_signer", feature = "file_io"))]
+use crate::{AsyncSigner, RemoteSigner};
+
use log::{debug, error, warn};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
@@ -595,6 +598,31 @@ impl Manifest {
Ok(store)
}
+ // factor out this code to set up the destination path with a file
+ // so we can use set_asset_from_path to initialize the right fields in Manifest
+ #[cfg(feature = "file_io")]
+ fn embed_prep<P: AsRef<Path>>(&mut self, source_path: P, dest_path: P) -> Result<P> {
+ let mut copied = false;
+
+ if !source_path.as_ref().exists() {
+ let path = source_path.as_ref().to_string_lossy().into_owned();
+ return Err(Error::FileNotFound(path));
+ }
+ // we need to copy the source to target before setting the asset info
+ if !dest_path.as_ref().exists() {
+ std::fs::copy(&source_path, &dest_path)?;
+ copied = true;
+ }
+ // first add the information about the target file
+ self.set_asset_from_path(dest_path.as_ref());
+
+ if copied {
+ Ok(dest_path)
+ } else {
+ Ok(source_path)
+ }
+ }
+
/// Embed a signed manifest into the target file using a supplied signer.
///
/// # Example: Embed a manifest in a file
@@ -631,48 +659,56 @@ impl Manifest {
dest_path: P,
signer: &dyn Signer,
) -> Result<Store> {
- if !source_path.as_ref().exists() {
- let path = source_path.as_ref().to_string_lossy().into_owned();
- return Err(Error::FileNotFound(path));
- }
- // we need to copy the source to target before setting the asset info
- let mut copied = false;
- if !dest_path.as_ref().exists() {
- std::fs::copy(&source_path, &dest_path)?;
- copied = true;
- }
- // first add the information about the target file
- self.set_asset_from_path(dest_path.as_ref());
+ // Add manifest info for this target file
+ let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
+
// convert the manifest to a store
let mut store = self.to_store()?;
+
// sign and write our store to to the output image file
- if copied {
- // set source and dest to same path to avoid an unnecessary copy since we already copied above
- store.save_to_asset(dest_path.as_ref(), signer, dest_path.as_ref())?;
- } else {
- // save to asset will do the copy
- store.save_to_asset(source_path.as_ref(), signer, dest_path.as_ref())?;
- }
+ store.save_to_asset(source_path.as_ref(), signer, dest_path.as_ref())?;
// todo: update xmp
Ok(store)
}
- /// Embed a signed manifest into the target file using a supplied async signer
+ /// Embed a signed manifest into the target file using a supplied [`AsyncSigner`].
#[cfg(feature = "file_io")]
#[cfg(feature = "async_signer")]
- pub async fn embed_async<P: AsRef<Path>>(
+ pub async fn embed_async_signed<P: AsRef<Path>>(
&mut self,
- target_path: &P,
- signer: &dyn crate::signer::AsyncSigner,
+ source_path: P,
+ dest_path: P,
+ signer: &dyn AsyncSigner,
) -> Result<Store> {
- // first add the information about the target file
- self.set_asset_from_path(target_path);
+ // Add manifest info for this target file
+ let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
// convert the manifest to a store
let mut store = self.to_store()?;
// sign and write our store to to the output image file
store
- .save_to_asset_async(target_path.as_ref(), signer, target_path.as_ref())
+ .save_to_asset_async(source_path.as_ref(), signer, dest_path.as_ref())
+ .await?;
+
+ // todo: update xmp
+ Ok(store)
+ }
+
+ /// Embed a signed manifest into the target file using a supplied [`RemoteSigner`].
+ #[cfg(all(feature = "file_io", feature = "async_signer"))]
+ pub async fn embed_remote_signed<P: AsRef<Path>>(
+ &mut self,
+ source_path: P,
+ dest_path: P,
+ signer: &dyn RemoteSigner,
+ ) -> Result<Store> {
+ // Add manifest info for this target file
+ let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?;
+ // convert the manifest to a store
+ let mut store = self.to_store()?;
+ // sign and write our store to to the output image file
+ store
+ .save_to_asset_remote_signed(source_path.as_ref(), signer, dest_path.as_ref())
.await?;
// todo: update xmp
@@ -957,4 +993,68 @@ pub(crate) mod tests {
assert!(action2.is_ok());
assert_eq!(action2.unwrap().actions()[0].action(), c2pa_action::EDITED);
}
+
+ #[cfg(all(feature = "file_io", feature = "async_signer"))]
+ #[actix::test]
+ async fn test_embed_async_sign() {
+ let temp_dir = tempdir().expect("temp dir");
+ let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
+
+ let async_signer =
+ crate::openssl::temp_signer_async::AsyncSignerAdapter::new(crate::SigningAlg::Ps256);
+
+ let mut manifest = test_manifest();
+ manifest
+ .embed_async_signed(&output, &output, &async_signer)
+ .await
+ .expect("embed");
+ let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ assert!(manifest_store.active_label().is_some());
+ assert_eq!(
+ manifest_store.get_active().unwrap().title().unwrap(),
+ TEST_SMALL_JPEG
+ );
+ }
+
+ #[cfg(all(feature = "file_io", feature = "async_signer"))]
+ #[actix::test]
+ async fn test_embed_remote_sign() {
+ struct MyRemoteSigner {}
+
+ #[async_trait::async_trait]
+ impl crate::signer::RemoteSigner for MyRemoteSigner {
+ async fn sign_remote(&self, claim_bytes: &[u8]) -> crate::error::Result<Vec<u8>> {
+ let signer = crate::openssl::temp_signer_async::AsyncSignerAdapter::new(
+ crate::SigningAlg::Ps256,
+ );
+
+ // this would happen on some remote server
+ let cose_sign1_box =
+ crate::cose_sign::cose_sign_async(&signer, claim_bytes, self.reserve_size())
+ .await;
+
+ cose_sign1_box
+ }
+ fn reserve_size(&self) -> usize {
+ 10000
+ }
+ }
+
+ let temp_dir = tempdir().expect("temp dir");
+ let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
+
+ let remote_signer = MyRemoteSigner {};
+
+ let mut manifest = test_manifest();
+ manifest
+ .embed_remote_signed(&output, &output, &remote_signer)
+ .await
+ .expect("embed");
+ let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ assert!(manifest_store.active_label().is_some());
+ assert_eq!(
+ manifest_store.get_active().unwrap().title().unwrap(),
+ TEST_SMALL_JPEG
+ );
+ }
}
diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs
@@ -102,3 +102,20 @@ pub trait AsyncSigner: Sync {
None
}
}
+
+#[cfg(feature = "async_signer")]
+#[async_trait]
+pub trait RemoteSigner: Sync {
+ /// Returns the `CoseSign1` bytes signed by the [`RemoteSigner`].
+ ///
+ /// The size of returned `Vec` must match the value returned by `reserve_size`.
+ /// This data will be embedded in the JUMBF `c2pa.signature` box of the manifest.
+ /// `data` are the bytes of the claim to be remotely signed.
+ async fn sign_remote(&self, data: &[u8]) -> Result<Vec<u8>>;
+
+ /// Returns the size in bytes of the largest possible expected signature.
+ ///
+ /// Signing will fail if the result of the `sign` function is larger
+ /// than this value.
+ fn reserve_size(&self) -> usize;
+}
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -1440,6 +1440,32 @@ impl Store {
}
}
+ /// Embed the claims store as jumbf into an asset using an CoseSign box generated remotely. Updates XMP with provenance record.
+ #[cfg(feature = "async_signer")]
+ pub async fn save_to_asset_remote_signed(
+ &mut self,
+ asset_path: &Path,
+ remote_signer: &dyn crate::signer::RemoteSigner,
+ output_path: &Path,
+ ) -> Result<()> {
+ let jumbf_bytes = self.start_save(asset_path, output_path, remote_signer.reserve_size())?;
+
+ let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
+ let sig = remote_signer.sign_remote(&pc.data()?).await?;
+
+ let sig_placeholder = self.sign_claim_placeholder(pc, remote_signer.reserve_size());
+
+ match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) {
+ Ok(v) => {
+ // save sig so store is up to date
+ let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+ pc_mut.set_signature_val(v);
+ Ok(())
+ }
+ Err(e) => Err(e),
+ }
+ }
+
#[cfg(feature = "file_io")]
fn start_save(
&mut self,
@@ -2088,6 +2114,27 @@ pub mod tests {
}
}
+ #[cfg(feature = "async_signer")]
+ struct MyRemoteSigner {}
+
+ #[cfg(feature = "async_signer")]
+ #[async_trait::async_trait]
+ impl crate::signer::RemoteSigner for MyRemoteSigner {
+ async fn sign_remote(&self, claim_bytes: &[u8]) -> crate::error::Result<Vec<u8>> {
+ let signer =
+ crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256);
+
+ // this would happen on some remote server
+ let cose_sign1_box =
+ crate::cose_sign::cose_sign_async(&signer, claim_bytes, self.reserve_size()).await;
+
+ cose_sign1_box
+ }
+ fn reserve_size(&self) -> usize {
+ 10000
+ }
+ }
+
#[test]
#[cfg(feature = "file_io")]
fn test_detects_unverifiable_signature() {
@@ -2231,6 +2278,34 @@ pub mod tests {
let _new_store = Store::load_from_asset(&op, true, &mut report).unwrap();
}
+ #[cfg(feature = "async_signer")]
+ #[actix::test]
+ async fn test_jumbf_generation_remote() {
+ // test adding to actual image
+ let ap = fixture_path("earth_apollo17.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "test-async.jpg");
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let claim1 = create_test_claim().unwrap();
+
+ // create my remote signer to map the CoseSign1 data back into the asset
+ let remote_signer = MyRemoteSigner {};
+
+ store.commit_claim(claim1).unwrap();
+ store
+ .save_to_asset_remote_signed(&ap, &remote_signer, &op)
+ .await
+ .unwrap();
+
+ // make sure we can read from new file
+ let mut report = DetailedStatusTracker::new();
+ let _new_store = Store::load_from_asset(&op, true, &mut report).unwrap();
+ }
+
#[test]
#[cfg(feature = "file_io")]
fn test_png_jumbf_generation() {