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
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ _default:
bench:
cargo +{{NIGHTLY_TOOLCHAIN}} bench --package bip324 --bench cipher_session

# Run fuzz test: handshake.
@fuzz target="handshake" time="60":
# Run fuzz test: receive_key, receive_garbage, receive_version.
@fuzz target="receive_garbage" time="60":
cargo install cargo-fuzz@0.12.0
cd protocol && cargo +{{NIGHTLY_TOOLCHAIN}} fuzz run {{target}} -- -max_total_time={{time}}

Expand Down
145 changes: 92 additions & 53 deletions protocol/benches/cipher_session.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,99 @@
// SPDX-License-Identifier: CC0-1.0

#![feature(test)]

extern crate test;

use bip324::{CipherSession, Handshake, InboundCipher, Network, OutboundCipher, PacketType, Role};
use bip324::{
CipherSession, GarbageResult, Handshake, InboundCipher, Initialized, Network, OutboundCipher,
PacketType, ReceivedKey, Role, VersionResult, NUM_LENGTH_BYTES,
};
use test::{black_box, Bencher};

fn create_cipher_session_pair() -> (CipherSession, CipherSession) {
// Create a proper handshake between Alice and Bob.
let mut alice_init_buffer = vec![0u8; 64];
let mut alice_handshake = Handshake::new(
Network::Bitcoin,
Role::Initiator,
None,
&mut alice_init_buffer,
)
.unwrap();

let mut bob_init_buffer = vec![0u8; 100];
let mut bob_handshake = Handshake::new(
Network::Bitcoin,
Role::Responder,
None,
&mut bob_init_buffer,
)
.unwrap();

// Bob completes materials with Alice's key.
bob_handshake
.complete_materials(
alice_init_buffer[..64].try_into().unwrap(),
&mut bob_init_buffer[64..],
None,
)
// Send Alice's key.
let alice_handshake = Handshake::<Initialized>::new(Network::Bitcoin, Role::Initiator).unwrap();
let mut alice_key_buffer = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
let alice_handshake = alice_handshake
.send_key(None, &mut alice_key_buffer)
.unwrap();

// Send Bob's key
let bob_handshake = Handshake::<Initialized>::new(Network::Bitcoin, Role::Responder).unwrap();
let mut bob_key_buffer = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
let bob_handshake = bob_handshake.send_key(None, &mut bob_key_buffer).unwrap();

// Alice receives Bob's key.
let alice_handshake = alice_handshake
.receive_key(bob_key_buffer.try_into().unwrap())
.unwrap();

// Bob receives Alice's key.
let bob_handshake = bob_handshake
.receive_key(alice_key_buffer.try_into().unwrap())
.unwrap();

// Alice sends version.
let mut alice_version_buffer = vec![0u8; Handshake::<ReceivedKey>::send_version_len(None)];
let alice_handshake = alice_handshake
.send_version(&mut alice_version_buffer, None)
.unwrap();

// Alice completes materials with Bob's key.
let mut alice_response_buffer = vec![0u8; 36];
alice_handshake
.complete_materials(
bob_init_buffer[..64].try_into().unwrap(),
&mut alice_response_buffer,
None,
)
// Bob sends version.
let mut bob_version_buffer = vec![0u8; Handshake::<ReceivedKey>::send_version_len(None)];
let bob_handshake = bob_handshake
.send_version(&mut bob_version_buffer, None)
.unwrap();

// Authenticate.
let mut packet_buffer = vec![0u8; 4096];
alice_handshake
.authenticate_garbage_and_version(&bob_init_buffer[64..], &mut packet_buffer)
// Alice receives Bob's version.
// First handle Bob's garbage terminator
let (mut alice_handshake, consumed) = match alice_handshake
.receive_garbage(&bob_version_buffer)
.unwrap()
{
GarbageResult::FoundGarbage {
handshake,
consumed_bytes,
} => (handshake, consumed_bytes),
GarbageResult::NeedMoreData(_) => panic!("Should have found garbage terminator"),
};

// Process Bob's version packet
let remaining = &bob_version_buffer[consumed..];
let packet_len = alice_handshake
.decrypt_packet_len(remaining[..NUM_LENGTH_BYTES].try_into().unwrap())
.unwrap();
bob_handshake
.authenticate_garbage_and_version(&alice_response_buffer, &mut packet_buffer)
let mut packet = remaining[NUM_LENGTH_BYTES..NUM_LENGTH_BYTES + packet_len].to_vec();

let alice = match alice_handshake.receive_version(&mut packet).unwrap() {
VersionResult::Complete { cipher } => cipher,
VersionResult::Decoy(_) => panic!("Should have completed"),
};

// Bob receives Alice's version.
// First handle Alice's garbage terminator
let (mut bob_handshake, consumed) = match bob_handshake
.receive_garbage(&alice_version_buffer)
.unwrap()
{
GarbageResult::FoundGarbage {
handshake,
consumed_bytes,
} => (handshake, consumed_bytes),
GarbageResult::NeedMoreData(_) => panic!("Should have found garbage terminator"),
};

// Process Alice's version packet
let remaining = &alice_version_buffer[consumed..];
let packet_len = bob_handshake
.decrypt_packet_len(remaining[..NUM_LENGTH_BYTES].try_into().unwrap())
.unwrap();
let mut packet = remaining[NUM_LENGTH_BYTES..NUM_LENGTH_BYTES + packet_len].to_vec();

let alice = alice_handshake.finalize().unwrap();
let bob = bob_handshake.finalize().unwrap();
let bob = match bob_handshake.receive_version(&mut packet).unwrap() {
VersionResult::Complete { cipher } => cipher,
VersionResult::Decoy(_) => panic!("Should have completed"),
};

(alice, bob)
}
Expand All @@ -78,16 +117,16 @@ fn bench_round_trip_small_packet(b: &mut Bencher) {
)
.unwrap();

