mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 12:49:23 +00:00
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.
383 lines
11 KiB
Rust
383 lines
11 KiB
Rust
#![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.
|
|
|
|
use core::{fmt::Debug, ops::Deref};
|
|
use std::{io::Read, sync::Arc, collections::HashMap};
|
|
|
|
use thiserror::Error;
|
|
|
|
use zeroize::{Zeroize, Zeroizing};
|
|
|
|
use group::{
|
|
ff::{Field, PrimeField},
|
|
GroupEncoding,
|
|
};
|
|
|
|
use ciphersuite::Ciphersuite;
|
|
|
|
mod encryption;
|
|
|
|
/// The distributed key generation protocol described in the
|
|
/// [FROST paper](https://eprint.iacr.org/2020/852).
|
|
pub mod frost;
|
|
|
|
/// Promote keys between ciphersuites.
|
|
pub mod promote;
|
|
|
|
/// Tests for application-provided curves and algorithms.
|
|
#[cfg(any(test, feature = "tests"))]
|
|
pub mod tests;
|
|
|
|
// Validate a map of values to have the expected included participants
|
|
pub(crate) fn validate_map<T>(
|
|
map: &HashMap<u16, T>,
|
|
included: &[u16],
|
|
ours: u16,
|
|
) -> Result<(), DkgError> {
|
|
if (map.len() + 1) != included.len() {
|
|
Err(DkgError::InvalidParticipantQuantity(included.len(), map.len() + 1))?;
|
|
}
|
|
|
|
for included in included {
|
|
if *included == ours {
|
|
if map.contains_key(included) {
|
|
Err(DkgError::DuplicatedIndex(*included))?;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if !map.contains_key(included) {
|
|
Err(DkgError::MissingParticipant(*included))?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Parameters for a multisig.
|
|
// These fields should not be made public as they should be static
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
pub struct ThresholdParams {
|
|
/// Participants needed to sign on behalf of the group.
|
|
t: u16,
|
|
/// Amount of participants.
|
|
n: u16,
|
|
/// Index of the participant being acted for.
|
|
i: u16,
|
|
}
|
|
|
|
impl ThresholdParams {
|
|
pub fn new(t: u16, n: u16, i: u16) -> Result<ThresholdParams, DkgError> {
|
|
if (t == 0) || (n == 0) {
|
|
Err(DkgError::ZeroParameter(t, n))?;
|
|
}
|
|
|
|
// 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))?;
|
|
}
|
|
if (i == 0) || (i > n) {
|
|
Err(DkgError::InvalidParticipantIndex(n, i))?;
|
|
}
|
|
|
|
Ok(ThresholdParams { t, n, i })
|
|
}
|
|
|
|
pub fn t(&self) -> u16 {
|
|
self.t
|
|
}
|
|
pub fn n(&self) -> u16 {
|
|
self.n
|
|
}
|
|
pub fn i(&self) -> u16 {
|
|
self.i
|
|
}
|
|
}
|
|
|
|
/// Various errors possible during key generation/signing.
|
|
#[derive(Copy, Clone, Error, Debug)]
|
|
pub enum DkgError {
|
|
#[error("a parameter was 0 (required {0}, participants {1})")]
|
|
ZeroParameter(u16, u16),
|
|
#[error("invalid amount of required participants (max {1}, got {0})")]
|
|
InvalidRequiredQuantity(u16, u16),
|
|
#[error("invalid participant index (0 < index <= {0}, yet index is {1})")]
|
|
InvalidParticipantIndex(u16, u16),
|
|
|
|
#[error("invalid signing set")]
|
|
InvalidSigningSet,
|
|
#[error("invalid participant quantity (expected {0}, got {1})")]
|
|
InvalidParticipantQuantity(usize, usize),
|
|
#[error("duplicated participant index ({0})")]
|
|
DuplicatedIndex(u16),
|
|
#[error("missing participant {0}")]
|
|
MissingParticipant(u16),
|
|
|
|
#[error("invalid proof of knowledge (participant {0})")]
|
|
InvalidProofOfKnowledge(u16),
|
|
#[error("invalid share (participant {0})")]
|
|
InvalidShare(u16),
|
|
|
|
#[error("internal error ({0})")]
|
|
InternalError(&'static str),
|
|
}
|
|
|
|
/// Calculate the lagrange coefficient for a signing set.
|
|
pub fn lagrange<F: PrimeField>(i: u16, included: &[u16]) -> F {
|
|
let mut num = F::one();
|
|
let mut denom = F::one();
|
|
for l in included {
|
|
if i == *l {
|
|
continue;
|
|
}
|
|
|
|
let share = F::from(u64::try_from(*l).unwrap());
|
|
num *= share;
|
|
denom *= share - F::from(u64::try_from(i).unwrap());
|
|
}
|
|
|
|
// Safe as this will only be 0 if we're part of the above loop
|
|
// (which we have an if case to avoid)
|
|
num * denom.invert().unwrap()
|
|
}
|
|
|
|
/// Keys and verification shares generated by a DKG.
|
|
/// Called core as they're expected to be wrapped into an Arc before usage in various operations.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub struct ThresholdCore<C: Ciphersuite> {
|
|
/// Threshold Parameters.
|
|
params: ThresholdParams,
|
|
|
|
/// Secret share key.
|
|
secret_share: Zeroizing<C::F>,
|
|
/// Group key.
|
|
group_key: C::G,
|
|
/// Verification shares.
|
|
verification_shares: HashMap<u16, C::G>,
|
|
}
|
|
|
|
impl<C: Ciphersuite> Zeroize for ThresholdCore<C> {
|
|
fn zeroize(&mut self) {
|
|
self.params.zeroize();
|
|
self.secret_share.zeroize();
|
|
self.group_key.zeroize();
|
|
for (_, share) in self.verification_shares.iter_mut() {
|
|
share.zeroize();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<C: Ciphersuite> ThresholdCore<C> {
|
|
pub(crate) fn new(
|
|
params: ThresholdParams,
|
|
secret_share: Zeroizing<C::F>,
|
|
verification_shares: HashMap<u16, C::G>,
|
|
) -> ThresholdCore<C> {
|
|
#[cfg(debug_assertions)]
|
|
validate_map(&verification_shares, &(0 ..= params.n).collect::<Vec<_>>(), 0).unwrap();
|
|
|
|
let t = (1 ..= params.t).collect::<Vec<_>>();
|
|
ThresholdCore {
|
|
params,
|
|
secret_share,
|
|
group_key: t.iter().map(|i| verification_shares[i] * lagrange::<C::F>(*i, &t)).sum(),
|
|
verification_shares,
|
|
}
|
|
}
|
|
pub fn params(&self) -> ThresholdParams {
|
|
self.params
|
|
}
|
|
|
|
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
|
&self.secret_share
|
|
}
|
|
|
|
pub fn group_key(&self) -> C::G {
|
|
self.group_key
|
|
}
|
|
|
|
pub(crate) fn verification_shares(&self) -> HashMap<u16, C::G> {
|
|
self.verification_shares.clone()
|
|
}
|
|
|
|
pub fn serialize(&self) -> Vec<u8> {
|
|
let mut serialized = vec![];
|
|
serialized.extend(u32::try_from(C::ID.len()).unwrap().to_be_bytes());
|
|
serialized.extend(C::ID);
|
|
serialized.extend(self.params.t.to_be_bytes());
|
|
serialized.extend(self.params.n.to_be_bytes());
|
|
serialized.extend(self.params.i.to_be_bytes());
|
|
serialized.extend(self.secret_share.to_repr().as_ref());
|
|
for l in 1 ..= self.params.n {
|
|
serialized.extend(self.verification_shares[&l].to_bytes().as_ref());
|
|
}
|
|
serialized
|
|
}
|
|
|
|
pub fn deserialize<R: Read>(reader: &mut R) -> Result<ThresholdCore<C>, DkgError> {
|
|
{
|
|
let missing = DkgError::InternalError("ThresholdCore serialization is missing its curve");
|
|
let different = DkgError::InternalError("deserializing ThresholdCore for another curve");
|
|
|
|
let mut id_len = [0; 4];
|
|
reader.read_exact(&mut id_len).map_err(|_| missing)?;
|
|
if u32::try_from(C::ID.len()).unwrap().to_be_bytes() != id_len {
|
|
Err(different)?;
|
|
}
|
|
|
|
let mut id = vec![0; C::ID.len()];
|
|
reader.read_exact(&mut id).map_err(|_| missing)?;
|
|
if id != C::ID {
|
|
Err(different)?;
|
|
}
|
|
}
|
|
|
|
let (t, n, i) = {
|
|
let mut read_u16 = || {
|
|
let mut value = [0; 2];
|
|
reader
|
|
.read_exact(&mut value)
|
|
.map_err(|_| DkgError::InternalError("missing participant quantities"))?;
|
|
Ok(u16::from_be_bytes(value))
|
|
};
|
|
(read_u16()?, read_u16()?, read_u16()?)
|
|
};
|
|
|
|
let secret_share = Zeroizing::new(
|
|
C::read_F(reader).map_err(|_| DkgError::InternalError("invalid secret share"))?,
|
|
);
|
|
|
|
let mut verification_shares = HashMap::new();
|
|
for l in 1 ..= n {
|
|
verification_shares.insert(
|
|
l,
|
|
<C as Ciphersuite>::read_G(reader)
|
|
.map_err(|_| DkgError::InternalError("invalid verification share"))?,
|
|
);
|
|
}
|
|
|
|
Ok(ThresholdCore::new(
|
|
ThresholdParams::new(t, n, i).map_err(|_| DkgError::InternalError("invalid parameters"))?,
|
|
secret_share,
|
|
verification_shares,
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Threshold keys usable for signing.
|
|
#[derive(Clone, Debug, Zeroize)]
|
|
pub struct ThresholdKeys<C: Ciphersuite> {
|
|
/// Core keys.
|
|
#[zeroize(skip)]
|
|
core: Arc<ThresholdCore<C>>,
|
|
|
|
/// Offset applied to these keys.
|
|
pub(crate) offset: Option<C::F>,
|
|
}
|
|
|
|
/// View of keys passed to algorithm implementations.
|
|
#[derive(Clone, Zeroize)]
|
|
pub struct ThresholdView<C: Ciphersuite> {
|
|
group_key: C::G,
|
|
#[zeroize(skip)]
|
|
included: Vec<u16>,
|
|
secret_share: Zeroizing<C::F>,
|
|
#[zeroize(skip)]
|
|
verification_shares: HashMap<u16, C::G>,
|
|
}
|
|
|
|
impl<C: Ciphersuite> ThresholdKeys<C> {
|
|
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.
|
|
pub fn offset(&self, offset: C::F) -> ThresholdKeys<C> {
|
|
let mut res = self.clone();
|
|
// Carry any existing offset
|
|
// Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a
|
|
// one-time-key offset
|
|
res.offset = Some(offset + res.offset.unwrap_or_else(C::F::zero));
|
|
res
|
|
}
|
|
|
|
/// Returns the current offset in-use for these keys.
|
|
pub fn current_offset(&self) -> Option<C::F> {
|
|
self.offset
|
|
}
|
|
|
|
pub fn params(&self) -> ThresholdParams {
|
|
self.core.params
|
|
}
|
|
|
|
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
|
&self.core.secret_share
|
|
}
|
|
|
|
/// Returns 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.
|
|
pub(crate) fn verification_shares(&self) -> HashMap<u16, C::G> {
|
|
self.core.verification_shares()
|
|
}
|
|
|
|
pub fn serialize(&self) -> Vec<u8> {
|
|
self.core.serialize()
|
|
}
|
|
|
|
pub fn view(&self, included: &[u16]) -> Result<ThresholdView<C>, DkgError> {
|
|
if (included.len() < self.params().t.into()) || (usize::from(self.params().n) < included.len())
|
|
{
|
|
Err(DkgError::InvalidSigningSet)?;
|
|
}
|
|
|
|
let offset_share = self.offset.unwrap_or_else(C::F::zero) *
|
|
C::F::from(included.len().try_into().unwrap()).invert().unwrap();
|
|
let offset_verification_share = C::generator() * offset_share;
|
|
|
|
Ok(ThresholdView {
|
|
group_key: self.group_key(),
|
|
secret_share: Zeroizing::new(
|
|
(lagrange::<C::F>(self.params().i, included) * self.secret_share().deref()) + offset_share,
|
|
),
|
|
verification_shares: self
|
|
.verification_shares()
|
|
.iter()
|
|
.map(|(l, share)| {
|
|
(*l, (*share * lagrange::<C::F>(*l, included)) + offset_verification_share)
|
|
})
|
|
.collect(),
|
|
included: included.to_vec(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<C: Ciphersuite> ThresholdView<C> {
|
|
pub fn group_key(&self) -> C::G {
|
|
self.group_key
|
|
}
|
|
|
|
pub fn included(&self) -> Vec<u16> {
|
|
self.included.clone()
|
|
}
|
|
|
|
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
|
&self.secret_share
|
|
}
|
|
|
|
pub fn verification_share(&self, l: u16) -> C::G {
|
|
self.verification_shares[&l]
|
|
}
|
|
}
|