serai/crypto/dkg/src/lib.rs
Luke Parker af86b7a499
Support caching preprocesses in FROST (#190)
* Remove the explicit included participants from FROST

Now, whoever submits preprocesses becomes the signing set. Better separates
preprocess from sign, at the cost of slightly more annoying integrations
(Monero needs to now independently lagrange/offset its key images).

* Support caching preprocesses

Closes https://github.com/serai-dex/serai/issues/40.

I *could* have added a serialization trait to Algorithm and written a ton of
data to disk, while requiring Algorithm implementors also accept such work.
Instead, I moved preprocess to a seeded RNG (Chacha20) which should be as
secure as the regular RNG. Rebuilding from cache simply loads the previously
used Chacha seed, making the Algorithm oblivious to the fact it's being
rebuilt from a cache. This removes any requirements for it to be modified
while guaranteeing equivalency.

This builds on the last commit which delayed determining the signing set till
post-preprocess acquisition. Unfortunately, that commit did force preprocess
from ThresholdView to ThresholdKeys which had visible effects on Monero.

Serai will actually need delayed set determination for #163, and overall,
it remains better, hence it's inclusion.

* Document FROST preprocess caching

* Update ethereum to new FROST

* Fix bug in Monero offset calculation and update processor
2022-12-08 19:04:35 -05:00

394 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> {
offset: C::F,
group_key: C::G,
included: Vec<u16>,
secret_share: Zeroizing<C::F>,
#[zeroize(skip)]
original_verification_shares: HashMap<u16, C::G>,
#[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 {
offset: self.offset.unwrap_or_else(C::F::zero),
group_key: self.group_key(),
secret_share: Zeroizing::new(
(lagrange::<C::F>(self.params().i, included) * self.secret_share().deref()) + offset_share,
),
original_verification_shares: self.verification_shares(),
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 offset(&self) -> C::F {
self.offset
}
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 original_verification_share(&self, l: u16) -> C::G {
self.original_verification_shares[&l]
}
pub fn verification_share(&self, l: u16) -> C::G {
self.verification_shares[&l]
}
}