Skip to content

Commit

Permalink
Add two more fuzzers
Browse files Browse the repository at this point in the history
  • Loading branch information
larseggert committed Apr 11, 2024
1 parent c9d7d8a commit a826a07
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 0 deletions.
15 changes: 15 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ cargo-fuzz = true
neqo-common = { path = "../neqo-common" }
neqo-crypto = { path = "../neqo-crypto" }
neqo-transport = { path = "../neqo-transport" }
test-fixture = { path = "../test-fixture" }

[target.'cfg(not(windows))'.dependencies]
libfuzzer-sys = { version = "0.4" }
Expand All @@ -35,3 +36,17 @@ path = "fuzz_targets/frame.rs"
test = false
doc = false
bench = false

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

[[bin]]
name = "server_initial"
path = "fuzz_targets/server_initial.rs"
test = false
doc = false
bench = false
68 changes: 68 additions & 0 deletions fuzz/fuzz_targets/client_initial.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#![cfg_attr(all(fuzzing, not(windows)), no_main)]

#[cfg(all(fuzzing, not(windows)))]
use libfuzzer_sys::fuzz_target;

#[cfg(all(fuzzing, not(windows)))]
fuzz_target!(|data: &[u8]| {
use fuzz::{
apply_header_protection, decode_initial_header, initial_aead_and_hp,
remove_header_protection,
};
use neqo_common::{Datagram, Encoder, Role};
use neqo_transport::Version;
use test_fixture::{default_client, default_server, now};

let mut client = default_client();
let ci = client.process(None, now()).dgram().expect("a datagram");
let (header, d_cid, s_cid, payload) = decode_initial_header(&ci, Role::Client);
let (aead, hp) = initial_aead_and_hp(d_cid, Role::Client);
let (_, pn) = remove_header_protection(&hp, header, payload);

let mut payload_enc = Encoder::with_capacity(1200);
payload_enc.encode(data); // Add fuzzed data.

// Make a new header with a 1 byte packet number length.
let mut header_enc = Encoder::new();
header_enc
.encode_byte(0xc0) // Initial with 1 byte packet number.
.encode_uint(4, Version::default().wire_version())
.encode_vec(1, d_cid)
.encode_vec(1, s_cid)
.encode_vvec(&[])
.encode_varint(u64::try_from(payload_enc.len() + aead.expansion() + 1).unwrap())
.encode_byte(u8::try_from(pn).unwrap());

let mut ciphertext = header_enc.as_ref().to_vec();
ciphertext.resize(header_enc.len() + payload_enc.len() + aead.expansion(), 0);
let v = aead
.encrypt(
pn,
header_enc.as_ref(),
payload_enc.as_ref(),
&mut ciphertext[header_enc.len()..],
)
.unwrap();
assert_eq!(header_enc.len() + v.len(), ciphertext.len());
// Pad with zero to get up to 1200.
ciphertext.resize(1200, 0);

apply_header_protection(
&hp,
&mut ciphertext,
(header_enc.len() - 1)..header_enc.len(),
);
let fuzzed_ci = Datagram::new(
ci.source(),
ci.destination(),
ci.tos(),
ci.ttl(),
ciphertext,
);

let mut server = default_server();
let _response = server.process(Some(&fuzzed_ci), now());
});

#[cfg(any(not(fuzzing), windows))]
fn main() {}
72 changes: 72 additions & 0 deletions fuzz/fuzz_targets/server_initial.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#![cfg_attr(all(fuzzing, not(windows)), no_main)]

#[cfg(all(fuzzing, not(windows)))]
use libfuzzer_sys::fuzz_target;

#[cfg(all(fuzzing, not(windows)))]
fuzz_target!(|data: &[u8]| {
use fuzz::{
apply_header_protection, decode_initial_header, initial_aead_and_hp,
remove_header_protection,
};
use neqo_common::{Datagram, Encoder, Role};
use neqo_transport::Version;
use test_fixture::{default_client, default_server, now};

let mut client = default_client();
let ci = client.process(None, now()).dgram().expect("a datagram");
let mut server = default_server();
let si = server
.process(Some(&ci), now())
.dgram()
.expect("a datagram");

let (header, d_cid, s_cid, payload) = decode_initial_header(&si, Role::Server);
let (aead, hp) = initial_aead_and_hp(d_cid, Role::Server);
let (_, pn) = remove_header_protection(&hp, header, payload);

let mut payload_enc = Encoder::with_capacity(1200);
payload_enc.encode(data); // Add fuzzed data.

// Make a new header with a 1 byte packet number length.
let mut header_enc = Encoder::new();
header_enc
.encode_byte(0xc0) // Initial with 1 byte packet number.
.encode_uint(4, Version::default().wire_version())
.encode_vec(1, d_cid)
.encode_vec(1, s_cid)
.encode_vvec(&[])
.encode_varint(u64::try_from(payload_enc.len() + aead.expansion() + 1).unwrap())
.encode_byte(u8::try_from(pn).unwrap());

let mut ciphertext = header_enc.as_ref().to_vec();
ciphertext.resize(header_enc.len() + payload_enc.len() + aead.expansion(), 0);
let v = aead
.encrypt(
pn,
header_enc.as_ref(),
payload_enc.as_ref(),
&mut ciphertext[header_enc.len()..],
)
.unwrap();
assert_eq!(header_enc.len() + v.len(), ciphertext.len());
// Pad with zero to get up to 1200.
ciphertext.resize(1200, 0);

apply_header_protection(
&hp,
&mut ciphertext,
(header_enc.len() - 1)..header_enc.len(),
);
let fuzzed_si = Datagram::new(
si.source(),
si.destination(),
si.tos(),
si.ttl(),
ciphertext,
);
let _response = client.process(Some(&fuzzed_si), now());
});

