-
Notifications
You must be signed in to change notification settings - Fork 190
add fuzz target for the noise_sv2 crate.
#2106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Shourya742
merged 1 commit into
stratum-mining:main
from
lucasbalieiro:add-fuzz-target-noise-sv2
Mar 17, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
fuzz/fuzz_targets/fuzz_noise_handshake_and_roundtrip_encryption.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
lucasbalieiro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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 { | ||
GitGab19 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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]) { | ||
lucasbalieiro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if should_corrupt && !message.is_empty() { | ||
| let index = corruption_index % message.len(); | ||
Shourya742 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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" | ||
| ); | ||
| }); | ||
plebhash marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.