ml-dsa: add Wycheproof verification tests (#1176)

Adds Wycheproof verification tests for ML-DSA-44/65/87.

On ML-DSA-44, this reproduced an issue previously reported as
GHSA-5x2r-hc65-25f9 by @orenyomtov:

> Test #18: signature with a repeated hint (Invalid)

Likewise, on ML-DSA-44 only we are also failing this case:

> Test #68: public key with t1 component set to zero (Valid)

I have added an exceptions for those two for now, and all the other
tests pass.

I opened #1183 as a tracking issue for these verification failures.
This commit is contained in:
Tony Arcieri
2026-01-26 09:38:27 -07:00
committed by GitHub
parent e90411eb91
commit 39d03e27e5
+87 -16
View File
@@ -2,9 +2,9 @@
// Implementation is based in part on `rsa` which is in turn based on Graviola.
use ml_dsa::{KeyGen, MlDsa44, MlDsa65, MlDsa87};
use ml_dsa::{KeyGen, MlDsa44, MlDsa65, MlDsa87, Signature, VerifyingKey};
use serde::Deserialize;
use signature::{SignatureEncoding, Signer};
use signature::{SignatureEncoding, Signer, Verifier};
use std::fs::File;
#[derive(Deserialize, Debug)]
@@ -53,18 +53,25 @@ enum ExpectedResult {
Acceptable,
}
macro_rules! mldsa_sign_from_seed_test {
macro_rules! load_json_file {
($json_file:expr) => {{
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(""));
tests
}};
}
macro_rules! mldsa_sign_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(""));
let tests = load_json_file!($json_file);
for group in tests.groups {
let sk = $keypair::from_seed(&group.private_seed.as_slice().try_into().unwrap());
@@ -80,10 +87,10 @@ macro_rules! mldsa_sign_from_seed_test {
match test.result {
ExpectedResult::Valid => {
assert_eq!(&*result.unwrap().to_bytes(), test.sig.as_slice());
assert_eq!(&*result.unwrap().to_bytes(), test.sig.as_slice())
}
ExpectedResult::Invalid => {
assert!(result.is_err());
assert!(result.is_err())
}
other => todo!("{:?}", other),
}
@@ -94,18 +101,82 @@ macro_rules! mldsa_sign_from_seed_test {
};
}
mldsa_sign_from_seed_test!(
macro_rules! mldsa_verify_test {
($name:ident, $json_file:expr, $keypair:ident) => {
mldsa_verify_test!($name, $json_file, $keypair, []);
};
($name:ident, $json_file:expr, $keypair:ident, $skip:expr) => {
#[test]
fn $name() {
let tests = load_json_file!($json_file);
for group in &tests.groups {
if let Ok(encoded_vk) = group.public_key.as_slice().try_into() {
let vk = VerifyingKey::<MlDsa44>::decode(&encoded_vk);
for test in &group.tests {
println!("Test #{}: {} ({:?})", test.id, &test.comment, &test.result);
if $skip.contains(&test.id) {
println!("Test #{} is in skip list, skipping!", test.id);
continue;
}
if let Some(sig) = test
.sig
.as_slice()
.try_into()
.ok()
.and_then(|sig| Signature::<MlDsa44>::decode(&sig))
{
if test.ctx.is_empty() {
let result = vk.verify(&test.msg, &sig);
match test.result {
ExpectedResult::Valid => assert!(result.is_ok()),
ExpectedResult::Invalid => assert!(result.is_err()),
other => todo!("{:?}", other),
}
} else {
// TODO(test) contexts
}
} else {
println!("error decoding signature (length: {})", test.sig.len(),);
assert_eq!(test.result, ExpectedResult::Invalid);
}
}
}
}
}
};
}
// TODO(tarcieri): debug these test failures
const SIGNATURE_WITH_A_REPEATED_HINT_TEST_ID: usize = 18;
const PUBLIC_KEY_WITH_T1_COMPONENT_SET_TO_ZERO_TEST_ID: usize = 68;
mldsa_sign_seed_test!(
mldsa_44_sign_seed_test,
"mldsa_44_sign_seed_test.json",
MlDsa44
);
mldsa_sign_from_seed_test!(
mldsa_sign_seed_test!(
mldsa_65_sign_seed_test,
"mldsa_65_sign_seed_test.json",
MlDsa65
);
mldsa_sign_from_seed_test!(
mldsa_sign_seed_test!(
mldsa_87_sign_seed_test,
"mldsa_87_sign_seed_test.json",
MlDsa87
);
mldsa_verify_test!(
mldsa_44_verify_test,
"mldsa_44_verify_test.json",
MlDsa44,
[
SIGNATURE_WITH_A_REPEATED_HINT_TEST_ID,
PUBLIC_KEY_WITH_T1_COMPONENT_SET_TO_ZERO_TEST_ID
]
);
mldsa_verify_test!(mldsa_65_verify_test, "mldsa_65_verify_test.json", MlDsa65);
mldsa_verify_test!(mldsa_87_verify_test, "mldsa_87_verify_test.json", MlDsa87);