From a826a07dca460d7e39b0ce964778d98eaedfa18a Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Thu, 11 Apr 2024 13:28:05 +0300 Subject: [PATCH] Add two more fuzzers --- fuzz/Cargo.toml | 15 ++++ fuzz/fuzz_targets/client_initial.rs | 68 +++++++++++++++ fuzz/fuzz_targets/server_initial.rs | 72 ++++++++++++++++ fuzz/src/lib.rs | 125 ++++++++++++++++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 fuzz/fuzz_targets/client_initial.rs create mode 100644 fuzz/fuzz_targets/server_initial.rs create mode 100644 fuzz/src/lib.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index d3d5878085..6fd0941867 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -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" } @@ -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 diff --git a/fuzz/fuzz_targets/client_initial.rs b/fuzz/fuzz_targets/client_initial.rs new file mode 100644 index 0000000000..9689b70628 --- /dev/null +++ b/fuzz/fuzz_targets/client_initial.rs @@ -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() {} diff --git a/fuzz/fuzz_targets/server_initial.rs b/fuzz/fuzz_targets/server_initial.rs new file mode 100644 index 0000000000..513a7eaad5 --- /dev/null +++ b/fuzz/fuzz_targets/server_initial.rs @@ -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() {} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs new file mode 100644 index 0000000000..7cb939cc23 --- /dev/null +++ b/fuzz/src/lib.rs @@ -0,0 +1,125 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , 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, 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) { + 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]; + } +}