diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml index ca4b88640..ebae7c119 100644 --- a/make_test_images/Cargo.toml +++ b/make_test_images/Cargo.toml @@ -28,7 +28,7 @@ nom = "7.1.3" regex = "1.5.6" serde = "1.0.197" serde_json = { version = "1.0.117", features = ["preserve_order"] } -tempfile = "3.10.1" +tempfile = "3.15.0" [features] # prevents these features from being always enabled in the workspace diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index bfe81113b..0b2ef29f7 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -78,10 +78,7 @@ byteorder = { version = "1.4.3", default-features = false } byteordered = "0.6.0" c2pa-crypto = { path = "../internal/crypto", version = "0.3.0" } c2pa-status-tracker = { path = "../internal/status-tracker", version = "0.3.0" } -chrono = { version = "0.4.39", default-features = false, features = [ - "serde", - "wasmbind", -] } +chrono = { version = "0.4.39", default-features = false, features = ["serde"] } ciborium = "0.2.2" config = { version = "0.14.0", default-features = false, features = [ "json", @@ -124,30 +121,46 @@ serde_with = "3.11.0" serde-transcode = "1.1.1" sha1 = "0.10.6" sha2 = "0.10.6" -tempfile = "3.10.1" +tempfile = "3.15" thiserror = "2.0.8" treeline = "0.1.0" url = "2.5.3" -uuid = { version = "1.10.0", features = ["serde", "v4", "js"] } +uuid = { version = "1.10.0", features = ["serde", "v4"] } +x509-certificate = "0.23.1" x509-parser = "0.16.0" -x509-certificate = "0.21.0" zip = { version = "2.2.1", default-features = false } +[target.'cfg(target_arch = "wasm32")'.dependencies] +rsa = { version = "0.9.6", features = ["sha2"] } +spki = "0.7.3" + +[target.'cfg(target_env = "p2")'.dependencies] +tempfile = { version = "3.15", features = ["nightly"] } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ureq = "2.4.0" + +[target.'cfg(any(target_os = "wasi", not(target_arch = "wasm32")))'.dependencies] image = { version = "0.24.7", default-features = false, features = [ "jpeg", "png", ], optional = true } -[target.'cfg(target_arch = "wasm32")'.dependencies] +[target.'cfg(target_os = "wasi")'.dependencies] +getrandom = "0.2.7" + + +[target.'cfg(all(target_arch = "wasm32",not(target_os = "wasi")))'.dependencies] +chrono = { version = "0.4.39", default-features = false, features = [ + "serde", + "wasmbind", +] } console_log = { version = "1.0.0", features = ["color"] } getrandom = { version = "0.2.7", features = ["js"] } js-sys = "0.3.58" rand_core = "0.9.0-alpha.2" -rsa = { version = "0.9.6", features = ["sha2"] } serde-wasm-bindgen = "0.6.5" -spki = "0.7.3" +uuid = { version = "1.10.0", features = ["serde", "v4", "js"] } wasm-bindgen = "0.2.83" wasm-bindgen-futures = "0.4.31" web-sys = { version = "0.3.58", features = [ @@ -168,8 +181,8 @@ hex-literal = "0.4.1" jumbf = "0.4.0" mockall = "0.13.1" -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.31" +[target.'cfg(all(target_arch = "wasm32",not(target_os = "wasi")))'.dev-dependencies] +wasm-bindgen-test = "0.3.45" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] actix = "0.13.1" diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs index e993799e5..20b5ac697 100644 --- a/sdk/src/asset_handlers/jpeg_io.rs +++ b/sdk/src/asset_handlers/jpeg_io.rs @@ -1125,7 +1125,7 @@ pub mod tests { use std::io::{Read, Seek}; - #[cfg(target_arch = "wasm32")] + #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use wasm_bindgen_test::*; use super::*; @@ -1223,7 +1223,10 @@ pub mod tests { } #[cfg_attr(not(target_arch = "wasm32"), actix::test)] - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr( + all(target_arch = "wasm32", not(target_os = "wasi")), + wasm_bindgen_test + )] async fn test_xmp_read_write_stream() { let source_bytes = include_bytes!("../../tests/fixtures/CA.jpg"); diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 55cb5eb26..53621c4ea 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1081,6 +1081,7 @@ impl Builder { mod tests { #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] + #![cfg(not(target_os = "wasi"))] use std::io::Cursor; use c2pa_crypto::raw_signature::SigningAlg; diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs index 093d53bf0..d94e73c02 100644 --- a/sdk/src/ingredient.rs +++ b/sdk/src/ingredient.rs @@ -1451,6 +1451,7 @@ impl IngredientOptions for DefaultOptions { mod tests { #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] + #![cfg(not(target_os = wasi))] #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index 800b6e956..e6884939c 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -1501,6 +1501,7 @@ impl SignatureInfo { pub(crate) mod tests { #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] + #![cfg(not(target_os = wasi))] use std::io::Cursor; diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs index bcdb8d525..6509c6983 100644 --- a/sdk/src/manifest_store.rs +++ b/sdk/src/manifest_store.rs @@ -584,6 +584,7 @@ impl std::fmt::Display for ManifestStore { mod tests { #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] + #![cfg(not(target_os = wasi))] use c2pa_status_tracker::OneShotStatusTracker; #[cfg(target_arch = "wasm32")] diff --git a/sdk/src/salt.rs b/sdk/src/salt.rs index 09aa1e3c8..6e4eec406 100644 --- a/sdk/src/salt.rs +++ b/sdk/src/salt.rs @@ -55,11 +55,11 @@ impl Default for DefaultSalt { impl SaltGenerator for DefaultSalt { fn generate_salt(&self) -> Option> { - #[cfg(target_arch = "wasm32")] + #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] { Some(crate::wasm::util::get_random_values(self.salt_len).ok()?) } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] { use rand::prelude::*; diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs index d14a6b3bd..bdb913e7e 100644 --- a/sdk/src/utils/test.rs +++ b/sdk/src/utils/test.rs @@ -479,7 +479,7 @@ impl WebCryptoSigner { } } -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] #[async_trait::async_trait(?Send)] impl AsyncSigner for WebCryptoSigner { fn alg(&self) -> SigningAlg { diff --git a/sdk/src/wasm/mod.rs b/sdk/src/wasm/mod.rs index 2b54e20cd..489733ba7 100644 --- a/sdk/src/wasm/mod.rs +++ b/sdk/src/wasm/mod.rs @@ -11,5 +11,5 @@ // specific language governing permissions and limitations under // each license. -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] pub(crate) mod util; diff --git a/sdk/src/wasm/wasicrypto_validator.rs b/sdk/src/wasm/wasicrypto_validator.rs new file mode 100644 index 000000000..adff88fc7 --- /dev/null +++ b/sdk/src/wasm/wasicrypto_validator.rs @@ -0,0 +1,439 @@ +// Copyright 2022 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + +use std::convert::TryFrom; + +use async_generic::async_generic; +use spki::{DecodePublicKey, SubjectPublicKeyInfoRef}; +use x509_parser::der_parser::ber::{parse_ber_sequence, BerObject}; + +use crate::{Error, Result, SigningAlg}; + +// Conversion utility from num-bigint::BigUint (used by x509_parser) +// to num-bigint-dig::BigUint (used by rsa) +fn biguint_val(ber_object: &BerObject) -> rsa::BigUint { + ber_object + .as_biguint() + .map(|x| x.to_u32_digits()) + .map(rsa::BigUint::new) + .unwrap_or_default() +} + +// Validate an Ed25519 signature for the provided data. The pkey must +// be the raw bytes representing CompressedEdwardsY. The length must 32 bytes. +fn ed25519_validate(sig: Vec, data: Vec, pkey: Vec) -> Result { + use ed25519_dalek::{Signature, Verifier, VerifyingKey, PUBLIC_KEY_LENGTH}; + + if pkey.len() == PUBLIC_KEY_LENGTH { + let ed_sig = Signature::from_slice(&sig).map_err(|_| Error::CoseInvalidCert)?; + + // convert to VerifyingKey + let mut cert_slice: [u8; 32] = Default::default(); + cert_slice.copy_from_slice(&pkey[0..PUBLIC_KEY_LENGTH]); + + let vk = VerifyingKey::from_bytes(&cert_slice).map_err(|_| Error::CoseInvalidCert)?; + + match vk.verify(&data, &ed_sig) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + } else { + /* + web_sys::console::debug_2( + &"Ed25519 public key incorrect length: ".into(), + &pkey.len().to_string().into(), + ); + */ + Err(Error::CoseInvalidCert) + } +} + +pub(crate) fn validate_signature( + algo: String, + hash: String, + _salt_len: u32, + pkey: Vec, + sig: Vec, + data: Vec, +) -> Result { + use rsa::{ + sha2::{Sha256, Sha384, Sha512}, + RsaPublicKey, + }; + + match algo.as_ref() { + "RSASSA-PKCS1-v1_5" => { + use rsa::{pkcs1v15::Signature, signature::Verifier}; + + // used for certificate validation + let spki = SubjectPublicKeyInfoRef::try_from(pkey.as_ref()) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + + let (_, seq) = parse_ber_sequence(spki.subject_public_key.raw_bytes()) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + + 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 normalized_hash = hash.clone().replace("-", "").to_lowercase(); + + let result = match normalized_hash.as_ref() { + "sha256" => { + let vk = rsa::pkcs1v15::VerifyingKey::::new(public_key); + let signature: Signature = sig.as_slice().try_into().map_err(|_e| { + Error::WasmRsaKeyImport("could no process RSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + "sha384" => { + let vk = rsa::pkcs1v15::VerifyingKey::::new(public_key); + let signature: Signature = sig.as_slice().try_into().map_err(|_e| { + Error::WasmRsaKeyImport("could no process RSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + "sha512" => { + let vk = rsa::pkcs1v15::VerifyingKey::::new(public_key); + let signature: Signature = sig.as_slice().try_into().map_err(|_e| { + Error::WasmRsaKeyImport("could no process RSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + _ => return Err(Error::UnknownAlgorithm), + }; + + match result { + Ok(()) => { + //web_sys::console::debug_1(&"RSA validation success:".into()); + Ok(true) + } + Err(err) => { + /* + web_sys::console::debug_2( + &"RSA validation failed:".into(), + &err.to_string().into(), + ); + */ + Ok(false) + } + } + } + "RSA-PSS" => { + use rsa::{pss::Signature, signature::Verifier}; + + let spki = SubjectPublicKeyInfoRef::try_from(pkey.as_ref()) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + + let (_, seq) = parse_ber_sequence(&spki.subject_public_key.raw_bytes()) + .map_err(|err| Error::WasmRsaKeyImport(err.to_string()))?; + + // We need to normalize this from SHA-256 (the format WebCrypto uses) to sha256 + // (the format the util function expects) so that it maps correctly + let normalized_hash = hash.clone().replace("-", "").to_lowercase(); + 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 result = match normalized_hash.as_ref() { + "sha256" => { + let vk = rsa::pss::VerifyingKey::::new(public_key); + let signature: Signature = sig.as_slice().try_into().map_err(|_e| { + Error::WasmRsaKeyImport("could no process RSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + "sha384" => { + let vk = rsa::pss::VerifyingKey::::new(public_key); + let signature: Signature = sig.as_slice().try_into().map_err(|_e| { + Error::WasmRsaKeyImport("could no process RSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + "sha512" => { + let vk = rsa::pss::VerifyingKey::::new(public_key); + let signature: Signature = sig.as_slice().try_into().map_err(|_e| { + Error::WasmRsaKeyImport("could no process RSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + _ => return Err(Error::UnknownAlgorithm), + }; + + match result { + Ok(()) => { + //web_sys::console::debug_1(&"RSA-PSS validation success:".into()); + Ok(true) + } + Err(err) => { + /* + web_sys::console::debug_2( + &"RSA-PSS validation failed:".into(), + &err.to_string().into(), + ); + */ + Ok(false) + } + } + } + "ECDSA" => { + use ecdsa::{signature::Verifier as EcdsaVerifier, Signature as EcdsaSignature}; + use p256::ecdsa::VerifyingKey as P256VerifyingKey; + use p384::ecdsa::VerifyingKey as P384VerifyingKey; + let result = match hash.as_ref() { + "SHA-256" => { + let vk = P256VerifyingKey::from_public_key_der(&pkey) + .map_err(|_| Error::WasmRsaKeyImport("Invalid P-256 key".to_string()))?; + let signature = EcdsaSignature::from_slice(&sig).map_err(|_| { + Error::WasmRsaKeyImport("Invalid ECDSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + "SHA-384" => { + let vk = P384VerifyingKey::from_public_key_der(&pkey) + .map_err(|_| Error::WasmRsaKeyImport("Invalid P-384 key".to_string()))?; + let signature = EcdsaSignature::from_slice(&sig).map_err(|_| { + Error::WasmRsaKeyImport("Invalid ECDSA signature".to_string()) + })?; + vk.verify(&data, &signature) + } + _ => return Err(Error::UnknownAlgorithm), + }; + + match result { + Ok(_) => Ok(true), + Err(err) => { + /* + web_sys::console::debug_2( + &"ECDSA validation failed:".into(), + &err.to_string().into(), + ); + */ + Ok(false) + } + } + } + "ED25519" => { + use x509_parser::{prelude::*, public_key::PublicKey}; + + // pull out raw Ed code points + if let Ok((_, certificate_public_key)) = SubjectPublicKeyInfo::from_der(&pkey) { + match certificate_public_key.parsed() { + Ok(key) => match key { + PublicKey::Unknown(raw_key) => { + ed25519_validate(sig, data, raw_key.to_vec()) + } + _ => Err(Error::OtherError( + "could not unwrap Ed25519 public key".into(), + )), + }, + Err(_) => Err(Error::OtherError( + "could not recognize Ed25519 public key".into(), + )), + } + } else { + Err(Error::OtherError( + "could not parse Ed25519 public key".into(), + )) + } + } + _ => Err(Error::UnsupportedType), + } +} + +// This interface is called from CoseValidator. RSA validation not supported here. +#[async_generic] +pub fn validate(alg: SigningAlg, sig: &[u8], data: &[u8], pkey: &[u8]) -> Result { + //web_sys::console::debug_2(&"Validating with algorithm".into(), &alg.to_string().into()); + + match alg { + SigningAlg::Ps256 => validate_signature( + "RSA-PSS".to_string(), + "SHA-256".to_string(), + 32, + pkey.to_vec(), + sig.to_vec(), + data.to_vec(), + ), + SigningAlg::Ps384 => validate_signature( + "RSA-PSS".to_string(), + "SHA-384".to_string(), + 48, + pkey.to_vec(), + sig.to_vec(), + data.to_vec(), + ), + SigningAlg::Ps512 => validate_signature( + "RSA-PSS".to_string(), + "SHA-512".to_string(), + 64, + pkey.to_vec(), + sig.to_vec(), + data.to_vec(), + ), + // "rs256" => { + // validate_signature( + // "RSASSA-PKCS1-v1_5".to_string(), + // "SHA-256".to_string(), + // 0, + // pkey.to_vec(), + // sig.to_vec(), + // data.to_vec(), + // ) + // .await + // } + // "rs384" => { + // validate_signature( + // "RSASSA-PKCS1-v1_5".to_string(), + // "SHA-384".to_string(), + // 0, + // pkey.to_vec(), + // sig.to_vec(), + // data.to_vec(), + // ) + // .await + // } + // "rs512" => { + // validate_signature( + // "RSASSA-PKCS1-v1_5".to_string(), + // "SHA-512".to_string(), + // 0, + // pkey.to_vec(), + // sig.to_vec(), + // data.to_vec(), + // ) + // .await + // } + SigningAlg::Es256 => validate_signature( + "ECDSA".to_string(), + "SHA-256".to_string(), + 0, + pkey.to_vec(), + sig.to_vec(), + data.to_vec(), + ), + SigningAlg::Es384 => validate_signature( + "ECDSA".to_string(), + "SHA-384".to_string(), + 0, + pkey.to_vec(), + sig.to_vec(), + data.to_vec(), + ), + SigningAlg::Es512 => validate_signature( + "ECDSA".to_string(), + "SHA-512".to_string(), + 0, + pkey.to_vec(), + sig.to_vec(), + data.to_vec(), + ), + SigningAlg::Ed25519 => validate_signature( + "ED25519".to_string(), + "SHA-512".to_string(), + 0, + pkey.to_vec(), + sig.to_vec(), + data.to_vec(), + ), + } +} + +#[cfg(test)] +pub mod tests { + #![allow(clippy::unwrap_used)] + + use super::*; + use crate::SigningAlg; + + async fn test_async_verify_rsa_pss() { + // PS signatures + 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 validated = validate_async(SigningAlg::Ps256, sig_bytes, data_bytes, key_bytes) + .await + .unwrap(); + + assert_eq!(validated, true); + } + + 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"); + + let mut validated = validate_async( + SigningAlg::Es384, + sig_es384_bytes, + data_es384_bytes, + key_es384_bytes, + ) + .await + .unwrap(); + + assert_eq!(validated, true); + + let sig_es512_bytes = include_bytes!("../../tests/fixtures/sig_es512.data"); + let data_es512_bytes = include_bytes!("../../tests/fixtures/data_es512.data"); + let key_es512_bytes = include_bytes!("../../tests/fixtures/key_es512.data"); + + validated = validate_async( + SigningAlg::Es512, + sig_es512_bytes, + data_es512_bytes, + key_es512_bytes, + ) + .await + .unwrap(); + + assert_eq!(validated, true); + + let sig_es256_bytes = include_bytes!("../../tests/fixtures/sig_es256.data"); + let data_es256_bytes = include_bytes!("../../tests/fixtures/data_es256.data"); + let key_es256_bytes = include_bytes!("../../tests/fixtures/key_es256.data"); + + let validated = validate_async( + SigningAlg::Es256, + sig_es256_bytes, + data_es256_bytes, + key_es256_bytes, + ) + .await + .unwrap(); + + assert_eq!(validated, true); + } + + #[ignore] + async fn test_async_verify_bad() { + 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'; + bad_bytes[1] = b'2'; + bad_bytes[2] = b'p'; + bad_bytes[3] = b'a'; + + let validated = validate_async(SigningAlg::Ps256, sig_bytes, &bad_bytes, key_bytes) + .await + .unwrap(); + + assert_eq!(validated, false); + } +} diff --git a/sdk/src/wasm/wasipki_trust_handler.rs b/sdk/src/wasm/wasipki_trust_handler.rs new file mode 100644 index 000000000..9c2743db0 --- /dev/null +++ b/sdk/src/wasm/wasipki_trust_handler.rs @@ -0,0 +1,672 @@ +// Copyright 2023 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + +use std::{ + collections::HashSet, + io::{BufRead, BufReader, Cursor, Read}, + str::FromStr, +}; + +use asn1_rs::{nom::AsBytes, Any, Class, Header, Tag}; +use async_generic::async_generic; +use x509_parser::{ + der_parser::der::{parse_der_integer, parse_der_sequence_of}, + prelude::*, +}; + +use crate::{ + cose_validator::*, + error::{Error, Result}, + hash_utils::{hash_sha256, vec_compare}, + trust_handler::{ + has_allowed_oid, load_eku_configuration, load_trust_from_data, TrustHandlerConfig, + }, + utils::base64, + wasm::wasicrypto_validator::validate_signature, + SigningAlg, +}; + +// Struct to handle verification of trust chains +pub(crate) struct WasiTrustHandlerConfig { + pub trust_anchors: Vec>, + pub private_anchors: Vec>, + allowed_cert_set: HashSet, + config_store: Vec, +} + +impl std::fmt::Debug for WasiTrustHandlerConfig { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{} trust anchors, {} private anchors.", + self.trust_anchors.len(), + self.private_anchors.len() + ) + } +} + +impl WasiTrustHandlerConfig { + pub fn load_default_trust(&mut self) -> Result<()> { + // load config store + let config = include_bytes!("./store.cfg"); + let mut config_reader = Cursor::new(config); + self.load_configuration(&mut config_reader)?; + + // load debug/test private trust anchors + if cfg!(test) { + let pa = include_bytes!("./test_cert_root_bundle.pem"); + let mut pa_reader = Cursor::new(pa); + + self.append_private_trust_data(&mut pa_reader)?; + } + + Ok(()) + } +} + +impl TrustHandlerConfig for WasiTrustHandlerConfig { + fn new() -> Self { + let mut th = WasiTrustHandlerConfig { + trust_anchors: Vec::new(), + private_anchors: Vec::new(), + allowed_cert_set: HashSet::new(), + config_store: Vec::new(), + }; + + if th.load_default_trust().is_err() { + th.clear(); // just use empty trust handler to fail automatically + } + + th + } + + // add trust anchors + fn load_trust_anchors_from_data(&mut self, trust_data_reader: &mut dyn Read) -> Result<()> { + let mut trust_data = Vec::new(); + trust_data_reader.read_to_end(&mut trust_data)?; + + let mut anchors = load_trust_from_data(&trust_data)?; + self.trust_anchors.append(&mut anchors); + Ok(()) + } + + // append private trust anchors + fn append_private_trust_data(&mut self, private_anchors_reader: &mut dyn Read) -> Result<()> { + let mut private_anchors_data = Vec::new(); + private_anchors_reader.read_to_end(&mut private_anchors_data)?; + + let mut anchors = load_trust_from_data(&private_anchors_data)?; + self.private_anchors.append(&mut anchors); + + Ok(()) + } + + fn clear(&mut self) { + self.trust_anchors = Vec::new(); + self.private_anchors = Vec::new(); + } + + // load EKU configuration + fn load_configuration(&mut self, config_data: &mut dyn Read) -> Result<()> { + config_data.read_to_end(&mut self.config_store)?; + Ok(()) + } + + // list off auxillary allowed EKU Oid + fn get_auxillary_ekus(&self) -> Vec { + let mut oids = Vec::new(); + if let Ok(oid_strings) = load_eku_configuration(&mut Cursor::new(&self.config_store)) { + for oid_str in &oid_strings { + if let Ok(oid) = asn1_rs::Oid::from_str(oid_str) { + oids.push(oid); + } + } + } + oids + } + + fn get_anchors(&self) -> Vec> { + let mut anchors = Vec::new(); + + anchors.append(&mut self.trust_anchors.clone()); + anchors.append(&mut self.private_anchors.clone()); + + anchors + } + + // add allowed list entries + fn load_allowed_list(&mut self, allowed_list: &mut dyn Read) -> Result<()> { + let mut buffer = Vec::new(); + allowed_list.read_to_end(&mut buffer)?; + + if let Ok(cert_list) = load_trust_from_data(&buffer) { + for cert_der in &cert_list { + let cert_sha256 = hash_sha256(cert_der); + let cert_hash_base64 = base64::encode(&cert_sha256); + + self.allowed_cert_set.insert(cert_hash_base64); + } + } + + // try to load the of base64 encoded encoding of the sha256 hash of the certificate DER encoding + let reader = Cursor::new(buffer); + let buf_reader = BufReader::new(reader); + + let mut inside_cert_block = false; + for l in buf_reader.lines().flatten() { + if l.contains("-----BEGIN") { + inside_cert_block = true; + } + if l.contains("-----END") { + inside_cert_block = false; + } + + // sanity check that data is base64 encoded and outside of certificate block + if !inside_cert_block && base64::decode(&l).is_ok() && !l.is_empty() { + self.allowed_cert_set.insert(l); + } + } + + Ok(()) + } + + // set of allowed cert hashes + fn get_allowed_list(&self) -> &HashSet { + &self.allowed_cert_set + } +} + +fn find_allowed_eku<'a>( + cert_der: &'a [u8], + allowed_ekus: &'a [asn1_rs::Oid<'a>], +) -> Option<&'a asn1_rs::Oid<'a>> { + if let Ok((_rem, cert)) = X509Certificate::from_der(cert_der) { + if let Ok(Some(eku)) = cert.extended_key_usage() { + if let Some(o) = has_allowed_oid(eku.value, allowed_ekus) { + return Some(o); + } + } + } + None +} +fn cert_signing_alg(cert: &x509_parser::certificate::X509Certificate) -> Option { + let cert_alg = cert.signature_algorithm.algorithm.clone(); + + let signing_alg = if cert_alg == SHA256_WITH_RSAENCRYPTION_OID { + "rsa256".to_string() + } else if cert_alg == SHA384_WITH_RSAENCRYPTION_OID { + "rsa384".to_string() + } else if cert_alg == SHA512_WITH_RSAENCRYPTION_OID { + "rsa512".to_string() + } else if cert_alg == ECDSA_WITH_SHA256_OID { + SigningAlg::Es256.to_string() + } else if cert_alg == ECDSA_WITH_SHA384_OID { + SigningAlg::Es384.to_string() + } else if cert_alg == ECDSA_WITH_SHA512_OID { + SigningAlg::Es512.to_string() + } else if cert_alg == RSASSA_PSS_OID { + if let Some(parameters) = &cert.signature_algorithm.parameters { + let seq = match parameters.as_sequence() { + Ok(s) => s, + Err(_) => return None, + }; + + let (_i, (ha_alg, mgf_ai)) = match seq.parse(|i| { + let (i, h) =
::from_der(i)?; + if h.class() != Class::ContextSpecific || h.tag() != Tag(0) { + return Err(nom::Err::Error(asn1_rs::Error::BerValueError)); + } + + let (i, ha_alg) = AlgorithmIdentifier::from_der(i) + .map_err(|_| nom::Err::Error(asn1_rs::Error::BerValueError))?; + + let (i, h) =
::from_der(i)?; + if h.class() != Class::ContextSpecific || h.tag() != Tag(1) { + return Err(nom::Err::Error(asn1_rs::Error::BerValueError)); + } + + let (i, mgf_ai) = AlgorithmIdentifier::from_der(i) + .map_err(|_| nom::Err::Error(asn1_rs::Error::BerValueError))?; + + // Ignore anything that follows these two parameters. + + Ok((i, (ha_alg, mgf_ai))) + }) { + Ok((ii, (h, m))) => (ii, (h, m)), + Err(_) => return None, + }; + + let mgf_ai_parameters = match mgf_ai.parameters { + Some(m) => m, + None => return None, + }; + + let mgf_ai_parameters = match mgf_ai_parameters.as_sequence() { + Ok(m) => m, + Err(_) => return None, + }; + + let (_i, mgf_ai_params_algorithm) = + match ::from_der(&mgf_ai_parameters.content) { + Ok((i, m)) => (i, m), + Err(_) => return None, + }; + + let mgf_ai_params_algorithm = match mgf_ai_params_algorithm.as_oid() { + Ok(m) => m, + Err(_) => return None, + }; + + // must be the same + if ha_alg.algorithm.to_id_string() != mgf_ai_params_algorithm.to_id_string() { + return None; + } + + // check for one of the mandatory types + if ha_alg.algorithm == SHA256_OID { + "ps256".to_string() + } else if ha_alg.algorithm == SHA384_OID { + "ps384".to_string() + } else if ha_alg.algorithm == SHA512_OID { + "ps512".to_string() + } else { + return None; + } + } else { + return None; + } + } else if cert_alg == ED25519_OID { + SigningAlg::Ed25519.to_string() + } else { + return None; + }; + + Some(signing_alg) +} + +#[async_generic] +pub(crate) fn verify_data( + cert_der: Vec, + sig_alg: Option, + sig: Vec, + data: Vec, +) -> Result { + use x509_parser::prelude::*; + + let (_, cert) = + X509Certificate::from_der(cert_der.as_bytes()).map_err(|_e| Error::CoseCertUntrusted)?; + + let certificate_public_key = cert.public_key(); + + if let Some(cert_alg_string) = sig_alg { + let (algo, hash, salt_len) = match cert_alg_string.as_str() { + "rsa256" => ("RSASSA-PKCS1-v1_5".to_string(), "SHA-256".to_string(), 0), + "rsa384" => ("RSASSA-PKCS1-v1_5".to_string(), "SHA-384".to_string(), 0), + "rsa512" => ("RSASSA-PKCS1-v1_5".to_string(), "SHA-512".to_string(), 0), + "es256" => ("ECDSA".to_string(), "SHA-256".to_string().to_string(), 0), + "es384" => ("ECDSA".to_string(), "SHA-384".to_string(), 0), + "es512" => ("ECDSA".to_string(), "SHA-512".to_string(), 0), + "ps256" => ("RSA-PSS".to_string(), "SHA-256".to_string(), 32), + "ps384" => ("RSA-PSS".to_string(), "SHA-384".to_string(), 48), + "ps512" => ("RSA-PSS".to_string(), "SHA-512".to_string(), 64), + "ed25519" => ("ED25519".to_string(), "SHA-512".to_string(), 0), + _ => return Err(Error::UnsupportedType), + }; + + let adjusted_sig = if cert_alg_string.starts_with("es") { + let parsed_alg_string: SigningAlg = cert_alg_string + .parse() + .map_err(|_| Error::UnknownAlgorithm)?; + match der_to_p1363(&sig, parsed_alg_string) { + Some(p1363) => p1363, + None => sig, + } + } else { + sig + }; + + validate_signature( + algo, + hash, + salt_len, + certificate_public_key.raw.to_vec(), + adjusted_sig, + data, + ) + } else { + Err(Error::BadParam("unknown alg processing cert".to_string())) + } +} +// convert der signatures to P1363 format: r | s +fn der_to_p1363(data: &[u8], alg: SigningAlg) -> Option> { + // handle if this is a der sequence + if let Ok((_, bo)) = parse_der_sequence_of(parse_der_integer)(data) { + let seq = bo.as_sequence().ok()?; + + if seq.len() != 2 { + return None; + } + + let rp = seq[0].as_bigint().ok()?; + let sp = seq[1].as_bigint().ok()?; + + let mut r = rp.to_str_radix(16); + let mut s = sp.to_str_radix(16); + + let sig_len: usize = match alg { + SigningAlg::Es256 => 64, + SigningAlg::Es384 => 96, + SigningAlg::Es512 => 132, + _ => return None, + }; + + // pad or truncate as needed + let rp = if r.len() > sig_len { + // truncate + let offset = r.len() - sig_len; + &r[offset..r.len()] + } else { + // pad + while r.len() != sig_len { + r.insert(0, '0'); + } + r.as_ref() + }; + + let sp = if s.len() > sig_len { + // truncate + let offset = s.len() - sig_len; + &s[offset..s.len()] + } else { + // pad + while s.len() != sig_len { + s.insert(0, '0'); + } + s.as_ref() + }; + + if rp.len() != sig_len || rp.len() != sp.len() { + return None; + } + + // merge r and s strings + let mut new_sig = rp.to_string(); + new_sig.push_str(sp); + + // convert back from hex string to byte array + let result = (0..new_sig.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&new_sig[i..i + 2], 16) + .map_err(|_err| crate::Error::InvalidEcdsaSignature) + }) + .collect(); + + if let Ok(p1363) = result { + Some(p1363) + } else { + None + } + } else { + Some(data.to_vec()) + } +} + +fn check_chain_order(certs: &[Vec]) -> Result<()> { + use x509_parser::prelude::*; + + let chain_length = certs.len(); + if chain_length < 2 { + return Ok(()); + } + + for i in 1..chain_length { + let (_, current_cert) = + X509Certificate::from_der(&certs[i - 1]).map_err(|_e| Error::CoseCertUntrusted)?; + + let issuer_der = certs[i].to_vec(); + let data = current_cert.tbs_certificate.as_ref(); + let sig = current_cert.signature_value.as_ref(); + + let sig_alg = cert_signing_alg(¤t_cert); + + let result = verify_data(issuer_der, sig_alg, sig.to_vec(), data.to_vec()); + + // keep going as long as it validate + match result { + Ok(b) => { + if !b { + return Err(Error::OtherError("cert chain order invalid".into())); + } + } + Err(e) => return Err(e), + } + } + Ok(()) +} + +fn on_trust_list(th: &dyn TrustHandlerConfig, certs: &[Vec], ee_der: &[u8]) -> Result { + use x509_parser::prelude::*; + + // check the cert against the allowed list first + let cert_sha256 = hash_sha256(ee_der); + let cert_hash_base64 = base64::encode(&cert_sha256); + if th.get_allowed_list().contains(&cert_hash_base64) { + return Ok(true); + } + + // add ee cert if needed to the chain + let full_chain = if !certs.is_empty() && vec_compare(ee_der, &certs[0]) { + certs.to_vec() + } else { + let mut full_chain: Vec> = Vec::new(); + full_chain.push(ee_der.to_vec()); + let mut in_chain = certs.to_vec(); + full_chain.append(&mut in_chain); + full_chain + }; + + // make sure chain is in the correct order and valid + check_chain_order(&full_chain)?; + + // build anchors and check against trust anchors, + let mut anchors: Vec = Vec::new(); + let source_anchors = th.get_anchors(); + for anchor_der in &source_anchors { + let (_, anchor) = + X509Certificate::from_der(anchor_der).map_err(|_e| Error::CoseCertUntrusted)?; + anchors.push(anchor); + } + + if anchors.is_empty() { + return Ok(false); + } + + // work back from last cert in chain against the trust anchors + for cert in certs.iter().rev() { + let (_, chain_cert) = + X509Certificate::from_der(cert).map_err(|_e| Error::CoseCertUntrusted)?; + + for anchor in &source_anchors { + let data = chain_cert.tbs_certificate.as_ref(); + let sig = chain_cert.signature_value.as_ref(); + + let sig_alg = cert_signing_alg(&chain_cert); + + let (_, anchor_cert) = + X509Certificate::from_der(anchor).map_err(|_e| Error::CoseCertUntrusted)?; + + if chain_cert.issuer() == anchor_cert.subject() { + let result = verify_data(anchor.clone(), sig_alg, sig.to_vec(), data.to_vec()); + + match result { + Ok(b) => { + if b { + return Ok(true); + } + } + Err(_) => continue, + } + } + } + } + // todo: consider (path check and names restrictions) + + Ok(false) +} + +// verify certificate and trust chain +#[async_generic] +pub(crate) fn verify_trust( + th: &dyn TrustHandlerConfig, + chain_der: &[Vec], + cert_der: &[u8], + _signing_time_epoc: Option, +) -> Result { + // check configured EKUs against end-entity cert + find_allowed_eku(cert_der, &th.get_auxillary_ekus()).ok_or(Error::CoseCertUntrusted)?; + + on_trust_list(th, chain_der, cert_der) +} + +#[cfg(test)] +pub mod tests { + #![allow(clippy::expect_used)] + #![allow(clippy::panic)] + #![allow(clippy::unwrap_used)] + + use super::*; + #[test] + async fn test_trust_store() { + let mut th = WasiTrustHandlerConfig::new(); + th.clear(); + + th.load_default_trust().unwrap(); + + // test all the certs + let ps256 = include_bytes!("../../tests/fixtures/certs/ps256.pub"); + let ps384 = include_bytes!("../../tests/fixtures/certs/ps384.pub"); + let ps512 = include_bytes!("../../tests/fixtures/certs/ps512.pub"); + let es256 = include_bytes!("../../tests/fixtures/certs/es256.pub"); + let es384 = include_bytes!("../../tests/fixtures/certs/es384.pub"); + let es512 = include_bytes!("../../tests/fixtures/certs/es512.pub"); + let ed25519 = include_bytes!("../../tests/fixtures/certs/ed25519.pub"); + + let ps256_certs = load_trust_from_data(ps256).unwrap(); + let ps384_certs = load_trust_from_data(ps384).unwrap(); + let ps512_certs = load_trust_from_data(ps512).unwrap(); + let es256_certs = load_trust_from_data(es256).unwrap(); + let es384_certs = load_trust_from_data(es384).unwrap(); + let es512_certs = load_trust_from_data(es512).unwrap(); + let ed25519_certs = load_trust_from_data(ed25519).unwrap(); + + assert!( + verify_trust_async(&th, &ps256_certs[1..], &ps256_certs[0], None) + .await + .unwrap() + ); + assert!( + verify_trust_async(&th, &ps384_certs[1..], &ps384_certs[0], None) + .await + .unwrap() + ); + assert!( + verify_trust_async(&th, &ps512_certs[1..], &ps512_certs[0], None) + .await + .unwrap() + ); + assert!( + verify_trust_async(&th, &es256_certs[1..], &es256_certs[0], None) + .await + .unwrap() + ); + + assert!( + verify_trust_async(&th, &es384_certs[1..], &es384_certs[0], None) + .await + .unwrap() + ); + assert!( + verify_trust_async(&th, &es512_certs[1..], &es512_certs[0], None) + .await + .unwrap() + ); + + assert!( + verify_trust_async(&th, &ed25519_certs[1..], &ed25519_certs[0], None) + .await + .unwrap() + ); + } + + #[test] + async fn test_broken_trust_chain() { + let mut th = WasiTrustHandlerConfig::new(); + th.clear(); + + th.load_default_trust().unwrap(); + + // test all the certs + let ps256 = include_bytes!("../../tests/fixtures/certs/ps256.pub"); + let ps384 = include_bytes!("../../tests/fixtures/certs/ps384.pub"); + let ps512 = include_bytes!("../../tests/fixtures/certs/ps512.pub"); + let es256 = include_bytes!("../../tests/fixtures/certs/es256.pub"); + let es384 = include_bytes!("../../tests/fixtures/certs/es384.pub"); + let es512 = include_bytes!("../../tests/fixtures/certs/es512.pub"); + let ed25519 = include_bytes!("../../tests/fixtures/certs/ed25519.pub"); + + let ps256_certs = load_trust_from_data(ps256).unwrap(); + let ps384_certs = load_trust_from_data(ps384).unwrap(); + let ps512_certs = load_trust_from_data(ps512).unwrap(); + let es256_certs = load_trust_from_data(es256).unwrap(); + let es384_certs = load_trust_from_data(es384).unwrap(); + let es512_certs = load_trust_from_data(es512).unwrap(); + let ed25519_certs = load_trust_from_data(ed25519).unwrap(); + + assert!( + !verify_trust_async(&th, &ps256_certs[2..], &ps256_certs[0], None) + .await + .unwrap() + ); + assert!( + !verify_trust_async(&th, &ps384_certs[2..], &ps384_certs[0], None) + .await + .unwrap() + ); + assert!( + !verify_trust_async(&th, &ps512_certs[2..], &ps512_certs[0], None) + .await + .unwrap() + ); + assert!( + !verify_trust_async(&th, &es256_certs[2..], &es256_certs[0], None) + .await + .unwrap() + ); + assert!( + !verify_trust_async(&th, &es384_certs[2..], &es384_certs[0], None) + .await + .unwrap() + ); + assert!( + !verify_trust_async(&th, &es512_certs[2..], &es512_certs[0], None) + .await + .unwrap() + ); + assert!( + !verify_trust_async(&th, &ed25519_certs[2..], &ed25519_certs[0], None) + .await + .unwrap() + ); + } +}