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 0934fc170a935559e7e413a5e024561d780e9d3a
parent 86c2952146d99277f44303b82bfbcc43194196cb
Author: Dave Kozma <dkozma@adobe.com>
Date:   Fri, 15 Jul 2022 12:21:32 -0400

Use rsa crate for RSA-PSS verification in Wasm (#77)

* Use rsa crate for RSA-PSS verification in Wasm

* Remove support for `RSASSA-PKCS1-v1_5`, add better fixture naming

* Update dependencies

* Remove PEM from test fixtures

* Update rand dependency

* Update sha2 crate across the board
Diffstat:
Msdk/Cargo.toml | 21++++++++++++---------
Msdk/src/cose_validator.rs | 12++++++------
Msdk/src/error.rs | 6++++++
Msdk/src/manifest_store.rs | 22++++++++++++++++++++++
Msdk/src/wasm/webcrypto_validator.rs | 254++++++++++++++++++++++---------------------------------------------------------
Dsdk/tests/fixtures/data.data | 5-----
Asdk/tests/fixtures/data_ps256.data | 2++
Dsdk/tests/fixtures/key.data | 0
Asdk/tests/fixtures/key_ps256.data | 0
Dsdk/tests/fixtures/sig.data | 0
Asdk/tests/fixtures/sig_ps256.data | 0
11 files changed, 118 insertions(+), 204 deletions(-)

diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -59,7 +59,7 @@ serde_cbor = "0.11.1" serde_derive = "1.0.127" serde_json = "1.0.66" serde-transcode = "1.1.1" -sha2 = "0.9.5" +sha2 = "0.10.2" tempfile = "3.1.0" thiserror = ">= 1.0.20, < 1.0.32" time = ">= 0.2.23" @@ -78,18 +78,21 @@ xmp_toolkit = { version = "0.5.1", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] console_log = { version = "0.2", features = ["color"] } -getrandom = { version = "0.2.2", features = ["js"] } +getrandom = { version = "0.2.7", features = ["js"] } # We need to use the `inaccurate` flag here to ensure usage of the JavaScript Date API # to handle certificate timestamp checking correctly. -instant = { version = "0.1.0", features = ["wasm-bindgen", "inaccurate"] } -js-sys = "0.3.54" -serde-wasm-bindgen = "0.4.1" -wasm-bindgen = "0.2.77" -wasm-bindgen-futures = "0.4.27" -web-sys = { version = "0.3.54", features = ["Crypto", "SubtleCrypto", "CryptoKey", "Window", "WorkerGlobalScope"] } +instant = { version = "0.1.12", features = ["wasm-bindgen", "inaccurate"] } +js-sys = "0.3.58" +rand = "0.8.5" +rsa = "0.6.1" +serde-wasm-bindgen = "0.4.3" +spki = "0.6.0" +wasm-bindgen = "0.2.81" +wasm-bindgen-futures = "0.4.31" +web-sys = { version = "0.3.58", features = ["Crypto", "SubtleCrypto", "CryptoKey", "Window", "WorkerGlobalScope"] } [dev-dependencies] anyhow = "1.0.40" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.0" +wasm-bindgen-test = "0.3.31" diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs @@ -1000,9 +1000,9 @@ pub mod tests { fn test_verify_cose_good() { let validator = get_validator("ps256").unwrap(); - let sig_bytes = include_bytes!("../tests/fixtures/sig.data"); - let data_bytes = include_bytes!("../tests/fixtures/data.data"); - let key_bytes = include_bytes!("../tests/fixtures/key.data"); + let sig_bytes = include_bytes!("../tests/fixtures/sig_ps256.data"); + let data_bytes = include_bytes!("../tests/fixtures/data_ps256.data"); + let key_bytes = include_bytes!("../tests/fixtures/key_ps256.data"); assert!(validator .validate(sig_bytes, data_bytes, key_bytes) @@ -1037,9 +1037,9 @@ pub mod tests { fn test_verify_cose_bad() { let validator = get_validator("ps256").unwrap(); - let sig_bytes = include_bytes!("../tests/fixtures/sig.data"); - let data_bytes = include_bytes!("../tests/fixtures/data.data"); - let key_bytes = include_bytes!("../tests/fixtures/key.data"); + let sig_bytes = include_bytes!("../tests/fixtures/sig_ps256.data"); + let data_bytes = include_bytes!("../tests/fixtures/data_ps256.data"); + let key_bytes = include_bytes!("../tests/fixtures/key_ps256.data"); let mut bad_bytes = data_bytes.to_vec(); bad_bytes[0] = b'c'; diff --git a/sdk/src/error.rs b/sdk/src/error.rs @@ -136,6 +136,12 @@ pub enum Error { #[error("WASM verifier error")] WasmVerifier, + #[error("WASM RSA-PSS key import error: {0}")] + WasmRsaKeyImport(String), + + #[error("WASM RSA-PSS verification error")] + WasmRsaVerification, + #[error("WASM crypto key error")] WasmKey, diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs @@ -263,6 +263,28 @@ mod tests { assert!(manifest.time().is_some()); } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[ignore] + #[allow(dead_code)] + async fn manifest_report_image_wasm() { + let image_bytes = include_bytes!("../tests/fixtures/CA.jpg"); + + let manifest_store = + ManifestStore::from_bytes_async("image/jpeg", image_bytes.to_vec(), true) + .await + .unwrap(); + + assert!(!manifest_store.manifests.is_empty()); + assert!(manifest_store.active_label().is_some()); + assert!(manifest_store.get_active().is_some()); + assert!(!manifest_store.manifests().is_empty()); + assert!(manifest_store.validation_status().is_none()); + let manifest = manifest_store.get_active().unwrap(); + assert!(!manifest.ingredients().is_empty()); + assert_eq!(manifest.issuer().unwrap(), "C2PA Test Signing Cert"); + assert!(manifest.time().is_some()); + } + #[test] #[cfg(feature = "file_io")] fn manifest_report_from_file() { diff --git a/sdk/src/wasm/webcrypto_validator.rs b/sdk/src/wasm/webcrypto_validator.rs @@ -11,39 +11,18 @@ // specific language governing permissions and limitations under // each license. +use crate::utils::hash_utils::hash_by_alg; use crate::wasm::context::WindowOrWorker; use crate::{Error, Result}; use js_sys::{Array, ArrayBuffer, Object, Reflect, Uint8Array}; +use rsa::{BigUint, PaddingScheme, PublicKey, RsaPublicKey}; +use sha2::{Sha256, Sha384, Sha512}; +use spki::SubjectPublicKeyInfo; +use std::convert::TryFrom; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::{CryptoKey, SubtleCrypto}; -pub struct RsaHashedImportParams { - name: String, - hash: String, -} - -impl RsaHashedImportParams { - pub fn new(name: &str, hash: &str) -> Self { - RsaHashedImportParams { - name: name.to_owned(), - hash: hash.to_owned(), - } - } - - pub fn as_js_object(&self) -> Object { - let obj = Object::new(); - Reflect::set(&obj, &"name".into(), &self.name.clone().into()).expect("not valid name"); - - let inner_obj = Object::new(); - Reflect::set(&inner_obj, &"name".into(), &self.hash.clone().into()) - .expect("not valid name"); - - Reflect::set(&obj, &"hash".into(), &inner_obj).expect("not valid name"); - - obj - } -} - +use x509_parser::der_parser::ber::{parse_ber_sequence, BerObject}; pub struct EcKeyImportParams { name: String, named_curve: String, @@ -75,26 +54,6 @@ impl EcKeyImportParams { } } -pub struct RsaPssParams { - name: String, - salt_length: u32, -} - -impl RsaPssParams { - pub fn new(name: &str, salt_length: u32) -> Self { - RsaPssParams { - name: name.to_owned(), - salt_length, - } - } - - pub fn as_js_object(&self) -> Object { - let obj = Object::new(); - Reflect::set(&obj, &"name".into(), &self.name.clone().into()).expect("not valid name"); - Reflect::set(&obj, &"saltLength".into(), &self.salt_length.into()).expect("not valid name"); - obj - } -} pub struct EcdsaParams { name: String, hash: String, @@ -128,22 +87,6 @@ fn data_as_array_buffer(data: &[u8]) -> ArrayBuffer { typed_array.buffer() } -// Alternate salt length computation function for signed data that doesn't adhere to the conventional -// salt length in the RSA-PSS spec, which should equal the length of the hash function in bytes -fn alternate_salt_length(crypto_key: &CryptoKey, salt_len: &u32) -> Result<u32> { - let algo: Object = crypto_key - .algorithm() - .map_err(|_err| Error::WasmKey)? - .into(); - let key_size: f64 = js_sys::Reflect::get(&algo, &"modulusLength".into()) - .map_err(|_err| Error::WasmKey)? - .as_f64() - .ok_or(Error::WasmKey)? - .into(); - let key_byte_len: f32 = (key_size as f32 - 1.0) / 8.0; - Ok((key_byte_len.ceil() as u32) - salt_len - 2) -} - async fn crypto_is_verified( subtle_crypto: &SubtleCrypto, alg: &Object, @@ -163,6 +106,32 @@ async fn crypto_is_verified( Ok(result) } +// Conversion utility from num-bigint::BigUint (used by x509_parser) +// to num-bigint-dig::BigUint (used by rsa) +fn biguint_val(ber_object: &BerObject) -> BigUint { + ber_object + .as_biguint() + .map(|x| x.to_u32_digits()) + .map(BigUint::new) + .unwrap_or_default() +} + +fn pss_padding_from_hash(hash: &str, salt_len: &u32) -> Result<PaddingScheme> { + let salt_len = usize::try_from(salt_len.clone()) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + let rng = rand::thread_rng(); + + match hash { + "SHA-256" => Ok(PaddingScheme::new_pss_with_salt::<Sha256, _>(rng, salt_len)), + "SHA-384" => Ok(PaddingScheme::new_pss_with_salt::<Sha384, _>(rng, salt_len)), + "SHA-512" => Ok(PaddingScheme::new_pss_with_salt::<Sha512, _>(rng, salt_len)), + &_ => Err(Error::WasmRsaKeyImport(format!( + "Invalid PSS hash supplied for padding: {}", + hash + ))), + } +} + async fn async_validate( algo: String, hash: String, @@ -178,85 +147,29 @@ async fn async_validate( match algo.as_ref() { "RSA-PSS" => { - // Create key - let mut algorithm = RsaHashedImportParams::new(&algo, &hash).as_js_object(); - let key_array_buf = data_as_array_buffer(&pkey); - let usages = Array::new(); - usages.push(&"verify".into()); - - let promise = subtle_crypto - .import_key_with_object("spki", &key_array_buf, &algorithm, true, &usages) - .map_err(|_err| Error::WasmKey)?; - let crypto_key: CryptoKey = JsFuture::from(promise) - .await - .map_err(|_err| Error::WasmKey)? - .into(); - web_sys::console::debug_2(&"CryptoKey".into(), &crypto_key); - - // Create verifier - // WebCrypto requires us to pass in the salt length to validate the signature unlike some other implementations. - // Certain beta images don't use the conventional salt length in the RSA-PSS specification, which should equal - // the length of the output of the hash function in bytes. - // First, let's try to validate with the conventional salt length: - algorithm = RsaPssParams::new(&algo, salt_len).as_js_object(); - web_sys::console::debug_2( - &"Attempting verification with salt length".into(), - &salt_len.into(), - ); - let verified = crypto_is_verified( - &subtle_crypto, - &algorithm, - &crypto_key, - &sig_array_buf, - &data_array_buf, - ) - .await?; - if verified { - Ok(verified) - } else { - // If this doesn't work, we can try validating against an alternate salt length: - let salt_len = alternate_salt_length(&crypto_key, &salt_len)?; - web_sys::console::debug_2( - &"Attempting fallback verification with salt length".into(), - &salt_len.into(), - ); - algorithm = RsaPssParams::new(&algo, salt_len).as_js_object(); - crypto_is_verified( - &subtle_crypto, - &algorithm, - &crypto_key, - &sig_array_buf, - &data_array_buf, - ) - .await + let spki = SubjectPublicKeyInfo::try_from(pkey.as_ref()) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + let (_, seq) = parse_ber_sequence(spki.subject_public_key) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + let hashed_data = hash_by_alg(&hash, &data, None); + let modulus = biguint_val(&seq[0]); + let exp = biguint_val(&seq[1]); + let public_key = RsaPublicKey::new(modulus, exp) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + let padding = pss_padding_from_hash(&hash, &salt_len)?; + let result = public_key.verify(padding, &hashed_data, &sig); + + match result { + Ok(()) => Ok(true), + Err(err) => { + web_sys::console::debug_2( + &"RSA-PSS validation failed:".into(), + &err.to_string().into(), + ); + Ok(false) + } } } - "RSASSA-PKCS1-v1_5" => { - // Create Key - let algorithm = RsaHashedImportParams::new(&algo, &hash).as_js_object(); - let key_array_buf = data_as_array_buffer(&pkey); - let usages = Array::new(); - usages.push(&"verify".into()); - - let promise = subtle_crypto - .import_key_with_object("spki", &key_array_buf, &algorithm, true, &usages) - .map_err(|_err| Error::WasmKey)?; - let crypto_key: CryptoKey = JsFuture::from(promise) - .await - .map_err(|_err| Error::WasmKey)? - .into(); - web_sys::console::debug_2(&"CryptoKey".into(), &crypto_key); - - // Create verifier - crypto_is_verified( - &subtle_crypto, - &algorithm, - &crypto_key, - &sig_array_buf, - &data_array_buf, - ) - .await - } "ECDSA" => { // Create Key let named_curve = match hash.as_ref() { @@ -331,39 +244,6 @@ pub async fn validate_async(alg: &str, sig: &[u8], data: &[u8], pkey: &[u8]) -> ) .await } - "rs256" => { - async_validate( - "RSASSA-PKCS1-v1_5".to_string(), - "SHA-256".to_string(), - 0, - pkey.to_vec(), - sig.to_vec(), - data.to_vec(), - ) - .await - } - "rs384" => { - async_validate( - "RSASSA-PKCS1-v1_5".to_string(), - "SHA-384".to_string(), - 0, - pkey.to_vec(), - sig.to_vec(), - data.to_vec(), - ) - .await - } - "rs512" => { - async_validate( - "RSASSA-PKCS1-v1_5".to_string(), - "SHA-512".to_string(), - 0, - pkey.to_vec(), - sig.to_vec(), - data.to_vec(), - ) - .await - } "es256" => { async_validate( "ECDSA".to_string(), @@ -416,26 +296,32 @@ pub mod tests { #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[wasm_bindgen_test] - async fn test_async_verify_good() { + async fn test_async_verify_rsa_pss() { // PS signatures - let sig_bytes = include_bytes!("../../tests/fixtures/sig.data"); - let data_bytes = include_bytes!("../../tests/fixtures/data.data"); - let key_bytes = include_bytes!("../../tests/fixtures/key.data"); + let sig_bytes = include_bytes!("../../tests/fixtures/sig_ps256.data"); + let data_bytes = include_bytes!("../../tests/fixtures/data_ps256.data"); + let key_bytes = include_bytes!("../../tests/fixtures/key_ps256.data"); - let mut validated = validate_async("ps256", sig_bytes, data_bytes, key_bytes) + let validated = validate_async("ps256", sig_bytes, data_bytes, key_bytes) .await .unwrap(); assert_eq!(validated, true); + } + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[wasm_bindgen_test] + async fn test_async_verify_ecdsa() { // EC signatures let sig_es384_bytes = include_bytes!("../../tests/fixtures/sig_es384.data"); let data_es384_bytes = include_bytes!("../../tests/fixtures/data_es384.data"); let key_es384_bytes = include_bytes!("../../tests/fixtures/key_es384.data"); - validated = validate_async("es384", sig_es384_bytes, data_es384_bytes, key_es384_bytes) - .await - .unwrap(); + let mut validated = + validate_async("es384", sig_es384_bytes, data_es384_bytes, key_es384_bytes) + .await + .unwrap(); assert_eq!(validated, true); @@ -465,9 +351,9 @@ pub mod tests { #[wasm_bindgen_test] #[ignore] async fn test_async_verify_bad() { - let sig_bytes = include_bytes!("../../tests/fixtures/sig.data"); - let data_bytes = include_bytes!("../../tests/fixtures/data.data"); - let key_bytes = include_bytes!("../../tests/fixtures/key.data"); + let sig_bytes = include_bytes!("../../tests/fixtures/sig_ps256.data"); + let data_bytes = include_bytes!("../../tests/fixtures/data_ps256.data"); + let key_bytes = include_bytes!("../../tests/fixtures/key_ps256.data"); let mut bad_bytes = data_bytes.to_vec(); bad_bytes[0] = b'c'; diff --git a/sdk/tests/fixtures/data.data b/sdk/tests/fixtures/data.data @@ -1,4 +0,0 @@ -jSignature1D8$@Y֦idc:format`jinstanceID`oclaim_generatoroadobe unit testisignaturexRself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.signaturejassertionscurl x`self#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.actionscalgfsha256dhashX WKVn|jO#;~a/*O+curl xcself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.cloud-datacalgfsha256dhashX Ѕ@}4&snxcV -HP2#curl xgself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.location.broadcalgfsha256dhashX ku}kB -r^:d$+KO{pCxEcurl xiself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.location.precisecalgfsha256dhashX $s`}m -Q!%\Kpcurl xrself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.thumbnail.ingredient.jpegcalgfsha256dhashX S1RCyH%DZFȽZq>쭣curl xuself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.thumbnail.ingredient__1.jpegcalgfsha256dhashX S1RCyH%DZFȽZq>쭣curl xuself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.thumbnail.ingredient__2.jpegcalgfsha256dhashX S1RCyH%DZFȽZq>쭣curl xbself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.hash.datacalgfsha256dhashX brN>)c7pa[רHf)lcalgfsha256 -\ No newline at end of file diff --git a/sdk/tests/fixtures/data_ps256.data b/sdk/tests/fixtures/data_ps256.data @@ -0,0 +1 @@ +jSignature1D8$@YChdc:titleeC.jpgidc:formatjimage/jpegjinstanceIDx,xmp:iid:fc3529a4-92e0-4786-af3c-13d124303649oclaim_generatorx$make_test_images/0.7.0 c2pa-rs/0.7.0isignaturexself#jumbf=c2pa.signaturejassertionscurlx4self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpegdhashX B>@rgW\DN9ip竫*p:ޢcurlx7self#jumbf=c2pa.assertions/stds.schema-org.CreativeWorkdhashX Hcmry?1~[~g¥!J¯rGcurlx'self#jumbf=c2pa.assertions/c2pa.actionsdhashX it+x!"NXIiks ʯcurlx)self#jumbf=c2pa.assertions/c2pa.hash.datadhashX I LNOPfeAvcalgfsha256 +\ No newline at end of file diff --git a/sdk/tests/fixtures/key.data b/sdk/tests/fixtures/key.data Binary files differ. diff --git a/sdk/tests/fixtures/key_ps256.data b/sdk/tests/fixtures/key_ps256.data Binary files differ. diff --git a/sdk/tests/fixtures/sig.data b/sdk/tests/fixtures/sig.data Binary files differ. diff --git a/sdk/tests/fixtures/sig_ps256.data b/sdk/tests/fixtures/sig_ps256.data Binary files differ.