mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-24 11:36:18 +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 = [
|
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",
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
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 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))?,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
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