Clean and document the DKG library's encryption

Encryption used to be inlined into FROST. When writing the documentation, I
realized it was decently hard to review. It also was antagonistic to other
hosted DKG algorithms by not allowing code re-use.

Encryption is now a standalone module, providing clear boundaries and
reusability.

Additionally, the DKG protocol itself used to use the ciphersuite's specified
hash function (with an HKDF to prevent length extension attacks). Now,
RecommendedTranscript is used to achieve much more robust transcripting and
remove the HKDF dependency. This does add Blake2 into all consumers yet is
preferred for its security properties and ease of review.
This commit is contained in:
Luke Parker 2022-12-07 17:20:20 -05:00
parent ba157ea84b
commit 13977f6287
No known key found for this signature in database
8 changed files with 265 additions and 147 deletions

2
Cargo.lock generated
View file

@ -1669,12 +1669,10 @@ version = "0.2.0"
dependencies = [
"chacha20 0.9.0",
"ciphersuite",
"digest 0.10.6",
"dleq",
"flexible-transcript",
"group",
"hex",
"hkdf",
"multiexp",
"rand_core 0.6.4",
"schnorr-signatures",

View file

@ -22,18 +22,12 @@ subtle = "2"
hex = "0.4"
digest = "0.10"
hkdf = "0.12"
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.2", features = ["recommended"] }
chacha20 = { version = "0.9", features = ["zeroize"] }
group = "0.12"
ciphersuite = { path = "../ciphersuite", version = "0.1", features = ["std"] }
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.2", features = ["recommended"] }
multiexp = { path = "../multiexp", version = "0.2", features = ["batch"] }
ciphersuite = { path = "../ciphersuite", version = "0.1", features = ["std"] }
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "0.2" }
dleq = { path = "../dleq", version = "0.2", features = ["serialize"] }

View file

@ -0,0 +1,181 @@
use core::{hash::Hash, fmt::Debug};
use std::{
ops::Deref,
io::{self, Read, Write},
collections::HashMap,
};
use zeroize::{Zeroize, Zeroizing};
use rand_core::{RngCore, CryptoRng};
use chacha20::{
cipher::{crypto_common::KeyIvInit, StreamCipher},
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
};
use group::GroupEncoding;
use ciphersuite::Ciphersuite;
use transcript::{Transcript, RecommendedTranscript};
use crate::ThresholdParams;
pub trait ReadWrite: Sized {
fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self>;
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()>;
fn serialize(&self) -> Vec<u8> {
let mut buf = vec![];
self.write(&mut buf).unwrap();
buf
}
}
pub trait Message: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite {}
impl<M: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite> Message for M {}
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
msg: M,
enc_key: C::G,
}
// Doesn't impl ReadWrite so that doesn't need to be imported
impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
Ok(Self { msg: M::read(reader, params)?, enc_key: C::read_G(reader)? })
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
self.msg.write(writer)?;
writer.write_all(self.enc_key.to_bytes().as_ref())
}
pub fn serialize(&self) -> Vec<u8> {
let mut buf = vec![];
self.write(&mut buf).unwrap();
buf
}
}
pub trait Encryptable: Clone + AsMut<[u8]> + Zeroize + ReadWrite {}
impl<E: Clone + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
#[derive(Clone, Zeroize)]
pub struct EncryptedMessage<E: Encryptable>(Zeroizing<E>);
impl<E: Encryptable> EncryptedMessage<E> {
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
Ok(Self(Zeroizing::new(E::read(reader, params)?)))
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
self.0.write(writer)
}
pub fn serialize(&self) -> Vec<u8> {
let mut buf = vec![];
self.write(&mut buf).unwrap();
buf
}
}
#[derive(Clone)]
pub(crate) struct Encryption<Id: Eq + Hash, C: Ciphersuite> {
dst: &'static [u8],
enc_key: Zeroizing<C::F>,
enc_pub_key: C::G,
enc_keys: HashMap<Id, C::G>,
}
impl<Id: Eq + Hash, C: Ciphersuite> Zeroize for Encryption<Id, C> {
fn zeroize(&mut self) {
self.enc_key.zeroize();
self.enc_pub_key.zeroize();
for (_, value) in self.enc_keys.drain() {
value.zeroize();
}
}
}
impl<Id: Eq + Hash, C: Ciphersuite> Encryption<Id, C> {
pub(crate) fn new<R: RngCore + CryptoRng>(dst: &'static [u8], rng: &mut R) -> Self {
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() }
}
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
}
pub(crate) fn register<M: Message>(
&mut self,
participant: Id,
msg: EncryptionKeyMessage<C, M>,
) -> M {
if self.enc_keys.contains_key(&participant) {
panic!("Re-registering encryption key for a participant");
}
self.enc_keys.insert(participant, msg.enc_key);
msg.msg
}
fn cipher(&self, participant: Id, encrypt: bool) -> ChaCha20 {
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
// TODO
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0");
transcript.domain_separate(self.dst);
let other = self.enc_keys[&participant];
if encrypt {
transcript.append_message(b"sender", self.enc_pub_key.to_bytes());
transcript.append_message(b"receiver", other.to_bytes());
} else {
transcript.append_message(b"sender", other.to_bytes());
transcript.append_message(b"receiver", self.enc_pub_key.to_bytes());
}
let mut shared = Zeroizing::new(other * self.enc_key.deref()).deref().to_bytes();
transcript.append_message(b"shared_key", shared.as_ref());
shared.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
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
}
}

