-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add cargo-fuzz test harness for the qos_p256 crate for automated cove…
…rage guided testing
- Loading branch information
Showing
10 changed files
with
443 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
src/qos_p256/fuzz/fuzz_targets/2_public_sign_key_round_trip.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
39
src/qos_p256/fuzz/fuzz_targets/3_public_sign_key_import.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
src/qos_p256/fuzz/fuzz_targets/6_basic_encrypt_decrypt_aesgcm.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
}; | ||
}); |
Oops, something went wrong.