From 44452d9bfe0a2437ffac98008dbc6e6027e77e46 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 3 Jun 2022 01:25:46 -0400 Subject: [PATCH] Verify being FROST v5 compliant No functional changes have been made to signing, with solely slight API changes being made. Technically not actually FROST v5 compatible, due to differing on zero checks and randomness, yet the vectors do confirm the core algorithm. For any valid FROST implementation, this will be interoperable if they can successfully communicate. For any devious FROST implementation, this will be fingerprintable, yet should still be valid. Relevant to https://github.com/serai-dex/serai/issues/9 as any curve can now specify vectors for itself and be tested against them. Moves the FROST testing curve from k256 to p256. Does not expose p256 despite being compliant. It's not at a point I'm happy with it, notably regarding hash to curve, and I'm not sure I care to support p256. If it has value to the larger FROST ecosystem... --- coins/monero/src/frost.rs | 14 +- crypto/frost/Cargo.toml | 3 +- crypto/frost/src/key_gen.rs | 7 +- crypto/frost/src/lib.rs | 18 +- crypto/frost/src/sign.rs | 16 +- crypto/frost/src/tests/literal/mod.rs | 2 +- crypto/frost/src/tests/literal/p256.rs | 222 ++++++++++++++++++++ crypto/frost/src/tests/literal/schnorr.rs | 16 +- crypto/frost/src/tests/literal/secp256k1.rs | 120 ----------- crypto/frost/src/tests/mod.rs | 1 + crypto/frost/src/tests/vectors.rs | 117 +++++++++++ 11 files changed, 387 insertions(+), 149 deletions(-) create mode 100644 crypto/frost/src/tests/literal/p256.rs delete mode 100644 crypto/frost/src/tests/literal/secp256k1.rs create mode 100644 crypto/frost/src/tests/vectors.rs diff --git a/coins/monero/src/frost.rs b/coins/monero/src/frost.rs index dfe24ad6..0da52dc0 100644 --- a/coins/monero/src/frost.rs +++ b/coins/monero/src/frost.rs @@ -39,14 +39,14 @@ impl Curve for Ed25519 { type G = dfg::EdwardsPoint; type T = &'static dfg::EdwardsBasepointTable; - fn id() -> String { - "Ed25519".to_string() - } - fn id_len() -> u8 { u8::try_from(Self::id().len()).unwrap() } + fn id() -> &'static [u8] { + b"Ed25519" + } + fn generator() -> Self::G { Self::G::generator() } @@ -67,11 +67,11 @@ impl Curve for Ed25519 { } fn hash_binding_factor(binding: &[u8]) -> Self::F { - Self::hash_to_F(&[b"rho", binding].concat()) + Self::hash_to_F(b"rho", binding) } - fn hash_to_F(data: &[u8]) -> Self::F { - dfg::Scalar::from_hash(Blake2b512::new().chain(data)) + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + dfg::Scalar::from_hash(Blake2b512::new().chain(dst).chain(msg)) } fn F_len() -> usize { diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index e0ae0b85..d5f5f2dc 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" thiserror = "1" rand_core = "0.6" +hex = "0.4" ff = "0.11" group = "0.11" @@ -21,4 +22,4 @@ multiexp = { path = "../multiexp", features = ["batch"] } [dev-dependencies] rand = "0.8" sha2 = "0.10" -k256 = { version = "0.10", features = ["arithmetic"] } +p256 = { version = "0.10", features = ["arithmetic"] } diff --git a/crypto/frost/src/key_gen.rs b/crypto/frost/src/key_gen.rs index 67e32a76..643a2454 100644 --- a/crypto/frost/src/key_gen.rs +++ b/crypto/frost/src/key_gen.rs @@ -16,8 +16,13 @@ use crate::{ #[allow(non_snake_case)] fn challenge(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F { const DST: &'static [u8] = b"FROST Schnorr Proof of Knowledge"; + // Uses hash_msg to get a fixed size value out of the context string - C::hash_to_F(&[DST, &C::hash_msg(context.as_bytes()), &l.to_be_bytes(), R, Am].concat()) + let mut transcript = C::hash_msg(context.as_bytes()); + transcript.extend(l.to_be_bytes()); + transcript.extend(R); + transcript.extend(Am); + C::hash_to_F(DST, &transcript) } // Implements steps 1 through 3 of round 1 of FROST DKG. Returns the coefficients, commitments, and diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs index 35071889..a600466a 100644 --- a/crypto/frost/src/lib.rs +++ b/crypto/frost/src/lib.rs @@ -40,12 +40,11 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug { /// Precomputed table type type T: Mul; - /// ID for this curve - fn id() -> String; /// Byte length of the curve ID - // While curve.id().len() is trivial, this bounds it to u8 and lets us ignore the possibility it - // contains Unicode, therefore having a String length which is different from its byte length + // While C::id().len() is trivial, this bounds it to u8 for any proper Curve implementation fn id_len() -> u8; + /// ID for this curve + fn id() -> &'static [u8]; /// Generator for the group // While group does provide this in its API, Jubjub users will want to use a custom basepoint @@ -79,7 +78,7 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug { // Not parameterized by Digest as it's fine for it to use its own hash function as relevant to // hash_msg and hash_binding_factor #[allow(non_snake_case)] - fn hash_to_F(data: &[u8]) -> Self::F; + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F; /// Constant size of a serialized field element // The alternative way to grab this would be either serializing a junk element and getting its @@ -255,6 +254,10 @@ pub struct MultisigKeys { } impl MultisigKeys { + /// 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 + /// Not IETF compliant pub fn offset(&self, offset: C::F) -> MultisigKeys { let mut res = self.clone(); // Carry any existing offset @@ -311,7 +314,7 @@ impl MultisigKeys { 1 + usize::from(C::id_len()) + MultisigKeys::::serialized_len(self.params.n) ); serialized.push(C::id_len()); - serialized.extend(C::id().as_bytes()); + serialized.extend(C::id()); serialized.extend(&self.params.t.to_be_bytes()); serialized.extend(&self.params.n.to_be_bytes()); serialized.extend(&self.params.i.to_be_bytes()); @@ -336,8 +339,7 @@ impl MultisigKeys { Err(FrostError::InternalError("ID wasn't included".to_string()))?; } - let id = &serialized[cursor .. (cursor + id_len)]; - if C::id().as_bytes() != id { + if C::id() != &serialized[cursor .. (cursor + id_len)] { Err( FrostError::InternalError( "curve is distinct between serialization and deserialization".to_string() diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs index 23a63f50..ae33735b 100644 --- a/crypto/frost/src/sign.rs +++ b/crypto/frost/src/sign.rs @@ -69,9 +69,9 @@ impl> Params { } } -struct PreprocessPackage { - nonces: [C::F; 2], - serialized: Vec, +pub(crate) struct PreprocessPackage { + pub(crate) nonces: [C::F; 2], + pub(crate) serialized: Vec, } // This library unifies the preprocessing step with signing due to security concerns and to provide @@ -306,6 +306,16 @@ impl> AlgorithmMachine { } ) } + + pub(crate) fn unsafe_override_preprocess(&mut self, preprocess: PreprocessPackage) { + if self.state != State::Fresh { + // This would be unacceptable, yet this is pub(crate) and explicitly labelled unsafe + // It's solely used in a testing environment, which is how it's justified + Err::<(), _>(FrostError::InvalidSignTransition(State::Fresh, self.state)).unwrap(); + } + self.preprocess = Some(preprocess); + self.state = State::Preprocessed; + } } impl> StateMachine for AlgorithmMachine { diff --git a/crypto/frost/src/tests/literal/mod.rs b/crypto/frost/src/tests/literal/mod.rs index d766f844..adb87b1a 100644 --- a/crypto/frost/src/tests/literal/mod.rs +++ b/crypto/frost/src/tests/literal/mod.rs @@ -1,2 +1,2 @@ -mod secp256k1; +mod p256; mod schnorr; diff --git a/crypto/frost/src/tests/literal/p256.rs b/crypto/frost/src/tests/literal/p256.rs new file mode 100644 index 00000000..75aa07bb --- /dev/null +++ b/crypto/frost/src/tests/literal/p256.rs @@ -0,0 +1,222 @@ +use core::convert::TryInto; + +use rand::rngs::OsRng; + +use ff::{Field, PrimeField}; +use group::GroupEncoding; + +use sha2::{digest::Update, Digest, Sha256}; + +use p256::{elliptic_curve::bigint::{Encoding, U384}, Scalar, ProjectivePoint}; + +use crate::{ + CurveError, Curve, + algorithm::Hram, + tests::{curve::test_curve, vectors::{Vectors, vectors}} +}; + +const CONTEXT_STRING: &[u8] = b"FROST-P256-SHA256-v5"; + +fn expand_message_xmd_sha256(dst: &[u8], msg: &[u8], len: u16) -> Option> { + const OUTPUT_SIZE: u16 = 32; + const BLOCK_SIZE: u16 = 64; + + let blocks = ((len + OUTPUT_SIZE) - 1) / OUTPUT_SIZE; + if blocks > 255 { + return None; + } + let blocks = blocks as u8; + + let mut dst = dst; + let oversize = Sha256::digest([b"H2C-OVERSIZE-DST-", dst].concat()); + if dst.len() > 255 { + dst = &oversize; + } + let dst_prime = &[dst, &[dst.len() as u8]].concat(); + + let mut msg_prime = vec![0; BLOCK_SIZE.into()]; + msg_prime.extend(msg); + msg_prime.extend(len.to_be_bytes()); + msg_prime.push(0); + msg_prime.extend(dst_prime); + + let mut b = vec![Sha256::digest(&msg_prime).to_vec()]; + + { + let mut b1 = b[0].clone(); + b1.push(1); + b1.extend(dst_prime); + b.push(Sha256::digest(&b1).to_vec()); + } + + for i in 2 ..= blocks { + let mut msg = b[0] + .iter().zip(b[usize::from(i) - 1].iter()) + .map(|(a, b)| *a ^ b).collect::>(); + msg.push(i); + msg.extend(dst_prime); + b.push(Sha256::digest(msg).to_vec()); + } + + Some(b[1 ..].concat()[.. usize::from(len)].to_vec()) +} + +#[test] +fn test_xmd_sha256() { + assert_eq!( + hex::encode(expand_message_xmd_sha256(b"QUUX-V01-CS02-with-expander", b"", 0x80).unwrap()), + ( + "8bcffd1a3cae24cf9cd7ab85628fd111bb17e3739d3b53f8".to_owned() + + "9580d217aa79526f1708354a76a402d3569d6a9d19ef3de4d0b991" + + "e4f54b9f20dcde9b95a66824cbdf6c1a963a1913d43fd7ac443a02" + + "fc5d9d8d77e2071b86ab114a9f34150954a7531da568a1ea8c7608" + + "61c0cde2005afc2c114042ee7b5848f5303f0611cf297f" + ) + ); +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct P256; +impl Curve for P256 { + type F = Scalar; + type G = ProjectivePoint; + type T = ProjectivePoint; + + fn id_len() -> u8 { + u8::try_from(Self::id().len()).unwrap() + } + + fn id() -> &'static [u8] { + b"P-256" + } + + fn generator() -> Self::G { + Self::G::GENERATOR + } + + fn generator_table() -> Self::T { + Self::G::GENERATOR + } + + fn little_endian() -> bool { + false + } + + fn hash_msg(msg: &[u8]) -> Vec { + (&Sha256::new() + .chain(CONTEXT_STRING) + .chain(b"digest") + .chain(msg) + .finalize() + ).to_vec() + } + + fn hash_binding_factor(binding: &[u8]) -> Self::F { + Self::hash_to_F(&[CONTEXT_STRING, b"rho"].concat(), binding) + } + + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + let mut modulus = vec![0; 16]; + modulus.extend(&(Scalar::zero() - Scalar::one()).to_repr()); + let modulus = U384::from_be_slice(&modulus).wrapping_add(&U384::ONE); + Self::F_from_slice( + &U384::from_be_slice( + &expand_message_xmd_sha256(dst, msg, 48).unwrap() + ).reduce(&modulus).unwrap().to_be_bytes()[16 ..] + ).unwrap() + } + + fn F_len() -> usize { + 32 + } + + fn G_len() -> usize { + 33 + } + + fn F_from_slice(slice: &[u8]) -> Result { + let bytes: [u8; 32] = slice.try_into() + .map_err(|_| CurveError::InvalidLength(32, slice.len()))?; + + let scalar = Scalar::from_repr(bytes.into()); + if scalar.is_none().into() { + Err(CurveError::InvalidScalar)?; + } + + Ok(scalar.unwrap()) + } + + fn G_from_slice(slice: &[u8]) -> Result { + let bytes: [u8; 33] = slice.try_into() + .map_err(|_| CurveError::InvalidLength(33, slice.len()))?; + + let point = ProjectivePoint::from_bytes(&bytes.into()); + if point.is_none().into() { + Err(CurveError::InvalidPoint)?; + } + + Ok(point.unwrap()) + } + + fn F_to_bytes(f: &Self::F) -> Vec { + (&f.to_bytes()).to_vec() + } + + fn G_to_bytes(g: &Self::G) -> Vec { + (&g.to_bytes()).to_vec() + } +} + +#[test] +fn p256_curve() { + test_curve::<_, P256>(&mut OsRng); +} + +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct IetfP256Hram {} +impl Hram for IetfP256Hram { + #[allow(non_snake_case)] + fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { + P256::hash_to_F( + &[CONTEXT_STRING, b"chal"].concat(), + &[&P256::G_to_bytes(R), &P256::G_to_bytes(A), m].concat() + ) + } +} + +#[test] +fn p256_vectors() { + vectors::( + Vectors { + threshold: 2, + shares: &[ + "0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731", + "8d8e787bef0ff6c2f494ca45f4dad198c6bee01212d6c84067159c52e1863ad5", + "0e80d6e8f6192c003b5488ce1eec8f5429587d48cf001541e713b2d53c09d928" + ], + group_secret: "8ba9bba2e0fd8c4767154d35a0b7562244a4aaf6f36c8fb8735fa48b301bd8de", + group_key: "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70", + + msg: "74657374", + included: &[1, 3], + nonces: &[ + [ + "081617b24375e069b39f649d4c4ce2fba6e38b73e7c16759de0b6079a22c4c7e", + "4de5fb77d99f03a2491a83a6a4cb91ca3c82a3f34ce94cec939174f47c9f95dd" + ], + [ + "d186ea92593f83ea83181b184d41aa93493301ac2bc5b4b1767e94d2db943e38", + "486e2ee25a3fbc8e6399d748b077a2755fde99fa85cc24fa647ea4ebf5811a15" + ] + ], + binding: "cf7ffe4b8ad6edb6237efaa8cbfb2dfb2fd08d163b6ad9063720f14779a9e143", + sig_shares: &[ + "9e4d8865faf8c7b3193a3b35eda3d9e12118447114b1e7d5b4809ea28067f8a9", + "b7d094eab6305ae74daeed1acd31abba9ab81f638d38b72c132cb25a5dfae1fc" + ], + sig: "0342c14c77f9d4ef9b8bd64fb0d7bbfdb9f8216a44e5f7bbe6ac0f3ed5e1a57367".to_owned() + + "561e1d51b129229966e92850bad5859bfee96926fad3007cd3f38639e1ffb554" + } + ); +} diff --git a/crypto/frost/src/tests/literal/schnorr.rs b/crypto/frost/src/tests/literal/schnorr.rs index 1ceac748..0d81bf8a 100644 --- a/crypto/frost/src/tests/literal/schnorr.rs +++ b/crypto/frost/src/tests/literal/schnorr.rs @@ -4,7 +4,7 @@ use rand::rngs::OsRng; use crate::{ Curve, schnorr, algorithm::{Hram, Schnorr}, - tests::{key_gen, algorithm_machines, sign as sign_test, literal::secp256k1::{Secp256k1, TestHram}} + tests::{key_gen, algorithm_machines, sign as sign_test, literal::p256::{P256, IetfP256Hram}} }; const MESSAGE: &[u8] = b"Hello World"; @@ -15,8 +15,8 @@ fn sign() { &mut OsRng, algorithm_machines( &mut OsRng, - Schnorr::::new(), - &key_gen::<_, Secp256k1>(&mut OsRng) + Schnorr::::new(), + &key_gen::<_, P256>(&mut OsRng) ), MESSAGE ); @@ -24,19 +24,19 @@ fn sign() { #[test] fn sign_with_offset() { - let mut keys = key_gen::<_, Secp256k1>(&mut OsRng); + let mut keys = key_gen::<_, P256>(&mut OsRng); let group_key = keys[&1].group_key(); - let offset = Secp256k1::hash_to_F(b"offset"); + let offset = P256::hash_to_F(b"offset", &[]); for i in 1 ..= u16::try_from(keys.len()).unwrap() { keys.insert(i, Rc::new(keys[&i].offset(offset))); } - let offset_key = group_key + (Secp256k1::generator_table() * offset); + let offset_key = group_key + (P256::generator_table() * offset); let sig = sign_test( &mut OsRng, - algorithm_machines(&mut OsRng, Schnorr::::new(), &keys), + algorithm_machines(&mut OsRng, Schnorr::::new(), &keys), MESSAGE ); - assert!(schnorr::verify(offset_key, TestHram::hram(&sig.R, &offset_key, MESSAGE), &sig)); + assert!(schnorr::verify(offset_key, IetfP256Hram::hram(&sig.R, &offset_key, MESSAGE), &sig)); } diff --git a/crypto/frost/src/tests/literal/secp256k1.rs b/crypto/frost/src/tests/literal/secp256k1.rs deleted file mode 100644 index e10bee07..00000000 --- a/crypto/frost/src/tests/literal/secp256k1.rs +++ /dev/null @@ -1,120 +0,0 @@ -use core::convert::TryInto; - -use rand::rngs::OsRng; - -use ff::PrimeField; -use group::GroupEncoding; - -use sha2::{Digest, Sha256, Sha512}; - -use k256::{ - elliptic_curve::{generic_array::GenericArray, bigint::{ArrayEncoding, U512}, ops::Reduce}, - Scalar, - ProjectivePoint -}; - -use crate::{CurveError, Curve, algorithm::Hram, tests::curve::test_curve}; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Secp256k1; -impl Curve for Secp256k1 { - type F = Scalar; - type G = ProjectivePoint; - type T = ProjectivePoint; - - fn id() -> String { - "secp256k1".to_string() - } - - fn id_len() -> u8 { - u8::try_from(Self::id().len()).unwrap() - } - - fn generator() -> Self::G { - Self::G::GENERATOR - } - - fn generator_table() -> Self::T { - Self::G::GENERATOR - } - - fn little_endian() -> bool { - false - } - - // The IETF draft doesn't specify a secp256k1 ciphersuite - // This test just uses the simplest ciphersuite which would still be viable to deploy - // The comparable P-256 curve uses hash_to_field from the Hash To Curve IETF draft with a context - // string and further DST for H1 ("rho") and H3 ("digest"). It's not currently worth it to add - // that weight, yet if secp256k1 is ever officially acknowledged (not just a testing curve), it - // must be properly implemented. - fn hash_msg(msg: &[u8]) -> Vec { - (&Sha256::digest(msg)).to_vec() - } - - fn hash_binding_factor(binding: &[u8]) -> Self::F { - Self::hash_to_F(&[b"rho", binding].concat()) - } - - // Use wide reduction for security - fn hash_to_F(data: &[u8]) -> Self::F { - Scalar::from_uint_reduced(U512::from_be_byte_array(Sha512::digest(data))) - } - - fn F_len() -> usize { - 32 - } - - fn G_len() -> usize { - 33 - } - - fn F_from_slice(slice: &[u8]) -> Result { - let bytes: [u8; 32] = slice.try_into() - .map_err(|_| CurveError::InvalidLength(32, slice.len()))?; - let scalar = Scalar::from_repr(bytes.into()); - if scalar.is_none().unwrap_u8() == 1 { - Err(CurveError::InvalidScalar)?; - } - Ok(scalar.unwrap()) - } - - fn G_from_slice(slice: &[u8]) -> Result { - let point = ProjectivePoint::from_bytes(GenericArray::from_slice(slice)); - if point.is_none().unwrap_u8() == 1 { - Err(CurveError::InvalidScalar)?; - } - Ok(point.unwrap()) - } - - fn F_to_bytes(f: &Self::F) -> Vec { - (&f.to_bytes()).to_vec() - } - - fn G_to_bytes(g: &Self::G) -> Vec { - (&g.to_bytes()).to_vec() - } -} - -#[allow(non_snake_case)] -#[derive(Clone)] -pub struct TestHram {} -impl Hram for TestHram { - #[allow(non_snake_case)] - fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { - Scalar::from_uint_reduced( - U512::from_be_byte_array( - Sha512::new() - .chain_update(Secp256k1::G_to_bytes(R)) - .chain_update(Secp256k1::G_to_bytes(A)) - .chain_update(m) - .finalize() - ) - ) - } -} - -#[test] -fn secp256k1_curve() { - test_curve::<_, Secp256k1>(&mut OsRng); -} diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs index f87ce812..cc9c8aad 100644 --- a/crypto/frost/src/tests/mod.rs +++ b/crypto/frost/src/tests/mod.rs @@ -18,6 +18,7 @@ mod schnorr; // Test suites for public usage pub mod curve; +pub mod vectors; // Literal test definitions to run during `cargo test` #[cfg(test)] diff --git a/crypto/frost/src/tests/vectors.rs b/crypto/frost/src/tests/vectors.rs new file mode 100644 index 00000000..5c984aa3 --- /dev/null +++ b/crypto/frost/src/tests/vectors.rs @@ -0,0 +1,117 @@ +use std::{rc::Rc, collections::HashMap}; + +use crate::{ + Curve, MultisigKeys, + algorithm::{Schnorr, Hram}, + sign::{PreprocessPackage, StateMachine, AlgorithmMachine}, + tests::recover +}; + +pub struct Vectors { + pub threshold: u16, + pub shares: &'static [&'static str], + pub group_secret: &'static str, + pub group_key: &'static str, + + pub msg: &'static str, + pub included: &'static [u16], + pub nonces: &'static [[&'static str; 2]], + pub binding: &'static str, + pub sig_shares: &'static [&'static str], + pub sig: String +} + +// Load these vectors into MultisigKeys using a custom serialization it'll deserialize +fn vectors_to_multisig_keys(vectors: &Vectors) -> HashMap> { + let shares = vectors.shares.iter().map( + |secret| C::F_from_slice(&hex::decode(secret).unwrap()).unwrap() + ).collect::>(); + let verification_shares = shares.iter().map( + |secret| C::generator() * secret + ).collect::>(); + + let mut keys = HashMap::new(); + for i in 1 ..= u16::try_from(shares.len()).unwrap() { + let mut serialized = vec![]; + serialized.push(C::id_len()); + serialized.extend(C::id()); + serialized.extend(vectors.threshold.to_be_bytes()); + serialized.extend(u16::try_from(shares.len()).unwrap().to_be_bytes()); + serialized.extend(i.to_be_bytes()); + serialized.extend(C::F_to_bytes(&shares[usize::from(i) - 1])); + serialized.extend(&hex::decode(vectors.group_key).unwrap()); + for share in &verification_shares { + serialized.extend(&C::G_to_bytes(share)); + } + + let these_keys = MultisigKeys::::deserialize(&serialized).unwrap(); + assert_eq!(these_keys.params().t(), vectors.threshold); + assert_eq!(usize::from(these_keys.params().n()), shares.len()); + assert_eq!(these_keys.params().i(), i); + assert_eq!(these_keys.secret_share(), shares[usize::from(i - 1)]); + assert_eq!(&hex::encode(&C::G_to_bytes(&these_keys.group_key())), vectors.group_key); + keys.insert(i, these_keys); + } + + keys +} + +pub fn vectors>(vectors: Vectors) { + let keys = vectors_to_multisig_keys::(&vectors); + let group_key = C::G_from_slice(&hex::decode(vectors.group_key).unwrap()).unwrap(); + assert_eq!( + C::generator() * C::F_from_slice(&hex::decode(vectors.group_secret).unwrap()).unwrap(), + group_key + ); + assert_eq!( + recover(&keys), + C::F_from_slice(&hex::decode(vectors.group_secret).unwrap()).unwrap() + ); + + let mut machines = vec![]; + for i in vectors.included { + machines.push(( + *i, + AlgorithmMachine::new( + Schnorr::::new(), + Rc::new(keys[i].clone()), + vectors.included.clone() + ).unwrap() + )); + } + + let mut commitments = HashMap::new(); + let mut c = 0; + for (i, machine) in machines.iter_mut() { + let nonces = [ + C::F_from_slice(&hex::decode(vectors.nonces[c][0]).unwrap()).unwrap(), + C::F_from_slice(&hex::decode(vectors.nonces[c][1]).unwrap()).unwrap() + ]; + + let mut serialized = C::G_to_bytes(&(C::generator() * nonces[0])); + serialized.extend(&C::G_to_bytes(&(C::generator() * nonces[1]))); + + machine.unsafe_override_preprocess( + PreprocessPackage { nonces, serialized: serialized.clone() } + ); + + commitments.insert(*i, serialized); + c += 1; + } + + let mut shares = HashMap::new(); + c = 0; + for (i, machine) in machines.iter_mut() { + let share = machine.sign(commitments.clone(), &hex::decode(vectors.msg).unwrap()).unwrap(); + assert_eq!(share, hex::decode(vectors.sig_shares[c]).unwrap()); + shares.insert(*i, share); + c += 1; + } + + for (_, machine) in machines.iter_mut() { + let sig = machine.complete(shares.clone()).unwrap(); + let mut serialized = C::G_to_bytes(&sig.R); + serialized.extend(C::F_to_bytes(&sig.s)); + assert_eq!(hex::encode(serialized), vectors.sig); + } +}