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 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:
Msdk/src/lib.rs | 4++--
Msdk/src/manifest.rs | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msdk/src/signer.rs | 17+++++++++++++++++
Msdk/src/store.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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() {