commit 5160b6fad73947fe0490ba363c9b052eed1d1ffc
parent c017f933d2e6747839ada386c189dabbae96134e
Author: Prakash Duggaraju <duggaraju@gmail.com>
Date: Wed, 1 May 2024 15:31:45 -0700
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
Diffstat:
5 files changed, 299 insertions(+), 139 deletions(-)
diff --git 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
@@ -42,6 +42,10 @@ pub trait Signer {
None
}
+ fn timestamp_request_body(&self, message: &[u8]) -> Result<Vec<u8>> {
+ 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<Result<Vec<u8>>> {
- let headers: Option<Vec<(String, String)>> = 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<Vec<(String, String)>> = 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<Result<Vec<u8>>> {
@@ -134,6 +143,10 @@ pub trait AsyncSigner: Sync {
None
}
+ fn timestamp_request_body(&self, message: &[u8]) -> Result<Vec<u8>> {
+ 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<Result<Vec<u8>>> {
// NOTE: This is currently synchronous, but may become
// async in the future.
- let headers: Option<Vec<(String, String)>> = 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<Vec<(String, String)>> = 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<Result<Vec<u8>>>;
diff --git 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<Vec<u8>> {
- 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::<bool>("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<Vec<u8>> {
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::<bool>("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<Vec<u8>> {
- let intermediate_output: Vec<u8> = 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<Vec<u8>> {
let intermediate_output: Vec<u8> = 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
@@ -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<Vec<(String, String)>>,
message: &[u8],
digest_algorithm: DigestAlgorithm,
-) -> Result<Vec<u8>> {
+) -> Result<crate::asn1::rfc3161::TimeStampReq> {
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<Vec<(String, String)>>,
data: &[u8],
+ message: &[u8],
) -> Result<Vec<u8>> {
- {
- 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<Vec<u8>> {
+ use bcder::encode::Values;
+ let request = time_stamp_message_http(data, x509_certificate::DigestAlgorithm::Sha256)?;
+
+ let mut body = Vec::<u8>::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
@@ -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<dyn Signer> {
}
}
+#[cfg(any(target_arch = "wasm32", feature = "openssl_sign"))]
+pub fn temp_async_signer() -> Box<dyn crate::signer::AsyncSigner> {
+ #[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<Vec<u8>>,
+ key: Vec<u8>,
+}
+
+#[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<Vec<Vec<u8>>> {
+ Ok(self.certs.clone())
+ }
+
+ async fn sign(&self, claim_bytes: Vec<u8>) -> crate::error::Result<Vec<u8>> {
+ 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<Result<Vec<u8>>> {
+ None
+ }
+}
+
/// Create a [`RemoteSigner`] instance that can be used for testing purposes.
///
/// # Returns