// Decrypt the length from first 3 bytes (real-world step).
let packet_length = bob
.inbound()
.decrypt_packet_len(black_box(encrypted[0..3].try_into().unwrap()));
// Decrypt the length from first NUM_LENGTH_BYTES bytes (real-world step).
let packet_length = bob.inbound().decrypt_packet_len(black_box(
encrypted[0..NUM_LENGTH_BYTES].try_into().unwrap(),
));

// Decrypt the payload using the decrypted length.
let mut decrypted = vec![0u8; InboundCipher::decryption_buffer_len(packet_length)];
bob.inbound()
.decrypt(
black_box(&encrypted[3..3 + packet_length]),
black_box(&encrypted[NUM_LENGTH_BYTES..NUM_LENGTH_BYTES + packet_length]),
&mut decrypted,
None,
)
Expand Down Expand Up @@ -117,16 +156,16 @@ fn bench_round_trip_large_packet(b: &mut Bencher) {
)
.unwrap();

// Decrypt the length from first 3 bytes (real-world step).
let packet_length = bob
.inbound()
.decrypt_packet_len(black_box(encrypted[0..3].try_into().unwrap()));
// Decrypt the length from first NUM_LENGTH_BYTES bytes (real-world step).
let packet_length = bob.inbound().decrypt_packet_len(black_box(
encrypted[0..NUM_LENGTH_BYTES].try_into().unwrap(),
));

// Decrypt the payload using the decrypted length.
let mut decrypted = vec![0u8; InboundCipher::decryption_buffer_len(packet_length)];
bob.inbound()
.decrypt(
black_box(&encrypted[3..3 + packet_length]),
black_box(&encrypted[NUM_LENGTH_BYTES..NUM_LENGTH_BYTES + packet_length]),
&mut decrypted,
None,
)
Expand Down
18 changes: 16 additions & 2 deletions protocol/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,22 @@ libfuzzer-sys = "0.4"
bip324 = { path = ".." }

[[bin]]
name = "handshake"
path = "fuzz_targets/handshake.rs"
name = "receive_garbage"
path = "fuzz_targets/receive_garbage.rs"
test = false
doc = false
bench = false

