diff --git a/Cargo.toml b/Cargo.toml index 7f2138e..a9fc68b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ blake2b_simd = "0.5" jubjub = "0.3" [dev-dependencies] -rand = "0.7" +rand_chacha = "0.2" +proptest = "0.9" [features] nightly = [] diff --git a/src/lib.rs b/src/lib.rs index ca9fabd..ca02315 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,18 +68,3 @@ pub(crate) mod private { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sign_and_verify() { - let sk = SecretKey::::new(rand::thread_rng()); - let msg = b"test"; - let sig = sk.sign(rand::thread_rng(), msg); - let pk = PublicKey::from(&sk); - - assert_eq!(pk.verify(msg, &sig), Ok(())); - } -} diff --git a/tests/proptests.rs b/tests/proptests.rs new file mode 100644 index 0000000..36692f5 --- /dev/null +++ b/tests/proptests.rs @@ -0,0 +1,127 @@ +use std::convert::TryFrom; + +use proptest::prelude::*; +use rand_core::{CryptoRng, RngCore}; + +use redjubjub_zebra as rjj; + +use rjj::{PublicKey, PublicKeyBytes, SecretKey, SigType, Signature}; + +/// A signature test-case, containing signature data and expected validity. +#[derive(Clone, Debug)] +struct SignatureCase { + msg: Vec, + sig: Signature, + pk_bytes: PublicKeyBytes, + is_valid: bool, +} + +impl SignatureCase { + fn new(mut rng: R, msg: Vec) -> Self { + let sk = SecretKey::new(&mut rng); + let sig = sk.sign(&mut rng, &msg); + let pk_bytes = PublicKey::from(&sk).into(); + Self { + msg, + sig, + pk_bytes, + is_valid: true, + } + } + + // Check that signature verification succeeds or fails, as expected. + fn check(&self) -> bool { + // The signature data is stored in (refined) byte types, but do a round trip + // conversion to raw bytes to exercise those code paths. + let sig = { + let bytes: [u8; 64] = self.sig.into(); + Signature::::from(bytes) + }; + let pk_bytes = { + let bytes: [u8; 32] = self.pk_bytes.into(); + PublicKeyBytes::::from(bytes) + }; + + // Check that signature validation has the expected result. + self.is_valid + == PublicKey::try_from(pk_bytes) + .and_then(|pk| pk.verify(&self.msg, &sig)) + .is_ok() + } +} + +#[derive(Copy, Clone, Debug)] +enum Tweak { + None, + ChangeMessage, +} + +impl Tweak { + fn apply(&self, case: SignatureCase) -> SignatureCase { + use Tweak::*; + let SignatureCase { + mut msg, + sig, + pk_bytes, + is_valid, + } = case; + match (self, is_valid) { + (None, is_valid) => { + // This is a no-op, so return the original case. + SignatureCase { + msg, + sig, + pk_bytes, + is_valid, + } + } + (ChangeMessage, _) => { + // Changing the message makes the signature invalid. + msg.push(90); + SignatureCase { + msg, + sig, + pk_bytes, + is_valid: false, + } + } + } + } +} + +fn tweak_strategy() -> impl Strategy { + prop_oneof![ + 10 => Just(Tweak::None), + 1 => Just(Tweak::ChangeMessage), + ] +} + +proptest! { + #[test] + fn tweak_signature( + tweaks in prop::collection::vec(tweak_strategy(), (0,5)), + rng_seed in any::(), + ) { + use rjj::{Binding, SpendAuth, }; + use rand_core::SeedableRng; + + // Use a deterministic RNG so that test failures can be reproduced. + // Seeding with 64 bits of entropy is INSECURE and this code should + // not be copied outside of this test! + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(rng_seed); + + // Create a test case for each signature type. + let msg = b"test message for proptests"; + let mut binding = SignatureCase::::new(&mut rng, msg.to_vec()); + let mut spendauth = SignatureCase::::new(&mut rng, msg.to_vec()); + + // Apply tweaks to each case. + for tweak in &tweaks { + binding = tweak.apply(binding); + spendauth = tweak.apply(spendauth); + } + + assert!(binding.check()); + assert!(spendauth.check()); + } +}