mirror of
https://github.com/serai-dex/serai.git
synced 2025-03-22 15:19:06 +00:00
DKG Blame (#196)
* Standardize the DLEq serialization function naming They mismatched from the rest of the project. This commit is technically incomplete as it doesn't update the dkg crate. * Rewrite DKG encryption to enable per-message decryption without side effects This isn't technically true as I already know a break in this which I'll correct for shortly. Does update documentation to explain the new scheme. Required for blame. * Add a verifiable system for blame during the FROST DKG Previously, if sent an invalid key share, the participant would realize that and could accuse the sender. Without further evidence, either the accuser or the accused could be guilty. Now, the accuser has a proof the accused is in the wrong. Reworks KeyMachine to return BlameMachine. This explicitly acknowledges how locally complete keys still need group acknowledgement before the protocol can be complete and provides a way for others to verify blame, even after a locally successful run. If any blame is cast, the protocol is no longer considered complete-able (instead aborting). Further accusations of blame can still be handled however. Updates documentation on network behavior. Also starts to remove "OnDrop". We now use Zeroizing for anything which should be zeroized on drop. This is a lot more piece-meal and reduces clones. * Tweak Zeroizing and Debug impls Expands Zeroizing to be more comprehensive. Also updates Zeroizing<CachedPreprocess([u8; 32])> to CachedPreprocess(Zeroizing<[u8; 32]>) so zeroizing is the first thing done and last step before exposing the copy-able [u8; 32]. Removes private keys from Debug. * Fix a bug where adversaries could claim to be using another user's encryption keys to learn their messages Mentioned a few commits ago, now fixed. This wouldn't have affected Serai, which aborts on failure, nor any DKG currently supported. It's just about ensuring the DKG encryption is robust and proper. * Finish moving dleq from ser/deser to write/read * Add tests for dkg blame * Add a FROST test for invalid signature shares * Batch verify encrypted messages' ephemeral keys' PoP
This commit is contained in:
parent
3b4c600c60
commit
5b3c9bf5d0
21 changed files with 1003 additions and 238 deletions
|
@ -83,7 +83,7 @@ pub struct ClsagAddendum {
|
||||||
impl WriteAddendum for ClsagAddendum {
|
impl WriteAddendum for ClsagAddendum {
|
||||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
writer.write_all(self.key_image.compress().to_bytes().as_ref())?;
|
writer.write_all(self.key_image.compress().to_bytes().as_ref())?;
|
||||||
self.dleq.serialize(writer)
|
self.dleq.write(writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +197,7 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "non-canonical key image"))?;
|
Err(io::Error::new(io::ErrorKind::Other, "non-canonical key image"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::deserialize(reader)? })
|
Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::read(reader)? })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_addendum(
|
fn process_addendum(
|
||||||
|
|
|
@ -4,7 +4,6 @@ use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
|
@ -217,18 +216,14 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||||
type SignatureShare = Vec<SignatureShare<Ed25519>>;
|
type SignatureShare = Vec<SignatureShare<Ed25519>>;
|
||||||
type SignatureMachine = TransactionSignatureMachine;
|
type SignatureMachine = TransactionSignatureMachine;
|
||||||
|
|
||||||
fn cache(self) -> Zeroizing<CachedPreprocess> {
|
fn cache(self) -> CachedPreprocess {
|
||||||
unimplemented!(
|
unimplemented!(
|
||||||
"Monero transactions don't support caching their preprocesses due to {}",
|
"Monero transactions don't support caching their preprocesses due to {}",
|
||||||
"being already bound to a specific transaction"
|
"being already bound to a specific transaction"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_cache(
|
fn from_cache(_: (), _: ThresholdKeys<Ed25519>, _: CachedPreprocess) -> Result<Self, FrostError> {
|
||||||
_: (),
|
|
||||||
_: ThresholdKeys<Ed25519>,
|
|
||||||
_: Zeroizing<CachedPreprocess>,
|
|
||||||
) -> Result<Self, FrostError> {
|
|
||||||
unimplemented!(
|
unimplemented!(
|
||||||
"Monero transactions don't support caching their preprocesses due to {}",
|
"Monero transactions don't support caching their preprocesses due to {}",
|
||||||
"being already bound to a specific transaction"
|
"being already bound to a specific transaction"
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use core::{hash::Hash, fmt::Debug};
|
use core::fmt::Debug;
|
||||||
use std::{
|
use std::{
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
@ -13,12 +15,17 @@ use chacha20::{
|
||||||
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
|
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
|
||||||
};
|
};
|
||||||
|
|
||||||
use group::GroupEncoding;
|
|
||||||
|
|
||||||
use ciphersuite::Ciphersuite;
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use group::ff::Field;
|
||||||
|
use group::GroupEncoding;
|
||||||
|
use ciphersuite::Ciphersuite;
|
||||||
|
use multiexp::BatchVerifier;
|
||||||
|
|
||||||
|
use schnorr::SchnorrSignature;
|
||||||
|
use dleq::DLEqProof;
|
||||||
|
|
||||||
use crate::ThresholdParams;
|
use crate::ThresholdParams;
|
||||||
|
|
||||||
pub trait ReadWrite: Sized {
|
pub trait ReadWrite: Sized {
|
||||||
|
@ -35,6 +42,7 @@ pub trait ReadWrite: Sized {
|
||||||
pub trait Message: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite {}
|
pub trait Message: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite {}
|
||||||
impl<M: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite> Message for M {}
|
impl<M: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite> Message for M {}
|
||||||
|
|
||||||
|
/// Wraps a message with a key to use for encryption in the future.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
|
pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
|
||||||
msg: M,
|
msg: M,
|
||||||
|
@ -57,20 +65,115 @@ impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
|
||||||
self.write(&mut buf).unwrap();
|
self.write(&mut buf).unwrap();
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used by tests
|
||||||
|
pub(crate) fn enc_key(&self) -> C::G {
|
||||||
|
self.enc_key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Encryptable: Clone + AsMut<[u8]> + Zeroize + ReadWrite {}
|
pub trait Encryptable: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite {}
|
||||||
impl<E: Clone + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
|
impl<E: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
|
||||||
#[derive(Clone, Zeroize)]
|
|
||||||
pub struct EncryptedMessage<E: Encryptable>(Zeroizing<E>);
|
|
||||||
|
|
||||||
impl<E: Encryptable> EncryptedMessage<E> {
|
/// 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cipher<C: Ciphersuite>(dst: &'static [u8], 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.domain_separate(dst);
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
// The RecommendedTranscript isn't vulnerable to length extension attacks, yet if it was,
|
||||||
|
// it'd make sense to clone it (and fork it) just to hedge against that
|
||||||
|
let mut iv = Cc20Iv::default();
|
||||||
|
let mut challenge = transcript.challenge(b"iv");
|
||||||
|
iv.copy_from_slice(&challenge[.. 12]);
|
||||||
|
zeroize(challenge.as_mut());
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
zeroize(iv.as_mut());
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
|
||||||
|
rng: &mut R,
|
||||||
|
dst: &'static [u8],
|
||||||
|
from: u16,
|
||||||
|
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();
|
||||||
|
*/
|
||||||
|
|
||||||
|
let key = Zeroizing::new(C::random_nonzero_F(rng));
|
||||||
|
cipher::<C>(dst, &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>(pub_nonce, pub_key, from, msg.deref().as_ref()),
|
||||||
|
),
|
||||||
|
msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
|
||||||
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
||||||
Ok(Self(Zeroizing::new(E::read(reader, params)?)))
|
Ok(Self {
|
||||||
|
key: C::read_G(reader)?,
|
||||||
|
pop: SchnorrSignature::<C>::read(reader)?,
|
||||||
|
msg: Zeroizing::new(E::read(reader, params)?),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
self.0.write(writer)
|
writer.write_all(self.key.to_bytes().as_ref())?;
|
||||||
|
self.pop.write(writer)?;
|
||||||
|
self.msg.write(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
@ -78,17 +181,150 @@ impl<E: Encryptable> EncryptedMessage<E> {
|
||||||
self.write(&mut buf).unwrap();
|
self.write(&mut buf).unwrap();
|
||||||
buf
|
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, from: u16) {
|
||||||
|
// 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>(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,
|
||||||
|
dst: &'static [u8],
|
||||||
|
from: u16,
|
||||||
|
to: C::G,
|
||||||
|
) {
|
||||||
|
use group::ff::PrimeField;
|
||||||
|
|
||||||
|
let mut repr = <C::F as PrimeField>::Repr::default();
|
||||||
|
for b in repr.as_mut().iter_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, dst, 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,
|
||||||
|
dst: &'static [u8],
|
||||||
|
from: u16,
|
||||||
|
to: C::G,
|
||||||
|
) {
|
||||||
|
use 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, dst, from, to, self.msg.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A proof that the provided point is the 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: Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
|
Ok(Self { key: Zeroizing::new(C::read_G(reader)?), dleq: DLEqProof::read(reader)? })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: 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>(nonce: C::G, key: C::G, sender: u16, msg: &[u8]) -> C::F {
|
||||||
|
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2");
|
||||||
|
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_le_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() -> RecommendedTranscript {
|
||||||
|
RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 encryption.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Encryption<Id: Eq + Hash, C: Ciphersuite> {
|
pub(crate) struct Encryption<C: Ciphersuite> {
|
||||||
dst: &'static [u8],
|
dst: &'static [u8],
|
||||||
|
i: u16,
|
||||||
enc_key: Zeroizing<C::F>,
|
enc_key: Zeroizing<C::F>,
|
||||||
enc_pub_key: C::G,
|
enc_pub_key: C::G,
|
||||||
enc_keys: HashMap<Id, C::G>,
|
enc_keys: HashMap<u16, C::G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Id: Eq + Hash, C: Ciphersuite> Zeroize for Encryption<Id, C> {
|
impl<C: Ciphersuite> Zeroize for Encryption<C> {
|
||||||
fn zeroize(&mut self) {
|
fn zeroize(&mut self) {
|
||||||
self.enc_key.zeroize();
|
self.enc_key.zeroize();
|
||||||
self.enc_pub_key.zeroize();
|
self.enc_pub_key.zeroize();
|
||||||
|
@ -98,10 +334,16 @@ impl<Id: Eq + Hash, C: Ciphersuite> Zeroize for Encryption<Id, C> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Id: Eq + Hash, C: Ciphersuite> Encryption<Id, C> {
|
impl<C: Ciphersuite> Encryption<C> {
|
||||||
pub(crate) fn new<R: RngCore + CryptoRng>(dst: &'static [u8], rng: &mut R) -> Self {
|
pub(crate) fn new<R: RngCore + CryptoRng>(dst: &'static [u8], i: u16, rng: &mut R) -> Self {
|
||||||
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
|
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
|
||||||
Self { dst, enc_pub_key: C::generator() * enc_key.deref(), enc_key, enc_keys: HashMap::new() }
|
Self {
|
||||||
|
dst,
|
||||||
|
i,
|
||||||
|
enc_pub_key: C::generator() * enc_key.deref(),
|
||||||
|
enc_key,
|
||||||
|
enc_keys: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
|
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
|
||||||
|
@ -110,7 +352,7 @@ impl<Id: Eq + Hash, C: Ciphersuite> Encryption<Id, C> {
|
||||||
|
|
||||||
pub(crate) fn register<M: Message>(
|
pub(crate) fn register<M: Message>(
|
||||||
&mut self,
|
&mut self,
|
||||||
participant: Id,
|
participant: u16,
|
||||||
msg: EncryptionKeyMessage<C, M>,
|
msg: EncryptionKeyMessage<C, M>,
|
||||||
) -> M {
|
) -> M {
|
||||||
if self.enc_keys.contains_key(&participant) {
|
if self.enc_keys.contains_key(&participant) {
|
||||||
|
@ -120,62 +362,81 @@ impl<Id: Eq + Hash, C: Ciphersuite> Encryption<Id, C> {
|
||||||
msg.msg
|
msg.msg
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cipher(&self, participant: Id, encrypt: bool) -> ChaCha20 {
|
pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>(
|
||||||
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
|
&self,
|
||||||
// TODO: https://github.com/serai-dex/serai/issues/151
|
rng: &mut R,
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2");
|
participant: u16,
|
||||||
transcript.domain_separate(self.dst);
|
msg: Zeroizing<E>,
|
||||||
|
) -> EncryptedMessage<C, E> {
|
||||||
|
encrypt(rng, self.dst, self.i, self.enc_keys[&participant], msg)
|
||||||
|
}
|
||||||
|
|
||||||
let other = self.enc_keys[&participant];
|
pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>(
|
||||||
if encrypt {
|
&self,
|
||||||
transcript.append_message(b"sender", self.enc_pub_key.to_bytes());
|
rng: &mut R,
|
||||||
transcript.append_message(b"receiver", other.to_bytes());
|
batch: &mut BatchVerifier<I, C::G>,
|
||||||
} else {
|
// Uses a distinct batch ID so if this batch verifier is reused, we know its the PoP aspect
|
||||||
transcript.append_message(b"sender", other.to_bytes());
|
// which failed, and therefore to use None for the blame
|
||||||
transcript.append_message(b"receiver", self.enc_pub_key.to_bytes());
|
batch_id: I,
|
||||||
|
from: u16,
|
||||||
|
mut msg: EncryptedMessage<C, E>,
|
||||||
|
) -> (Zeroizing<E>, EncryptionKeyProof<C>) {
|
||||||
|
msg.pop.batch_verify(
|
||||||
|
rng,
|
||||||
|
batch,
|
||||||
|
batch_id,
|
||||||
|
msg.key,
|
||||||
|
pop_challenge::<C>(msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let key = ecdh::<C>(&self.enc_key, msg.key);
|
||||||
|
cipher::<C>(self.dst, &key).apply_keystream(msg.msg.as_mut().as_mut());
|
||||||
|
(
|
||||||
|
msg.msg,
|
||||||
|
EncryptionKeyProof {
|
||||||
|
key,
|
||||||
|
dleq: DLEqProof::prove(
|
||||||
|
rng,
|
||||||
|
&mut encryption_key_transcript(),
|
||||||
|
&[C::generator(), msg.key],
|
||||||
|
&self.enc_key,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: u16,
|
||||||
|
decryptor: u16,
|
||||||
|
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>(msg.pop.R, msg.key, from, msg.msg.deref().as_ref()))
|
||||||
|
{
|
||||||
|
Err(DecryptionError::InvalidSignature)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut shared = Zeroizing::new(other * self.enc_key.deref()).deref().to_bytes();
|
if let Some(proof) = proof {
|
||||||
transcript.append_message(b"shared_key", shared.as_ref());
|
// Verify this is the decryption key for this message
|
||||||
shared.as_mut().zeroize();
|
proof
|
||||||
|
.dleq
|
||||||
|
.verify(
|
||||||
|
&mut encryption_key_transcript(),
|
||||||
|
&[C::generator(), msg.key],
|
||||||
|
&[self.enc_keys[&decryptor], *proof.key],
|
||||||
|
)
|
||||||
|
.map_err(|_| DecryptionError::InvalidProof)?;
|
||||||
|
|
||||||
let zeroize = |buf: &mut [u8]| buf.zeroize();
|
cipher::<C>(self.dst, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
|
||||||
|
Ok(msg.msg)
|
||||||
let mut key = Cc20Key::default();
|
} else {
|
||||||
let mut challenge = transcript.challenge(b"key");
|
Err(DecryptionError::InvalidProof)
|
||||||
key.copy_from_slice(&challenge[.. 32]);
|
}
|
||||||
zeroize(challenge.as_mut());
|
|
||||||
|
|
||||||
// The RecommendedTranscript isn't vulnerable to length extension attacks, yet if it was,
|
|
||||||
// it'd make sense to clone it (and fork it) just to hedge against that
|
|
||||||
let mut iv = Cc20Iv::default();
|
|
||||||
let mut challenge = transcript.challenge(b"iv");
|
|
||||||
iv.copy_from_slice(&challenge[.. 12]);
|
|
||||||
zeroize(challenge.as_mut());
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
zeroize(iv.as_mut());
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn encrypt<E: Encryptable>(
|
|
||||||
&self,
|
|
||||||
participant: Id,
|
|
||||||
mut msg: Zeroizing<E>,
|
|
||||||
) -> EncryptedMessage<E> {
|
|
||||||
self.cipher(participant, true).apply_keystream(msg.as_mut().as_mut());
|
|
||||||
EncryptedMessage(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn decrypt<E: Encryptable>(
|
|
||||||
&self,
|
|
||||||
participant: Id,
|
|
||||||
mut msg: EncryptedMessage<E>,
|
|
||||||
) -> Zeroizing<E> {
|
|
||||||
self.cipher(participant, false).apply_keystream(msg.0.as_mut().as_mut());
|
|
||||||
msg.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use std::{
|
use core::{
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
@ -13,7 +16,7 @@ use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
|
||||||
use group::{
|
use group::{
|
||||||
ff::{Field, PrimeField},
|
ff::{Field, PrimeField},
|
||||||
GroupEncoding,
|
Group, GroupEncoding,
|
||||||
};
|
};
|
||||||
use ciphersuite::Ciphersuite;
|
use ciphersuite::Ciphersuite;
|
||||||
use multiexp::{multiexp_vartime, BatchVerifier};
|
use multiexp::{multiexp_vartime, BatchVerifier};
|
||||||
|
@ -22,9 +25,14 @@ use schnorr::SchnorrSignature;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
DkgError, ThresholdParams, ThresholdCore, validate_map,
|
DkgError, ThresholdParams, ThresholdCore, validate_map,
|
||||||
encryption::{ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption},
|
encryption::{
|
||||||
|
ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, EncryptionKeyProof,
|
||||||
|
DecryptionError,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FrostError<C> = DkgError<EncryptionKeyProof<C>>;
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn challenge<C: Ciphersuite>(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F {
|
fn challenge<C: Ciphersuite>(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F {
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2");
|
let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2");
|
||||||
|
@ -33,10 +41,15 @@ fn challenge<C: Ciphersuite>(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F
|
||||||
transcript.append_message(b"participant", l.to_le_bytes());
|
transcript.append_message(b"participant", l.to_le_bytes());
|
||||||
transcript.append_message(b"nonce", R);
|
transcript.append_message(b"nonce", R);
|
||||||
transcript.append_message(b"commitments", Am);
|
transcript.append_message(b"commitments", Am);
|
||||||
C::hash_to_F(b"PoK 0", &transcript.challenge(b"challenge"))
|
C::hash_to_F(b"DKG-FROST-proof_of_knowledge-0", &transcript.challenge(b"schnorr"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commitments message to be broadcast to all other parties.
|
/// 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 also unable to detect if any participant is
|
||||||
|
/// so faulty. That responsibility lies with the caller.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
pub struct Commitments<C: Ciphersuite> {
|
pub struct Commitments<C: Ciphersuite> {
|
||||||
commitments: Vec<C::G>,
|
commitments: Vec<C::G>,
|
||||||
|
@ -119,7 +132,7 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Additionally create an encryption mechanism to protect the secret shares
|
// Additionally create an encryption mechanism to protect the secret shares
|
||||||
let encryption = Encryption::new(b"FROST", rng);
|
let encryption = Encryption::new(b"FROST", self.params.i, rng);
|
||||||
|
|
||||||
// Step 4: Broadcast
|
// Step 4: Broadcast
|
||||||
let msg =
|
let msg =
|
||||||
|
@ -149,19 +162,39 @@ fn polynomial<F: PrimeField + Zeroize>(coefficients: &[Zeroizing<F>], l: u16) ->
|
||||||
share
|
share
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Secret share to be sent to the party it's intended for over an authenticated channel.
|
/// The secret share message, to be sent to the party it's intended for over an authenticated
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
/// 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);
|
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> {
|
impl<F: PrimeField> AsMut<[u8]> for SecretShare<F> {
|
||||||
fn as_mut(&mut self) -> &mut [u8] {
|
fn as_mut(&mut self) -> &mut [u8] {
|
||||||
self.0.as_mut()
|
self.0.as_mut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl<F: PrimeField> Debug for SecretShare<F> {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||||
|
fmt.debug_struct("SecretShare").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
impl<F: PrimeField> Zeroize for SecretShare<F> {
|
impl<F: PrimeField> Zeroize for SecretShare<F> {
|
||||||
fn zeroize(&mut self) {
|
fn zeroize(&mut self) {
|
||||||
self.0.as_mut().zeroize()
|
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 thr expected behavior with these and remove a layer
|
||||||
|
// of depth.
|
||||||
impl<F: PrimeField> Drop for SecretShare<F> {
|
impl<F: PrimeField> Drop for SecretShare<F> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.zeroize();
|
self.zeroize();
|
||||||
|
@ -188,7 +221,7 @@ pub struct SecretShareMachine<C: Ciphersuite> {
|
||||||
context: String,
|
context: String,
|
||||||
coefficients: Vec<Zeroizing<C::F>>,
|
coefficients: Vec<Zeroizing<C::F>>,
|
||||||
our_commitments: Vec<C::G>,
|
our_commitments: Vec<C::G>,
|
||||||
encryption: Encryption<u16, C>,
|
encryption: Encryption<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> SecretShareMachine<C> {
|
impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||||
|
@ -198,7 +231,7 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
mut commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
|
mut commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||||
) -> Result<HashMap<u16, Vec<C::G>>, DkgError> {
|
) -> Result<HashMap<u16, Vec<C::G>>, FrostError<C>> {
|
||||||
validate_map(&commitments, &(1 ..= self.params.n()).collect::<Vec<_>>(), self.params.i())?;
|
validate_map(&commitments, &(1 ..= self.params.n()).collect::<Vec<_>>(), self.params.i())?;
|
||||||
|
|
||||||
let mut batch = BatchVerifier::<u16, C::G>::new(commitments.len());
|
let mut batch = BatchVerifier::<u16, C::G>::new(commitments.len());
|
||||||
|
@ -221,21 +254,23 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
batch.verify_with_vartime_blame().map_err(DkgError::InvalidProofOfKnowledge)?;
|
batch.verify_with_vartime_blame().map_err(FrostError::InvalidProofOfKnowledge)?;
|
||||||
|
|
||||||
commitments.insert(self.params.i, self.our_commitments.drain(..).collect());
|
commitments.insert(self.params.i, self.our_commitments.drain(..).collect());
|
||||||
Ok(commitments)
|
Ok(commitments)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Continue generating a key.
|
/// Continue generating a key.
|
||||||
/// Takes in everyone else's commitments. Returns a HashMap of secret shares to be sent over
|
/// Takes in everyone else's commitments. Returns a HashMap of encrypted secret shares to be sent
|
||||||
/// authenticated channels to their relevant counterparties.
|
/// over authenticated channels to their relevant counterparties.
|
||||||
|
/// If any participant sends multiple secret shares to another participant, they are faulty.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
|
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
|
||||||
mut self,
|
mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
|
commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||||
) -> Result<(KeyMachine<C>, HashMap<u16, EncryptedMessage<SecretShare<C::F>>>), DkgError> {
|
) -> Result<(KeyMachine<C>, HashMap<u16, EncryptedMessage<C, SecretShare<C::F>>>), FrostError<C>>
|
||||||
|
{
|
||||||
let commitments = self.verify_r1(&mut *rng, commitments)?;
|
let commitments = self.verify_r1(&mut *rng, commitments)?;
|
||||||
|
|
||||||
// Step 1: Generate secret shares for all other parties
|
// Step 1: Generate secret shares for all other parties
|
||||||
|
@ -250,7 +285,7 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||||
let mut share = polynomial(&self.coefficients, l);
|
let mut share = polynomial(&self.coefficients, l);
|
||||||
let share_bytes = Zeroizing::new(SecretShare::<C::F>(share.to_repr()));
|
let share_bytes = Zeroizing::new(SecretShare::<C::F>(share.to_repr()));
|
||||||
share.zeroize();
|
share.zeroize();
|
||||||
res.insert(l, self.encryption.encrypt(l, share_bytes));
|
res.insert(l, self.encryption.encrypt(rng, l, share_bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate our own share
|
// Calculate our own share
|
||||||
|
@ -264,13 +299,18 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Final step of the key generation protocol.
|
/// Advancement of the the secret share state machine protocol.
|
||||||
|
/// This machine will 'complete' the protocol, by a local perspective, and can be the last
|
||||||
|
/// interactive component. 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 modelled by one
|
||||||
|
/// more state transition.
|
||||||
pub struct KeyMachine<C: Ciphersuite> {
|
pub struct KeyMachine<C: Ciphersuite> {
|
||||||
params: ThresholdParams,
|
params: ThresholdParams,
|
||||||
secret: Zeroizing<C::F>,
|
secret: Zeroizing<C::F>,
|
||||||
commitments: HashMap<u16, Vec<C::G>>,
|
commitments: HashMap<u16, Vec<C::G>>,
|
||||||
encryption: Encryption<u16, C>,
|
encryption: Encryption<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for KeyMachine<C> {
|
impl<C: Ciphersuite> Zeroize for KeyMachine<C> {
|
||||||
fn zeroize(&mut self) {
|
fn zeroize(&mut self) {
|
||||||
self.params.zeroize();
|
self.params.zeroize();
|
||||||
|
@ -281,59 +321,84 @@ impl<C: Ciphersuite> Zeroize for KeyMachine<C> {
|
||||||
self.encryption.zeroize();
|
self.encryption.zeroize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<C: Ciphersuite> Drop for KeyMachine<C> {
|
|
||||||
fn drop(&mut self) {
|
// Calculate the exponent for a given participant and apply it to a series of commitments
|
||||||
self.zeroize()
|
// 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: u16, values: &[C::G]) -> Vec<(C::F, C::G)> {
|
||||||
|
let i = C::F::from(i.into());
|
||||||
|
let mut res = Vec::with_capacity(values.len());
|
||||||
|
(0 .. values.len()).into_iter().fold(C::F::one(), |exp, l| {
|
||||||
|
res.push((exp, values[l]));
|
||||||
|
exp * i
|
||||||
|
});
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_verification_statements<C: Ciphersuite>(
|
||||||
|
target: u16,
|
||||||
|
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(u16),
|
||||||
|
Share(u16),
|
||||||
}
|
}
|
||||||
impl<C: Ciphersuite> ZeroizeOnDrop for KeyMachine<C> {}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> KeyMachine<C> {
|
impl<C: Ciphersuite> KeyMachine<C> {
|
||||||
/// Complete key generation.
|
/// Calculate our share given the shares sent to us.
|
||||||
/// Takes in everyone elses' shares submitted to us. Returns a ThresholdCore object representing
|
/// Returns a BlameMachine usable to determine if faults in the protocol occurred.
|
||||||
/// the generated keys. Successful protocol completion MUST be confirmed by all parties before
|
/// Will error on, and return a blame proof for, the first-observed case of faulty behavior.
|
||||||
/// these keys may be safely used.
|
pub fn calculate_share<R: RngCore + CryptoRng>(
|
||||||
pub fn complete<R: RngCore + CryptoRng>(
|
|
||||||
mut self,
|
mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
mut shares: HashMap<u16, EncryptedMessage<SecretShare<C::F>>>,
|
mut shares: HashMap<u16, EncryptedMessage<C, SecretShare<C::F>>>,
|
||||||
) -> Result<ThresholdCore<C>, DkgError> {
|
) -> Result<BlameMachine<C>, FrostError<C>> {
|
||||||
validate_map(&shares, &(1 ..= self.params.n()).collect::<Vec<_>>(), self.params.i())?;
|
validate_map(&shares, &(1 ..= self.params.n()).collect::<Vec<_>>(), self.params.i())?;
|
||||||
|
|
||||||
// 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
|
|
||||||
let exponential = |i: u16, values: &[_]| {
|
|
||||||
let i = C::F::from(i.into());
|
|
||||||
let mut res = Vec::with_capacity(self.params.t().into());
|
|
||||||
(0 .. usize::from(self.params.t())).into_iter().fold(C::F::one(), |exp, l| {
|
|
||||||
res.push((exp, values[l]));
|
|
||||||
exp * i
|
|
||||||
});
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut batch = BatchVerifier::new(shares.len());
|
let mut batch = BatchVerifier::new(shares.len());
|
||||||
|
let mut blames = HashMap::new();
|
||||||
for (l, share_bytes) in shares.drain() {
|
for (l, share_bytes) in shares.drain() {
|
||||||
let mut share_bytes = self.encryption.decrypt(l, share_bytes);
|
let (mut share_bytes, blame) =
|
||||||
let mut share = Zeroizing::new(
|
self.encryption.decrypt(rng, &mut batch, BatchId::Decryption(l), l, share_bytes);
|
||||||
Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or(DkgError::InvalidShare(l))?,
|
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();
|
share_bytes.zeroize();
|
||||||
*self.secret += share.deref();
|
*self.secret += share.deref();
|
||||||
|
|
||||||
// This can be insecurely linearized from n * t to just n using the below sums for a given
|
blames.insert(l, blame);
|
||||||
// stripe. Doing so uses naive addition which is subject to malleability. The only way to
|
batch.queue(
|
||||||
// ensure that malleability isn't present is to use this n * t algorithm, which runs
|
rng,
|
||||||
// per sender and not as an aggregate of all senders, which also enables blame
|
BatchId::Share(l),
|
||||||
let mut values = exponential(self.params.i, &self.commitments[&l]);
|
share_verification_statements::<C>(self.params.i(), &self.commitments[&l], share),
|
||||||
// multiexp will Zeroize this when it's done with it
|
);
|
||||||
values.push((-*share.deref(), C::generator()));
|
|
||||||
share.zeroize();
|
|
||||||
|
|
||||||
batch.queue(rng, l, values);
|
|
||||||
}
|
}
|
||||||
batch.verify_with_vartime_blame().map_err(DkgError::InvalidShare)?;
|
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
|
// Stripe commitments per t and sum them in advance. Calculating verification shares relies on
|
||||||
// these sums so preprocessing them is a massive speedup
|
// these sums so preprocessing them is a massive speedup
|
||||||
|
@ -352,16 +417,136 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||||
if i == self.params.i() {
|
if i == self.params.i() {
|
||||||
C::generator() * self.secret.deref()
|
C::generator() * self.secret.deref()
|
||||||
} else {
|
} else {
|
||||||
multiexp_vartime(&exponential(i, &stripes))
|
multiexp_vartime(&exponential::<C>(i, &stripes))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ThresholdCore {
|
let KeyMachine { commitments, encryption, params, secret } = self;
|
||||||
params: self.params,
|
Ok(BlameMachine {
|
||||||
secret_share: self.secret.clone(),
|
commitments,
|
||||||
group_key: stripes[0],
|
encryption,
|
||||||
verification_shares,
|
result: ThresholdCore {
|
||||||
|
params,
|
||||||
|
secret_share: secret,
|
||||||
|
group_key: stripes[0],
|
||||||
|
verification_shares,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct BlameMachine<C: Ciphersuite> {
|
||||||
|
commitments: HashMap<u16, Vec<C::G>>,
|
||||||
|
encryption: Encryption<C>,
|
||||||
|
result: ThresholdCore<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> Zeroize for BlameMachine<C> {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
for (_, commitments) in self.commitments.iter_mut() {
|
||||||
|
commitments.zeroize();
|
||||||
|
}
|
||||||
|
self.encryption.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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blame_internal(
|
||||||
|
&self,
|
||||||
|
sender: u16,
|
||||||
|
recipient: u16,
|
||||||
|
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
||||||
|
proof: Option<EncryptionKeyProof<C>>,
|
||||||
|
) -> u16 {
|
||||||
|
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 share = match Option::<C::F>::from(C::F::from_repr(share_bytes.0)) {
|
||||||
|
Some(share) => share,
|
||||||
|
// If this isn't a valid scalar, the sender is faulty
|
||||||
|
None => 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: u16,
|
||||||
|
recipient: u16,
|
||||||
|
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
||||||
|
proof: Option<EncryptionKeyProof<C>>,
|
||||||
|
) -> (AdditionalBlameMachine<C>, u16) {
|
||||||
|
let faulty = self.blame_internal(sender, recipient, msg, proof);
|
||||||
|
(AdditionalBlameMachine(self), faulty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Zeroize)]
|
||||||
|
pub struct AdditionalBlameMachine<C: Ciphersuite>(BlameMachine<C>);
|
||||||
|
impl<C: Ciphersuite> AdditionalBlameMachine<C> {
|
||||||
|
/// 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: u16,
|
||||||
|
recipient: u16,
|
||||||
|
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
||||||
|
proof: Option<EncryptionKeyProof<C>>,
|
||||||
|
) -> u16 {
|
||||||
|
self.0.blame_internal(sender, recipient, msg, proof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
//! Additional utilities around them, such as promotion from one generator to another, are also
|
//! Additional utilities around them, such as promotion from one generator to another, are also
|
||||||
//! provided.
|
//! provided.
|
||||||
|
|
||||||
use core::{fmt::Debug, ops::Deref};
|
use core::{
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
use std::{io::Read, sync::Arc, collections::HashMap};
|
use std::{io::Read, sync::Arc, collections::HashMap};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -34,8 +37,8 @@ pub mod promote;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
|
||||||
/// Various errors possible during key generation/signing.
|
/// Various errors possible during key generation/signing.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||||
pub enum DkgError {
|
pub enum DkgError<B: Clone + PartialEq + Eq + Debug> {
|
||||||
#[error("a parameter was 0 (required {0}, participants {1})")]
|
#[error("a parameter was 0 (required {0}, participants {1})")]
|
||||||
ZeroParameter(u16, u16),
|
ZeroParameter(u16, u16),
|
||||||
#[error("invalid amount of required participants (max {1}, got {0})")]
|
#[error("invalid amount of required participants (max {1}, got {0})")]
|
||||||
|
@ -54,19 +57,19 @@ pub enum DkgError {
|
||||||
|
|
||||||
#[error("invalid proof of knowledge (participant {0})")]
|
#[error("invalid proof of knowledge (participant {0})")]
|
||||||
InvalidProofOfKnowledge(u16),
|
InvalidProofOfKnowledge(u16),
|
||||||
#[error("invalid share (participant {0})")]
|
#[error("invalid share (participant {participant}, blame {blame})")]
|
||||||
InvalidShare(u16),
|
InvalidShare { participant: u16, blame: Option<B> },
|
||||||
|
|
||||||
#[error("internal error ({0})")]
|
#[error("internal error ({0})")]
|
||||||
InternalError(&'static str),
|
InternalError(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate a map of values to have the expected included participants
|
// Validate a map of values to have the expected included participants
|
||||||
pub(crate) fn validate_map<T>(
|
pub(crate) fn validate_map<T, B: Clone + PartialEq + Eq + Debug>(
|
||||||
map: &HashMap<u16, T>,
|
map: &HashMap<u16, T>,
|
||||||
included: &[u16],
|
included: &[u16],
|
||||||
ours: u16,
|
ours: u16,
|
||||||
) -> Result<(), DkgError> {
|
) -> Result<(), DkgError<B>> {
|
||||||
if (map.len() + 1) != included.len() {
|
if (map.len() + 1) != included.len() {
|
||||||
Err(DkgError::InvalidParticipantQuantity(included.len(), map.len() + 1))?;
|
Err(DkgError::InvalidParticipantQuantity(included.len(), map.len() + 1))?;
|
||||||
}
|
}
|
||||||
|
@ -100,7 +103,7 @@ pub struct ThresholdParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThresholdParams {
|
impl ThresholdParams {
|
||||||
pub fn new(t: u16, n: u16, i: u16) -> Result<ThresholdParams, DkgError> {
|
pub fn new(t: u16, n: u16, i: u16) -> Result<ThresholdParams, DkgError<()>> {
|
||||||
if (t == 0) || (n == 0) {
|
if (t == 0) || (n == 0) {
|
||||||
Err(DkgError::ZeroParameter(t, n))?;
|
Err(DkgError::ZeroParameter(t, n))?;
|
||||||
}
|
}
|
||||||
|
@ -149,7 +152,7 @@ pub fn lagrange<F: PrimeField>(i: u16, included: &[u16]) -> F {
|
||||||
|
|
||||||
/// Keys and verification shares generated by a DKG.
|
/// Keys and verification shares generated by a DKG.
|
||||||
/// Called core as they're expected to be wrapped into an Arc before usage in various operations.
|
/// Called core as they're expected to be wrapped into an Arc before usage in various operations.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct ThresholdCore<C: Ciphersuite> {
|
pub struct ThresholdCore<C: Ciphersuite> {
|
||||||
/// Threshold Parameters.
|
/// Threshold Parameters.
|
||||||
params: ThresholdParams,
|
params: ThresholdParams,
|
||||||
|
@ -162,6 +165,17 @@ pub struct ThresholdCore<C: Ciphersuite> {
|
||||||
verification_shares: HashMap<u16, C::G>,
|
verification_shares: HashMap<u16, C::G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> Debug for ThresholdCore<C> {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||||
|
fmt
|
||||||
|
.debug_struct("ThresholdCore")
|
||||||
|
.field("params", &self.params)
|
||||||
|
.field("group_key", &self.group_key)
|
||||||
|
.field("verification_shares", &self.verification_shares)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for ThresholdCore<C> {
|
impl<C: Ciphersuite> Zeroize for ThresholdCore<C> {
|
||||||
fn zeroize(&mut self) {
|
fn zeroize(&mut self) {
|
||||||
self.params.zeroize();
|
self.params.zeroize();
|
||||||
|
@ -179,8 +193,12 @@ impl<C: Ciphersuite> ThresholdCore<C> {
|
||||||
secret_share: Zeroizing<C::F>,
|
secret_share: Zeroizing<C::F>,
|
||||||
verification_shares: HashMap<u16, C::G>,
|
verification_shares: HashMap<u16, C::G>,
|
||||||
) -> ThresholdCore<C> {
|
) -> ThresholdCore<C> {
|
||||||
#[cfg(debug_assertions)]
|
debug_assert!(validate_map::<_, ()>(
|
||||||
validate_map(&verification_shares, &(0 ..= params.n).collect::<Vec<_>>(), 0).unwrap();
|
&verification_shares,
|
||||||
|
&(0 ..= params.n).collect::<Vec<_>>(),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
let t = (1 ..= params.t).collect::<Vec<_>>();
|
let t = (1 ..= params.t).collect::<Vec<_>>();
|
||||||
ThresholdCore {
|
ThresholdCore {
|
||||||
|
@ -220,15 +238,15 @@ impl<C: Ciphersuite> ThresholdCore<C> {
|
||||||
serialized
|
serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize<R: Read>(reader: &mut R) -> Result<ThresholdCore<C>, DkgError> {
|
pub fn deserialize<R: Read>(reader: &mut R) -> Result<ThresholdCore<C>, DkgError<()>> {
|
||||||
{
|
{
|
||||||
let missing = DkgError::InternalError("ThresholdCore serialization is missing its curve");
|
let missing = DkgError::InternalError("ThresholdCore serialization is missing its curve");
|
||||||
let different = DkgError::InternalError("deserializing ThresholdCore for another curve");
|
let different = DkgError::InternalError("deserializing ThresholdCore for another curve");
|
||||||
|
|
||||||
let mut id_len = [0; 4];
|
let mut id_len = [0; 4];
|
||||||
reader.read_exact(&mut id_len).map_err(|_| missing)?;
|
reader.read_exact(&mut id_len).map_err(|_| missing.clone())?;
|
||||||
if u32::try_from(C::ID.len()).unwrap().to_be_bytes() != id_len {
|
if u32::try_from(C::ID.len()).unwrap().to_be_bytes() != id_len {
|
||||||
Err(different)?;
|
Err(different.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut id = vec![0; C::ID.len()];
|
let mut id = vec![0; C::ID.len()];
|
||||||
|
@ -273,27 +291,42 @@ impl<C: Ciphersuite> ThresholdCore<C> {
|
||||||
/// Threshold keys usable for signing.
|
/// Threshold keys usable for signing.
|
||||||
#[derive(Clone, Debug, Zeroize)]
|
#[derive(Clone, Debug, Zeroize)]
|
||||||
pub struct ThresholdKeys<C: Ciphersuite> {
|
pub struct ThresholdKeys<C: Ciphersuite> {
|
||||||
/// Core keys.
|
// Core keys.
|
||||||
|
// If this is the last reference, the underlying keys will be dropped. When that happens, the
|
||||||
|
// private key present within it will be zeroed out (as it's within Zeroizing).
|
||||||
#[zeroize(skip)]
|
#[zeroize(skip)]
|
||||||
core: Arc<ThresholdCore<C>>,
|
core: Arc<ThresholdCore<C>>,
|
||||||
|
|
||||||
/// Offset applied to these keys.
|
// Offset applied to these keys.
|
||||||
pub(crate) offset: Option<C::F>,
|
pub(crate) offset: Option<C::F>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// View of keys passed to algorithm implementations.
|
/// View of keys passed to algorithm implementations.
|
||||||
#[derive(Clone, Zeroize)]
|
#[derive(Clone)]
|
||||||
pub struct ThresholdView<C: Ciphersuite> {
|
pub struct ThresholdView<C: Ciphersuite> {
|
||||||
offset: C::F,
|
offset: C::F,
|
||||||
group_key: C::G,
|
group_key: C::G,
|
||||||
included: Vec<u16>,
|
included: Vec<u16>,
|
||||||
secret_share: Zeroizing<C::F>,
|
secret_share: Zeroizing<C::F>,
|
||||||
#[zeroize(skip)]
|
|
||||||
original_verification_shares: HashMap<u16, C::G>,
|
original_verification_shares: HashMap<u16, C::G>,
|
||||||
#[zeroize(skip)]
|
|
||||||
verification_shares: HashMap<u16, C::G>,
|
verification_shares: HashMap<u16, C::G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> Zeroize for ThresholdView<C> {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.offset.zeroize();
|
||||||
|
self.group_key.zeroize();
|
||||||
|
self.included.zeroize();
|
||||||
|
self.secret_share.zeroize();
|
||||||
|
for (_, share) in self.original_verification_shares.iter_mut() {
|
||||||
|
share.zeroize();
|
||||||
|
}
|
||||||
|
for (_, share) in self.verification_shares.iter_mut() {
|
||||||
|
share.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> ThresholdKeys<C> {
|
impl<C: Ciphersuite> ThresholdKeys<C> {
|
||||||
pub fn new(core: ThresholdCore<C>) -> ThresholdKeys<C> {
|
pub fn new(core: ThresholdCore<C>) -> ThresholdKeys<C> {
|
||||||
ThresholdKeys { core: Arc::new(core), offset: None }
|
ThresholdKeys { core: Arc::new(core), offset: None }
|
||||||
|
@ -338,7 +371,7 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
|
||||||
self.core.serialize()
|
self.core.serialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self, included: &[u16]) -> Result<ThresholdView<C>, DkgError> {
|
pub fn view(&self, included: &[u16]) -> Result<ThresholdView<C>, DkgError<()>> {
|
||||||
if (included.len() < self.params().t.into()) || (usize::from(self.params().n) < included.len())
|
if (included.len() < self.params().t.into()) || (usize::from(self.params().n) < included.len())
|
||||||
{
|
{
|
||||||
Err(DkgError::InvalidSigningSet)?;
|
Err(DkgError::InvalidSigningSet)?;
|
||||||
|
|
|
@ -44,13 +44,13 @@ pub struct GeneratorProof<C: Ciphersuite> {
|
||||||
impl<C: Ciphersuite> GeneratorProof<C> {
|
impl<C: Ciphersuite> GeneratorProof<C> {
|
||||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
writer.write_all(self.share.to_bytes().as_ref())?;
|
writer.write_all(self.share.to_bytes().as_ref())?;
|
||||||
self.proof.serialize(writer)
|
self.proof.write(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(reader: &mut R) -> io::Result<GeneratorProof<C>> {
|
pub fn read<R: Read>(reader: &mut R) -> io::Result<GeneratorProof<C>> {
|
||||||
Ok(GeneratorProof {
|
Ok(GeneratorProof {
|
||||||
share: <C as Ciphersuite>::read_G(reader)?,
|
share: <C as Ciphersuite>::read_G(reader)?,
|
||||||
proof: DLEqProof::deserialize(reader)?,
|
proof: DLEqProof::read(reader)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ where
|
||||||
pub fn complete(
|
pub fn complete(
|
||||||
self,
|
self,
|
||||||
proofs: &HashMap<u16, GeneratorProof<C1>>,
|
proofs: &HashMap<u16, GeneratorProof<C1>>,
|
||||||
) -> Result<ThresholdKeys<C2>, DkgError> {
|
) -> Result<ThresholdKeys<C2>, DkgError<()>> {
|
||||||
let params = self.base.params();
|
let params = self.base.params();
|
||||||
validate_map(proofs, &(1 ..= params.n).collect::<Vec<_>>(), params.i)?;
|
validate_map(proofs, &(1 ..= params.n).collect::<Vec<_>>(), params.i)?;
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,23 @@ use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Ciphersuite, ThresholdParams, ThresholdCore,
|
Ciphersuite, ThresholdParams, ThresholdCore,
|
||||||
frost::KeyGenMachine,
|
frost::{KeyGenMachine, SecretShare, KeyMachine},
|
||||||
encryption::{EncryptionKeyMessage, EncryptedMessage},
|
encryption::{EncryptionKeyMessage, EncryptedMessage},
|
||||||
tests::{THRESHOLD, PARTICIPANTS, clone_without},
|
tests::{THRESHOLD, PARTICIPANTS, clone_without},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Fully perform the FROST key generation algorithm.
|
// Needed so rustfmt doesn't fail to format on line length issues
|
||||||
pub fn frost_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
type FrostEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
|
||||||
|
type FrostSecretShares<C> = HashMap<u16, FrostEncryptedMessage<C>>;
|
||||||
|
|
||||||
|
// Commit, then return enc key and shares
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
) -> HashMap<u16, ThresholdCore<C>> {
|
) -> (HashMap<u16, KeyMachine<C>>, HashMap<u16, C::G>, HashMap<u16, FrostSecretShares<C>>) {
|
||||||
let mut machines = HashMap::new();
|
let mut machines = HashMap::new();
|
||||||
let mut commitments = HashMap::new();
|
let mut commitments = HashMap::new();
|
||||||
|
let mut enc_keys = HashMap::new();
|
||||||
for i in 1 ..= PARTICIPANTS {
|
for i in 1 ..= PARTICIPANTS {
|
||||||
let machine = KeyGenMachine::<C>::new(
|
let machine = KeyGenMachine::<C>::new(
|
||||||
ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap(),
|
ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap(),
|
||||||
|
@ -31,10 +37,11 @@ pub fn frost_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
enc_keys.insert(i, commitments[&i].enc_key());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut secret_shares = HashMap::new();
|
let mut secret_shares = HashMap::new();
|
||||||
let mut machines = machines
|
let machines = machines
|
||||||
.drain()
|
.drain()
|
||||||
.map(|(l, machine)| {
|
.map(|(l, machine)| {
|
||||||
let (machine, mut shares) =
|
let (machine, mut shares) =
|
||||||
|
@ -57,19 +64,36 @@ pub fn frost_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
(machines, enc_keys, secret_shares)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_secret_shares<C: Ciphersuite>(
|
||||||
|
shares: &HashMap<u16, FrostSecretShares<C>>,
|
||||||
|
recipient: u16,
|
||||||
|
) -> FrostSecretShares<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 FROST key generation algorithm.
|
||||||
|
pub fn frost_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
|
rng: &mut R,
|
||||||
|
) -> HashMap<u16, ThresholdCore<C>> {
|
||||||
|
let (mut machines, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng);
|
||||||
|
|
||||||
let mut verification_shares = None;
|
let mut verification_shares = None;
|
||||||
let mut group_key = None;
|
let mut group_key = None;
|
||||||
machines
|
machines
|
||||||
.drain()
|
.drain()
|
||||||
.map(|(i, machine)| {
|
.map(|(i, machine)| {
|
||||||
let mut our_secret_shares = HashMap::new();
|
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||||
for (l, shares) in &secret_shares {
|
let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete();
|
||||||
if i == *l {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
our_secret_shares.insert(*l, shares[&i].clone());
|
|
||||||
}
|
|
||||||
let these_keys = machine.complete(rng, our_secret_shares).unwrap();
|
|
||||||
|
|
||||||
// Verify the verification_shares are agreed upon
|
// Verify the verification_shares are agreed upon
|
||||||
if verification_shares.is_none() {
|
if verification_shares.is_none() {
|
||||||
|
@ -87,3 +111,188 @@ pub fn frost_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>()
|
.collect::<HashMap<_, _>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod literal {
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use ciphersuite::Ristretto;
|
||||||
|
|
||||||
|
use crate::{DkgError, encryption::EncryptionKeyProof, frost::BlameMachine};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_blame(
|
||||||
|
machines: Vec<BlameMachine<Ristretto>>,
|
||||||
|
msg: FrostEncryptedMessage<Ristretto>,
|
||||||
|
blame: Option<EncryptionKeyProof<Ristretto>>,
|
||||||
|
) {
|
||||||
|
for machine in machines {
|
||||||
|
let (additional, blamed) = machine.blame(1, 2, msg.clone(), blame.clone());
|
||||||
|
assert_eq!(blamed, 1);
|
||||||
|
// Verify additional blame also works
|
||||||
|
assert_eq!(additional.blame(1, 2, msg.clone(), blame.clone()), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Write a macro which expands to the following
|
||||||
|
#[test]
|
||||||
|
fn invalid_encryption_pop_blame() {
|
||||||
|
let (mut machines, _, 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(&1).unwrap().get_mut(&2).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 == 2 {
|
||||||
|
assert_eq!(machine.err(), Some(DkgError::InvalidShare { participant: 1, 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(machines, secret_shares[&1][&2].clone(), blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_ecdh_blame() {
|
||||||
|
let (mut machines, _, 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(&2).unwrap().get_mut(&1).unwrap().invalidate_msg(&mut OsRng, 2);
|
||||||
|
|
||||||
|
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 == 1 {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(DkgError::InvalidShare { participant: 2, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
blame.as_mut().unwrap().as_mut().unwrap().invalidate_key();
|
||||||
|
test_blame(machines, secret_shares[&2][&1].clone(), blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should be largely equivalent to the prior test
|
||||||
|
#[test]
|
||||||
|
fn invalid_dleq_blame() {
|
||||||
|
let (mut machines, _, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
secret_shares.get_mut(&2).unwrap().get_mut(&1).unwrap().invalidate_msg(&mut OsRng, 2);
|
||||||
|
|
||||||
|
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 == 1 {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(DkgError::InvalidShare { participant: 2, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq();
|
||||||
|
test_blame(machines, secret_shares[&2][&1].clone(), blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_share_serialization_blame() {
|
||||||
|
let (mut machines, enc_keys, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
secret_shares.get_mut(&1).unwrap().get_mut(&2).unwrap().invalidate_share_serialization(
|
||||||
|
&mut OsRng,
|
||||||
|
b"FROST",
|
||||||
|
1,
|
||||||
|
enc_keys[&2],
|
||||||
|
);
|
||||||
|
|
||||||
|
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 == 2 {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(DkgError::InvalidShare { participant: 1, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
test_blame(machines, secret_shares[&1][&2].clone(), blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_share_value_blame() {
|
||||||
|
let (mut machines, enc_keys, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
secret_shares.get_mut(&1).unwrap().get_mut(&2).unwrap().invalidate_share_value(
|
||||||
|
&mut OsRng,
|
||||||
|
b"FROST",
|
||||||
|
1,
|
||||||
|
enc_keys[&2],
|
||||||
|
);
|
||||||
|
|
||||||
|
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 == 2 {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(DkgError::InvalidShare { participant: 1, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
test_blame(machines, secret_shares[&1][&2].clone(), blame.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -210,7 +210,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub(crate) fn serialize<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
pub(crate) fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
match self.Re_0 {
|
match self.Re_0 {
|
||||||
Re::R(R0, R1) => {
|
Re::R(R0, R1) => {
|
||||||
|
@ -230,7 +230,7 @@ where
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub(crate) fn deserialize<R: Read>(r: &mut R, mut Re_0: Re<G0, G1>) -> std::io::Result<Self> {
|
pub(crate) fn read<R: Read>(r: &mut R, mut Re_0: Re<G0, G1>) -> std::io::Result<Self> {
|
||||||
match Re_0 {
|
match Re_0 {
|
||||||
Re::R(ref mut R0, ref mut R1) => {
|
Re::R(ref mut R0, ref mut R1) => {
|
||||||
*R0 = read_point(r)?;
|
*R0 = read_point(r)?;
|
||||||
|
|
|
@ -166,17 +166,17 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub(crate) fn serialize<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
pub(crate) fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
w.write_all(self.commitments.0.to_bytes().as_ref())?;
|
w.write_all(self.commitments.0.to_bytes().as_ref())?;
|
||||||
w.write_all(self.commitments.1.to_bytes().as_ref())?;
|
w.write_all(self.commitments.1.to_bytes().as_ref())?;
|
||||||
self.signature.serialize(w)
|
self.signature.write(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub(crate) fn deserialize<R: Read>(r: &mut R) -> std::io::Result<Self> {
|
pub(crate) fn read<R: Read>(r: &mut R) -> std::io::Result<Self> {
|
||||||
Ok(Bits {
|
Ok(Bits {
|
||||||
commitments: (read_point(r)?, read_point(r)?),
|
commitments: (read_point(r)?, read_point(r)?),
|
||||||
signature: Aos::deserialize(r, BitSignature::from(SIGNATURE).aos_form())?,
|
signature: Aos::read(r, BitSignature::from(SIGNATURE).aos_form())?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -367,36 +367,32 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub fn serialize<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
for bit in &self.bits {
|
for bit in &self.bits {
|
||||||
bit.serialize(w)?;
|
bit.write(w)?;
|
||||||
}
|
}
|
||||||
if let Some(bit) = &self.remainder {
|
if let Some(bit) = &self.remainder {
|
||||||
bit.serialize(w)?;
|
bit.write(w)?;
|
||||||
}
|
}
|
||||||
self.poks.0.serialize(w)?;
|
self.poks.0.write(w)?;
|
||||||
self.poks.1.serialize(w)
|
self.poks.1.write(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub fn deserialize<R: Read>(r: &mut R) -> std::io::Result<Self> {
|
pub fn read<R: Read>(r: &mut R) -> std::io::Result<Self> {
|
||||||
let capacity = usize::try_from(G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY)).unwrap();
|
let capacity = usize::try_from(G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY)).unwrap();
|
||||||
let bits_per_group = BitSignature::from(SIGNATURE).bits();
|
let bits_per_group = BitSignature::from(SIGNATURE).bits();
|
||||||
|
|
||||||
let mut bits = Vec::with_capacity(capacity / bits_per_group);
|
let mut bits = Vec::with_capacity(capacity / bits_per_group);
|
||||||
for _ in 0 .. (capacity / bits_per_group) {
|
for _ in 0 .. (capacity / bits_per_group) {
|
||||||
bits.push(Bits::deserialize(r)?);
|
bits.push(Bits::read(r)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remainder = None;
|
let mut remainder = None;
|
||||||
if (capacity % bits_per_group) != 0 {
|
if (capacity % bits_per_group) != 0 {
|
||||||
remainder = Some(Bits::deserialize(r)?);
|
remainder = Some(Bits::read(r)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(__DLEqProof {
|
Ok(__DLEqProof { bits, remainder, poks: (SchnorrPoK::read(r)?, SchnorrPoK::read(r)?) })
|
||||||
bits,
|
|
||||||
remainder,
|
|
||||||
poks: (SchnorrPoK::deserialize(r)?, SchnorrPoK::deserialize(r)?),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,13 +79,13 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub fn serialize<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
w.write_all(self.R.to_bytes().as_ref())?;
|
w.write_all(self.R.to_bytes().as_ref())?;
|
||||||
w.write_all(self.s.to_repr().as_ref())
|
w.write_all(self.s.to_repr().as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub fn deserialize<R: Read>(r: &mut R) -> std::io::Result<SchnorrPoK<G>> {
|
pub fn read<R: Read>(r: &mut R) -> std::io::Result<SchnorrPoK<G>> {
|
||||||
Ok(SchnorrPoK { R: read_point(r)?, s: read_scalar(r)? })
|
Ok(SchnorrPoK { R: read_point(r)?, s: read_scalar(r)? })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,10 +90,12 @@ impl<G: PrimeGroup> DLEqProof<G> {
|
||||||
|
|
||||||
transcript.domain_separate(b"dleq");
|
transcript.domain_separate(b"dleq");
|
||||||
for generator in generators {
|
for generator in generators {
|
||||||
|
// R, A
|
||||||
Self::transcript(transcript, *generator, *generator * r.deref(), *generator * scalar.deref());
|
Self::transcript(transcript, *generator, *generator * r.deref(), *generator * scalar.deref());
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = challenge(transcript);
|
let c = challenge(transcript);
|
||||||
|
// r + ca
|
||||||
let s = (c * scalar.deref()) + r.deref();
|
let s = (c * scalar.deref()) + r.deref();
|
||||||
|
|
||||||
DLEqProof { c, s }
|
DLEqProof { c, s }
|
||||||
|
@ -111,6 +113,9 @@ impl<G: PrimeGroup> DLEqProof<G> {
|
||||||
|
|
||||||
transcript.domain_separate(b"dleq");
|
transcript.domain_separate(b"dleq");
|
||||||
for (generator, point) in generators.iter().zip(points) {
|
for (generator, point) in generators.iter().zip(points) {
|
||||||
|
// s = r + ca
|
||||||
|
// sG - cA = R
|
||||||
|
// R, A
|
||||||
Self::transcript(transcript, *generator, (*generator * self.s) - (*point * self.c), *point);
|
Self::transcript(transcript, *generator, (*generator * self.s) - (*point * self.c), *point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,13 +127,20 @@ impl<G: PrimeGroup> DLEqProof<G> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub fn serialize<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
w.write_all(self.c.to_repr().as_ref())?;
|
w.write_all(self.c.to_repr().as_ref())?;
|
||||||
w.write_all(self.s.to_repr().as_ref())
|
w.write_all(self.s.to_repr().as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
pub fn deserialize<R: Read>(r: &mut R) -> io::Result<DLEqProof<G>> {
|
pub fn read<R: Read>(r: &mut R) -> io::Result<DLEqProof<G>> {
|
||||||
Ok(DLEqProof { c: read_scalar(r)?, s: read_scalar(r)? })
|
Ok(DLEqProof { c: read_scalar(r)?, s: read_scalar(r)? })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = vec![];
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ use crate::{
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
fn test_aos_serialization<const RING_LEN: usize>(proof: Aos<G0, G1, RING_LEN>, Re_0: Re<G0, G1>) {
|
fn test_aos_serialization<const RING_LEN: usize>(proof: Aos<G0, G1, RING_LEN>, Re_0: Re<G0, G1>) {
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
proof.serialize(&mut buf).unwrap();
|
proof.write(&mut buf).unwrap();
|
||||||
let deserialized = Aos::deserialize(&mut std::io::Cursor::new(buf), Re_0).unwrap();
|
let deserialized = Aos::read::<&[u8]>(&mut buf.as_ref(), Re_0).unwrap();
|
||||||
assert_eq!(proof, deserialized);
|
assert_eq!(proof, deserialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,8 +60,8 @@ macro_rules! verify_and_deserialize {
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
{
|
{
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
$proof.serialize(&mut buf).unwrap();
|
$proof.write(&mut buf).unwrap();
|
||||||
let deserialized = <$type>::deserialize(&mut std::io::Cursor::new(&buf)).unwrap();
|
let deserialized = <$type>::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||||
assert_eq!($proof, deserialized);
|
assert_eq!($proof, deserialized);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -96,7 +96,7 @@ macro_rules! test_dleq {
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
{
|
{
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
proofs[0].serialize(&mut buf).unwrap();
|
proofs[0].write(&mut buf).unwrap();
|
||||||
println!("{} had a proof size of {} bytes", $str, buf.len());
|
println!("{} had a proof size of {} bytes", $str, buf.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,9 +88,8 @@ fn test_dleq() {
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
{
|
{
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
proof.serialize(&mut buf).unwrap();
|
proof.write(&mut buf).unwrap();
|
||||||
let deserialized =
|
let deserialized = DLEqProof::<ProjectivePoint>::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
||||||
DLEqProof::<ProjectivePoint>::deserialize(&mut std::io::Cursor::new(&buf)).unwrap();
|
|
||||||
assert_eq!(proof, deserialized);
|
assert_eq!(proof, deserialized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ impl<C: Curve> NonceCommitments<C> {
|
||||||
let mut dleqs = None;
|
let mut dleqs = None;
|
||||||
if generators.len() >= 2 {
|
if generators.len() >= 2 {
|
||||||
let mut verify = |i| -> io::Result<_> {
|
let mut verify = |i| -> io::Result<_> {
|
||||||
let dleq = DLEqProof::deserialize(reader)?;
|
let dleq = DLEqProof::read(reader)?;
|
||||||
dleq
|
dleq
|
||||||
.verify(
|
.verify(
|
||||||
&mut dleq_transcript::<T>(context),
|
&mut dleq_transcript::<T>(context),
|
||||||
|
@ -140,8 +140,8 @@ impl<C: Curve> NonceCommitments<C> {
|
||||||
generator.write(writer)?;
|
generator.write(writer)?;
|
||||||
}
|
}
|
||||||
if let Some(dleqs) = &self.dleqs {
|
if let Some(dleqs) = &self.dleqs {
|
||||||
dleqs[0].serialize(writer)?;
|
dleqs[0].write(writer)?;
|
||||||
dleqs[1].serialize(writer)?;
|
dleqs[1].write(writer)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ impl<C: Curve> Commitments<C> {
|
||||||
if let Some(dleqs) = &nonce.dleqs {
|
if let Some(dleqs) = &nonce.dleqs {
|
||||||
let mut transcript_dleq = |label, dleq: &DLEqProof<C::G>| {
|
let mut transcript_dleq = |label, dleq: &DLEqProof<C::G>| {
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
dleq.serialize(&mut buf).unwrap();
|
dleq.write(&mut buf).unwrap();
|
||||||
t.append_message(label, &buf);
|
t.append_message(label, &buf);
|
||||||
};
|
};
|
||||||
transcript_dleq(b"dleq_D", &dleqs[0]);
|
transcript_dleq(b"dleq_D", &dleqs[0]);
|
||||||
|
|
|
@ -7,11 +7,14 @@ use std::{
|
||||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use transcript::Transcript;
|
use transcript::Transcript;
|
||||||
|
|
||||||
use group::{ff::PrimeField, GroupEncoding};
|
use group::{
|
||||||
|
ff::{Field, PrimeField},
|
||||||
|
GroupEncoding,
|
||||||
|
};
|
||||||
use multiexp::BatchVerifier;
|
use multiexp::BatchVerifier;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -46,6 +49,7 @@ impl<T: Writable> Writable for Vec<T> {
|
||||||
/// Pairing of an Algorithm with a ThresholdKeys instance and this specific signing set.
|
/// Pairing of an Algorithm with a ThresholdKeys instance and this specific signing set.
|
||||||
#[derive(Clone, Zeroize)]
|
#[derive(Clone, Zeroize)]
|
||||||
pub struct Params<C: Curve, A: Algorithm<C>> {
|
pub struct Params<C: Curve, A: Algorithm<C>> {
|
||||||
|
// Skips the algorithm due to being too large a bound to feasibly enforce on users
|
||||||
#[zeroize(skip)]
|
#[zeroize(skip)]
|
||||||
algorithm: A,
|
algorithm: A,
|
||||||
keys: ThresholdKeys<C>,
|
keys: ThresholdKeys<C>,
|
||||||
|
@ -78,8 +82,11 @@ impl<C: Curve, A: Addendum> Writable for Preprocess<C, A> {
|
||||||
/// A cached preprocess. A preprocess MUST only be used once. Reuse will enable third-party
|
/// A cached preprocess. A preprocess MUST only be used once. Reuse will enable third-party
|
||||||
/// recovery of your private key share. Additionally, this MUST be handled with the same security
|
/// recovery of your private key share. Additionally, this MUST be handled with the same security
|
||||||
/// as your private key share, as knowledge of it also enables recovery.
|
/// as your private key share, as knowledge of it also enables recovery.
|
||||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
// Directly exposes the [u8; 32] member to void needing to route through std::io interfaces.
|
||||||
pub struct CachedPreprocess(pub [u8; 32]);
|
// Still uses Zeroizing internally so when users grab it, they have a higher likelihood of
|
||||||
|
// appreciating how to handle it and don't immediately start copying it just by grabbing it.
|
||||||
|
#[derive(Zeroize)]
|
||||||
|
pub struct CachedPreprocess(pub Zeroizing<[u8; 32]>);
|
||||||
|
|
||||||
/// Trait for the initial state machine of a two-round signing protocol.
|
/// Trait for the initial state machine of a two-round signing protocol.
|
||||||
pub trait PreprocessMachine {
|
pub trait PreprocessMachine {
|
||||||
|
@ -110,11 +117,11 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
||||||
|
|
||||||
fn seeded_preprocess(
|
fn seeded_preprocess(
|
||||||
self,
|
self,
|
||||||
seed: Zeroizing<CachedPreprocess>,
|
seed: CachedPreprocess,
|
||||||
) -> (AlgorithmSignMachine<C, A>, Preprocess<C, A::Addendum>) {
|
) -> (AlgorithmSignMachine<C, A>, Preprocess<C, A::Addendum>) {
|
||||||
let mut params = self.params;
|
let mut params = self.params;
|
||||||
|
|
||||||
let mut rng = ChaCha20Rng::from_seed(seed.0);
|
let mut rng = ChaCha20Rng::from_seed(*seed.0);
|
||||||
// Get a challenge to the existing transcript for use when proving for the commitments
|
// Get a challenge to the existing transcript for use when proving for the commitments
|
||||||
let commitments_challenge = params.algorithm.transcript().challenge(b"commitments");
|
let commitments_challenge = params.algorithm.transcript().challenge(b"commitments");
|
||||||
let (nonces, commitments) = Commitments::new::<_, A::Transcript>(
|
let (nonces, commitments) = Commitments::new::<_, A::Transcript>(
|
||||||
|
@ -153,7 +160,7 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
||||||
commitments_challenge: self.params.algorithm.transcript().challenge(b"commitments"),
|
commitments_challenge: self.params.algorithm.transcript().challenge(b"commitments"),
|
||||||
|
|
||||||
params: self.params,
|
params: self.params,
|
||||||
seed: Zeroizing::new(CachedPreprocess([0; 32])),
|
seed: CachedPreprocess(Zeroizing::new([0; 32])),
|
||||||
|
|
||||||
nonces,
|
nonces,
|
||||||
preprocess,
|
preprocess,
|
||||||
|
@ -174,7 +181,7 @@ impl<C: Curve, A: Algorithm<C>> PreprocessMachine for AlgorithmMachine<C, A> {
|
||||||
self,
|
self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
) -> (Self::SignMachine, Preprocess<C, A::Addendum>) {
|
) -> (Self::SignMachine, Preprocess<C, A::Addendum>) {
|
||||||
let mut seed = Zeroizing::new(CachedPreprocess([0; 32]));
|
let mut seed = CachedPreprocess(Zeroizing::new([0; 32]));
|
||||||
rng.fill_bytes(seed.0.as_mut());
|
rng.fill_bytes(seed.0.as_mut());
|
||||||
self.seeded_preprocess(seed)
|
self.seeded_preprocess(seed)
|
||||||
}
|
}
|
||||||
|
@ -188,6 +195,12 @@ impl<C: Curve> Writable for SignatureShare<C> {
|
||||||
writer.write_all(self.0.to_repr().as_ref())
|
writer.write_all(self.0.to_repr().as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(any(test, feature = "tests"))]
|
||||||
|
impl<C: Curve> SignatureShare<C> {
|
||||||
|
pub(crate) fn invalidate(&mut self) {
|
||||||
|
self.0 += C::F::one();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Trait for the second machine of a two-round signing protocol.
|
/// Trait for the second machine of a two-round signing protocol.
|
||||||
pub trait SignMachine<S>: Sized {
|
pub trait SignMachine<S>: Sized {
|
||||||
|
@ -206,14 +219,14 @@ pub trait SignMachine<S>: Sized {
|
||||||
/// of it enables recovery of your private key share. Third-party recovery of a cached preprocess
|
/// of it enables recovery of your private key share. Third-party recovery of a cached preprocess
|
||||||
/// also enables recovery of your private key share, so this MUST be treated with the same
|
/// also enables recovery of your private key share, so this MUST be treated with the same
|
||||||
/// security as your private key share.
|
/// security as your private key share.
|
||||||
fn cache(self) -> Zeroizing<CachedPreprocess>;
|
fn cache(self) -> CachedPreprocess;
|
||||||
|
|
||||||
/// Create a sign machine from a cached preprocess. After this, the preprocess should be fully
|
/// Create a sign machine from a cached preprocess. After this, the preprocess should be fully
|
||||||
/// deleted, as it must never be reused. It is
|
/// deleted, as it must never be reused. It is
|
||||||
fn from_cache(
|
fn from_cache(
|
||||||
params: Self::Params,
|
params: Self::Params,
|
||||||
keys: Self::Keys,
|
keys: Self::Keys,
|
||||||
cache: Zeroizing<CachedPreprocess>,
|
cache: CachedPreprocess,
|
||||||
) -> Result<Self, FrostError>;
|
) -> Result<Self, FrostError>;
|
||||||
|
|
||||||
/// Read a Preprocess message. Despite taking self, this does not save the preprocess.
|
/// Read a Preprocess message. Despite taking self, this does not save the preprocess.
|
||||||
|
@ -235,10 +248,11 @@ pub trait SignMachine<S>: Sized {
|
||||||
#[derive(Zeroize)]
|
#[derive(Zeroize)]
|
||||||
pub struct AlgorithmSignMachine<C: Curve, A: Algorithm<C>> {
|
pub struct AlgorithmSignMachine<C: Curve, A: Algorithm<C>> {
|
||||||
params: Params<C, A>,
|
params: Params<C, A>,
|
||||||
seed: Zeroizing<CachedPreprocess>,
|
seed: CachedPreprocess,
|
||||||
|
|
||||||
commitments_challenge: <A::Transcript as Transcript>::Challenge,
|
commitments_challenge: <A::Transcript as Transcript>::Challenge,
|
||||||
pub(crate) nonces: Vec<Nonce<C>>,
|
pub(crate) nonces: Vec<Nonce<C>>,
|
||||||
|
// Skips the preprocess due to being too large a bound to feasibly enforce on users
|
||||||
#[zeroize(skip)]
|
#[zeroize(skip)]
|
||||||
pub(crate) preprocess: Preprocess<C, A::Addendum>,
|
pub(crate) preprocess: Preprocess<C, A::Addendum>,
|
||||||
pub(crate) blame_entropy: [u8; 32],
|
pub(crate) blame_entropy: [u8; 32],
|
||||||
|
@ -251,14 +265,14 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
||||||
type SignatureShare = SignatureShare<C>;
|
type SignatureShare = SignatureShare<C>;
|
||||||
type SignatureMachine = AlgorithmSignatureMachine<C, A>;
|
type SignatureMachine = AlgorithmSignatureMachine<C, A>;
|
||||||
|
|
||||||
fn cache(self) -> Zeroizing<CachedPreprocess> {
|
fn cache(self) -> CachedPreprocess {
|
||||||
self.seed
|
self.seed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_cache(
|
fn from_cache(
|
||||||
algorithm: A,
|
algorithm: A,
|
||||||
keys: ThresholdKeys<C>,
|
keys: ThresholdKeys<C>,
|
||||||
cache: Zeroizing<CachedPreprocess>,
|
cache: CachedPreprocess,
|
||||||
) -> Result<Self, FrostError> {
|
) -> Result<Self, FrostError> {
|
||||||
let (machine, _) = AlgorithmMachine::new(algorithm, keys)?.seeded_preprocess(cache);
|
let (machine, _) = AlgorithmMachine::new(algorithm, keys)?.seeded_preprocess(cache);
|
||||||
Ok(machine)
|
Ok(machine)
|
||||||
|
|
|
@ -59,7 +59,9 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_internal<
|
// Run the commit step and generate signature shares
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub(crate) fn commit_and_shares<
|
||||||
R: RngCore + CryptoRng,
|
R: RngCore + CryptoRng,
|
||||||
M: PreprocessMachine,
|
M: PreprocessMachine,
|
||||||
F: FnMut(&mut R, &mut HashMap<u16, M::SignMachine>),
|
F: FnMut(&mut R, &mut HashMap<u16, M::SignMachine>),
|
||||||
|
@ -68,7 +70,10 @@ fn sign_internal<
|
||||||
mut machines: HashMap<u16, M>,
|
mut machines: HashMap<u16, M>,
|
||||||
mut cache: F,
|
mut cache: F,
|
||||||
msg: &[u8],
|
msg: &[u8],
|
||||||
) -> M::Signature {
|
) -> (
|
||||||
|
HashMap<u16, <M::SignMachine as SignMachine<M::Signature>>::SignatureMachine>,
|
||||||
|
HashMap<u16, <M::SignMachine as SignMachine<M::Signature>>::SignatureShare>,
|
||||||
|
) {
|
||||||
let mut commitments = HashMap::new();
|
let mut commitments = HashMap::new();
|
||||||
let mut machines = machines
|
let mut machines = machines
|
||||||
.drain()
|
.drain()
|
||||||
|
@ -86,7 +91,7 @@ fn sign_internal<
|
||||||
cache(rng, &mut machines);
|
cache(rng, &mut machines);
|
||||||
|
|
||||||
let mut shares = HashMap::new();
|
let mut shares = HashMap::new();
|
||||||
let mut machines = machines
|
let machines = machines
|
||||||
.drain()
|
.drain()
|
||||||
.map(|(i, machine)| {
|
.map(|(i, machine)| {
|
||||||
let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap();
|
let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap();
|
||||||
|
@ -99,6 +104,21 @@ fn sign_internal<
|
||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
(machines, shares)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_internal<
|
||||||
|
R: RngCore + CryptoRng,
|
||||||
|
M: PreprocessMachine,
|
||||||
|
F: FnMut(&mut R, &mut HashMap<u16, M::SignMachine>),
|
||||||
|
>(
|
||||||
|
rng: &mut R,
|
||||||
|
machines: HashMap<u16, M>,
|
||||||
|
cache: F,
|
||||||
|
msg: &[u8],
|
||||||
|
) -> M::Signature {
|
||||||
|
let (mut machines, shares) = commit_and_shares(rng, machines, cache, msg);
|
||||||
|
|
||||||
let mut signature = None;
|
let mut signature = None;
|
||||||
for (i, machine) in machines.drain() {
|
for (i, machine) in machines.drain() {
|
||||||
let sig = machine.complete(clone_without(&shares, &i)).unwrap();
|
let sig = machine.complete(clone_without(&shares, &i)).unwrap();
|
||||||
|
|
|
@ -13,13 +13,13 @@ use dkg::tests::key_gen;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
curve::Curve,
|
curve::Curve,
|
||||||
ThresholdCore, ThresholdKeys,
|
ThresholdCore, ThresholdKeys, FrostError,
|
||||||
algorithm::{Schnorr, Hram},
|
algorithm::{Schnorr, Hram},
|
||||||
sign::{
|
sign::{
|
||||||
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, SignMachine,
|
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, SignMachine,
|
||||||
SignatureMachine, AlgorithmMachine,
|
SignatureMachine, AlgorithmMachine,
|
||||||
},
|
},
|
||||||
tests::{clone_without, recover_key, algorithm_machines, sign},
|
tests::{clone_without, recover_key, algorithm_machines, commit_and_shares, sign},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Vectors {
|
pub struct Vectors {
|
||||||
|
@ -127,6 +127,27 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
||||||
assert!(sig.verify(keys[&1].group_key(), H::hram(&sig.R, &keys[&1].group_key(), MSG)));
|
assert!(sig.verify(keys[&1].group_key(), H::hram(&sig.R, &keys[&1].group_key(), MSG)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test blame on an invalid Schnorr signature share
|
||||||
|
{
|
||||||
|
let keys = key_gen(&mut *rng);
|
||||||
|
let machines = algorithm_machines(&mut *rng, Schnorr::<C, H>::new(), &keys);
|
||||||
|
const MSG: &[u8] = b"Hello, World!";
|
||||||
|
|
||||||
|
let (mut machines, mut shares) = commit_and_shares(&mut *rng, machines, |_, _| {}, MSG);
|
||||||
|
let faulty = *shares.keys().into_iter().next().unwrap();
|
||||||
|
shares.get_mut(&faulty).unwrap().invalidate();
|
||||||
|
|
||||||
|
for (i, machine) in machines.drain() {
|
||||||
|
if i == faulty {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
machine.complete(clone_without(&shares, &i)).err(),
|
||||||
|
Some(FrostError::InvalidShare(faulty))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test against the vectors
|
// Test against the vectors
|
||||||
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
||||||
let group_key =
|
let group_key =
|
||||||
|
|
|
@ -7,7 +7,7 @@ pub struct MerlinTranscript(pub merlin::Transcript);
|
||||||
// Merlin doesn't implement Debug so provide a stub which won't panic
|
// Merlin doesn't implement Debug so provide a stub which won't panic
|
||||||
impl Debug for MerlinTranscript {
|
impl Debug for MerlinTranscript {
|
||||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
|
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||||
fmt.debug_struct("MerlinTranscript").finish()
|
fmt.debug_struct("MerlinTranscript").finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,27 @@ This results in a two-round protocol.
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
In order to protect the secret shares during communication, the `dkg` library
|
In order to protect the secret shares during communication, the `dkg` library
|
||||||
additionally sends an encryption key. These encryption keys are used in an ECDH
|
establishes a public key for encryption at the start of a given protocol.
|
||||||
to derive a shared key. This key is then hashed to obtain two keys and IVs, one
|
Every encrypted message (such as the secret shares) then includes a per-message
|
||||||
for sending and one for receiving, with the given counterparty. Chacha20 is used
|
encryption key. These two keys are used in an Elliptic-curve Diffie-Hellman
|
||||||
as the stream cipher.
|
handshake to derive a shared key. This shared key is then hashed to obtain a key
|
||||||
|
and IV for use in a ChaCha20 stream cipher instance, which is xor'd against a
|
||||||
|
message to encrypt it.
|
||||||
|
|
||||||
|
### Blame
|
||||||
|
|
||||||
|
Since each message has a distinct key attached, and accordingly a distinct
|
||||||
|
shared key, it's possible to reveal the shared key for a specific message
|
||||||
|
without revealing any other message's decryption keys. This is utilized when a
|
||||||
|
participant misbehaves. A participant who receives an invalid encrypted message
|
||||||
|
publishes its key, able to without concern for side effects, With the key
|
||||||
|
published, all participants can decrypt the message in order to decide blame.
|
||||||
|
|
||||||
|
While key reuse by a participant is considered as them revealing the messages
|
||||||
|
themselves, and therefore out of scope, there is an attack where a malicious
|
||||||
|
adversary claims another participant's encryption key. They'll fail to encrypt
|
||||||
|
their message, and the recipient will issue a blame statement. This blame
|
||||||
|
statement, intended to reveal the malicious adversary, also reveals the message
|
||||||
|
by the participant whose keys were co-opted. To resolve this, a
|
||||||
|
proof-of-possession is also included with encrypted messages, ensuring only
|
||||||
|
those actually with per-message keys can claim to use them.
|
||||||
|
|
Loading…
Reference in a new issue