mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-08 20:09:54 +00:00
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:
parent
ba157ea84b
commit
13977f6287
8 changed files with 265 additions and 147 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
181
crypto/dkg/src/encryption.rs
Normal file
181
crypto/dkg/src/encryption.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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))?,
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
15
docs/cryptography/Distributed Key Generation.md
Normal file
15
docs/cryptography/Distributed Key Generation.md
Normal 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.
|
Loading…
Reference in a new issue