Skip to content

Commit

Permalink
Support caching preprocesses
Browse files Browse the repository at this point in the history
Closes #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.
  • Loading branch information
kayabaNerve committed Dec 8, 2022
1 parent 15236a2 commit 361a022
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 43 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 20 additions & 20 deletions coins/monero/src/tests/clsag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,28 +101,28 @@ fn clsag_multisig() {
}

let mask_sum = random_scalar(&mut OsRng);
let algorithm = ClsagMultisig::new(
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
keys[&1].group_key().0,
Arc::new(RwLock::new(Some(ClsagDetails::new(
ClsagInput::new(
Commitment::new(randomness, AMOUNT),
Decoys {
i: RING_INDEX,
offsets: (1 ..= RING_LEN).into_iter().collect(),
ring: ring.clone(),
},
)
.unwrap(),
mask_sum,
)))),
);

sign(
&mut OsRng,
algorithm_machines(
&mut OsRng,
ClsagMultisig::new(
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
keys[&1].group_key().0,
Arc::new(RwLock::new(Some(ClsagDetails::new(
ClsagInput::new(
Commitment::new(randomness, AMOUNT),
Decoys {
i: RING_INDEX,
offsets: (1 ..= RING_LEN).into_iter().collect(),
ring: ring.clone(),
},
)
.unwrap(),
mask_sum,
)))),
),
&keys,
),
algorithm.clone(),
keys.clone(),
algorithm_machines(&mut OsRng, algorithm, &keys),
&[1; 32],
);
}
25 changes: 23 additions & 2 deletions coins/monero/src/wallet/send/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::{
collections::HashMap,
};

use zeroize::Zeroizing;
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha20Rng;

Expand All @@ -14,8 +15,8 @@ use frost::{
curve::Ed25519,
FrostError, ThresholdKeys,
sign::{
Writable, Preprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine,
AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine,
SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
},
};

Expand Down Expand Up @@ -205,10 +206,30 @@ impl PreprocessMachine for TransactionMachine {
}

impl SignMachine<Transaction> for TransactionSignMachine {
type Params = ();
type Keys = ThresholdKeys<Ed25519>;
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
type SignatureShare = Vec<SignatureShare<Ed25519>>;
type SignatureMachine = TransactionSignatureMachine;

fn cache(self) -> Zeroizing<CachedPreprocess> {
unimplemented!(
"Monero transactions don't support caching their preprocesses due to {}",
"being already bound to a specific transaction"
);
}

fn from_cache(
_: (),
_: ThresholdKeys<Ed25519>,
_: Zeroizing<CachedPreprocess>,
) -> Result<Self, FrostError> {
unimplemented!(
"Monero transactions don't support caching their preprocesses due to {}",
"being already bound to a specific transaction"
);
}

fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect()
}
Expand Down
2 changes: 1 addition & 1 deletion coins/monero/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ macro_rules! test {
);
}

frost::tests::sign(&mut OsRng, machines, &vec![])
frost::tests::sign_without_caching(&mut OsRng, machines, &vec![])
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crypto/frost/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
90 changes: 74 additions & 16 deletions crypto/frost/src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -73,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.
Expand Down Expand Up @@ -100,13 +107,36 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
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"))]
pub(crate) fn unsafe_override_preprocess(
self,
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,
}
}
}

Expand All @@ -119,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.keys.secret_share(),
&params.algorithm.nonces(),
);
let addendum = params.algorithm.preprocess_addendum(rng, &params.keys);

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)
}
}

Expand All @@ -143,15 +165,34 @@ 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.
type SignatureShare: Clone + PartialEq + Writable;
/// 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.
Expand All @@ -169,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())?,
Expand Down
50 changes: 48 additions & 2 deletions crypto/frost/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,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();
Expand All @@ -81,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()
Expand All @@ -105,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,
)
}
13 changes: 11 additions & 2 deletions crypto/frost/src/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 =
Expand Down

0 comments on commit 361a022

Please sign in to comment.