mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
3.6.2 Test nonce generation
There's two ways which this could be tested. 1) Preprocess not taking in an arbitrary RNG item, yet the relevant bytes This would be an unsafe level of refactoring, in my opinion. 2) Test random_nonce and test the passed in RNG eventually ends up at random_nonce. This takes the latter route, both verifying random_nonce meets the vectors and that the FROST machine calls random_nonce properly.
This commit is contained in:
@@ -90,9 +90,7 @@ pub trait Ciphersuite: Clone + Copy + PartialEq + Eq + Debug + Zeroize {
|
|||||||
// ff mandates this is canonical
|
// ff mandates this is canonical
|
||||||
let res = Option::<Self::F>::from(Self::F::from_repr(encoding))
|
let res = Option::<Self::F>::from(Self::F::from_repr(encoding))
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "non-canonical scalar"));
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "non-canonical scalar"));
|
||||||
for b in encoding.as_mut() {
|
encoding.as_mut().zeroize();
|
||||||
b.zeroize();
|
|
||||||
}
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,10 +92,7 @@ pub trait Curve: Ciphersuite {
|
|||||||
seed = Zeroizing::new(vec![0; 32]);
|
seed = Zeroizing::new(vec![0; 32]);
|
||||||
rng.fill_bytes(&mut seed);
|
rng.fill_bytes(&mut seed);
|
||||||
}
|
}
|
||||||
|
repr.as_mut().zeroize();
|
||||||
for i in repr.as_mut() {
|
|
||||||
i.zeroize();
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use std::collections::HashMap;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
use group::{ff::PrimeField, GroupEncoding};
|
use group::{ff::PrimeField, GroupEncoding};
|
||||||
|
|
||||||
@@ -16,8 +18,8 @@ use crate::{
|
|||||||
Participant, ThresholdCore, ThresholdKeys, FrostError,
|
Participant, ThresholdCore, ThresholdKeys, FrostError,
|
||||||
algorithm::{Schnorr, Hram},
|
algorithm::{Schnorr, Hram},
|
||||||
sign::{
|
sign::{
|
||||||
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, SignMachine,
|
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess,
|
||||||
SignatureMachine, AlgorithmMachine,
|
PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine,
|
||||||
},
|
},
|
||||||
tests::{clone_without, recover_key, algorithm_machines, commit_and_shares, sign},
|
tests::{clone_without, recover_key, algorithm_machines, commit_and_shares, sign},
|
||||||
};
|
};
|
||||||
@@ -31,6 +33,8 @@ pub struct Vectors {
|
|||||||
|
|
||||||
pub msg: String,
|
pub msg: String,
|
||||||
pub included: Vec<Participant>,
|
pub included: Vec<Participant>,
|
||||||
|
|
||||||
|
pub nonce_randomness: Vec<[String; 2]>,
|
||||||
pub nonces: Vec<[String; 2]>,
|
pub nonces: Vec<[String; 2]>,
|
||||||
|
|
||||||
pub sig_shares: Vec<String>,
|
pub sig_shares: Vec<String>,
|
||||||
@@ -63,6 +67,15 @@ impl From<serde_json::Value> for Vectors {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|i| Participant::new(*i).unwrap())
|
.map(|i| Participant::new(*i).unwrap())
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
||||||
|
nonce_randomness: value["round_one_outputs"]["participants"]
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
.map(|value| {
|
||||||
|
[to_str(&value["hiding_nonce_randomness"]), to_str(&value["binding_nonce_randomness"])]
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
nonces: value["round_one_outputs"]["participants"]
|
nonces: value["round_one_outputs"]["participants"]
|
||||||
.as_object()
|
.as_object()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -155,8 +168,10 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
|
|
||||||
// Test against the vectors
|
// Test against the vectors
|
||||||
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
||||||
|
{
|
||||||
let group_key =
|
let group_key =
|
||||||
<C as Curve>::read_G::<&[u8]>(&mut hex::decode(&vectors.group_key).unwrap().as_ref()).unwrap();
|
<C as Curve>::read_G::<&[u8]>(&mut hex::decode(&vectors.group_key).unwrap().as_ref())
|
||||||
|
.unwrap();
|
||||||
let secret =
|
let secret =
|
||||||
C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap();
|
C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap();
|
||||||
assert_eq!(C::generator() * secret, group_key);
|
assert_eq!(C::generator() * secret, group_key);
|
||||||
@@ -168,17 +183,16 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut commitments = HashMap::new();
|
let mut commitments = HashMap::new();
|
||||||
let mut c = 0;
|
|
||||||
let mut machines = machines
|
let mut machines = machines
|
||||||
.drain(..)
|
.drain(..)
|
||||||
.map(|(i, machine)| {
|
.enumerate()
|
||||||
|
.map(|(c, (i, machine))| {
|
||||||
let nonce = |i| {
|
let nonce = |i| {
|
||||||
Zeroizing::new(
|
Zeroizing::new(
|
||||||
C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][i]).unwrap().as_ref()).unwrap(),
|
C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][i]).unwrap().as_ref()).unwrap(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let nonces = [nonce(0), nonce(1)];
|
let nonces = [nonce(0), nonce(1)];
|
||||||
c += 1;
|
|
||||||
let these_commitments =
|
let these_commitments =
|
||||||
[C::generator() * nonces[0].deref(), C::generator() * nonces[1].deref()];
|
[C::generator() * nonces[0].deref(), C::generator() * nonces[1].deref()];
|
||||||
let machine = machine.unsafe_override_preprocess(
|
let machine = machine.unsafe_override_preprocess(
|
||||||
@@ -212,12 +226,13 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut shares = HashMap::new();
|
let mut shares = HashMap::new();
|
||||||
c = 0;
|
|
||||||
let mut machines = machines
|
let mut machines = machines
|
||||||
.drain(..)
|
.drain(..)
|
||||||
.map(|(i, machine)| {
|
.enumerate()
|
||||||
let (machine, share) =
|
.map(|(c, (i, machine))| {
|
||||||
machine.sign(clone_without(&commitments, i), &hex::decode(&vectors.msg).unwrap()).unwrap();
|
let (machine, share) = machine
|
||||||
|
.sign(clone_without(&commitments, i), &hex::decode(&vectors.msg).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let share = {
|
let share = {
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
@@ -225,7 +240,6 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
buf
|
buf
|
||||||
};
|
};
|
||||||
assert_eq!(share, hex::decode(&vectors.sig_shares[c]).unwrap());
|
assert_eq!(share, hex::decode(&vectors.sig_shares[c]).unwrap());
|
||||||
c += 1;
|
|
||||||
|
|
||||||
shares.insert(*i, machine.read_share::<&[u8]>(&mut share.as_ref()).unwrap());
|
shares.insert(*i, machine.read_share::<&[u8]>(&mut share.as_ref()).unwrap());
|
||||||
(i, machine)
|
(i, machine)
|
||||||
@@ -238,4 +252,102 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
serialized.extend(sig.s.to_repr().as_ref());
|
serialized.extend(sig.s.to_repr().as_ref());
|
||||||
assert_eq!(hex::encode(serialized), vectors.sig);
|
assert_eq!(hex::encode(serialized), vectors.sig);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The above code didn't test the nonce generation due to the infeasibility of doing so against
|
||||||
|
// the current codebase
|
||||||
|
|
||||||
|
// A transparent RNG which has a fixed output
|
||||||
|
struct TransparentRng(Option<[u8; 32]>);
|
||||||
|
impl RngCore for TransparentRng {
|
||||||
|
fn next_u32(&mut self) -> u32 {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn next_u64(&mut self) -> u64 {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||||
|
dest.copy_from_slice(&self.0.take().unwrap())
|
||||||
|
}
|
||||||
|
fn try_fill_bytes(&mut self, _: &mut [u8]) -> Result<(), rand_core::Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// CryptoRng requires the output not reveal any info about any other outputs
|
||||||
|
// Since this only will produce one output, this is actually met, even though it'd be fine to
|
||||||
|
// fake it as this is a test
|
||||||
|
impl CryptoRng for TransparentRng {}
|
||||||
|
|
||||||
|
// Test C::random_nonce matches the expected vectors
|
||||||
|
for (i, l) in vectors.included.iter().enumerate() {
|
||||||
|
let l = usize::from(u16::from(*l));
|
||||||
|
|
||||||
|
// Shares are a zero-indexed array of all participants, hence l - 1
|
||||||
|
let share = Zeroizing::new(
|
||||||
|
C::read_F::<&[u8]>(&mut hex::decode(&vectors.shares[l - 1]).unwrap().as_ref()).unwrap(),
|
||||||
|
);
|
||||||
|
for nonce in 0 .. 2 {
|
||||||
|
// Nonces are only present for participating signers, hence i
|
||||||
|
assert_eq!(
|
||||||
|
C::random_nonce(
|
||||||
|
&share,
|
||||||
|
&mut TransparentRng(Some(
|
||||||
|
hex::decode(&vectors.nonce_randomness[i][nonce]).unwrap().try_into().unwrap()
|
||||||
|
))
|
||||||
|
),
|
||||||
|
Zeroizing::new(
|
||||||
|
C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[i][nonce]).unwrap().as_ref())
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This doesn't verify C::random_nonce is called correctly, where the code should call it with
|
||||||
|
// the output from a ChaCha20 stream
|
||||||
|
// Create a known ChaCha20 stream to verify it ends up at random_nonce properly
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut chacha_seed = [0; 32];
|
||||||
|
rng.fill_bytes(&mut chacha_seed);
|
||||||
|
let mut ours = ChaCha20Rng::from_seed(chacha_seed);
|
||||||
|
let frosts = ours.clone();
|
||||||
|
|
||||||
|
// The machines should geenerate a seed, and then use that seed in a ChaCha20 RNG for nonces
|
||||||
|
let mut preprocess_seed = [0; 32];
|
||||||
|
ours.fill_bytes(&mut preprocess_seed);
|
||||||
|
let mut ours = ChaCha20Rng::from_seed(preprocess_seed);
|
||||||
|
|
||||||
|
// Get the randomness which will be used
|
||||||
|
let mut randomness = ([0; 32], [0; 32]);
|
||||||
|
ours.fill_bytes(&mut randomness.0);
|
||||||
|
ours.fill_bytes(&mut randomness.1);
|
||||||
|
|
||||||
|
// Create the machines
|
||||||
|
let mut machines = vec![];
|
||||||
|
for i in &vectors.included {
|
||||||
|
machines.push((i, AlgorithmMachine::new(Schnorr::<C, H>::new(), keys[i].clone()).unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, machine) in machines.drain(..) {
|
||||||
|
let (_, preprocess) = machine.preprocess(&mut frosts.clone());
|
||||||
|
|
||||||
|
// Calculate the expected nonces
|
||||||
|
let mut expected = (C::generator() *
|
||||||
|
C::random_nonce(keys[i].secret_share(), &mut TransparentRng(Some(randomness.0))).deref())
|
||||||
|
.to_bytes()
|
||||||
|
.as_ref()
|
||||||
|
.to_vec();
|
||||||
|
expected.extend(
|
||||||
|
(C::generator() *
|
||||||
|
C::random_nonce(keys[i].secret_share(), &mut TransparentRng(Some(randomness.1)))
|
||||||
|
.deref())
|
||||||
|
.to_bytes()
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure they match
|
||||||
|
assert_eq!(preprocess.serialize(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user