Skip to content
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

Add cargo-fuzz test harness for the qos_p256 crate #439

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
81 changes: 81 additions & 0 deletions src/qos_p256/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
[package]
name = "qos_p256_fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
arbitrary = { version = "1", features = ["derive"] }

qos_p256 = { path = "../"}

# Prevent this from interfering with workspaces
[workspace]
members = ["."]

[profile.release]
debug = 1
# enable integer overflow checks
overflow-checks = true

[features]
# feature used by some harnesses to signal a special mode, does nothing on other targets
fuzzer_corpus_seed1 = []

[[bin]]
name = "1_sign_then_verify"
path = "fuzz_targets/1_sign_then_verify.rs"
test = false
doc = false

[[bin]]
name = "2_public_sign_key_round_trip"
path = "fuzz_targets/2_public_sign_key_round_trip.rs"
test = false
doc = false

[[bin]]
name = "3_public_sign_key_import"
path = "fuzz_targets/3_public_sign_key_import.rs"
test = false
doc = false

[[bin]]
name = "4_public_key_import"
path = "fuzz_targets/4_public_key_import.rs"
test = false
doc = false

[[bin]]
name = "5_basic_encrypt_decrypt"
path = "fuzz_targets/5_basic_encrypt_decrypt.rs"
test = false
doc = false

[[bin]]
name = "6_basic_encrypt_decrypt_aesgcm"
path = "fuzz_targets/6_basic_encrypt_decrypt_aesgcm.rs"
test = false
doc = false

[[bin]]
name = "7_decrypt_aesgcm"
path = "fuzz_targets/7_decrypt_aesgcm.rs"
test = false
doc = false

[[bin]]
name = "8_decrypt_p256"
path = "fuzz_targets/8_decrypt_p256.rs"
test = false
doc = false

[[bin]]
name = "9_decrypt_shared_secret"
path = "fuzz_targets/9_decrypt_shared_secret.rs"
test = false
doc = false
34 changes: 34 additions & 0 deletions src/qos_p256/fuzz/fuzz_targets/1_sign_then_verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use qos_p256::sign::P256SignPair;
use qos_p256::P256Pair;
use std::{convert::TryFrom, iter};

#[derive(Clone, Debug, arbitrary::Arbitrary)]
pub struct FuzzKeyDataStruct {
key: [u8; qos_p256::MASTER_SEED_LEN],
data: Box<[u8]>,
}

// this harness is based on the sign_and_verification_works() unit test

fuzz_target!(|input: FuzzKeyDataStruct| {
// let the fuzzer control the key and data that is going to be signed

let keypair = match P256Pair::from_master_seed(&input.key) {
Ok(pair) => pair,
Err(_err) => {
return;
}
};

let input_data: &[u8] = &input.data.clone();

// produce a signature over the data input the fuzzer controls
let signature = keypair.sign(input_data).unwrap();

// verify the just-generated signature
// this should always succeed
assert!(keypair.public_key().verify(input_data, &signature).is_ok());
});
46 changes: 46 additions & 0 deletions src/qos_p256/fuzz/fuzz_targets/2_public_sign_key_round_trip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

use qos_p256::sign::P256SignPair;
use qos_p256::sign::P256SignPublic;

#[derive(Clone, Debug, arbitrary::Arbitrary)]
pub struct FuzzKeyDataStruct {
key: [u8; qos_p256::MASTER_SEED_LEN],
data: Box<[u8]>,
}

// this harness is based on the public_key_round_trip_bytes_works() unit test

