diff --git a/src/qos_p256/fuzz/Cargo.toml b/src/qos_p256/fuzz/Cargo.toml new file mode 100644 index 00000000..19cc2447 --- /dev/null +++ b/src/qos_p256/fuzz/Cargo.toml @@ -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 \ No newline at end of file diff --git a/src/qos_p256/fuzz/fuzz_targets/1_sign_then_verify.rs b/src/qos_p256/fuzz/fuzz_targets/1_sign_then_verify.rs new file mode 100644 index 00000000..3657ebe9 --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/1_sign_then_verify.rs @@ -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()); +}); diff --git a/src/qos_p256/fuzz/fuzz_targets/2_public_sign_key_round_trip.rs b/src/qos_p256/fuzz/fuzz_targets/2_public_sign_key_round_trip.rs new file mode 100644 index 00000000..ed4dccb3 --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/2_public_sign_key_round_trip.rs @@ -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()); +}); diff --git a/src/qos_p256/fuzz/fuzz_targets/3_public_sign_key_import.rs b/src/qos_p256/fuzz/fuzz_targets/3_public_sign_key_import.rs new file mode 100644 index 00000000..f41f2c3e --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/3_public_sign_key_import.rs @@ -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]); +}); diff --git a/src/qos_p256/fuzz/fuzz_targets/4_public_key_import.rs b/src/qos_p256/fuzz/fuzz_targets/4_public_key_import.rs new file mode 100644 index 00000000..dab66e01 --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/4_public_key_import.rs @@ -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); +}); diff --git a/src/qos_p256/fuzz/fuzz_targets/5_basic_encrypt_decrypt.rs b/src/qos_p256/fuzz/fuzz_targets/5_basic_encrypt_decrypt.rs new file mode 100644 index 00000000..7aa0ac69 --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/5_basic_encrypt_decrypt.rs @@ -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); +}); diff --git a/src/qos_p256/fuzz/fuzz_targets/6_basic_encrypt_decrypt_aesgcm.rs b/src/qos_p256/fuzz/fuzz_targets/6_basic_encrypt_decrypt_aesgcm.rs new file mode 100644 index 00000000..950d5925 --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/6_basic_encrypt_decrypt_aesgcm.rs @@ -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()); +}); diff --git a/src/qos_p256/fuzz/fuzz_targets/7_decrypt_aesgcm.rs b/src/qos_p256/fuzz/fuzz_targets/7_decrypt_aesgcm.rs new file mode 100644 index 00000000..e5b3af4f --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/7_decrypt_aesgcm.rs @@ -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; + }, + }; +}); \ No newline at end of file diff --git a/src/qos_p256/fuzz/fuzz_targets/8_decrypt_p256.rs b/src/qos_p256/fuzz/fuzz_targets/8_decrypt_p256.rs new file mode 100644 index 00000000..2f8dcc74 --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/8_decrypt_p256.rs @@ -0,0 +1,30 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +use qos_p256::encrypt::P256EncryptPair; + +#[derive(Clone, Debug, arbitrary::Arbitrary)] +pub struct FuzzKeyDataStruct { + key: [u8; qos_p256::MASTER_SEED_LEN], + data: Box<[u8]>, +} + +fuzz_target!(|input: FuzzKeyDataStruct| { + let key = match P256EncryptPair::from_bytes(&input.key) { + Ok(pair) => pair, + Err(_err) => { + return; + } + }; + + // let the fuzzer control an encrypted message ciphertext to test decrypt() robustness + match key.decrypt(&input.data) { + Ok(_res) => panic!( + "the fuzzer should be unable to create a validly signed encrypted message" + ), + Err(_err) => { + return; + } + }; +}); diff --git a/src/qos_p256/fuzz/fuzz_targets/9_decrypt_shared_secret.rs b/src/qos_p256/fuzz/fuzz_targets/9_decrypt_shared_secret.rs new file mode 100644 index 00000000..7a2f107b --- /dev/null +++ b/src/qos_p256/fuzz/fuzz_targets/9_decrypt_shared_secret.rs @@ -0,0 +1,31 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +// use qos_p256::encrypt::P256EncryptPair; +use qos_p256::encrypt::P256EncryptPublic; + +#[derive(Clone, Debug, arbitrary::Arbitrary)] +pub struct FuzzKeyDataStruct { + public: Box<[u8]>, // this should be 65 byte in theory, but both 33 and 65 byte work + secret: Box<[u8]>, + data: Box<[u8]>, +} + +fuzz_target!(|input: FuzzKeyDataStruct| { + let pubkey = match P256EncryptPublic::from_bytes(&input.public) { + Ok(pair) => pair, + Err(_err) => { + return; + } + }; + + match pubkey.decrypt_from_shared_secret(&input.secret, &input.data) { + Ok(_res) => panic!( + "the fuzzer should be unable to create a validly signed encrypted message" + ), + Err(_err) => { + return; + } + }; +});