modular_frost/tests/
vectors.rs

1use core::ops::Deref;
2
3use std::collections::HashMap;
4#[cfg(test)]
5use std::str::FromStr;
6
7use zeroize::Zeroizing;
8
9use rand_core::{RngCore, CryptoRng, SeedableRng};
10use rand_chacha::ChaCha20Rng;
11
12use ciphersuite::group::{ff::PrimeField, GroupEncoding};
13
14use crate::{
15  curve::Curve,
16  Participant, ThresholdKeys,
17  algorithm::{Hram, IetfSchnorr},
18  sign::{
19    Writable, Nonce, GeneratorCommitments, NonceCommitments, Commitments, Preprocess,
20    PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine,
21  },
22  tests::{clone_without, recover_key, test_ciphersuite},
23};
24
25/// Vectors for a ciphersuite.
26pub struct Vectors {
27  pub threshold: u16,
28
29  pub group_secret: String,
30  pub group_key: String,
31  pub shares: Vec<String>,
32
33  pub msg: String,
34  pub included: Vec<Participant>,
35
36  pub nonce_randomness: Vec<[String; 2]>,
37  pub nonces: Vec<[String; 2]>,
38  pub commitments: Vec<[String; 2]>,
39
40  pub sig_shares: Vec<String>,
41
42  pub sig: String,
43}
44
45// Vectors are expected to be formatted per the IETF proof of concept
46// The included vectors are directly from
47// https://github.com/cfrg/draft-irtf-cfrg-frost/tree/draft-irtf-cfrg-frost-14/poc
48#[cfg(test)]
49impl From<serde_json::Value> for Vectors {
50  fn from(value: serde_json::Value) -> Vectors {
51    let to_str = |value: &serde_json::Value| value.as_str().unwrap().to_string();
52    Vectors {
53      threshold: u16::from_str(value["config"]["NUM_PARTICIPANTS"].as_str().unwrap()).unwrap(),
54
55      group_secret: to_str(&value["inputs"]["group_secret_key"]),
56      group_key: to_str(&value["inputs"]["group_public_key"]),
57      shares: value["inputs"]["participant_shares"]
58        .as_array()
59        .unwrap()
60        .iter()
61        .map(|share| to_str(&share["participant_share"]))
62        .collect(),
63
64      msg: to_str(&value["inputs"]["message"]),
65      included: value["inputs"]["participant_list"]
66        .as_array()
67        .unwrap()
68        .iter()
69        .map(|i| Participant::new(u16::try_from(i.as_u64().unwrap()).unwrap()).unwrap())
70        .collect(),
71
72      nonce_randomness: value["round_one_outputs"]["outputs"]
73        .as_array()
74        .unwrap()
75        .iter()
76        .map(|value| {
77          [to_str(&value["hiding_nonce_randomness"]), to_str(&value["binding_nonce_randomness"])]
78        })
79        .collect(),
80      nonces: value["round_one_outputs"]["outputs"]
81        .as_array()
82        .unwrap()
83        .iter()
84        .map(|value| [to_str(&value["hiding_nonce"]), to_str(&value["binding_nonce"])])
85        .collect(),
86      commitments: value["round_one_outputs"]["outputs"]
87        .as_array()
88        .unwrap()
89        .iter()
90        .map(|value| {
91          [to_str(&value["hiding_nonce_commitment"]), to_str(&value["binding_nonce_commitment"])]
92        })
93        .collect(),
94
95      sig_shares: value["round_two_outputs"]["outputs"]
96        .as_array()
97        .unwrap()
98        .iter()
99        .map(|value| to_str(&value["sig_share"]))
100        .collect(),
101
102      sig: to_str(&value["final_output"]["sig"]),
103    }
104  }
105}
106
107// Load these vectors into ThresholdKeys using a custom serialization it'll deserialize
108fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<Participant, ThresholdKeys<C>> {
109  let shares = vectors
110    .shares
111    .iter()
112    .map(|secret| C::read_F::<&[u8]>(&mut hex::decode(secret).unwrap().as_ref()).unwrap())
113    .collect::<Vec<_>>();
114  let verification_shares = shares.iter().map(|secret| C::generator() * secret).collect::<Vec<_>>();
115
116  let mut keys = HashMap::new();
117  for i in 1 ..= u16::try_from(shares.len()).unwrap() {
118    // Manually re-implement the serialization for ThresholdKeys to import this data
119    let mut serialized = vec![];
120    serialized.extend(u32::try_from(C::ID.len()).unwrap().to_le_bytes());
121    serialized.extend(C::ID);
122    serialized.extend(vectors.threshold.to_le_bytes());
123    serialized.extend(u16::try_from(shares.len()).unwrap().to_le_bytes());
124    serialized.extend(i.to_le_bytes());
125    serialized.push(1);
126    serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref());
127    for share in &verification_shares {
128      serialized.extend(share.to_bytes().as_ref());
129    }
130
131    let these_keys = ThresholdKeys::<C>::read::<&[u8]>(&mut serialized.as_ref()).unwrap();
132    assert_eq!(these_keys.params().t(), vectors.threshold);
133    assert_eq!(usize::from(these_keys.params().n()), shares.len());
134    let participant = Participant::new(i).unwrap();
135    assert_eq!(these_keys.params().i(), participant);
136    assert_eq!(these_keys.original_secret_share().deref(), &shares[usize::from(i - 1)]);
137    assert_eq!(hex::encode(these_keys.group_key().to_bytes().as_ref()), vectors.group_key);
138    keys.insert(participant, these_keys);
139  }
140
141  keys
142}
143
144/// Test a Ciphersuite with its vectors.
145pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
146  rng: &mut R,
147  vectors: &Vectors,
148) {
149  test_ciphersuite::<R, C, H>(rng);
150
151  // Test against the vectors
152  let keys = vectors_to_multisig_keys::<C>(vectors);
153  {
154    let group_key =
155      <C as Curve>::read_G::<&[u8]>(&mut hex::decode(&vectors.group_key).unwrap().as_ref())
156        .unwrap();
157    let secret =
158      C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap();
159    assert_eq!(C::generator() * secret, group_key);
160    assert_eq!(*recover_key(&keys.values().cloned().collect::<Vec<_>>()).unwrap(), secret);
161
162    let mut machines = vec![];
163    for i in &vectors.included {
164      machines.push((i, AlgorithmMachine::new(IetfSchnorr::<C, H>::ietf(), keys[i].clone())));
165    }
166
167    let mut commitments = HashMap::new();
168    let machines = machines
169      .into_iter()
170      .enumerate()
171      .map(|(c, (i, machine))| {
172        let nonce = |i| {
173          Zeroizing::new(
174            C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][i]).unwrap().as_ref()).unwrap(),
175          )
176        };
177        let nonces = [nonce(0), nonce(1)];
178        let these_commitments =
179          [C::generator() * nonces[0].deref(), C::generator() * nonces[1].deref()];
180
181        assert_eq!(
182          these_commitments[0].to_bytes().as_ref(),
183          hex::decode(&vectors.commitments[c][0]).unwrap()
184        );
185        assert_eq!(
186          these_commitments[1].to_bytes().as_ref(),
187          hex::decode(&vectors.commitments[c][1]).unwrap()
188        );
189
190        let preprocess = Preprocess {
191          commitments: Commitments {
192            nonces: vec![NonceCommitments {
193              generators: vec![GeneratorCommitments(these_commitments)],
194            }],
195          },
196          addendum: (),
197        };
198        // FROST doesn't specify how to serialize these together, yet this is sane
199        // (and the simplest option)
200        assert_eq!(
201          preprocess.serialize(),
202          hex::decode(vectors.commitments[c][0].clone() + &vectors.commitments[c][1]).unwrap()
203        );
204
205        let machine = machine.unsafe_override_preprocess(vec![Nonce(nonces)], preprocess);
206
207        commitments.insert(
208          *i,
209          machine
210            .read_preprocess::<&[u8]>(
211              &mut [
212                these_commitments[0].to_bytes().as_ref(),
213                these_commitments[1].to_bytes().as_ref(),
214              ]
215              .concat()
216              .as_ref(),
217            )
218            .unwrap(),
219        );
220        (i, machine)
221      })
222      .collect::<Vec<_>>();
223
224    let mut shares = HashMap::new();
225    let machines = machines
226      .into_iter()
227      .enumerate()
228      .map(|(c, (i, machine))| {
229        let (machine, share) = machine
230          .sign(clone_without(&commitments, i), &hex::decode(&vectors.msg).unwrap())
231          .unwrap();
232
233        let share = {
234          let mut buf = vec![];
235          share.write(&mut buf).unwrap();
236          buf
237        };
238        assert_eq!(share, hex::decode(&vectors.sig_shares[c]).unwrap());
239
240        shares.insert(*i, machine.read_share::<&[u8]>(&mut share.as_ref()).unwrap());
241        (i, machine)
242      })
243      .collect::<Vec<_>>();
244
245    for (i, machine) in machines {
246      let sig = machine.complete(clone_without(&shares, i)).unwrap();
247      let mut serialized = sig.R.to_bytes().as_ref().to_vec();
248      serialized.extend(sig.s.to_repr().as_ref());
249      assert_eq!(hex::encode(serialized), vectors.sig);
250    }
251  }
252
253  // The above code didn't test the nonce generation due to the infeasibility of doing so against
254  // the current codebase
255
256  // A transparent RNG which has a fixed output
257  struct TransparentRng(Vec<[u8; 32]>);
258  impl RngCore for TransparentRng {
259    fn next_u32(&mut self) -> u32 {
260      unimplemented!()
261    }
262    fn next_u64(&mut self) -> u64 {
263      unimplemented!()
264    }
265    fn fill_bytes(&mut self, dest: &mut [u8]) {
266      dest.copy_from_slice(&self.0.remove(0))
267    }
268    fn try_fill_bytes(&mut self, _: &mut [u8]) -> Result<(), rand_core::Error> {
269      unimplemented!()
270    }
271  }
272  // CryptoRng requires the output not reveal any info about any other outputs
273  // Since this only will produce one output, this is actually met, even though it'd be fine to
274  // fake it as this is a test
275  impl CryptoRng for TransparentRng {}
276
277  // Test C::random_nonce matches the expected vectors
278  for (i, l) in vectors.included.iter().enumerate() {
279    let l = usize::from(u16::from(*l));
280
281    // Shares are a zero-indexed array of all participants, hence l - 1
282    let share = Zeroizing::new(
283      C::read_F::<&[u8]>(&mut hex::decode(&vectors.shares[l - 1]).unwrap().as_ref()).unwrap(),
284    );
285
286    let randomness = vectors.nonce_randomness[i]
287      .iter()
288      .map(|randomness| hex::decode(randomness).unwrap().try_into().unwrap())
289      .collect::<Vec<_>>();
290
291    let nonces = vectors.nonces[i]
292      .iter()
293      .map(|nonce| {
294        Zeroizing::new(C::read_F::<&[u8]>(&mut hex::decode(nonce).unwrap().as_ref()).unwrap())
295      })
296      .collect::<Vec<_>>();
297
298    for (randomness, nonce) in randomness.iter().zip(&nonces) {
299      // Nonces are only present for participating signers, hence i
300      assert_eq!(C::random_nonce(&share, &mut TransparentRng(vec![*randomness])), *nonce);
301    }
302
303    // Also test it at the Commitments level
304    let (generated_nonces, commitments) =
305      Commitments::<C>::new::<_>(&mut TransparentRng(randomness), &share, &[vec![C::generator()]]);
306
307    assert_eq!(generated_nonces.len(), 1);
308    assert_eq!(generated_nonces[0].0, [nonces[0].clone(), nonces[1].clone()]);
309
310    let mut commitments_bytes = vec![];
311    commitments.write(&mut commitments_bytes).unwrap();
312    assert_eq!(
313      commitments_bytes,
314      hex::decode(vectors.commitments[i][0].clone() + &vectors.commitments[i][1]).unwrap()
315    );
316  }
317
318  // This doesn't verify C::random_nonce is called correctly, where the code should call it with
319  // the output from a ChaCha20 stream
320  // Create a known ChaCha20 stream to verify it ends up at random_nonce properly
321
322  {
323    let mut chacha_seed = [0; 32];
324    rng.fill_bytes(&mut chacha_seed);
325    let mut ours = ChaCha20Rng::from_seed(chacha_seed);
326    let frosts = ours.clone();
327
328    // The machines should geenerate a seed, and then use that seed in a ChaCha20 RNG for nonces
329    let mut preprocess_seed = [0; 32];
330    ours.fill_bytes(&mut preprocess_seed);
331    let mut ours = ChaCha20Rng::from_seed(preprocess_seed);
332
333    // Get the randomness which will be used
334    let mut randomness = ([0; 32], [0; 32]);
335    ours.fill_bytes(&mut randomness.0);
336    ours.fill_bytes(&mut randomness.1);
337
338    // Create the machines
339    let mut machines = vec![];
340    for i in &vectors.included {
341      machines.push((i, AlgorithmMachine::new(IetfSchnorr::<C, H>::ietf(), keys[i].clone())));
342    }
343
344    for (i, machine) in machines {
345      let (_, preprocess) = machine.preprocess(&mut frosts.clone());
346
347      // Calculate the expected nonces
348      let mut expected = (C::generator() *
349        C::random_nonce(
350          keys[i].original_secret_share(),
351          &mut TransparentRng(vec![randomness.0]),
352        )
353        .deref())
354      .to_bytes()
355      .as_ref()
356      .to_vec();
357      expected.extend(
358        (C::generator() *
359          C::random_nonce(
360            keys[i].original_secret_share(),
361            &mut TransparentRng(vec![randomness.1]),
362          )
363          .deref())
364        .to_bytes()
365        .as_ref(),
366      );
367
368      // Ensure they match
369      assert_eq!(preprocess.serialize(), expected);
370    }
371  }
372}