Skip to content

Commit b316da4

Browse files
authored
ml-dsa: adapt many_round_trip_test into a proptest (#1184)
Replaces ad hoc randomized testing with proptest, which can automatically report seeds to reproduce failures experienced in CI, and can automatically track and test previous regressions.
1 parent c147ef0 commit b316da4

File tree

3 files changed

+75
-150
lines changed

3 files changed

+75
-150
lines changed

ml-dsa/src/lib.rs

Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -943,11 +943,6 @@ where
943943
mod test {
944944
use super::*;
945945
use crate::param::*;
946-
use getrandom::{
947-
SysRng,
948-
rand_core::{Rng, UnwrapErr},
949-
};
950-
use signature::digest::Update;
951946

952947
#[test]
953948
fn output_sizes() {
@@ -1046,42 +1041,6 @@ mod test {
10461041
sign_verify_round_trip_test::<MlDsa87>();
10471042
}
10481043

1049-
fn many_round_trip_test<P>()
1050-
where
1051-
P: MlDsaParams,
1052-
{
1053-
const ITERATIONS: usize = 1000;
1054-
1055-
let mut rng = UnwrapErr(SysRng);
1056-
let mut seed = B32::default();
1057-
1058-
for _i in 0..ITERATIONS {
1059-
let seed_data: &mut [u8] = seed.as_mut();
1060-
rng.fill_bytes(seed_data);
1061-
1062-
let kp = P::from_seed(&seed);
1063-
let sk = kp.signing_key;
1064-
let vk = kp.verifying_key;
1065-
1066-
let M = b"Hello world";
1067-
let rnd = Array([0u8; 32]);
1068-
let sig = sk.sign_internal(&[M], &rnd);
1069-
1070-
let sig_enc = sig.encode();
1071-
let sig_dec = Signature::<P>::decode(&sig_enc).unwrap();
1072-
1073-
assert_eq!(sig_dec, sig);
1074-
assert!(vk.verify_internal(M, &sig_dec));
1075-
}
1076-
}
1077-
1078-
#[test]
1079-
fn many_round_trip() {
1080-
many_round_trip_test::<MlDsa44>();
1081-
many_round_trip_test::<MlDsa65>();
1082-
many_round_trip_test::<MlDsa87>();
1083-
}
1084-
10851044
#[test]
10861045
fn sign_mu_verify_mu_round_trip() {
10871046
fn sign_mu_verify_mu<P>()
@@ -1148,63 +1107,6 @@ mod test {
11481107
sign_internal_verify_mu::<MlDsa87>();
11491108
}
11501109

1151-
#[test]
1152-
fn sign_digest_round_trip() {
1153-
fn sign_digest<P>()
1154-
where
1155-
P: MlDsaParams,
1156-
{
1157-
let kp = P::from_seed(&Array::default());
1158-
let sk = kp.signing_key;
1159-
let vk = kp.verifying_key;
1160-
1161-
let M = b"Hello world";
1162-
let sig = sk.sign_digest(|digest| digest.update(M));
1163-
assert_eq!(sig, sk.sign(M));
1164-
1165-
vk.verify_digest(
1166-
|digest| {
1167-
digest.update(M);
1168-
Ok(())
1169-
},
1170-
&sig,
1171-
)
1172-
.unwrap();
1173-
}
1174-
sign_digest::<MlDsa44>();
1175-
sign_digest::<MlDsa65>();
1176-
sign_digest::<MlDsa87>();
1177-
}
1178-
1179-
#[test]
1180-
#[cfg(feature = "rand_core")]
1181-
fn sign_randomized_digest_round_trip() {
1182-
fn sign_digest<P>()
1183-
where
1184-
P: MlDsaParams,
1185-
{
1186-
let kp = P::from_seed(&Array::default());
1187-
let sk = kp.signing_key;
1188-
let vk = kp.verifying_key;
1189-
1190-
let M = b"Hello world";
1191-
let mut rng = UnwrapErr(SysRng);
1192-
let sig = sk.sign_digest_with_rng(&mut rng, |digest| digest.update(M));
1193-
1194-
vk.verify_digest(
1195-
|digest| {
1196-
digest.update(M);
1197-
Ok(())
1198-
},
1199-
&sig,
1200-
)
1201-
.unwrap();
1202-
}
1203-
sign_digest::<MlDsa44>();
1204-
sign_digest::<MlDsa65>();
1205-
sign_digest::<MlDsa87>();
1206-
}
1207-
12081110
#[test]
12091111
fn from_seed_implementations_match() {
12101112
fn assert_from_seed_equality<P>()

ml-dsa/tests/proptests.proptest-regressions

Lines changed: 3 additions & 2 deletions
Large diffs are not rendered by default.

ml-dsa/tests/proptests.rs

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,79 @@
1-
use hybrid_array::AsArrayRef;
2-
use ml_dsa::{
3-
KeyGen, KeyPair, MlDsa44, MlDsa65, MlDsa87, Signature,
4-
signature::{Signer, Verifier},
5-
};
6-
use proptest::prelude::*;
7-
8-
/// Example message
9-
const MSG: &[u8] = b"Hello world";
10-
11-
// Keypairs
12-
prop_compose! {
13-
fn mldsa44_keypair()(seed_bytes in any::<[u8; 32]>()) -> KeyPair<MlDsa44> {
14-
MlDsa44::from_seed(seed_bytes.as_array_ref())
15-
}
16-
}
17-
prop_compose! {
18-
fn mldsa65_keypair()(seed_bytes in any::<[u8; 32]>()) -> KeyPair<MlDsa65> {
19-
MlDsa65::from_seed(seed_bytes.as_array_ref())
20-
}
21-
}
22-
prop_compose! {
23-
fn mldsa87_keypair()(seed_bytes in any::<[u8; 32]>()) -> KeyPair<MlDsa87> {
24-
MlDsa87::from_seed(seed_bytes.as_array_ref())
25-
}
1+
//! Property-based tests for the `ml-dsa` crate.
2+
3+
macro_rules! signature_round_trip_encode {
4+
($alg:ident, $sig:expr) => {{
5+
let sig_enc = $sig.encode();
6+
let sig_dec_result = Signature::<$alg>::decode(&sig_enc);
7+
prop_assert_eq!(&sig_dec_result, &Some($sig));
8+
sig_dec_result.unwrap()
9+
}};
2610
}
2711

28-
macro_rules! round_trip_test {
29-
($params:path, $keypair:expr) => {
30-
let sig = $keypair.signing_key().sign(MSG);
12+
macro_rules! mldsa_proptests {
13+
($name:ident, $alg:ident) => {
14+
mod $name {
15+
use ml_dsa::{
16+
KeyGen, Signature,
17+
signature::{DigestSigner, DigestVerifier, digest::Update},
18+
$alg,
19+
};
20+
use proptest::{collection, prelude::*};
21+
22+
#[cfg(feature = "rand_core")]
23+
use ml_dsa::signature::RandomizedDigestSigner;
24+
25+
proptest! {
26+
#[test]
27+
fn round_trip_test(
28+
seed in any::<[u8; 32]>(),
29+
msg in collection::vec(0u8..u8::MAX, 0..65536),
30+
rnd in any::<[u8; 32]>()
31+
) {
32+
let kp = $alg::from_seed(&seed.into());
33+
let sk = kp.signing_key();
34+
let vk = kp.verifying_key();
3135

32-
// Check signature verification
33-
let verify_result = $keypair.verifying_key().verify(MSG, &sig);
34-
prop_assert!(verify_result.is_ok());
36+
let sig = sk.sign_internal(&[&msg], &rnd.into());
37+
let sig_dec = signature_round_trip_encode!($alg, sig);
38+
assert!(vk.verify_internal(&msg, &sig_dec));
39+
}
3540

36-
// Check signature encoding round trip
37-
let sig_decoded = Signature::<$params>::decode(&sig.encode());
38-
prop_assert_eq!(Some(sig), sig_decoded);
41+
#[test]
42+
fn round_trip_digest_test(
43+
seed in any::<[u8; 32]>(),
44+
msg in collection::vec(0u8..u8::MAX, 0..65536),
45+
) {
46+
let kp = $alg::from_seed(&seed.into());
47+
let sk = kp.signing_key();
48+
let vk = kp.verifying_key();
49+
50+
let sig = sk.sign_digest(|digest| digest.update(&msg));
51+
let sig_dec = signature_round_trip_encode!($alg, sig);
52+
let verify_result = vk.verify_digest(|digest| Ok(digest.update(&msg)), &sig_dec);
53+
assert!(verify_result.is_ok());
54+
}
55+
56+
#[cfg(feature = "rand_core")]
57+
#[test]
58+
fn round_trip_randomized_digest_test(
59+
seed in any::<[u8; 32]>(),
60+
msg in collection::vec(0u8..u8::MAX, 0..65536),
61+
) {
62+
let kp = $alg::from_seed(&seed.into());
63+
let sk = kp.signing_key();
64+
let vk = kp.verifying_key();
65+
66+
let mut rng = rand_core::UnwrapErr(getrandom::SysRng);
67+
let sig = sk.sign_digest_with_rng(&mut rng, |digest| digest.update(&msg));
68+
let sig_dec = signature_round_trip_encode!($alg, sig);
69+
let verify_result = vk.verify_digest(|digest| Ok(digest.update(&msg)), &sig_dec);
70+
assert!(verify_result.is_ok());
71+
}
72+
}
73+
}
3974
};
4075
}
4176

42-
proptest! {
43-
#[test]
44-
fn mldsa44_round_trip(keypair in mldsa44_keypair()) {
45-
round_trip_test!(MlDsa44, keypair);
46-
}
47-
48-
#[test]
49-
fn mldsa65_round_trip(keypair in mldsa65_keypair()) {
50-
round_trip_test!(MlDsa65, keypair);
51-
}
52-
53-
#[test]
54-
fn mldsa87_round_trip(keypair in mldsa87_keypair()) {
55-
round_trip_test!(MlDsa87, keypair);
56-
}
57-
}
77+
mldsa_proptests!(mldsa44, MlDsa44);
78+
mldsa_proptests!(mldsa65, MlDsa65);
79+
mldsa_proptests!(mldsa87, MlDsa87);

0 commit comments

Comments
 (0)