Skip to content

Commit 582529c

Browse files
committed
new fuzzers
1 parent 8ded3db commit 582529c

File tree

6 files changed

+272
-9
lines changed

6 files changed

+272
-9
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 0.4.5 (2024-11-07)
9+
10+
- Bug fix in Hash-ML-DSA - thank you @codespree
11+
- Two new fuzzers with tons of coverage: fuzz_sign and fuzz_verify
12+
813
## 0.4.4 (2024-10-29)
914

1015
- Significant shrink of required stack size

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ workspace = { exclude = ["ct_cm4", "dudect", "fuzz", "wasm"] }
22

33
[package]
44
name = "fips204"
5-
version = "0.4.4"
5+
version = "0.4.5"
66
authors = ["Eric Schorn <[email protected]>"]
77
description = "FIPS 204: Module-Lattice-Based Digital Signature"
88
categories = ["cryptography", "no-std"]

fuzz/Cargo.toml

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fips204-fuzz"
3-
version = "0.4.4"
3+
version = "0.4.5"
44
authors = ["Eric Schorn <[email protected]>"]
55
description = "Fuzz harness for FIPS 204 (draft) ML-DSA"
66
edition = "2021"
@@ -16,6 +16,7 @@ cargo-fuzz = true
1616
[dependencies]
1717
libfuzzer-sys = "0.4"
1818
rand_core = { version = "0.6.4", default-features = false }
19+
rand_chacha = "0.3.1"
1920

2021

2122
[dependencies.fips204]
@@ -39,3 +40,15 @@ name = "fuzz_all"
3940
path = "fuzz_targets/fuzz_all.rs"
4041
test = false
4142
doc = false
43+
44+
[[bin]]
45+
name = "fuzz_verify"
46+
path = "fuzz_targets/fuzz_verify.rs"
47+
test = false
48+
doc = false
49+
50+
[[bin]]
51+
name = "fuzz_sign"
52+
path = "fuzz_targets/fuzz_sign.rs"
53+
test = false
54+
doc = false

fuzz/README.md

+25-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
This is a work in progress, but good results currently.
22

