Fully document crypto/

This commit is contained in:
Luke Parker
2023-03-20 20:10:00 -04:00
parent e1bb2c191b
commit 8d4d630e0f
45 changed files with 335 additions and 208 deletions

View File

@@ -3,15 +3,14 @@
A collection of implementations of various distributed key generation protocols.
All included protocols resolve into the provided `Threshold` types, intended to
enable their modularity.
enable their modularity. Additional utilities around these types, such as
promotion from one generator to another, are also provided.
Additional utilities around them, such as promotion from one generator to
another, are also provided.
Currently included is the two-round protocol from the
Currently, the only included protocol is the two-round protocol from the
[FROST paper](https://eprint.iacr.org/2020/852).
This library was
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/74924095e1a0f266b58181b539d9e74fa35dc37a/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
culminating in commit 669d2dbffc1dafb82a09d9419ea182667115df06. Any subsequent
changes have not undergone auditing.
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
culminating in commit
[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06).
Any subsequent changes have not undergone auditing.

View File

@@ -1,8 +1,5 @@
use core::{ops::Deref, fmt};
use std::{
io::{self, Read, Write},
collections::HashMap,
};
use std::{io, collections::HashMap};
use thiserror::Error;
@@ -26,19 +23,27 @@ use dleq::DLEqProof;
use crate::{Participant, 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<()>;
mod sealed {
use super::*;
fn serialize(&self) -> Vec<u8> {
let mut buf = vec![];
self.write(&mut buf).unwrap();
buf
pub trait ReadWrite: Sized {
fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self>;
fn write<W: io::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 + fmt::Debug + Zeroize + ReadWrite {}
impl<M: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite> Message for M {}
pub trait Message: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite {}
impl<M: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite> Message for M {}
pub trait Encryptable: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite {}
impl<E: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
}
pub(crate) use sealed::*;
/// Wraps a message with a key to use for encryption in the future.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
@@ -49,11 +54,11 @@ pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
// 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> {
pub fn read<R: io::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<()> {
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
self.msg.write(writer)?;
writer.write_all(self.enc_key.to_bytes().as_ref())
}
@@ -70,9 +75,6 @@ impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
}
}
pub trait Encryptable: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite {}
impl<E: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
/// An encrypted message, with a per-message encryption key enabling revealing specific messages
/// without side effects.
#[derive(Clone, Zeroize)]
@@ -166,7 +168,7 @@ fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
}
impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
pub fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
Ok(Self {
key: C::read_G(reader)?,
pop: SchnorrSignature::<C>::read(reader)?,
@@ -174,7 +176,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
})
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.key.to_bytes().as_ref())?;
self.pop.write(writer)?;
self.msg.write(writer)
@@ -254,7 +256,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
}
}
/// A proof that the provided point is the legitimately derived shared key for some message.
/// A proof that the provided encryption key is a legitimately derived shared key for some message.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct EncryptionKeyProof<C: Ciphersuite> {
key: Zeroizing<C::G>,
@@ -262,11 +264,11 @@ pub struct EncryptionKeyProof<C: Ciphersuite> {
}
impl<C: Ciphersuite> EncryptionKeyProof<C> {
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
Ok(Self { key: Zeroizing::new(C::read_G(reader)?), dleq: DLEqProof::read(reader)? })
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.key.to_bytes().as_ref())?;
self.dleq.write(writer)
}

View File