fuzz_target!(|input: FuzzKeyDataStruct| {
// Let the fuzzer pick a P256 key
let keypair = match P256SignPair::from_bytes(&input.key) {
Ok(pair) => pair,
Err(_err) => {
return;
}
};

// create valid signature
let signature = keypair.sign(&input.data).unwrap();

// derive public key and export it to bytes
let bytes_public = keypair.public_key().to_bytes();

// re-import public key from bytes
// this should always succeed
let public_reimported = P256SignPublic::from_bytes(&bytes_public)
.expect("We just generated and exported this pubkey");

assert!(keypair.public_key().verify(&input.data, &signature).is_ok());
// expect the signature verification with the reconstructed pubkey to always succeed
assert!(public_reimported.verify(&input.data, &signature).is_ok());

let mut wrong_signature = signature.clone();
let wrong_signature_last_element_index = wrong_signature.len() - 1;
// flip a bit in the signature
wrong_signature[wrong_signature_last_element_index] = wrong_signature[wrong_signature_last_element_index] ^ 1;
// expect the verification to fail since the signature is bad
assert!(public_reimported.verify(&input.data, &wrong_signature).is_err());
});
39 changes: 39 additions & 0 deletions src/qos_p256/fuzz/fuzz_targets/3_public_sign_key_import.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

use qos_p256::sign::P256SignPublic;

// this harness is partially based on the public_key_round_trip_bytes_works() unit test
// it is a simpler variant of another public key import harness

fuzz_target!(|data: &[u8]| {
// let the fuzzer control the P256 signing pubkey

// import public key from bytes, silently exit in case of errors
let pubkey_special = match P256SignPublic::from_bytes(data) {
Ok(pubkey) => pubkey,
Err(_err) => {
return;
}
};

// we don't have the private key that belongs to this public key,
// so we can't generate valid signatures
// however, we can check the behavior against bad signatures

// static plaintext message
let message = b"a message to authenticate";
// dummy signature full of zeroes
let bad_signature = vec![0; 64];
// this should never succeed
assert!(pubkey_special.verify(message, &bad_signature).is_err());

let re_exported_public_key_data = pubkey_special.to_bytes();
// the exported data doesn't actually have to be identical to initial input,
// since P256SignPublic::from_bytes() accepts compressed points as well
//
// workaround: compare only the 32 data bytes corresponding to the first sub-point,
// ignoring the first format byte and any trailing data
assert_eq!(data[1..33], re_exported_public_key_data[1..33]);
});
64 changes: 64 additions & 0 deletions src/qos_p256/fuzz/fuzz_targets/4_public_key_import.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#![no_main]

#[cfg(feature = "fuzzer_corpus_seed1")]
use libfuzzer_sys::fuzz_mutator;
use libfuzzer_sys::fuzz_target;

#[cfg(feature = "fuzzer_corpus_seed1")]
use qos_p256::P256Pair;
use qos_p256::P256Public;

// this helps the fuzzer over the major obstacle of learning what a valid P256Public object looks like
#[cfg(feature = "fuzzer_corpus_seed1")]
fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, _seed: u32| {
// this is random and does not depend on the input
let random_key_pair = P256Pair::generate().unwrap();

let mut public_bytes = random_key_pair.public_key().to_bytes();
let public_bytes_length = public_bytes.len();

// this mutates the generated data in-place in its buffer
// and denies buffer length extensions, which is overly restrictive
let _mutated_data_size = libfuzzer_sys::fuzzer_mutate(
&mut public_bytes,
public_bytes_length,
public_bytes_length,
);

// calculate the new requested output size and return the corresponding data
let new_size = std::cmp::min(max_size, public_bytes_length);
data[..new_size].copy_from_slice(&public_bytes[..new_size]);
new_size
});

// this harness is partially based on the public_key_round_trip_bytes_works() unit test

fuzz_target!(|data: &[u8]| {
// let the fuzzer control the P256 signing pubkey and P256 encryption pubkey

// the fuzzer has problems synthesizing a working input without additional help
// see fuzz_mutator!() for a workaround

// import public keys from bytes
// silently exit in case of errors
let pubkey_special = match P256Public::from_bytes(data) {
Ok(pubkey) => pubkey,
Err(_err) => {
return;
}
};

// we don't have the private key that belongs to this public key,
// so we can't generate valid signatures
// however, we can check the behavior against bad signatures

// static plaintext message
let message = b"a message to authenticate";
// dummy signature full of zeroes
let bad_signature = vec![0; 64];
// this should never succeed
assert!(pubkey_special.verify(message, &bad_signature).is_err());

let re_exported_public_key_data = pubkey_special.to_bytes();
assert_eq!(data, re_exported_public_key_data);
});
38 changes: 38 additions & 0 deletions src/qos_p256/fuzz/fuzz_targets/5_basic_encrypt_decrypt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