[[bin]]
name = "receive_version"
path = "fuzz_targets/receive_version.rs"
test = false
doc = false
bench = false

[[bin]]
name = "receive_key"
path = "fuzz_targets/receive_key.rs"
test = false
doc = false
bench = false
58 changes: 0 additions & 58 deletions protocol/fuzz/fuzz_targets/handshake.rs

This file was deleted.

63 changes: 63 additions & 0 deletions protocol/fuzz/fuzz_targets/receive_garbage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: CC0-1.0

//! Fuzz test for the receive_garbage function.
//!
//! This focused test fuzzes only the garbage terminator detection logic,
//! which is more effective than trying to fuzz the entire handshake.

#![no_main]
use bip324::{GarbageResult, Handshake, Initialized, Network, ReceivedKey, Role};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
// Set up a valid handshake in the SentVersion state
let initiator = Handshake::<Initialized>::new(Network::Bitcoin, Role::Initiator).unwrap();
let mut initiator_key = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
let initiator = initiator.send_key(None, &mut initiator_key).unwrap();

let responder = Handshake::<Initialized>::new(Network::Bitcoin, Role::Responder).unwrap();
let mut responder_key = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
let responder = responder.send_key(None, &mut responder_key).unwrap();

// Exchange keys using real keys to get valid ECDH shared secrets
let initiator = initiator
.receive_key(responder_key[..64].try_into().unwrap())
.unwrap();
let _responder = responder
.receive_key(initiator_key[..64].try_into().unwrap())
.unwrap();

// Send version to reach SentVersion state
let mut initiator_version = vec![0u8; Handshake::<ReceivedKey>::send_version_len(None)];
let initiator = initiator
.send_version(&mut initiator_version, None)
.unwrap();

// Now fuzz the receive_garbage function with arbitrary data
match initiator.receive_garbage(data) {
Ok(GarbageResult::FoundGarbage {
handshake: _,
consumed_bytes,
}) => {
// Successfully found garbage terminator
// Verify consumed_bytes is reasonable
assert!(consumed_bytes <= data.len());
assert!(consumed_bytes >= 16); // At least the terminator size

// The garbage should be everything before the terminator
let garbage_len = consumed_bytes - 16;
assert!(garbage_len <= 4095); // Max garbage size
}
Ok(GarbageResult::NeedMoreData(_)) => {
// Need more data - valid outcome for short inputs
// This should happen when:
// 1. Buffer is too short to contain terminator
// 2. Buffer doesn't contain the terminator yet
}
Err(_) => {
// Error parsing garbage - valid outcome
// This should happen when:
// 1. No terminator found within max garbage size
}
}
});
43 changes: 43 additions & 0 deletions protocol/fuzz/fuzz_targets/receive_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: CC0-1.0

//! Fuzz test for the receive_key function.
//!
//! This focused test fuzzes the elliptic curve point validation and ECDH logic.

#![no_main]
use bip324::{Handshake, Initialized, Network, Role};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
// Skip if data is not exactly 64 bytes
if data.len() != 64 {
return;
}

// Set up a handshake in the SentKey state
let handshake = Handshake::<Initialized>::new(Network::Bitcoin, Role::Initiator).unwrap();
let mut key_buffer = vec![0u8; Handshake::<Initialized>::send_key_len(None)];
let handshake = handshake.send_key(None, &mut key_buffer).unwrap();

// Fuzz the receive_key function with arbitrary 64-byte data
let mut key_bytes = [0u8; 64];
key_bytes.copy_from_slice(data);

match handshake.receive_key(key_bytes) {
Ok(_handshake) => {
// Successfully processed the key
// This means:
// 1. The 64 bytes represent a valid ElligatorSwift encoding
// 2. The ECDH operation succeeded
// 3. The key derivation worked
// 4. It's not the V1 protocol magic bytes
}
Err(_) => {
// Failed to process the key
// This could be:
// 1. Invalid ElligatorSwift encoding
// 2. V1 protocol detected (first 4 bytes match network magic)
// 3. ECDH or key derivation failure
}
}
});
Loading