3-
Harness code is in fuzz/fuzz_targets/fuzz_all.rs. The Cargo.toml file specifies
4-
that overflow-checks and debug-assertions are enabled (so the fuzzer can find these panics).
3+
Harness code is in fuzz/fuzz_targets/*. The Cargo.toml file specifies that overflow-checks and
4+
debug-assertions are enabled (so the fuzzer can find these panics).
55

66
See <https://rust-fuzz.github.io/book/introduction.html>
77

@@ -14,20 +14,38 @@ $ for i in $(seq 1 2); do head -c 6292 </dev/urandom > corpus/fuzz_all/seed$i; d
1414
$ dd if=/dev/zero bs=1 count=6292 | tr '\0x00' '\377' > corpus/fuzz_all/seed3
1515
$ cargo fuzz run fuzz_all -j 4 -- -max_total_time=1000 # run three times
1616
17-
#1020: cov: 13306 ft: 9885 corp: 153 exec/s 0 oom/timeout/crash: 0/0/0 time: 867s job: 50 dft_time: 0
18-
#1046: cov: 13306 ft: 9910 corp: 155 exec/s 0 oom/timeout/crash: 0/0/0 time: 883s job: 51 dft_time: 0
19-
#1096: cov: 13306 ft: 9912 corp: 156 exec/s 0 oom/timeout/crash: 0/0/0 time: 905s job: 52 dft_time: 0
20-
#1132: cov: 13306 ft: 9912 corp: 156 exec/s 0 oom/timeout/crash: 0/0/0 time: 914s job: 53 dft_time: 0
2117
#1184: cov: 13306 ft: 9913 corp: 157 exec/s 0 oom/timeout/crash: 0/0/0 time: 927s job: 54 dft_time: 0
2218
#1245: cov: 13306 ft: 9985 corp: 160 exec/s 1 oom/timeout/crash: 0/0/0 time: 945s job: 55 dft_time: 0
2319
#1270: cov: 13306 ft: 9985 corp: 160 exec/s 0 oom/timeout/crash: 0/0/0 time: 964s job: 56 dft_time: 0
2420
#1297: cov: 13306 ft: 9990 corp: 162 exec/s 0 oom/timeout/crash: 0/0/0 time: 979s job: 57 dft_time: 0
2521
#1331: cov: 13306 ft: 9997 corp: 164 exec/s 0 oom/timeout/crash: 0/0/0 time: 996s job: 58 dft_time: 0
2622
INFO: fuzzed for 1005 seconds, wrapping up soon
2723
INFO: exiting: 0 time: 1019s
24+
25+
26+
$ cargo fuzz run fuzz_sign -j 4 -- -max_total_time=1000
27+
28+
#241: cov: 18829 ft: 12381 corp: 18 exec/s 0 oom/timeout/crash: 0/0/0 time: 890s job: 63 dft_time: 0
29+
#247: cov: 18829 ft: 12474 corp: 19 exec/s 0 oom/timeout/crash: 0/0/0 time: 905s job: 64 dft_time: 0
30+
#253: cov: 18829 ft: 12575 corp: 20 exec/s 0 oom/timeout/crash: 0/0/0 time: 952s job: 65 dft_time: 0
31+
#259: cov: 18829 ft: 12588 corp: 21 exec/s 0 oom/timeout/crash: 0/0/0 time: 968s job: 66 dft_time: 0
32+
#266: cov: 18829 ft: 12702 corp: 22 exec/s 0 oom/timeout/crash: 0/0/0 time: 998s job: 67 dft_time: 0
33+
INFO: fuzzed for 1014 seconds, wrapping up soon
34+
INFO: exiting: 0 time: 1047s
35+
36+
37+
$ cargo fuzz run fuzz_verify -j 4 -- -max_total_time=1000
38+
39+
#307: cov: 18818 ft: 12996 corp: 30 exec/s 0 oom/timeout/crash: 0/0/0 time: 915s job: 57 dft_time: 0
40+
#314: cov: 18818 ft: 13023 corp: 32 exec/s 0 oom/timeout/crash: 0/0/0 time: 934s job: 58 dft_time: 0
41+
#321: cov: 18818 ft: 13040 corp: 33 exec/s 0 oom/timeout/crash: 0/0/0 time: 945s job: 59 dft_time: 0
42+
#328: cov: 18818 ft: 13063 corp: 34 exec/s 0 oom/timeout/crash: 0/0/0 time: 964s job: 60 dft_time: 0
43+
#336: cov: 18818 ft: 13078 corp: 35 exec/s 0 oom/timeout/crash: 0/0/0 time: 998s job: 61 dft_time: 0
44+
INFO: fuzzed for 1018 seconds, wrapping up soon
45+
INFO: exiting: 0 time: 1031s
2846
~~~
2947

30-
Coverage status of ml_dsa_44 is robust, see:
48+
Coverage status of, for example, ml_dsa_44 is robust, see:
3149

3250
~~~
3351
$ cargo fuzz coverage fuzz_all

fuzz/fuzz_targets/fuzz_sign.rs

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#![no_main]
2+
use fips204::traits::{KeyGen, SerDes, Signer, Verifier};
3+
use fips204::Ph;
4+
use fips204::{ml_dsa_44, ml_dsa_65, ml_dsa_87};
5+
use libfuzzer_sys::fuzz_target;
6+
use rand_chacha::ChaCha20Rng;
7+
use rand_core::{CryptoRngCore, SeedableRng};
8+
9+
10+
// Helper to create deterministic RNG from data
11+
fn create_rng(seed_data: &[u8]) -> ChaCha20Rng {
12+
let seed = if seed_data.len() >= 32 {
13+
let mut arr = [0u8; 32];
14+
arr.copy_from_slice(&seed_data[..32]);
15+
arr
16+
} else {
17+
let mut arr = [0u8; 32];
18+
arr[..seed_data.len()].copy_from_slice(seed_data);
19+
arr
20+
};
21+
ChaCha20Rng::from_seed(seed)
22+
}
23+
24+
25+
// Helper function to test signing operations for a specific parameter set
26+
fn fuzz_signer_for_params<S, V>(
27+
data: &[u8], rng: &mut impl CryptoRngCore, keypair: &(V, S), ctx: &[u8],
28+
) where
29+
S: Signer<PublicKey = V>,
30+
V: Verifier<Signature = S::Signature> + SerDes + Clone,
31+
<S as Signer>::Signature: PartialEq,
32+
<V as SerDes>::ByteArray: PartialEq,
33+
{
34+
let (pk, sk) = keypair;
35+
36+
// Test regular signing
37+
if let Ok(sig1) = sk.try_sign_with_rng(rng, data, ctx) {
38+
// Verify the signature works
39+
assert!(pk.clone().verify(data, &sig1, ctx));
40+
41+
// Test that signing the same message twice produces different signatures
42+
if let Ok(sig2) = sk.try_sign_with_rng(rng, data, ctx) {
43+
// Signatures should be different (due to randomization)
44+
assert!(sig1 != sig2);
45+
// But both should verify
46+
assert!(pk.clone().verify(data, &sig2, ctx));
47+
}
48+
49+
// Verify public key derivation
50+
let derived_pk = sk.get_public_key();
51+
assert!(derived_pk.clone().into_bytes() == pk.clone().into_bytes());
52+
assert!(derived_pk.verify(data, &sig1, ctx));
53+
}
54+
55+
// Test hash signing with different hash functions
56+
for ph in [Ph::SHA256, Ph::SHA512, Ph::SHAKE128] {
57+
if let Ok(sig) = sk.try_hash_sign_with_rng(rng, data, ctx, &ph) {
58+
// Verify the hash signature works
59+
assert!(pk.hash_verify(data, &sig, ctx, &ph));
60+
61+
// Test that hash signing the same message twice produces different signatures
62+
if let Ok(sig2) = sk.try_hash_sign_with_rng(rng, data, ctx, &ph) {
63+
assert!(sig != sig2);
64+
assert!(pk.hash_verify(data, &sig2, ctx, &ph));
65+
}
66+
67+
// Verify signature doesn't work with wrong hash function
68+
let wrong_ph = match ph {
69+
Ph::SHA256 => Ph::SHA512,
70+
_ => Ph::SHA256,
71+
};
72+
assert!(!pk.hash_verify(data, &sig, ctx, &wrong_ph));
73+
}
74+
}
75+
}
76+
77+
78+
fuzz_target!(|data: &[u8]| {
79+
// Skip empty inputs
80+
if data.is_empty() {
81+
return;
82+
}
83+
84+
// Create deterministic RNG from first part of input
85+
let mut rng = create_rng(data);
86+
87+
// Generate keypairs using the RNG
88+
let ml_dsa_44_keypair = ml_dsa_44::KG::try_keygen_with_rng(&mut rng).unwrap();
89+
let ml_dsa_65_keypair = ml_dsa_65::KG::try_keygen_with_rng(&mut rng).unwrap();
90+
let ml_dsa_87_keypair = ml_dsa_87::KG::try_keygen_with_rng(&mut rng).unwrap();
91+
92+
// Use first byte as context length
93+
let ctx_len = (data[0] as usize) % 8;
94+
let (ctx, msg) = data.split_at(ctx_len.min(data.len()));
95+
96+
// Test all parameter sets
97+
fuzz_signer_for_params(msg, &mut rng, &ml_dsa_44_keypair, ctx);
98+
fuzz_signer_for_params(msg, &mut rng, &ml_dsa_65_keypair, ctx);
99+
fuzz_signer_for_params(msg, &mut rng, &ml_dsa_87_keypair, ctx);
100+
101+
// Test edge cases
102+
if let Some((pk, sk)) = Some(&ml_dsa_65_keypair) {
103+
// Test empty message
104+
if let Ok(sig) = sk.try_sign_with_rng(&mut rng, &[], ctx) {
105+
assert!(pk.verify(&[], &sig, ctx));
106+
}
107+
108+
// Test empty context
109+
if let Ok(sig) = sk.try_sign_with_rng(&mut rng, msg, &[]) {
110+
assert!(pk.verify(msg, &sig, &[]));
111+
}
112+
113+
// Test large message (if we have enough data)
114+
if msg.len() > 100 {
115+
if let Ok(sig) = sk.try_sign_with_rng(&mut rng, msg, ctx) {
116+
assert!(pk.verify(msg, &sig, ctx));
117+
}
118+
}
119+
120+
// Test signing with different RNG seeds
121+
if msg.len() > 32 {
122+
let mut different_rng = create_rng(&msg[..32]);
123+
if let Ok(sig1) = sk.try_sign_with_rng(&mut rng, msg, ctx) {
124+
if let Ok(sig2) = sk.try_sign_with_rng(&mut different_rng, msg, ctx) {
125+
assert!(sig1 != sig2);
126+
assert!(pk.verify(msg, &sig1, ctx));
127+
assert!(pk.verify(msg, &sig2, ctx));
128+
}
129+
}
130+
}
131+
}
132+
});

fuzz/fuzz_targets/fuzz_verify.rs

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#![no_main]
2+
use fips204::traits::{KeyGen, SerDes, Signer, Verifier};
3+
use fips204::Ph;
4+
use fips204::{ml_dsa_44, ml_dsa_65, ml_dsa_87};
5+
use libfuzzer_sys::fuzz_target;
6+
7+
8+
// Helper function to test a specific parameter set
9+
fn fuzz_verify_for_params<S, V>(data: &[u8], keypair: &(V, S), ctx: &[u8])
10+
where
11+
S: Signer,
12+
V: Verifier<Signature = S::Signature>,
13+
{
14+
let (pk, sk) = keypair;
15+
16+
// Test regular verify
17+
if let Ok(sig) = sk.try_sign(data, ctx) {
18+
// Valid signature should verify
19+
assert!(pk.verify(data, &sig, ctx));
20+
21+
// Modified message should not verify
22+
if !data.is_empty() {
23+
let mut modified_msg = data.to_vec();
24+
modified_msg[0] ^= 1;
25+
assert!(!pk.verify(&modified_msg, &sig, ctx));
26+
}
27+
28+
// Modified context should not verify
29+
let mut modified_ctx = ctx.to_vec();
30+
modified_ctx.push(1);
31+
assert!(!pk.verify(data, &sig, &modified_ctx));
32+
}
33+
34+
// Test hash verify
35+
for ph in [Ph::SHA256, Ph::SHA512, Ph::SHAKE128] {
36+
if let Ok(sig) = sk.try_hash_sign(data, ctx, &ph) {
37+
// Valid signature should verify
38+
assert!(pk.hash_verify(data, &sig, ctx, &ph));
39+
40+
// Modified message should not verify
41+
if !data.is_empty() {
42+
let mut modified_msg = data.to_vec();
43+
modified_msg[0] ^= 1;
44+
assert!(!pk.hash_verify(&modified_msg, &sig, ctx, &ph));
45+
}
46+
47+
// Modified context should not verify
48+
let mut modified_ctx = ctx.to_vec();
49+
modified_ctx.push(1);
50+
assert!(!pk.hash_verify(data, &sig, &modified_ctx, &ph));
51+
52+
// Different hash function should not verify
53+
let different_ph = match ph {
54+
Ph::SHA256 => Ph::SHA512,
55+
_ => Ph::SHA256,
56+
};
57+
assert!(!pk.hash_verify(data, &sig, ctx, &different_ph));
58+
}
59+
}
60+
}
61+
62+
63+
fuzz_target!(|data: &[u8]| {
64+
// Skip empty inputs
65+
if data.is_empty() {
66+
return;
67+
}
68+
69+
// Generate static keypairs (for speed)
70+
let seed = [42u8; 32];
71+
let ml_dsa_44_keypair = ml_dsa_44::KG::keygen_from_seed(&seed);
72+
let ml_dsa_65_keypair = ml_dsa_65::KG::keygen_from_seed(&seed);
73+
let ml_dsa_87_keypair = ml_dsa_87::KG::keygen_from_seed(&seed);
74+
75+
// Use first byte as context length
76+
let ctx_len = (data[0] as usize) % 8;
77+
let (ctx, msg) = data.split_at(ctx_len.min(data.len()));
78+
79+
// Test all parameter sets
80+
fuzz_verify_for_params(msg, &ml_dsa_44_keypair, ctx);
81+
fuzz_verify_for_params(msg, &ml_dsa_65_keypair, ctx);
82+
fuzz_verify_for_params(msg, &ml_dsa_87_keypair, ctx);
83+
84+
// Test serialization/deserialization
85+
if !msg.is_empty() {
86+
let (pk, sk) = &ml_dsa_65_keypair;
87+
if let Ok(sig) = sk.try_sign(msg, ctx) {
88+
// Serialize and deserialize the public key
89+
let pk_bytes = pk.clone().into_bytes();
90+
if let Ok(recovered_pk) = ml_dsa_65::PublicKey::try_from_bytes(pk_bytes) {
91+
assert!(recovered_pk.verify(msg, &sig, ctx));
92+
}
93+
}
94+
}
95+
});

0 commit comments

Comments
 (0)