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 = [ dependencies = [
"chacha20 0.9.0", "chacha20 0.9.0",
"ciphersuite", "ciphersuite",
"digest 0.10.6",
"dleq", "dleq",
"flexible-transcript", "flexible-transcript",
"group", "group",
"hex", "hex",
"hkdf",
"multiexp", "multiexp",
"rand_core 0.6.4", "rand_core 0.6.4",
"schnorr-signatures", "schnorr-signatures",

View file

@ -22,18 +22,12 @@ subtle = "2"
hex = "0.4" hex = "0.4"
digest = "0.10" transcript = { package = "flexible-transcript", path = "../transcript", version = "0.2", features = ["recommended"] }
hkdf = "0.12"
chacha20 = { version = "0.9", features = ["zeroize"] } chacha20 = { version = "0.9", features = ["zeroize"] }
group = "0.12" 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"] } 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" } schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "0.2" }
dleq = { path = "../dleq", version = "0.2", features = ["serialize"] } 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 zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use digest::Digest; use transcript::{Transcript, RecommendedTranscript};
use hkdf::{Hkdf, hmac::SimpleHmac};
use chacha20::{
cipher::{crypto_common::KeyIvInit, StreamCipher},
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
};
use group::{ use group::{
ff::{Field, PrimeField}, ff::{Field, PrimeField},
GroupEncoding, GroupEncoding,
}; };
use ciphersuite::Ciphersuite; use ciphersuite::Ciphersuite;
use multiexp::{multiexp_vartime, BatchVerifier}; use multiexp::{multiexp_vartime, BatchVerifier};
use schnorr::SchnorrSignature; 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)] #[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 {
const DST: &[u8] = b"FROST Schnorr Proof of Knowledge"; let mut transcript = RecommendedTranscript::new(b"DKG FROST v0");
transcript.domain_separate(b"Schnorr Proof of Knowledge");
// Hashes the context to get a fixed size value out of it transcript.append_message(b"context", context.as_bytes());
let mut transcript = C::H::digest(context.as_bytes()).as_ref().to_vec(); transcript.append_message(b"participant", l.to_le_bytes());
transcript.extend(l.to_be_bytes()); transcript.append_message(b"nonce", R);
transcript.extend(R); transcript.append_message(b"commitments", Am);
transcript.extend(Am); C::hash_to_F(b"PoK 0", &transcript.challenge(b"challenge"))
C::hash_to_F(DST, &transcript)
} }
/// Commitments message to be broadcast to all other parties. /// Commitments message to be broadcast to all other parties.
#[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>,
enc_key: C::G,
cached_msg: Vec<u8>, cached_msg: Vec<u8>,
sig: SchnorrSignature<C>, sig: SchnorrSignature<C>,
} }
impl<C: Ciphersuite> Commitments<C> { impl<C: Ciphersuite> ReadWrite for Commitments<C> {
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> { fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
let mut commitments = Vec::with_capacity(params.t().into()); let mut commitments = Vec::with_capacity(params.t().into());
let mut cached_msg = vec![]; let mut cached_msg = vec![];
@ -67,21 +61,14 @@ impl<C: Ciphersuite> Commitments<C> {
for _ in 0 .. params.t() { for _ in 0 .. params.t() {
commitments.push(read_G()?); 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)?; writer.write_all(&self.cached_msg)?;
self.sig.write(writer) 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. /// State machine to begin the key generation protocol.
@ -104,7 +91,7 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
pub fn generate_coefficients<R: RngCore + CryptoRng>( pub fn generate_coefficients<R: RngCore + CryptoRng>(
self, self,
rng: &mut R, rng: &mut R,
) -> (SecretShareMachine<C>, Commitments<C>) { ) -> (SecretShareMachine<C>, EncryptionKeyMessage<C, Commitments<C>>) {
let t = usize::from(self.params.t); let t = usize::from(self.params.t);
let mut coefficients = Vec::with_capacity(t); let mut coefficients = Vec::with_capacity(t);
let mut commitments = 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()); 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 // Step 2: Provide a proof of knowledge
let r = Zeroizing::new(C::random_nonzero_F(rng)); let r = Zeroizing::new(C::random_nonzero_F(rng));
let nonce = C::generator() * r.deref(); 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), 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 // Step 4: Broadcast
let msg =
encryption.registration(Commitments { commitments: commitments.clone(), cached_msg, sig });
( (
SecretShareMachine { SecretShareMachine {
params: self.params, params: self.params,
context: self.context, context: self.context,
coefficients, coefficients,
our_commitments: commitments.clone(), our_commitments: commitments,
enc_key, encryption,
pub_enc_key,
}, },
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. /// Secret share to be sent to the party it's intended for over an authenticated channel.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct SecretShare<F: PrimeField>(F::Repr); 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> { 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()
@ -181,59 +169,16 @@ impl<F: PrimeField> Drop for SecretShare<F> {
} }
impl<F: PrimeField> ZeroizeOnDrop for SecretShare<F> {} impl<F: PrimeField> ZeroizeOnDrop for SecretShare<F> {}
impl<F: PrimeField> SecretShare<F> { impl<F: PrimeField> ReadWrite for SecretShare<F> {
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> { fn read<R: Read>(reader: &mut R, _: ThresholdParams) -> io::Result<Self> {
let mut repr = F::Repr::default(); let mut repr = F::Repr::default();
reader.read_exact(repr.as_mut())?; reader.read_exact(repr.as_mut())?;
Ok(SecretShare(repr)) 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()) 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. /// Advancement of the key generation state machine.
@ -243,8 +188,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>,
enc_key: Zeroizing<C::F>, encryption: Encryption<u16, C>,
pub_enc_key: C::G,
} }
impl<C: Ciphersuite> SecretShareMachine<C> { impl<C: Ciphersuite> SecretShareMachine<C> {
@ -253,16 +197,15 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
fn verify_r1<R: RngCore + CryptoRng>( fn verify_r1<R: RngCore + CryptoRng>(
&mut self, &mut self,
rng: &mut R, rng: &mut R,
mut commitments: HashMap<u16, Commitments<C>>, mut commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
) -> Result<(HashMap<u16, Vec<C::G>>, HashMap<u16, C::G>), DkgError> { ) -> Result<HashMap<u16, Vec<C::G>>, DkgError> {
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 enc_keys = HashMap::new();
let mut batch = BatchVerifier::<u16, C::G>::new(commitments.len()); let mut batch = BatchVerifier::<u16, C::G>::new(commitments.len());
let mut commitments = commitments let mut commitments = commitments
.drain() .drain()
.map(|(l, mut msg)| { .map(|(l, msg)| {
enc_keys.insert(l, msg.enc_key); let mut msg = self.encryption.register(l, msg);
// Step 5: Validate each proof of knowledge // Step 5: Validate each proof of knowledge
// This is solely the prep step for the latter batch verification // 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)?; batch.verify_with_vartime_blame().map_err(DkgError::InvalidProofOfKnowledge)?;
commitments.insert(self.params.i, self.our_commitments.drain(..).collect()); commitments.insert(self.params.i, self.our_commitments.drain(..).collect());
Ok((commitments, enc_keys)) Ok(commitments)
} }
/// Continue generating a key. /// Continue generating a key.
@ -291,13 +234,11 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
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, Commitments<C>>, commitments: HashMap<u16, EncryptionKeyMessage<C, Commitments<C>>>,
) -> Result<(KeyMachine<C>, HashMap<u16, SecretShare<C::F>>), DkgError> { ) -> Result<(KeyMachine<C>, HashMap<u16, EncryptedMessage<SecretShare<C::F>>>), DkgError> {
let (commitments, mut enc_keys) = 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
let sender = self.pub_enc_key.to_bytes();
let mut ciphers = HashMap::new();
let mut res = HashMap::new(); let mut res = HashMap::new();
for l in 1 ..= self.params.n() { for l in 1 ..= self.params.n() {
// Don't insert our own shares to the byte buffer which is meant to be sent around // 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; 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 = polynomial(&self.coefficients, l);
let mut share_bytes = 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));
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();
} }
self.enc_key.zeroize();
// Calculate our own share // Calculate our own share
let share = polynomial(&self.coefficients, self.params.i()); let share = polynomial(&self.coefficients, self.params.i());
self.coefficients.zeroize(); 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> { pub struct KeyMachine<C: Ciphersuite> {
params: ThresholdParams, params: ThresholdParams,
secret: Zeroizing<C::F>, secret: Zeroizing<C::F>,
ciphers: HashMap<u16, ChaCha20>,
commitments: HashMap<u16, Vec<C::G>>, commitments: HashMap<u16, Vec<C::G>>,
encryption: Encryption<u16, 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();
self.secret.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() { for (_, commitments) in self.commitments.iter_mut() {
commitments.zeroize(); commitments.zeroize();
} }
self.encryption.zeroize();
} }
} }
impl<C: Ciphersuite> Drop for KeyMachine<C> { impl<C: Ciphersuite> Drop for KeyMachine<C> {
@ -373,7 +296,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
pub fn complete<R: RngCore + CryptoRng>( pub fn complete<R: RngCore + CryptoRng>(
mut self, mut self,
rng: &mut R, rng: &mut R,
mut shares: HashMap<u16, SecretShare<C::F>>, mut shares: HashMap<u16, EncryptedMessage<SecretShare<C::F>>>,
) -> Result<ThresholdCore<C>, DkgError> { ) -> Result<ThresholdCore<C>, DkgError> {
validate_map(&shares, &(1 ..= self.params.n()).collect::<Vec<_>>(), self.params.i())?; 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()); let mut batch = BatchVerifier::new(shares.len());
for (l, mut share_bytes) in shares.drain() { for (l, share_bytes) in shares.drain() {
let mut cipher = self.ciphers.remove(&l).unwrap(); let mut share_bytes = self.encryption.decrypt(l, share_bytes);
cipher.apply_keystream(share_bytes.0.as_mut());
drop(cipher);
let mut share = Zeroizing::new( let mut share = Zeroizing::new(
Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or(DkgError::InvalidShare(l))?, 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; use ciphersuite::Ciphersuite;
mod encryption;
/// The distributed key generation protocol described in the /// The distributed key generation protocol described in the
/// [FROST paper](https://eprint.iacr.org/2020/852). /// [FROST paper](https://eprint.iacr.org/2020/852).
pub mod frost; pub mod frost;

View file

@ -28,7 +28,7 @@ pub trait CiphersuitePromote<C2: Ciphersuite> {
} }
fn transcript<G: GroupEncoding>(key: G, i: u16) -> RecommendedTranscript { 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"group_key", key.to_bytes());
transcript.append_message(b"participant", i.to_be_bytes()); transcript.append_message(b"participant", i.to_be_bytes());
transcript transcript

View file

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