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.
This commit is contained in:
Luke Parker
2024-07-28 01:14:44 -04:00
parent c960d6baaf
commit f93bd42b99
3 changed files with 196 additions and 122 deletions

View File

@@ -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
<<C as EvrfCurve>::EmbeddedCurve as Ciphersuite>::G:
DivisorCurve<FieldElement = <C as Ciphersuite>::F>,
{
/// Sample generators for this ciphersuite.
pub fn generators(max_threshold: u16, max_participants: u16) -> Generators<C> {
Evrf::<C>::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<C>,
generators: &EvrfGenerators<C>,
context: [u8; 32],
t: u16,
evrf_public_keys: &[<C::EmbeddedCurve as Ciphersuite>::G],
evrf_private_key: &Zeroizing<<C::EmbeddedCurve as Ciphersuite>::F>,
) -> Result<Participation<C>, EvrfError> {
if generators.g() != C::generator() {
todo!("TODO");
}
let evrf_public_key = <C::EmbeddedCurve as Ciphersuite>::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<C>,
generators: &EvrfGenerators<C>,
context: [u8; 32],
t: u16,
evrf_public_keys: &[<C::EmbeddedCurve as Ciphersuite>::G],
participations: &HashMap<Participant, Participation<C>>,
) -> Result<VerifyResult<C>, 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::<C>::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::<C>::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<<C::EmbeddedCurve as Ciphersuite>::F>,
) -> Option<ThresholdCore<C>> {
) -> Vec<ThresholdKeys<C>> {
let evrf_public_key = <C::EmbeddedCurve as Ciphersuite>::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) =
<C::EmbeddedCurve as Ciphersuite>::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) =
<C::EmbeddedCurve as Ciphersuite>::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
}
}

View File

@@ -43,6 +43,32 @@ fn sample_point<C: Ciphersuite>(rng: &mut (impl RngCore + CryptoRng)) -> C::G {
}
}
/// Generators for eVRF proof.
#[derive(Clone, Debug)]
pub struct EvrfGenerators<C: EvrfCurve>(pub(crate) Generators<C>);
impl<C: EvrfCurve> EvrfGenerators<C>
where
<<C as EvrfCurve>::EmbeddedCurve as Ciphersuite>::G:
DivisorCurve<FieldElement = <C as Ciphersuite>::F>,
{
/// Create a new set of generators.
pub fn new(max_threshold: u16, max_participants: u16) -> EvrfGenerators<C> {
let g = C::generator();
let mut rng = ChaCha20Rng::from_seed(Blake2s256::digest(g.to_bytes()).into());
let h = sample_point::<C>(&mut rng);
let (_, generators) =
Evrf::<C>::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::<C>(&mut rng));
h_bold.push(sample_point::<C>(&mut rng));
}
Self(Generators::new(g, h, g_bold, h_bold).unwrap())
}
}
/// The result of proving for an eVRF.
pub(crate) struct EvrfProveResult<C: Ciphersuite> {
/// The coefficients for use in the DKG.
@@ -76,22 +102,6 @@ where
<<C as EvrfCurve>::EmbeddedCurve as Ciphersuite>::G:
DivisorCurve<FieldElement = <C as Ciphersuite>::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<C> {
let g = C::generator();
let mut rng = ChaCha20Rng::from_seed(Blake2s256::digest(g.to_bytes()).into());
let h = sample_point::<C>(&mut rng);
let (_, generators) =
Evrf::<C>::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::<C>(&mut rng));
h_bold.push(sample_point::<C>(&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: &<C::EmbeddedCurve as Ciphersuite>::F) -> Vec<u64> {
let num_bits = <<C as EvrfCurve>::EmbeddedCurve as Ciphersuite>::F::NUM_BITS;
let num_bits = u64::from(<<C as EvrfCurve>::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(&<C::EmbeddedCurve as Ciphersuite>::F::from(i));
}
let mut decomposition_of_modulus = vec![0; num_bits_usize];
// Decompose negative one
for (i, bit) in (-<C::EmbeddedCurve as Ciphersuite>::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::<u64>().ct_eq(&u64::from(num_bits))));
debug_assert!(bool::from(decomposition.iter().sum::<u64>().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<C>,

View File

@@ -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::<Pallas>::generators(THRESHOLD, PARTICIPANTS);
let generators = EvrfGenerators::<Pallas>::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());
}