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
This commit is contained in:
Luke Parker
2022-12-08 19:04:35 -05:00
committed by GitHub
parent 873d27685a
commit af86b7a499
20 changed files with 339 additions and 162 deletions

View File

@@ -284,11 +284,13 @@ pub struct ThresholdKeys<C: Ciphersuite> {
/// View of keys passed to algorithm implementations.
#[derive(Clone, Zeroize)]
pub struct ThresholdView<C: Ciphersuite> {
offset: C::F,
group_key: C::G,
#[zeroize(skip)]
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>,
}
@@ -347,10 +349,12 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
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()
@@ -364,6 +368,10 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
}
impl<C: Ciphersuite> ThresholdView<C> {
pub fn offset(&self) -> C::F {
self.offset
}
pub fn group_key(&self) -> C::G {
self.group_key
}
@@ -376,6 +384,10 @@ impl<C: Ciphersuite> ThresholdView<C> {
&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]
}

View File

@@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"]
thiserror = "1"
rand_core = "0.6"
rand_chacha = "0.3"
zeroize = { version = "1.5", features = ["zeroize_derive"] }
subtle = "2"

View File

@@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng};
use transcript::Transcript;
use crate::{Curve, FrostError, ThresholdView};
use crate::{Curve, FrostError, ThresholdKeys, ThresholdView};
pub use schnorr::SchnorrSignature;
/// Write an addendum to a writer.
@@ -45,7 +45,7 @@ pub trait Algorithm<C: Curve>: Clone {
fn preprocess_addendum<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
params: &ThresholdView<C>,
keys: &ThresholdKeys<C>,
) -> Self::Addendum;
/// Read an addendum from a reader.
@@ -148,7 +148,7 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
vec![vec![C::generator()]]
}
fn preprocess_addendum<R: RngCore + CryptoRng>(&mut self, _: &mut R, _: &ThresholdView<C>) {}
fn preprocess_addendum<R: RngCore + CryptoRng>(&mut self, _: &mut R, _: &ThresholdKeys<C>) {}
fn read_addendum<R: Read>(&self, _: &mut R) -> io::Result<Self::Addendum> {
Ok(())

View File

@@ -36,7 +36,7 @@ pub mod sign;
pub mod tests;
// Validate a map of values to have the expected included participants
pub(crate) fn validate_map<T>(
pub fn validate_map<T>(
map: &HashMap<u16, T>,
included: &[u16],
ours: u16,

View File

@@ -4,9 +4,10 @@ use std::{
collections::HashMap,
};
use rand_core::{RngCore, CryptoRng};
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use zeroize::Zeroize;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use transcript::Transcript;
@@ -47,54 +48,16 @@ pub struct Params<C: Curve, A: Algorithm<C>> {
#[zeroize(skip)]
algorithm: A,
keys: ThresholdKeys<C>,
view: ThresholdView<C>,
}
impl<C: Curve, A: Algorithm<C>> Params<C, A> {
pub fn new(
algorithm: A,
keys: ThresholdKeys<C>,
included: &[u16],
) -> Result<Params<C, A>, FrostError> {
let params = keys.params();
let mut included = included.to_vec();
included.sort_unstable();
// Included < threshold
if included.len() < usize::from(params.t()) {
Err(FrostError::InvalidSigningSet("not enough signers"))?;
}
// Invalid index
if included[0] == 0 {
Err(FrostError::InvalidParticipantIndex(included[0], params.n()))?;
}
// OOB index
if included[included.len() - 1] > params.n() {
Err(FrostError::InvalidParticipantIndex(included[included.len() - 1], params.n()))?;
}
// Same signer included multiple times
for i in 0 .. (included.len() - 1) {
if included[i] == included[i + 1] {
Err(FrostError::DuplicatedIndex(included[i]))?;
}
}
// Not included
if !included.contains(&params.i()) {
Err(FrostError::InvalidSigningSet("signing despite not being included"))?;
}
// Out of order arguments to prevent additional cloning
Ok(Params { algorithm, view: keys.view(&included).unwrap(), keys })
pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> Result<Params<C, A>, FrostError> {
Ok(Params { algorithm, keys })
}
pub fn multisig_params(&self) -> ThresholdParams {
self.keys.params()
}
pub fn view(&self) -> ThresholdView<C> {
self.view.clone()
}
}
/// Preprocess for an instance of the FROST signing protocol.
@@ -111,6 +74,12 @@ impl<C: Curve, A: Addendum> Writable for Preprocess<C, A> {
}
}
/// A cached preprocess. A preprocess MUST only be used once. Reuse will enable third-party
/// recovery of your private key share. Additionally, this MUST be handled with the same security
/// as your private key share, as knowledge of it also enables recovery.
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct CachedPreprocess(pub [u8; 32]);
/// Trait for the initial state machine of a two-round signing protocol.
pub trait PreprocessMachine {
/// Preprocess message for this machine.
@@ -134,12 +103,26 @@ pub struct AlgorithmMachine<C: Curve, A: Algorithm<C>> {
impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
/// Creates a new machine to generate a signature with the specified keys.
pub fn new(
algorithm: A,
keys: ThresholdKeys<C>,
included: &[u16],
) -> Result<AlgorithmMachine<C, A>, FrostError> {
Ok(AlgorithmMachine { params: Params::new(algorithm, keys, included)? })
pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> Result<AlgorithmMachine<C, A>, FrostError> {
Ok(AlgorithmMachine { params: Params::new(algorithm, keys)? })
}
fn seeded_preprocess(
self,
seed: Zeroizing<CachedPreprocess>,
) -> (AlgorithmSignMachine<C, A>, Preprocess<C, A::Addendum>) {
let mut params = self.params;
let mut rng = ChaCha20Rng::from_seed(seed.0);
let (nonces, commitments) = Commitments::new::<_, A::Transcript>(
&mut rng,
params.keys.secret_share(),
&params.algorithm.nonces(),
);
let addendum = params.algorithm.preprocess_addendum(&mut rng, &params.keys);
let preprocess = Preprocess { commitments, addendum };
(AlgorithmSignMachine { params, seed, nonces, preprocess: preprocess.clone() }, preprocess)
}
#[cfg(any(test, feature = "tests"))]
@@ -148,7 +131,12 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
nonces: Vec<Nonce<C>>,
preprocess: Preprocess<C, A::Addendum>,
) -> AlgorithmSignMachine<C, A> {
AlgorithmSignMachine { params: self.params, nonces, preprocess }
AlgorithmSignMachine {
params: self.params,
seed: Zeroizing::new(CachedPreprocess([0; 32])),
nonces,
preprocess,
}
}
}
@@ -161,17 +149,9 @@ impl<C: Curve, A: Algorithm<C>> PreprocessMachine for AlgorithmMachine<C, A> {
self,
rng: &mut R,
) -> (Self::SignMachine, Preprocess<C, A::Addendum>) {
let mut params = self.params;
let (nonces, commitments) = Commitments::new::<_, A::Transcript>(
&mut *rng,
params.view().secret_share(),
&params.algorithm.nonces(),
);
let addendum = params.algorithm.preprocess_addendum(rng, &params.view);
let preprocess = Preprocess { commitments, addendum };
(AlgorithmSignMachine { params, nonces, preprocess: preprocess.clone() }, preprocess)
let mut seed = Zeroizing::new(CachedPreprocess([0; 32]));
rng.fill_bytes(seed.0.as_mut());
self.seeded_preprocess(seed)
}
}
@@ -185,7 +165,11 @@ impl<C: Curve> Writable for SignatureShare<C> {
}
/// Trait for the second machine of a two-round signing protocol.
pub trait SignMachine<S> {
pub trait SignMachine<S>: Sized {
/// Params used to instantiate this machine which can be used to rebuild from a cache.
type Params: Clone;
/// Keys used for signing operations.
type Keys;
/// Preprocess message for this machine.
type Preprocess: Clone + PartialEq + Writable;
/// SignatureShare message for this machine.
@@ -193,12 +177,28 @@ pub trait SignMachine<S> {
/// SignatureMachine this SignMachine turns into.
type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>;
/// Read a Preprocess message.
/// Cache this preprocess for usage later. This cached preprocess MUST only be used once. Reuse
/// of it enables recovery of your private key share. Third-party recovery of a cached preprocess
/// also enables recovery of your private key share, so this MUST be treated with the same
/// security as your private key share.
fn cache(self) -> Zeroizing<CachedPreprocess>;
/// Create a sign machine from a cached preprocess. After this, the preprocess should be fully
/// deleted, as it must never be reused. It is
fn from_cache(
params: Self::Params,
keys: Self::Keys,
cache: Zeroizing<CachedPreprocess>,
) -> Result<Self, FrostError>;
/// Read a Preprocess message. Despite taking self, this does not save the preprocess.
/// It must be externally cached and passed into sign.
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>;
/// Sign a message.
/// Takes in the participants' preprocess messages. Returns the signature share to be broadcast
/// to all participants, over an authenticated channel.
/// to all participants, over an authenticated channel. The parties who participate here will
/// become the signing set for this session.
fn sign(
self,
commitments: HashMap<u16, Self::Preprocess>,
@@ -210,16 +210,33 @@ pub trait SignMachine<S> {
#[derive(Zeroize)]
pub struct AlgorithmSignMachine<C: Curve, A: Algorithm<C>> {
params: Params<C, A>,
seed: Zeroizing<CachedPreprocess>,
pub(crate) nonces: Vec<Nonce<C>>,
#[zeroize(skip)]
pub(crate) preprocess: Preprocess<C, A::Addendum>,
}
impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachine<C, A> {
type Params = A;
type Keys = ThresholdKeys<C>;
type Preprocess = Preprocess<C, A::Addendum>;
type SignatureShare = SignatureShare<C>;
type SignatureMachine = AlgorithmSignatureMachine<C, A>;
fn cache(self) -> Zeroizing<CachedPreprocess> {
self.seed
}
fn from_cache(
algorithm: A,
keys: ThresholdKeys<C>,
cache: Zeroizing<CachedPreprocess>,
) -> Result<Self, FrostError> {
let (machine, _) = AlgorithmMachine::new(algorithm, keys)?.seeded_preprocess(cache);
Ok(machine)
}
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
Ok(Preprocess {
commitments: Commitments::read::<_, A::Transcript>(reader, &self.params.algorithm.nonces())?,
@@ -233,7 +250,35 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
msg: &[u8],
) -> Result<(Self::SignatureMachine, SignatureShare<C>), FrostError> {
let multisig_params = self.params.multisig_params();
validate_map(&preprocesses, &self.params.view.included(), multisig_params.i())?;
let mut included = Vec::with_capacity(preprocesses.len() + 1);
included.push(multisig_params.i());
for l in preprocesses.keys() {
included.push(*l);
}
included.sort_unstable();
// Included < threshold
if included.len() < usize::from(multisig_params.t()) {
Err(FrostError::InvalidSigningSet("not enough signers"))?;
}
// Invalid index
if included[0] == 0 {
Err(FrostError::InvalidParticipantIndex(included[0], multisig_params.n()))?;
}
// OOB index
if included[included.len() - 1] > multisig_params.n() {
Err(FrostError::InvalidParticipantIndex(included[included.len() - 1], multisig_params.n()))?;
}
// Same signer included multiple times
for i in 0 .. (included.len() - 1) {
if included[i] == included[i + 1] {
Err(FrostError::DuplicatedIndex(included[i]))?;
}
}
let view = self.params.keys.view(&included).unwrap();
validate_map(&preprocesses, &included, multisig_params.i())?;
{
// Domain separate FROST
@@ -242,10 +287,10 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
let nonces = self.params.algorithm.nonces();
#[allow(non_snake_case)]
let mut B = BindingFactor(HashMap::<u16, _>::with_capacity(self.params.view.included().len()));
let mut B = BindingFactor(HashMap::<u16, _>::with_capacity(included.len()));
{
// Parse the preprocesses
for l in &self.params.view.included() {
for l in &included {
{
self
.params
@@ -266,7 +311,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
}
B.insert(*l, commitments);
self.params.algorithm.process_addendum(&self.params.view, *l, addendum)?;
self.params.algorithm.process_addendum(&view, *l, addendum)?;
} else {
let preprocess = preprocesses.remove(l).unwrap();
preprocess.commitments.transcript(self.params.algorithm.transcript());
@@ -277,7 +322,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
}
B.insert(*l, preprocess.commitments);
self.params.algorithm.process_addendum(&self.params.view, *l, preprocess.addendum)?;
self.params.algorithm.process_addendum(&view, *l, preprocess.addendum)?;
}
}
@@ -333,10 +378,10 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
})
.collect::<Vec<_>>();
let share = self.params.algorithm.sign_share(&self.params.view, &Rs, nonces, msg);
let share = self.params.algorithm.sign_share(&view, &Rs, nonces, msg);
Ok((
AlgorithmSignatureMachine { params: self.params.clone(), B, Rs, share },
AlgorithmSignatureMachine { params: self.params.clone(), view, B, Rs, share },
SignatureShare(share),
))
}
@@ -359,6 +404,7 @@ pub trait SignatureMachine<S> {
#[allow(non_snake_case)]
pub struct AlgorithmSignatureMachine<C: Curve, A: Algorithm<C>> {
params: Params<C, A>,
view: ThresholdView<C>,
B: BindingFactor<C>,
Rs: Vec<Vec<C::G>>,
share: C::F,
@@ -376,7 +422,7 @@ impl<C: Curve, A: Algorithm<C>> SignatureMachine<A::Signature> for AlgorithmSign
mut shares: HashMap<u16, SignatureShare<C>>,
) -> Result<A::Signature, FrostError> {
let params = self.params.multisig_params();
validate_map(&shares, &self.params.view.included(), params.i())?;
validate_map(&shares, &self.view.included(), params.i())?;
let mut responses = HashMap::new();
responses.insert(params.i(), self.share);
@@ -389,16 +435,16 @@ impl<C: Curve, A: Algorithm<C>> SignatureMachine<A::Signature> for AlgorithmSign
// Perform signature validation instead of individual share validation
// For the success route, which should be much more frequent, this should be faster
// It also acts as an integrity check of this library's signing function
if let Some(sig) = self.params.algorithm.verify(self.params.view.group_key(), &self.Rs, sum) {
if let Some(sig) = self.params.algorithm.verify(self.view.group_key(), &self.Rs, sum) {
return Ok(sig);
}
// Find out who misbehaved. It may be beneficial to randomly sort this to have detection be
// within n / 2 on average, and not gameable to n, though that should be minor
// TODO
for l in &self.params.view.included() {
for l in &self.view.included() {
if !self.params.algorithm.verify_share(
self.params.view.verification_share(*l),
self.view.verification_share(*l),
&self.B.bound(*l),
responses[l],
) {

View File

@@ -53,10 +53,7 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
.iter()
.filter_map(|(i, keys)| {
if included.contains(i) {
Some((
*i,
AlgorithmMachine::new(algorithm.clone(), keys.clone(), &included.clone()).unwrap(),
))
Some((*i, AlgorithmMachine::new(algorithm.clone(), keys.clone()).unwrap()))
} else {
None
}
@@ -64,10 +61,14 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
.collect()
}
/// Execute the signing protocol.
pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
fn sign_internal<
R: RngCore + CryptoRng,
M: PreprocessMachine,
F: FnMut(&mut R, &mut HashMap<u16, M::SignMachine>),
>(
rng: &mut R,
mut machines: HashMap<u16, M>,
mut cache: F,
msg: &[u8],
) -> M::Signature {
let mut commitments = HashMap::new();
@@ -84,6 +85,8 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
})
.collect::<HashMap<_, _>>();
cache(rng, &mut machines);
let mut shares = HashMap::new();
let mut machines = machines
.drain()
@@ -108,3 +111,43 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
}
signature.unwrap()
}
/// Execute the signing protocol, without caching any machines. This isn't as comprehensive at
/// testing as sign, and accordingly isn't preferred, yet is usable for machines not supporting
/// caching.
pub fn sign_without_caching<R: RngCore + CryptoRng, M: PreprocessMachine>(
rng: &mut R,
machines: HashMap<u16, M>,
msg: &[u8],
) -> M::Signature {
sign_internal(rng, machines, |_, _| {}, msg)
}
/// Execute the signing protocol, randomly caching various machines to ensure they can cache
/// successfully.
pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
rng: &mut R,
params: <M::SignMachine as SignMachine<M::Signature>>::Params,
mut keys: HashMap<u16, <M::SignMachine as SignMachine<M::Signature>>::Keys>,
machines: HashMap<u16, M>,
msg: &[u8],
) -> M::Signature {
sign_internal(
rng,
machines,
|rng, machines| {
// Cache and rebuild half of the machines
let mut included = machines.keys().into_iter().cloned().collect::<Vec<_>>();
for i in included.drain(..) {
if (rng.next_u64() % 2) == 0 {
let cache = machines.remove(&i).unwrap().cache();
machines.insert(
i,
M::SignMachine::from_cache(params.clone(), keys.remove(&i).unwrap(), cache).unwrap(),
);
}
}
},
msg,
)
}

View File

@@ -9,7 +9,7 @@ use rand_core::{RngCore, CryptoRng};
use group::{ff::PrimeField, GroupEncoding};
use dkg::tests::{test_ciphersuite as test_dkg};
use dkg::tests::{key_gen, test_ciphersuite as test_dkg};
use crate::{
curve::Curve,
@@ -19,7 +19,7 @@ use crate::{
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, SignMachine,
SignatureMachine, AlgorithmMachine,
},
tests::{clone_without, recover_key, curve::test_curve},
tests::{clone_without, recover_key, algorithm_machines, sign, curve::test_curve},
};
pub struct Vectors {
@@ -124,6 +124,15 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
// Test the DKG
test_dkg::<_, C>(&mut *rng);
// Test a basic Schnorr signature
{
let keys = key_gen(&mut *rng);
let machines = algorithm_machines(&mut *rng, Schnorr::<C, H>::new(), &keys);
const MSG: &[u8] = b"Hello, World!";
let sig = sign(&mut *rng, Schnorr::<C, H>::new(), keys.clone(), machines, MSG);
assert!(sig.verify(keys[&1].group_key(), H::hram(&sig.R, &keys[&1].group_key(), MSG)));
}
// Test against the vectors
let keys = vectors_to_multisig_keys::<C>(&vectors);
let group_key =
@@ -135,15 +144,7 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
let mut machines = vec![];
for i in &vectors.included {
machines.push((
i,
AlgorithmMachine::new(
Schnorr::<C, H>::new(),
keys[i].clone(),
&vectors.included.to_vec().clone(),
)
.unwrap(),
));
machines.push((i, AlgorithmMachine::new(Schnorr::<C, H>::new(), keys[i].clone()).unwrap()));
}
let mut commitments = HashMap::new();