Skip to content

Commit 72e7acf

Browse files
committed
tests: Add new fuzz testing for PeerHandshake
Write a new fuzz test for the PeerHandshake module that is able to generate failure paths that occur after a partial handshake sequence has been completed. To enable this, a new testing object FuzzGen has been introduced that can deterministically generate bytes and bools based on the random fuzz input. These building blocks are enough for the test code to generates different execution paths and complete partial phases of the handshake protocol before generating an error. When going through a test cycle where the handshake completes, it will also verify that the initiator and responder can communicate successfully through the returned conduits sending variable length data and validating the contents.
1 parent e4efd65 commit 72e7acf

File tree

1 file changed

+273
-58
lines changed

1 file changed

+273
-58
lines changed

fuzz/src/peer_crypt.rs

Lines changed: 273 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,81 +7,241 @@
77
// You may not use this file except in accordance with one or both of these
88
// licenses.
99

10-
use lightning::ln::peer_channel_encryptor::PeerChannelEncryptor;
10+
use bitcoin::secp256k1;
1111

1212
use bitcoin::secp256k1::key::{PublicKey,SecretKey};
13-
13+
use lightning::ln::peers::conduit::Conduit;
14+
use lightning::ln::peers::handshake::PeerHandshake;
1415
use utils::test_logger;
1516

16-
#[inline]
17-
fn slice_to_be16(v: &[u8]) -> u16 {
18-
((v[0] as u16) << 8*1) |
19-
((v[1] as u16) << 8*0)
17+
// Test structure used to generate "random" values based on input data. It is used throughout
18+
// the various test cases to send random messages between nodes and to ensure the code does not fail
19+
// unexpectedly.
20+
pub struct FuzzGen<'a> {
21+
read_pos: usize,
22+
data: &'a [u8],
2023
}
2124

