diff --git a/fastcrypto-tbls/benches/dkg.rs b/fastcrypto-tbls/benches/dkg.rs index fda6731bff..60ba9eadaf 100644 --- a/fastcrypto-tbls/benches/dkg.rs +++ b/fastcrypto-tbls/benches/dkg.rs @@ -3,8 +3,8 @@ use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; use fastcrypto::groups::{bls12381, ristretto255}; -use fastcrypto_tbls::dkg_v0::Party; -use fastcrypto_tbls::ecies_v0; +use fastcrypto_tbls::dkg::Party; +use fastcrypto_tbls::ecies; use fastcrypto_tbls::nodes::{Node, Nodes, PartyId}; use fastcrypto_tbls::random_oracle::RandomOracle; use itertools::iproduct; @@ -13,11 +13,11 @@ use rand::thread_rng; type G = bls12381::G2Element; type EG = ristretto255::RistrettoPoint; -fn gen_ecies_keys(n: u16) -> Vec<(PartyId, ecies_v0::PrivateKey, ecies_v0::PublicKey)> { +fn gen_ecies_keys(n: u16) -> Vec<(PartyId, ecies::PrivateKey, ecies::PublicKey)> { (0..n) .map(|id| { - let sk = ecies_v0::PrivateKey::::new(&mut thread_rng()); - let pk = ecies_v0::PublicKey::::from_private_key(&sk); + let sk = ecies::PrivateKey::::new(&mut thread_rng()); + let pk = ecies::PublicKey::::from_private_key(&sk); (id, sk, pk) }) .collect() @@ -27,7 +27,7 @@ pub fn setup_party( id: PartyId, threshold: u16, weight: u16, - keys: &[(PartyId, ecies_v0::PrivateKey, ecies_v0::PublicKey)], + keys: &[(PartyId, ecies::PrivateKey, ecies::PublicKey)], ) -> Party { let nodes = keys .iter() diff --git a/fastcrypto-tbls/src/dkg.rs b/fastcrypto-tbls/src/dkg.rs new file mode 100644 index 0000000000..6564f693bc --- /dev/null +++ b/fastcrypto-tbls/src/dkg.rs @@ -0,0 +1,68 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Some of the code below is based on code from https://github.com/celo-org/celo-threshold-bls-rs, +// modified for our needs. +// + +use crate::ecies; +use crate::ecies::RecoveryPackage; +use crate::nodes::{Nodes, PartyId}; +use crate::polynomial::{Poly, PrivatePoly}; +use crate::random_oracle::RandomOracle; +use crate::tbls::Share; +use fastcrypto::groups::GroupElement; +use serde::{Deserialize, Serialize}; + +/// Generics below use `G: GroupElement' for the group of the VSS public key, and `EG: GroupElement' +/// for the group of the ECIES public key. + +/// Party in the DKG protocol. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Party { + pub id: PartyId, + pub(crate) nodes: Nodes, + pub t: u16, + pub random_oracle: RandomOracle, + pub(crate) enc_sk: ecies::PrivateKey, + pub(crate) vss_sk: PrivatePoly, +} + +/// Assumptions: +/// - The high-level protocol is responsible for verifying that the 'sender' is correct in the +/// following messages (based on the chain's authentication). +/// - The high-level protocol is responsible that all parties see the same order of messages. + +/// A complaint/fraud claim against a dealer that created invalid encrypted share. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Complaint { + pub(crate) accused_sender: PartyId, + pub(crate) proof: RecoveryPackage, +} + +/// A [Confirmation] is sent during the second phase of the protocol. It includes complaints +/// created by receiver of invalid encrypted shares (if any). +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Confirmation { + pub sender: PartyId, + /// List of complaints against other parties. Empty if there are none. + pub complaints: Vec>, +} + +// Upper bound on the size of binary serialized incoming messages assuming <=3333 shares, <=400 +// parties, and using G2Element for encryption. This is a safe upper bound since: +// - Message is O(96*t + 32*n) bytes. +// - Confirmation is O((96*3 + 32) * k) bytes. +// Could be used as a sanity safety check before deserializing an incoming message. +pub const DKG_MESSAGES_MAX_SIZE: usize = 400_000; // 400 KB + +/// [Output] is the final output of the DKG protocol in case it runs +/// successfully. It can be used later with [ThresholdBls], see examples in tests. +/// +/// If shares is None, the object can only be used for verifying (partial and full) signatures. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Output { + pub nodes: Nodes, + pub vss_pk: Poly, + pub shares: Option>>, // None if some shares are missing or weight is zero. +} diff --git a/fastcrypto-tbls/src/dkg_v0.rs b/fastcrypto-tbls/src/dkg_v0.rs index 78df67a0d8..48450c6c75 100644 --- a/fastcrypto-tbls/src/dkg_v0.rs +++ b/fastcrypto-tbls/src/dkg_v0.rs @@ -6,13 +6,12 @@ // use crate::dl_verification::verify_poly_evals; -use crate::ecies_v0; -use crate::ecies_v0::{MultiRecipientEncryption, RecoveryPackage}; use crate::nodes::{Nodes, PartyId}; -use crate::polynomial::{Eval, Poly, PrivatePoly, PublicPoly}; +use crate::polynomial::{Eval, PrivatePoly, PublicPoly}; use crate::random_oracle::RandomOracle; use crate::tbls::Share; use crate::types::ShareIndex; +use crate::{ecies, ecies_v0}; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; use fastcrypto::groups::{FiatShamirChallenge, GroupElement, MultiScalarMul}; use fastcrypto::traits::AllowedRng; @@ -20,28 +19,12 @@ use itertools::Itertools; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; +use crate::dkg::{Complaint, Confirmation, Output, Party}; +use crate::ecies::RecoveryPackage; +use crate::ecies_v0::MultiRecipientEncryption; use tap::prelude::*; use tracing::{debug, error, info, warn}; -/// Generics below use `G: GroupElement' for the group of the VSS public key, and `EG: GroupElement' -/// for the group of the ECIES public key. - -/// Party in the DKG protocol. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Party { - pub id: PartyId, - nodes: Nodes, - t: u16, - random_oracle: RandomOracle, - enc_sk: ecies_v0::PrivateKey, - vss_sk: PrivatePoly, -} - -/// Assumptions: -/// - The high-level protocol is responsible for verifying that the 'sender' is correct in the -/// following messages (based on the chain's authentication). -/// - The high-level protocol is responsible that all parties see the same order of messages. - /// [Message] holds all encrypted shares a dealer sends during the first phase of the /// protocol. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -53,29 +36,6 @@ pub struct Message { pub encrypted_shares: MultiRecipientEncryption, } -/// A complaint/fraud claim against a dealer that created invalid encrypted share. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Complaint { - accused_sender: PartyId, - proof: RecoveryPackage, -} - -/// A [Confirmation] is sent during the second phase of the protocol. It includes complaints -/// created by receiver of invalid encrypted shares (if any). -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Confirmation { - pub sender: PartyId, - /// List of complaints against other parties. Empty if there are none. - pub complaints: Vec>, -} - -// Upper bound on the size of binary serialized incoming messages assuming <=3333 shares, <=400 -// parties, and using G2Element for encryption. This is a safe upper bound since: -// - Message is O(96*t + 32*n) bytes. -// - Confirmation is O((96*3 + 32) * k) bytes. -// Could be used as a sanity safety check before deserializing an incoming message. -pub const DKG_MESSAGES_MAX_SIZE: usize = 400_000; // 400 KB - /// Wrapper for collecting everything related to a processed message. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ProcessedMessage { @@ -135,17 +95,6 @@ impl VerifiedProcessedMessages { } } -/// [Output] is the final output of the DKG protocol in case it runs -/// successfully. It can be used later with [ThresholdBls], see examples in tests. -/// -/// If shares is None, the object can only be used for verifying (partial and full) signatures. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Output { - pub nodes: Nodes, - pub vss_pk: Poly, - pub shares: Option>>, // None if some shares are missing or weight is zero. -} - /// A dealer in the DKG ceremony. /// /// Can be instantiated with G1Curve or G2Curve. @@ -160,14 +109,14 @@ where /// 3. Create a new Party instance with the ECIES private key and the set of nodes. pub fn new( - enc_sk: ecies_v0::PrivateKey, + enc_sk: ecies::PrivateKey, nodes: Nodes, t: u16, // The number of parties that are needed to reconstruct the full key/signature (f+1). random_oracle: RandomOracle, // Should be unique for each invocation, but the same for all parties. rng: &mut R, ) -> FastCryptoResult { // Confirm that my ecies pk is in the nodes. - let enc_pk = ecies_v0::PublicKey::::from_private_key(&enc_sk); + let enc_pk = ecies::PublicKey::::from_private_key(&enc_sk); let my_node = nodes .iter() .find(|n| n.pk == enc_pk) @@ -712,7 +661,7 @@ where } fn decrypt_and_get_share( - sk: &ecies_v0::PrivateKey, + sk: &ecies::PrivateKey, encrypted_shares: &ecies_v0::Encryption, ) -> FastCryptoResult> { let buffer = sk.decrypt(encrypted_shares); @@ -722,7 +671,7 @@ where // Returns an error if the *complaint* is invalid (counterintuitive). fn check_complaint_proof( recovery_pkg: &RecoveryPackage, - ecies_pk: &ecies_v0::PublicKey, + ecies_pk: &ecies::PublicKey, share_ids: &[ShareIndex], vss_pk: &PublicPoly, encrypted_share: &ecies_v0::Encryption, @@ -772,8 +721,8 @@ where EG: GroupElement + Serialize + DeserializeOwned, ::ScalarType: FiatShamirChallenge, { - let sk = ecies_v0::PrivateKey::::new(&mut rand::thread_rng()); - let pk = ecies_v0::PublicKey::::from_private_key(&sk); + let sk = ecies::PrivateKey::::new(&mut rand::thread_rng()); + let pk = ecies::PublicKey::::from_private_key(&sk); let encryption = pk.encrypt(b"test", &mut rand::thread_rng()); let ro = RandomOracle::new("test"); let pkg = sk.create_recovery_package(&encryption, &ro, &mut rand::thread_rng()); diff --git a/fastcrypto-tbls/src/dkg_v1.rs b/fastcrypto-tbls/src/dkg_v1.rs new file mode 100644 index 0000000000..03671e8a6b --- /dev/null +++ b/fastcrypto-tbls/src/dkg_v1.rs @@ -0,0 +1,677 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Some of the code below is based on code from https://github.com/celo-org/celo-threshold-bls-rs, +// modified for our needs. +// + +use crate::dl_verification::verify_poly_evals; +use crate::nodes::PartyId; +use crate::polynomial::{Eval, PublicPoly}; +use crate::random_oracle::RandomOracle; +use crate::tbls::Share; +use crate::types::ShareIndex; +use fastcrypto::error::{FastCryptoError, FastCryptoResult}; +use fastcrypto::groups::{FiatShamirChallenge, GroupElement, HashToGroupElement, MultiScalarMul}; +use fastcrypto::traits::AllowedRng; +use itertools::Itertools; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; + +use crate::dkg::{Complaint, Confirmation, Output, Party}; +use crate::{ecies, ecies_v1}; + +use tap::prelude::*; +use tracing::{debug, error, info, warn}; + +// TODO: Move Party, Complaint, Confirmation, Output here and remove old APIs + +/// [Message] holds all encrypted shares a dealer sends during the first phase of the +/// protocol. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Message { + pub sender: PartyId, + /// The commitment of the secret polynomial created by the sender. + pub vss_pk: PublicPoly, + /// The encrypted shares created by the sender. Sorted according to the receivers. + pub encrypted_shares: ecies_v1::MultiRecipientEncryption, +} + +/// Wrapper for collecting everything related to a processed message. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProcessedMessage { + pub message: Message, + /// Possibly empty + pub shares: Vec>, + pub complaint: Option>, +} + +/// Unique processed messages that are being used in the protocol. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UsedProcessedMessages( + pub Vec>, +); + +impl From<&[ProcessedMessage]> + for UsedProcessedMessages +{ + // Assumes all parties see the same order of messages. + fn from(msgs: &[ProcessedMessage]) -> Self { + let filtered = msgs + .iter() + .unique_by(|&m| m.message.sender) // stable + .cloned() + .collect::>(); + Self(filtered) + } +} + +/// Processed messages that were not excluded after the third phase of the protocol. +pub struct VerifiedProcessedMessages( + Vec>, +); + +impl VerifiedProcessedMessages { + fn filter_from(msgs: &UsedProcessedMessages, to_exclude: &[PartyId]) -> Self { + let filtered = msgs + .0 + .iter() + .filter(|m| !to_exclude.contains(&m.message.sender)) + .cloned() + .collect::>(); + Self(filtered) + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + #[cfg(test)] + pub fn data(&self) -> &[ProcessedMessage] { + &self.0 + } +} + +/// A dealer in the DKG ceremony. +/// +/// Can be instantiated with G1Curve or G2Curve. +impl Party +where + G: GroupElement + MultiScalarMul + Serialize + DeserializeOwned, + EG: GroupElement + Serialize + HashToGroupElement + DeserializeOwned, + EG::ScalarType: FiatShamirChallenge, +{ + /// 1. Create a new ECIES private key and send the public key to all parties. + /// 2. After *all* parties have sent their ECIES public keys, create the (same) set of nodes. + + /// 3. Create a new Party instance with the ECIES private key and the set of nodes. + // TODO: Move new() and t() here + + /// 4. Create the first message to be broadcasted. + /// + /// Returns IgnoredMessage if the party has zero weight (so no need to create a message). + pub fn create_message_v1( + &self, + rng: &mut R, + ) -> FastCryptoResult> { + let node = self.nodes.node_id_to_node(self.id).expect("my id is valid"); + if node.weight == 0 { + return Err(FastCryptoError::IgnoredMessage); + } + + let vss_pk = self.vss_sk.commit(); + let encryption_ro = self.encryption_random_oracle(self.id); + info!( + "DKG: Creating message for party {} with vss pk c0 {:?}, ro {:?}", + self.id, + vss_pk.c0(), + encryption_ro, + ); + // Create a vector of a public key and shares per receiver. + let pk_and_shares = self + .nodes + .iter() + .map(|node| { + let share_ids = self + .nodes + .share_ids_of(node.id) + .expect("iterating on valid nodes"); + let shares = share_ids + .iter() + .map(|share_id| self.vss_sk.eval(*share_id).value) + .collect::>(); + // Works even with empty shares_ids (will result in [0]). + let buff = bcs::to_bytes(&shares).expect("serialize of shares should never fail"); + (node.pk.clone(), buff) + }) + .collect::>(); + // Encrypt everything. + let encrypted_shares = + ecies_v1::MultiRecipientEncryption::encrypt(&pk_and_shares, &encryption_ro, rng); + + debug!( + "DKG: Created message using {:?}, with eph key {:?} nizk {:?}", + encryption_ro, + encrypted_shares.ephemeral_key(), + encrypted_shares.proof(), + ); + + Ok(Message { + sender: self.id, + vss_pk, + encrypted_shares, + }) + } + + // Sanity checks that can be done by any party on a received message. + fn sanity_check_message_v1(&self, msg: &Message) -> FastCryptoResult<()> { + let node = self + .nodes + .node_id_to_node(msg.sender) + .tap_err(|_| { + warn!( + "DKG: Message sanity check failed, invalid id {}", + msg.sender + ) + }) + .map_err(|_| FastCryptoError::InvalidMessage)?; + if node.weight == 0 { + warn!( + "DKG: Message sanity check failed for id {}, zero weight", + msg.sender + ); + return Err(FastCryptoError::InvalidMessage); + }; + + if self.t as usize != msg.vss_pk.degree() + 1 { + warn!( + "DKG: Message sanity check failed for id {}, expected degree={}, got {}", + msg.sender, + self.t - 1, + msg.vss_pk.degree() + ); + return Err(FastCryptoError::InvalidMessage); + } + + if *msg.vss_pk.c0() == G::zero() { + warn!( + "DKG: Message sanity check failed for id {}, zero c0", + msg.sender, + ); + return Err(FastCryptoError::InvalidMessage); + } + + if self.nodes.num_nodes() != msg.encrypted_shares.len() { + warn!( + "DKG: Message sanity check failed for id {}, expected encrypted_shares.len={}, got {}", + msg.sender, + self.nodes.num_nodes(), + msg.encrypted_shares.len() + ); + return Err(FastCryptoError::InvalidMessage); + } + + let encryption_ro = self.encryption_random_oracle(msg.sender); + msg.encrypted_shares + .verify(&encryption_ro) + .tap_err(|e| { + warn!("DKG: Message sanity check failed for id {}, verify with RO {:?}, eph key {:?} and proof {:?}, returned err: {:?}", + msg.sender, + encryption_ro, + msg.encrypted_shares.ephemeral_key(), + msg.encrypted_shares.proof(), + e) + }) + .map_err(|_| FastCryptoError::InvalidMessage) + } + + /// 5. Process a message and create the second message to be broadcasted. + /// The second message contains the list of complaints on invalid shares. In addition, it + /// returns a set of valid shares (so far). + /// + /// We split this function into two parts: process_message and merge, so that the caller can + /// process messages concurrently and then merge the results. + + /// [process_message] processes a message and returns an intermediate object ProcessedMessage. + /// + /// Returns error InvalidMessage if the message is invalid and should be ignored (note that we + /// could count it as part of the f+1 messages we wait for, but it's also safe to ignore it + /// and just wait for f+1 valid messages). + /// + /// Assumptions: Called only once per sender (the high level protocol is responsible for deduplication). + pub fn process_message_v1( + &self, + message: Message, + rng: &mut R, + ) -> FastCryptoResult> { + debug!( + "DKG: Processing message from party {} with vss pk c0 {:?}", + message.sender, + message.vss_pk.c0() + ); + // Ignore if invalid (and other honest parties will ignore as well). + self.sanity_check_message_v1(&message)?; + + let my_share_ids = self.nodes.share_ids_of(self.id).expect("my id is valid"); + let encryption_ro = self.encryption_random_oracle(message.sender); + let buffer = + message + .encrypted_shares + .decrypt(&self.enc_sk, &encryption_ro, self.id as usize); + let decrypted_shares: Option> = bcs::from_bytes(buffer.as_slice()) + .map_err(|_| FastCryptoError::InvalidInput) + .ok(); + + if decrypted_shares.is_none() + || decrypted_shares.as_ref().unwrap().len() != my_share_ids.len() + { + warn!( + "DKG: Processing message from party {} failed, invalid number of decrypted shares", + message.sender + ); + let complaint = Complaint { + accused_sender: message.sender, + proof: message.encrypted_shares.create_recovery_package( + &self.enc_sk, + &self.recovery_random_oracle(self.id, message.sender), + rng, + ), + }; + return Ok(ProcessedMessage { + message, + shares: vec![], + complaint: Some(complaint), + }); + } + + let decrypted_shares = decrypted_shares + .expect("checked above") + .iter() + .zip(my_share_ids) + .map(|(s, i)| Eval { + index: i, + value: *s, + }) + .collect::>(); + debug!( + "DKG: Successfully decrypted shares from party {}", + message.sender + ); + // Verify all shares in a batch. + if verify_poly_evals(&decrypted_shares, &message.vss_pk, rng).is_err() { + warn!( + "DKG: Processing message from party {} failed, invalid shares", + message.sender + ); + let complaint = Complaint { + accused_sender: message.sender, + proof: message.encrypted_shares.create_recovery_package( + &self.enc_sk, + &self.recovery_random_oracle(self.id, message.sender), + rng, + ), + }; + return Ok(ProcessedMessage { + message, + shares: vec![], + complaint: Some(complaint), + }); + } + + info!( + "DKG: Successfully processed message from party {}", + message.sender + ); + Ok(ProcessedMessage { + message, + shares: decrypted_shares, + complaint: None, + }) + } + + /// 6. Merge results from multiple ProcessedMessages so only one message needs to be sent. + /// + /// Returns NotEnoughInputs if the threshold t is not met. + /// + /// Assumptions: processed_messages is the result of process_message on the same set of messages + /// on all parties. + pub fn merge_v1( + &self, + processed_messages: &[ProcessedMessage], + ) -> FastCryptoResult<(Confirmation, UsedProcessedMessages)> { + debug!("DKG: Trying to merge {} messages", processed_messages.len()); + let filtered_messages = UsedProcessedMessages::from(processed_messages); + // Verify we have enough messages + let total_weight = filtered_messages + .0 + .iter() + .map(|m| { + self.nodes + .node_id_to_node(m.message.sender) + .expect("checked in process_message") + .weight as u32 + }) + .sum::(); + if total_weight < (self.t as u32) { + debug!("Merge failed with total weight {total_weight}"); + return Err(FastCryptoError::NotEnoughInputs); + } + + info!("DKG: Merging messages with total weight {total_weight}"); + + // Log used parties. + let used_parties = filtered_messages + .0 + .iter() + .map(|m| m.message.sender.to_string()) + .collect::>() + .join(","); + debug!("DKG: Using messages from parties: {}", used_parties); + + let mut conf = Confirmation { + sender: self.id, + complaints: Vec::new(), + }; + for m in &filtered_messages.0 { + if let Some(complaint) = &m.complaint { + info!("DKG: Including a complaint on party {}", m.message.sender); + conf.complaints.push(complaint.clone()); + } + } + + if filtered_messages.0.iter().all(|m| m.complaint.is_some()) { + error!("DKG: All processed messages resulted in complaints, this should never happen"); + return Err(FastCryptoError::GeneralError( + "All processed messages resulted in complaints".to_string(), + )); + } + + Ok((conf, filtered_messages)) + } + + /// 7. Process all confirmations, check all complaints, and update the local set of + /// valid shares accordingly. + /// + /// Returns NotEnoughInputs if the threshold minimal_threshold is not met. + /// + /// Assumptions: All parties use the same set of confirmations (and outputs from merge). + pub(crate) fn process_confirmations_v1( + &self, + messages: &UsedProcessedMessages, + confirmations: &[Confirmation], + rng: &mut R, + ) -> FastCryptoResult> { + debug!("Processing {} confirmations", confirmations.len()); + let required_threshold = 2 * (self.t as u32) - 1; // guarantee that at least t honest nodes have valid shares. + + // Ignore confirmations with invalid sender or zero weights + let confirmations = confirmations + .iter() + .filter(|c| { + self.nodes + .node_id_to_node(c.sender) + .is_ok_and(|n| n.weight > 0) + }) + .unique_by(|m| m.sender) + .collect::>(); + // Verify we have enough confirmations + let total_weight = confirmations + .iter() + .map(|c| { + self.nodes + .node_id_to_node(c.sender) + .expect("checked above") + .weight as u32 + }) + .sum::(); + if total_weight < required_threshold { + debug!("Processing confirmations failed with total weight {total_weight}"); + return Err(FastCryptoError::NotEnoughInputs); + } + + info!("DKG: Processing confirmations with total weight {total_weight}, expected {required_threshold}"); + + // Two hash maps for faster access in the main loop below. + let id_to_pk = self + .nodes + .iter() + .map(|n| (n.id, &n.pk)) + .collect::>(); + let id_to_m1 = messages + .0 + .iter() + .map(|m| (m.message.sender, &m.message)) + .collect::>(); + + let mut to_exclude = HashSet::new(); + 'outer: for m2 in confirmations { + 'inner: for complaint in &m2.complaints { + let accused = complaint.accused_sender; + let accuser = m2.sender; + debug!("DKG: Checking complaint from {accuser} on {accused}"); + let accuser_pk = id_to_pk + .get(&accuser) + .expect("checked above that accuser is valid id"); + // If the claim refers to a non existing message, it's an invalid complaint. + let valid_complaint = match id_to_m1.get(&accused) { + Some(related_m1) => Self::check_complaint_proof_v1( + &complaint.proof, + accuser_pk, + accuser, + &self + .nodes + .share_ids_of(accuser) + .expect("checked above the accuser is valid id"), + &related_m1.vss_pk, + &related_m1.encrypted_shares, + &self.recovery_random_oracle(accuser, accused), + &self.encryption_random_oracle(accused), + rng, + ) + .is_ok(), + None => false, + }; + match valid_complaint { + // Ignore accused from now on, and continue processing complaints from the + // current accuser. + true => { + warn!("DKG: Processing confirmations excluded accused party {accused}"); + to_exclude.insert(accused); + continue 'inner; + } + // Ignore the accuser from now on, including its other complaints (not critical + // for security, just saves some work). + false => { + warn!("DKG: Processing confirmations excluded accuser {accuser}"); + to_exclude.insert(accuser); + continue 'outer; + } + } + } + } + + let verified_messages = VerifiedProcessedMessages::filter_from( + messages, + &to_exclude.into_iter().collect::>(), + ); + + if verified_messages.is_empty() { + error!( + "DKG: No verified messages after processing complaints, this should never happen" + ); + return Err(FastCryptoError::GeneralError( + "No verified messages after processing complaints".to_string(), + )); + } + + // Log verified messages parties. + let used_parties = verified_messages + .0 + .iter() + .map(|m| m.message.sender.to_string()) + .collect::>() + .join(","); + debug!( + "DKG: Using verified messages from parties: {}", + used_parties + ); + + Ok(verified_messages) + } + + /// 8. Aggregate the valid shares (as returned from the previous step) and the public key. + pub(crate) fn aggregate_v1( + &self, + messages: &VerifiedProcessedMessages, + ) -> Output { + debug!( + "Aggregating shares from {} verified messages", + messages.0.len() + ); + let id_to_m1 = messages + .0 + .iter() + .map(|m| (m.message.sender, &m.message)) + .collect::>(); + let mut vss_pk = PublicPoly::::zero(); + let my_share_ids = self.nodes.share_ids_of(self.id).expect("my id is valid"); + + let mut final_shares = my_share_ids + .iter() + .map(|share_id| { + ( + share_id, + Share { + index: *share_id, + value: G::ScalarType::zero(), + }, + ) + }) + .collect::>(); + + for m in &messages.0 { + vss_pk.add( + &id_to_m1 + .get(&m.message.sender) + .expect("shares only includes shares from valid first messages") + .vss_pk, + ); + for share in &m.shares { + final_shares + .get_mut(&share.index) + .expect("created above") + .value += share.value; + } + } + + // If I didn't receive a valid share for one of the verified messages (i.e., my complaint + // was not processed), then I don't have a valid share for the final key. + let has_invalid_share = messages.0.iter().any(|m| m.complaint.is_some()); + let has_zero_shares = final_shares.is_empty(); // if my weight is zero + info!( + "DKG: Aggregating my shares completed with has_invalid_share={}, has_zero_shares={}", + has_invalid_share, has_zero_shares + ); + if has_invalid_share { + warn!("DKG: Aggregating my shares failed"); + } + + let shares = if !has_invalid_share && !has_zero_shares { + Some( + final_shares + .values() + .cloned() + .sorted_by_key(|s| s.index) + .collect(), + ) + } else { + None + }; + + Output { + nodes: self.nodes.clone(), + vss_pk, + shares, + } + } + + /// Execute the previous two steps together. + pub fn complete_v1( + &self, + messages: &UsedProcessedMessages, + confirmations: &[Confirmation], + rng: &mut R, + ) -> FastCryptoResult> { + let verified_messages = self.process_confirmations_v1(messages, confirmations, rng)?; + Ok(self.aggregate_v1(&verified_messages)) + } + + // Returns an error if the *complaint* is invalid (counterintuitive). + #[allow(clippy::too_many_arguments)] + fn check_complaint_proof_v1( + recovery_pkg: &ecies::RecoveryPackage, + receiver_pk: &ecies::PublicKey, + receiver_id: PartyId, + share_ids: &[ShareIndex], + vss_pk: &PublicPoly, + encryption: &ecies_v1::MultiRecipientEncryption, + recovery_random_oracle: &RandomOracle, + encryption_random_oracle: &RandomOracle, + rng: &mut R, + ) -> FastCryptoResult<()> { + // Check that the recovery package is valid, and if not, return an error since the complaint + // is invalid. + let buffer = encryption.decrypt_with_recovery_package( + recovery_pkg, + recovery_random_oracle, + encryption_random_oracle, + receiver_pk, + receiver_id as usize, + )?; + + let decrypted_shares: Vec = match bcs::from_bytes(buffer.as_slice()) { + Ok(s) => s, + Err(_) => { + debug!("DKG: check_complaint_proof failed to deserialize shares"); + return Ok(()); + } + }; + + if decrypted_shares.len() != share_ids.len() { + debug!("DKG: check_complaint_proof recovered invalid number of shares"); + return Ok(()); + } + + let decrypted_shares = decrypted_shares + .into_iter() + .zip(share_ids) + .map(|(s, i)| Eval { + index: *i, + value: s, + }) + .collect::>(); + + match verify_poly_evals(&decrypted_shares, vss_pk, rng) { + Ok(()) => Err(FastCryptoError::InvalidProof), + Err(_) => { + debug!("DKG: check_complaint_proof failed to verify shares"); + Ok(()) + } + } + } + + fn encryption_random_oracle(&self, sender: PartyId) -> RandomOracle { + self.random_oracle.extend(&format!("encs {}", sender)) + } + + fn recovery_random_oracle(&self, accuser: PartyId, accused: PartyId) -> RandomOracle { + self.random_oracle.extend(&format!( + "recovery of {} received from {}", + accuser, accused + )) + } +} diff --git a/fastcrypto-tbls/src/ecies.rs b/fastcrypto-tbls/src/ecies.rs new file mode 100644 index 0000000000..7f354752e9 --- /dev/null +++ b/fastcrypto-tbls/src/ecies.rs @@ -0,0 +1,23 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::nizk::DdhTupleNizk; +use fastcrypto::groups::GroupElement; +use serde::{Deserialize, Serialize}; + +// TODO: Use ZeroizeOnDrop. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PrivateKey(pub(crate) G::ScalarType); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PublicKey(pub(crate) G); + +/// A recovery package that allows decrypting a *specific* ECIES Encryption. +/// It also includes a NIZK proof of correctness. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RecoveryPackage { + pub(crate) ephemeral_key: G, + pub(crate) proof: DdhTupleNizk, +} + +pub const AES_KEY_LENGTH: usize = 32; diff --git a/fastcrypto-tbls/src/ecies_v0.rs b/fastcrypto-tbls/src/ecies_v0.rs index d15fa8d210..833037a6eb 100644 --- a/fastcrypto-tbls/src/ecies_v0.rs +++ b/fastcrypto-tbls/src/ecies_v0.rs @@ -1,6 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::ecies::{PrivateKey, PublicKey, RecoveryPackage, AES_KEY_LENGTH}; use crate::nizk::{DLNizk, DdhTupleNizk}; use crate::random_oracle::RandomOracle; use fastcrypto::aes::{Aes256Ctr, AesKey, Cipher, InitializationVector}; @@ -24,13 +25,6 @@ use typenum::consts::{U16, U32}; /// /// The encryption uses AES Counter mode and is not CCA secure as is. -// TODO: Use ZeroizeOnDrop. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PrivateKey(G::ScalarType); - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PublicKey(G); - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Encryption { ephemeral_key: G, @@ -43,16 +37,6 @@ pub struct Encryption { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MultiRecipientEncryption(G, Vec>, DLNizk); -/// A recovery package that allows decrypting a *specific* ECIES Encryption. -/// It also includes a NIZK proof of correctness. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RecoveryPackage { - ephemeral_key: G, - proof: DdhTupleNizk, -} - -const AES_KEY_LENGTH: usize = 32; - impl PrivateKey where G: GroupElement + Serialize, diff --git a/fastcrypto-tbls/src/ecies_v1.rs b/fastcrypto-tbls/src/ecies_v1.rs new file mode 100644 index 0000000000..2bd1b71894 --- /dev/null +++ b/fastcrypto-tbls/src/ecies_v1.rs @@ -0,0 +1,215 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecies::{PrivateKey, PublicKey, RecoveryPackage}; +use crate::nizk::DdhTupleNizk; +use crate::random_oracle::RandomOracle; +use fastcrypto::aes::{Aes256Ctr, AesKey, Cipher, InitializationVector}; +use fastcrypto::error::{FastCryptoError, FastCryptoResult}; +use fastcrypto::groups::{FiatShamirChallenge, GroupElement, HashToGroupElement, Scalar}; +use fastcrypto::traits::{AllowedRng, ToFromBytes}; +use serde::{Deserialize, Serialize}; +use typenum::consts::{U16, U32}; +use typenum::Unsigned; + +/// Simple ECIES encryption using a generic group and AES-256-counter. +/// +/// Random oracles are extended from two oracles provided by the caller, one for +/// encryptions/decryptions and one for recovery packages. We assume that the +/// caller extended the random oracles with the relevant tags and omit them here. +/// +/// The encryption uses AES Counter mode and is not CCA secure as is. +/// + +// TODO: move PrivateKey and PublicKey here and remove old APIs + +/// Multi-recipient encryption with a proof-of-possession of the ephemeral key. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MultiRecipientEncryption { + c: G, + c_hat: G, + encs: Vec>, + pub proof: DdhTupleNizk, +} + +impl MultiRecipientEncryption +where + ::ScalarType: FiatShamirChallenge, + G: HashToGroupElement, +{ + pub fn encrypt( + pk_and_msgs: &[(PublicKey, Vec)], + encryption_random_oracle: &RandomOracle, + rng: &mut R, + ) -> MultiRecipientEncryption { + let r = G::ScalarType::rand(rng); + let c = G::generator() * r; + let g_hat = G::hash_to_group_element( + &Self::g_hat_random_oracle(encryption_random_oracle).evaluate(&c), + ); + let c_hat = g_hat * r; + let proof = DdhTupleNizk::::create( + &r, + &g_hat, + &c, + &c_hat, + &Self::zk_random_oracle(encryption_random_oracle), + rng, + ); + + let encs_ro = Self::encs_random_oracle(encryption_random_oracle); + let encs = pk_and_msgs + .iter() + .enumerate() + .map(|(receiver_index, (receiver_pk, msg))| { + let pk_r = receiver_pk.0 * r; + let k = encs_ro.evaluate(&(receiver_index, pk_r)); + let cipher = sym_cipher(&k); + // Since k is fresh per encryption, we can safely use a fixed nonce. + cipher.encrypt(&fixed_zero_nonce(), msg) + }) + .collect::>(); + + MultiRecipientEncryption { + c, + c_hat, + encs, + proof, + } + } + + pub fn verify(&self, encryption_random_oracle: &RandomOracle) -> FastCryptoResult<()> { + let g_hat = G::hash_to_group_element( + &Self::g_hat_random_oracle(encryption_random_oracle).evaluate(&self.c), + ); + self.proof.verify( + &g_hat, + &self.c, + &self.c_hat, + &Self::zk_random_oracle(encryption_random_oracle), + )?; + // Encryptions should not be empty. + self.encs + .iter() + .all(|e| !e.is_empty()) + .then_some(()) + .ok_or(FastCryptoError::InvalidInput) + } + + /// We assume that verify is called before decrypt and do not call it again here to + /// avoid redundant checks. + pub fn decrypt( + &self, + sk: &PrivateKey, + encryption_random_oracle: &RandomOracle, + receiver_index: usize, + ) -> Vec { + let enc_ro = Self::encs_random_oracle(encryption_random_oracle); + let ephemeral_key = self.c * sk.0; + let k = enc_ro.evaluate(&(receiver_index, ephemeral_key)); + let cipher = sym_cipher(&k); + cipher + .decrypt(&fixed_zero_nonce(), &self.encs[receiver_index]) + .expect("Decrypt should never fail for CTR mode") + } + + /// We assume that verify is called before decrypt and do not call it again here to + /// avoid redundant checks. + pub fn create_recovery_package( + &self, + sk: &PrivateKey, + recovery_random_oracle: &RandomOracle, + rng: &mut R, + ) -> RecoveryPackage { + let pk = G::generator() * sk.0; + let ephemeral_key = self.c * sk.0; + + let proof = DdhTupleNizk::::create( + &sk.0, + &self.c, + &pk, + &ephemeral_key, + recovery_random_oracle, + rng, + ); + + RecoveryPackage { + ephemeral_key, + proof, + } + } + + pub fn decrypt_with_recovery_package( + &self, + pkg: &RecoveryPackage, + recovery_random_oracle: &RandomOracle, + encryption_random_oracle: &RandomOracle, + receiver_pk: &PublicKey, + receiver_index: usize, + ) -> FastCryptoResult> { + assert!(receiver_index < self.encs.len()); + pkg.proof.verify( + &self.c, + &receiver_pk.0, + &pkg.ephemeral_key, + recovery_random_oracle, + )?; + let encs_ro = Self::encs_random_oracle(encryption_random_oracle); + let k = encs_ro.evaluate(&(receiver_index, pkg.ephemeral_key)); + let cipher = sym_cipher(&k); + Ok(cipher + .decrypt(&fixed_zero_nonce(), &self.encs[receiver_index]) + .expect("Decrypt should never fail for CTR mode")) + } + + pub fn len(&self) -> usize { + self.encs.len() + } + pub fn is_empty(&self) -> bool { + self.encs.is_empty() + } + + // Used for debugging + pub fn ephemeral_key(&self) -> &G { + &self.c + } + + // Used for debugging + pub fn proof(&self) -> &DdhTupleNizk { + &self.proof + } + + fn encs_random_oracle(encryption_random_oracle: &RandomOracle) -> RandomOracle { + encryption_random_oracle.extend("encs") + } + + fn zk_random_oracle(encryption_random_oracle: &RandomOracle) -> RandomOracle { + encryption_random_oracle.extend("zk") + } + + fn g_hat_random_oracle(encryption_random_oracle: &RandomOracle) -> RandomOracle { + encryption_random_oracle.extend("g_hat") + } + + #[cfg(test)] + pub fn modify_c_hat_for_testing(&mut self, c_hat: G) { + self.c_hat = c_hat; + } + + #[cfg(test)] + pub fn copy_for_testing(&mut self, src: usize, dst: usize) { + self.encs[dst] = self.encs[src].clone(); + } +} + +fn fixed_zero_nonce() -> InitializationVector { + InitializationVector::::from_bytes(&[0u8; 16]) + .expect("U16 could always be set from a 16 bytes array of zeros") +} + +fn sym_cipher(k: &[u8; 64]) -> Aes256Ctr { + Aes256Ctr::new( + AesKey::::from_bytes(&k[0..U32::USIZE]) + .expect("New shouldn't fail as use fixed size key is used"), + ) +} diff --git a/fastcrypto-tbls/src/lib.rs b/fastcrypto-tbls/src/lib.rs index efc2c24e3a..f4616ddaf5 100644 --- a/fastcrypto-tbls/src/lib.rs +++ b/fastcrypto-tbls/src/lib.rs @@ -11,9 +11,13 @@ //! A crate that implements threshold BLS (tBLS) and distributed key generation (DKG) //! protocols. +pub mod dkg; pub mod dkg_v0; +pub mod dkg_v1; pub mod dl_verification; +pub mod ecies; pub mod ecies_v0; +pub mod ecies_v1; pub mod mocked_dkg; pub mod nizk; pub mod nodes; @@ -37,6 +41,10 @@ pub mod polynomial_tests; #[path = "tests/ecies_v0_tests.rs"] pub mod ecies_v0_tests; +#[cfg(test)] +#[path = "tests/ecies_v1_tests.rs"] +pub mod ecies_v1_tests; + #[cfg(test)] #[path = "tests/random_oracle_tests.rs"] pub mod random_oracle_tests; @@ -45,6 +53,10 @@ pub mod random_oracle_tests; #[path = "tests/dkg_v0_tests.rs"] pub mod dkg_v0_tests; +#[cfg(test)] +#[path = "tests/dkg_v1_tests.rs"] +pub mod dkg_v1_tests; + #[cfg(test)] #[path = "tests/nodes_tests.rs"] pub mod nodes_tests; diff --git a/fastcrypto-tbls/src/mocked_dkg.rs b/fastcrypto-tbls/src/mocked_dkg.rs index ab35bc8551..01621f850e 100644 --- a/fastcrypto-tbls/src/mocked_dkg.rs +++ b/fastcrypto-tbls/src/mocked_dkg.rs @@ -1,7 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::dkg_v0::Output; +use crate::dkg::Output; use crate::nodes::{Nodes, PartyId}; use crate::polynomial::PrivatePoly; use fastcrypto::groups::GroupElement; diff --git a/fastcrypto-tbls/src/nidkg.rs b/fastcrypto-tbls/src/nidkg.rs index 405b97b44c..3136c19af2 100644 --- a/fastcrypto-tbls/src/nidkg.rs +++ b/fastcrypto-tbls/src/nidkg.rs @@ -5,12 +5,12 @@ use crate::dl_verification::{ verify_deg_t_poly, verify_equal_exponents, verify_pairs, verify_triplets, }; -use crate::ecies_v0; -use crate::ecies_v0::{PublicKey, RecoveryPackage}; +use crate::ecies::{PublicKey, RecoveryPackage}; use crate::nodes::{Node, Nodes, PartyId}; use crate::polynomial::{Eval, Poly, PrivatePoly}; use crate::random_oracle::RandomOracle; use crate::types::ShareIndex; +use crate::{ecies, ecies_v0}; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; use fastcrypto::groups::{bls12381, FiatShamirChallenge, GroupElement, MultiScalarMul, Scalar}; use fastcrypto::hmac::{hmac_sha3_256, HmacKey}; @@ -29,7 +29,7 @@ where nodes: Nodes, t: u16, random_oracle: RandomOracle, - ecies_sk: ecies_v0::PrivateKey, + ecies_sk: ecies::PrivateKey, vss_sk: PrivatePoly, // Precomputed values to be used when verifying messages. precomputed_dual_code_coefficients: Vec, @@ -92,13 +92,13 @@ where /// /// 3. Create a new Party instance with the private key and the set of nodes. pub fn new( - ecies_sk: ecies_v0::PrivateKey, + ecies_sk: ecies::PrivateKey, nodes: Vec>, t: u16, // The number of shares that are needed to reconstruct the full signature. random_oracle: RandomOracle, rng: &mut R, ) -> Result { - let ecies_pk = ecies_v0::PublicKey::::from_private_key(&ecies_sk); + let ecies_pk = ecies::PublicKey::::from_private_key(&ecies_sk); let my_id = nodes .iter() .find(|n| n.pk == ecies_pk) diff --git a/fastcrypto-tbls/src/nizk.rs b/fastcrypto-tbls/src/nizk.rs index aa5c2877ee..7093e9491e 100644 --- a/fastcrypto-tbls/src/nizk.rs +++ b/fastcrypto-tbls/src/nizk.rs @@ -14,7 +14,7 @@ use tracing::debug; /// - Verifier checks that zG=A+c(xG) and zH=B+c(xH). /// The NIZK is (A, B, z) where c is implicitly computed using a random oracle. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub(crate) struct DdhTupleNizk(G, G, G::ScalarType); +pub struct DdhTupleNizk(G, G, G::ScalarType); impl DdhTupleNizk where diff --git a/fastcrypto-tbls/src/nodes.rs b/fastcrypto-tbls/src/nodes.rs index 954cc5b038..a38ca8bf39 100644 --- a/fastcrypto-tbls/src/nodes.rs +++ b/fastcrypto-tbls/src/nodes.rs @@ -1,7 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::ecies_v0; +use crate::ecies; use crate::types::ShareIndex; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; use fastcrypto::groups::GroupElement; @@ -15,7 +15,7 @@ pub type PartyId = u16; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Node { pub id: PartyId, - pub pk: ecies_v0::PublicKey, + pub pk: ecies::PublicKey, pub weight: u16, // May be zero after reduce() } diff --git a/fastcrypto-tbls/src/tests/dkg_v0_tests.rs b/fastcrypto-tbls/src/tests/dkg_v0_tests.rs index 2ec4e46a65..11b15c19a0 100644 --- a/fastcrypto-tbls/src/tests/dkg_v0_tests.rs +++ b/fastcrypto-tbls/src/tests/dkg_v0_tests.rs @@ -1,11 +1,10 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::dkg_v0::{ - create_fake_complaint, Confirmation, Message, Output, Party, ProcessedMessage, - DKG_MESSAGES_MAX_SIZE, -}; -use crate::ecies_v0::{MultiRecipientEncryption, PrivateKey, PublicKey}; +use crate::dkg::{Confirmation, Output, Party, DKG_MESSAGES_MAX_SIZE}; +use crate::dkg_v0::{create_fake_complaint, Message, ProcessedMessage}; +use crate::ecies::{PrivateKey, PublicKey}; +use crate::ecies_v0::MultiRecipientEncryption; use crate::mocked_dkg::generate_mocked_output; use crate::nodes::{Node, Nodes, PartyId}; use crate::polynomial::Poly; diff --git a/fastcrypto-tbls/src/tests/dkg_v1_tests.rs b/fastcrypto-tbls/src/tests/dkg_v1_tests.rs new file mode 100644 index 0000000000..16b8c57b4b --- /dev/null +++ b/fastcrypto-tbls/src/tests/dkg_v1_tests.rs @@ -0,0 +1,657 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::dkg::{Confirmation, Party, DKG_MESSAGES_MAX_SIZE}; +use crate::dkg_v0::create_fake_complaint; +use crate::dkg_v1::{Message, ProcessedMessage}; +use crate::ecies::{PrivateKey, PublicKey}; +use crate::ecies_v1::MultiRecipientEncryption; +use crate::nodes::{Node, Nodes, PartyId}; +use crate::polynomial::Poly; +use crate::random_oracle::RandomOracle; +use crate::tbls::ThresholdBls; +use crate::types::ThresholdBls12381MinSig; +use fastcrypto::error::FastCryptoError; +use fastcrypto::groups::bls12381::G2Element; +use fastcrypto::groups::GroupElement; +use rand::thread_rng; + +const MSG: [u8; 4] = [1, 2, 3, 4]; + +type G = G2Element; +type S = ThresholdBls12381MinSig; +type EG = G2Element; + +type KeyNodePair = (PartyId, PrivateKey, PublicKey); + +fn gen_keys_and_nodes(n: usize) -> (Vec>, Nodes) { + let keys = (0..n) + .map(|id| { + let sk = PrivateKey::::new(&mut thread_rng()); + let pk = PublicKey::::from_private_key(&sk); + (id as u16, sk, pk) + }) + .collect::>(); + let nodes = keys + .iter() + .map(|(id, _sk, pk)| Node:: { + id: *id, + pk: pk.clone(), + weight: if *id == 2 { 0 } else { 2 + id }, + }) + .collect(); + let nodes = Nodes::new(nodes).unwrap(); + (keys, nodes) +} + +// Enable if logs are needed +// #[traced_test] +#[test] +fn test_dkg_e2e_5_parties_min_weight_2_threshold_3() { + let ro = RandomOracle::new("dkg"); + let t = 3; + let (keys, nodes) = gen_keys_and_nodes(6); + + // Create the parties + let d0 = Party::::new( + keys.first().unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + assert_eq!(d0.t(), t); + let d1 = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + // Party with weight 0 + let d2 = Party::::new( + keys.get(2_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let d3 = Party::::new( + keys.get(3_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let d4 = Party::::new( + keys.get(4_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let d5 = Party::::new( + keys.get(5_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + + // Only the first messages of d0, d4, d5 will pass all tests. d4's messages will be excluded + // later because of an invalid complaint + let msg4 = d4.create_message_v1(&mut thread_rng()).unwrap(); + let msg5 = d5.create_message_v1(&mut thread_rng()).unwrap(); + // zero weight + assert_eq!( + d2.create_message_v1(&mut thread_rng()).err(), + Some(FastCryptoError::IgnoredMessage) + ); + // d5 will receive invalid shares from d0, but its complaint will not be processed on time. + let mut msg0 = d0.create_message_v1(&mut thread_rng()).unwrap(); + let mut pk_and_msgs = decrypt_and_prepare_for_reenc(&keys, &nodes, &msg0, &ro); + pk_and_msgs[5] = pk_and_msgs[0].clone(); + msg0.encrypted_shares = + MultiRecipientEncryption::encrypt(&pk_and_msgs, &ro.extend("encs 0"), &mut thread_rng()); + + // We will modify d1's message to make it invalid (emulating a cheating party). d0 and d1 + // should detect that and send complaints. + let mut msg1 = d1.create_message_v1(&mut thread_rng()).unwrap(); + let mut pk_and_msgs = decrypt_and_prepare_for_reenc(&keys, &nodes, &msg1, &ro); + pk_and_msgs.swap(0, 1); + msg1.encrypted_shares = + MultiRecipientEncryption::encrypt(&pk_and_msgs, &ro.extend("encs 1"), &mut thread_rng()); + // d2 and d3 are ignored here (emulating slow parties). + + let all_messages = vec![msg0.clone(), msg1, msg0.clone(), msg4.clone(), msg5.clone()]; // duplicates should be ignored + + // expect failure - merge() requires t messages (even if some are duplicated) + let proc0 = d0 + .process_message_v1(msg0.clone(), &mut thread_rng()) + .unwrap(); + assert_eq!( + d0.merge_v1(&[proc0.clone()]).err(), + Some(FastCryptoError::NotEnoughInputs) + ); + assert_eq!( + d0.merge_v1(&[proc0.clone(), proc0.clone()]).err(), + Some(FastCryptoError::NotEnoughInputs) + ); + + // merge() should succeed and ignore duplicates and include 1 complaint + let proc_msg0 = &all_messages + .iter() + .map(|m| d0.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf0, used_msgs0) = d0.merge_v1(proc_msg0).unwrap(); + assert_eq!(conf0.complaints.len(), 1); + assert_eq!(used_msgs0.0.len(), 4); + assert_eq!(proc0.message, msg0); + + let proc_msg1 = &all_messages + .iter() + .map(|m| d1.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf1, used_msgs1) = d1.merge_v1(proc_msg1).unwrap(); + + let proc_msg2 = &all_messages + .iter() + .map(|m| d2.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf2, used_msgs2) = d2.merge_v1(proc_msg2).unwrap(); + assert!(conf2.complaints.is_empty()); + + // Note that d3's first round message is not included but it should still be able to receive + // shares and post complaints. + let proc_msg3 = &all_messages + .iter() + .map(|m| d3.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf3, used_msgs3) = d3.merge_v1(proc_msg3).unwrap(); + + let proc_msg5 = &all_messages + .iter() + .map(|m| d5.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf5, used_msgs5) = d5.merge_v1(proc_msg5).unwrap(); + assert_eq!(conf5.complaints.len(), 1); + + // There should be some complaints on the first messages of d1. + assert!( + !conf0.complaints.is_empty() + || !conf1.complaints.is_empty() + || !conf3.complaints.is_empty() + ); + // But also no complaints from one of the parties (since we switched only 2 encryptions) + assert!( + conf0.complaints.is_empty() || conf1.complaints.is_empty() || conf3.complaints.is_empty() + ); + + // create a simple invalid complaint from d4. + let mut conf4 = conf1.clone(); + conf4.sender = 4; + + let all_confirmations = vec![ + conf0.clone(), + conf2.clone(), + conf1.clone(), + conf3, + conf1.clone(), + conf4, + ]; // duplicates and zero weights should be ignored + + // expect failure - process_confirmations() should receive enough messages (non duplicated). + assert_eq!( + d1.process_confirmations_v1( + &used_msgs0, + &[conf0.clone(), conf0.clone(), conf0.clone()], + &mut thread_rng(), + ) + .err(), + Some(FastCryptoError::NotEnoughInputs) + ); + + // back to the happy case + let ver_msg0 = d1 + .process_confirmations_v1(&used_msgs0, &all_confirmations, &mut thread_rng()) + .unwrap(); + let ver_msg1 = d1 + .process_confirmations_v1(&used_msgs1, &all_confirmations, &mut thread_rng()) + .unwrap(); + let ver_msg2 = d2 + .process_confirmations_v1(&used_msgs2, &all_confirmations, &mut thread_rng()) + .unwrap(); + let ver_msg3 = d3 + .process_confirmations_v1(&used_msgs3, &all_confirmations, &mut thread_rng()) + .unwrap(); + let ver_msg5 = d5 + .process_confirmations_v1(&used_msgs5, &all_confirmations, &mut thread_rng()) + .unwrap(); + assert_eq!(ver_msg0.len(), 2); // only msg0, msg5 were valid and didn't send invalid complaints + assert_eq!(ver_msg1.len(), 2); + assert_eq!(ver_msg2.len(), 2); + assert_eq!(ver_msg3.len(), 2); + assert_eq!(ver_msg5.len(), 2); + + let o0 = d0.aggregate_v1(&ver_msg0); + let _o1 = d1.aggregate_v1(&ver_msg1); + let o2 = d2.aggregate_v1(&ver_msg2); + let o3 = d3.aggregate_v1(&ver_msg3); + let o5 = d5.aggregate_v1(&ver_msg5); + assert!(o0.shares.is_some()); + assert!(o2.shares.is_none()); + assert!(o3.shares.is_some()); + assert!(o5.shares.is_none()); // recall that it didn't receive valid share from msg0 + assert_eq!(o0.vss_pk, o2.vss_pk); + assert_eq!(o0.vss_pk, o3.vss_pk); + assert_eq!(o0.vss_pk, o5.vss_pk); + + // check the resulting vss pk + let mut poly = msg0.vss_pk.clone(); + poly.add(&msg5.vss_pk); + assert_eq!(poly, o0.vss_pk); + + // Use the shares to sign the message. + let sig00 = S::partial_sign(&o0.shares.as_ref().unwrap()[0], &MSG); + let sig30 = S::partial_sign(&o3.shares.as_ref().unwrap()[0], &MSG); + let sig31 = S::partial_sign(&o3.shares.as_ref().unwrap()[1], &MSG); + + S::partial_verify(&o0.vss_pk, &MSG, &sig00).unwrap(); + S::partial_verify(&o3.vss_pk, &MSG, &sig30).unwrap(); + S::partial_verify(&o3.vss_pk, &MSG, &sig31).unwrap(); + + let sigs = vec![sig00, sig30, sig31]; + let sig = S::aggregate(d0.t(), sigs.iter()).unwrap(); + S::verify(o0.vss_pk.c0(), &MSG, &sig).unwrap(); +} + +fn decrypt_and_prepare_for_reenc( + keys: &[KeyNodePair], + nodes: &Nodes, + msg0: &Message, + ro: &RandomOracle, +) -> Vec<(PublicKey, Vec)> { + nodes + .iter() + .map(|n| { + let key = keys[n.id as usize].1.clone(); + ( + n.pk.clone(), + msg0.encrypted_shares.decrypt( + &key, + &ro.extend(&format!("encs {}", msg0.sender)), + n.id as usize, + ), + ) + }) + .collect::>() +} + +#[test] +fn test_party_new_errors() { + let ro = RandomOracle::new("dkg"); + let (keys, nodes) = gen_keys_and_nodes(4); + + // t is zero + assert!(Party::::new( + keys.first().unwrap().1.clone(), + nodes.clone(), + 0, + ro.clone(), + &mut thread_rng(), + ) + .is_err()); + // t is too large + assert!(Party::::new( + keys.first().unwrap().1.clone(), + nodes.clone(), + 6, + ro.clone(), + &mut thread_rng(), + ) + .is_err()); + // Invalid pk + assert!(Party::::new( + PrivateKey::::new(&mut thread_rng()), + nodes.clone(), + 3, + ro.clone(), + &mut thread_rng(), + ) + .is_err()); +} + +#[test] +fn test_process_message_failures() { + let ro = RandomOracle::new("dkg"); + let t = 3; + let (keys, nodes) = gen_keys_and_nodes(4); + + let d0 = Party::::new( + keys.first().unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + + // invalid sender + let d1 = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let mut invalid_msg = d1.create_message_v1(&mut thread_rng()).unwrap(); + invalid_msg.sender = 50; + assert!(d0 + .process_message_v1(invalid_msg, &mut thread_rng()) + .is_err()); + + // zero weight + let d1 = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let mut fake_msg_from_d2 = d1.create_message_v1(&mut thread_rng()).unwrap(); + fake_msg_from_d2.sender = 2; + assert!(d0 + .process_message_v1(fake_msg_from_d2, &mut thread_rng()) + .is_err()); + + // invalid degree + let d1 = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + t - 1, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let invalid_msg = d1.create_message_v1(&mut thread_rng()).unwrap(); + assert!(d0 + .process_message_v1(invalid_msg, &mut thread_rng()) + .is_err()); + + // invalid c0 + let d1 = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let mut invalid_msg = d1.create_message_v1(&mut thread_rng()).unwrap(); + let mut poly: Vec = invalid_msg.vss_pk.as_vec().clone(); + poly[0] = G::zero(); + invalid_msg.vss_pk = poly.into(); + assert!(d0 + .process_message_v1(invalid_msg, &mut thread_rng()) + .is_err()); + + // invalid total number of encrypted shares + let mut msg1 = d1.create_message_v1(&mut thread_rng()).unwrap(); + // Switch the encrypted shares of two receivers. + let mut pk_and_msgs = decrypt_and_prepare_for_reenc(&keys, &nodes, &msg1, &ro); + pk_and_msgs.pop(); + msg1.encrypted_shares = + MultiRecipientEncryption::encrypt(&pk_and_msgs, &ro.extend("encs 1"), &mut thread_rng()); + assert!(d0.process_message_v1(msg1, &mut thread_rng()).is_err()); + + // invalid encryption's proof + let mut msg1 = d1.create_message_v1(&mut thread_rng()).unwrap(); + // Switch the encrypted shares of two receivers. + msg1.encrypted_shares + .modify_c_hat_for_testing(*msg1.encrypted_shares.ephemeral_key()); + assert!(d0.process_message_v1(msg1, &mut thread_rng()).is_err()); + + // invalid number of encrypted shares for specific receiver + let mut msg1 = d1.create_message_v1(&mut thread_rng()).unwrap(); + // Switch the encrypted shares of two receivers. + let mut pk_and_msgs = decrypt_and_prepare_for_reenc(&keys, &nodes, &msg1, &ro); + pk_and_msgs.swap(0, 1); + msg1.encrypted_shares = + MultiRecipientEncryption::encrypt(&pk_and_msgs, &ro.extend("encs 1"), &mut thread_rng()); + let ProcessedMessage { + message: _, + shares, + complaint, + } = d0.process_message_v1(msg1, &mut thread_rng()).unwrap(); + if !shares.is_empty() || complaint.is_none() { + panic!("expected complaint"); + }; + + // invalid share + // use another d1 with a different vss_sk to create an encryption with "invalid" shares + let another_d1 = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let msg1_from_another_d1 = another_d1.create_message_v1(&mut thread_rng()).unwrap(); + let mut msg1 = d1.create_message_v1(&mut thread_rng()).unwrap(); + msg1.encrypted_shares = msg1_from_another_d1.encrypted_shares; + let ProcessedMessage { + message: _, + shares, + complaint, + } = d0.process_message_v1(msg1, &mut thread_rng()).unwrap(); + if !shares.is_empty() || complaint.is_none() { + panic!("expected complaint"); + }; +} + +#[test] +fn test_test_process_confirmations() { + let ro = RandomOracle::new("dkg"); + let t = 3; + let (keys, nodes) = gen_keys_and_nodes(6); + + let d0 = Party::::new( + keys.first().unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let d1 = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let d2 = Party::::new( + keys.get(2_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + let d3 = Party::::new( + keys.get(3_usize).unwrap().1.clone(), + nodes.clone(), + t, + ro.clone(), + &mut thread_rng(), + ) + .unwrap(); + + let msg0 = d0.create_message_v1(&mut thread_rng()).unwrap(); + let msg1 = d1.create_message_v1(&mut thread_rng()).unwrap(); + // zero weight + assert_eq!( + d2.create_message_v1(&mut thread_rng()).err(), + Some(FastCryptoError::IgnoredMessage) + ); + let mut msg3 = d3.create_message_v1(&mut thread_rng()).unwrap(); + let mut pk_and_msgs = decrypt_and_prepare_for_reenc(&keys, &nodes, &msg3, &ro); + pk_and_msgs.swap(0, 1); + msg3.encrypted_shares = + MultiRecipientEncryption::encrypt(&pk_and_msgs, &ro.extend("encs 3"), &mut thread_rng()); + + let all_messages = vec![msg0, msg1, msg3]; + + let proc_msg0 = &all_messages + .iter() + .map(|m| d0.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf0, used_msgs0) = d0.merge_v1(proc_msg0).unwrap(); + + let proc_msg1 = &all_messages + .iter() + .map(|m| d1.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf1, _used_msgs1) = d1.merge_v1(proc_msg1).unwrap(); + + let proc_msg2 = &all_messages + .iter() + .map(|m| d2.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf2, _used_msgs2) = d2.merge_v1(proc_msg2).unwrap(); + + let proc_msg3 = &all_messages + .iter() + .map(|m| d3.process_message_v1(m.clone(), &mut thread_rng()).unwrap()) + .collect::>(); + let (conf3, _used_msgs3) = d3.merge_v1(proc_msg3).unwrap(); + + // sanity check that with the current confirmations, all messages are in + let ver_msg = d1 + .process_confirmations_v1( + &used_msgs0, + &[conf0.clone(), conf1.clone(), conf2.clone(), conf3.clone()], + &mut thread_rng(), + ) + .unwrap(); + // d3 is ignored because it sent an invalid message, d2 is ignored because it's weight is 0 + assert_eq!( + ver_msg + .data() + .iter() + .map(|m| m.message.sender) + .collect::>(), + vec![0, 1] + ); + + // invalid senders should be ignored + let mut conf7 = conf3.clone(); + conf7.sender = 7; // Should be ignored since it's an invalid sender + let ver_msg = d1 + .process_confirmations_v1( + &used_msgs0, + &[conf2.clone(), conf3.clone(), conf7], + &mut thread_rng(), + ) + .unwrap(); + + // d3 is not ignored since conf7 is ignored, d2 is ignored because it's weight is 0 + assert_eq!( + ver_msg + .data() + .iter() + .map(|m| m.message.sender) + .collect::>(), + vec![0, 1, 3] + ); + + // create an invalid complaint from d4 (invalid recovery package) + assert!(conf2.complaints.is_empty()); // since only d0, d1 received invalid shares + let mut conf2 = conf1.clone(); + conf2.sender = 2; + let ver_msg = d1 + .process_confirmations_v1( + &used_msgs0, + &[conf0.clone(), conf1.clone(), conf2.clone(), conf3.clone()], + &mut thread_rng(), + ) + .unwrap(); + // now also d2 is ignored because it sent an invalid complaint + assert_eq!( + ver_msg + .data() + .iter() + .map(|m| m.message.sender) + .collect::>(), + vec![0, 1] + ); +} + +#[test] +fn create_message_generates_valid_message() { + let (keys, nodes) = gen_keys_and_nodes(4); + let d = Party::::new( + keys.get(1_usize).unwrap().1.clone(), + nodes.clone(), + 3, + RandomOracle::new("dkg"), + &mut thread_rng(), + ) + .unwrap(); + let msg = d.create_message_v1(&mut thread_rng()).unwrap(); + + assert_eq!(msg.sender, 1); + assert_eq!(msg.encrypted_shares.len(), 4); + assert_eq!(msg.vss_pk.degree(), 2); +} + +#[test] +fn test_size_limits() { + // Confirm that messages sizes are within the limit for the extreme expected parameters. + let n = 3333; + let t = n / 3; + let k = 400; + + let p = Poly::<::ScalarType>::rand(t as u16, &mut thread_rng()); + let ro = RandomOracle::new("test"); + let keys_and_msg = (0..k) + .map(|i| { + let sk = PrivateKey::::new(&mut thread_rng()); + let pk = PublicKey::::from_private_key(&sk); + (sk, pk, format!("test {}", i)) + }) + .collect::>(); + let encrypted_shares = MultiRecipientEncryption::encrypt( + &keys_and_msg + .iter() + .map(|(_, pk, msg)| (pk.clone(), msg.as_bytes().to_vec())) + .collect::>(), + &ro, + &mut thread_rng(), + ); + let msg = Message { + sender: 0, + vss_pk: p.commit::(), + encrypted_shares, + }; + assert!(bcs::to_bytes(&msg).unwrap().len() <= DKG_MESSAGES_MAX_SIZE); + + let complaints = (0..k) + .map(|_| create_fake_complaint::()) + .collect::>(); + let conf = Confirmation { + sender: 0, + complaints, + }; + assert!(bcs::to_bytes(&conf).unwrap().len() <= DKG_MESSAGES_MAX_SIZE); +} diff --git a/fastcrypto-tbls/src/tests/ecies_v0_tests.rs b/fastcrypto-tbls/src/tests/ecies_v0_tests.rs index 026b000b9e..f72a8a46e1 100644 --- a/fastcrypto-tbls/src/tests/ecies_v0_tests.rs +++ b/fastcrypto-tbls/src/tests/ecies_v0_tests.rs @@ -1,6 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::ecies::{PrivateKey, PublicKey}; use crate::ecies_v0::*; use crate::random_oracle::RandomOracle; use fastcrypto::bls12381::min_sig::BLS12381KeyPair; @@ -17,6 +18,7 @@ const MSG: &[u8; 4] = b"test"; #[generic_tests::define] mod point_tests { use super::*; + use crate::ecies::{PrivateKey, PublicKey}; #[test] fn test_decryption() diff --git a/fastcrypto-tbls/src/tests/ecies_v1_tests.rs b/fastcrypto-tbls/src/tests/ecies_v1_tests.rs new file mode 100644 index 0000000000..80c5e8d0eb --- /dev/null +++ b/fastcrypto-tbls/src/tests/ecies_v1_tests.rs @@ -0,0 +1,121 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecies::{PrivateKey, PublicKey}; +use crate::ecies_v1::*; +use crate::random_oracle::RandomOracle; +use fastcrypto::bls12381::min_sig::BLS12381KeyPair; +use fastcrypto::groups::bls12381::{G1Element, G2Element, Scalar}; +use fastcrypto::groups::{FiatShamirChallenge, GroupElement}; +use fastcrypto::traits::KeyPair; +use rand::thread_rng; +use serde::de::DeserializeOwned; +use serde::Serialize; + +#[generic_tests::define] +mod point_tests { + use super::*; + use crate::ecies::{PrivateKey, PublicKey}; + use fastcrypto::groups::HashToGroupElement; + + #[test] + fn test_multi_rec() + where + Group::ScalarType: FiatShamirChallenge, + Group: HashToGroupElement, + { + let ro = RandomOracle::new("test"); + let keys_and_msg = (0..10u32) + .map(|i| { + let sk = PrivateKey::::new(&mut thread_rng()); + let pk = PublicKey::::from_private_key(&sk); + ( + sk, + pk, + format!( + "test {} 12345678901234567890123456789012345678901234567890", + i + ), + ) + }) + .collect::>(); + + let mr_enc = MultiRecipientEncryption::encrypt( + &keys_and_msg + .iter() + .map(|(_, pk, msg)| (pk.clone(), msg.as_bytes().to_vec())) + .collect::>(), + &ro, + &mut thread_rng(), + ); + + assert!(mr_enc.verify(&ro).is_ok()); + assert!(mr_enc.verify(&ro.extend("bla")).is_err()); + + for (i, (sk, _, msg)) in keys_and_msg.iter().enumerate() { + let decrypted = mr_enc.decrypt(sk, &ro, i); + assert_eq!(msg.as_bytes(), &decrypted); + } + + // test empty messages + let mr_enc2 = MultiRecipientEncryption::encrypt( + &keys_and_msg + .iter() + .map(|(_, pk, _)| (pk.clone(), vec![])) + .collect::>(), + &ro, + &mut thread_rng(), + ); + assert!(mr_enc2.verify(&ro).is_err()); + + // test recovery package + let recovery_ro = ro.extend("recovery"); + let mr_enc2 = MultiRecipientEncryption::encrypt( + &keys_and_msg + .iter() + .map(|(_, pk, msg)| (pk.clone(), msg.as_bytes().to_vec())) + .collect::>(), + &ro, + &mut thread_rng(), + ); + + for (i, (sk, pk, msg)) in keys_and_msg.iter().enumerate() { + let pkg = mr_enc.create_recovery_package(sk, &recovery_ro, &mut thread_rng()); + let decrypted = mr_enc + .decrypt_with_recovery_package(&pkg, &recovery_ro, &ro, pk, i) + .unwrap(); + assert_eq!(msg.as_bytes(), &decrypted); + + // Should fail for a different RO. + assert!(mr_enc + .decrypt_with_recovery_package(&pkg, &recovery_ro.extend("bla"), &ro, pk, i) + .is_err()); + + // Same package will fail on a different encryption + assert!(mr_enc2 + .decrypt_with_recovery_package(&pkg, &recovery_ro, &ro, pk, i) + .is_err()); + } + } + + #[instantiate_tests()] + mod g1_element {} + + #[instantiate_tests()] + mod g2_element {} +} + +#[test] +fn test_blskeypair_to_group() { + let pair = BLS12381KeyPair::generate(&mut thread_rng()); + let (pk, sk) = (pair.public().clone(), pair.private()); + let pk: G2Element = bcs::from_bytes(pk.as_ref()).expect("should work"); + let ecies_pk = PublicKey::::from(pk); + let sk: Scalar = bcs::from_bytes(sk.as_ref()).expect("should work"); + let ecies_sk = PrivateKey::::from(sk); + assert_eq!( + ecies_pk, + PublicKey::::from_private_key(&ecies_sk) + ); + assert_eq!(*ecies_pk.as_element(), G2Element::generator() * sk); +} diff --git a/fastcrypto-tbls/src/tests/nidkg_tests.rs b/fastcrypto-tbls/src/tests/nidkg_tests.rs index 0e9bb67f72..2bd31e5eb1 100644 --- a/fastcrypto-tbls/src/tests/nidkg_tests.rs +++ b/fastcrypto-tbls/src/tests/nidkg_tests.rs @@ -1,7 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::ecies_v0; +use crate::ecies; use crate::nidkg::Party; use crate::nodes::Node; use crate::random_oracle::RandomOracle; @@ -12,11 +12,11 @@ use rand::thread_rng; type G = G1Element; -pub fn gen_ecies_keys(n: u16) -> Vec<(u16, ecies_v0::PrivateKey, ecies_v0::PublicKey)> { +pub fn gen_ecies_keys(n: u16) -> Vec<(u16, ecies::PrivateKey, ecies::PublicKey)> { (0..n) .map(|id| { - let sk = ecies_v0::PrivateKey::::new(&mut thread_rng()); - let pk = ecies_v0::PublicKey::::from_private_key(&sk); + let sk = ecies::PrivateKey::::new(&mut thread_rng()); + let pk = ecies::PublicKey::::from_private_key(&sk); (id, sk, pk) }) .collect() @@ -25,7 +25,7 @@ pub fn gen_ecies_keys(n: u16) -> Vec<(u16, ecies_v0::PrivateKey, ecies_v0::Pu pub fn setup_party( id: usize, threshold: u16, - keys: &[(u16, ecies_v0::PrivateKey, ecies_v0::PublicKey)], + keys: &[(u16, ecies::PrivateKey, ecies::PublicKey)], ) -> Party { let nodes = keys .iter() diff --git a/fastcrypto-tbls/src/tests/nodes_tests.rs b/fastcrypto-tbls/src/tests/nodes_tests.rs index c2b8d1aa2d..4627a7507d 100644 --- a/fastcrypto-tbls/src/tests/nodes_tests.rs +++ b/fastcrypto-tbls/src/tests/nodes_tests.rs @@ -1,7 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::ecies_v0; +use crate::ecies; use crate::nodes::{Node, Nodes}; use fastcrypto::groups::bls12381::G2Element; use fastcrypto::groups::ristretto255::RistrettoPoint; @@ -17,8 +17,8 @@ where G: GroupElement + Serialize + DeserializeOwned, G::ScalarType: FiatShamirChallenge, { - let sk = ecies_v0::PrivateKey::::new(&mut thread_rng()); - let pk = ecies_v0::PublicKey::::from_private_key(&sk); + let sk = ecies::PrivateKey::::new(&mut thread_rng()); + let pk = ecies::PublicKey::::from_private_key(&sk); (0..n) .map(|i| Node { id: i, diff --git a/fastcrypto-tbls/src/types.rs b/fastcrypto-tbls/src/types.rs index 68ed9d39cf..7c74b5c4ad 100644 --- a/fastcrypto-tbls/src/types.rs +++ b/fastcrypto-tbls/src/types.rs @@ -2,9 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::polynomial::{Eval, PublicPoly}; -use crate::{ecies_v0, tbls}; +use crate::tbls; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; -use fastcrypto::groups::ristretto255::RistrettoPoint; use fastcrypto::groups::{bls12381, GroupElement, HashToGroupElement, Pairing}; use serde::{Deserialize, Serialize}; use std::num::NonZeroU16; @@ -84,10 +83,3 @@ impl UnindexedValues { Ok(values) } } - -/// ECIES related types with Ristretto points. -/// -pub type PrivateEciesKey = ecies_v0::PrivateKey; -pub type PublicEciesKey = ecies_v0::PublicKey; -pub type EciesEncryption = ecies_v0::Encryption; -pub type RecoveryPackage = ecies_v0::RecoveryPackage;