Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ cargo-fuzz = true
libfuzzer-sys = "0.4.10"
arbitrary = { version = "1.4.2", features = ["derive"] }
binary_sv2 = { path = "../sv2/binary-sv2"}
noise_sv2 = { path = "../sv2/noise-sv2" }
parsers_sv2 = { path = "../sv2/parsers-sv2" }
framing_sv2 = { path = "../sv2/framing-sv2" }
codec_sv2 = { path = "../sv2/codec-sv2", features = ["noise_sv2"]}
common_messages_sv2 = { path = "../sv2/subprotocols/common-messages" }
job_declaration_sv2 = { path = "../sv2/subprotocols/job-declaration" }
mining_sv2 = {path = "../sv2/subprotocols/mining" }
template_distribution_sv2 = { path = "../sv2/subprotocols/template-distribution" }
secp256k1 = { version = "0.28.2", default-features = false, features = ["alloc", "rand"] }
rand = { version = "0.8.5", default-features = false }

[[bin]]
name = "deserialize_sv2frame"
Expand Down Expand Up @@ -67,3 +70,8 @@ path = "fuzz_targets/end_to_end_serialization_for_datatypes.rs"
test = false
doc = false

[[bin]]
name = "fuzz_noise_handshake_and_roundtrip_encryption"
path = "fuzz_targets/fuzz_noise_handshake_and_roundtrip_encryption.rs"
test = false
doc = false
186 changes: 186 additions & 0 deletions fuzz/fuzz_targets/fuzz_noise_handshake_and_roundtrip_encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#![no_main]

use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use noise_sv2::{Initiator, Responder};
use secp256k1::{Keypair, Secp256k1, XOnlyPublicKey};

use rand::{rngs::StdRng, SeedableRng};

// Generates a secp256k1 keypair from a given random seed.
// Used to create deterministic keys for both initiator and responder during fuzzing.
//
// Per specification 4.3.1.1: No assumption is made about the parity of Y-coordinate.
// For signing (certificate) and ECDH (handshake) it is not necessary to "grind" the private key.
// see: https://github.com/stratum-mining/sv2-spec/blob/161f21cf2c618327b6c929ee0cf706e85a59f6d9/04-Protocol-Security.md?plain=1#L67
fn generate_key(rand_seed: u64) -> Keypair {
let secp = Secp256k1::new();
let mut rng = StdRng::seed_from_u64(rand_seed);

let (secret_key, _public_key) = secp.generate_keypair(&mut rng);

Keypair::from_secret_key(&secp, &secret_key)
}

// Represents the relationship between the initiator and responder during the Noise handshake.
// This determines whether the initiator knows and trusts the responder's public key.
#[derive(PartialEq, Arbitrary, Debug)]
enum PeerMode {
// Initiator does not know the responder's public key -> performs anonymous handshake
Unknown,
// Initiator knows and expects the responder's specific public key -> authenticated handshake
Known,
// Initiator knows a different peer's key (not the responder's) -> handshake should fail
PeerDifferentFromResponder,
}

// Input structure for the fuzzer, containing all parameters to control the test.
// Generated by the arbitrary crate from raw fuzz input bytes.
#[derive(Debug, Arbitrary)]
struct FuzzInput {
// Controls whether initiator knows responder's public key
peer_mode: PeerMode,
// Certificate validity duration in seconds (passed to responder)
cert_validity: u32,
// Message payload to encrypt and decrypt (fuzzed data)
message: Vec<u8>,
// Whether to corrupt message/handshake with bit flips
should_corrupt_message: bool,
// Index into the message/handshake where corruption occurs
corruption_index: usize,
// Seed for generating cryptographic keys
rand_seed: u64,
}

// Corrupts a message by XORing a byte at the specified index with 0xFF.
// This simulates bit-flipping noise on the wire, testing error handling.
//
// # Arguments
// * `should_corrupt` - If true, performs the corruption
// * `corruption_index` - Index of byte to flip (wrapped via modulo)
// * `message` - Mutable slice to corrupt in-place
fn maybe_corrupt_message(should_corrupt: bool, corruption_index: usize, message: &mut [u8]) {
if should_corrupt && !message.is_empty() {
let index = corruption_index % message.len();
message[index] ^= 0xFF;
}
}

