From f93bd42b998df81927ff77cb21dccc05e308822c Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 28 Jul 2024 01:14:44 -0400 Subject: [PATCH] Resolve various TODOs Supports recovering multiple key shares from the eVRF DKG. Inlines two loops to save 2**16 iterations. Adds support for creating a constant time representation of scalars < NUM_BITS. --- crypto/dkg/src/evrf/mod.rs | 103 +++++++-------- crypto/dkg/src/evrf/proof.rs | 207 +++++++++++++++++++++---------- crypto/dkg/src/tests/evrf/mod.rs | 8 +- 3 files changed, 196 insertions(+), 122 deletions(-) diff --git a/crypto/dkg/src/evrf/mod.rs b/crypto/dkg/src/evrf/mod.rs index 6020f2c5..729c7406 100644 --- a/crypto/dkg/src/evrf/mod.rs +++ b/crypto/dkg/src/evrf/mod.rs @@ -84,14 +84,14 @@ use ciphersuite::{ }; use multiexp::multiexp_vartime; -use generalized_bulletproofs::{Generators, arithmetic_circuit_proof::*}; +use generalized_bulletproofs::arithmetic_circuit_proof::*; use ec_divisors::DivisorCurve; -use crate::{Participant, DkgError, ThresholdParams, ThresholdCore}; +use crate::{Participant, ThresholdParams, ThresholdCore, ThresholdKeys}; pub(crate) mod proof; use proof::*; -pub use proof::EvrfCurve; +pub use proof::{EvrfCurve, EvrfGenerators}; /// Participation in the DKG. /// @@ -240,27 +240,18 @@ where <::EmbeddedCurve as Ciphersuite>::G: DivisorCurve::F>, { - /// Sample generators for this ciphersuite. - pub fn generators(max_threshold: u16, max_participants: u16) -> Generators { - Evrf::::generators(max_threshold, max_participants) - } - /// Participate in performing the DKG for the specified parameters. /// /// The context MUST be unique across invocations. Reuse of context will lead to sharing /// prior-shared secrets. pub fn participate( rng: &mut (impl RngCore + CryptoRng), - generators: &Generators, + generators: &EvrfGenerators, context: [u8; 32], t: u16, evrf_public_keys: &[::G], evrf_private_key: &Zeroizing<::F>, ) -> Result, EvrfError> { - if generators.g() != C::generator() { - todo!("TODO"); - } - let evrf_public_key = ::generator() * evrf_private_key.deref(); let Ok(n) = u16::try_from(evrf_public_keys.len()) else { Err(EvrfError::TooManyParticipants)? }; if (t == 0) || (t > n) { @@ -272,7 +263,7 @@ where let EvrfProveResult { coefficients, encryption_masks, proof } = match Evrf::prove( rng, - generators, + &generators.0, evrf_private_key, context, usize::from(t), @@ -313,16 +304,12 @@ where /// participate. pub fn verify( rng: &mut (impl RngCore + CryptoRng), - generators: &Generators, + generators: &EvrfGenerators, context: [u8; 32], t: u16, evrf_public_keys: &[::G], participations: &HashMap>, ) -> Result, EvrfError> { - if generators.g() != C::generator() { - todo!("TODO"); - } - let Ok(n) = u16::try_from(evrf_public_keys.len()) else { Err(EvrfError::TooManyParticipants)? }; if (t == 0) || (t > n) { Err(EvrfError::InvalidThreshold)?; @@ -336,13 +323,13 @@ where let mut valid = HashMap::with_capacity(participations.len()); let mut faulty = HashSet::new(); - let mut evrf_verifier = generators.batch_verifier(); + let mut evrf_verifier = generators.0.batch_verifier(); for (i, participation) in participations { // Clone the verifier so if this proof is faulty, it doesn't corrupt the verifier let mut verifier_clone = evrf_verifier.clone(); let Ok(data) = Evrf::::verify( rng, - generators, + &generators.0, &mut verifier_clone, evrf_public_keys[usize::from(u16::from(*i)) - 1], context, @@ -360,16 +347,16 @@ where debug_assert_eq!(valid.len() + faulty.len(), participations.len()); // Perform the batch verification of the eVRFs - if !generators.verify(evrf_verifier) { + if !generators.0.verify(evrf_verifier) { // If the batch failed, verify them each individually for (i, participation) in participations { if faulty.contains(i) { continue; } - let mut evrf_verifier = generators.batch_verifier(); + let mut evrf_verifier = generators.0.batch_verifier(); Evrf::::verify( rng, - generators, + &generators.0, &mut evrf_verifier, evrf_public_keys[usize::from(u16::from(*i)) - 1], context, @@ -378,7 +365,7 @@ where &participation.proof, ) .expect("evrf failed basic checks yet prover wasn't prior marked faulty"); - if !generators.verify(evrf_verifier) { + if !generators.0.verify(evrf_verifier) { valid.remove(i); faulty.insert(*i); } @@ -412,7 +399,7 @@ where // Also push this g_scalar onto these_pairs so these_pairs can be verified individually // upon error - these_pairs.push((this_g_scalar, generators.g())); + these_pairs.push((this_g_scalar, generators.0.g())); share_verification_statements_actual.insert(*i, these_pairs); // Also format this data as we'd need it upon success @@ -437,7 +424,7 @@ where } all_encrypted_secret_shares.insert(*i, formatted_encrypted_secret_shares); } - pairs.push((g_scalar, generators.g())); + pairs.push((g_scalar, generators.0.g())); bool::from(multiexp_vartime(&pairs).is_identity()) } { // If the batch failed, verify them each individually @@ -483,41 +470,47 @@ where })) } - // TODO: Return all keys for this participant, not just the first pub fn keys( &self, evrf_private_key: &Zeroizing<::F>, - ) -> Option> { + ) -> Vec> { let evrf_public_key = ::generator() * evrf_private_key.deref(); - let Some(i) = self.evrf_public_keys.iter().position(|key| *key == evrf_public_key) else { - None? - }; - let i = u16::try_from(i).expect("n <= u16::MAX yet i > u16::MAX?"); - let i = Participant(1 + i); - - let mut secret_share = Zeroizing::new(C::F::ZERO); - for shares in self.encrypted_secret_shares.values() { - let (ecdh_keys, enc_share) = shares[&i]; - - let mut ecdh = Zeroizing::new(C::F::ZERO); - for point in ecdh_keys { - // TODO: Explicitly ban 0-ECDH commitments, 0-eVRF public keys, and gen non-zero keys - let (mut x, mut y) = - ::G::to_xy(point * evrf_private_key.deref()).unwrap(); - *ecdh += x; - x.zeroize(); - y.zeroize(); + let mut is = Vec::with_capacity(1); + for (i, evrf_key) in self.evrf_public_keys.iter().enumerate() { + if *evrf_key == evrf_public_key { + let i = u16::try_from(i).expect("n <= u16::MAX yet i > u16::MAX?"); + let i = Participant(1 + i); + is.push(i); } - *secret_share += enc_share - ecdh.deref(); } - debug_assert_eq!(self.verification_shares[&i], C::generator() * secret_share.deref()); + let mut res = Vec::with_capacity(is.len()); + for i in is { + let mut secret_share = Zeroizing::new(C::F::ZERO); + for shares in self.encrypted_secret_shares.values() { + let (ecdh_keys, enc_share) = shares[&i]; - Some(ThresholdCore { - params: ThresholdParams::new(self.t, self.n, i).unwrap(), - secret_share, - group_key: self.group_key, - verification_shares: self.verification_shares.clone(), - }) + let mut ecdh = Zeroizing::new(C::F::ZERO); + for point in ecdh_keys { + // TODO: Explicitly ban 0-ECDH commitments, 0-eVRF public keys, and gen non-zero keys + let (mut x, mut y) = + ::G::to_xy(point * evrf_private_key.deref()).unwrap(); + *ecdh += x; + x.zeroize(); + y.zeroize(); + } + *secret_share += enc_share - ecdh.deref(); + } + + debug_assert_eq!(self.verification_shares[&i], C::generator() * secret_share.deref()); + + res.push(ThresholdKeys::from(ThresholdCore { + params: ThresholdParams::new(self.t, self.n, i).unwrap(), + secret_share, + group_key: self.group_key, + verification_shares: self.verification_shares.clone(), + })); + } + res } } diff --git a/crypto/dkg/src/evrf/proof.rs b/crypto/dkg/src/evrf/proof.rs index bc353baf..da5e4259 100644 --- a/crypto/dkg/src/evrf/proof.rs +++ b/crypto/dkg/src/evrf/proof.rs @@ -43,6 +43,32 @@ fn sample_point(rng: &mut (impl RngCore + CryptoRng)) -> C::G { } } +/// Generators for eVRF proof. +#[derive(Clone, Debug)] +pub struct EvrfGenerators(pub(crate) Generators); + +impl EvrfGenerators +where + <::EmbeddedCurve as Ciphersuite>::G: + DivisorCurve::F>, +{ + /// Create a new set of generators. + pub fn new(max_threshold: u16, max_participants: u16) -> EvrfGenerators { + let g = C::generator(); + let mut rng = ChaCha20Rng::from_seed(Blake2s256::digest(g.to_bytes()).into()); + let h = sample_point::(&mut rng); + let (_, generators) = + Evrf::::muls_and_generators_to_use(max_threshold.into(), max_participants.into()); + let mut g_bold = vec![]; + let mut h_bold = vec![]; + for _ in 0 .. generators { + g_bold.push(sample_point::(&mut rng)); + h_bold.push(sample_point::(&mut rng)); + } + Self(Generators::new(g, h, g_bold, h_bold).unwrap()) + } +} + /// The result of proving for an eVRF. pub(crate) struct EvrfProveResult { /// The coefficients for use in the DKG. @@ -76,22 +102,6 @@ where <::EmbeddedCurve as Ciphersuite>::G: DivisorCurve::F>, { - // TODO: Wrap these Generators so we can enforce g == C::generator() with type safety - pub(crate) fn generators(max_threshold: u16, max_participants: u16) -> Generators { - let g = C::generator(); - let mut rng = ChaCha20Rng::from_seed(Blake2s256::digest(g.to_bytes()).into()); - let h = sample_point::(&mut rng); - let (_, generators) = - Evrf::::muls_and_generators_to_use(max_threshold.into(), max_participants.into()); - let mut g_bold = vec![]; - let mut h_bold = vec![]; - for _ in 0 .. generators { - g_bold.push(sample_point::(&mut rng)); - h_bold.push(sample_point::(&mut rng)); - } - Generators::new(g, h, g_bold, h_bold).unwrap() - } - // Sample uniform points (via rejection-sampling) on the embedded elliptic curve fn transcript_to_points( seed: [u8; 32], @@ -290,72 +300,142 @@ where /// Convert a scalar to a sequence of coefficients for the polynomial 2**i, where the sum of the /// coefficients is F::NUM_BITS. /// - /// We'll presumably use this scalar in a discrete log proof. That requires calculating a divisor - /// which is variable time to the sum of the coefficients in the polynomial. This causes all - /// scalars to have a constant sum of their coefficients (instead one variable to the bits set). + /// Despite the name, the returned coefficients are not guaranteed to be bits (0 or 1). + /// + /// This scalar will presumably be used in a discrete log proof. That requires calculating a + /// divisor which is variable time to the amount of points interpolated. Since the amount of + /// points interpolated is equal to the sum of the coefficients in the polynomial, we need all + /// scalars to have a constant sum of their coefficients (instead of one variable to its bits). /// /// We achieve this by finding the highest non-0 coefficient, decrementing it, and increasing the /// immediately less significant coefficient by 2. This increases the sum of the coefficients by /// 1 (-1+2=1). - // TODO: Support scalars which have a value < F::NUM_BITS - #[allow(clippy::cast_possible_truncation)] fn scalar_to_bits(scalar: &::F) -> Vec { - let num_bits = <::EmbeddedCurve as Ciphersuite>::F::NUM_BITS; + let num_bits = u64::from(<::EmbeddedCurve as Ciphersuite>::F::NUM_BITS); // Obtain the bits of the private key - let mut sum_of_coefficients: u64 = 0; - let mut dlog = vec![0; num_bits as usize]; - for (i, bit) in scalar.to_le_bits().into_iter().take(num_bits as usize).enumerate() { + let num_bits_usize = usize::try_from(num_bits).unwrap(); + let mut decomposition = vec![0; num_bits_usize]; + for (i, bit) in scalar.to_le_bits().into_iter().take(num_bits_usize).enumerate() { let bit = u64::from(u8::from(bit)); - dlog[i] = bit; - sum_of_coefficients += bit; + decomposition[i] = bit; + } + + // The following algorithm only works if the value of the scalar exceeds num_bits + // If it isn't, we increase it by the modulus such that it does exceed num_bits + { + let mut less_than_num_bits = Choice::from(0); + for i in 0 .. num_bits { + less_than_num_bits |= scalar.ct_eq(&::F::from(i)); + } + let mut decomposition_of_modulus = vec![0; num_bits_usize]; + // Decompose negative one + for (i, bit) in (-::F::ONE) + .to_le_bits() + .into_iter() + .take(num_bits_usize) + .enumerate() + { + let bit = u64::from(u8::from(bit)); + decomposition_of_modulus[i] = bit; + } + // Increment it by one + decomposition_of_modulus[0] += 1; + + // Add the decomposition onto the decomposition of the modulus + for i in 0 .. num_bits_usize { + let new_decomposition = <_>::conditional_select( + &decomposition[i], + &(decomposition[i] + decomposition_of_modulus[i]), + less_than_num_bits, + ); + decomposition[i] = new_decomposition; + } + } + + // Calculcate the sum of the coefficients + let mut sum_of_coefficients: u64 = 0; + for decomposition in &decomposition { + sum_of_coefficients += *decomposition; + } + + /* + Now, because we added a log2(k)-bit number to a k-bit number, we may have our sum of + coefficients be *too high*. We attempt to reduce the sum of the coefficients accordingly. + + This algorithm is guaranteed to complete as expected. Take the sequence `222`. `222` becomes + `032` becomes `013`. Even if the next coefficient in the sequence is `2`, the third + coefficient will be reduced once and the next coefficient (`2`, increased to `3`) will only + be eligible for reduction once. This demonstrates, even for a worst case of log2(k) `2`s + followed by `1`s (as possible if the modulus is a Mersenne prime), the log2(k) `2`s can be + reduced as necessary so long as there is a single coefficient after (requiring the entire + sequence be at least of length log2(k) + 1). For a 2-bit number, log2(k) + 1 == 2, so this + holds for any odd prime field. + + To fully type out the demonstration for the Mersenne prime 3, with scalar to encode 1 (the + highest value less than the number of bits): + + 10 - Little-endian bits of 1 + 21 - Little-endian bits of 1, plus the modulus + 02 - After one reduction, where the sum of the coefficients does in fact equal 2 (the target) + */ + { + let mut log2_num_bits = 0; + while (1 << log2_num_bits) < num_bits { + log2_num_bits += 1; + } + + for _ in 0 .. log2_num_bits { + // If the sum of coefficients is the amount of bits, we're done + let mut done = sum_of_coefficients.ct_eq(&num_bits); + + for i in 0 .. (num_bits_usize - 1) { + let should_act = (!done) & decomposition[i].ct_gt(&1); + // Subtract 2 from this coefficient + let amount_to_sub = <_>::conditional_select(&0, &2, should_act); + decomposition[i] -= amount_to_sub; + // Add 1 to the next coefficient + let amount_to_add = <_>::conditional_select(&0, &1, should_act); + decomposition[i + 1] += amount_to_add; + + // Also update the sum of coefficients + sum_of_coefficients -= <_>::conditional_select(&0, &1, should_act); + + // If we updated the coefficients this loop iter, we're done for this loop iter + done |= should_act; + } + } } for _ in 0 .. num_bits { + // If the sum of coefficients is the amount of bits, we're done + let mut done = sum_of_coefficients.ct_eq(&num_bits); + // Find the highest coefficient currently non-zero - let mut h = 1u32; - // The value of this highest coefficient, and the coefficient prior to it - let mut h_value = dlog[h as usize]; - let mut h_prior_value = dlog[(h as usize) - 1]; + for i in (1 .. decomposition.len()).rev() { + // If this is non-zero, we should decrement this coefficient if we haven't already + // decremented a coefficient this round + let is_non_zero = !(0.ct_eq(&decomposition[i])); + let should_act = (!done) & is_non_zero; - // TODO: Squash the following two loops by iterating from the top bit to the bottom bit + // Update this coefficient and the prior coefficient + let amount_to_sub = <_>::conditional_select(&0, &1, should_act); + decomposition[i] -= amount_to_sub; - let mut prior_coefficient = dlog[(h as usize) - 1]; - for (i, coefficient) in dlog.iter().enumerate().skip(h as usize) { - let is_zero = 0.ct_eq(coefficient); + let amount_to_add = <_>::conditional_select(&0, &2, should_act); + // i must be at least 1, so i - 1 will be at least 0 (meaning it's safe to index with) + decomposition[i - 1] += amount_to_add; - // Set `h_*` if this value is non-0 - h = u32::conditional_select(&h, &(i as u32), !is_zero); - h_value = <_>::conditional_select(&h_value, coefficient, !is_zero); - h_prior_value = <_>::conditional_select(&h_prior_value, &prior_coefficient, !is_zero); + // Also update the sum of coefficients + sum_of_coefficients += <_>::conditional_select(&0, &1, should_act); - // Update prior_coefficient - prior_coefficient = *coefficient; - } - - // We should not have selected a value equivalent to 0 - // TODO: Ban evrf keys < NUM_BITS and accordingly unable to be so coerced - assert!(!bool::from(h_value.ct_eq(&0))); - - // Update h_value, h_prior_value as necessary - h_value -= 1; - h_prior_value += 2; - - // Now, set these values if we should - let should_set = !sum_of_coefficients.ct_eq(&u64::from(num_bits)); - sum_of_coefficients += u64::conditional_select(&0, &1, should_set); - for (i, coefficient) in dlog.iter_mut().enumerate() { - let this_is_prior = (i as u32).ct_eq(&(h - 1)); - let this_is_high = (i as u32).ct_eq(&h); - - *coefficient = - <_>::conditional_select(coefficient, &h_prior_value, should_set & this_is_prior); - *coefficient = <_>::conditional_select(coefficient, &h_value, should_set & this_is_high); + // If we updated the coefficients this loop iter, we're done for this loop iter + done |= should_act; } } - debug_assert!(bool::from(dlog.iter().sum::().ct_eq(&u64::from(num_bits)))); + debug_assert!(bool::from(decomposition.iter().sum::().ct_eq(&num_bits))); - dlog + decomposition } fn transcript( @@ -654,6 +734,7 @@ where // TODO: Dedicated error /// Verify an eVRF proof, returning the commitments output. + #[allow(clippy::too_many_arguments)] pub(crate) fn verify( rng: &mut (impl RngCore + CryptoRng), generators: &Generators, diff --git a/crypto/dkg/src/tests/evrf/mod.rs b/crypto/dkg/src/tests/evrf/mod.rs index 48407e8f..e6fd2230 100644 --- a/crypto/dkg/src/tests/evrf/mod.rs +++ b/crypto/dkg/src/tests/evrf/mod.rs @@ -7,7 +7,7 @@ use rand::seq::SliceRandom; use ciphersuite::{group::ff::Field, Ciphersuite}; use crate::{ - Participant, ThresholdKeys, + Participant, evrf::*, tests::{THRESHOLD, PARTICIPANTS, recover_key}, }; @@ -17,7 +17,7 @@ use proof::{Pallas, Vesta}; #[test] fn evrf_dkg() { - let generators = EvrfDkg::::generators(THRESHOLD, PARTICIPANTS); + let generators = EvrfGenerators::::new(THRESHOLD, PARTICIPANTS); let context = [0; 32]; let mut priv_keys = vec![]; @@ -62,7 +62,7 @@ fn evrf_dkg() { let mut verification_shares = None; let mut all_keys = HashMap::new(); for (i, priv_key) in priv_keys { - let keys = ThresholdKeys::from(dkg.keys(&priv_key).unwrap()); + let keys = dkg.keys(&priv_key).into_iter().next().unwrap(); assert_eq!(keys.params().i(), i); assert_eq!(keys.params().t(), THRESHOLD); assert_eq!(keys.params().n(), PARTICIPANTS); @@ -74,6 +74,6 @@ fn evrf_dkg() { all_keys.insert(i, keys); } - // TODO: Test fo all possible combinations of keys + // TODO: Test for all possible combinations of keys assert_eq!(Pallas::generator() * recover_key(&all_keys), group_key.unwrap()); }