Skip to content

Commit 1dcf5f9

Browse files
authored
Merge pull request #2106 from lucasbalieiro/add-fuzz-target-noise-sv2
add fuzz target for the `noise_sv2` crate.
2 parents 78585d7 + 6836a11 commit 1dcf5f9

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

fuzz/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ cargo-fuzz = true
1717
libfuzzer-sys = "0.4.10"
1818
arbitrary = { version = "1.4.2", features = ["derive"] }
1919
binary_sv2 = { path = "../sv2/binary-sv2"}
20+
noise_sv2 = { path = "../sv2/noise-sv2" }
2021
parsers_sv2 = { path = "../sv2/parsers-sv2" }
2122
framing_sv2 = { path = "../sv2/framing-sv2" }
2223
codec_sv2 = { path = "../sv2/codec-sv2", features = ["noise_sv2"]}
2324
common_messages_sv2 = { path = "../sv2/subprotocols/common-messages" }
2425
job_declaration_sv2 = { path = "../sv2/subprotocols/job-declaration" }
2526
mining_sv2 = {path = "../sv2/subprotocols/mining" }
2627
template_distribution_sv2 = { path = "../sv2/subprotocols/template-distribution" }
28+
secp256k1 = { version = "0.28.2", default-features = false, features = ["alloc", "rand"] }
29+
rand = { version = "0.8.5", default-features = false }
2730

2831
[[bin]]
2932
name = "deserialize_sv2frame"
@@ -67,3 +70,8 @@ path = "fuzz_targets/end_to_end_serialization_for_datatypes.rs"
6770
test = false
6871
doc = false
6972