@@ -43,11 +43,11 @@ fn challenge<C: Ciphersuite>(context: &str, l: Participant, R: &[u8], Am: &[u8])
}
/// The commitments message, intended to be broadcast to all other parties.
/// Every participant should only provide one set of commitments to all parties.
/// If any participant sends multiple sets of commitments, they are faulty and should be presumed
/// malicious.
/// As this library does not handle networking, it is also unable to detect if any participant is
/// so faulty. That responsibility lies with the caller.
///
/// Every participant should only provide one set of commitments to all parties. If any
/// participant sends multiple sets of commitments, they are faulty and should be presumed
/// malicious. As this library does not handle networking, it is unable to detect if any
/// participant is so faulty. That responsibility lies with the caller.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Commitments<C: Ciphersuite> {
commitments: Vec<C::G>,
@@ -91,13 +91,14 @@ pub struct KeyGenMachine<C: Ciphersuite> {
}
impl<C: Ciphersuite> KeyGenMachine<C> {
/// Creates a new machine to generate a key for the specified curve in the specified multisig.
/// Create a new machine to generate a key.
// The context string should be unique among multisigs.
pub fn new(params: ThresholdParams, context: String) -> KeyGenMachine<C> {
KeyGenMachine { params, context, _curve: PhantomData }
}
/// Start generating a key according to the FROST DKG spec.
///
/// Returns a commitments message to be sent to all parties over an authenticated channel. If any
/// party submits multiple sets of commitments, they MUST be treated as malicious.
pub fn generate_coefficients<R: RngCore + CryptoRng>(
@@ -168,7 +169,9 @@ fn polynomial<F: PrimeField + Zeroize>(
/// The secret share message, to be sent to the party it's intended for over an authenticated
/// channel.
///
/// If any participant sends multiple secret shares to another participant, they are faulty.
// This should presumably be written as SecretShare(Zeroizing<F::Repr>).
// It's unfortunately not possible as F::Repr doesn't have Zeroize as a bound.
// The encryption system also explicitly uses Zeroizing<M> so it can ensure anything being
@@ -281,8 +284,10 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
}
/// Continue generating a key.
///
/// Takes in everyone else's commitments. Returns a HashMap of encrypted secret shares to be sent
/// over authenticated channels to their relevant counterparties.
///
/// If any participant sends multiple secret shares to another participant, they are faulty.
#[allow(clippy::type_complexity)]
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
@@ -321,11 +326,11 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
}
}
/// Advancement of the the secret share state machine protocol.
/// This machine will 'complete' the protocol, by a local perspective, and can be the last
/// interactive component. In order to be secure, the parties must confirm having successfully
/// completed the protocol (an effort out of scope to this library), yet this is modelled by one
/// more state transition.
/// Advancement of the the secret share state machine.
///
/// This machine will 'complete' the protocol, by a local perspective. In order to be secure,
/// the parties must confirm having successfully completed the protocol (an effort out of scope to
/// this library), yet this is modeled by one more state transition (BlameMachine).
pub struct KeyMachine<C: Ciphersuite> {
params: ThresholdParams,
secret: Zeroizing<C::F>,
@@ -397,8 +402,10 @@ enum BatchId {
impl<C: Ciphersuite> KeyMachine<C> {
/// Calculate our share given the shares sent to us.
///
/// Returns a BlameMachine usable to determine if faults in the protocol occurred.
/// Will error on, and return a blame proof for, the first-observed case of faulty behavior.
///
/// This will error on, and return a blame proof for, the first-observed case of faulty behavior.
pub fn calculate_share<R: RngCore + CryptoRng>(
mut self,
rng: &mut R,
@@ -473,6 +480,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
}
}
/// A machine capable of handling blame proofs.
pub struct BlameMachine<C: Ciphersuite> {
commitments: HashMap<Participant, Vec<C::G>>,
encryption: Encryption<C>,
@@ -574,6 +582,7 @@ impl<C: Ciphersuite> BlameMachine<C> {
}
}
/// A machine capable of handling an arbitrary amount of additional blame proofs.
#[derive(Debug, Zeroize)]
pub struct AdditionalBlameMachine<C: Ciphersuite>(BlameMachine<C>);
impl<C: Ciphersuite> AdditionalBlameMachine<C> {

View File

@@ -1,10 +1,5 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
//! A collection of implementations of various distributed key generation protocols.
//! They all resolve into the provided Threshold types intended to enable their modularity.
//! Additional utilities around them, such as promotion from one generator to another, are also
//! provided.
#![doc = include_str!("../README.md")]
use core::{
fmt::{self, Debug},
@@ -43,6 +38,7 @@ pub mod tests;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Participant(pub(crate) u16);
impl Participant {
/// Create a new Participant identifier from a u16.
pub fn new(i: u16) -> Option<Participant> {
if i == 0 {
None
@@ -51,6 +47,7 @@ impl Participant {
}
}
/// Convert a Participant identifier to bytes.
#[allow(clippy::wrong_self_convention)]
pub fn to_bytes(&self) -> [u8; 2] {
self.0.to_le_bytes()
@@ -69,32 +66,38 @@ impl fmt::Display for Participant {
}
}
/// Various errors possible during key generation/signing.
/// Various errors possible during key generation.
#[derive(Clone, PartialEq, Eq, Debug, Error)]
pub enum DkgError<B: Clone + PartialEq + Eq + Debug> {
/// A parameter was zero.
#[error("a parameter was 0 (threshold {0}, participants {1})")]
ZeroParameter(u16, u16),
#[error("invalid amount of required participants (max {1}, got {0})")]
InvalidRequiredQuantity(u16, u16),
/// The threshold exceeded the amount of participants.
#[error("invalid threshold (max {1}, got {0})")]
InvalidThreshold(u16, u16),
/// Invalid participant identifier.
#[error("invalid participant (0 < participant <= {0}, yet participant is {1})")]
InvalidParticipant(u16, Participant),
/// Invalid signing set.
#[error("invalid signing set")]
InvalidSigningSet,
/// Invalid amount of participants.
#[error("invalid participant quantity (expected {0}, got {1})")]
InvalidParticipantQuantity(usize, usize),
/// A participant was duplicated.
#[error("duplicated participant ({0})")]
DuplicatedParticipant(Participant),
/// A participant was missing.
#[error("missing participant {0}")]
MissingParticipant(Participant),
/// An invalid proof of knowledge was provided.
#[error("invalid proof of knowledge (participant {0})")]
InvalidProofOfKnowledge(Participant),
/// An invalid DKG share was provided.
#[error("invalid share (participant {participant}, blame {blame})")]
InvalidShare { participant: Participant, blame: Option<B> },
#[error("internal error ({0})")]
InternalError(&'static str),
}
// Validate a map of values to have the expected included participants
@@ -137,6 +140,7 @@ pub struct ThresholdParams {
}
impl ThresholdParams {
/// Create a new set of parameters.
pub fn new(t: u16, n: u16, i: Participant) -> Result<ThresholdParams, DkgError<()>> {
if (t == 0) || (n == 0) {
Err(DkgError::ZeroParameter(t, n))?;
@@ -145,7 +149,7 @@ impl ThresholdParams {
// When t == n, this shouldn't be used (MuSig2 and other variants of MuSig exist for a reason),
// but it's not invalid to do so
if t > n {
Err(DkgError::InvalidRequiredQuantity(t, n))?;
Err(DkgError::InvalidThreshold(t, n))?;
}
if u16::from(i) > n {
Err(DkgError::InvalidParticipant(n, i))?;
@@ -154,12 +158,15 @@ impl ThresholdParams {
Ok(ThresholdParams { t, n, i })
}
/// Return the threshold for a multisig with these parameters.
pub fn t(&self) -> u16 {
self.t
}
/// Return the amount of participants for a multisig with these parameters.
pub fn n(&self) -> u16 {
self.n
}
/// Return the participant index of the share with these parameters.
pub fn i(&self) -> Participant {
self.i
}
@@ -237,14 +244,18 @@ impl<C: Ciphersuite> ThresholdCore<C> {
verification_shares,
}
}
/// Parameters for these keys.
pub fn params(&self) -> ThresholdParams {
self.params
}
/// Secret share for these keys.
pub fn secret_share(&self) -> &Zeroizing<C::F> {
&self.secret_share
}
/// Group key for these keys.
pub fn group_key(&self) -> C::G {
self.group_key
}
@@ -253,6 +264,7 @@ impl<C: Ciphersuite> ThresholdCore<C> {
self.verification_shares.clone()
}
/// Write these keys to a type satisfying std::io::Write.
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&u32::try_from(C::ID.len()).unwrap().to_le_bytes())?;
writer.write_all(C::ID)?;
@@ -269,61 +281,56 @@ impl<C: Ciphersuite> ThresholdCore<C> {
Ok(())
}
/// Serialize these keys to a `Vec<u8>`.
pub fn serialize(&self) -> Zeroizing<Vec<u8>> {
let mut serialized = Zeroizing::new(vec![]);
self.write::<Vec<u8>>(serialized.as_mut()).unwrap();
serialized
}
pub fn read<R: io::Read>(reader: &mut R) -> Result<ThresholdCore<C>, DkgError<()>> {
/// Read keys from a type satisfying std::io::Read.
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<ThresholdCore<C>> {
{
let missing = DkgError::InternalError("ThresholdCore serialization is missing its curve");
let different = DkgError::InternalError("deserializing ThresholdCore for another curve");
let different =
|| io::Error::new(io::ErrorKind::Other, "deserializing ThresholdCore for another curve");
let mut id_len = [0; 4];
reader.read_exact(&mut id_len).map_err(|_| missing.clone())?;
reader.read_exact(&mut id_len)?;
if u32::try_from(C::ID.len()).unwrap().to_le_bytes() != id_len {
Err(different.clone())?;
Err(different())?;
}
let mut id = vec![0; C::ID.len()];
reader.read_exact(&mut id).map_err(|_| missing)?;
reader.read_exact(&mut id)?;
if id != C::ID {
Err(different)?;
Err(different())?;
}
}
let (t, n, i) = {
let mut read_u16 = || {
let mut read_u16 = || -> io::Result<u16> {
let mut value = [0; 2];
reader
.read_exact(&mut value)
.map_err(|_| DkgError::InternalError("missing participant quantities"))?;
reader.read_exact(&mut value)?;
Ok(u16::from_le_bytes(value))
};
(
read_u16()?,
read_u16()?,
Participant::new(read_u16()?)
.ok_or(DkgError::InternalError("invalid participant index"))?,
.ok_or(io::Error::new(io::ErrorKind::Other, "invalid participant index"))?,
)
};
let secret_share = Zeroizing::new(
C::read_F(reader).map_err(|_| DkgError::InternalError("invalid secret share"))?,
);
let secret_share = Zeroizing::new(C::read_F(reader)?);
let mut verification_shares = HashMap::new();
for l in (1 ..= n).map(Participant) {
verification_shares.insert(
l,
<C as Ciphersuite>::read_G(reader)
.map_err(|_| DkgError::InternalError("invalid verification share"))?,
);
verification_shares.insert(l, <C as Ciphersuite>::read_G(reader)?);
}
Ok(ThresholdCore::new(
ThresholdParams::new(t, n, i).map_err(|_| DkgError::InternalError("invalid parameters"))?,
ThresholdParams::new(t, n, i)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid parameters"))?,
secret_share,
verification_shares,
))
@@ -343,7 +350,7 @@ pub struct ThresholdKeys<C: Ciphersuite> {
pub(crate) offset: Option<C::F>,
}
/// View of keys passed to algorithm implementations.
/// View of keys, interpolated and offset for usage.
#[derive(Clone)]
pub struct ThresholdView<C: Ciphersuite> {
offset: C::F,
@@ -383,13 +390,15 @@ impl<C: Ciphersuite> Zeroize for ThresholdView<C> {
}
impl<C: Ciphersuite> ThresholdKeys<C> {
/// Create a new set of ThresholdKeys from a ThresholdCore.
pub fn new(core: ThresholdCore<C>) -> ThresholdKeys<C> {
ThresholdKeys { core: Arc::new(core), offset: None }
}
/// Offset the keys by a given scalar to allow for account and privacy schemes.
/// This offset is ephemeral and will not be included when these keys are serialized.
/// Keys offset multiple times will form a new offset of their sum.
/// Offset the keys by a given scalar to allow for various account and privacy schemes.
///
/// This offset is ephemeral and will not be included when these keys are serialized. It also
/// accumulates, so calling offset multiple times will produce a offset of the offsets' sum.
#[must_use]
pub fn offset(&self, offset: C::F) -> ThresholdKeys<C> {
let mut res = self.clone();
@@ -400,33 +409,38 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
res
}
/// Returns the current offset in-use for these keys.
/// Return the current offset in-use for these keys.
pub fn current_offset(&self) -> Option<C::F> {
self.offset
}
/// Return the parameters for these keys.
pub fn params(&self) -> ThresholdParams {
self.core.params
}
/// Return the secret share for these keys.
pub fn secret_share(&self) -> &Zeroizing<C::F> {
&self.core.secret_share
}
/// Returns the group key with any offset applied.
/// Return the group key, with any offset applied.
pub fn group_key(&self) -> C::G {
self.core.group_key + (C::generator() * self.offset.unwrap_or_else(C::F::zero))
}
/// Returns all participants' verification shares without any offsetting.
/// Return all participants' verification shares without any offsetting.
pub(crate) fn verification_shares(&self) -> HashMap<Participant, C::G> {
self.core.verification_shares()
}
/// Serialize these keys to a `Vec<u8>`.
pub fn serialize(&self) -> Zeroizing<Vec<u8>> {
self.core.serialize()
}
/// Obtain a view of these keys, with any offset applied, interpolated for the specified signing
/// set.
pub fn view(&self, mut included: Vec<Participant>) -> Result<ThresholdView<C>, DkgError<()>> {
if (included.len() < self.params().t.into()) || (usize::from(self.params().n) < included.len())
{
@@ -460,27 +474,39 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
}
}
impl<C: Ciphersuite> From<ThresholdCore<C>> for ThresholdKeys<C> {
fn from(keys: ThresholdCore<C>) -> ThresholdKeys<C> {
ThresholdKeys::new(keys)
}
}
impl<C: Ciphersuite> ThresholdView<C> {
/// Return the offset for this view.
pub fn offset(&self) -> C::F {
self.offset
}
/// Return the group key.
pub fn group_key(&self) -> C::G {
self.group_key
}
/// Return the included signers.
pub fn included(&self) -> &[Participant] {
&self.included
}
/// Return the interpolated, offset secret share.
pub fn secret_share(&self) -> &Zeroizing<C::F> {
&self.secret_share
}
/// Return the original verification share for the specified participant.
pub fn original_verification_share(&self, l: Participant) -> C::G {
self.original_verification_shares[&l]
}
/// Return the interpolated, offset verification share for the specified participant.
pub fn verification_share(&self, l: Participant) -> C::G {
self.verification_shares[&l]
}

View File

@@ -60,9 +60,10 @@ impl<C: Ciphersuite> GeneratorProof<C> {
}
/// Promote a set of keys from one generator to another, where the elliptic curve is the same.
///
/// Since the Ciphersuite trait additionally specifies a generator, this provides an O(n) way to
/// update the generator used with keys. This outperforms the key generation protocol which is
// exponential.
/// exponential.
pub struct GeneratorPromotion<C1: Ciphersuite, C2: Ciphersuite> {
base: ThresholdKeys<C1>,
proof: GeneratorProof<C1>,

View File

@@ -7,7 +7,7 @@ use ciphersuite::{group::ff::Field, Ciphersuite};
use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange};
/// FROST generation test.
/// FROST key generation testing utility.
pub mod frost;
use frost::frost_gen;
@@ -17,7 +17,7 @@ use promote::test_generator_promotion;
/// Constant amount of participants to use when testing.
pub const PARTICIPANTS: u16 = 5;
/// Constant threshold of participants to use when signing.
/// Constant threshold of participants to use when testing.
pub const THRESHOLD: u16 = ((PARTICIPANTS / 3) * 2) + 1;
/// Clone a map without a specific value.