22-
#[inline]
23-
pub fn do_test(data: &[u8]) {
24-
let mut read_pos = 0;
25-
macro_rules! get_slice {
26-
($len: expr) => {
27-
{
28-
let slice_len = $len as usize;
29-
if data.len() < read_pos + slice_len {
30-
return;
31-
}
32-
read_pos += slice_len;
33-
&data[read_pos - slice_len..read_pos]
25+
impl<'a> FuzzGen<'a> {
26+
pub fn new(data: &'a [u8]) -> Self {
27+
Self {
28+
read_pos: 0,
29+
data
30+
}
31+
}
32+
33+
pub fn generate_bytes(&mut self, num: usize) -> Result<&'a [u8], String> {
34+
if self.data.len() < self.read_pos + num {
35+
return Err("out of bytes".to_string());
36+
}
37+
38+
self.read_pos += num;
39+
Ok(&self.data[self.read_pos - num..self.read_pos])
40+
}
41+
42+
pub fn generate_secret_key(&mut self) -> Result<SecretKey, String> {
43+
// Loop through the input 32 bytes at a time until a valid
44+
// secret key can be created or we run out of bytes
45+
loop {
46+
match SecretKey::from_slice(self.generate_bytes(32)?) {
47+
Ok(key) => { return Ok(key) },
48+
_ => {}
3449
}
3550
}
3651
}
3752

38-
let our_network_key = match SecretKey::from_slice(get_slice!(32)) {
39-
Ok(key) => key,
40-
Err(_) => return,
53+
pub fn generate_bool(&mut self) -> Result<bool, String> {
54+
let next_byte = self.generate_bytes(1)?;
55+
Ok(next_byte[0] & 1 == 1)
56+
}
57+
}
58+
59+
struct TestCtx {
60+
initiator_static_public_key: PublicKey,
61+
initiator_handshake: PeerHandshake,
62+
responder_static_public_key: PublicKey,
63+
responder_handshake: PeerHandshake,
64+
act1: Vec<u8>
65+
}
66+
67+
impl TestCtx {
68+
fn make(generator: &mut FuzzGen) -> Result<Self, String> {
69+
let curve = secp256k1::Secp256k1::new();
70+
71+
// Generate needed keys for successful handshake
72+
let initiator_static_private_key = generator.generate_secret_key()?;
73+
let initiator_static_public_key = PublicKey::from_secret_key(&curve, &initiator_static_private_key);
74+
let initiator_ephemeral_private_key = generator.generate_secret_key()?;
75+
let responder_static_private_key = generator.generate_secret_key()?;
76+
let responder_static_public_key = PublicKey::from_secret_key(&curve, &responder_static_private_key);
77+
let responder_ephemeral_private_key = generator.generate_secret_key()?;
78+
79+
let (act1, initiator_handshake) = PeerHandshake::create_and_initialize_outbound(&initiator_static_private_key, &responder_static_public_key, &initiator_ephemeral_private_key);
80+
let responder_handshake = PeerHandshake::new_inbound(&responder_static_private_key, &responder_ephemeral_private_key);
81+
82+
Ok(TestCtx {
83+
initiator_static_public_key,
84+
initiator_handshake,
85+
responder_static_public_key,
86+
responder_handshake,
87+
act1
88+
})
89+
}
90+
}
91+
92+
// Common test function that sends encrypted messages between two conduits until the source data
93+
// runs out.
94+
#[inline]
95+
fn do_conduit_tests(generator: &mut FuzzGen, initiator_conduit: &mut Conduit, responder_conduit: &mut Conduit) -> Result<(), String> {
96+
// Keep sending messages back and forth until the input data is consumed
97+
loop {
98+
// Randomly generate message length
99+
let msg_len_raw = generator.generate_bytes(1)?;
100+
let msg_len = msg_len_raw[0] as usize;
101+
102+
// Randomly generate message
103+
let sender_unencrypted_msg = generator.generate_bytes(msg_len)?;
104+
105+
// randomly choose sender of message
106+
let receiver_unencrypted_msg = if generator.generate_bool()? {
107+
let encrypted_msg = initiator_conduit.encrypt(sender_unencrypted_msg);
108+
responder_conduit.decrypt_single_message(Some(&encrypted_msg))
109+
} else {
110+
let encrypted_msg = responder_conduit.encrypt(sender_unencrypted_msg);
111+
initiator_conduit.decrypt_single_message(Some(&encrypted_msg))
112+
};
113+
114+
assert_eq!(sender_unencrypted_msg, receiver_unencrypted_msg.unwrap().as_slice());
115+
}
116+
}
117+
118+
// This test completes a valid handshake based on "random" private keys and then sends variable
119+
// length encrypted messages between two conduits to validate that they can communicate.
120+
#[inline]
121+
fn do_completed_handshake_test(generator: &mut FuzzGen) -> Result<(), String> {
122+
let mut test_ctx = TestCtx::make(generator)?;
123+
124+
// The handshake should complete with any valid private keys
125+
let act2 = test_ctx.responder_handshake.process_act(&test_ctx.act1).unwrap().0.unwrap();
126+
let (act3, (mut initiator_conduit, responder_pubkey)) = match test_ctx.initiator_handshake.process_act(&act2) {
127+
Ok((Some(act3), Some((conduit, remote_pubkey)))) => {
128+
(act3, (conduit, remote_pubkey))
129+
}
130+
_ => panic!("handshake failed")
41131
};
42-
let ephemeral_key = match SecretKey::from_slice(get_slice!(32)) {
43-
Ok(key) => key,
44-
Err(_) => return,
132+
133+
let (mut responder_conduit, initiator_pubkey) = match test_ctx.responder_handshake.process_act(&act3) {
134+
Ok((None, Some((conduit, remote_pubkey)))) => {
135+
(conduit, remote_pubkey)
136+
}
137+
_ => panic!("handshake failed")
45138
};
46139

47-
let mut crypter = if get_slice!(1)[0] != 0 {
48-
let their_pubkey = match PublicKey::from_slice(get_slice!(33)) {
49-
Ok(key) => key,
50-
Err(_) => return,
51-
};
52-
let mut crypter = PeerChannelEncryptor::new_outbound(their_pubkey, ephemeral_key);
53-
crypter.get_act_one();
54-
match crypter.process_act_two(get_slice!(50), &our_network_key) {
55-
Ok(_) => {},
56-
Err(_) => return,
140+
// The handshake should complete with each peer knowing the static_public_key of the remote peer
141+
assert_eq!(initiator_pubkey, test_ctx.initiator_static_public_key);
142+
assert_eq!(responder_pubkey, test_ctx.responder_static_public_key);
143+
144+
// The nodes should be able to communicate over the conduit
145+
do_conduit_tests(generator, &mut initiator_conduit, &mut responder_conduit)?;
146+
147+
unreachable!();
148+
}
149+
150+
// This test variant goes through the peer handshake between the initiator and responder with
151+
// "random" failures to verify that nothing panics.
152+
// In the event of an error from process_act() we validate that the input data was generated
153+
// randomly to ensure real act generation still produces valid transitions.
154+
#[inline]
155+
fn do_handshake_test(generator: &mut FuzzGen) -> Result<(), String> {
156+
let mut test_ctx = TestCtx::make(generator)?;
157+
let mut used_generated_data = false;
158+
159+
// Possibly generate bad data for act1 and ensure that the responder does not panic
160+
let mut act1 = test_ctx.act1;
161+
if generator.generate_bool()? {
162+
act1 = (generator.generate_bytes(50)?).to_vec();
163+
used_generated_data = true;
164+
}
165+
166+
let mut act2 = match test_ctx.responder_handshake.process_act(&act1) {
167+
Ok((Some(act2), None)) => {
168+
act2
57169
}
58-
assert!(crypter.is_ready_for_encryption());
59-
crypter
60-
} else {
61-
let mut crypter = PeerChannelEncryptor::new_inbound(&our_network_key);
62-
match crypter.process_act_one_with_keys(get_slice!(50), &our_network_key, ephemeral_key) {
63-
Ok(_) => {},
64-
Err(_) => return,
170+
Err(_) => {
171+
assert!(used_generated_data);
172+
return Err("generated expected failure with bad data".to_string());
65173
}
66-
match crypter.process_act_three(get_slice!(66)) {
67-
Ok(_) => {},
68-
Err(_) => return,
174+
_ => panic!("responder required to output act bytes and no conduit/pubkey")
175+
};
176+
177+
// Possibly generate bad data for act2 and ensure that the initiator does not panic
178+
if generator.generate_bool()? {
179+
act2 = (generator.generate_bytes(50)?).to_vec();
180+
used_generated_data = true;
181+
}
182+
183+
let (mut act3, (mut initiator_conduit, responder_pubkey)) = match test_ctx.initiator_handshake.process_act(&act2) {
184+
Ok((Some(act3), Some((conduit, remote_pubkey)))) => {
185+
(act3, (conduit, remote_pubkey))
69186
}
70-
assert!(crypter.is_ready_for_encryption());
71-
crypter
187+
Err(_) => {
188+
assert!(used_generated_data);
189+
return Err("generated expected failure with bad data".to_string());
190+
}
191+
_ => panic!("initiator required to output act bytes and no conduit/pubkey")
72192
};
73-
loop {
74-
if get_slice!(1)[0] == 0 {
75-
crypter.encrypt_message(get_slice!(slice_to_be16(get_slice!(2))));
76-
} else {
77-
let len = match crypter.decrypt_length_header(get_slice!(16+2)) {
78-
Ok(len) => len,
79-
Err(_) => return,
80-
};
81-
match crypter.decrypt_message(get_slice!(len as usize + 16)) {
82-
Ok(_) => {},
83-
Err(_) => return,
84-
}
193+
194+
// Possibly generate bad data for act3 and ensure that the responder does not panic
195+
if generator.generate_bool()? {
196+
act3 = (generator.generate_bytes(66)?).to_vec();
197+
used_generated_data = true;
198+
}
199+
200+
let (mut responder_conduit, initiator_pubkey) = match test_ctx.responder_handshake.process_act(&act3) {
201+
Ok((None, Some((conduit, remote_pubkey)))) => {
202+
(conduit, remote_pubkey)
203+
}
204+
Err(_) => {
205+
// extremely unlikely we randomly generate a correct act3, but if so.. reset this
206+
assert!(used_generated_data);
207+
return Err("generated expected failure with bad data".to_string());
208+
},
209+
_ => panic!("responder required to output conduit/remote pubkey and no act bytes")
210+
};
211+
212+
// The handshake should complete with each peer knowing the static_public_key of the remote peer
213+
if initiator_pubkey != test_ctx.initiator_static_public_key {
214+
assert!(used_generated_data);
215+
return Ok(());
216+
}
217+
if responder_pubkey != test_ctx.responder_static_public_key {
218+
assert!(used_generated_data);
219+
return Ok(());
220+
}
221+
222+
// The nodes should be able to communicate over the conduit
223+
do_conduit_tests(generator, &mut initiator_conduit, &mut responder_conduit)?;
224+
225+
unreachable!();
226+
}
227+
228+
#[inline]
229+
fn do_test(data: &[u8]) {
230+
let mut generator = FuzzGen::new(data);
231+
232+
// Based on a "random" bool, decide which test variant to run
233+
let do_valid_handshake = match generator.generate_bool() {
234+
Ok(value) => { value },
235+
_ => { return }
236+
};
237+
238+
if do_valid_handshake {
239+
match do_completed_handshake_test(&mut generator) {
240+
_ => {}
241+
}
242+
} else {
243+
match do_handshake_test(&mut generator) {
244+
_ => {}
85245
}
86246
}
87247
}
@@ -94,3 +254,58 @@ pub fn peer_crypt_test<Out: test_logger::Output>(data: &[u8], _out: Out) {
94254
pub extern "C" fn peer_crypt_run(data: *const u8, datalen: usize) {
95255
do_test(unsafe { std::slice::from_raw_parts(data, datalen) });
96256
}
257+
258+
#[cfg(test)]
259+
mod test {
260+
use super::*;
261+
262+
#[test]
263+
fn data_generator_empty() {
264+
let mut generator = FuzzGen::new(&[]);
265+
assert_eq!(generator.generate_bool().err(), Some("out of bytes".to_string()));
266+
}
267+
268+
#[test]
269+
fn data_generator_bool_true() {
270+
let mut generator = FuzzGen::new(&[1]);
271+
assert!(generator.generate_bool().unwrap());
272+
}
273+
274+
#[test]
275+
fn data_generator_bool_false() {
276+
let mut generator = FuzzGen::new(&[0]);
277+
assert!(!generator.generate_bool().unwrap());
278+
}
279+
280+
#[test]
281+
fn data_generator_bool_then_error() {
282+
let mut generator = FuzzGen::new(&[1]);
283+
assert!(generator.generate_bool().unwrap());
284+
assert_eq!(generator.generate_bool().err(), Some("out of bytes".to_string()));
285+
}
286+
287+
#[test]
288+
fn data_generator_bytes_too_many() {
289+
let mut generator = FuzzGen::new(&[1, 2, 3, 4]);
290+
assert_eq!(generator.generate_bytes(5).err(), Some("out of bytes".to_string()));
291+
}
292+
293+
#[test]
294+
fn data_generator_bytes() {
295+
let input = [1, 2, 3, 4];
296+
let mut generator = FuzzGen::new(&input);
297+
let result = generator.generate_bytes(4).unwrap();
298+
assert_eq!(result, input);
299+
}
300+
301+
#[test]
302+
fn data_generator_bytes_sequential() {
303+
let input = [1, 2, 3, 4];
304+
let mut generator = FuzzGen::new(&input);
305+
let result = generator.generate_bytes(2).unwrap();
306+
assert_eq!(result, &input[..2]);
307+
let result = generator.generate_bytes(2).unwrap();
308+
assert_eq!(result, &input[2..]);
309+
assert_eq!(generator.generate_bytes(1).err(), Some("out of bytes".to_string()));
310+
}
311+
}

0 commit comments

Comments
 (0)