From c3cc8d51b7db200224fabd422d8af9ce4e56f499 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 12 Jul 2022 01:56:08 -0400 Subject: [PATCH] Update the Chaum Pedersen proof to verify the new multi-nonce FROST Provides further health and reference to https://github.com/serai-dex/serai/issues/14. --- coins/firo/Cargo.toml | 10 +- coins/firo/src/spark/chaum/multisig.rs | 128 ++++++------------------- coins/firo/src/spark/frost.rs | 100 ------------------- coins/firo/src/spark/mod.rs | 7 +- coins/firo/src/tests/mod.rs | 8 +- 5 files changed, 41 insertions(+), 212 deletions(-) delete mode 100644 coins/firo/src/spark/frost.rs diff --git a/coins/firo/Cargo.toml b/coins/firo/Cargo.toml index 4b881cac..b4a0ecad 100644 --- a/coins/firo/Cargo.toml +++ b/coins/firo/Cargo.toml @@ -15,13 +15,13 @@ rand_chacha = { version = "0.3", optional = true } sha2 = "0.10" -ff = "0.11" -group = "0.11" -k256 = { version = "0.10", features = ["arithmetic"] } +ff = "0.12" +group = "0.12" +k256 = { version = "0.11", features = ["arithmetic"] } blake2 = { version = "0.10", optional = true } -transcript = { path = "../../crypto/transcript", optional = true } -frost = { path = "../../crypto/frost", optional = true } +transcript = { path = "../../crypto/transcript", package = "flexible-transcript", features = ["recommended"], optional = true } +frost = { path = "../../crypto/frost", package = "modular-frost", features = ["secp256k1"], optional = true } [dev-dependencies] rand = "0.8" diff --git a/coins/firo/src/spark/chaum/multisig.rs b/coins/firo/src/spark/chaum/multisig.rs index dc46749c..a9ef2880 100644 --- a/coins/firo/src/spark/chaum/multisig.rs +++ b/coins/firo/src/spark/chaum/multisig.rs @@ -1,44 +1,26 @@ -use std::collections::HashMap; - use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha12Rng; use ff::Field; -use group::GroupEncoding; use k256::{Scalar, ProjectivePoint}; -use transcript::Transcript as _; -use frost::{CurveError, Curve, FrostError, MultisigView, algorithm::Algorithm}; +use transcript::{Transcript, RecommendedTranscript}; +use frost::{curve::Secp256k1, FrostError, FrostView, algorithm::Algorithm}; -use crate::spark::{ - G, GENERATORS_TRANSCRIPT, - frost::{Transcript, Secp256k1}, - chaum::{ChaumWitness, ChaumProof} -}; +use crate::spark::{G, GENERATORS_TRANSCRIPT, chaum::{ChaumWitness, ChaumProof}}; #[derive(Clone)] pub struct ChaumMultisig { - transcript: Transcript, + transcript: RecommendedTranscript, len: usize, witness: ChaumWitness, - // The following is ugly as hell as it's re-implementing the nonce code FROST is meant to handle - // Using FROST's provided SchnorrSignature algorithm multiple times would work, handling nonces - // for us, except you need the commitments for the challenge which means you need the binding - // factors, which means then you're re-calculating those, and... - // The best solution would be for FROST itself to support multi-nonce protocols, if there is - // sufficient reason for it to - additional_nonces: Vec<(Scalar, Scalar)>, - nonces: HashMap>, - sum: Vec<(ProjectivePoint, ProjectivePoint)>, - challenge: Scalar, - binding: Scalar, proof: Option } impl ChaumMultisig { - pub fn new(mut transcript: Transcript, witness: ChaumWitness) -> ChaumMultisig { + pub fn new(mut transcript: RecommendedTranscript, witness: ChaumWitness) -> ChaumMultisig { transcript.domain_separate(b"Chaum"); transcript.append_message(b"generators", &*GENERATORS_TRANSCRIPT); transcript.append_message(b"statement", &witness.statement.transcript()); @@ -53,11 +35,6 @@ impl ChaumMultisig { len, witness, - additional_nonces: Vec::with_capacity(len - 1), - nonces: HashMap::new(), - sum: vec![(ProjectivePoint::IDENTITY, ProjectivePoint::IDENTITY); len - 1], - - binding: Scalar::zero(), challenge: Scalar::zero(), proof: None } @@ -65,100 +42,57 @@ impl ChaumMultisig { } impl Algorithm for ChaumMultisig { - type Transcript = Transcript; + type Transcript = RecommendedTranscript; type Signature = ChaumProof; fn transcript(&mut self) -> &mut Self::Transcript { &mut self.transcript } + fn nonces(&self) -> Vec> { + vec![vec![*G]; self.len] + } + fn preprocess_addendum( &mut self, - rng: &mut R, - _: &MultisigView, - _: &[Scalar; 2], + _: &mut R, + _: &FrostView ) -> Vec { - // While FROST will provide D_0 and E_0, we need D_i and E_i - let mut res = Vec::with_capacity((self.len - 1) * 33); - for _ in 1 .. self.len { - let d = Scalar::random(&mut *rng); - let e = Scalar::random(&mut *rng); - res.extend(&(*G * d).to_bytes()); - res.extend(&(*G * e).to_bytes()); - self.additional_nonces.push((d, e)); - } - res + vec![] } fn process_addendum( &mut self, - _: &MultisigView, - l: u16, - _: &[ProjectivePoint; 2], - addendum: &[u8], + _: &FrostView, + _: u16, + _: &[u8] ) -> Result<(), FrostError> { - let mut nonces = Vec::with_capacity(self.len - 1); - for i in 0 .. (self.len - 1) { - let p = i * 2; - let (D, E) = (|| Ok(( - Secp256k1::G_from_slice(&addendum[(p * 33) .. ((p + 1) * 33)])?, - Secp256k1::G_from_slice(&addendum[((p + 1) * 33) .. ((p + 2) * 33)])? - )))().map_err(|_: CurveError| FrostError::InvalidCommitment(l))?; - self.transcript.append_message(b"participant", &l.to_be_bytes()); - self.transcript.append_message(b"commitment_D_additional", &D.to_bytes()); - self.transcript.append_message(b"commitment_E_additional", &E.to_bytes()); - self.sum[i].0 += D; - self.sum[i].1 += E; - nonces.push((D, E)); - } - self.nonces.insert(l, nonces); Ok(()) } fn sign_share( &mut self, - view: &MultisigView, - sum_0: ProjectivePoint, - binding: Scalar, - nonce_0: Scalar, - _: &[u8], + view: &FrostView, + nonce_sums: &[Vec], + nonces: &[Scalar], + _: &[u8] ) -> Scalar { - self.binding = binding; - let (rs, t3, mut commitments) = ChaumProof::r_t_commitments( &mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"r_t")), &self.witness ); - let mut sum = ProjectivePoint::IDENTITY; for i in 0 .. self.len { - let nonce = if i == 0 { - sum_0 - } else { - self.sum[i - 1].0 + (self.sum[i - 1].1 * binding) - }; - commitments.A2[i] += nonce; - sum += nonce; - } - commitments.A1 += sum; - - let mut nonces = Vec::with_capacity(self.len); - for i in 0 .. self.len { - nonces.push( - if i == 0 { - nonce_0 - } else { - self.additional_nonces[i - 1].0 + (self.additional_nonces[i - 1].1 * binding) - } - ); + commitments.A2[i] += nonce_sums[i][0]; } + commitments.A1 += nonce_sums.iter().map(|sum| sum[0]).sum::(); let (challenge, proof) = ChaumProof::t_prove( &self.witness, &rs, t3, commitments, - &nonces, + nonces, &view.secret_share() ); self.challenge = challenge; @@ -170,7 +104,7 @@ impl Algorithm for ChaumMultisig { fn verify( &self, _: ProjectivePoint, - _: ProjectivePoint, + _: &[Vec], sum: Scalar ) -> Option { let mut proof = self.proof.clone().unwrap(); @@ -180,23 +114,17 @@ impl Algorithm for ChaumMultisig { fn verify_share( &self, - l: u16, + _: u16, verification_share: ProjectivePoint, - nonce: ProjectivePoint, - share: Scalar, + nonces: &[Vec], + share: Scalar ) -> bool { let mut t2 = ProjectivePoint::IDENTITY; let mut accum = self.challenge; for i in 0 .. self.len { - let nonce = if i == 0 { - nonce - } else { - self.nonces[&l][i - 1].0 + (self.nonces[&l][i - 1].1 * self.binding) - }; - t2 += nonce + (verification_share * accum); + t2 += nonces[i][0] + (verification_share * accum); accum *= self.challenge; } - (*G * share) == t2 } } diff --git a/coins/firo/src/spark/frost.rs b/coins/firo/src/spark/frost.rs deleted file mode 100644 index be5a754d..00000000 --- a/coins/firo/src/spark/frost.rs +++ /dev/null @@ -1,100 +0,0 @@ -use core::convert::TryInto; - -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 transcript::DigestTranscript; -use frost::{CurveError, Curve}; - -use crate::spark::G; - -const CONTEXT: &[u8] = b"FROST-K256-SHA"; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) 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 { - *G - } - - fn generator_table() -> Self::T { - *G - } - - 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"). With lack of hash_to_field, wide - // reduction is used - fn hash_msg(msg: &[u8]) -> Vec { - (&Sha256::digest(&[CONTEXT, b"digest", msg].concat())).to_vec() - } - - fn hash_binding_factor(binding: &[u8]) -> Self::F { - Self::hash_to_F(&[CONTEXT, b"rho", binding].concat()) - } - - 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() - } -} - -pub type Transcript = DigestTranscript::; diff --git a/coins/firo/src/spark/mod.rs b/coins/firo/src/spark/mod.rs index 106aeff9..3dd1759f 100644 --- a/coins/firo/src/spark/mod.rs +++ b/coins/firo/src/spark/mod.rs @@ -7,11 +7,12 @@ use k256::{ProjectivePoint, CompressedPoint}; pub mod chaum; -#[cfg(feature = "frost")] -pub(crate) mod frost; - // Extremely basic hash to curve, which should not be used, yet which offers the needed generators fn generator(letter: u8) -> ProjectivePoint { + if letter == b'G' { + return ProjectivePoint::GENERATOR; + } + let mut point = [2; 33]; let mut g = b"Generator ".to_vec(); diff --git a/coins/firo/src/tests/mod.rs b/coins/firo/src/tests/mod.rs index ca4390f4..dd2d2495 100644 --- a/coins/firo/src/tests/mod.rs +++ b/coins/firo/src/tests/mod.rs @@ -4,11 +4,11 @@ use ff::Field; use k256::Scalar; #[cfg(feature = "multisig")] -use frost::tests::{key_gen, algorithm_machines, sign}; +use transcript::{Transcript, RecommendedTranscript}; +#[cfg(feature = "multisig")] +use frost::{curve::Secp256k1, tests::{key_gen, algorithm_machines, sign}}; use crate::spark::{F, G, H, U, chaum::*}; -#[cfg(feature = "multisig")] -use crate::spark::frost::{Transcript, Secp256k1}; #[test] fn chaum() { @@ -63,7 +63,7 @@ fn chaum_multisig() { &mut OsRng, algorithm_machines( &mut OsRng, - ChaumMultisig::new(Transcript::new(b"Firo Serai Chaum Test".to_vec()), witness), + ChaumMultisig::new(RecommendedTranscript::new(b"Firo Serai Chaum Test"), witness), &keys ), &[]