diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dde75d0..70f19a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,8 @@ jobs: - stable steps: - uses: actions/checkout@v4 + with: + submodules: recursive - uses: RustCrypto/actions/cargo-cache@master - uses: dtolnay/rust-toolchain@master with: @@ -52,6 +54,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - uses: RustCrypto/actions/cargo-cache@master - uses: dtolnay/rust-toolchain@nightly - run: cargo update -Z minimal-versions @@ -62,6 +66,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - uses: RustCrypto/actions/cargo-cache@master - uses: dtolnay/rust-toolchain@master with: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0e5a77f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "thirdparty/wycheproof"] + path = thirdparty/wycheproof + url = git@github.com:C2SP/wycheproof diff --git a/Cargo.lock b/Cargo.lock index 5476f5d..011651e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,15 @@ dependencies = [ "polyval", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + [[package]] name = "hex-literal" version = "0.4.1" @@ -294,6 +303,12 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + [[package]] name = "keccak" version = "0.2.0-pre.0" @@ -321,6 +336,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "num-traits" version = "0.2.19" @@ -518,6 +539,7 @@ dependencies = [ "crypto-bigint", "crypto-primes", "digest", + "hex", "hex-literal", "pkcs1", "pkcs8", @@ -527,6 +549,7 @@ dependencies = [ "rand_core", "rand_xorshift", "serde", + "serde_json", "serde_test", "serdect", "sha1", @@ -563,6 +586,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + [[package]] name = "salsa20" version = "0.11.0-pre.2" @@ -604,6 +633,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_test" version = "1.0.177" diff --git a/Cargo.toml b/Cargo.toml index d276465..9cb4428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,9 @@ rand_core = { version = "0.6", default-features = false } sha1 = { version = "=0.11.0-pre.4", default-features = false, features = ["oid"] } sha2 = { version = "=0.11.0-pre.4", default-features = false, features = ["oid"] } sha3 = { version = "=0.11.0-pre.4", default-features = false, features = ["oid"] } +hex = { version = "0.4.3", features = ["serde"] } +serde_json = "1.0.138" +serde = { version = "1.0.184", features = ["derive"] } [[bench]] name = "key" diff --git a/tests/wycheproof.rs b/tests/wycheproof.rs new file mode 100644 index 0000000..c36e538 --- /dev/null +++ b/tests/wycheproof.rs @@ -0,0 +1,281 @@ +//! Executes tests based on the wycheproof testsuite. + +// This implementation here is based on +// + +use std::fs::File; + +use pkcs1::DecodeRsaPublicKey; +use rsa::{ + pkcs1v15, pss, + signature::{Error as SignatureError, Verifier}, + RsaPublicKey, +}; +use serde::Deserialize; +use sha1::Sha1; +use sha2::{Sha224, Sha256, Sha384, Sha512}; + +#[derive(Deserialize, Debug)] +struct TestFile { + #[serde(rename(deserialize = "testGroups"))] + groups: Vec, + header: Vec, + algorithm: String, +} + +#[derive(Deserialize, Debug)] +struct TestGroup { + #[serde(rename(deserialize = "type"))] + typ: String, + + #[serde(default, rename(deserialize = "publicKeyAsn"), with = "hex::serde")] + public_key_asn: Vec, + + #[serde(default)] + sha: String, + + #[serde(default, rename(deserialize = "mgfSha"))] + mgf_sha: String, + + #[serde(default, rename(deserialize = "sLen"))] + salt_len: usize, + + tests: Vec, +} + +#[derive(Deserialize, Debug)] +struct Test { + #[serde(rename(deserialize = "tcId"))] + #[allow(unused)] // for Debug + id: usize, + #[allow(unused)] // for Debug + comment: String, + #[serde(default, with = "hex::serde")] + msg: Vec, + #[serde(default, with = "hex::serde")] + sig: Vec, + result: ExpectedResult, +} + +#[derive(Copy, Clone, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +enum ExpectedResult { + Valid, + Invalid, + Acceptable, +} + +#[derive(Debug)] +struct Summary { + started: usize, + skipped: usize, + failed: usize, + in_test: bool, +} + +impl Summary { + fn new() -> Self { + Self { + started: 0, + skipped: 0, + failed: 0, + in_test: false, + } + } + + fn fail(&mut self, test: Test, res: Option) { + if self.in_test { + eprintln!( + " failed: {}: expected {:?}, got {:?}", + test.id, test.result, res + ); + self.failed += 1; + self.in_test = false; + } + } + + fn group(&mut self, group: &TestGroup) { + println!(" group: {:?}", group.typ); + self.in_test = false; + } + + fn start(&mut self, test: &Test) { + println!(" test {}:", test.id); + self.started += 1; + self.in_test = true; + } + + fn skipped(&mut self, why: &str) { + if self.in_test { + println!(" skipped: {why}"); + self.skipped += 1; + self.in_test = false; + } else { + println!(" skipped group: {why}"); + } + } +} + +impl Drop for Summary { + fn drop(&mut self) { + let passed = self.started - self.skipped - self.failed; + println!( + "DONE: started {} passed {} skipped {} failed {}", + self.started, passed, self.skipped, self.failed + ); + assert!(passed > 0, "no tests have passed"); + + if self.failed > 0 { + panic!("{} tests failed", self.failed); + } + } +} + +#[test] +fn test_rsa_pkcs1_verify() { + for file in &[ + "rsa_signature_2048_sha256_test.json", + "rsa_signature_2048_sha384_test.json", + "rsa_signature_2048_sha512_test.json", + "rsa_signature_3072_sha256_test.json", + "rsa_signature_3072_sha384_test.json", + "rsa_signature_3072_sha512_test.json", + "rsa_signature_4096_sha256_test.json", + "rsa_signature_4096_sha384_test.json", + "rsa_signature_4096_sha512_test.json", + // "rsa_signature_8192_sha256_test.json", TODO: needs disabling of maxsize + // "rsa_signature_8192_sha384_test.json", TODO: needs disabling of maxsize + // "rsa_signature_8192_sha512_test.json", TODO: needs disabling of maxsize + ] { + let path = format!("thirdparty/wycheproof/testvectors_v1/{file}"); + let data_file = File::open(&path).expect("failed to open data file"); + println!("Loading file: {path}"); + + let tests: TestFile = serde_json::from_reader(data_file).expect("invalid test JSON"); + + println!("{}:\n{}\n", tests.algorithm, tests.header.join("")); + let mut summary = Summary::new(); + + for group in tests.groups { + summary.group(&group); + + let key = RsaPublicKey::from_pkcs1_der(&group.public_key_asn).unwrap(); + println!("key is {:?}", key); + + for test in group.tests { + summary.start(&test); + + let sig = pkcs1v15::Signature::try_from(&test.sig[..]).expect("invalid signature"); + let result = match group.sha.as_ref() { + "SHA-256" => { + let vk = pkcs1v15::VerifyingKey::::new(key.clone()); + vk.verify(&test.msg, &sig) + } + "SHA-384" => { + let vk = pkcs1v15::VerifyingKey::::new(key.clone()); + vk.verify(&test.msg, &sig) + } + "SHA-512" => { + let vk = pkcs1v15::VerifyingKey::::new(key.clone()); + vk.verify(&test.msg, &sig) + } + other => panic!("unhandled sha {other:?}"), + }; + + match (test.result, &result) { + (ExpectedResult::Valid, Ok(())) => {} + (ExpectedResult::Invalid | ExpectedResult::Acceptable, Err(_err)) => {} + _ => summary.fail(test, result.err()), + } + } + } + } +} + +#[test] +fn test_rsa_pss_verify() { + for file in &[ + "rsa_pss_2048_sha256_mgf1_0_test.json", + "rsa_pss_2048_sha256_mgf1_32_test.json", + "rsa_pss_2048_sha384_mgf1_48_test.json", + "rsa_pss_3072_sha256_mgf1_32_test.json", + "rsa_pss_4096_sha256_mgf1_32_test.json", + "rsa_pss_4096_sha384_mgf1_48_test.json", + "rsa_pss_4096_sha512_mgf1_64_test.json", + "rsa_pss_misc_test.json", + ] { + let path = format!("thirdparty/wycheproof/testvectors_v1/{file}"); + let data_file = File::open(&path).expect("failed to open data file"); + println!("Loading file: {path}"); + + let tests: TestFile = serde_json::from_reader(data_file).expect("invalid test JSON"); + + println!("{}:\n{}\n", tests.algorithm, tests.header.join("")); + let mut summary = Summary::new(); + + for group in tests.groups { + summary.group(&group); + + let key = rsa::RsaPublicKey::from_pkcs1_der(&group.public_key_asn).unwrap(); + println!("key is {:?}", key); + + for test in group.tests { + summary.start(&test); + + if group.sha != group.mgf_sha { + summary.skipped(&format!( + "pss with sha={} mgf={} salt_len={} not supported", + group.sha, group.mgf_sha, group.salt_len, + )); + } + let sig = pss::Signature::try_from(&test.sig[..]).expect("invalid signature"); + let result = match group.sha.as_ref() { + "SHA-1" => { + let vk = pss::VerifyingKey::::new_with_salt_len( + key.clone(), + group.salt_len, + ); + vk.verify(&test.msg, &sig) + } + "SHA-256" => { + let vk = pss::VerifyingKey::::new_with_salt_len( + key.clone(), + group.salt_len, + ); + vk.verify(&test.msg, &sig) + } + "SHA-224" => { + let vk = pss::VerifyingKey::::new_with_salt_len( + key.clone(), + group.salt_len, + ); + vk.verify(&test.msg, &sig) + } + "SHA-384" => { + let vk = pss::VerifyingKey::::new_with_salt_len( + key.clone(), + group.salt_len, + ); + vk.verify(&test.msg, &sig) + } + "SHA-512" => { + let vk = pss::VerifyingKey::::new_with_salt_len( + key.clone(), + group.salt_len, + ); + vk.verify(&test.msg, &sig) + } + other => panic!("unhandled sha {other:?}"), + }; + + match (test.result, &result) { + (ExpectedResult::Valid, Ok(())) => {} + (ExpectedResult::Invalid | ExpectedResult::Acceptable, Err(_err)) => {} + _ => { + summary.fail(test, result.err()); + } + }; + } + } + } +} diff --git a/thirdparty/wycheproof b/thirdparty/wycheproof new file mode 160000 index 0000000..cd27d64 --- /dev/null +++ b/thirdparty/wycheproof @@ -0,0 +1 @@ +Subproject commit cd27d6419bedd83cbd24611ec54b6d4bfdb0cdca