Files
nym/common/crypto/dkg/src/dealing.rs
Jędrzej Stuczyński 37de4bf2f7 Crypto part of the Groth's NIDKG (#1182)
* Work in progress NIDKG

* Encryption of multiple shares

* Extracted baby-step giant-step lookup table as a separate entity

* Proof of discrete log

* Adjusted discrete log domainn

* Producing proof of log during keygen

* Zeroize for epoch

* Proof of secret sharing

* empty main for compiler appeasement

* Construction of proof of chunking

* Initial untested verification of proof of chunking

* Converted chunk responses from Scalar to u64

* Additional tests for proof of chunking

* Minor cleanup and reorganisation

* Fixed enc/dec to use f0

* Deriving node coverage of required tree nodes

* Finally seemingnly working encryption under nonzero epoch

* Branch park

* Decryption key updates to specified epochs

* Ciphertext integrity checks

* Progress in integration tests

* Fixed ciphertext combining and integration test

* Dealing type and simplification of the integration test

* Benchmark for creation of baby-step-giant-step lookup table

* Initial import cleanup + broken 2nd integration test

* Using correct assertions in the integration test (and correctly combining shares)

* Removed unused modules

* Changed proof of sharing to allow for node indices being different from [1,2,...n]

* Reorganised bte module

* Benchmark for g2 precomputation

* Created more strongly typed Epoch type

which is essentially a Tau such that it is a leaf node

* Extending tau with a temporary oracle output

* Using random oracle for tau extension

* More benchmarks!

* encryption-related benchmarks

* Serialization of PublicKeyWithProof

* Typos

* Removed any changes made in validator-api or smart contracts

* Made the integration test slightly more concise

* Further purge of unused modules

* Fixed combining share to use lagrangian interpolation

* Recovery of verification keys from the dealings

* Verification key verification + extended integration tests

* Fixed Tau not being included in digest for producing Tau_h

* Tau serialization

* Serialization of a BTE Node

* Serialization of DecryptionKey

* Serialization of PublicCoefficients

* Utility method for setting constant coefficient of a polynomial

* Serialization of Ciphertexts

* Serialization of Proof of Secret Sharing

* Serialization of Proof of Chunking

* Serialization of Dealing

* Adjusted capacity of responses_r in proof of chunking

* Made notation more consistent with the paper equivalents

* Optional arguments for creating/verifying resharing dealings
2022-04-12 11:59:26 +01:00

501 lines
17 KiB
Rust

// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::proof_chunking::ProofOfChunking;
use crate::bte::proof_sharing::ProofOfSecretSharing;
use crate::bte::{
encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Epoch, Params, PublicKey,
};
use crate::error::DkgError;
use crate::interpolation::polynomial::{Polynomial, PublicCoefficients};
use crate::interpolation::{
perform_lagrangian_interpolation_at_origin, perform_lagrangian_interpolation_at_x,
};
use crate::{NodeIndex, Share, Threshold};
use bls12_381::{G2Projective, Scalar};
use rand_core::RngCore;
use std::collections::BTreeMap;
use zeroize::Zeroize;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Dealing {
pub public_coefficients: PublicCoefficients,
pub ciphertexts: Ciphertexts,
pub proof_of_chunking: ProofOfChunking,
pub proof_of_sharing: ProofOfSecretSharing,
}
impl Dealing {
// I'm not a big fan of this function signature, but I'm not clear on how to improve it while
// allowing the dealer to skip decryption of its own share if it was also one of the receivers
pub fn create(
mut rng: impl RngCore,
params: &Params,
dealer_index: NodeIndex,
threshold: Threshold,
epoch: Epoch,
// BTreeMap ensures the keys are sorted by their indices
receivers: &BTreeMap<NodeIndex, PublicKey>,
prior_resharing_secret: Option<Scalar>,
) -> (Self, Option<Share>) {
assert!(threshold > 0);
let mut polynomial = Polynomial::new_random(&mut rng, threshold - 1);
if let Some(prior_secret) = prior_resharing_secret {
polynomial.set_constant_coefficient(prior_secret)
}
let mut shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) =
encrypt_shares(&remote_share_key_pairs, epoch, params, &mut rng);
// create proofs of knowledge
let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
let proof_of_chunking =
ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares)
.expect("failed to construct proof of chunking");
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
let mut combined_r = hazmat.combine_rs();
let combined_rr = ciphertexts.combine_rs();
let public_coefficients = polynomial.public_coefficients();
let sharing_instance = proof_sharing::Instance::new(
receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
let proof_of_sharing =
ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares)
.expect("failed to construct proof of secret sharing");
combined_r.zeroize();
let dealing = Dealing {
public_coefficients,
ciphertexts,
proof_of_chunking,
proof_of_sharing,
};
let dealers_key_index = receivers
.keys()
.position(|node_index| node_index == &dealer_index);
if let Some(dealer_key_index) = dealers_key_index {
let dealers_share = shares.remove(dealer_key_index);
shares.zeroize();
(dealing, Some(dealers_share))
} else {
(dealing, None)
}
}
// rather than returning a bool for whether the dealing is valid or not, a Result is returned
// instead so that we would have more information regarding a possible failure cause
pub fn verify(
&self,
params: &Params,
epoch: Epoch,
threshold: Threshold,
receivers: &BTreeMap<NodeIndex, PublicKey>,
prior_resharing_public: Option<G2Projective>,
) -> Result<(), DkgError> {
if threshold == 0 || threshold as usize > receivers.len() {
return Err(DkgError::InvalidThreshold {
actual: threshold as usize,
participating: receivers.len(),
});
}
if self.ciphertexts.ciphertext_chunks.len() != receivers.len() {
return Err(DkgError::WrongCiphertextSize {
actual: self.ciphertexts.ciphertext_chunks.len(),
expected: receivers.len(),
});
}
if self.public_coefficients.size() != threshold as usize {
return Err(DkgError::WrongPublicCoefficientsSize {
actual: self.public_coefficients.size(),
expected: threshold as usize,
});
}
if !self.ciphertexts.verify_integrity(params, epoch) {
return Err(DkgError::FailedCiphertextIntegrityCheck);
}
// TODO: perhaps change the underlying arguments in proofs of knowledge to avoid this allocation?
let sorted_receivers = receivers.values().copied().collect::<Vec<_>>();
let chunking_instance = proof_chunking::Instance::new(&sorted_receivers, &self.ciphertexts);
if !self.proof_of_chunking.verify(chunking_instance) {
return Err(DkgError::InvalidProofOfChunking);
}
let combined_randomizer = &self.ciphertexts.combine_rs();
let combined_ciphertexts = &self.ciphertexts.combine_ciphertexts();
let sharing_instance = proof_sharing::Instance::new(
receivers,
&self.public_coefficients,
combined_randomizer,
combined_ciphertexts,
);
if !self.proof_of_sharing.verify(sharing_instance) {
return Err(DkgError::InvalidProofOfSharing);
}
if let Some(prior_public) = prior_resharing_public {
let dealt_public = &self.public_coefficients[0];
if dealt_public != &prior_public {
return Err(DkgError::InvalidResharing);
}
}
Ok(())
}
// coeff_len || coeff || cc_len || cc || pi_c_len || pi_c || pi_s_len || pi_s
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let mut coefficients_bytes = self.public_coefficients.to_bytes();
bytes.extend_from_slice(&(coefficients_bytes.len() as u32).to_be_bytes());
bytes.append(&mut coefficients_bytes);
let mut ciphertexts_bytes = self.ciphertexts.to_bytes();
bytes.extend_from_slice(&(ciphertexts_bytes.len() as u32).to_be_bytes());
bytes.append(&mut ciphertexts_bytes);
let mut proof_sharing_bytes = self.proof_of_sharing.to_bytes();
bytes.extend_from_slice(&(proof_sharing_bytes.len() as u32).to_be_bytes());
bytes.append(&mut proof_sharing_bytes);
let mut proof_chunking_bytes = self.proof_of_chunking.to_bytes();
bytes.extend_from_slice(&(proof_chunking_bytes.len() as u32).to_be_bytes());
bytes.append(&mut proof_chunking_bytes);
bytes
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
// can we read the length of serialized public coefficients?
if bytes.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"Dealing",
"insufficient number of bytes provided",
));
}
let mut i = 0;
let coefficients_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let public_coefficients =
PublicCoefficients::try_from_bytes(&bytes[i..i + coefficients_bytes_len])?;
i += coefficients_bytes_len;
let ciphertexts_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let ciphertexts = Ciphertexts::try_from_bytes(&bytes[i..i + ciphertexts_bytes_len])?;
i += ciphertexts_bytes_len;
let proof_of_sharing_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let proof_of_sharing =
ProofOfSecretSharing::try_from_bytes(&bytes[i..i + proof_of_sharing_bytes_len])?;
i += proof_of_sharing_bytes_len;
let proof_of_chunking_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() != proof_of_chunking_bytes_len {
return Err(DkgError::new_deserialization_failure(
"Dealing",
"invalid number of bytes provided",
));
}
let proof_of_chunking = ProofOfChunking::try_from_bytes(&bytes[i..])?;
Ok(Dealing {
public_coefficients,
ciphertexts,
proof_of_chunking,
proof_of_sharing,
})
}
}
// this assumes all dealings have been verified
pub fn try_recover_verification_keys(
dealings: &[Dealing],
threshold: Threshold,
receivers: &BTreeMap<NodeIndex, PublicKey>,
) -> Result<(G2Projective, Vec<G2Projective>), DkgError> {
if dealings.is_empty() {
return Err(DkgError::NoDealingsAvailable);
}
let threshold_usize = threshold as usize;
if !dealings
.iter()
.all(|dealing| dealing.public_coefficients.size() == threshold_usize)
{
return Err(DkgError::MismatchedDealings);
}
// currently we expect every dealer to also be a receiver. This restriction might be relaxed in the future
if dealings.len() != receivers.len() {
return Err(DkgError::MismatchedDealings);
}
let indices = receivers.keys().collect::<Vec<_>>();
// Compute A0, ..., A_{t-1}
let mut interpolated_coefficients = Vec::with_capacity(threshold_usize);
for k in 0..threshold_usize {
let mut samples = Vec::with_capacity(indices.len());
for (j, dealing) in dealings.iter().enumerate() {
samples.push((
Scalar::from(*indices[j]),
*dealing.public_coefficients.nth(k),
))
}
let interpolated = perform_lagrangian_interpolation_at_origin(&samples)?;
interpolated_coefficients.push(interpolated);
}
let master_verification_key = interpolated_coefficients[0];
let interpolated_coefficients = PublicCoefficients {
coefficients: interpolated_coefficients,
};
// shvk_j = A0^{j^0} * A1^{j^1} * ... * A_{t-1}^{j^{t-1}}
let verification_key_shares = receivers
.keys()
.map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index)))
.collect();
Ok((master_verification_key, verification_key_shares))
}
pub fn verify_verification_keys(
master_key: &G2Projective,
shares: &[G2Projective],
receivers: &BTreeMap<NodeIndex, PublicKey>,
threshold: Threshold,
) -> Result<(), DkgError> {
if shares.len() != receivers.len() {
return Err(DkgError::NotEnoughReceiversProvided);
}
if threshold as usize > receivers.len() {
return Err(DkgError::InvalidThreshold {
actual: threshold as usize,
participating: receivers.len(),
});
}
let indices = receivers.keys().copied().collect::<Vec<_>>();
let indices_with_origin = std::iter::once(&0)
.chain(receivers.keys())
.collect::<Vec<_>>();
let all_shares = std::iter::once(master_key)
.chain(shares.iter())
.collect::<Vec<_>>();
for (i, share) in shares.iter().enumerate() {
let samples = indices_with_origin
.iter()
.zip(all_shares.iter())
.map(|(&node_index, &share)| (Scalar::from(*node_index), *share))
.take(threshold as usize)
.collect::<Vec<_>>();
let interpolated =
perform_lagrangian_interpolation_at_x(&Scalar::from(indices[i]), &samples)?;
if share != &interpolated {
return Err(DkgError::MismatchedVerificationKey);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::{decrypt_share, keygen, setup};
use crate::combine_shares;
use rand_core::SeedableRng;
#[test]
fn recovering_partial_verification_keys() {
// START OF SETUP
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let node_indices = vec![1, 4, 7];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let shares = dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap())
.collect();
derived_secrets.push(
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
)
}
let master_secret = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[2]), derived_secrets[2]),
(Scalar::from(node_indices[1]), derived_secrets[1]),
])
.unwrap();
// END OF SETUP
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
let g2 = G2Projective::generator();
assert_eq!(g2 * master_secret, recovered_master);
assert_eq!(g2 * derived_secrets[0], recovered_partials[0]);
assert_eq!(g2 * derived_secrets[1], recovered_partials[1]);
assert_eq!(g2 * derived_secrets[2], recovered_partials[2]);
}
#[test]
fn verifying_partial_verification_keys() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let node_indices = vec![1, 4, 7];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
assert!(verify_verification_keys(
&recovered_master,
&recovered_partials,
&receivers,
threshold
)
.is_ok())
}
#[test]
fn dealing_roundtrip() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let parties = 5;
let threshold = ((parties as f32 * 2.) / 3. + 1.) as Threshold;
let node_indices = (1..=parties).collect::<Vec<_>>();
let epoch = Epoch::new(2);
let mut receivers = BTreeMap::new();
for index in &node_indices {
let (_, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
}
let (dealing, _) = Dealing::create(
&mut rng,
&params,
node_indices[0],
threshold,
epoch,
&receivers,
None,
);
let bytes = dealing.to_bytes();
let recovered = Dealing::try_from_bytes(&bytes).unwrap();
assert_eq!(dealing, recovered);
}
}