use qos_p256::encrypt::P256EncryptPair;

// this harness is partially based on the basic_encrypt_decrypt_works() unit test

#[derive(Clone, Debug, arbitrary::Arbitrary)]
pub struct FuzzKeyDataStruct {
key: [u8; qos_p256::MASTER_SEED_LEN],
data: Box<[u8]>,
}

fuzz_target!(|input: FuzzKeyDataStruct| {
// let the fuzzer control a message plaintext that is encrypted and then decrypted again

// private key generation is non-deterministic: not ideal
let key_pair = match P256EncryptPair::from_bytes(&input.key) {
Ok(pair) => pair,
Err(_err) => {
return;
}
};

let public_key = key_pair.public_key();
let data = input.data.to_vec();

// the encryption is non-deterministic due to the internal random nonce generation
// not ideal, can't be avoided due to API structure?
let serialized_envelope = public_key.encrypt(&data[..]).unwrap();

// expected to always succeed
let decrypted_data = key_pair.decrypt(&serialized_envelope).unwrap();

// check roundtrip data consistency, assert should always hold
assert_eq!(decrypted_data, data);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

use qos_p256::encrypt::AesGcm256Secret;

#[derive(Clone, Debug, arbitrary::Arbitrary)]
pub struct FuzzKeyDataStruct {
key: [u8; qos_p256::MASTER_SEED_LEN],
data: Box<[u8]>,
}

// this harness is partially based on the encrypt_decrypt_round_trip() unit test

fuzz_target!(|input: FuzzKeyDataStruct| {
// let the fuzzer control a message plaintext that is encrypted and then decrypted again

// private key generation is non-deterministic: not ideal
// let random_key = AesGcm256Secret::generate();
let random_key = match AesGcm256Secret::from_bytes(input.key) {
Ok(pair) => pair,
Err(_err) => {
return;
}
};

let data = input.data.to_vec();

// the encryption is non-deterministic due to the internal random nonce generation
// not ideal, can't be avoided due to API structure?
// expected to always succeed
let encrypted_envelope = random_key.encrypt(&data[..]).unwrap();

// expected to always succeed
let decrypted_data = random_key.decrypt(&encrypted_envelope).unwrap();
// check roundtrip data consistency, assert should always hold
assert_eq!(decrypted_data, data);

let mut corrupted_encrypted_envelope = encrypted_envelope.clone();
let last_element_index_envelope = corrupted_encrypted_envelope.len() - 1;
// flip one bit in the end of the message as a simple example of data corruption
corrupted_encrypted_envelope[last_element_index_envelope] =
corrupted_encrypted_envelope[last_element_index_envelope] ^ 1;
// expect detection of the corruption
assert!(random_key.decrypt(&corrupted_encrypted_envelope).is_err());
});
34 changes: 34 additions & 0 deletions src/qos_p256/fuzz/fuzz_targets/7_decrypt_aesgcm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

use qos_p256::encrypt::AesGcm256Secret;

#[derive(Clone, Debug, arbitrary::Arbitrary)]
pub struct FuzzKeyDataStruct {
key: [u8; 32], // AES256_KEY_LEN == 32
data: Box<[u8]>,
}

// this harness is partially based on the encrypt_decrypt_round_trip() unit test

fuzz_target!(|input: FuzzKeyDataStruct| {
// let the fuzzer control a message plaintext that is encrypted and then decrypted again

// private key generation is non-deterministic: not ideal
// let random_key = AesGcm256Secret::generate();
let key = match AesGcm256Secret::from_bytes(input.key) {
Ok(pair) => pair,
Err(_err) => {
return;
}
};

// we expect this to fail
match key.decrypt(&input.data) {
Ok(_res) => panic!("the fuzzer can't create valid AEAD protected encrypted messages"),
Err(_err) => {
return;
},
};
});
Loading
Loading