mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 19:49:22 +00:00
3.6.9 Add several tests to the FROST library
Offset signing is now tested. Multi-nonce algorithms are now tested. Multi-generator nonce algorithms are now tested. More fault cases are now tested as well.
This commit is contained in:
parent
c6284b85a4
commit
2fd5cd8161
5 changed files with 336 additions and 47 deletions
|
@ -365,6 +365,7 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
|
|||
/// 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.
|
||||
#[must_use]
|
||||
pub fn offset(&self, offset: C::F) -> ThresholdKeys<C> {
|
||||
let mut res = self.clone();
|
||||
// Carry any existing offset
|
||||
|
|
|
@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng};
|
|||
|
||||
use transcript::Transcript;
|
||||
|
||||
use crate::{Curve, Participant, FrostError, ThresholdKeys, ThresholdView};
|
||||
use crate::{Participant, ThresholdKeys, ThresholdView, Curve, FrostError};
|
||||
pub use schnorr::SchnorrSignature;
|
||||
|
||||
/// Write an addendum to a writer.
|
||||
|
|
|
@ -5,11 +5,15 @@ use rand_core::{RngCore, CryptoRng};
|
|||
pub use dkg::tests::{key_gen, recover_key};
|
||||
|
||||
use crate::{
|
||||
Curve, Participant, ThresholdKeys,
|
||||
algorithm::Algorithm,
|
||||
Curve, Participant, ThresholdKeys, FrostError,
|
||||
algorithm::{Algorithm, Hram, Schnorr},
|
||||
sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine},
|
||||
};
|
||||
|
||||
/// Tests for the nonce handling code.
|
||||
pub mod nonces;
|
||||
use nonces::{test_multi_nonce, test_invalid_commitment, test_invalid_dleq_proof};
|
||||
|
||||
/// Vectorized test suite to ensure consistency.
|
||||
pub mod vectors;
|
||||
|
||||
|
@ -62,9 +66,8 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
|||
.collect()
|
||||
}
|
||||
|
||||
// Run the commit step and generate signature shares
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn commit_and_shares<
|
||||
// Run the preprocess step
|
||||
pub(crate) fn preprocess<
|
||||
R: RngCore + CryptoRng,
|
||||
M: PreprocessMachine,
|
||||
F: FnMut(&mut R, &mut HashMap<Participant, M::SignMachine>),
|
||||
|
@ -72,11 +75,7 @@ pub(crate) fn commit_and_shares<
|
|||
rng: &mut R,
|
||||
mut machines: HashMap<Participant, M>,
|
||||
mut cache: F,
|
||||
msg: &[u8],
|
||||
) -> (
|
||||
HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::SignatureMachine>,
|
||||
HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::SignatureShare>,
|
||||
) {
|
||||
) -> (HashMap<Participant, M::SignMachine>, HashMap<Participant, M::Preprocess>) {
|
||||
let mut commitments = HashMap::new();
|
||||
let mut machines = machines
|
||||
.drain()
|
||||
|
@ -93,6 +92,26 @@ pub(crate) fn commit_and_shares<
|
|||
|
||||
cache(rng, &mut machines);
|
||||
|
||||
(machines, commitments)
|
||||
}
|
||||
|
||||
// Run the preprocess and generate signature shares
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn preprocess_and_shares<
|
||||
R: RngCore + CryptoRng,
|
||||
M: PreprocessMachine,
|
||||
F: FnMut(&mut R, &mut HashMap<Participant, M::SignMachine>),
|
||||
>(
|
||||
rng: &mut R,
|
||||
machines: HashMap<Participant, M>,
|
||||
cache: F,
|
||||
msg: &[u8],
|
||||
) -> (
|
||||
HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::SignatureMachine>,
|
||||
HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::SignatureShare>,
|
||||
) {
|
||||
let (mut machines, commitments) = preprocess(rng, machines, cache);
|
||||
|
||||
let mut shares = HashMap::new();
|
||||
let machines = machines
|
||||
.drain()
|
||||
|
@ -120,7 +139,7 @@ fn sign_internal<
|
|||
cache: F,
|
||||
msg: &[u8],
|
||||
) -> M::Signature {
|
||||
let (mut machines, shares) = commit_and_shares(rng, machines, cache, msg);
|
||||
let (mut machines, shares) = preprocess_and_shares(rng, machines, cache, msg);
|
||||
|
||||
let mut signature = None;
|
||||
for (i, machine) in machines.drain() {
|
||||
|
@ -172,3 +191,67 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
|||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
/// Test a basic Schnorr signature.
|
||||
pub fn test_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
const MSG: &[u8] = b"Hello, World!";
|
||||
|
||||
let keys = key_gen(&mut *rng);
|
||||
let machines = algorithm_machines(&mut *rng, Schnorr::<C, H>::new(), &keys);
|
||||
let sig = sign(&mut *rng, Schnorr::<C, H>::new(), keys.clone(), machines, MSG);
|
||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||
assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG)));
|
||||
}
|
||||
|
||||
// Test an offset Schnorr signature.
|
||||
pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
const MSG: &[u8] = b"Hello, World!";
|
||||
|
||||
let mut keys = key_gen(&mut *rng);
|
||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||
|
||||
let offset = C::F::from(5);
|
||||
let offset_key = group_key + (C::generator() * offset);
|
||||
for (_, keys) in keys.iter_mut() {
|
||||
*keys = keys.offset(offset);
|
||||
assert_eq!(keys.group_key(), offset_key);
|
||||
}
|
||||
|
||||
let machines = algorithm_machines(&mut *rng, Schnorr::<C, H>::new(), &keys);
|
||||
let sig = sign(&mut *rng, Schnorr::<C, H>::new(), keys.clone(), machines, MSG);
|
||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||
assert!(sig.verify(offset_key, H::hram(&sig.R, &group_key, MSG)));
|
||||
}
|
||||
|
||||
// Test blame for an invalid Schnorr signature share.
|
||||
pub fn test_schnorr_blame<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
const MSG: &[u8] = b"Hello, World!";
|
||||
|
||||
let keys = key_gen(&mut *rng);
|
||||
let machines = algorithm_machines(&mut *rng, Schnorr::<C, H>::new(), &keys);
|
||||
|
||||
let (mut machines, shares) = preprocess_and_shares(&mut *rng, machines, |_, _| {}, MSG);
|
||||
|
||||
for (i, machine) in machines.drain() {
|
||||
let mut shares = clone_without(&shares, &i);
|
||||
|
||||
// Select a random participant to give an invalid share
|
||||
let participants = shares.keys().collect::<Vec<_>>();
|
||||
let faulty = *participants
|
||||
[usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()];
|
||||
shares.get_mut(&faulty).unwrap().invalidate();
|
||||
|
||||
assert_eq!(machine.complete(shares).err(), Some(FrostError::InvalidShare(faulty)));
|
||||
}
|
||||
}
|
||||
|
||||
// Run a variety of tests against a ciphersuite.
|
||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
test_schnorr::<R, C, H>(rng);
|
||||
test_offset_schnorr::<R, C, H>(rng);
|
||||
test_schnorr_blame::<R, C, H>(rng);
|
||||
|
||||
test_multi_nonce::<R, C>(rng);
|
||||
test_invalid_commitment::<R, C>(rng);
|
||||
test_invalid_dleq_proof::<R, C>(rng);
|
||||
}
|
||||
|
|
236
crypto/frost/src/tests/nonces.rs
Normal file
236
crypto/frost/src/tests/nonces.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
use std::io::{self, Read};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use group::{ff::Field, Group, GroupEncoding};
|
||||
|
||||
use dleq::MultiDLEqProof;
|
||||
pub use dkg::tests::{key_gen, recover_key};
|
||||
|
||||
use crate::{
|
||||
Curve, Participant, ThresholdView, ThresholdKeys, FrostError,
|
||||
algorithm::Algorithm,
|
||||
sign::{Writable, SignMachine},
|
||||
tests::{algorithm_machines, preprocess, sign},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MultiNonce<C: Curve> {
|
||||
transcript: RecommendedTranscript,
|
||||
nonces: Option<Vec<Vec<C::G>>>,
|
||||
}
|
||||
|
||||
impl<C: Curve> MultiNonce<C> {
|
||||
fn new() -> MultiNonce<C> {
|
||||
MultiNonce {
|
||||
transcript: RecommendedTranscript::new(b"FROST MultiNonce Algorithm Test"),
|
||||
nonces: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nonces<C: Curve>() -> Vec<Vec<C::G>> {
|
||||
vec![
|
||||
vec![C::generator(), C::generator().double()],
|
||||
vec![C::generator(), C::generator() * C::F::from(3), C::generator() * C::F::from(4)],
|
||||
]
|
||||
}
|
||||
|
||||
fn verify_nonces<C: Curve>(nonces: &[Vec<C::G>]) {
|
||||
assert_eq!(nonces.len(), 2);
|
||||
|
||||
// Each nonce should be a series of commitments, over some generators, which share a discrete log
|
||||
// Since they share a discrete log, their only distinction should be the generator
|
||||
// Above, the generators were created with a known relationship
|
||||
// Accordingly, we can check here that relationship holds to make sure these commitments are well
|
||||
// formed
|
||||
assert_eq!(nonces[0].len(), 2);
|
||||
assert_eq!(nonces[0][0].double(), nonces[0][1]);
|
||||
|
||||
assert_eq!(nonces[1].len(), 3);
|
||||
assert_eq!(nonces[1][0] * C::F::from(3), nonces[1][1]);
|
||||
assert_eq!(nonces[1][0] * C::F::from(4), nonces[1][2]);
|
||||
|
||||
assert!(nonces[0][0] != nonces[1][0]);
|
||||
}
|
||||
|
||||
impl<C: Curve> Algorithm<C> for MultiNonce<C> {
|
||||
type Transcript = RecommendedTranscript;
|
||||
type Addendum = ();
|
||||
type Signature = ();
|
||||
|
||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
||||
&mut self.transcript
|
||||
}
|
||||
|
||||
fn nonces(&self) -> Vec<Vec<C::G>> {
|
||||
nonces::<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(())
|
||||
}
|
||||
|
||||
fn process_addendum(
|
||||
&mut self,
|
||||
_: &ThresholdView<C>,
|
||||
_: Participant,
|
||||
_: (),
|
||||
) -> Result<(), FrostError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_share(
|
||||
&mut self,
|
||||
_: &ThresholdView<C>,
|
||||
nonce_sums: &[Vec<C::G>],
|
||||
nonces: Vec<Zeroizing<C::F>>,
|
||||
_: &[u8],
|
||||
) -> C::F {
|
||||
// Verify the nonce sums are as expected
|
||||
verify_nonces::<C>(nonce_sums);
|
||||
|
||||
// Verify we actually have two nonces and that they're distinct
|
||||
assert_eq!(nonces.len(), 2);
|
||||
assert!(nonces[0] != nonces[1]);
|
||||
|
||||
// Save the nonce sums for later so we can check they're consistent with the call to verify
|
||||
assert!(self.nonces.is_none());
|
||||
self.nonces = Some(nonce_sums.to_vec());
|
||||
|
||||
// Sum the nonces so we can later check they actually have a relationship to nonce_sums
|
||||
let mut res = C::F::zero();
|
||||
|
||||
// Weight each nonce
|
||||
// This is probably overkill, since their unweighted forms would practically still require
|
||||
// some level of crafting to pass a naive sum via malleability, yet this makes it more robust
|
||||
for nonce in nonce_sums {
|
||||
self.transcript.domain_separate(b"nonce");
|
||||
for commitment in nonce {
|
||||
self.transcript.append_message(b"commitment", commitment.to_bytes());
|
||||
}
|
||||
}
|
||||
let mut rng = ChaCha20Rng::from_seed(self.transcript.clone().rng_seed(b"weight"));
|
||||
|
||||
for nonce in nonces {
|
||||
res += *nonce * C::F::random(&mut rng);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn verify(&self, _: C::G, nonces: &[Vec<C::G>], sum: C::F) -> Option<Self::Signature> {
|
||||
verify_nonces::<C>(nonces);
|
||||
assert_eq!(&self.nonces.clone().unwrap(), nonces);
|
||||
|
||||
// Make sure the nonce sums actually relate to the nonces
|
||||
let mut res = C::G::identity();
|
||||
let mut rng = ChaCha20Rng::from_seed(self.transcript.clone().rng_seed(b"weight"));
|
||||
for nonce in nonces {
|
||||
res += nonce[0] * C::F::random(&mut rng);
|
||||
}
|
||||
assert_eq!(res, C::generator() * sum);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn verify_share(&self, _: C::G, _: &[Vec<C::G>], _: C::F) -> Result<Vec<(C::F, C::G)>, ()> {
|
||||
panic!("share verification triggered");
|
||||
}
|
||||
}
|
||||
|
||||
/// Test a multi-nonce, multi-generator algorithm.
|
||||
// Specifically verifies this library can:
|
||||
// 1) Generate multiple nonces
|
||||
// 2) Provide the group nonces (nonce_sums) across multiple generators, still with the same
|
||||
// discrete log
|
||||
// 3) Provide algorithms with nonces which match the group nonces
|
||||
pub fn test_multi_nonce<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
let keys = key_gen::<R, C>(&mut *rng);
|
||||
let machines = algorithm_machines(&mut *rng, MultiNonce::<C>::new(), &keys);
|
||||
sign(&mut *rng, MultiNonce::<C>::new(), keys.clone(), machines, &[]);
|
||||
}
|
||||
|
||||
/// Test malleating a commitment for a nonce across generators causes the preprocess to error.
|
||||
pub fn test_invalid_commitment<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
let keys = key_gen::<R, C>(&mut *rng);
|
||||
let machines = algorithm_machines(&mut *rng, MultiNonce::<C>::new(), &keys);
|
||||
let (machines, mut preprocesses) = preprocess(&mut *rng, machines, |_, _| {});
|
||||
|
||||
// Select a random participant to give an invalid commitment
|
||||
let participants = preprocesses.keys().collect::<Vec<_>>();
|
||||
let faulty = *participants
|
||||
[usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()];
|
||||
|
||||
// Grab their preprocess
|
||||
let mut preprocess = preprocesses.remove(&faulty).unwrap();
|
||||
|
||||
// Mutate one of the commitments
|
||||
let nonce =
|
||||
preprocess.commitments.nonces.get_mut(usize::try_from(rng.next_u64()).unwrap() % 2).unwrap();
|
||||
let generators_len = nonce.generators.len();
|
||||
*nonce
|
||||
.generators
|
||||
.get_mut(usize::try_from(rng.next_u64()).unwrap() % generators_len)
|
||||
.unwrap()
|
||||
.0
|
||||
.get_mut(usize::try_from(rng.next_u64()).unwrap() % 2)
|
||||
.unwrap() = C::G::random(&mut *rng);
|
||||
|
||||
// The commitments are validated at time of deserialization (read_preprocess)
|
||||
// Accordingly, serialize it and read it again to make sure that errors
|
||||
assert!(machines
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1
|
||||
.read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref())
|
||||
.is_err());
|
||||
}
|
||||
|
||||
/// Test malleating the DLEq proof for a preprocess causes it to error.
|
||||
pub fn test_invalid_dleq_proof<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
let keys = key_gen::<R, C>(&mut *rng);
|
||||
let machines = algorithm_machines(&mut *rng, MultiNonce::<C>::new(), &keys);
|
||||
let (machines, mut preprocesses) = preprocess(&mut *rng, machines, |_, _| {});
|
||||
|
||||
// Select a random participant to give an invalid DLEq proof
|
||||
let participants = preprocesses.keys().collect::<Vec<_>>();
|
||||
let faulty = *participants
|
||||
[usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()];
|
||||
|
||||
// Invalidate it by replacing it with a completely different proof
|
||||
let dlogs = [Zeroizing::new(C::F::random(&mut *rng)), Zeroizing::new(C::F::random(&mut *rng))];
|
||||
let mut preprocess = preprocesses.remove(&faulty).unwrap();
|
||||
preprocess.commitments.dleq = Some(MultiDLEqProof::prove(
|
||||
&mut *rng,
|
||||
&mut RecommendedTranscript::new(b"Invalid DLEq Proof"),
|
||||
&nonces::<C>(),
|
||||
&dlogs,
|
||||
));
|
||||
|
||||
assert!(machines
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1
|
||||
.read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref())
|
||||
.is_err());
|
||||
|
||||
// Also test None for a proof will cause an error
|
||||
preprocess.commitments.dleq = None;
|
||||
assert!(machines
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1
|
||||
.read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref())
|
||||
.is_err());
|
||||
}
|
|
@ -11,17 +11,15 @@ use rand_chacha::ChaCha20Rng;
|
|||
|
||||
use group::{ff::PrimeField, GroupEncoding};
|
||||
|
||||
use dkg::tests::key_gen;
|
||||
|
||||
use crate::{
|
||||
curve::Curve,
|
||||
Participant, ThresholdCore, ThresholdKeys, FrostError,
|
||||
algorithm::{IetfTranscript, Schnorr, Hram},
|
||||
Participant, ThresholdCore, ThresholdKeys,
|
||||
algorithm::{IetfTranscript, Hram, Schnorr},
|
||||
sign::{
|
||||
Writable, Nonce, GeneratorCommitments, NonceCommitments, Commitments, Preprocess,
|
||||
PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine,
|
||||
},
|
||||
tests::{clone_without, recover_key, algorithm_machines, commit_and_shares, sign},
|
||||
tests::{clone_without, recover_key, test_ciphersuite},
|
||||
};
|
||||
|
||||
pub struct Vectors {
|
||||
|
@ -147,36 +145,7 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||
rng: &mut R,
|
||||
vectors: Vectors,
|
||||
) {
|
||||
// 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);
|
||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||
assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG)));
|
||||
}
|
||||
|
||||
// Test blame on an invalid Schnorr signature share
|
||||
{
|
||||
let keys = key_gen(&mut *rng);
|
||||
let machines = algorithm_machines(&mut *rng, Schnorr::<C, H>::new(), &keys);
|
||||
const MSG: &[u8] = b"Hello, World!";
|
||||
|
||||
let (mut machines, mut shares) = commit_and_shares(&mut *rng, machines, |_, _| {}, MSG);
|
||||
let faulty = *shares.keys().next().unwrap();
|
||||
shares.get_mut(&faulty).unwrap().invalidate();
|
||||
|
||||
for (i, machine) in machines.drain() {
|
||||
if i == faulty {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(
|
||||
machine.complete(clone_without(&shares, &i)).err(),
|
||||
Some(FrostError::InvalidShare(faulty))
|
||||
);
|
||||
}
|
||||
}
|
||||
test_ciphersuite::<R, C, H>(rng);
|
||||
|
||||
// Test against the vectors
|
||||
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
||||
|
|
Loading…
Reference in a new issue