// Main fuzz target that tests the Noise protocol handshake and encrypted message roundtrip.
//
// This fuzzer exercises:
// 1. Full Noise handshake between initiator and responder
// 2. Encryption of messages after successful handshake
// 3. Decryption of messages by the receiver
// 4. Error handling for corrupted data and mismatched peers
//
// The fuzzer validates both success cases (valid handshake + clean message)
// and failure cases (corrupted messages should fail, wrong peer should fail).
fuzz_target!(|input: FuzzInput| {
let mut secret_message = input.message.clone();

// Generate the responder's keypair from the fuzzed seed
let responder_kp = generate_key(input.rand_seed);
let responder_pk: XOnlyPublicKey = responder_kp.public_key().x_only_public_key().0;

// Determine if the initiator knows the responder's key
// This affects the Noise handshake pattern (anonymous vs authenticated)
let known_peer = match input.peer_mode {
// Initiator has no prior knowledge of responder -> performs anonymous handshake
PeerMode::Unknown => None,
// Initiator knows responder's exact public key -> authenticated handshake
PeerMode::Known => Some(responder_pk),
// Initiator knows a DIFFERENT peer's key -> handshake should fail
PeerMode::PeerDifferentFromResponder => Some(
generate_key(input.rand_seed ^ 0xcafe_babe)
.public_key()
.x_only_public_key()
.0,
),
};

// Create initiator (client) - optionally knows the responder
let mut initiator = Initiator::new(known_peer);

// Create responder (server) with the generated keypair and certificate validity
let mut responder = Responder::new(responder_kp, input.cert_validity);

// ========== Handshake Phase ==========

// Step 0: Initiator creates its first handshake message
let first_message = initiator
.step_0()
.expect("Initiator failed first step of handshake");

// Step 1: Responder processes initiator's message and creates response
let (mut second_message, mut responder_state) = responder
.step_1(first_message)
.expect("Responder failed second step of handshake");

// Optionally corrupt the responder's handshake message to test error handling
maybe_corrupt_message(
input.should_corrupt_message,
input.corruption_index,
&mut second_message,
);

// Step 2: Initiator processes responder's message, completing the handshake
// This returns the encrypted session state on success
let mut initiator_state = match initiator.step_2(second_message) {
Ok(state) => {
// If we used a different peer key, handshake should have failed
if input.peer_mode == PeerMode::PeerDifferentFromResponder {
panic!(
"Initiator should have failed handshake with a peer different from responder"
);
}
state
}
Err(e) => {
// If we didn't use a wrong peer AND didn't corrupt, handshake should succeed
if input.peer_mode != PeerMode::PeerDifferentFromResponder
&& !input.should_corrupt_message
{
panic!("Initiator should have succeeded handshake with known peer: {e:?}");
}
return;
}
};

// ========== Encrypted Message Phase ==========

// Initiator encrypts the message using the established session
initiator_state
.encrypt(&mut secret_message)
.expect("Initiator failed to encrypt message");

// Optionally corrupt the encrypted message to test decryption failure handling
maybe_corrupt_message(
input.should_corrupt_message,
input.corruption_index,
&mut secret_message,
);

// Responder attempts to decrypt the message
match responder_state.decrypt(&mut secret_message) {
Ok(()) => {
// If we corrupted the message, decryption should have failed
if input.should_corrupt_message {
panic!("Responder should have failed to decrypt corrupted message");
}
}
Err(e) => {
// If message wasn't corrupted, decryption should succeed
if !input.should_corrupt_message {
panic!("Responder should have succeeded to decrypt uncorrupted message: {e:?}");
}
return;
}
}

// Verify the decrypted message matches the original
assert_eq!(
input.message, secret_message,
"Decrypted message does not match original"
);
});
Loading