diff --git a/.github/workflows/ml-dsa.yml b/.github/workflows/ml-dsa.yml index e62338f..bcddcfb 100644 --- a/.github/workflows/ml-dsa.yml +++ b/.github/workflows/ml-dsa.yml @@ -32,6 +32,8 @@ jobs: - stable steps: - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0b040ef --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "wycheproof"] + path = thirdparty/wycheproof + url = https://github.com/C2SP/wycheproof.git diff --git a/ml-dsa/Cargo.toml b/ml-dsa/Cargo.toml index 0f8a713..d7bd17b 100644 --- a/ml-dsa/Cargo.toml +++ b/ml-dsa/Cargo.toml @@ -45,11 +45,11 @@ zeroize = { version = "1.8.1", optional = true, default-features = false } [dev-dependencies] criterion = "0.7" +getrandom = { version = "0.4.0-rc.0", features = ["sys_rng"] } hex = { version = "0.4", features = ["serde"] } hex-literal = "1" pkcs8 = { version = "0.11.0-rc.8", features = ["pem"] } proptest = "1" -getrandom = { version = "0.4.0-rc.0", features = ["sys_rng"] } serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.132" diff --git a/ml-dsa/tests/wycheproof.rs b/ml-dsa/tests/wycheproof.rs new file mode 100644 index 0000000..94274e3 --- /dev/null +++ b/ml-dsa/tests/wycheproof.rs @@ -0,0 +1,111 @@ +//! Test against the Wycheproof test vectors. + +// Implementation is based in part on `rsa` which is in turn based on Graviola. + +use ml_dsa::{KeyGen, MlDsa44, MlDsa65, MlDsa87}; +use serde::Deserialize; +use signature::{SignatureEncoding, Signer}; +use std::fs::File; + +#[derive(Deserialize, Debug)] +struct TestFile { + #[serde(rename(deserialize = "testGroups"))] + groups: Vec, + header: Vec, + algorithm: String, +} + +#[derive(Deserialize, Debug)] +struct TestGroup { + #[allow(dead_code)] + #[serde(rename(deserialize = "type"))] + type_: String, + + #[serde(default, rename(deserialize = "privateSeed"), with = "hex::serde")] + private_seed: Vec, + + #[serde(default, rename(deserialize = "publicKey"), with = "hex::serde")] + #[allow(dead_code)] + public_key: Vec, + + tests: Vec, +} + +#[derive(Deserialize, Debug)] +struct Test { + #[serde(rename(deserialize = "tcId"))] + id: usize, + comment: String, + #[serde(with = "hex::serde")] + msg: Vec, + #[serde(default, with = "hex::serde")] + ctx: Vec, + #[serde(with = "hex::serde")] + sig: Vec, + result: ExpectedResult, +} + +#[derive(Copy, Clone, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +enum ExpectedResult { + Valid, + Invalid, + Acceptable, +} + +macro_rules! mldsa_sign_from_seed_test { + ($name:ident, $json_file:expr, $keypair:ident) => { + #[test] + fn $name() { + let path = format!("../thirdparty/wycheproof/testvectors_v1/{}", $json_file); + let data_file = File::open(&path) + .expect("failed to open data file (try running `git submodule update --init`)"); + + 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("")); + + for group in tests.groups { + let sk = $keypair::from_seed(&group.private_seed.as_slice().try_into().unwrap()); + + for test in &group.tests { + println!("Test #{}: {} ({:?})", test.id, &test.comment, &test.result); + + if test.ctx.is_empty() { + let sig = sk.sign(&test.msg); + assert_eq!(&*sig.to_bytes(), test.sig.as_slice()); + } else { + let result = sk.signing_key().sign_deterministic(&test.msg, &test.ctx); + + match test.result { + ExpectedResult::Valid => { + assert_eq!(&*result.unwrap().to_bytes(), test.sig.as_slice()); + } + ExpectedResult::Invalid => { + assert!(result.is_err()); + } + other => todo!("{:?}", other), + } + } + } + } + } + }; +} + +mldsa_sign_from_seed_test!( + mldsa_44_sign_seed_test, + "mldsa_44_sign_seed_test.json", + MlDsa44 +); +mldsa_sign_from_seed_test!( + mldsa_65_sign_seed_test, + "mldsa_65_sign_seed_test.json", + MlDsa65 +); +mldsa_sign_from_seed_test!( + mldsa_87_sign_seed_test, + "mldsa_87_sign_seed_test.json", + MlDsa87 +); diff --git a/thirdparty/wycheproof b/thirdparty/wycheproof new file mode 160000 index 0000000..0fd0ec1 --- /dev/null +++ b/thirdparty/wycheproof @@ -0,0 +1 @@ +Subproject commit 0fd0ec1cf2114f456f5c3e7c61ba807fb1311b45