73+
[[bin]]
74+
name = "fuzz_noise_handshake_and_roundtrip_encryption"
75+
path = "fuzz_targets/fuzz_noise_handshake_and_roundtrip_encryption.rs"
76+
test = false
77+
doc = false
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#![no_main]
2+
3+
use arbitrary::Arbitrary;
4+
use libfuzzer_sys::fuzz_target;
5+
use noise_sv2::{Initiator, Responder};
6+
use secp256k1::{Keypair, Secp256k1, XOnlyPublicKey};
7+
8+
use rand::{rngs::StdRng, SeedableRng};
9+
10+
// Generates a secp256k1 keypair from a given random seed.
11+
// Used to create deterministic keys for both initiator and responder during fuzzing.
12+
//
13+
// Per specification 4.3.1.1: No assumption is made about the parity of Y-coordinate.
14+
// For signing (certificate) and ECDH (handshake) it is not necessary to "grind" the private key.
15+
// see: https://github.com/stratum-mining/sv2-spec/blob/161f21cf2c618327b6c929ee0cf706e85a59f6d9/04-Protocol-Security.md?plain=1#L67
16+
fn generate_key(rand_seed: u64) -> Keypair {
17+
let secp = Secp256k1::new();
18+
let mut rng = StdRng::seed_from_u64(rand_seed);
19+
20+
let (secret_key, _public_key) = secp.generate_keypair(&mut rng);
21+
22+
Keypair::from_secret_key(&secp, &secret_key)
23+
}
24+
25+
// Represents the relationship between the initiator and responder during the Noise handshake.
26+
// This determines whether the initiator knows and trusts the responder's public key.
27+
#[derive(PartialEq, Arbitrary, Debug)]
28+
enum PeerMode {
29+
// Initiator does not know the responder's public key -> performs anonymous handshake
30+
Unknown,
31+
// Initiator knows and expects the responder's specific public key -> authenticated handshake
32+
Known,
33+
// Initiator knows a different peer's key (not the responder's) -> handshake should fail
34+
PeerDifferentFromResponder,
35+
}
36+
37+
// Input structure for the fuzzer, containing all parameters to control the test.
38+
// Generated by the arbitrary crate from raw fuzz input bytes.
39+
#[derive(Debug, Arbitrary)]
40+
struct FuzzInput {
41+
// Controls whether initiator knows responder's public key
42+
peer_mode: PeerMode,
43+
// Certificate validity duration in seconds (passed to responder)
44+
cert_validity: u32,
45+
// Message payload to encrypt and decrypt (fuzzed data)
46+
message: Vec<u8>,
47+
// Whether to corrupt message/handshake with bit flips
48+
should_corrupt_message: bool,
49+
// Index into the message/handshake where corruption occurs
50+
corruption_index: usize,
51+
// Seed for generating cryptographic keys
52+
rand_seed: u64,
53+
}
54+
55+
// Corrupts a message by XORing a byte at the specified index with 0xFF.
56+
// This simulates bit-flipping noise on the wire, testing error handling.
57+
//
58+
// # Arguments
59+
// * `should_corrupt` - If true, performs the corruption
60+
// * `corruption_index` - Index of byte to flip (wrapped via modulo)
61+
// * `message` - Mutable slice to corrupt in-place
62+
fn maybe_corrupt_message(should_corrupt: bool, corruption_index: usize, message: &mut [u8]) {
63+
if should_corrupt && !message.is_empty() {
64+
let index = corruption_index % message.len();
65+
message[index] ^= 0xFF;
66+
}
67+
}
68+
69+
// Main fuzz target that tests the Noise protocol handshake and encrypted message roundtrip.
70+
//
71+
// This fuzzer exercises:
72+
// 1. Full Noise handshake between initiator and responder
73+
// 2. Encryption of messages after successful handshake
74+
// 3. Decryption of messages by the receiver
75+
// 4. Error handling for corrupted data and mismatched peers
76+
//
77+
// The fuzzer validates both success cases (valid handshake + clean message)
78+
// and failure cases (corrupted messages should fail, wrong peer should fail).
79+
fuzz_target!(|input: FuzzInput| {
80+
let mut secret_message = input.message.clone();
81+
82+
// Generate the responder's keypair from the fuzzed seed
83+
let responder_kp = generate_key(input.rand_seed);
84+
let responder_pk: XOnlyPublicKey = responder_kp.public_key().x_only_public_key().0;
85+
86+
// Determine if the initiator knows the responder's key
87+
// This affects the Noise handshake pattern (anonymous vs authenticated)
88+
let known_peer = match input.peer_mode {
89+
// Initiator has no prior knowledge of responder -> performs anonymous handshake
90+
PeerMode::Unknown => None,
91+
// Initiator knows responder's exact public key -> authenticated handshake
92+
PeerMode::Known => Some(responder_pk),
93+
// Initiator knows a DIFFERENT peer's key -> handshake should fail
94+
PeerMode::PeerDifferentFromResponder => Some(
95+
generate_key(input.rand_seed ^ 0xcafe_babe)
96+
.public_key()
97+
.x_only_public_key()
98+
.0,
99+
),
100+
};
101+
102+
// Create initiator (client) - optionally knows the responder
103+
let mut initiator = Initiator::new(known_peer);
104+
105+
// Create responder (server) with the generated keypair and certificate validity
106+
let mut responder = Responder::new(responder_kp, input.cert_validity);
107+
108+
// ========== Handshake Phase ==========
109+
110+
// Step 0: Initiator creates its first handshake message
111+
let first_message = initiator
112+
.step_0()
113+
.expect("Initiator failed first step of handshake");
114+
115+
// Step 1: Responder processes initiator's message and creates response
116+
let (mut second_message, mut responder_state) = responder
117+
.step_1(first_message)
118+
.expect("Responder failed second step of handshake");
119+
120+
// Optionally corrupt the responder's handshake message to test error handling
121+
maybe_corrupt_message(
122+
input.should_corrupt_message,
123+
input.corruption_index,
124+
&mut second_message,
125+
);
126+
127+
// Step 2: Initiator processes responder's message, completing the handshake
128+
// This returns the encrypted session state on success
129+
let mut initiator_state = match initiator.step_2(second_message) {
130+
Ok(state) => {
131+
// If we used a different peer key, handshake should have failed
132+
if input.peer_mode == PeerMode::PeerDifferentFromResponder {
133+
panic!(
134+
"Initiator should have failed handshake with a peer different from responder"
135+
);
136+
}
137+
state
138+
}
139+
Err(e) => {
140+
// If we didn't use a wrong peer AND didn't corrupt, handshake should succeed
141+
if input.peer_mode != PeerMode::PeerDifferentFromResponder
142+
&& !input.should_corrupt_message
143+
{
144+
panic!("Initiator should have succeeded handshake with known peer: {e:?}");
145+
}
146+
return;
147+
}
148+
};
149+
150+
// ========== Encrypted Message Phase ==========
151+
152+
// Initiator encrypts the message using the established session
153+
initiator_state
154+
.encrypt(&mut secret_message)
155+
.expect("Initiator failed to encrypt message");
156+
157+
// Optionally corrupt the encrypted message to test decryption failure handling
158+
maybe_corrupt_message(
159+
input.should_corrupt_message,
160+
input.corruption_index,
161+
&mut secret_message,
162+
);
163+
164+
// Responder attempts to decrypt the message
165+
match responder_state.decrypt(&mut secret_message) {
166+
Ok(()) => {
167+
// If we corrupted the message, decryption should have failed
168+
if input.should_corrupt_message {
169+
panic!("Responder should have failed to decrypt corrupted message");
170+
}
171+
}
172+
Err(e) => {
173+
// If message wasn't corrupted, decryption should succeed
174+
if !input.should_corrupt_message {
175+
panic!("Responder should have succeeded to decrypt uncorrupted message: {e:?}");
176+
}
177+
return;
178+
}
179+
}
180+
181+
// Verify the decrypted message matches the original
182+
assert_eq!(
183+
input.message, secret_message,
184+
"Decrypted message does not match original"
185+
);
186+
});

0 commit comments

Comments
 (0)