Skip to content

Commit

Permalink
Add cargo-fuzz test harness for the qos_p256 crate for automated cove…
Browse files Browse the repository at this point in the history
…rage guided testing
  • Loading branch information
cr-tk committed Dec 21, 2024
1 parent 39e8b41 commit 9d9cc90
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 0 deletions.
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);
});
46 changes: 46 additions & 0 deletions src/qos_p256/fuzz/fuzz_targets/6_basic_encrypt_decrypt_aesgcm.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::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

0 comments on commit 9d9cc90

Please sign in to comment.