View file

@ -9,49 +9,43 @@ use rand_core::{RngCore, CryptoRng};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use digest::Digest;
use hkdf::{Hkdf, hmac::SimpleHmac};
use chacha20::{
cipher::{crypto_common::KeyIvInit, StreamCipher},
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
};
use transcript::{Transcript, RecommendedTranscript};
use group::{
ff::{Field, PrimeField},
GroupEncoding,
};
use ciphersuite::Ciphersuite;
use multiexp::{multiexp_vartime, BatchVerifier};
use schnorr::SchnorrSignature;
use crate::{DkgError, ThresholdParams, ThresholdCore, validate_map};
use crate::{
DkgError, ThresholdParams, ThresholdCore, validate_map,
encryption::{ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption},
};
#[allow(non_snake_case)]
fn challenge<C: Ciphersuite>(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F {
const DST: &[u8] = b"FROST Schnorr Proof of Knowledge";
// Hashes the context to get a fixed size value out of it
let mut transcript = C::H::digest(context.as_bytes()).as_ref().to_vec();
transcript.extend(l.to_be_bytes());
transcript.extend(R);
transcript.extend(Am);
C::hash_to_F(DST, &transcript)
let mut transcript = RecommendedTranscript::new(b"DKG FROST v0");
transcript.domain_separate(b"Schnorr Proof of Knowledge");
transcript.append_message(b"context", context.as_bytes());
transcript.append_message(b"participant", l.to_le_bytes());
transcript.append_message(b"nonce", R);
transcript.append_message(b"commitments", Am);
C::hash_to_F(b"PoK 0", &transcript.challenge(b"challenge"))
}
/// Commitments message to be broadcast to all other parties.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Commitments<C: Ciphersuite> {
commitments: Vec<C::G>,
enc_key: C::G,
cached_msg: Vec<u8>,
sig: SchnorrSignature<C>,
}
impl<C: Ciphersuite> Commitments<C> {
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
impl<C: Ciphersuite> ReadWrite for Commitments<C> {
fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
let mut commitments = Vec::with_capacity(params.t().into());
let mut cached_msg = vec![];
@ -67,21 +61,14 @@ impl<C: Ciphersuite> Commitments<C> {
for _ in 0 .. params.t() {
commitments.push(read_G()?);
}
let enc_key = read_G()?;
Ok(Commitments { commitments, enc_key, cached_msg, sig: SchnorrSignature::read(reader)? })
Ok(Commitments { commitments, cached_msg, sig: SchnorrSignature::read(reader)? })
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.cached_msg)?;
self.sig.write(writer)
}
pub fn serialize(&self) -> Vec<u8> {
let mut buf = vec![];
self.write(&mut buf).unwrap();
buf
}
}
/// State machine to begin the key generation protocol.
@ -104,7 +91,7 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
pub fn generate_coefficients<R: RngCore + CryptoRng>(
self,
rng: &mut R,
) -> (SecretShareMachine<C>, Commitments<C>) {
) -> (SecretShareMachine<C>, EncryptionKeyMessage<C, Commitments<C>>) {
let t = usize::from(self.params.t);
let mut coefficients = Vec::with_capacity(t);
let mut commitments = Vec::with_capacity(t);
@ -118,14 +105,6 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
cached_msg.extend(commitments[i].to_bytes().as_ref());
}
// Generate an encryption key for transmitting the secret shares
// It would probably be perfectly fine to use one of our polynomial elements, yet doing so
// puts the integrity of FROST at risk. While there's almost no way it could, as it's used in
// an ECDH with validated group elemnents, better to avoid any questions on it
let enc_key = Zeroizing::new(C::random_nonzero_F(&mut *rng));
let pub_enc_key = C::generator() * enc_key.deref();
cached_msg.extend(pub_enc_key.to_bytes().as_ref());
// Step 2: Provide a proof of knowledge
let r = Zeroizing::new(C::random_nonzero_F(rng));
let nonce = C::generator() * r.deref();
@ -139,17 +118,21 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
challenge::<C>(&self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg),
);
// Additionally create an encryption mechanism to protect the secret shares
let encryption = Encryption::new(b"FROST", rng);
// Step 4: Broadcast
let msg =
encryption.registration(Commitments { commitments: commitments.clone(), cached_msg, sig });
(
SecretShareMachine {
params: self.params,
context: self.context,
coefficients,
our_commitments: commitments.clone(),
enc_key,
pub_enc_key,
our_commitments: commitments,
encryption,
},
Commitments { commitments, enc_key: pub_enc_key, cached_msg, sig },
msg,
)
}
}
@ -169,6 +152,11 @@ fn polynomial<F: PrimeField + Zeroize>(coefficients: &[Zeroizing<F>], l: u16) ->
/// Secret share to be sent to the party it's intended for over an authenticated channel.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SecretShare<F: PrimeField>(F::Repr);
impl<F: PrimeField> AsMut<[u8]> for SecretShare<F> {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
}
}
impl<F: PrimeField> Zeroize for SecretShare<F> {
fn zeroize(&mut self) {
self.0.as_mut().zeroize()
@ -181,59 +169,16 @@ impl<F: PrimeField> Drop for SecretShare<F> {
}
impl<F: PrimeField> ZeroizeOnDrop for SecretShare<F> {}
impl<F: PrimeField> SecretShare<F> {
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
impl<F: PrimeField> ReadWrite for SecretShare<F> {
fn read<R: Read>(reader: &mut R, _: ThresholdParams) -> io::Result<Self> {
let mut repr = F::Repr::default();
reader.read_exact(repr.as_mut())?;
Ok(SecretShare(repr))
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.0.as_ref())
}
pub fn serialize(&self) -> Vec<u8> {
let mut buf = vec![];
self.write(&mut buf).unwrap();
buf
}
}
fn create_ciphers<C: Ciphersuite>(
mut sender: <C::G as GroupEncoding>::Repr,
receiver: &mut <C::G as GroupEncoding>::Repr,
ecdh: &mut <C::G as GroupEncoding>::Repr,
) -> (ChaCha20, ChaCha20) {
let directional = |sender: &mut <C::G as GroupEncoding>::Repr| {
let mut key = Cc20Key::default();
key.copy_from_slice(
&Hkdf::<C::H, SimpleHmac<C::H>>::extract(
Some(b"key"),
&[sender.as_ref(), ecdh.as_ref()].concat(),
)
.0
.as_ref()[.. 32],
);
let mut iv = Cc20Iv::default();
iv.copy_from_slice(
&Hkdf::<C::H, SimpleHmac<C::H>>::extract(
Some(b"iv"),
&[sender.as_ref(), ecdh.as_ref()].concat(),
)
.0
.as_ref()[.. 12],
);
sender.as_mut().zeroize();
let res = ChaCha20::new(&key, &iv);
<Cc20Key as AsMut<[u8]>>::as_mut(&mut key).zeroize();
<Cc20Iv as AsMut<[u8]>>::as_mut(&mut iv).zeroize();
res
};
let res = (directional(&mut sender), directional(receiver));
ecdh.as_mut().zeroize();
res
}
/// Advancement of the key generation state machine.
@ -243,8 +188,7 @@ pub struct SecretShareMachine<C: Ciphersuite> {
context: String,
coefficients: Vec<Zeroizing<C::F>>,
our_commitments: Vec<C::G>,
enc_key: Zeroizing<C::F>,
pub_enc_key: C::G,
encryption: Encryption<u16, C>,
}
impl<C: Ciphersuite> SecretShareMachine<C> {
@ -253,16 +197,15 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
fn verify_r1<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
mut commitments: HashMap<u16, Commitments<C>>,
) -> Result<(HashMap<u16, Vec<C::G>>, HashMap<u16, C::G>), DkgError> {
mut commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
) -> Result<HashMap<u16, Vec<C::G>>, DkgError> {
validate_map(&commitments, &(1 ..= self.params.n()).collect::<Vec<_>>(), self.params.i())?;
let mut enc_keys = HashMap::new();
let mut batch = BatchVerifier::<u16, C::G>::new(commitments.len());
let mut commitments = commitments
.drain()
.map(|(l, mut msg)| {
enc_keys.insert(l, msg.enc_key);
.map(|(l, msg)| {
let mut msg = self.encryption.register(l, msg);
// Step 5: Validate each proof of knowledge
// This is solely the prep step for the latter batch verification
@ -281,7 +224,7 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
batch.verify_with_vartime_blame().map_err(DkgError::InvalidProofOfKnowledge)?;
commitments.insert(self.params.i, self.our_commitments.drain(..).collect());
Ok((commitments, enc_keys))
Ok(commitments)
}
/// Continue generating a key.
@ -291,13 +234,11 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
mut self,
rng: &mut R,
commitments: HashMap<u16, Commitments<C>>,
) -> Result<(KeyMachine<C>, HashMap<u16, SecretShare<C::F>>), DkgError> {
let (commitments, mut enc_keys) = self.verify_r1(&mut *rng, commitments)?;
commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
) -> Result<(KeyMachine<C>, HashMap<u16, EncryptedMessage<SecretShare<C::F>>>), DkgError> {
let commitments = self.verify_r1(&mut *rng, commitments)?;
// Step 1: Generate secret shares for all other parties
let sender = self.pub_enc_key.to_bytes();
let mut ciphers = HashMap::new();
let mut res = HashMap::new();
for l in 1 ..= self.params.n() {
// Don't insert our own shares to the byte buffer which is meant to be sent around
@ -306,31 +247,20 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
continue;
}
let (mut cipher_send, cipher_recv) = {
let receiver = enc_keys.get_mut(&l).unwrap();
let mut ecdh = (*receiver * self.enc_key.deref()).to_bytes();
create_ciphers::<C>(sender, &mut receiver.to_bytes(), &mut ecdh)
};
let mut share = polynomial(&self.coefficients, l);
let mut share_bytes = share.to_repr();
let share_bytes = Zeroizing::new(SecretShare::<C::F>(share.to_repr()));
share.zeroize();
cipher_send.apply_keystream(share_bytes.as_mut());
drop(cipher_send);
ciphers.insert(l, cipher_recv);
res.insert(l, SecretShare::<C::F>(share_bytes));
share_bytes.as_mut().zeroize();
res.insert(l, self.encryption.encrypt(l, share_bytes));
}
self.enc_key.zeroize();
// Calculate our own share
let share = polynomial(&self.coefficients, self.params.i());
self.coefficients.zeroize();
Ok((KeyMachine { params: self.params, secret: share, commitments, ciphers }, res))
Ok((
KeyMachine { params: self.params, secret: share, commitments, encryption: self.encryption },
res,
))
}
}
@ -338,24 +268,17 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
pub struct KeyMachine<C: Ciphersuite> {
params: ThresholdParams,
secret: Zeroizing<C::F>,
ciphers: HashMap<u16, ChaCha20>,
commitments: HashMap<u16, Vec<C::G>>,
encryption: Encryption<u16, C>,
}
impl<C: Ciphersuite> Zeroize for KeyMachine<C> {
fn zeroize(&mut self) {
self.params.zeroize();
self.secret.zeroize();
// cipher implements ZeroizeOnDrop and zeroizes on drop, yet doesn't implement Zeroize
// The following is redundant, as Rust should automatically handle dropping it, yet it shows
// awareness of this quirk and at least attempts to be comprehensive
for (_, cipher) in self.ciphers.drain() {
drop(cipher);
}
for (_, commitments) in self.commitments.iter_mut() {
commitments.zeroize();
}
self.encryption.zeroize();
}
}
impl<C: Ciphersuite> Drop for KeyMachine<C> {
@ -373,7 +296,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
pub fn complete<R: RngCore + CryptoRng>(
mut self,
rng: &mut R,
mut shares: HashMap<u16, SecretShare<C::F>>,
mut shares: HashMap<u16, EncryptedMessage<SecretShare<C::F>>>,
) -> Result<ThresholdCore<C>, DkgError> {
validate_map(&shares, &(1 ..= self.params.n()).collect::<Vec<_>>(), self.params.i())?;
@ -391,11 +314,8 @@ impl<C: Ciphersuite> KeyMachine<C> {
};
let mut batch = BatchVerifier::new(shares.len());
for (l, mut share_bytes) in shares.drain() {
let mut cipher = self.ciphers.remove(&l).unwrap();
cipher.apply_keystream(share_bytes.0.as_mut());
drop(cipher);
for (l, share_bytes) in shares.drain() {
let mut share_bytes = self.encryption.decrypt(l, share_bytes);
let mut share = Zeroizing::new(
Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or(DkgError::InvalidShare(l))?,
);

View file

@ -20,6 +20,8 @@ use group::{
use ciphersuite::Ciphersuite;
mod encryption;
/// The distributed key generation protocol described in the
/// [FROST paper](https://eprint.iacr.org/2020/852).
pub mod frost;

View file

@ -28,7 +28,7 @@ pub trait CiphersuitePromote<C2: Ciphersuite> {
}
fn transcript<G: GroupEncoding>(key: G, i: u16) -> RecommendedTranscript {
let mut transcript = RecommendedTranscript::new(b"FROST Generator Update");
let mut transcript = RecommendedTranscript::new(b"DKG Generator Promotion v0");
transcript.append_message(b"group_key", key.to_bytes());
transcript.append_message(b"participant", i.to_be_bytes());
transcript

View file

@ -4,7 +4,8 @@ use rand_core::{RngCore, CryptoRng};
use crate::{
Ciphersuite, ThresholdParams, ThresholdCore,
frost::{SecretShare, Commitments, KeyGenMachine},
frost::KeyGenMachine,
encryption::{EncryptionKeyMessage, EncryptedMessage},
tests::{THRESHOLD, PARTICIPANTS, clone_without},
};
@ -24,7 +25,7 @@ pub fn frost_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
commitments.insert(
i,
Commitments::read::<&[u8]>(
EncryptionKeyMessage::read::<&[u8]>(
&mut these_commitments.serialize().as_ref(),
ThresholdParams { t: THRESHOLD, n: PARTICIPANTS, i: 1 },
)
@ -41,7 +42,14 @@ pub fn frost_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
let shares = shares
.drain()
.map(|(l, share)| {
(l, SecretShare::<C::F>::read::<&[u8]>(&mut share.serialize().as_ref()).unwrap())
(
l,
EncryptedMessage::read::<&[u8]>(
&mut share.serialize().as_ref(),
ThresholdParams { t: THRESHOLD, n: PARTICIPANTS, i: 1 },
)
.unwrap(),
)
})
.collect::<HashMap<_, _>>();
secret_shares.insert(l, shares);

View file

@ -0,0 +1,15 @@
# Distributed Key Generation
Serai uses a modification of Pedersen's Distributed Key Generation, which is
actually Feldman's Verifiable Secret Sharing Scheme run by every participant, as
described in the FROST paper. The modification included in FROST was to include
a Schnorr Proof of Knowledge for coefficient zero, preventing rogue key attacks.
This results in a two-round protocol.
### Encryption
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
to derive a shared key. This key is then hashed to obtain two keys and IVs, one
for sending and one for receiving, with the given counterparty. Chacha20 is used
as the stream cipher.