From f9b6cb8f7a7c6a41373b935ac8293331422f2266 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Thu, 28 Sep 2023 02:21:05 -0500 Subject: [PATCH] feat!: differentiate scalar parsing from byte arrays (#194) Currently, creating a scalar `RistrettoSecretKey` [from a byte array](https://github.com/tari-project/tari-crypto/blob/053119f2110aaf3089c7b9df96f50b8cc8d3217a/src/ristretto/ristretto_keys.rs#L90-L100) performs modular reduction on 32 bytes. For cases where the input is intended to be canonical, this is suboptimal. For cases where the input is produced from a hashing operation, wide reduction should be used to mitigate bias. This work renames `SecretKey::from_bytes` to `SecretKey::from_canonical_bytes` to support an underlying `ByteArray` trait update. In the case of `RistrettoSecretKey`, it uses the curve library's canonical parser and returns an error if the provided byte slice is not a canonical scalar encoding. It also adds a new `SecretKey::from_uniform_bytes` function that uses wide reduction. For constructions like signatures and KDFs that use hashing operations to produce scalar values, this function is used and the underlying hashers are updated to produce 64-byte output in the case of `RistrettoSecretKey`. It updates the Schnorr signature API to support raw signing and verification using challenge byte slices that are either canonical encodings or uniform. It renames several existing functions for clarity. It corrects a few typos that were discovered along the way. Closes #189. BREAKING CHANGE: This changes the way that scalars are produced from byte arrays, modifies the `SecretKey` trait and corresponding `RistrettoSecretKey` implementation, and updates the Schnorr signature API. --- Cargo.toml | 4 +- benches/signatures.rs | 6 +- src/commitment.rs | 4 +- src/errors.rs | 2 +- src/hashing.rs | 46 ++++--- src/keys.rs | 9 +- src/ristretto/mod.rs | 1 - src/ristretto/pedersen/mod.rs | 2 +- src/ristretto/ristretto_com_and_pub_sig.rs | 57 ++++----- src/ristretto/ristretto_com_sig.rs | 58 ++++----- src/ristretto/ristretto_keys.rs | 82 +++++++----- src/ristretto/ristretto_sig.rs | 74 +++++------ src/ristretto/serialize.rs | 4 +- src/ristretto/utils.rs | 58 --------- .../commitment_and_public_key_signature.rs | 8 +- src/signatures/commitment_signature.rs | 10 +- src/signatures/schnorr.rs | 117 ++++++++++-------- 17 files changed, 254 insertions(+), 288 deletions(-) delete mode 100644 src/ristretto/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 739618fb..1fe9a40f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ version = "0.18.0" edition = "2018" [dependencies] -tari_utilities = { version = "0.5", default-features = false, features = ["zero"] } +tari_utilities = { version = "0.6", default-features = false, features = ["zero"] } blake2 = { version = "0.10", default-features = false } borsh = { version = "0.10" , optional = true , default-features = false} bulletproofs_plus = { package = "tari_bulletproofs_plus", version = "0.3", optional = true } @@ -27,7 +27,7 @@ snafu = { version = "0.7", default-features = false} zeroize = {version = "1" , default-features = false} [dev-dependencies] -tari_utilities = { version = "0.5", features = ["std"] } +tari_utilities = { version = "0.6", features = ["std"] } serde = { version = "1.0"} bincode = { version = "1.1" } criterion = { version = "0.5", default-features = false } diff --git a/benches/signatures.rs b/benches/signatures.rs index 224bd554..8ab2d9e9 100644 --- a/benches/signatures.rs +++ b/benches/signatures.rs @@ -46,7 +46,7 @@ fn sign_message(c: &mut Criterion) { b.iter_batched( gen_keypair, |d| { - let _sig = RistrettoSchnorr::sign_message(&d.k, d.m, &mut OsRng).unwrap(); + let _sig = RistrettoSchnorr::sign(&d.k, d.m, &mut OsRng).unwrap(); }, BatchSize::SmallInput, ); @@ -60,10 +60,10 @@ fn verify_message(c: &mut Criterion) { b.iter_batched( || { let d = gen_keypair(); - let s = RistrettoSchnorr::sign_message(&d.k, d.m, &mut OsRng).unwrap(); + let s = RistrettoSchnorr::sign(&d.k, d.m, &mut OsRng).unwrap(); (d, s) }, - |(d, s)| assert!(s.verify_message(&d.p, d.m)), + |(d, s)| assert!(s.verify(&d.p, d.m)), BatchSize::SmallInput, ); }); diff --git a/src/commitment.rs b/src/commitment.rs index 84561da0..9abfa208 100644 --- a/src/commitment.rs +++ b/src/commitment.rs @@ -68,8 +68,8 @@ where P: PublicKey impl

ByteArray for HomomorphicCommitment

where P: PublicKey { - fn from_bytes(bytes: &[u8]) -> Result { - let p = P::from_bytes(bytes)?; + fn from_canonical_bytes(bytes: &[u8]) -> Result { + let p = P::from_canonical_bytes(bytes)?; Ok(Self(p)) } diff --git a/src/errors.rs b/src/errors.rs index db0e623c..7c1a0ad6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -70,7 +70,7 @@ pub enum HashingError { reason: String, }, /// The digest does not produce enough output - #[snafu(display("The digest does produce enough output.`{bytes}' bytes are required."))] + #[snafu(display("The digest does not produce enough output.`{bytes}' bytes are required."))] DigestTooShort { /// The number of bytes required bytes: usize, diff --git a/src/hashing.rs b/src/hashing.rs index be1b29b5..435cf87c 100644 --- a/src/hashing.rs +++ b/src/hashing.rs @@ -32,9 +32,16 @@ use alloc::string::String; use core::{marker::PhantomData, ops::Deref}; use blake2::{Blake2b, Blake2bVar}; -use digest::{consts::U32, Digest, FixedOutput, FixedOutputReset, Output, OutputSizeUser, Update}; +use digest::{ + consts::{U32, U64}, + Digest, + FixedOutput, + FixedOutputReset, + Output, + OutputSizeUser, + Update, +}; use sha3::Sha3_256; -use tari_utilities::ByteArray; use crate::{ alloc::string::ToString, @@ -209,10 +216,10 @@ impl AsRef<[u8]> for DomainSeparatedHash { /// Calculating a signature challenge /// /// ``` +/// # use blake2::Blake2b; +/// # use digest::{consts::U32, Digest}; /// # use tari_utilities::hex::{to_hex, Hex}; -/// use blake2::{Blake2b, Digest}; -/// use digest::consts::U32; -/// use tari_crypto::{ +/// # use tari_crypto::{ /// hash_domain, /// hashing::{DomainSeparatedHash, DomainSeparatedHasher, DomainSeparation}, /// }; @@ -418,6 +425,8 @@ impl LengthExtensionAttackResistant for Sha3_256 {} impl LengthExtensionAttackResistant for Blake2b {} +impl LengthExtensionAttackResistant for Blake2b {} + //------------------------------------------------ HMAC ------------------------------------------------------------ /// A domain separation tag for use in MAC derivation algorithms. pub struct MacDomain; @@ -517,6 +526,8 @@ impl Deref for Mac { /// `RistrettoKdf` is an implementation of [`DerivedKeyDomain`] that generates Ristretto keys. /// /// ``` +/// # use blake2::Blake2b; +/// # use digest::{consts::U64, Digest}; /// # use tari_utilities::ByteArray; /// # use tari_utilities::hex::Hex; /// # use tari_crypto::errors::HashingError; @@ -524,14 +535,12 @@ impl Deref for Mac { /// # use tari_crypto::keys::SecretKey; /// # use tari_crypto::ristretto::ristretto_keys::RistrettoKdf; /// # use tari_crypto::ristretto::RistrettoSecretKey; -/// # use digest::consts::U32; -/// # use blake2::Blake2b; /// /// fn wallet_keys( /// primary_key: &RistrettoSecretKey, /// index: usize, /// ) -> Result { -/// RistrettoKdf::generate::>( +/// RistrettoKdf::generate::>( /// primary_key.as_bytes(), /// &index.to_le_bytes(), /// "wallet", @@ -539,40 +548,47 @@ impl Deref for Mac { /// } /// /// let key = RistrettoSecretKey::from_hex( -/// "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", +/// "a8fb609c5ab7cc07548b076b6c25cc3237c4526fb7a6dcb83b26f457b172c20a", /// ) /// .unwrap(); /// let key_1 = wallet_keys(&key, 1).unwrap(); /// assert_eq!( /// key_1.to_hex(), -/// "b778b8b5041fbde6c78be5bafd6d62633824bf303c97736d7337b3f6f70c4e0b" +/// "08106b88a2ff4c52d1d8b458cf34802df8655ba989a7d91351e3504e087a2e0c" /// ); /// let key_64 = wallet_keys(&key, 64).unwrap(); /// assert_eq!( /// key_64.to_hex(), -/// "09e5204c93406ef3334ff5f7a4d5d84199ceb9119fafcb98928fa95e95f0ae05" +/// "2c2206dadd2a21e71b6c52dd321572cde0f2b00e7116e1123fb580b09ed1b70e" /// ); /// ``` pub trait DerivedKeyDomain: DomainSeparation { /// The associated derived secret key type type DerivedKeyType: SecretKey; - /// Derive a key from the input key using a suitable domain separation tag and the given application label. - /// An error is returned if the supplied primary key isn't at least as long as the digest algorithm's output size. + /// Derive a key from the input key using a suitable domain separation tag and the given application label by wide + /// reduction. An error is returned if the supplied primary key isn't at least as long as the derived key. /// If the digest's output size is not sufficient to generate the derived key type, then an error will be thrown. fn generate(primary_key: &[u8], data: &[u8], label: &'static str) -> Result where Self: Sized, D: Digest + Update, { - if primary_key.as_ref().len() < ::output_size() { + // Ensure the primary key is at least as long as the derived key + if primary_key.len() < ::KEY_LEN { return Err(HashingError::InputTooShort {}); } + + // Ensure the digest length is suitable for wide reduction + if ::output_size() != ::WIDE_REDUCTION_LEN { + return Err(HashingError::InputTooShort {}); + } + let hash = DomainSeparatedHasher::::new_with_label(label) .chain(primary_key) .chain(data) .finalize(); - let derived_key = Self::DerivedKeyType::from_bytes(hash.as_ref()) + let derived_key = Self::DerivedKeyType::from_uniform_bytes(hash.as_ref()) .map_err(|e| HashingError::ConversionFromBytes { reason: e.to_string() })?; Ok(derived_key) } diff --git a/src/keys.rs b/src/keys.rs index 14f8e250..b1692f26 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -9,7 +9,7 @@ use core::ops::Add; use rand_core::{CryptoRng, RngCore}; -use tari_utilities::ByteArray; +use tari_utilities::{ByteArray, ByteArrayError}; use zeroize::{Zeroize, ZeroizeOnDrop}; /// A trait specifying common behaviour for representing `SecretKey`s. Specific elliptic curve @@ -32,6 +32,9 @@ pub trait SecretKey: /// The length of the byte encoding of a key, in bytes const KEY_LEN: usize; + /// The number of bytes used for construction by wide reduction + const WIDE_REDUCTION_LEN: usize; + /// The length of the byte encoding of a key, in bytes fn key_length() -> usize { Self::KEY_LEN @@ -39,6 +42,10 @@ pub trait SecretKey: /// Generates a random secret key fn random(rng: &mut R) -> Self; + + /// Generates a secret key from a slice of uniformly-distributed bytes using wide reduction + /// If the number of bytes is incorrect, this will fail + fn from_uniform_bytes(bytes: &[u8]) -> Result; } //---------------------------------------- Public Keys ----------------------------------------// diff --git a/src/ristretto/mod.rs b/src/ristretto/mod.rs index af3102db..de44c132 100644 --- a/src/ristretto/mod.rs +++ b/src/ristretto/mod.rs @@ -13,7 +13,6 @@ pub mod ristretto_keys; mod ristretto_sig; #[cfg(feature = "serde")] pub mod serialize; -pub mod utils; pub use self::{ ristretto_com_and_pub_sig::RistrettoComAndPubSig, diff --git a/src/ristretto/pedersen/mod.rs b/src/ristretto/pedersen/mod.rs index 1230c05c..7fd70f49 100644 --- a/src/ristretto/pedersen/mod.rs +++ b/src/ristretto/pedersen/mod.rs @@ -90,7 +90,7 @@ mod test { let (_, p) = RistrettoPublicKey::random_keypair(&mut rng); let c = PedersenCommitment::from_public_key(&p); assert_eq!(c.as_public_key(), &p); - let c2 = PedersenCommitment::from_bytes(c.as_bytes()).unwrap(); + let c2 = PedersenCommitment::from_canonical_bytes(c.as_bytes()).unwrap(); assert_eq!(c, c2); } diff --git a/src/ristretto/ristretto_com_and_pub_sig.rs b/src/ristretto/ristretto_com_and_pub_sig.rs index 907b5beb..d3fd04f1 100644 --- a/src/ristretto/ristretto_com_and_pub_sig.rs +++ b/src/ristretto/ristretto_com_and_pub_sig.rs @@ -30,9 +30,18 @@ use crate::{ /// "8063d85e151abee630e643e2b3dc47bfaeb8aa859c9d10d60847985f286aad19", /// ) /// .unwrap(); -/// let u_a = RistrettoSecretKey::from_bytes(b"10000000000000000000000010000000").unwrap(); -/// let u_x = RistrettoSecretKey::from_bytes(b"a00000000000000000000000a0000000").unwrap(); -/// let u_y = RistrettoSecretKey::from_bytes(b"a00000000000000000000000a0000000").unwrap(); +/// let u_a = RistrettoSecretKey::from_hex( +/// "a8fb609c5ab7cc07548b076b6c25cc3237c4526fb7a6dcb83b26f457b172c20a", +/// ) +/// .unwrap(); +/// let u_x = RistrettoSecretKey::from_hex( +/// "0e689df8ad4ad9d2fd5aaf8cb0a66d85cb0d4b7a380405514d453625813b0b0f", +/// ) +/// .unwrap(); +/// let u_y = RistrettoSecretKey::from_hex( +/// "f494050bd0d4ed0ec514cdce9430d0564df6b35d2a12b7daa0e99c7d94a06509", +/// ) +/// .unwrap(); /// let sig = RistrettoComAndPubSig::new(ephemeral_commitment, ephemeral_pubkey, u_a, u_x, u_y); /// ``` /// @@ -48,7 +57,7 @@ use crate::{ /// # use tari_crypto::ristretto::pedersen::*; /// use tari_crypto::ristretto::pedersen::commitment_factory::PedersenCommitmentFactory; /// use tari_utilities::hex::Hex; -/// use digest::consts::U32; +/// use digest::consts::U64; /// /// let mut rng = rand::thread_rng(); /// let a_val = RistrettoSecretKey::random(&mut rng); @@ -57,7 +66,7 @@ use crate::{ /// let a_nonce = RistrettoSecretKey::random(&mut rng); /// let x_nonce = RistrettoSecretKey::random(&mut rng); /// let y_nonce = RistrettoSecretKey::random(&mut rng); -/// let e = Blake2b::::digest(b"Maskerade"); // In real life, this should be strong Fiat-Shamir! +/// let e = Blake2b::::digest(b"Maskerade"); // In real life, this should be strong Fiat-Shamir! /// let factory = PedersenCommitmentFactory::default(); /// let commitment = factory.commit(&x_val, &a_val); /// let pubkey = RistrettoPublicKey::from_secret_key(&y_val); @@ -72,8 +81,8 @@ pub type RistrettoComAndPubSig = CommitmentAndPublicKeySignature::new() + // Challenge; doesn't use domain-separated Fiat-Shamir, so it's for testing only! + let challenge = Blake2b::::new() .chain_update(commitment.as_bytes()) .chain_update(pubkey.as_bytes()) .chain_update(ephemeral_commitment.as_bytes()) .chain_update(ephemeral_pubkey.as_bytes()) .chain_update(b"Small Gods") .finalize(); - let e_key = RistrettoSecretKey::from_bytes(&challenge).unwrap(); + let e_key = RistrettoSecretKey::from_uniform_bytes(&challenge).unwrap(); // Responses let u_a = &r_a + e_key.clone() * &a_value; @@ -178,7 +187,7 @@ mod test { assert!(!sig.verify_challenge(&commitment, &evil_pubkey, &challenge, &factory, &mut rng)); // A different challenge should fail - let evil_challenge = Blake2b::::digest(b"Guards! Guards!"); + let evil_challenge = Blake2b::::digest(b"Guards! Guards!"); assert!(!sig.verify_challenge(&commitment, &pubkey, &evil_challenge, &factory, &mut rng)); } @@ -205,7 +214,7 @@ mod test { let ephemeral_pubkey = RistrettoPublicKey::from_secret_key(&r_y); // Challenge; doesn't use proper Fiat-Shamir, so it's for testing only! - let challenge = Blake2b::::new() + let challenge = Blake2b::::new() .chain_update(commitment.as_bytes()) .chain_update(pubkey.as_bytes()) .chain_update(ephemeral_commitment.as_bytes()) @@ -292,7 +301,7 @@ mod test { let ephemeral_pubkey_bob = RistrettoPublicKey::from_secret_key(&r_y_bob); // The challenge is common to Alice and Bob; here we use an arbitrary hash - let challenge = Blake2b::::digest(b"Test challenge"); + let challenge = Blake2b::::digest(b"Test challenge"); // Alice's signature let sig_alice = RistrettoComAndPubSig::sign( @@ -338,28 +347,6 @@ mod test { assert!(sig_sum.verify_challenge(&commitment_sum, &pubkey_sum, &challenge, &factory, &mut rng)) } - /// Ristretto scalars have a max value 2^255. This test checks that hashed messages above this value can still be - /// signed as a result of applying modulo arithmetic on the challenge value - #[test] - fn challenge_from_invalid_scalar() { - let mut rng = rand::thread_rng(); - let factory = PedersenCommitmentFactory::default(); - - let a_value = RistrettoSecretKey::random(&mut rng); - let x_value = RistrettoSecretKey::random(&mut rng); - let y_value = RistrettoSecretKey::random(&mut rng); - - let message = from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); - - let r_a = RistrettoSecretKey::random(&mut rng); - let r_x = RistrettoSecretKey::random(&mut rng); - let r_y = RistrettoSecretKey::random(&mut rng); - - assert!( - RistrettoComAndPubSig::sign(&a_value, &x_value, &y_value, &r_a, &r_x, &r_y, &message, &factory).is_ok() - ); - } - #[test] fn to_vec() { let sig = RistrettoComAndPubSig::default(); diff --git a/src/ristretto/ristretto_com_sig.rs b/src/ristretto/ristretto_com_sig.rs index 6e81fc73..4d790b38 100644 --- a/src/ristretto/ristretto_com_sig.rs +++ b/src/ristretto/ristretto_com_sig.rs @@ -13,7 +13,7 @@ use crate::{ /// /// ## Examples /// -/// You can create a `RistrettoComSig` from it's component parts: +/// You can create a `RistrettoComSig` from its component parts: /// /// ```edition2018 /// # use tari_crypto::ristretto::*; @@ -26,8 +26,14 @@ use crate::{ /// "8063d85e151abee630e643e2b3dc47bfaeb8aa859c9d10d60847985f286aad19", /// ) /// .unwrap(); -/// let u = RistrettoSecretKey::from_bytes(b"10000000000000000000000010000000").unwrap(); -/// let v = RistrettoSecretKey::from_bytes(b"a00000000000000000000000a0000000").unwrap(); +/// let u = RistrettoSecretKey::from_hex( +/// "a8fb609c5ab7cc07548b076b6c25cc3237c4526fb7a6dcb83b26f457b172c20a", +/// ) +/// .unwrap(); +/// let v = RistrettoSecretKey::from_hex( +/// "0e689df8ad4ad9d2fd5aaf8cb0a66d85cb0d4b7a380405514d453625813b0b0f", +/// ) +/// .unwrap(); /// let sig = RistrettoComSig::new(r_pub, u, v); /// ``` /// @@ -40,22 +46,20 @@ use crate::{ /// # use digest::Digest; /// # use tari_crypto::commitment::HomomorphicCommitmentFactory; /// # use tari_crypto::ristretto::pedersen::*; +/// use blake2::Blake2b; +/// use digest::consts::U64; /// use tari_crypto::ristretto::pedersen::commitment_factory::PedersenCommitmentFactory; /// use tari_utilities::hex::Hex; -/// use blake2::Blake2b; -/// use digest::consts::U32; /// /// let mut rng = rand::thread_rng(); /// let a_val = RistrettoSecretKey::random(&mut rng); /// let x_val = RistrettoSecretKey::random(&mut rng); /// let a_nonce = RistrettoSecretKey::random(&mut rng); /// let x_nonce = RistrettoSecretKey::random(&mut rng); -/// let e = Blake2b::::digest(b"Maskerade"); +/// let e = Blake2b::::digest(b"Maskerade"); /// let factory = PedersenCommitmentFactory::default(); /// let commitment = factory.commit(&x_val, &a_val); -/// // println!("commitment: {:?}", commitment.to_hex()); /// let sig = RistrettoComSig::sign(&a_val, &x_val, &a_nonce, &x_nonce, &e, &factory).unwrap(); -/// // println!("sig: R {:?} u {:?} v {:?}", sig.public_nonce().to_hex(), sig.u().to_hex(), sig.v().to_hex()); /// assert!(sig.verify_challenge(&commitment, &e, &factory)); /// ``` /// @@ -73,27 +77,27 @@ use crate::{ /// # use tari_utilities::ByteArray; /// # use digest::Digest; /// use blake2::Blake2b; -/// use digest::consts::U32; +/// use digest::consts::U64; /// use tari_crypto::ristretto::pedersen::commitment_factory::PedersenCommitmentFactory; /// /// let commitment = HomomorphicCommitment::from_hex( -/// "167c6df11bf8106e89328c297e57423dc2a9be53df1ee63f6e50b4610104ab4a", +/// "869b83416643258f1e03d028b5d0c652dc5b09decdae4a645fc5a43d87bd0a3e", /// ) /// .unwrap(); /// let r_nonce = HomomorphicCommitment::from_hex( -/// "4033e00996e61df2ea1abd1494b751b946663e21a20e2729c6592712beb15356", +/// "665400676bdf8b07679629f703ea86e9cfc7e145f0768d2fdde4bd257009260d", /// ) /// .unwrap(); /// let u = RistrettoSecretKey::from_hex( -/// "f44bbc3374b172f77ffa8b904ddf0ad9f879b3e6183f9e440c57e7f01e851300", +/// "f62fccf7734099d32937f7f767757abcb6eca70f43b3a7fb6500b2cb9ea12b02", /// ) /// .unwrap(); /// let v = RistrettoSecretKey::from_hex( -/// "fd54afb2d8008c8a3af10272b24161247b2b7ae11687813fe9fb03e34dd7f009", +/// "cb9e34a7745cabaec0f9b2c3e217bf18fbe7ee8f4c83c1a523cead32ec9b4700", /// ) /// .unwrap(); /// let sig = RistrettoComSig::new(r_nonce, u, v); -/// let e = Blake2b::::digest(b"Maskerade"); +/// let e = Blake2b::::digest(b"Maskerade"); /// let factory = PedersenCommitmentFactory::default(); /// assert!(sig.verify_challenge(&commitment, &e, &factory)); /// ``` @@ -102,8 +106,8 @@ pub type RistrettoComSig = CommitmentSignature::new() + let challenge = Blake2b::::new() .chain_update(commitment.as_bytes()) .chain_update(nonce_commitment.as_bytes()) .chain_update(b"Small Gods") .finalize(); - let e_key = RistrettoSecretKey::from_bytes(&challenge).unwrap(); + let e_key = RistrettoSecretKey::from_uniform_bytes(&challenge).unwrap(); let u_value = &k_1 + e_key.clone() * &x_value; let v_value = &k_2 + e_key * &a_value; let sig = RistrettoComSig::sign(&a_value, &x_value, &k_2, &k_1, &challenge, &factory).unwrap(); @@ -159,7 +163,7 @@ mod test { // Doesn't work for invalid credentials assert!(!sig.verify_challenge(&nonce_commitment, &challenge, &factory)); // Doesn't work for different challenge - let wrong_challenge = Blake2b::::digest(b"Guards! Guards!"); + let wrong_challenge = Blake2b::::digest(b"Guards! Guards!"); assert!(!sig.verify_challenge(&commitment, &wrong_challenge, &factory)); } @@ -185,7 +189,7 @@ mod test { let k_2_bob = RistrettoSecretKey::random(&mut rng); let nonce_commitment_bob = factory.commit(&k_1_bob, &k_2_bob); // Each of them creates the Challenge committing to both commitments of both parties - let challenge = Blake2b::::new() + let challenge = Blake2b::::new() .chain_update(commitment_alice.as_bytes()) .chain_update(commitment_bob.as_bytes()) .chain_update(nonce_commitment_alice.as_bytes()) @@ -212,20 +216,6 @@ mod test { assert!(s_agg.verify_challenge(&combined_commitment, &challenge, &factory)); } - /// Ristretto scalars have a max value 2^255. This test checks that hashed messages above this value can still be - /// signed as a result of applying modulo arithmetic on the challenge value - #[test] - fn challenge_from_invalid_scalar() { - let mut rng = rand::thread_rng(); - let factory = PedersenCommitmentFactory::default(); - let a_value = RistrettoSecretKey::random(&mut rng); - let x_value = RistrettoSecretKey::random(&mut rng); - let message = from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); - let k_1 = RistrettoSecretKey::random(&mut rng); - let k_2 = RistrettoSecretKey::random(&mut rng); - assert!(RistrettoComSig::sign(&a_value, &x_value, &k_2, &k_1, &message, &factory).is_ok()); - } - #[test] fn to_vec() { let sig = RistrettoComSig::default(); diff --git a/src/ristretto/ristretto_keys.rs b/src/ristretto/ristretto_keys.rs index 6a53b287..16fc25c3 100644 --- a/src/ristretto/ristretto_keys.rs +++ b/src/ristretto/ristretto_keys.rs @@ -22,7 +22,7 @@ use digest::{consts::U64, Digest}; use once_cell::sync::OnceCell; use rand_core::{CryptoRng, RngCore}; use tari_utilities::{hex::Hex, ByteArray, ByteArrayError, Hashable}; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use crate::{ errors::HashingError, @@ -44,7 +44,7 @@ use crate::{ /// use tari_utilities::{hex::Hex, ByteArray}; /// /// let mut rng = rand::thread_rng(); -/// let _k1 = RistrettoSecretKey::from_bytes(&[ +/// let _k1 = RistrettoSecretKey::from_canonical_bytes(&[ /// 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /// 0, 0, /// ]); @@ -66,7 +66,7 @@ impl borsh::BorshDeserialize for RistrettoSecretKey { fn deserialize_reader(reader: &mut R) -> Result where R: borsh::maybestd::io::Read { let bytes: Vec = borsh::BorshDeserialize::deserialize_reader(reader)?; - Self::from_bytes(bytes.as_slice()) + Self::from_canonical_bytes(bytes.as_slice()) .map_err(|e| borsh::maybestd::io::Error::new(borsh::maybestd::io::ErrorKind::InvalidInput, e.to_string())) } } @@ -74,29 +74,49 @@ impl borsh::BorshDeserialize for RistrettoSecretKey { //----------------------------------------- Ristretto Secret Key ------------------------------------------------// impl SecretKey for RistrettoSecretKey { const KEY_LEN: usize = 32; + const WIDE_REDUCTION_LEN: usize = 64; /// Return a random secret key on the `ristretto255` curve using the supplied CSPRNG. fn random(rng: &mut R) -> Self { RistrettoSecretKey(Scalar::random(rng)) } + + /// Return a secret key computed from a uniform byte slice using wide reduction + /// If the byte array is not exactly 64 bytes, returns an error + fn from_uniform_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::WIDE_REDUCTION_LEN { + return Err(ByteArrayError::IncorrectLength {}); + } + + let mut bytes_copied = Zeroizing::new([0u8; Self::WIDE_REDUCTION_LEN]); + bytes_copied.copy_from_slice(bytes); + + Ok(RistrettoSecretKey(Scalar::from_bytes_mod_order_wide(&bytes_copied))) + } } //------------------------------------- Ristretto Secret Key ByteArray ---------------------------------------------// impl ByteArray for RistrettoSecretKey { - /// Create a secret key on the Ristretto255 curve using the given little-endian byte array. If the byte array is - /// not exactly 32 bytes long, `from_bytes` returns an error. This function is guaranteed to return a valid key - /// in the group since it performs a mod _l_ on the input. - fn from_bytes(bytes: &[u8]) -> Result + /// Return a secret key computed from a canonical byte array + /// If the byte array is not exactly 32 bytes, returns an error + /// If the byte array does not represent a canonical encoding, returns an error + fn from_canonical_bytes(bytes: &[u8]) -> Result where Self: Sized { - if bytes.len() != 32 { + if bytes.len() != Self::KEY_LEN { return Err(ByteArrayError::IncorrectLength {}); } - let mut a = [0u8; 32]; - a.copy_from_slice(bytes); - let k = Scalar::from_bytes_mod_order(a); - a.zeroize(); - Ok(RistrettoSecretKey(k)) + + let mut bytes_copied = [0u8; 32]; + bytes_copied.copy_from_slice(bytes); + let scalar = Option::::from(Scalar::from_canonical_bytes(bytes_copied)).ok_or( + ByteArrayError::ConversionError { + reason: ("Invalid canonical scalar byte array".to_string()), + }, + )?; + bytes_copied.zeroize(); + + Ok(RistrettoSecretKey(scalar)) } /// Return the byte array for the secret key in little-endian order @@ -245,7 +265,7 @@ impl<'a> Borrow for &'a RistrettoSecretKey { /// use tari_utilities::{hex::Hex, ByteArray}; /// /// let mut rng = rand::thread_rng(); -/// let _p1 = RistrettoPublicKey::from_bytes(&[ +/// let _p1 = RistrettoPublicKey::from_canonical_bytes(&[ /// 224, 196, 24, 247, 200, 217, 196, 205, 215, 57, 91, 147, 234, 18, 79, 58, 217, 144, 33, /// 187, 104, 29, 252, 51, 2, 169, 217, 154, 46, 83, 230, 78, /// ]); @@ -273,7 +293,7 @@ impl borsh::BorshDeserialize for RistrettoPublicKey { fn deserialize_reader(reader: &mut R) -> Result where R: borsh::maybestd::io::Read { let bytes: Vec = borsh::BorshDeserialize::deserialize_reader(reader)?; - Self::from_bytes(bytes.as_slice()) + Self::from_canonical_bytes(bytes.as_slice()) .map_err(|e| borsh::maybestd::io::Error::new(borsh::maybestd::io::ErrorKind::InvalidInput, e.to_string())) } } @@ -484,7 +504,7 @@ impl ByteArray for RistrettoPublicKey { /// the following circumstances: /// * The byte array is not exactly 32 bytes /// * The byte array does not represent a valid (compressed) point on the ristretto255 curve - fn from_bytes(bytes: &[u8]) -> Result + fn from_canonical_bytes(bytes: &[u8]) -> Result where Self: Sized { // Check the length here, because The Ristretto constructor panics rather than returning an error if bytes.len() != 32 { @@ -598,7 +618,8 @@ impl From for CompressedRistretto { #[cfg(test)] mod test { - use digest::consts::U32; + use blake2::Blake2b; + use digest::consts::{U32, U64}; use tari_utilities::ByteArray; use super::*; @@ -629,7 +650,7 @@ mod test { #[test] fn invalid_secret_key_bytes() { - RistrettoSecretKey::from_bytes(&[1, 2, 3]).expect_err("Secret keys should be 32 bytes"); + RistrettoSecretKey::from_canonical_bytes(&[1, 2, 3]).expect_err("Secret keys should be 32 bytes"); } #[test] @@ -659,7 +680,7 @@ mod test { for i in 0u8..16 { let pk = RistrettoPublicKey::from_hex(encodings_of_small_multiples[i as usize]).unwrap(); bytes[0] = i; - let sk = RistrettoSecretKey::from_bytes(&bytes).unwrap(); + let sk = RistrettoSecretKey::from_canonical_bytes(&bytes).unwrap(); let pk2 = RistrettoPublicKey::from_secret_key(&sk); assert_eq!(pk, pk2); } @@ -967,35 +988,38 @@ mod test { } #[test] - fn kdf_key_too_short() { - let err = RistrettoKdf::generate::>(b"this_key_is_too_short", b"data", "test").err(); + fn kdf_too_short() { + let err = RistrettoKdf::generate::>(b"this_hasher_is_too_short", b"data", "test").err(); + assert!(matches!(err, Some(HashingError::InputTooShort {}))); + + let err = RistrettoKdf::generate::>(b"this_key_is_too_short", b"data", "test").err(); assert!(matches!(err, Some(HashingError::InputTooShort {}))); } #[test] fn kdf_test() { let key = - RistrettoSecretKey::from_hex("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c").unwrap(); - let derived1 = RistrettoKdf::generate::>(key.as_bytes(), b"derived1", "test").unwrap(); - let derived2 = RistrettoKdf::generate::>(key.as_bytes(), b"derived2", "test").unwrap(); + RistrettoSecretKey::from_hex("45c5b950e04167785ff735bead8d746740db04bce3ee2c1f6523bdc59023e50e").unwrap(); + let derived1 = RistrettoKdf::generate::>(key.as_bytes(), b"derived1", "test").unwrap(); + let derived2 = RistrettoKdf::generate::>(key.as_bytes(), b"derived2", "test").unwrap(); assert_eq!( derived1.to_hex(), - "e8df6fa40344c1fde721e9a35d46daadb48dc66f7901a9795ebb0374474ea601" + "22deb0c38ec2dc9f741912f6e3c2cd3f76a5b33142a289da15eecdcd882bda06" ); assert_eq!( derived2.to_hex(), - "3ae035e2663d9c561300cca67743ccdb56ea07ca7dacd8394356c4354b030e0c" + "fcca9ca5c993d817581ab6040d92feef4c78529b5a485b51d0e11af427944207" ); } #[test] fn visibility_test() { let key = - RistrettoSecretKey::from_hex("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c").unwrap(); + RistrettoSecretKey::from_hex("cd4f563f5e53a67ade2400b281e75f4b306253501da6529338e7be4f2b5c6200").unwrap(); let invisible = format!("{key:?}"); - assert!(!invisible.contains("016c")); + assert!(invisible.contains("***")); let visible = format!("{:?}", key.reveal()); - assert!(visible.contains("016c")); + assert!(visible.contains("cd4f563f5e53a67ade2400b281e75f4b306253501da6529338e7be4f2b5c6200")); } #[test] diff --git a/src/ristretto/ristretto_sig.rs b/src/ristretto/ristretto_sig.rs index 9e1c152e..055821b4 100644 --- a/src/ristretto/ristretto_sig.rs +++ b/src/ristretto/ristretto_sig.rs @@ -18,7 +18,7 @@ use crate::{ /// /// ## Creating signatures /// -/// You can create a `RisrettoSchnorr` from it's component parts: +/// You can create a `RisrettoSchnorr` from its component parts: /// /// ```edition2018 /// # use tari_crypto::ristretto::*; @@ -31,7 +31,10 @@ use crate::{ /// "6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919", /// ) /// .unwrap(); -/// let s = RistrettoSecretKey::from_bytes(b"10000000000000000000000000000000").unwrap(); +/// let s = RistrettoSecretKey::from_hex( +/// "f62fccf7734099d32937f7f767757abcb6eca70f43b3a7fb6500b2cb9ea12b02", +/// ) +/// .unwrap(); /// let sig = RistrettoSchnorr::new(public_r, s); /// ``` /// @@ -55,7 +58,7 @@ use crate::{ /// let (k, P) = get_keypair(); /// let msg = "Small Gods"; /// let mut rng = thread_rng(); -/// let sig = RistrettoSchnorr::sign_message(&k, &msg, &mut rng); +/// let sig = RistrettoSchnorr::sign(&k, &msg, &mut rng); /// ``` /// /// # Verifying signatures @@ -81,8 +84,8 @@ use crate::{ /// let P = RistrettoPublicKey::from_secret_key(&k); /// let mut rng = thread_rng(); /// let sig: SchnorrSignature = -/// SchnorrSignature::sign_message(&k, msg, &mut rng).unwrap(); -/// assert!(sig.verify_message(&P, msg)); +/// SchnorrSignature::sign(&k, msg, &mut rng).unwrap(); +/// assert!(sig.verify(&P, msg)); /// ``` pub type RistrettoSchnorr = SchnorrSignature; @@ -113,17 +116,17 @@ pub type RistrettoSchnorr = SchnorrSignature = -/// SchnorrSignature::sign_message(&k, msg, &mut rng).unwrap(); -/// assert!(sig.verify_message(&P, msg)); +/// SchnorrSignature::sign(&k, msg, &mut rng).unwrap(); +/// assert!(sig.verify(&P, msg)); /// ``` pub type RistrettoSchnorrWithDomain = SchnorrSignature; #[cfg(test)] mod test { use blake2::Blake2b; - use digest::{consts::U32, Digest}; + use digest::{consts::U64, Digest}; use tari_utilities::{ - hex::{from_hex, to_hex, Hex}, + hex::{to_hex, Hex}, ByteArray, }; @@ -154,23 +157,23 @@ mod test { let (k, P) = RistrettoPublicKey::random_keypair(&mut rng); let (r, R) = RistrettoPublicKey::random_keypair(&mut rng); // Use sign raw, and bind the nonce and public key manually - let e = Blake2b::::new() + let e = Blake2b::::new() .chain_update(P.as_bytes()) .chain_update(R.as_bytes()) .chain_update(b"Small Gods") .finalize(); - let e_key = RistrettoSecretKey::from_bytes(&e).unwrap(); + let e_key = RistrettoSecretKey::from_uniform_bytes(&e).unwrap(); let s = &r + &e_key * &k; - let sig = RistrettoSchnorr::sign_raw(&k, r, &e).unwrap(); + let sig = RistrettoSchnorr::sign_raw_uniform(&k, r, &e).unwrap(); let R_calc = sig.get_public_nonce(); assert_eq!(R, *R_calc); assert_eq!(sig.get_signature(), &s); - assert!(sig.verify_challenge(&P, &e)); + assert!(sig.verify_raw_uniform(&P, &e)); // Doesn't work for invalid credentials - assert!(!sig.verify_challenge(&R, &e)); + assert!(!sig.verify_raw_uniform(&R, &e)); // Doesn't work for different challenge - let wrong_challenge = Blake2b::::digest(b"Guards! Guards!"); - assert!(!sig.verify_challenge(&P, &wrong_challenge)); + let wrong_challenge = Blake2b::::digest(b"Guards! Guards!"); + assert!(!sig.verify_raw_uniform(&P, &wrong_challenge)); } /// This test checks that the linearity of Schnorr signatures hold, i.e. that s = s1 + s2 is validated by R1 + R2 @@ -185,7 +188,7 @@ mod test { let (k2, P2) = RistrettoPublicKey::random_keypair(&mut rng); let (r2, R2) = RistrettoPublicKey::random_keypair(&mut rng); // Each of them creates the Challenge = H(R1 || R2 || P1 || P2 || m) - let e = Blake2b::::new() + let e = Blake2b::::new() .chain_update(R1.as_bytes()) .chain_update(R2.as_bytes()) .chain_update(P1.as_bytes()) @@ -193,25 +196,13 @@ mod test { .chain_update(b"Moving Pictures") .finalize(); // Calculate Alice's signature - let s1 = RistrettoSchnorr::sign_raw(&k1, r1, &e).unwrap(); + let s1 = RistrettoSchnorr::sign_raw_uniform(&k1, r1, &e).unwrap(); // Calculate Bob's signature - let s2 = RistrettoSchnorr::sign_raw(&k2, r2, &e).unwrap(); + let s2 = RistrettoSchnorr::sign_raw_uniform(&k2, r2, &e).unwrap(); // Now add the two signatures together let s_agg = &s1 + &s2; // Check that the multi-sig verifies - assert!(s_agg.verify_challenge(&(P1 + P2), &e)); - } - - /// Ristretto scalars have a max value 2^255. This test checks that hashed messages above this value can still be - /// signed as a result of applying modulo arithmetic on the challenge value - #[test] - #[allow(non_snake_case)] - fn challenge_from_invalid_scalar() { - let mut rng = rand::thread_rng(); - let m = from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); - let k = RistrettoSecretKey::random(&mut rng); - let r = RistrettoSecretKey::random(&mut rng); - assert!(RistrettoSchnorr::sign_raw(&k, r, &m).is_ok()); + assert!(s_agg.verify_raw_uniform(&(P1 + P2), &e)); } #[test] @@ -222,10 +213,10 @@ mod test { let R = RistrettoPublicKey::from_hex("fa14cb581ce5717248444721242e6b195a482d503a853dea4acb513074d8d803").unwrap(); let msg = "Moving Pictures"; - let hash = SchnorrSignature::<_, _, SchnorrSigChallenge>::construct_domain_separated_challenge::<_, Blake2b>( + let hash = SchnorrSignature::<_, _, SchnorrSigChallenge>::construct_domain_separated_challenge::<_, Blake2b>( &R, &P, msg, ); - let naiive = Blake2b::::new() + let naiive = Blake2b::::new() .chain_update(R.as_bytes()) .chain_update(P.as_bytes()) .chain_update(msg) @@ -234,7 +225,7 @@ mod test { assert_ne!(hash.as_ref(), naiive.as_bytes()); assert_eq!( to_hex(hash.as_ref()), - "d8f6b29b641113c91175b8d44f265ff1167d58d5aa5ee03e6f1f521505b09d80" + "2db0656c9dd1482bf61d32f157726b05a88d567c31107bed9a5c60a02119518af35929f360726bffd846439ab12e7c9f4983cf5fab5ea735422e05e0f560ddfd" ); } @@ -256,8 +247,8 @@ mod test { // assert_ne!(sig1, sig2); // Prove that the nonces were reused. Again, NEVER do this assert_eq!(sig1.get_public_nonce(), sig2.get_public_nonce()); - assert!(sig1.verify_message(&P, msg)); - assert!(sig2.verify_message(&P, msg)); + assert!(sig1.verify(&P, msg)); + assert!(sig2.verify(&P, msg)); // But the signatures are different, for the same message, secret and nonce. assert_ne!(sig1.get_signature(), sig2.get_signature()); } @@ -267,10 +258,9 @@ mod test { fn sign_and_verify_message() { let mut rng = rand::thread_rng(); let (k, P) = RistrettoPublicKey::random_keypair(&mut rng); - let sig = - RistrettoSchnorr::sign_message(&k, "Queues are things that happen to other people", &mut rng).unwrap(); - assert!(sig.verify_message(&P, "Queues are things that happen to other people")); - assert!(!sig.verify_message(&P, "Qs are things that happen to other people")); - assert!(!sig.verify_message(&(&P + &P), "Queues are things that happen to other people")); + let sig = RistrettoSchnorr::sign(&k, "Queues are things that happen to other people", &mut rng).unwrap(); + assert!(sig.verify(&P, "Queues are things that happen to other people")); + assert!(!sig.verify(&P, "Qs are things that happen to other people")); + assert!(!sig.verify(&(&P + &P), "Queues are things that happen to other people")); } } diff --git a/src/ristretto/serialize.rs b/src/ristretto/serialize.rs index 6d7aafba..b213322b 100644 --- a/src/ristretto/serialize.rs +++ b/src/ristretto/serialize.rs @@ -51,7 +51,7 @@ impl<'de> Deserialize<'de> for RistrettoPublicKey { fn visit_bytes(self, v: &[u8]) -> Result where E: de::Error { - RistrettoPublicKey::from_bytes(v).map_err(E::custom) + RistrettoPublicKey::from_canonical_bytes(v).map_err(E::custom) } } @@ -89,7 +89,7 @@ impl<'de> Deserialize<'de> for RistrettoSecretKey { fn visit_bytes(self, v: &[u8]) -> Result where E: de::Error { - RistrettoSecretKey::from_bytes(v).map_err(E::custom) + RistrettoSecretKey::from_canonical_bytes(v).map_err(E::custom) } } diff --git a/src/ristretto/utils.rs b/src/ristretto/utils.rs deleted file mode 100644 index dfc2684a..00000000 --- a/src/ristretto/utils.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019. The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -//! Handy utility functions for use in tests and demo scripts - -use alloc::vec::Vec; - -use digest::Digest; -use rand_core::{CryptoRng, RngCore}; -use tari_utilities::ByteArray; - -use crate::{ - keys::PublicKey, - ristretto::{RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey}, - signatures::SchnorrSignatureError, -}; - -/// A set of keys and it's associated signature -pub struct SignatureSet { - /// The secret nonce - pub nonce: RistrettoSecretKey, - /// The public nonce - pub public_nonce: RistrettoPublicKey, - /// The message signed. Note that the [SignatureSet::public_nonce] is prepended to this message before signing - pub message: Vec, - /// The signature - pub signature: RistrettoSchnorr, -} - -/// Generate a random keypair and a signature for the provided message -/// -/// # Panics -/// -/// The function panics if it cannot generate a suitable signature -#[deprecated( - since = "0.16.0", - note = "Use SchnorrSignature::sign_message instead. This method will be removed in v1.0.0" -)] -pub fn sign( - private_key: &RistrettoSecretKey, - message: &[u8], - rng: &mut R, -) -> Result { - let (nonce, public_nonce) = RistrettoPublicKey::random_keypair(rng); - let message = D::new() - .chain_update(public_nonce.as_bytes()) - .chain_update(message) - .finalize() - .to_vec(); - let e = RistrettoSecretKey::from_bytes(&message).map_err(|_| SchnorrSignatureError::InvalidChallenge)?; - let s = RistrettoSchnorr::sign_raw(private_key, nonce.clone(), e.as_bytes())?; - Ok(SignatureSet { - nonce, - public_nonce, - message, - signature: s, - }) -} diff --git a/src/signatures/commitment_and_public_key_signature.rs b/src/signatures/commitment_and_public_key_signature.rs index a02d6ccc..cf526d46 100644 --- a/src/signatures/commitment_and_public_key_signature.rs +++ b/src/signatures/commitment_and_public_key_signature.rs @@ -102,8 +102,8 @@ where for<'a> &'a K: Mul<&'a K, Output = K>, C: HomomorphicCommitmentFactory

, { - // The challenge must be a valid scalar - let e = match K::from_bytes(challenge) { + // The challenge is computed by wide reduction + let e = match K::from_uniform_bytes(challenge) { Ok(e) => e, Err(_) => return Err(CommitmentAndPublicKeySignatureError::InvalidChallenge), }; @@ -148,8 +148,8 @@ where C: HomomorphicCommitmentFactory

, R: RngCore + CryptoRng, { - // The challenge must be a valid scalar - let e = match K::from_bytes(challenge) { + // The challenge is computed by wide reduction + let e = match K::from_uniform_bytes(challenge) { Ok(e) => e, Err(_) => return false, }; diff --git a/src/signatures/commitment_signature.rs b/src/signatures/commitment_signature.rs index a114f425..401d665c 100644 --- a/src/signatures/commitment_signature.rs +++ b/src/signatures/commitment_signature.rs @@ -92,7 +92,7 @@ where for<'a> &'a K: Mul<&'a K, Output = K>, C: HomomorphicCommitmentFactory

, { - let e = match K::from_bytes(challenge) { + let e = match K::from_uniform_bytes(challenge) { Ok(e) => e, Err(_) => return Err(CommitmentSignatureError::InvalidChallenge), }; @@ -120,7 +120,7 @@ where for<'b> &'b HomomorphicCommitment

: Add<&'b HomomorphicCommitment

, Output = HomomorphicCommitment

>, C: HomomorphicCommitmentFactory

, { - let e = match K::from_bytes(challenge) { + let e = match K::from_uniform_bytes(challenge) { Ok(e) => e, Err(_) => return false, }; @@ -178,9 +178,9 @@ where if buf.len() != P::KEY_LEN + 2 * K::key_length() { return Err(ByteArrayError::IncorrectLength {}); } - let public_nonce = HomomorphicCommitment::from_public_key(&P::from_bytes(&buf[0..P::KEY_LEN])?); - let u = K::from_bytes(&buf[P::KEY_LEN..P::KEY_LEN + K::key_length()])?; - let v = K::from_bytes(&buf[P::KEY_LEN + K::key_length()..P::KEY_LEN + 2 * K::key_length()])?; + let public_nonce = HomomorphicCommitment::from_public_key(&P::from_canonical_bytes(&buf[0..P::KEY_LEN])?); + let u = K::from_canonical_bytes(&buf[P::KEY_LEN..P::KEY_LEN + K::key_length()])?; + let v = K::from_canonical_bytes(&buf[P::KEY_LEN + K::key_length()..P::KEY_LEN + 2 * K::key_length()])?; Ok(Self { public_nonce, u, v }) } diff --git a/src/signatures/schnorr.rs b/src/signatures/schnorr.rs index e9447a14..535872ef 100644 --- a/src/signatures/schnorr.rs +++ b/src/signatures/schnorr.rs @@ -13,7 +13,7 @@ use core::{ }; use blake2::Blake2b; -use digest::{consts::U32, Digest}; +use digest::{consts::U64, Digest}; use rand_core::{CryptoRng, RngCore}; use snafu::prelude::*; use tari_utilities::ByteArray; @@ -73,40 +73,43 @@ where P::from_secret_key(&self.signature) } - /// Sign a challenge with the given `secret` and private `nonce`. Returns an SchnorrSignatureError if `::from_bytes(challenge)` returns an error. + /// Generate a signature using a given secret key, nonce, and challenge byte slice. /// - /// WARNING: The public key and nonce are NOT bound to the challenge. This method assumes that the challenge has - /// been constructed such that all commitments are already included in the challenge. + /// WARNING: This is intended for use cases where the challenge byte slice was generated correctly. + /// In particlar, it _must_ be the result of securely applying a cryptographic hash function to the correct public + /// key, public nonce, and input message; further, it must be of a length suitable for scalar wide reduction. + /// This function only checks that the byte slice is of the correct length. + /// The nonce _must_ also have been sampled uniformly at random and not reused with the same secret key and a + /// different message. /// - /// Use [`sign_raw`] instead if this is what you want. (This method is a deprecated alias for `sign_raw`). - /// - /// If you want a simple API that binds the nonce and public key to the message, use [`sign_message`] instead. - #[deprecated( - since = "0.16.0", - note = "This method probably doesn't do what you think it does. Please use `sign_message` or `sign_raw` \ - instead, depending on your use case. This function will be removed in v1.0.0" - )] - #[allow(clippy::needless_pass_by_value)] - pub fn sign(secret: K, nonce: K, challenge: &[u8]) -> Result - where - K: Add, - for<'a> K: Mul<&'a K, Output = K>, - { - Self::sign_raw(&secret, nonce, challenge) + /// If you aren't sure that you can meet these requirements, and want a simple and safe API, use [`sign`]. + pub fn sign_raw_uniform<'a>(secret: &'a K, nonce: K, challenge: &[u8]) -> Result + where K: Add + Mul<&'a K, Output = K> { + // s = r + e.k + let e = match K::from_uniform_bytes(challenge) { + Ok(e) => e, + Err(_) => return Err(SchnorrSignatureError::InvalidChallenge), + }; + let public_nonce = P::from_secret_key(&nonce); + let ek = e * secret; + let s = ek + nonce; + Ok(Self::new(public_nonce, s)) } - /// Sign a challenge with the given `secret` and private `nonce`. Returns an SchnorrSignatureError if `::from_bytes(challenge)` returns an error. + /// Generate a signature using a given secret key, nonce, and challenge byte slice. /// - /// WARNING: The public key and nonce are NOT bound to the challenge. This method assumes that the challenge has - /// been constructed such that all commitments are already included in the challenge. + /// WARNING: This is intended for use cases where the challenge byte slice was generated correctly. + /// In particlar, it _must_ be the result of securely applying a cryptographic hash function to the correct public + /// key, public nonce, and input message; further, it must be the canonical representation of a scalar. + /// This function only checks that the byte slice is of the correct length. + /// The nonce _must_ also have been sampled uniformly at random and not reused with the same secret key and a + /// different message. /// - /// If you want a simple API that binds the nonce and public key to the message, use [`sign_message`] instead. - pub fn sign_raw<'a>(secret: &'a K, nonce: K, challenge: &[u8]) -> Result + /// If you aren't sure that you can meet these requirements, and want a simple and safe API, use [`sign`]. + pub fn sign_raw_canonical<'a>(secret: &'a K, nonce: K, challenge: &[u8]) -> Result where K: Add + Mul<&'a K, Output = K> { // s = r + e.k - let e = match K::from_bytes(challenge) { + let e = match K::from_canonical_bytes(challenge) { Ok(e) => e, Err(_) => return Err(SchnorrSignatureError::InvalidChallenge), }; @@ -119,11 +122,8 @@ where /// Signs a message with the given secret key. /// /// This method correctly binds a nonce and the public key to the signature challenge, using domain-separated - /// hashing. The hasher is also opinionated in the sense that Blake2b 256-bit digest is always used. - /// - /// it is possible to customise the challenge by using [`construct_domain_separated_challenge`] and [`sign_raw`] - /// yourself, or even use [`sign_raw`] using a completely custom challenge. - pub fn sign_message<'a, B, R: RngCore + CryptoRng>( + /// hashing. The hasher is also opinionated in the sense that Blake2b 512-bit digest is always used. + pub fn sign<'a, B, R: RngCore + CryptoRng>( secret: &'a K, message: B, rng: &mut R, @@ -136,16 +136,13 @@ where Self::sign_with_nonce_and_message(secret, nonce, message) } - /// Signs a message with the given secret key and provided nonce. + /// Signs a message with the given secret key and nonce. /// /// This method correctly binds the nonce and the public key to the signature challenge, using domain-separated - /// hashing. The hasher is also opinionated in the sense that Blake2b 256-bit digest is always used. - /// - /// ** Important **: It is the caller's responsibility to ensure that the nonce is unique. This API tries to - /// prevent this by taking ownership of the nonce, which means that the caller has to explicitly clone the nonce - /// in order to re-use it, which is a small deterrent, but better than nothing. + /// hashing. The hasher is also opinionated in the sense that Blake2b 512-bit digest is always used. /// - /// To delegate nonce handling to the callee, use [`Self::sign_message`] instead. + /// WARNING: The nonce _must_ also have been sampled uniformly at random and not reused with the same secret key and + /// a different message. pub fn sign_with_nonce_and_message<'a, B>( secret: &'a K, nonce: K, @@ -158,8 +155,8 @@ where let public_nonce = P::from_secret_key(&nonce); let public_key = P::from_secret_key(secret); let challenge = - Self::construct_domain_separated_challenge::<_, Blake2b>(&public_nonce, &public_key, message); - Self::sign_raw(secret, nonce, challenge.as_ref()) + Self::construct_domain_separated_challenge::<_, Blake2b>(&public_nonce, &public_key, message); + Self::sign_raw_uniform(secret, nonce, challenge.as_ref()) } /// Constructs an opinionated challenge hash for the given public nonce, public key and message. @@ -168,8 +165,8 @@ where /// the challenge. In this implementation, the challenge is constructed by means of domain separated hashing /// using the provided digest. /// - /// This challenge is used in the [`sign_message`] and [`verify_message`] methods.If you wish to use a custom - /// challenge, you can use [`sign_raw`] instead. + /// This challenge is used in the [`sign_message`] and [`verify_message`] methods. If you wish to use a custom + /// challenge, you can use [`sign_raw_canonical`] or [`sign_raw_wide`] instead. pub fn construct_domain_separated_challenge( public_nonce: &P, public_key: &P, @@ -186,36 +183,50 @@ where .finalize() } - /// Verifies a signature created by the `sign_message` method. The function returns `true` if and only if the + /// Verifies a signature created by the `sign` method. The function returns `true` if and only if the /// message was signed by the secret key corresponding to the given public key, and that the challenge was /// constructed using the domain-separation method defined in [`construct_domain_separated_challenge`]. - pub fn verify_message<'a, B>(&self, public_key: &'a P, message: B) -> bool + pub fn verify<'a, B>(&self, public_key: &'a P, message: B) -> bool where for<'b> &'b K: Mul<&'a P, Output = P>, for<'b> &'b P: Add, B: AsRef<[u8]>, { let challenge = - Self::construct_domain_separated_challenge::<_, Blake2b>(&self.public_nonce, public_key, message); - self.verify_challenge(public_key, challenge.as_ref()) + Self::construct_domain_separated_challenge::<_, Blake2b>(&self.public_nonce, public_key, message); + self.verify_raw_uniform(public_key, challenge.as_ref()) + } + + /// Verifies a signature against a given public key and challenge byte slice. + /// The byte slice is converted to a scalar using wide reduction. + pub fn verify_raw_uniform<'a>(&self, public_key: &'a P, challenge: &[u8]) -> bool + where + for<'b> &'b K: Mul<&'a P, Output = P>, + for<'b> &'b P: Add, + { + let e = match K::from_uniform_bytes(challenge) { + Ok(e) => e, + Err(_) => return false, + }; + self.verify_challenge_scalar(public_key, &e) } - /// Returns true if this signature is valid for a public key and challenge, otherwise false. This will always return - /// false if `::from_bytes(challenge)` returns an error. - pub fn verify_challenge<'a>(&self, public_key: &'a P, challenge: &[u8]) -> bool + /// Verifies a signature against a given public key and challenge byte slice. + /// The byte slice is converted to a scalar assuming a canonical representation. + pub fn verify_raw_canonical<'a>(&self, public_key: &'a P, challenge: &[u8]) -> bool where for<'b> &'b K: Mul<&'a P, Output = P>, for<'b> &'b P: Add, { - let e = match K::from_bytes(challenge) { + let e = match K::from_canonical_bytes(challenge) { Ok(e) => e, Err(_) => return false, }; - self.verify(public_key, &e) + self.verify_challenge_scalar(public_key, &e) } /// Returns true if this signature is valid for a public key and challenge scalar, otherwise false. - pub fn verify<'a>(&self, public_key: &'a P, challenge: &K) -> bool + pub fn verify_challenge_scalar<'a>(&self, public_key: &'a P, challenge: &K) -> bool where for<'b> &'b K: Mul<&'a P, Output = P>, for<'b> &'b P: Add,