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:
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.