From 29a942193b9680dac9201334c7d3d04a465ef251 Mon Sep 17 00:00:00 2001 From: mmorrissette-devolutions Date: Thu, 18 Jun 2026 13:28:00 -0400 Subject: [PATCH 1/2] test(fuzz): add coverage for derive_encrypt and key_derivation parsing Add three fuzz targets covering untrusted-byte paths that previously had no coverage: - kdf_encrypted_data_deserialization: KdfEncryptedData::try_from(&[u8]) - derivation_parameters_deserialization: DerivationParameters::try_from(&[u8]) - decrypt_with_password: encrypt/decrypt round-trip via KdfEncryptedData, with Argon2 parameters clamped to keep key derivation cheap enough for fuzzing. CI discovers targets via `cargo fuzz list`, so no workflow change is needed. --- fuzz/Cargo.toml | 12 ++++ .../derive_encrypt/decrypt_with_password.rs | 63 +++++++++++++++++++ .../kdf_encrypted_data_deserialization.rs | 10 +++ .../derivation_parameters_deserialization.rs | 10 +++ 4 files changed, 95 insertions(+) create mode 100644 fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs create mode 100644 fuzz/fuzz_targets/derive_encrypt/kdf_encrypted_data_deserialization.rs create mode 100644 fuzz/fuzz_targets/key_derivation/derivation_parameters_deserialization.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 9f7512ce..6aeaa0f2 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -168,3 +168,15 @@ path = "fuzz_targets/key/generate_keypair.rs" [[bin]] name = "constant_time_equals" path = "fuzz_targets/utils/constant_time_equals.rs" + +[[bin]] +name = "kdf_encrypted_data_deserialization" +path = "fuzz_targets/derive_encrypt/kdf_encrypted_data_deserialization.rs" + +[[bin]] +name = "derivation_parameters_deserialization" +path = "fuzz_targets/key_derivation/derivation_parameters_deserialization.rs" + +[[bin]] +name = "decrypt_with_password" +path = "fuzz_targets/derive_encrypt/decrypt_with_password.rs" diff --git a/fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs b/fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs new file mode 100644 index 00000000..93f3c58e --- /dev/null +++ b/fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs @@ -0,0 +1,63 @@ +#![no_main] +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; + +use devolutions_crypto::derive_encrypt::encrypt_with_password_and_aad; +use devolutions_crypto::key_derivation::Argon2; +use devolutions_crypto::{Argon2Parameters, CiphertextVersion}; + +#[derive(Arbitrary, Clone, Debug)] +struct Input { + data: Vec, + password: Vec, + aad: Vec, + decrypt_password: Vec, + decrypt_aad: Vec, + length: u32, + lanes: u32, + memory: u32, + iterations: u32, + salt: Vec, +} + +fuzz_target!(|input: Input| { + // Clamp Argon2 parameters to keep key derivation cheap enough for fuzzing. + let length = input.length.clamp(1, 128); + let lanes = input.lanes.clamp(1, 16); + let memory = input.memory.clamp(8, 65536); + let iterations = input.iterations.clamp(1, 10); + + // Use only small salts for fuzzing performance. + let salt = if input.salt.len() > 64 { + &input.salt[..64] + } else if input.salt.is_empty() { + &[0u8; 8][..] + } else { + &input.salt[..] + }; + + let parameters = Argon2Parameters::builder() + .length(length) + .lanes(lanes) + .memory(memory) + .iterations(iterations) + .salt(salt.to_vec()) + .build(); + + let derivation_parameters = Argon2::with_params(parameters).parameters(); + + let blob = match encrypt_with_password_and_aad( + &input.data, + &input.password, + &input.aad, + derivation_parameters, + CiphertextVersion::Latest, + ) { + Ok(b) => b, + Err(_) => return, + }; + + // Exercise the decrypt path with both the correct and a fuzzed password/AAD. + let _ = blob.decrypt_with_password_and_aad(&input.password, &input.aad); + let _ = blob.decrypt_with_password_and_aad(&input.decrypt_password, &input.decrypt_aad); +}); diff --git a/fuzz/fuzz_targets/derive_encrypt/kdf_encrypted_data_deserialization.rs b/fuzz/fuzz_targets/derive_encrypt/kdf_encrypted_data_deserialization.rs new file mode 100644 index 00000000..557a3cdf --- /dev/null +++ b/fuzz/fuzz_targets/derive_encrypt/kdf_encrypted_data_deserialization.rs @@ -0,0 +1,10 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use devolutions_crypto::derive_encrypt::KdfEncryptedData; + +use std::convert::TryFrom; + +fuzz_target!(|data: &[u8]| { + let _ = KdfEncryptedData::try_from(data); +}); diff --git a/fuzz/fuzz_targets/key_derivation/derivation_parameters_deserialization.rs b/fuzz/fuzz_targets/key_derivation/derivation_parameters_deserialization.rs new file mode 100644 index 00000000..6488c20c --- /dev/null +++ b/fuzz/fuzz_targets/key_derivation/derivation_parameters_deserialization.rs @@ -0,0 +1,10 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use devolutions_crypto::key_derivation::DerivationParameters; + +use std::convert::TryFrom; + +fuzz_target!(|data: &[u8]| { + let _ = DerivationParameters::try_from(data); +}); From 4e19947294ffffb44e1bcd7d5faaa6619ac14259 Mon Sep 17 00:00:00 2001 From: mmorrissette-devolutions Date: Fri, 19 Jun 2026 07:58:53 -0400 Subject: [PATCH 2/2] test(fuzz): pin derived key length to 32 in decrypt_with_password A SecretKey requires exactly 32 raw bytes, so an arbitrary Argon2 output length (previously clamped to 1..128) made encrypt_with_password_and_aad fail before the decrypt path for ~127/128 of inputs. Pin the length to 32 so the password-decrypt path is actually exercised; arbitrary output lengths remain covered by the derive_key_argon2 target. --- fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs b/fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs index 93f3c58e..33b79b0c 100644 --- a/fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs +++ b/fuzz/fuzz_targets/derive_encrypt/decrypt_with_password.rs @@ -13,7 +13,6 @@ struct Input { aad: Vec, decrypt_password: Vec, decrypt_aad: Vec, - length: u32, lanes: u32, memory: u32, iterations: u32, @@ -22,7 +21,10 @@ struct Input { fuzz_target!(|input: Input| { // Clamp Argon2 parameters to keep key derivation cheap enough for fuzzing. - let length = input.length.clamp(1, 128); + // The derived key feeds a SecretKey, which requires exactly 32 bytes; any + // other length would fail in encrypt_with_password_and_aad before reaching + // the decrypt path. Arbitrary output lengths are covered by derive_key_argon2. + let length = 32; let lanes = input.lanes.clamp(1, 16); let memory = input.memory.clamp(8, 65536); let iterations = input.iterations.clamp(1, 10);