From 5160b6fad73947fe0490ba363c9b052eed1d1ffc Mon Sep 17 00:00:00 2001 From: Prakash Duggaraju Date: Wed, 1 May 2024 15:31:45 -0700 Subject: [PATCH] Expose authoring support in WASM (#369) * Expose authoring support in WASM Expose async version of 'embed_from_memory' which can be called from wasm. Update the SyncSigner/AsyncSigner to separate the timestamp message creation from sending HTTP request. Add unit test for the same. * additional merge changes --- sdk/src/manifest.rs | 73 +++++++++++++++++++-- sdk/src/signer.rs | 34 +++++++--- sdk/src/store.rs | 147 ++++++++++++++---------------------------- sdk/src/time_stamp.rs | 63 ++++++++++-------- sdk/src/utils/test.rs | 121 +++++++++++++++++++++++++++++++++- 5 files changed, 299 insertions(+), 139 deletions(-) diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index 49edae785..b2c5f39d4 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -15,6 +15,7 @@ use std::{borrow::Cow, collections::HashMap, io::Cursor}; #[cfg(feature = "file_io")] use std::{fs::create_dir_all, path::Path}; +use async_generic::async_generic; use log::{debug, error}; #[cfg(feature = "json_schema")] use schemars::JsonSchema; @@ -22,8 +23,6 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; -#[cfg(feature = "file_io")] -use crate::AsyncSigner; use crate::{ assertion::{AssertionBase, AssertionData}, assertions::{ @@ -38,7 +37,8 @@ use crate::{ resource_store::{skip_serializing_resources, ResourceRef, ResourceStore}, salt::DefaultSalt, store::Store, - ClaimGeneratorInfo, HashRange, ManifestAssertionKind, RemoteSigner, Signer, SigningAlg, + AsyncSigner, ClaimGeneratorInfo, HashRange, ManifestAssertionKind, RemoteSigner, Signer, + SigningAlg, }; /// A Manifest represents all the information in a c2pa manifest @@ -1000,6 +1000,12 @@ impl Manifest { /// Embed a signed manifest into a stream using a supplied signer. /// returns the bytes of the manifest that was embedded + #[async_generic(async_signature( + &mut self, + format: &str, + asset: &[u8], + signer: &dyn AsyncSigner, + ))] pub fn embed_from_memory( &mut self, format: &str, @@ -1011,7 +1017,12 @@ impl Manifest { let asset = asset.to_vec(); let mut stream = std::io::Cursor::new(asset); let mut output_stream = Cursor::new(Vec::new()); - self.embed_to_stream(format, &mut stream, &mut output_stream, signer)?; + if _sync { + self.embed_to_stream(format, &mut stream, &mut output_stream, signer)?; + } else { + self.embed_to_stream_async(format, &mut stream, &mut output_stream, signer) + .await?; + } Ok(output_stream.into_inner()) } @@ -1037,6 +1048,13 @@ impl Manifest { /// Embed a signed manifest into a stream using a supplied signer. /// /// Returns the bytes of c2pa_manifest that was embedded. + #[async_generic(async_signature( + &mut self, + format: &str, + source: &mut dyn CAIRead, + dest: &mut dyn CAIReadWrite, + signer: &dyn AsyncSigner, + ))] pub fn embed_to_stream( &mut self, format: &str, @@ -1064,7 +1082,13 @@ impl Manifest { let mut store = self.to_store()?; // sign and write our store to to the output image file - store.save_to_stream(format, source, dest, signer) + if _sync { + store.save_to_stream(format, source, dest, signer) + } else { + store + .save_to_stream_async(format, source, dest, signer) + .await + } } /// Embed a signed manifest into a stream using a supplied signer. @@ -1914,6 +1938,45 @@ pub(crate) mod tests { //println!("{manifest_store}");main } + #[cfg(any(target_arch = "wasm32", feature = "openssl_sign"))] + #[cfg_attr(feature = "openssl_sign", actix::test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + async fn test_embed_from_memory_async() { + use crate::{assertions::User, utils::test::temp_async_signer}; + let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); + // convert buffer to cursor with Read/Write/Seek capability + let mut stream = std::io::Cursor::new(image.to_vec()); + // let mut image = image.to_vec(); + // let mut stream = std::io::Cursor::new(image.as_mut_slice()); + + let mut manifest = Manifest::new("my_app".to_owned()); + manifest.set_title("EmbedStream"); + manifest + .add_assertion(&User::new( + "org.contentauth.mylabel", + r#"{"my_tag":"Anything I want"}"#, + )) + .unwrap(); + + let signer = temp_async_signer(); + let mut output = Cursor::new(Vec::new()); + // Embed a manifest using the signer. + manifest + .embed_to_stream_async("jpeg", &mut stream, &mut output, signer.as_ref()) + .await + .expect("embed_stream"); + + let manifest_store = crate::ManifestStore::from_bytes("jpeg", &output.into_inner(), true) + .expect("from_bytes"); + assert_eq!( + manifest_store.get_active().unwrap().title().unwrap(), + "EmbedStream" + ); + #[cfg(feature = "add_thumbnails")] + assert!(manifest_store.get_active().unwrap().thumbnail().is_some()); + //println!("{manifest_store}");main + } + #[cfg(feature = "file_io")] #[actix::test] /// Verify that an ingredient with error is reported on the ingredient and not on the manifest_store diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs index 6b572ea59..87f7e8311 100644 --- a/sdk/src/signer.rs +++ b/sdk/src/signer.rs @@ -42,6 +42,10 @@ pub trait Signer { None } + fn timestamp_request_body(&self, message: &[u8]) -> Result> { + crate::time_stamp::default_rfc3161_message(message) + } + /// Request RFC 3161 timestamp to be included in the manifest data /// structure. /// @@ -51,10 +55,15 @@ pub trait Signer { /// provided by [`Self::time_authority_url()`], if any. #[cfg(not(target_arch = "wasm32"))] fn send_timestamp_request(&self, message: &[u8]) -> Option>> { - let headers: Option> = self.timestamp_request_headers(); - - self.time_authority_url() - .map(|url| crate::time_stamp::default_rfc3161_request(&url, headers, message)) + if let Some(url) = self.time_authority_url() { + if let Ok(body) = self.timestamp_request_body(message) { + let headers: Option> = self.timestamp_request_headers(); + return Some(crate::time_stamp::default_rfc3161_request( + &url, headers, &body, message, + )); + } + } + None } #[cfg(target_arch = "wasm32")] fn send_timestamp_request(&self, _message: &[u8]) -> Option>> { @@ -134,6 +143,10 @@ pub trait AsyncSigner: Sync { None } + fn timestamp_request_body(&self, message: &[u8]) -> Result> { + crate::time_stamp::default_rfc3161_message(message) + } + /// Request RFC 3161 timestamp to be included in the manifest data /// structure. /// @@ -145,10 +158,15 @@ pub trait AsyncSigner: Sync { async fn send_timestamp_request(&self, message: &[u8]) -> Option>> { // NOTE: This is currently synchronous, but may become // async in the future. - let headers: Option> = self.timestamp_request_headers(); - - self.time_authority_url() - .map(|url| crate::time_stamp::default_rfc3161_request(&url, headers, message)) + if let Some(url) = self.time_authority_url() { + if let Ok(body) = self.timestamp_request_body(message) { + let headers: Option> = self.timestamp_request_headers(); + return Some(crate::time_stamp::default_rfc3161_request( + &url, headers, &body, message, + )); + } + } + None } #[cfg(target_arch = "wasm32")] async fn send_timestamp_request(&self, message: &[u8]) -> Option>>; diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 75806fbac..510fc7250 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -18,10 +18,9 @@ use std::{ #[cfg(feature = "file_io")] use std::{fs, path::Path}; +use async_generic::async_generic; use log::error; -#[cfg(feature = "openssl")] -use crate::cose_validator::{verify_cose, verify_cose_async}; #[cfg(feature = "file_io")] use crate::jumbf_io::{ get_file_extension, get_supported_file_extension, load_jumbf_from_file, object_locations, @@ -40,7 +39,7 @@ use crate::{ }, claim::{Claim, ClaimAssertion, ClaimAssetData, RemoteManifest}, cose_sign::{cose_sign, cose_sign_async}, - cose_validator::check_ocsp_status, + cose_validator::{check_ocsp_status, verify_cose, verify_cose_async}, error::{Error, Result}, hash_utils::{hash_by_alg, vec_compare, verify_by_alg}, jumbf::{ @@ -491,68 +490,55 @@ impl Store { } /// Sign the claim and return signature. - pub fn sign_claim( + #[async_generic(async_signature( &self, claim: &Claim, - signer: &dyn Signer, + signer: &dyn AsyncSigner, box_size: usize, - ) -> Result> { - let claim_bytes = claim.data()?; - - cose_sign(signer, &claim_bytes, box_size).and_then(|sig| { - // Sanity check: Ensure that this signature is valid. - #[cfg(feature = "openssl")] - if let Ok(verify_after_sign) = get_settings_value::("verify.verify_after_sign") { - if verify_after_sign { - let mut cose_log = OneShotStatusTracker::new(); - if let Err(err) = verify_cose( - &sig, - &claim_bytes, - b"", - false, - self.trust_handler(), - &mut cose_log, - ) { - error!( - "Signature that was just generated does not validate: {:#?}", - err - ); - return Err(err); - } - } - } - Ok(sig) - }) - } - - /// Sign the claim asynchronously and return signature. - pub async fn sign_claim_async( + ))] + pub fn sign_claim( &self, claim: &Claim, - signer: &dyn AsyncSigner, + signer: &dyn Signer, box_size: usize, ) -> Result> { let claim_bytes = claim.data()?; - match cose_sign_async(signer, &claim_bytes, box_size).await { - // Sanity check: Ensure that this signature is valid. + let result = if _sync { + cose_sign(signer, &claim_bytes, box_size) + } else { + cose_sign_async(signer, &claim_bytes, box_size).await + }; + match result { Ok(sig) => { - #[cfg(feature = "openssl")] + // Sanity check: Ensure that this signature is valid. if let Ok(verify_after_sign) = get_settings_value::("verify.verify_after_sign") { if verify_after_sign { let mut cose_log = OneShotStatusTracker::new(); - if let Err(err) = verify_cose_async( - sig.clone(), - claim_bytes, - b"".to_vec(), - false, - self.trust_handler(), - &mut cose_log, - ) - .await - { + + let result = if _sync { + verify_cose( + &sig, + &claim_bytes, + b"", + false, + self.trust_handler(), + &mut cose_log, + ) + } else { + verify_cose_async( + sig.clone(), + claim_bytes, + b"".to_vec(), + false, + self.trust_handler(), + &mut cose_log, + ) + .await + }; + if let Err(err) = result { error!( "Signature that was just generated does not validate: {:#?}", err @@ -2032,56 +2018,20 @@ impl Store { /// When called, the stream should contain an asset matching format. /// on return, the stream will contain the new manifest signed with signer /// This directly modifies the asset in stream, backup stream first if you need to preserve it. - pub fn save_to_stream( + /// This can also handle remote signing if direct_cose_handling() is true. + #[async_generic(async_signature( &mut self, format: &str, input_stream: &mut dyn CAIRead, output_stream: &mut dyn CAIReadWrite, - signer: &dyn Signer, - ) -> Result> { - let intermediate_output: Vec = Vec::new(); - let mut intermediate_stream = Cursor::new(intermediate_output); - - let jumbf_bytes = self.start_save_stream( - format, - input_stream, - &mut intermediate_stream, - signer.reserve_size(), - )?; - - intermediate_stream.set_position(0); - let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; - let sig = self.sign_claim(pc, signer, signer.reserve_size())?; - let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); - - match self.finish_save_stream( - jumbf_bytes, - format, - &mut intermediate_stream, - output_stream, - sig, - &sig_placeholder, - ) { - Ok((s, m)) => { - // save sig so store is up to date - let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - pc_mut.set_signature_val(s); - - Ok(m) - } - Err(e) => Err(e), - } - } - - /// Async version of save_to_stream - /// - /// This can also handle remote signing if direct_cose_handling() is true - pub async fn save_to_stream_async( + signer: &dyn AsyncSigner, + ))] + pub fn save_to_stream( &mut self, format: &str, input_stream: &mut dyn CAIRead, output_stream: &mut dyn CAIReadWrite, - signer: &dyn AsyncSigner, + signer: &dyn Signer, ) -> Result> { let intermediate_output: Vec = Vec::new(); let mut intermediate_stream = Cursor::new(intermediate_output); @@ -2093,18 +2043,21 @@ impl Store { signer.reserve_size(), )?; + intermediate_stream.set_position(0); let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; - let claim_bytes = pc.data()?; - let sig = if signer.direct_cose_handling() { - // let the signer do all the COSE processing and return the structured COSE data - signer.sign(claim_bytes).await? + let sig = if _sync { + self.sign_claim(pc, signer, signer.reserve_size())? + } else if signer.direct_cose_handling() { + // Let the signer do all the COSE processing and return the structured COSE data. + // This replaces the RemoteSigner interface. + signer.sign(pc.data()?).await? } else { self.sign_claim_async(pc, signer, signer.reserve_size()) .await? }; let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); - intermediate_stream.set_position(0); + intermediate_stream.rewind()?; match self.finish_save_stream( jumbf_bytes, format, diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs index a76068597..8bdbf8ed6 100644 --- a/sdk/src/time_stamp.rs +++ b/sdk/src/time_stamp.rs @@ -173,13 +173,10 @@ fn time_stamp_request_http( /// This is a wrapper around [time_stamp_request_http] that constructs the low-level /// ASN.1 request object with reasonable defaults. -#[cfg(not(target_arch = "wasm32"))] pub(crate) fn time_stamp_message_http( - url: &str, - headers: Option>, message: &[u8], digest_algorithm: DigestAlgorithm, -) -> Result> { +) -> Result { use rand::{thread_rng, Rng}; let mut h = digest_algorithm.digester(); @@ -203,7 +200,7 @@ pub(crate) fn time_stamp_message_http( extensions: None, }; - time_stamp_request_http(url, headers, &request) + Ok(request) } pub struct TimeStampResponse(TimeStampResp); @@ -284,20 +281,34 @@ pub fn default_rfc3161_request( url: &str, headers: Option>, data: &[u8], + message: &[u8], ) -> Result> { - { - let ts = time_stamp_message_http( - url, - headers, - data, - x509_certificate::DigestAlgorithm::Sha256, - )?; - - // sanity check - verify_timestamp(&ts, data)?; - - Ok(ts) - } + use crate::asn1::rfc3161::TimeStampReq; + let request = Constructed::decode( + bcder::decode::SliceSource::new(data), + bcder::Mode::Der, + TimeStampReq::take_from, + ) + .map_err(|_err| Error::CoseTimeStampGeneration)?; + + let ts = time_stamp_request_http(url, headers, &request)?; + + // sanity check + verify_timestamp(&ts, message)?; + + Ok(ts) +} + +#[allow(unused_variables)] +pub fn default_rfc3161_message(data: &[u8]) -> Result> { + use bcder::encode::Values; + let request = time_stamp_message_http(data, x509_certificate::DigestAlgorithm::Sha256)?; + + let mut body = Vec::::new(); + request + .encode_ref() + .write_encoded(bcder::Mode::Der, &mut body)?; + Ok(body) } pub fn gt_to_datetime( @@ -401,16 +412,12 @@ impl Default for TstContainer { /// Wrap rfc3161 TimeStampRsp in COSE sigTst object pub fn make_cose_timestamp(ts_data: &[u8]) -> TstContainer { - if cfg!(feature = "openssl_sign") { - let token = TstToken { - val: ts_data.to_vec(), - }; + let token = TstToken { + val: ts_data.to_vec(), + }; - let mut container = TstContainer::new(); - container.add_token(token); + let mut container = TstContainer::new(); + container.add_token(token); - container - } else { - TstContainer::new() - } + container } diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs index f1f7fee6f..b17bafe9e 100644 --- a/sdk/src/utils/test.rs +++ b/sdk/src/utils/test.rs @@ -35,7 +35,10 @@ use crate::{ jumbf_io::get_assetio_handler_from_path, }; #[cfg(feature = "openssl_sign")] -use crate::{openssl::RsaSigner, signer::ConfigurableSigner}; +use crate::{ + openssl::{AsyncSignerAdapter, RsaSigner}, + signer::ConfigurableSigner, +}; pub const TEST_SMALL_JPEG: &str = "earth_apollo17.jpg"; @@ -346,6 +349,22 @@ pub(crate) fn temp_signer() -> Box { } } +#[cfg(any(target_arch = "wasm32", feature = "openssl_sign"))] +pub fn temp_async_signer() -> Box { + #[cfg(feature = "openssl_sign")] + { + Box::new(AsyncSignerAdapter::new(SigningAlg::Es256)) + } + + #[cfg(target_arch = "wasm32")] + { + let sign_cert = include_str!("../../tests/fixtures/certs/es256.pub"); + let pem_key = include_str!("../../tests/fixtures/certs/es256.pem"); + let signer = WebCryptoSigner::new("es256", sign_cert, pem_key); + Box::new(signer) + } +} + /// Create a [`Signer`] instance for a specific algorithm that can be used for testing purposes. /// /// # Returns @@ -405,6 +424,106 @@ impl crate::signer::RemoteSigner for TempRemoteSigner { } } +#[cfg(target_arch = "wasm32")] +struct WebCryptoSigner { + signing_alg: SigningAlg, + signing_alg_name: String, + certs: Vec>, + key: Vec, +} + +#[cfg(target_arch = "wasm32")] +impl WebCryptoSigner { + pub fn new(alg: &str, cert: &str, key: &str) -> Self { + static START_CERTIFICATE: &str = "-----BEGIN CERTIFICATE-----"; + static END_CERTIFICATE: &str = "-----END CERTIFICATE-----"; + static START_KEY: &str = "-----BEGIN PRIVATE KEY-----"; + static END_KEY: &str = "-----END PRIVATE KEY-----"; + + let mut name = alg.to_owned().to_uppercase(); + name.insert(2, '-'); + + let key = key + .replace("\n", "") + .replace(START_KEY, "") + .replace(END_KEY, ""); + let key = crate::utils::base64::decode(&key).unwrap(); + + let certs = cert + .replace("\n", "") + .replace(START_CERTIFICATE, "") + .split(END_CERTIFICATE) + .map(|x| crate::utils::base64::decode(x).unwrap()) + .collect(); + + Self { + signing_alg: alg.parse().unwrap(), + signing_alg_name: name, + certs, + key, + } + } +} + +#[cfg(target_arch = "wasm32")] +#[async_trait::async_trait(?Send)] +impl crate::signer::AsyncSigner for WebCryptoSigner { + fn alg(&self) -> SigningAlg { + self.signing_alg + } + + fn certs(&self) -> Result>> { + Ok(self.certs.clone()) + } + + async fn sign(&self, claim_bytes: Vec) -> crate::error::Result> { + use js_sys::{Array, Object, Reflect, Uint8Array}; + use wasm_bindgen_futures::JsFuture; + use web_sys::CryptoKey; + + use crate::wasm::context::WindowOrWorker; + let context = WindowOrWorker::new().unwrap(); + let crypto = context.subtle_crypto().unwrap(); + + let mut data = claim_bytes.clone(); + let promise = crypto + .digest_with_str_and_u8_array("SHA-256", &mut data) + .unwrap(); + let result = JsFuture::from(promise).await.unwrap(); + let mut digest = Uint8Array::new(&result).to_vec(); + + let key = Uint8Array::new_with_length(self.key.len() as u32); + key.copy_from(&self.key); + let usages = Array::new(); + usages.push(&"sign".into()); + let alg = Object::new(); + Reflect::set(&alg, &"name".into(), &"ECDSA".into()).unwrap(); + Reflect::set(&alg, &"namedCurve".into(), &"P-256".into()).unwrap(); + + let promise = crypto + .import_key_with_object("pkcs8", &key, &alg, true, &usages) + .unwrap(); + let key: CryptoKey = JsFuture::from(promise).await.unwrap().into(); + + let alg = Object::new(); + Reflect::set(&alg, &"name".into(), &"ECDSA".into()).unwrap(); + Reflect::set(&alg, &"hash".into(), &"SHA-256".into()).unwrap(); + let promise = crypto + .sign_with_object_and_u8_array(&alg, &key, &mut digest) + .unwrap(); + let result = JsFuture::from(promise).await.unwrap(); + Ok(Uint8Array::new(&result).to_vec()) + } + + fn reserve_size(&self) -> usize { + 10000 + } + + async fn send_timestamp_request(&self, _: &[u8]) -> Option>> { + None + } +} + /// Create a [`RemoteSigner`] instance that can be used for testing purposes. /// /// # Returns