#[cfg(any(not(fuzzing), windows))]
fn main() {}
125 changes: 125 additions & 0 deletions fuzz/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

// These are identical to the functions in neqo_transport/tests/common/mod.rs, but
// those cannot be imported here.

use std::ops::Range;

use neqo_common::{Datagram, Decoder, Role};
use neqo_crypto::{
constants::{TLS_AES_128_GCM_SHA256, TLS_VERSION_1_3},
hkdf,
hp::HpKey,
Aead,
};

// Decode the header of a client Initial packet, returning three values:
// * the entire header short of the packet number,
// * just the DCID,
// * just the SCID, and
// * the protected payload including the packet number.
// Any token is thrown away.
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn decode_initial_header(dgram: &Datagram, role: Role) -> (&[u8], &[u8], &[u8], &[u8]) {
let mut dec = Decoder::new(&dgram[..]);
let type_and_ver = dec.decode(5).unwrap().to_vec();
// The client sets the QUIC bit, the server might not.
match role {
Role::Client => assert_eq!(type_and_ver[0] & 0xf0, 0xc0),
Role::Server => assert_eq!(type_and_ver[0] & 0xb0, 0x80),
}
let dest_cid = dec.decode_vec(1).unwrap();
let src_cid = dec.decode_vec(1).unwrap();
dec.skip_vvec(); // Ignore any the token.

// Need to read of the length separately so that we can find the packet number.
let payload_len = usize::try_from(dec.decode_varint().unwrap()).unwrap();
let pn_offset = dgram.len() - dec.remaining();
(
&dgram[..pn_offset],
dest_cid,
src_cid,
dec.decode(payload_len).unwrap(),
)
}

/// Generate an AEAD and header protection object for a client Initial.
/// Note that this works for QUIC version 1 only.
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn initial_aead_and_hp(dcid: &[u8], role: Role) -> (Aead, HpKey) {
const INITIAL_SALT: &[u8] = &[
0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c,
0xad, 0xcc, 0xbb, 0x7f, 0x0a,
];
let initial_secret = hkdf::extract(
TLS_VERSION_1_3,
TLS_AES_128_GCM_SHA256,
Some(
hkdf::import_key(TLS_VERSION_1_3, INITIAL_SALT)
.as_ref()
.unwrap(),
),
hkdf::import_key(TLS_VERSION_1_3, dcid).as_ref().unwrap(),
)
.unwrap();

let secret = hkdf::expand_label(
TLS_VERSION_1_3,
TLS_AES_128_GCM_SHA256,
&initial_secret,
&[],
match role {
Role::Client => "client in",
Role::Server => "server in",
},
)
.unwrap();
(
Aead::new(TLS_VERSION_1_3, TLS_AES_128_GCM_SHA256, &secret, "quic ").unwrap(),
HpKey::extract(TLS_VERSION_1_3, TLS_AES_128_GCM_SHA256, &secret, "quic hp").unwrap(),
)
}

// Remove header protection, returning the unmasked header and the packet number.
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn remove_header_protection(hp: &HpKey, header: &[u8], payload: &[u8]) -> (Vec<u8>, u64) {
// Make a copy of the header that can be modified.
let mut fixed_header = header.to_vec();
let pn_offset = header.len();
// Save 4 extra in case the packet number is that long.
fixed_header.extend_from_slice(&payload[..4]);

// Sample for masking and apply the mask.
let mask = hp.mask(&payload[4..20]).unwrap();
fixed_header[0] ^= mask[0] & 0xf;
let pn_len = 1 + usize::from(fixed_header[0] & 0x3);
for i in 0..pn_len {
fixed_header[pn_offset + i] ^= mask[1 + i];
}
// Trim down to size.
fixed_header.truncate(pn_offset + pn_len);
// The packet number should be 1.
let pn = Decoder::new(&fixed_header[pn_offset..])
.decode_uint(pn_len)
.unwrap();

(fixed_header, pn)
}

#[allow(clippy::missing_panics_doc)]
pub fn apply_header_protection(hp: &HpKey, packet: &mut [u8], pn_bytes: Range<usize>) {
let sample_start = pn_bytes.start + 4;
let sample_end = sample_start + 16;
let mask = hp.mask(&packet[sample_start..sample_end]).unwrap();
packet[0] ^= mask[0] & 0xf;
for i in 0..(pn_bytes.end - pn_bytes.start) {
packet[pn_bytes.start + i] ^= mask[1 + i];
}
}

0 comments on commit a826a07

Please sign in to comment.