mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +00:00
Smash dkg into dkg, dkg-[recovery, promote, musig, pedpop]
promote and pedpop require dleq, which don't support no-std. All three should be moved outside the Serai repository, per #597, as none are planned for use and worth covering under our BBP.
This commit is contained in:
@@ -1,506 +0,0 @@
|
||||
use core::{ops::Deref, fmt};
|
||||
use std::{io, collections::HashMap};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use chacha20::{
|
||||
cipher::{crypto_common::KeyIvInit, StreamCipher},
|
||||
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
|
||||
};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
#[cfg(test)]
|
||||
use ciphersuite::group::ff::Field;
|
||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||
use multiexp::BatchVerifier;
|
||||
|
||||
use schnorr::SchnorrSignature;
|
||||
use dleq::DLEqProof;
|
||||
|
||||
use crate::{Participant, ThresholdParams};
|
||||
|
||||
mod sealed {
|
||||
use super::*;
|
||||
|
||||
pub trait ReadWrite: Sized {
|
||||
fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self>;
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()>;
|
||||
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Message: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite {}
|
||||
impl<M: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite> Message for M {}
|
||||
|
||||
pub trait Encryptable: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite {}
|
||||
impl<E: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
|
||||
}
|
||||
pub(crate) use sealed::*;
|
||||
|
||||
/// Wraps a message with a key to use for encryption in the future.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
|
||||
msg: M,
|
||||
enc_key: C::G,
|
||||
}
|
||||
|
||||
// Doesn't impl ReadWrite so that doesn't need to be imported
|
||||
impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
|
||||
pub fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
||||
Ok(Self { msg: M::read(reader, params)?, enc_key: C::read_G(reader)? })
|
||||
}
|
||||
|
||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
self.msg.write(writer)?;
|
||||
writer.write_all(self.enc_key.to_bytes().as_ref())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "tests"))]
|
||||
pub(crate) fn enc_key(&self) -> C::G {
|
||||
self.enc_key
|
||||
}
|
||||
}
|
||||
|
||||
/// An encrypted message, with a per-message encryption key enabling revealing specific messages
|
||||
/// without side effects.
|
||||
#[derive(Clone, Zeroize)]
|
||||
pub struct EncryptedMessage<C: Ciphersuite, E: Encryptable> {
|
||||
key: C::G,
|
||||
// Also include a proof-of-possession for the key.
|
||||
// If this proof-of-possession wasn't here, Eve could observe Alice encrypt to Bob with key X,
|
||||
// then send Bob a message also claiming to use X.
|
||||
// While Eve's message would fail to meaningfully decrypt, Bob would then use this to create a
|
||||
// blame argument against Eve. When they do, they'd reveal bX, revealing Alice's message to Bob.
|
||||
// This is a massive side effect which could break some protocols, in the worst case.
|
||||
// While Eve can still reuse their own keys, causing Bob to leak all messages by revealing for
|
||||
// any single one, that's effectively Eve revealing themselves, and not considered relevant.
|
||||
pop: SchnorrSignature<C>,
|
||||
msg: Zeroizing<E>,
|
||||
}
|
||||
|
||||
fn ecdh<C: Ciphersuite>(private: &Zeroizing<C::F>, public: C::G) -> Zeroizing<C::G> {
|
||||
Zeroizing::new(public * private.deref())
|
||||
}
|
||||
|
||||
// Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be
|
||||
// leaked.
|
||||
fn cipher<C: Ciphersuite>(context: [u8; 32], ecdh: &Zeroizing<C::G>) -> ChaCha20 {
|
||||
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
|
||||
// TODO: https://github.com/serai-dex/serai/issues/151
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2");
|
||||
transcript.append_message(b"context", context);
|
||||
|
||||
transcript.domain_separate(b"encryption_key");
|
||||
|
||||
let mut ecdh = ecdh.to_bytes();
|
||||
transcript.append_message(b"shared_key", ecdh.as_ref());
|
||||
ecdh.as_mut().zeroize();
|
||||
|
||||
let zeroize = |buf: &mut [u8]| buf.zeroize();
|
||||
|
||||
let mut key = Cc20Key::default();
|
||||
let mut challenge = transcript.challenge(b"key");
|
||||
key.copy_from_slice(&challenge[.. 32]);
|
||||
zeroize(challenge.as_mut());
|
||||
|
||||
// Since the key is single-use, it doesn't matter what we use for the IV
|
||||
// The issue is key + IV reuse. If we never reuse the key, we can't have the opportunity to
|
||||
// reuse a nonce
|
||||
// Use a static IV in acknowledgement of this
|
||||
let mut iv = Cc20Iv::default();
|
||||
// The \0 is to satisfy the length requirement (12), not to be null terminated
|
||||
iv.copy_from_slice(b"DKG IV v0.2\0");
|
||||
|
||||
// ChaCha20 has the same commentary as the transcript regarding ZAlloc
|
||||
// TODO: https://github.com/serai-dex/serai/issues/151
|
||||
let res = ChaCha20::new(&key, &iv);
|
||||
zeroize(key.as_mut());
|
||||
res
|
||||
}
|
||||
|
||||
fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
|
||||
rng: &mut R,
|
||||
context: [u8; 32],
|
||||
from: Participant,
|
||||
to: C::G,
|
||||
mut msg: Zeroizing<E>,
|
||||
) -> EncryptedMessage<C, E> {
|
||||
/*
|
||||
The following code could be used to replace the requirement on an RNG here.
|
||||
It's just currently not an issue to require taking in an RNG here.
|
||||
let last = self.last_enc_key.to_bytes();
|
||||
self.last_enc_key = C::hash_to_F(b"encryption_base", last.as_ref());
|
||||
let key = C::hash_to_F(b"encryption_key", last.as_ref());
|
||||
last.as_mut().zeroize();
|
||||
*/
|
||||
|
||||
// Generate a new key for this message, satisfying cipher's requirement of distinct keys per
|
||||
// message, and enabling revealing this message without revealing any others
|
||||
let key = Zeroizing::new(C::random_nonzero_F(rng));
|
||||
cipher::<C>(context, &ecdh::<C>(&key, to)).apply_keystream(msg.as_mut().as_mut());
|
||||
|
||||
let pub_key = C::generator() * key.deref();
|
||||
let nonce = Zeroizing::new(C::random_nonzero_F(rng));
|
||||
let pub_nonce = C::generator() * nonce.deref();
|
||||
EncryptedMessage {
|
||||
key: pub_key,
|
||||
pop: SchnorrSignature::sign(
|
||||
&key,
|
||||
nonce,
|
||||
pop_challenge::<C>(context, pub_nonce, pub_key, from, msg.deref().as_ref()),
|
||||
),
|
||||
msg,
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
|
||||
pub fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
key: C::read_G(reader)?,
|
||||
pop: SchnorrSignature::<C>::read(reader)?,
|
||||
msg: Zeroizing::new(E::read(reader, params)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(self.key.to_bytes().as_ref())?;
|
||||
self.pop.write(writer)?;
|
||||
self.msg.write(writer)
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn invalidate_pop(&mut self) {
|
||||
self.pop.s += C::F::ONE;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn invalidate_msg<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
context: [u8; 32],
|
||||
from: Participant,
|
||||
) {
|
||||
// Invalidate the message by specifying a new key/Schnorr PoP
|
||||
// This will cause all initial checks to pass, yet a decrypt to gibberish
|
||||
let key = Zeroizing::new(C::random_nonzero_F(rng));
|
||||
let pub_key = C::generator() * key.deref();
|
||||
let nonce = Zeroizing::new(C::random_nonzero_F(rng));
|
||||
let pub_nonce = C::generator() * nonce.deref();
|
||||
self.key = pub_key;
|
||||
self.pop = SchnorrSignature::sign(
|
||||
&key,
|
||||
nonce,
|
||||
pop_challenge::<C>(context, pub_nonce, pub_key, from, self.msg.deref().as_ref()),
|
||||
);
|
||||
}
|
||||
|
||||
// Assumes the encrypted message is a secret share.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn invalidate_share_serialization<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
context: [u8; 32],
|
||||
from: Participant,
|
||||
to: C::G,
|
||||
) {
|
||||
use ciphersuite::group::ff::PrimeField;
|
||||
|
||||
let mut repr = <C::F as PrimeField>::Repr::default();
|
||||
for b in repr.as_mut() {
|
||||
*b = 255;
|
||||
}
|
||||
// Tries to guarantee the above assumption.
|
||||
assert_eq!(repr.as_ref().len(), self.msg.as_ref().len());
|
||||
// Checks that this isn't over a field where this is somehow valid
|
||||
assert!(!bool::from(C::F::from_repr(repr).is_some()));
|
||||
|
||||
self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref());
|
||||
*self = encrypt(rng, context, from, to, self.msg.clone());
|
||||
}
|
||||
|
||||
// Assumes the encrypted message is a secret share.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn invalidate_share_value<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
context: [u8; 32],
|
||||
from: Participant,
|
||||
to: C::G,
|
||||
) {
|
||||
use ciphersuite::group::ff::PrimeField;
|
||||
|
||||
// Assumes the share isn't randomly 1
|
||||
let repr = C::F::ONE.to_repr();
|
||||
self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref());
|
||||
*self = encrypt(rng, context, from, to, self.msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// A proof that the provided encryption key is a legitimately derived shared key for some message.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct EncryptionKeyProof<C: Ciphersuite> {
|
||||
key: Zeroizing<C::G>,
|
||||
dleq: DLEqProof<C::G>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> EncryptionKeyProof<C> {
|
||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
Ok(Self { key: Zeroizing::new(C::read_G(reader)?), dleq: DLEqProof::read(reader)? })
|
||||
}
|
||||
|
||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(self.key.to_bytes().as_ref())?;
|
||||
self.dleq.write(writer)
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn invalidate_key(&mut self) {
|
||||
*self.key += C::generator();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn invalidate_dleq(&mut self) {
|
||||
let mut buf = vec![];
|
||||
self.dleq.write(&mut buf).unwrap();
|
||||
// Adds one to c since this is serialized c, s
|
||||
// Adding one to c will leave a validly serialized c
|
||||
// Adding one to s may leave an invalidly serialized s
|
||||
buf[0] = buf[0].wrapping_add(1);
|
||||
self.dleq = DLEqProof::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// This doesn't need to take the msg. It just doesn't hurt as an extra layer.
|
||||
// This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no
|
||||
// root of trust other than their existence in the assumed-to-exist external authenticated channel.
|
||||
fn pop_challenge<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
nonce: C::G,
|
||||
key: C::G,
|
||||
sender: Participant,
|
||||
msg: &[u8],
|
||||
) -> C::F {
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2");
|
||||
transcript.append_message(b"context", context);
|
||||
|
||||
transcript.domain_separate(b"proof_of_possession");
|
||||
|
||||
transcript.append_message(b"nonce", nonce.to_bytes());
|
||||
transcript.append_message(b"key", key.to_bytes());
|
||||
// This is sufficient to prevent the attack this is meant to stop
|
||||
transcript.append_message(b"sender", sender.to_bytes());
|
||||
// This, as written above, doesn't hurt
|
||||
transcript.append_message(b"message", msg);
|
||||
// While this is a PoK and a PoP, it's called a PoP here since the important part is its owner
|
||||
// Elsewhere, where we use the term PoK, the important part is that it isn't some inverse, with
|
||||
// an unknown to anyone discrete log, breaking the system
|
||||
C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr"))
|
||||
}
|
||||
|
||||
fn encryption_key_transcript(context: [u8; 32]) -> RecommendedTranscript {
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2");
|
||||
transcript.append_message(b"context", context);
|
||||
transcript
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
||||
pub(crate) enum DecryptionError {
|
||||
#[error("accused provided an invalid signature")]
|
||||
InvalidSignature,
|
||||
#[error("accuser provided an invalid decryption key")]
|
||||
InvalidProof,
|
||||
}
|
||||
|
||||
// A simple box for managing decryption.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Decryption<C: Ciphersuite> {
|
||||
context: [u8; 32],
|
||||
enc_keys: HashMap<Participant, C::G>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Decryption<C> {
|
||||
pub(crate) fn new(context: [u8; 32]) -> Self {
|
||||
Self { context, enc_keys: HashMap::new() }
|
||||
}
|
||||
pub(crate) fn register<M: Message>(
|
||||
&mut self,
|
||||
participant: Participant,
|
||||
msg: EncryptionKeyMessage<C, M>,
|
||||
) -> M {
|
||||
assert!(
|
||||
!self.enc_keys.contains_key(&participant),
|
||||
"Re-registering encryption key for a participant"
|
||||
);
|
||||
self.enc_keys.insert(participant, msg.enc_key);
|
||||
msg.msg
|
||||
}
|
||||
|
||||
// Given a message, and the intended decryptor, and a proof for its key, decrypt the message.
|
||||
// Returns None if the key was wrong.
|
||||
pub(crate) fn decrypt_with_proof<E: Encryptable>(
|
||||
&self,
|
||||
from: Participant,
|
||||
decryptor: Participant,
|
||||
mut msg: EncryptedMessage<C, E>,
|
||||
// There's no encryption key proof if the accusation is of an invalid signature
|
||||
proof: Option<EncryptionKeyProof<C>>,
|
||||
) -> Result<Zeroizing<E>, DecryptionError> {
|
||||
if !msg.pop.verify(
|
||||
msg.key,
|
||||
pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
||||
) {
|
||||
Err(DecryptionError::InvalidSignature)?;
|
||||
}
|
||||
|
||||
if let Some(proof) = proof {
|
||||
// Verify this is the decryption key for this message
|
||||
proof
|
||||
.dleq
|
||||
.verify(
|
||||
&mut encryption_key_transcript(self.context),
|
||||
&[C::generator(), msg.key],
|
||||
&[self.enc_keys[&decryptor], *proof.key],
|
||||
)
|
||||
.map_err(|_| DecryptionError::InvalidProof)?;
|
||||
|
||||
cipher::<C>(self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
|
||||
Ok(msg.msg)
|
||||
} else {
|
||||
Err(DecryptionError::InvalidProof)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A simple box for managing encryption.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Encryption<C: Ciphersuite> {
|
||||
context: [u8; 32],
|
||||
i: Participant,
|
||||
enc_key: Zeroizing<C::F>,
|
||||
enc_pub_key: C::G,
|
||||
decryption: Decryption<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> fmt::Debug for Encryption<C> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt
|
||||
.debug_struct("Encryption")
|
||||
.field("context", &self.context)
|
||||
.field("i", &self.i)
|
||||
.field("enc_pub_key", &self.enc_pub_key)
|
||||
.field("decryption", &self.decryption)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Zeroize for Encryption<C> {
|
||||
fn zeroize(&mut self) {
|
||||
self.enc_key.zeroize();
|
||||
self.enc_pub_key.zeroize();
|
||||
for (_, mut value) in self.decryption.enc_keys.drain() {
|
||||
value.zeroize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Encryption<C> {
|
||||
pub(crate) fn new<R: RngCore + CryptoRng>(
|
||||
context: [u8; 32],
|
||||
i: Participant,
|
||||
rng: &mut R,
|
||||
) -> Self {
|
||||
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
|
||||
Self {
|
||||
context,
|
||||
i,
|
||||
enc_pub_key: C::generator() * enc_key.deref(),
|
||||
enc_key,
|
||||
decryption: Decryption::new(context),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
|
||||
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
|
||||
}
|
||||
|
||||
pub(crate) fn register<M: Message>(
|
||||
&mut self,
|
||||
participant: Participant,
|
||||
msg: EncryptionKeyMessage<C, M>,
|
||||
) -> M {
|
||||
self.decryption.register(participant, msg)
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
participant: Participant,
|
||||
msg: Zeroizing<E>,
|
||||
) -> EncryptedMessage<C, E> {
|
||||
encrypt(rng, self.context, self.i, self.decryption.enc_keys[&participant], msg)
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
batch: &mut BatchVerifier<I, C::G>,
|
||||
// Uses a distinct batch ID so if this batch verifier is reused, we know its the PoP aspect
|
||||
// which failed, and therefore to use None for the blame
|
||||
batch_id: I,
|
||||
from: Participant,
|
||||
mut msg: EncryptedMessage<C, E>,
|
||||
) -> (Zeroizing<E>, EncryptionKeyProof<C>) {
|
||||
msg.pop.batch_verify(
|
||||
rng,
|
||||
batch,
|
||||
batch_id,
|
||||
msg.key,
|
||||
pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
||||
);
|
||||
|
||||
let key = ecdh::<C>(&self.enc_key, msg.key);
|
||||
cipher::<C>(self.context, &key).apply_keystream(msg.msg.as_mut().as_mut());
|
||||
(
|
||||
msg.msg,
|
||||
EncryptionKeyProof {
|
||||
key,
|
||||
dleq: DLEqProof::prove(
|
||||
rng,
|
||||
&mut encryption_key_transcript(self.context),
|
||||
&[C::generator(), msg.key],
|
||||
&self.enc_key,
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn into_decryption(self) -> Decryption<C> {
|
||||
self.decryption
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,129 +0,0 @@
|
||||
#[cfg(feature = "std")]
|
||||
use core::ops::Deref;
|
||||
use std_shims::{vec, vec::Vec, collections::HashSet};
|
||||
#[cfg(feature = "std")]
|
||||
use std_shims::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use ciphersuite::{
|
||||
group::{Group, GroupEncoding},
|
||||
Ciphersuite,
|
||||
};
|
||||
|
||||
use crate::DkgError;
|
||||
#[cfg(feature = "std")]
|
||||
use crate::{Participant, ThresholdParams, Interpolation, ThresholdCore};
|
||||
|
||||
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, DkgError<()>> {
|
||||
if keys.is_empty() {
|
||||
Err(DkgError::InvalidSigningSet)?;
|
||||
}
|
||||
// Too many signers
|
||||
let keys_len = u16::try_from(keys.len()).map_err(|_| DkgError::InvalidSigningSet)?;
|
||||
|
||||
// Duplicated public keys
|
||||
if keys.iter().map(|key| key.to_bytes().as_ref().to_vec()).collect::<HashSet<_>>().len() !=
|
||||
keys.len()
|
||||
{
|
||||
Err(DkgError::InvalidSigningSet)?;
|
||||
}
|
||||
|
||||
Ok(keys_len)
|
||||
}
|
||||
|
||||
// This function panics if called with keys whose length exceed 2**16.
|
||||
// This is fine since it's internal and all calls occur after calling check_keys, which does check
|
||||
// the keys' length.
|
||||
fn binding_factor_transcript<C: Ciphersuite>(
|
||||
context: &[u8],
|
||||
keys: &[C::G],
|
||||
) -> Result<Vec<u8>, DkgError<()>> {
|
||||
let mut transcript = vec![];
|
||||
transcript.push(u8::try_from(context.len()).map_err(|_| DkgError::InvalidSigningSet)?);
|
||||
transcript.extend(context);
|
||||
transcript.extend(u16::try_from(keys.len()).unwrap().to_le_bytes());
|
||||
for key in keys {
|
||||
transcript.extend(key.to_bytes().as_ref());
|
||||
}
|
||||
Ok(transcript)
|
||||
}
|
||||
|
||||
fn binding_factor<C: Ciphersuite>(mut transcript: Vec<u8>, i: u16) -> C::F {
|
||||
transcript.extend(i.to_le_bytes());
|
||||
C::hash_to_F(b"musig", &transcript)
|
||||
}
|
||||
|
||||
/// The group key resulting from using this library's MuSig key gen.
|
||||
///
|
||||
/// This function will return an error if the context is longer than 255 bytes.
|
||||
///
|
||||
/// Creating an aggregate key with a list containing duplicated public keys will return an error.
|
||||
pub fn musig_key<C: Ciphersuite>(context: &[u8], keys: &[C::G]) -> Result<C::G, DkgError<()>> {
|
||||
let keys_len = check_keys::<C>(keys)?;
|
||||
let transcript = binding_factor_transcript::<C>(context, keys)?;
|
||||
let mut res = C::G::identity();
|
||||
for i in 1 ..= keys_len {
|
||||
// TODO: Calculate this with a multiexp
|
||||
res += keys[usize::from(i - 1)] * binding_factor::<C>(transcript.clone(), i);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key.
|
||||
///
|
||||
/// Creating an aggregate key with a list containing duplicated public keys returns an error.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn musig<C: Ciphersuite>(
|
||||
context: &[u8],
|
||||
private_key: &Zeroizing<C::F>,
|
||||
keys: &[C::G],
|
||||
) -> Result<ThresholdCore<C>, DkgError<()>> {
|
||||
let keys_len = check_keys::<C>(keys)?;
|
||||
|
||||
let our_pub_key = C::generator() * private_key.deref();
|
||||
let Some(pos) = keys.iter().position(|key| *key == our_pub_key) else {
|
||||
// Not present in signing set
|
||||
Err(DkgError::InvalidSigningSet)?
|
||||
};
|
||||
let params = ThresholdParams::new(
|
||||
keys_len,
|
||||
keys_len,
|
||||
// These errors shouldn't be possible, as pos is bounded to len - 1
|
||||
// Since len is prior guaranteed to be within u16::MAX, pos + 1 must also be
|
||||
Participant::new((pos + 1).try_into().map_err(|_| DkgError::InvalidSigningSet)?)
|
||||
.ok_or(DkgError::InvalidSigningSet)?,
|
||||
)?;
|
||||
|
||||
// Calculate the binding factor per-key
|
||||
let transcript = binding_factor_transcript::<C>(context, keys)?;
|
||||
let mut binding = Vec::with_capacity(keys.len());
|
||||
for i in 1 ..= keys_len {
|
||||
binding.push(binding_factor::<C>(transcript.clone(), i));
|
||||
}
|
||||
|
||||
// Our secret share is our private key
|
||||
let secret_share = private_key.clone();
|
||||
|
||||
// Calculate verification shares
|
||||
let mut verification_shares = HashMap::new();
|
||||
let mut group_key = C::G::identity();
|
||||
for l in 1 ..= keys_len {
|
||||
let key = keys[usize::from(l) - 1];
|
||||
// TODO: Use a multiexp for this
|
||||
group_key += key * binding[usize::from(l - 1)];
|
||||
|
||||
// These errors also shouldn't be possible, for the same reasons as documented above
|
||||
verification_shares.insert(Participant::new(l).ok_or(DkgError::InvalidSigningSet)?, key);
|
||||
}
|
||||
debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[¶ms.i()]);
|
||||
debug_assert_eq!(musig_key::<C>(context, keys).unwrap(), group_key);
|
||||
|
||||
Ok(ThresholdCore::new(
|
||||
params,
|
||||
Interpolation::Constant(binding),
|
||||
secret_share,
|
||||
verification_shares,
|
||||
))
|
||||
}
|
||||
@@ -1,635 +0,0 @@
|
||||
use core::{marker::PhantomData, ops::Deref, fmt};
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use ciphersuite::{
|
||||
group::{
|
||||
ff::{Field, PrimeField},
|
||||
Group, GroupEncoding,
|
||||
},
|
||||
Ciphersuite,
|
||||
};
|
||||
use multiexp::{multiexp_vartime, BatchVerifier};
|
||||
|
||||
use schnorr::SchnorrSignature;
|
||||
|
||||
use crate::{
|
||||
Participant, DkgError, ThresholdParams, Interpolation, ThresholdCore, validate_map,
|
||||
encryption::{
|
||||
ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, Decryption, EncryptionKeyProof,
|
||||
DecryptionError,
|
||||
},
|
||||
};
|
||||
|
||||
type FrostError<C> = DkgError<EncryptionKeyProof<C>>;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn challenge<C: Ciphersuite>(context: [u8; 32], l: Participant, R: &[u8], Am: &[u8]) -> C::F {
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2");
|
||||
transcript.domain_separate(b"schnorr_proof_of_knowledge");
|
||||
transcript.append_message(b"context", context);
|
||||
transcript.append_message(b"participant", l.to_bytes());
|
||||
transcript.append_message(b"nonce", R);
|
||||
transcript.append_message(b"commitments", Am);
|
||||
C::hash_to_F(b"DKG-FROST-proof_of_knowledge-0", &transcript.challenge(b"schnorr"))
|
||||
}
|
||||
|
||||
/// The commitments message, intended to be broadcast to all other parties.
|
||||
///
|
||||
/// Every participant should only provide one set of commitments to all parties. If any
|
||||
/// participant sends multiple sets of commitments, they are faulty and should be presumed
|
||||
/// malicious. As this library does not handle networking, it is unable to detect if any
|
||||
/// participant is so faulty. That responsibility lies with the caller.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Commitments<C: Ciphersuite> {
|
||||
commitments: Vec<C::G>,
|
||||
cached_msg: Vec<u8>,
|
||||
sig: SchnorrSignature<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> ReadWrite for Commitments<C> {
|
||||
fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
||||
let mut commitments = Vec::with_capacity(params.t().into());
|
||||
let mut cached_msg = vec![];
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let mut read_G = || -> io::Result<C::G> {
|
||||
let mut buf = <C::G as GroupEncoding>::Repr::default();
|
||||
reader.read_exact(buf.as_mut())?;
|
||||
let point = C::read_G(&mut buf.as_ref())?;
|
||||
cached_msg.extend(buf.as_ref());
|
||||
Ok(point)
|
||||
};
|
||||
|
||||
for _ in 0 .. params.t() {
|
||||
commitments.push(read_G()?);
|
||||
}
|
||||
|
||||
Ok(Commitments { commitments, cached_msg, sig: SchnorrSignature::read(reader)? })
|
||||
}
|
||||
|
||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(&self.cached_msg)?;
|
||||
self.sig.write(writer)
|
||||
}
|
||||
}
|
||||
|
||||
/// State machine to begin the key generation protocol.
|
||||
#[derive(Debug, Zeroize)]
|
||||
pub struct KeyGenMachine<C: Ciphersuite> {
|
||||
params: ThresholdParams,
|
||||
context: [u8; 32],
|
||||
_curve: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> KeyGenMachine<C> {
|
||||
/// Create a new machine to generate a key.
|
||||
///
|
||||
/// The context should be unique among multisigs.
|
||||
pub fn new(params: ThresholdParams, context: [u8; 32]) -> KeyGenMachine<C> {
|
||||
KeyGenMachine { params, context, _curve: PhantomData }
|
||||
}
|
||||
|
||||
/// Start generating a key according to the FROST DKG spec.
|
||||
///
|
||||
/// Returns a commitments message to be sent to all parties over an authenticated channel. If any
|
||||
/// party submits multiple sets of commitments, they MUST be treated as malicious.
|
||||
pub fn generate_coefficients<R: RngCore + CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> (SecretShareMachine<C>, EncryptionKeyMessage<C, Commitments<C>>) {
|
||||
let t = usize::from(self.params.t);
|
||||
let mut coefficients = Vec::with_capacity(t);
|
||||
let mut commitments = Vec::with_capacity(t);
|
||||
let mut cached_msg = vec![];
|
||||
|
||||
for i in 0 .. t {
|
||||
// Step 1: Generate t random values to form a polynomial with
|
||||
coefficients.push(Zeroizing::new(C::random_nonzero_F(&mut *rng)));
|
||||
// Step 3: Generate public commitments
|
||||
commitments.push(C::generator() * coefficients[i].deref());
|
||||
cached_msg.extend(commitments[i].to_bytes().as_ref());
|
||||
}
|
||||
|
||||
// Step 2: Provide a proof of knowledge
|
||||
let r = Zeroizing::new(C::random_nonzero_F(rng));
|
||||
let nonce = C::generator() * r.deref();
|
||||
let sig = SchnorrSignature::<C>::sign(
|
||||
&coefficients[0],
|
||||
// This could be deterministic as the PoK is a singleton never opened up to cooperative
|
||||
// discussion
|
||||
// There's no reason to spend the time and effort to make this deterministic besides a
|
||||
// general obsession with canonicity and determinism though
|
||||
r,
|
||||
challenge::<C>(self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg),
|
||||
);
|
||||
|
||||
// Additionally create an encryption mechanism to protect the secret shares
|
||||
let encryption = Encryption::new(self.context, self.params.i, rng);
|
||||
|
||||
// Step 4: Broadcast
|
||||
let msg =
|
||||
encryption.registration(Commitments { commitments: commitments.clone(), cached_msg, sig });
|
||||
(
|
||||
SecretShareMachine {
|
||||
params: self.params,
|
||||
context: self.context,
|
||||
coefficients,
|
||||
our_commitments: commitments,
|
||||
encryption,
|
||||
},
|
||||
msg,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn polynomial<F: PrimeField + Zeroize>(
|
||||
coefficients: &[Zeroizing<F>],
|
||||
l: Participant,
|
||||
) -> Zeroizing<F> {
|
||||
let l = F::from(u64::from(u16::from(l)));
|
||||
// This should never be reached since Participant is explicitly non-zero
|
||||
assert!(l != F::ZERO, "zero participant passed to polynomial");
|
||||
let mut share = Zeroizing::new(F::ZERO);
|
||||
for (idx, coefficient) in coefficients.iter().rev().enumerate() {
|
||||
*share += coefficient.deref();
|
||||
if idx != (coefficients.len() - 1) {
|
||||
*share *= l;
|
||||
}
|
||||
}
|
||||
share
|
||||
}
|
||||
|
||||
/// The secret share message, to be sent to the party it's intended for over an authenticated
|
||||
/// channel.
|
||||
///
|
||||
/// If any participant sends multiple secret shares to another participant, they are faulty.
|
||||
// This should presumably be written as SecretShare(Zeroizing<F::Repr>).
|
||||
// It's unfortunately not possible as F::Repr doesn't have Zeroize as a bound.
|
||||
// The encryption system also explicitly uses Zeroizing<M> so it can ensure anything being
|
||||
// encrypted is within Zeroizing. Accordingly, internally having Zeroizing would be redundant.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct SecretShare<F: PrimeField>(F::Repr);
|
||||
impl<F: PrimeField> AsRef<[u8]> for SecretShare<F> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> AsMut<[u8]> for SecretShare<F> {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> fmt::Debug for SecretShare<F> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt.debug_struct("SecretShare").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> Zeroize for SecretShare<F> {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.as_mut().zeroize()
|
||||
}
|
||||
}
|
||||
// Still manually implement ZeroizeOnDrop to ensure these don't stick around.
|
||||
// We could replace Zeroizing<M> with a bound M: ZeroizeOnDrop.
|
||||
// Doing so would potentially fail to highlight the expected behavior with these and remove a layer
|
||||
// of depth.
|
||||
impl<F: PrimeField> Drop for SecretShare<F> {
|
||||
fn drop(&mut self) {
|
||||
self.zeroize();
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> ZeroizeOnDrop for SecretShare<F> {}
|
||||
|
||||
impl<F: PrimeField> ReadWrite for SecretShare<F> {
|
||||
fn read<R: Read>(reader: &mut R, _: ThresholdParams) -> io::Result<Self> {
|
||||
let mut repr = F::Repr::default();
|
||||
reader.read_exact(repr.as_mut())?;
|
||||
Ok(SecretShare(repr))
|
||||
}
|
||||
|
||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(self.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Advancement of the key generation state machine.
|
||||
#[derive(Zeroize)]
|
||||
pub struct SecretShareMachine<C: Ciphersuite> {
|
||||
params: ThresholdParams,
|
||||
context: [u8; 32],
|
||||
coefficients: Vec<Zeroizing<C::F>>,
|
||||
our_commitments: Vec<C::G>,
|
||||
encryption: Encryption<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> fmt::Debug for SecretShareMachine<C> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt
|
||||
.debug_struct("SecretShareMachine")
|
||||
.field("params", &self.params)
|
||||
.field("context", &self.context)
|
||||
.field("our_commitments", &self.our_commitments)
|
||||
.field("encryption", &self.encryption)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||
/// Verify the data from the previous round (canonicity, PoKs, message authenticity)
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn verify_r1<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
) -> Result<HashMap<Participant, Vec<C::G>>, FrostError<C>> {
|
||||
validate_map(
|
||||
&commitment_msgs,
|
||||
&(1 ..= self.params.n()).map(Participant).collect::<Vec<_>>(),
|
||||
self.params.i(),
|
||||
)?;
|
||||
|
||||
let mut batch = BatchVerifier::<Participant, C::G>::new(commitment_msgs.len());
|
||||
let mut commitments = HashMap::new();
|
||||
for l in (1 ..= self.params.n()).map(Participant) {
|
||||
let Some(msg) = commitment_msgs.remove(&l) else { continue };
|
||||
let mut msg = self.encryption.register(l, msg);
|
||||
|
||||
if msg.commitments.len() != self.params.t().into() {
|
||||
Err(FrostError::InvalidCommitments(l))?;
|
||||
}
|
||||
|
||||
// Step 5: Validate each proof of knowledge
|
||||
// This is solely the prep step for the latter batch verification
|
||||
msg.sig.batch_verify(
|
||||
rng,
|
||||
&mut batch,
|
||||
l,
|
||||
msg.commitments[0],
|
||||
challenge::<C>(self.context, l, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg),
|
||||
);
|
||||
|
||||
commitments.insert(l, msg.commitments.drain(..).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
batch.verify_vartime_with_vartime_blame().map_err(FrostError::InvalidCommitments)?;
|
||||
|
||||
commitments.insert(self.params.i, self.our_commitments.drain(..).collect());
|
||||
Ok(commitments)
|
||||
}
|
||||
|
||||
/// Continue generating a key.
|
||||
///
|
||||
/// Takes in everyone else's commitments. Returns a HashMap of encrypted secret shares to be sent
|
||||
/// over authenticated channels to their relevant counterparties.
|
||||
///
|
||||
/// If any participant sends multiple secret shares to another participant, they are faulty.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
rng: &mut R,
|
||||
commitments: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
) -> Result<
|
||||
(KeyMachine<C>, HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>),
|
||||
FrostError<C>,
|
||||
> {
|
||||
let commitments = self.verify_r1(&mut *rng, commitments)?;
|
||||
|
||||
// Step 1: Generate secret shares for all other parties
|
||||
let mut res = HashMap::new();
|
||||
for l in (1 ..= self.params.n()).map(Participant) {
|
||||
// Don't insert our own shares to the byte buffer which is meant to be sent around
|
||||
// An app developer could accidentally send it. Best to keep this black boxed
|
||||
if l == self.params.i() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut share = polynomial(&self.coefficients, l);
|
||||
let share_bytes = Zeroizing::new(SecretShare::<C::F>(share.to_repr()));
|
||||
share.zeroize();
|
||||
res.insert(l, self.encryption.encrypt(rng, l, share_bytes));
|
||||
}
|
||||
|
||||
// Calculate our own share
|
||||
let share = polynomial(&self.coefficients, self.params.i());
|
||||
self.coefficients.zeroize();
|
||||
|
||||
Ok((
|
||||
KeyMachine { params: self.params, secret: share, commitments, encryption: self.encryption },
|
||||
res,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Advancement of the the secret share state machine.
|
||||
///
|
||||
/// This machine will 'complete' the protocol, by a local perspective. In order to be secure,
|
||||
/// the parties must confirm having successfully completed the protocol (an effort out of scope to
|
||||
/// this library), yet this is modeled by one more state transition (BlameMachine).
|
||||
pub struct KeyMachine<C: Ciphersuite> {
|
||||
params: ThresholdParams,
|
||||
secret: Zeroizing<C::F>,
|
||||
commitments: HashMap<Participant, Vec<C::G>>,
|
||||
encryption: Encryption<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> fmt::Debug for KeyMachine<C> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt
|
||||
.debug_struct("KeyMachine")
|
||||
.field("params", &self.params)
|
||||
.field("commitments", &self.commitments)
|
||||
.field("encryption", &self.encryption)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Zeroize for KeyMachine<C> {
|
||||
fn zeroize(&mut self) {
|
||||
self.params.zeroize();
|
||||
self.secret.zeroize();
|
||||
for commitments in self.commitments.values_mut() {
|
||||
commitments.zeroize();
|
||||
}
|
||||
self.encryption.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the exponent for a given participant and apply it to a series of commitments
|
||||
// Initially used with the actual commitments to verify the secret share, later used with
|
||||
// stripes to generate the verification shares
|
||||
fn exponential<C: Ciphersuite>(i: Participant, values: &[C::G]) -> Vec<(C::F, C::G)> {
|
||||
let i = C::F::from(u16::from(i).into());
|
||||
let mut res = Vec::with_capacity(values.len());
|
||||
(0 .. values.len()).fold(C::F::ONE, |exp, l| {
|
||||
res.push((exp, values[l]));
|
||||
exp * i
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
fn share_verification_statements<C: Ciphersuite>(
|
||||
target: Participant,
|
||||
commitments: &[C::G],
|
||||
mut share: Zeroizing<C::F>,
|
||||
) -> Vec<(C::F, C::G)> {
|
||||
// This can be insecurely linearized from n * t to just n using the below sums for a given
|
||||
// stripe. Doing so uses naive addition which is subject to malleability. The only way to
|
||||
// ensure that malleability isn't present is to use this n * t algorithm, which runs
|
||||
// per sender and not as an aggregate of all senders, which also enables blame
|
||||
let mut values = exponential::<C>(target, commitments);
|
||||
|
||||
// Perform the share multiplication outside of the multiexp to minimize stack copying
|
||||
// While the multiexp BatchVerifier does zeroize its flattened multiexp, and itself, it still
|
||||
// converts whatever we give to an iterator and then builds a Vec internally, welcoming copies
|
||||
let neg_share_pub = C::generator() * -*share;
|
||||
share.zeroize();
|
||||
values.push((C::F::ONE, neg_share_pub));
|
||||
|
||||
values
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, Debug, Zeroize)]
|
||||
enum BatchId {
|
||||
Decryption(Participant),
|
||||
Share(Participant),
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> KeyMachine<C> {
|
||||
/// Calculate our share given the shares sent to us.
|
||||
///
|
||||
/// Returns a BlameMachine usable to determine if faults in the protocol occurred.
|
||||
///
|
||||
/// This will error on, and return a blame proof for, the first-observed case of faulty behavior.
|
||||
pub fn calculate_share<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
rng: &mut R,
|
||||
mut shares: HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>,
|
||||
) -> Result<BlameMachine<C>, FrostError<C>> {
|
||||
validate_map(
|
||||
&shares,
|
||||
&(1 ..= self.params.n()).map(Participant).collect::<Vec<_>>(),
|
||||
self.params.i(),
|
||||
)?;
|
||||
|
||||
let mut batch = BatchVerifier::new(shares.len());
|
||||
let mut blames = HashMap::new();
|
||||
for (l, share_bytes) in shares.drain() {
|
||||
let (mut share_bytes, blame) =
|
||||
self.encryption.decrypt(rng, &mut batch, BatchId::Decryption(l), l, share_bytes);
|
||||
let share =
|
||||
Zeroizing::new(Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or_else(|| {
|
||||
FrostError::InvalidShare { participant: l, blame: Some(blame.clone()) }
|
||||
})?);
|
||||
share_bytes.zeroize();
|
||||
*self.secret += share.deref();
|
||||
|
||||
blames.insert(l, blame);
|
||||
batch.queue(
|
||||
rng,
|
||||
BatchId::Share(l),
|
||||
share_verification_statements::<C>(self.params.i(), &self.commitments[&l], share),
|
||||
);
|
||||
}
|
||||
batch.verify_with_vartime_blame().map_err(|id| {
|
||||
let (l, blame) = match id {
|
||||
BatchId::Decryption(l) => (l, None),
|
||||
BatchId::Share(l) => (l, Some(blames.remove(&l).unwrap())),
|
||||
};
|
||||
FrostError::InvalidShare { participant: l, blame }
|
||||
})?;
|
||||
|
||||
// Stripe commitments per t and sum them in advance. Calculating verification shares relies on
|
||||
// these sums so preprocessing them is a massive speedup
|
||||
// If these weren't just sums, yet the tables used in multiexp, this would be further optimized
|
||||
// As of right now, each multiexp will regenerate them
|
||||
let mut stripes = Vec::with_capacity(usize::from(self.params.t()));
|
||||
for t in 0 .. usize::from(self.params.t()) {
|
||||
stripes.push(self.commitments.values().map(|commitments| commitments[t]).sum());
|
||||
}
|
||||
|
||||
// Calculate each user's verification share
|
||||
let mut verification_shares = HashMap::new();
|
||||
for i in (1 ..= self.params.n()).map(Participant) {
|
||||
verification_shares.insert(
|
||||
i,
|
||||
if i == self.params.i() {
|
||||
C::generator() * self.secret.deref()
|
||||
} else {
|
||||
multiexp_vartime(&exponential::<C>(i, &stripes))
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let KeyMachine { commitments, encryption, params, secret } = self;
|
||||
Ok(BlameMachine {
|
||||
commitments,
|
||||
encryption: encryption.into_decryption(),
|
||||
result: Some(ThresholdCore {
|
||||
params,
|
||||
interpolation: Interpolation::Lagrange,
|
||||
secret_share: secret,
|
||||
group_key: stripes[0],
|
||||
verification_shares,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A machine capable of handling blame proofs.
|
||||
pub struct BlameMachine<C: Ciphersuite> {
|
||||
commitments: HashMap<Participant, Vec<C::G>>,
|
||||
encryption: Decryption<C>,
|
||||
result: Option<ThresholdCore<C>>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> fmt::Debug for BlameMachine<C> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt
|
||||
.debug_struct("BlameMachine")
|
||||
.field("commitments", &self.commitments)
|
||||
.field("encryption", &self.encryption)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Zeroize for BlameMachine<C> {
|
||||
fn zeroize(&mut self) {
|
||||
for commitments in self.commitments.values_mut() {
|
||||
commitments.zeroize();
|
||||
}
|
||||
self.result.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> BlameMachine<C> {
|
||||
/// Mark the protocol as having been successfully completed, returning the generated keys.
|
||||
/// This should only be called after having confirmed, with all participants, successful
|
||||
/// completion.
|
||||
///
|
||||
/// Confirming successful completion is not necessarily as simple as everyone reporting their
|
||||
/// completion. Everyone must also receive everyone's report of completion, entering into the
|
||||
/// territory of consensus protocols. This library does not handle that nor does it provide any
|
||||
/// tooling to do so. This function is solely intended to force users to acknowledge they're
|
||||
/// completing the protocol, not processing any blame.
|
||||
pub fn complete(self) -> ThresholdCore<C> {
|
||||
self.result.unwrap()
|
||||
}
|
||||
|
||||
fn blame_internal(
|
||||
&self,
|
||||
sender: Participant,
|
||||
recipient: Participant,
|
||||
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
||||
proof: Option<EncryptionKeyProof<C>>,
|
||||
) -> Participant {
|
||||
let share_bytes = match self.encryption.decrypt_with_proof(sender, recipient, msg, proof) {
|
||||
Ok(share_bytes) => share_bytes,
|
||||
// If there's an invalid signature, the sender did not send a properly formed message
|
||||
Err(DecryptionError::InvalidSignature) => return sender,
|
||||
// Decryption will fail if the provided ECDH key wasn't correct for the given message
|
||||
Err(DecryptionError::InvalidProof) => return recipient,
|
||||
};
|
||||
|
||||
let Some(share) = Option::<C::F>::from(C::F::from_repr(share_bytes.0)) else {
|
||||
// If this isn't a valid scalar, the sender is faulty
|
||||
return sender;
|
||||
};
|
||||
|
||||
// If this isn't a valid share, the sender is faulty
|
||||
if !bool::from(
|
||||
multiexp_vartime(&share_verification_statements::<C>(
|
||||
recipient,
|
||||
&self.commitments[&sender],
|
||||
Zeroizing::new(share),
|
||||
))
|
||||
.is_identity(),
|
||||
) {
|
||||
return sender;
|
||||
}
|
||||
|
||||
// The share was canonical and valid
|
||||
recipient
|
||||
}
|
||||
|
||||
/// Given an accusation of fault, determine the faulty party (either the sender, who sent an
|
||||
/// invalid secret share, or the receiver, who claimed a valid secret share was invalid). No
|
||||
/// matter which, prevent completion of the machine, forcing an abort of the protocol.
|
||||
///
|
||||
/// The message should be a copy of the encrypted secret share from the accused sender to the
|
||||
/// accusing recipient. This message must have been authenticated as actually having come from
|
||||
/// the sender in question.
|
||||
///
|
||||
/// In order to enable detecting multiple faults, an `AdditionalBlameMachine` is returned, which
|
||||
/// can be used to determine further blame. These machines will process the same blame statements
|
||||
/// multiple times, always identifying blame. It is the caller's job to ensure they're unique in
|
||||
/// order to prevent multiple instances of blame over a single incident.
|
||||
pub fn blame(
|
||||
self,
|
||||
sender: Participant,
|
||||
recipient: Participant,
|
||||
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
||||
proof: Option<EncryptionKeyProof<C>>,
|
||||
) -> (AdditionalBlameMachine<C>, Participant) {
|
||||
let faulty = self.blame_internal(sender, recipient, msg, proof);
|
||||
(AdditionalBlameMachine(self), faulty)
|
||||
}
|
||||
}
|
||||
|
||||
/// A machine capable of handling an arbitrary amount of additional blame proofs.
|
||||
#[derive(Debug, Zeroize)]
|
||||
pub struct AdditionalBlameMachine<C: Ciphersuite>(BlameMachine<C>);
|
||||
impl<C: Ciphersuite> AdditionalBlameMachine<C> {
|
||||
/// Create an AdditionalBlameMachine capable of evaluating Blame regardless of if the caller was
|
||||
/// a member in the DKG protocol.
|
||||
///
|
||||
/// Takes in the parameters for the DKG protocol and all of the participant's commitment
|
||||
/// messages.
|
||||
///
|
||||
/// This constructor assumes the full validity of the commitment messages. They must be fully
|
||||
/// authenticated as having come from the supposed party and verified as valid. Usage of invalid
|
||||
/// commitments is considered undefined behavior, and may cause everything from inaccurate blame
|
||||
/// to panics.
|
||||
pub fn new(
|
||||
context: [u8; 32],
|
||||
n: u16,
|
||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
) -> Result<Self, FrostError<C>> {
|
||||
let mut commitments = HashMap::new();
|
||||
let mut encryption = Decryption::new(context);
|
||||
for i in 1 ..= n {
|
||||
let i = Participant::new(i).unwrap();
|
||||
let Some(msg) = commitment_msgs.remove(&i) else { Err(DkgError::MissingParticipant(i))? };
|
||||
commitments.insert(i, encryption.register(i, msg).commitments);
|
||||
}
|
||||
Ok(AdditionalBlameMachine(BlameMachine { commitments, encryption, result: None }))
|
||||
}
|
||||
|
||||
/// Given an accusation of fault, determine the faulty party (either the sender, who sent an
|
||||
/// invalid secret share, or the receiver, who claimed a valid secret share was invalid).
|
||||
///
|
||||
/// The message should be a copy of the encrypted secret share from the accused sender to the
|
||||
/// accusing recipient. This message must have been authenticated as actually having come from
|
||||
/// the sender in question.
|
||||
///
|
||||
/// This will process the same blame statement multiple times, always identifying blame. It is
|
||||
/// the caller's job to ensure they're unique in order to prevent multiple instances of blame
|
||||
/// over a single incident.
|
||||
pub fn blame(
|
||||
&self,
|
||||
sender: Participant,
|
||||
recipient: Participant,
|
||||
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
||||
proof: Option<EncryptionKeyProof<C>>,
|
||||
) -> Participant {
|
||||
self.0.blame_internal(sender, recipient, msg, proof)
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
use core::{marker::PhantomData, ops::Deref};
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
sync::Arc,
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{
|
||||
group::{ff::Field, GroupEncoding},
|
||||
Ciphersuite,
|
||||
};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use dleq::DLEqProof;
|
||||
|
||||
use crate::{Participant, DkgError, ThresholdCore, ThresholdKeys, validate_map};
|
||||
|
||||
/// Promote a set of keys to another Ciphersuite definition.
|
||||
pub trait CiphersuitePromote<C2: Ciphersuite> {
|
||||
fn promote(self) -> ThresholdKeys<C2>;
|
||||
}
|
||||
|
||||
fn transcript<G: GroupEncoding>(key: &G, i: Participant) -> RecommendedTranscript {
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG Generator Promotion v0.2");
|
||||
transcript.append_message(b"group_key", key.to_bytes());
|
||||
transcript.append_message(b"participant", i.to_bytes());
|
||||
transcript
|
||||
}
|
||||
|
||||
/// Proof of valid promotion to another generator.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GeneratorProof<C: Ciphersuite> {
|
||||
share: C::G,
|
||||
proof: DLEqProof<C::G>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> GeneratorProof<C> {
|
||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(self.share.to_bytes().as_ref())?;
|
||||
self.proof.write(writer)
|
||||
}
|
||||
|
||||
pub fn read<R: Read>(reader: &mut R) -> io::Result<GeneratorProof<C>> {
|
||||
Ok(GeneratorProof {
|
||||
share: <C as Ciphersuite>::read_G(reader)?,
|
||||
proof: DLEqProof::read(reader)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
/// Promote a set of keys from one generator to another, where the elliptic curve is the same.
|
||||
///
|
||||
/// Since the Ciphersuite trait additionally specifies a generator, this provides an O(n) way to
|
||||
/// update the generator used with keys. This outperforms the key generation protocol which is
|
||||
/// exponential.
|
||||
pub struct GeneratorPromotion<C1: Ciphersuite, C2: Ciphersuite> {
|
||||
base: ThresholdKeys<C1>,
|
||||
proof: GeneratorProof<C1>,
|
||||
_c2: PhantomData<C2>,
|
||||
}
|
||||
|
||||
impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<C1, C2> {
|
||||
/// Begin promoting keys from one generator to another. Returns a proof this share was properly
|
||||
/// promoted.
|
||||
pub fn promote<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
base: ThresholdKeys<C1>,
|
||||
) -> (GeneratorPromotion<C1, C2>, GeneratorProof<C1>) {
|
||||
// Do a DLEqProof for the new generator
|
||||
let proof = GeneratorProof {
|
||||
share: C2::generator() * base.secret_share().deref(),
|
||||
proof: DLEqProof::prove(
|
||||
rng,
|
||||
&mut transcript(&base.core.group_key(), base.params().i),
|
||||
&[C1::generator(), C2::generator()],
|
||||
base.secret_share(),
|
||||
),
|
||||
};
|
||||
|
||||
(GeneratorPromotion { base, proof, _c2: PhantomData::<C2> }, proof)
|
||||
}
|
||||
|
||||
/// Complete promotion by taking in the proofs from all other participants.
|
||||
pub fn complete(
|
||||
self,
|
||||
proofs: &HashMap<Participant, GeneratorProof<C1>>,
|
||||
) -> Result<ThresholdKeys<C2>, DkgError<()>> {
|
||||
let params = self.base.params();
|
||||
validate_map(proofs, &(1 ..= params.n).map(Participant).collect::<Vec<_>>(), params.i)?;
|
||||
|
||||
let original_shares = self.base.verification_shares();
|
||||
|
||||
let mut verification_shares = HashMap::new();
|
||||
verification_shares.insert(params.i, self.proof.share);
|
||||
for (i, proof) in proofs {
|
||||
let i = *i;
|
||||
proof
|
||||
.proof
|
||||
.verify(
|
||||
&mut transcript(&self.base.core.group_key(), i),
|
||||
&[C1::generator(), C2::generator()],
|
||||
&[original_shares[&i], proof.share],
|
||||
)
|
||||
.map_err(|_| DkgError::InvalidCommitments(i))?;
|
||||
verification_shares.insert(i, proof.share);
|
||||
}
|
||||
|
||||
Ok(ThresholdKeys {
|
||||
core: Arc::new(ThresholdCore::new(
|
||||
params,
|
||||
self.base.core.interpolation.clone(),
|
||||
self.base.secret_share().clone(),
|
||||
verification_shares,
|
||||
)),
|
||||
scalar: C2::F::ONE,
|
||||
offset: C2::F::ZERO,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
use core::ops::Deref;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use crate::{Participant, ThresholdCore, ThresholdKeys, musig::musig as musig_fn};
|
||||
|
||||
mod musig;
|
||||
pub use musig::test_musig;
|
||||
|
||||
/// FROST key generation testing utility.
|
||||
pub mod pedpop;
|
||||
use pedpop::pedpop_gen;
|
||||
|
||||
// Promotion test.
|
||||
mod promote;
|
||||
use promote::test_generator_promotion;
|
||||
|
||||
/// Constant amount of participants to use when testing.
|
||||
pub const PARTICIPANTS: u16 = 5;
|
||||
/// Constant threshold of participants to use when testing.
|
||||
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
|
||||
|
||||
/// Clone a map without a specific value.
|
||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||
map: &HashMap<K, V>,
|
||||
without: &K,
|
||||
) -> HashMap<K, V> {
|
||||
let mut res = map.clone();
|
||||
res.remove(without).unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
/// Recover the secret from a collection of keys.
|
||||
///
|
||||
/// This will panic if no keys, an insufficient amount of keys, or the wrong keys are provided.
|
||||
pub fn recover_key<C: Ciphersuite>(keys: &HashMap<Participant, ThresholdKeys<C>>) -> C::F {
|
||||
let first = keys.values().next().expect("no keys provided");
|
||||
assert!(keys.len() >= first.params().t().into(), "not enough keys provided");
|
||||
let included = keys.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
let group_private = keys.iter().fold(C::F::ZERO, |accum, (i, keys)| {
|
||||
accum +
|
||||
(first.core.interpolation.interpolation_factor(*i, &included) * keys.secret_share().deref())
|
||||
});
|
||||
assert_eq!(C::generator() * group_private, first.group_key(), "failed to recover keys");
|
||||
group_private
|
||||
}
|
||||
|
||||
/// Generate threshold keys for tests.
|
||||
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||
let res = pedpop_gen(rng)
|
||||
.drain()
|
||||
.map(|(i, core)| {
|
||||
assert_eq!(
|
||||
&ThresholdCore::<C>::read::<&[u8]>(&mut core.serialize().as_ref()).unwrap(),
|
||||
&core
|
||||
);
|
||||
(i, ThresholdKeys::new(core))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key());
|
||||
res
|
||||
}
|
||||
|
||||
/// Generate MuSig keys for tests.
|
||||
pub fn musig_key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||
let mut keys = vec![];
|
||||
let mut pub_keys = vec![];
|
||||
for _ in 0 .. PARTICIPANTS {
|
||||
let key = Zeroizing::new(C::F::random(&mut *rng));
|
||||
pub_keys.push(C::generator() * *key);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
let mut res = HashMap::new();
|
||||
for key in keys {
|
||||
let these_keys = musig_fn::<C>(b"Test MuSig Key Gen", &key, &pub_keys).unwrap();
|
||||
res.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
||||
}
|
||||
|
||||
assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key());
|
||||
res
|
||||
}
|
||||
|
||||
/// Run the test suite on a ciphersuite.
|
||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||
key_gen::<_, C>(rng);
|
||||
test_generator_promotion::<_, C>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_ristretto() {
|
||||
test_ciphersuite::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use crate::{
|
||||
ThresholdKeys,
|
||||
musig::{musig_key, musig},
|
||||
tests::{PARTICIPANTS, recover_key},
|
||||
};
|
||||
|
||||
/// Tests MuSig key generation.
|
||||
pub fn test_musig<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||
let mut keys = vec![];
|
||||
let mut pub_keys = vec![];
|
||||
for _ in 0 .. PARTICIPANTS {
|
||||
let key = Zeroizing::new(C::F::random(&mut *rng));
|
||||
pub_keys.push(C::generator() * *key);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
const CONTEXT: &[u8] = b"MuSig Test";
|
||||
|
||||
// Empty signing set
|
||||
musig::<C>(CONTEXT, &Zeroizing::new(C::F::ZERO), &[]).unwrap_err();
|
||||
// Signing set we're not part of
|
||||
musig::<C>(CONTEXT, &Zeroizing::new(C::F::ZERO), &[C::generator()]).unwrap_err();
|
||||
|
||||
// Test with n keys
|
||||
{
|
||||
let mut created_keys = HashMap::new();
|
||||
let mut verification_shares = HashMap::new();
|
||||
let group_key = musig_key::<C>(CONTEXT, &pub_keys).unwrap();
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
let these_keys = musig::<C>(CONTEXT, key, &pub_keys).unwrap();
|
||||
assert_eq!(these_keys.params().t(), PARTICIPANTS);
|
||||
assert_eq!(these_keys.params().n(), PARTICIPANTS);
|
||||
assert_eq!(usize::from(these_keys.params().i().0), i + 1);
|
||||
|
||||
verification_shares
|
||||
.insert(these_keys.params().i(), C::generator() * **these_keys.secret_share());
|
||||
|
||||
assert_eq!(these_keys.group_key(), group_key);
|
||||
|
||||
created_keys.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
||||
}
|
||||
|
||||
for keys in created_keys.values() {
|
||||
assert_eq!(keys.verification_shares(), verification_shares);
|
||||
}
|
||||
|
||||
assert_eq!(C::generator() * recover_key(&created_keys), group_key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn musig_literal() {
|
||||
test_musig::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng)
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
use crate::{
|
||||
Participant, ThresholdParams, ThresholdCore,
|
||||
pedpop::{Commitments, KeyGenMachine, SecretShare, KeyMachine},
|
||||
encryption::{EncryptionKeyMessage, EncryptedMessage},
|
||||
tests::{THRESHOLD, PARTICIPANTS, clone_without},
|
||||
};
|
||||
|
||||
type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
|
||||
type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>;
|
||||
|
||||
const CONTEXT: [u8; 32] = *b"DKG Test Key Generation ";
|
||||
|
||||
// Commit, then return commitment messages, enc keys, and shares
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> (
|
||||
HashMap<Participant, KeyMachine<C>>,
|
||||
HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
HashMap<Participant, C::G>,
|
||||
HashMap<Participant, PedPoPSecretShares<C>>,
|
||||
) {
|
||||
let mut machines = HashMap::new();
|
||||
let mut commitments = HashMap::new();
|
||||
let mut enc_keys = HashMap::new();
|
||||
for i in (1 ..= PARTICIPANTS).map(Participant) {
|
||||
let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap();
|
||||
let machine = KeyGenMachine::<C>::new(params, CONTEXT);
|
||||
let (machine, these_commitments) = machine.generate_coefficients(rng);
|
||||
machines.insert(i, machine);
|
||||
|
||||
commitments.insert(
|
||||
i,
|
||||
EncryptionKeyMessage::read::<&[u8]>(&mut these_commitments.serialize().as_ref(), params)
|
||||
.unwrap(),
|
||||
);
|
||||
enc_keys.insert(i, commitments[&i].enc_key());
|
||||
}
|
||||
|
||||
let mut secret_shares = HashMap::new();
|
||||
let machines = machines
|
||||
.drain()
|
||||
.map(|(l, machine)| {
|
||||
let (machine, mut shares) =
|
||||
machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap();
|
||||
let shares = shares
|
||||
.drain()
|
||||
.map(|(l, share)| {
|
||||
(
|
||||
l,
|
||||
EncryptedMessage::read::<&[u8]>(
|
||||
&mut share.serialize().as_ref(),
|
||||
// Only t/n actually matters, so hardcode i to 1 here
|
||||
ThresholdParams { t: THRESHOLD, n: PARTICIPANTS, i: Participant(1) },
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
secret_shares.insert(l, shares);
|
||||
(l, machine)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
(machines, commitments, enc_keys, secret_shares)
|
||||
}
|
||||
|
||||
fn generate_secret_shares<C: Ciphersuite>(
|
||||
shares: &HashMap<Participant, PedPoPSecretShares<C>>,
|
||||
recipient: Participant,
|
||||
) -> PedPoPSecretShares<C> {
|
||||
let mut our_secret_shares = HashMap::new();
|
||||
for (i, shares) in shares {
|
||||
if recipient == *i {
|
||||
continue;
|
||||
}
|
||||
our_secret_shares.insert(*i, shares[&recipient].clone());
|
||||
}
|
||||
our_secret_shares
|
||||
}
|
||||
|
||||
/// Fully perform the PedPoP key generation algorithm.
|
||||
pub fn pedpop_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdCore<C>> {
|
||||
let (mut machines, _, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng);
|
||||
|
||||
let mut verification_shares = None;
|
||||
let mut group_key = None;
|
||||
machines
|
||||
.drain()
|
||||
.map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete();
|
||||
|
||||
// Verify the verification_shares are agreed upon
|
||||
if verification_shares.is_none() {
|
||||
verification_shares = Some(these_keys.verification_shares());
|
||||
}
|
||||
assert_eq!(verification_shares.as_ref().unwrap(), &these_keys.verification_shares());
|
||||
|
||||
// Verify the group keys are agreed upon
|
||||
if group_key.is_none() {
|
||||
group_key = Some(these_keys.group_key());
|
||||
}
|
||||
assert_eq!(group_key.unwrap(), these_keys.group_key());
|
||||
|
||||
(i, these_keys)
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod literal {
|
||||
use rand_core::OsRng;
|
||||
|
||||
use ciphersuite::Ristretto;
|
||||
|
||||
use crate::{
|
||||
DkgError,
|
||||
encryption::EncryptionKeyProof,
|
||||
pedpop::{BlameMachine, AdditionalBlameMachine},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const ONE: Participant = Participant(1);
|
||||
const TWO: Participant = Participant(2);
|
||||
|
||||
fn test_blame(
|
||||
commitment_msgs: &HashMap<Participant, EncryptionKeyMessage<Ristretto, Commitments<Ristretto>>>,
|
||||
machines: Vec<BlameMachine<Ristretto>>,
|
||||
msg: &PedPoPEncryptedMessage<Ristretto>,
|
||||
blame: &Option<EncryptionKeyProof<Ristretto>>,
|
||||
) {
|
||||
for machine in machines {
|
||||
let (additional, blamed) = machine.blame(ONE, TWO, msg.clone(), blame.clone());
|
||||
assert_eq!(blamed, ONE);
|
||||
// Verify additional blame also works
|
||||
assert_eq!(additional.blame(ONE, TWO, msg.clone(), blame.clone()), ONE);
|
||||
|
||||
// Verify machines constructed with AdditionalBlameMachine::new work
|
||||
assert_eq!(
|
||||
AdditionalBlameMachine::new(CONTEXT, PARTICIPANTS, commitment_msgs.clone()).unwrap().blame(
|
||||
ONE,
|
||||
TWO,
|
||||
msg.clone(),
|
||||
blame.clone()
|
||||
),
|
||||
ONE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Write a macro which expands to the following
|
||||
#[test]
|
||||
fn invalid_encryption_pop_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
// Mutate the PoP of the encrypted message from 1 to 2
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_pop();
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
assert_eq!(machine.err(), Some(DkgError::InvalidShare { participant: ONE, blame: None }));
|
||||
// Explicitly declare we have a blame object, which happens to be None since invalid PoP
|
||||
// is self-explainable
|
||||
blame = Some(None);
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ecdh_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
// Mutate the share to trigger a blame event
|
||||
// Mutates from 2 to 1, as 1 is expected to end up malicious for test_blame to pass
|
||||
// While here, 2 is malicious, this is so 1 creates the blame proof
|
||||
// We then malleate 1's blame proof, so 1 ends up malicious
|
||||
// Doesn't simply invalidate the PoP as that won't have a blame statement
|
||||
// By mutating the encrypted data, we do ensure a blame statement is created
|
||||
secret_shares
|
||||
.get_mut(&TWO)
|
||||
.unwrap()
|
||||
.get_mut(&ONE)
|
||||
.unwrap()
|
||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == ONE {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_key();
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
// This should be largely equivalent to the prior test
|
||||
#[test]
|
||||
fn invalid_dleq_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares
|
||||
.get_mut(&TWO)
|
||||
.unwrap()
|
||||
.get_mut(&ONE)
|
||||
.unwrap()
|
||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == ONE {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq();
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_share_serialization_blame() {
|
||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization(
|
||||
&mut OsRng,
|
||||
CONTEXT,
|
||||
ONE,
|
||||
enc_keys[&TWO],
|
||||
);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_share_value_blame() {
|
||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value(
|
||||
&mut OsRng,
|
||||
CONTEXT,
|
||||
ONE,
|
||||
enc_keys[&TWO],
|
||||
);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use core::{marker::PhantomData, ops::Deref};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use ciphersuite::{group::Group, Ciphersuite};
|
||||
|
||||
use crate::{
|
||||
promote::{GeneratorPromotion, GeneratorProof},
|
||||
tests::{clone_without, key_gen, recover_key},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
struct AltGenerator<C: Ciphersuite> {
|
||||
_curve: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
|
||||
type F = C::F;
|
||||
type G = C::G;
|
||||
type H = C::H;
|
||||
|
||||
const ID: &'static [u8] = b"Alternate Ciphersuite";
|
||||
|
||||
fn generator() -> Self::G {
|
||||
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
|
||||
}
|
||||
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
<C as Ciphersuite>::reduce_512(scalar)
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
<C as Ciphersuite>::hash_to_F(dst, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Test promotion of threshold keys to another generator
|
||||
pub(crate) fn test_generator_promotion<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||
let keys = key_gen::<_, C>(&mut *rng);
|
||||
|
||||
let mut promotions = HashMap::new();
|
||||
let mut proofs = HashMap::new();
|
||||
for (i, keys) in &keys {
|
||||
let (promotion, proof) =
|
||||
GeneratorPromotion::<_, AltGenerator<C>>::promote(&mut *rng, keys.clone());
|
||||
promotions.insert(*i, promotion);
|
||||
proofs.insert(*i, GeneratorProof::<C>::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap());
|
||||
}
|
||||
|
||||
let new_group_key = AltGenerator::<C>::generator() * recover_key(&keys);
|
||||
for (i, promoting) in promotions.drain() {
|
||||
let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap();
|
||||
assert_eq!(keys[&i].params(), promoted.params());
|
||||
assert_eq!(keys[&i].secret_share(), promoted.secret_share());
|
||||
assert_eq!(new_group_key, promoted.group_key());
|
||||
for (l, verification_share) in promoted.verification_shares() {
|
||||
assert_eq!(
|
||||
AltGenerator::<C>::generator() * keys[&l].secret_share().deref(),
|
||||
verification_share
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user