diff --git a/Cargo.lock b/Cargo.lock index 2ea83a04..ff81dd4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,6 +429,7 @@ dependencies = [ "ciphersuite", "flexible-transcript", "hex", + "modular-frost", "multiexp", "rand_core", "std-shims", diff --git a/crypto/generalized-schnorr/Cargo.toml b/crypto/generalized-schnorr/Cargo.toml index 66022e78..e5931dcb 100644 --- a/crypto/generalized-schnorr/Cargo.toml +++ b/crypto/generalized-schnorr/Cargo.toml @@ -28,6 +28,8 @@ transcript = { package = "flexible-transcript", path = "../transcript", version ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false, features = ["alloc"] } multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["batch"] } +frost = { package = "modular-frost", path = "../frost", version = "0.8", default-features = false, optional = true } + [dev-dependencies] hex = "0.4" @@ -37,6 +39,9 @@ transcript = { package = "flexible-transcript", path = "../transcript", features ciphersuite = { path = "../ciphersuite", features = ["ed25519"] } +frost = { package = "modular-frost", path = "../frost", features = ["ed25519", "tests"] } + [features] std = ["std-shims/std", "rand_core/std", "zeroize/std", "transcript/std", "ciphersuite/std", "multiexp/std"] +mpc = ["std", "frost"] default = ["std"] diff --git a/crypto/generalized-schnorr/src/lib.rs b/crypto/generalized-schnorr/src/lib.rs index 690e0b14..3a0b082f 100644 --- a/crypto/generalized-schnorr/src/lib.rs +++ b/crypto/generalized-schnorr/src/lib.rs @@ -24,6 +24,9 @@ use ciphersuite::{ }; use multiexp::{multiexp_vartime, BatchVerifier}; +#[cfg(feature = "mpc")] +mod mpc; + #[cfg(test)] mod tests; @@ -123,7 +126,7 @@ impl([C::G; OUTPUTS]); +impl WriteAddendum for OutputShare { + fn write(&self, writer: &mut W) -> io::Result<()> { + for point in &self.0 { + writer.write_all(point.to_bytes().as_ref())?; + } + Ok(()) + } +} + +/// An algorithm to produce a GeneralizedSchnorr with modular-frost. +#[derive(Clone, PartialEq, Debug)] +pub struct GeneralizedSchnorrAlgorithm< + C: Curve, + T: Sync + Clone + PartialEq + Debug + Transcript, + const OUTPUTS: usize, + const SCALARS: usize, + const SCALARS_PLUS_TWO: usize, +> { + mpc_transcript: T, + proof_transcript: T, + matrix: [[C::G; SCALARS]; OUTPUTS], + scalars: [Option>; SCALARS], + output_shares: HashMap, [C::G; OUTPUTS]>, + outputs: Option<[C::G; OUTPUTS]>, + R: Option<[C::G; OUTPUTS]>, + c: Option, + s: Option<[C::F; SCALARS]>, +} +impl< + C: Curve, + T: Sync + Clone + PartialEq + Debug + Transcript, + const OUTPUTS: usize, + const SCALARS: usize, + const SCALARS_PLUS_TWO: usize, + > Algorithm for GeneralizedSchnorrAlgorithm +{ + type Transcript = T; + type Addendum = OutputShare; + type Signature = ([C::G; OUTPUTS], T, GeneralizedSchnorr); + + fn transcript(&mut self) -> &mut Self::Transcript { + &mut self.mpc_transcript + } + + fn nonces(&self) -> Vec> { + let i = self + .scalars + .iter() + .position(Option::is_none) + .expect("constructed algorithm doesn't have a multi-party scalar"); + // One nonce, with representations for each generator in this column + vec![self.matrix.iter().map(|row| row[i]).collect()] + } + + fn preprocess_addendum( + &mut self, + _: &mut R, + keys: &ThresholdKeys, + ) -> Self::Addendum { + let j = self + .scalars + .iter() + .position(Option::is_none) + .expect("constructed algorithm doesn't have a multi-party scalar"); + OutputShare(core::array::from_fn(|i| self.matrix[i][j] * keys.secret_share().deref())) + } + + fn read_addendum(&self, reader: &mut R) -> io::Result { + let mut outputs = [C::G::identity(); OUTPUTS]; + for output in &mut outputs { + *output = ::read_G(reader)?; + } + Ok(OutputShare(outputs)) + } + + fn process_addendum( + &mut self, + view: &ThresholdView, + i: Participant, + addendum: Self::Addendum, + ) -> Result<(), FrostError> { + if self.outputs.is_none() { + self.mpc_transcript.domain_separate(b"generalized_schnorr-mpc-addendum"); + self.outputs = Some(core::array::from_fn(|i| { + let mut sum = C::G::identity(); + for j in 0 .. SCALARS { + sum += self.matrix[i][j] * + self.scalars[j].as_ref().unwrap_or(&Zeroizing::new(C::F::ZERO)).deref(); + } + sum + })); + } + + self.mpc_transcript.append_message(b"participant", i.to_bytes()); + let outputs = self.outputs.as_mut().unwrap(); + for (output, share) in outputs.iter_mut().zip(addendum.0.iter()) { + self.mpc_transcript.append_message(b"output_share", share.to_bytes()); + *output += *share * lagrange::(i, view.included()); + } + self.output_shares.insert(view.verification_share(i).to_bytes().as_ref().to_vec(), addendum.0); + Ok(()) + } + + fn sign_share( + &mut self, + params: &ThresholdView, + nonce_sums: &[Vec], + nonces: Vec>, + msg: &[u8], + ) -> C::F { + assert!(msg.is_empty(), "msg wasn't empty when the 'msg' is passed on construction"); + + let mut R: [C::G; OUTPUTS] = nonce_sums[0] + .clone() + .try_into() + .expect("didn't generate as many representations of the nonce as outputs"); + + // Deterministically derive nonces for the rest of the scalars + let mut deterministic_nonces: [C::F; SCALARS] = core::array::from_fn(|_| { + ::hash_to_F( + b"generalized_schnorr-mpc-nonce", + self.mpc_transcript.challenge(b"nonce").as_ref(), + ) + }); + + let mpc_scalar_index = self + .scalars + .iter() + .position(Option::is_none) + .expect("constructed algorithm doesn't have a multi-party scalar"); + // Don't add any nonce for what we actually generated a nonce for + deterministic_nonces[mpc_scalar_index] = C::F::ZERO; + + for (i, R) in R.iter_mut().enumerate() { + for (j, nonce) in deterministic_nonces.iter().enumerate() { + *R += self.matrix[i][j] * nonce; + } + } + self.R = Some(R); + + let c = GeneralizedSchnorr::::challenge( + &mut self.proof_transcript.clone(), + self.matrix, + self.outputs.expect("didn't process any addendums"), + R, + ); + self.c = Some(c); + self.mpc_transcript.append_message(b"c", c.to_repr()); + + let mut s = [C::F::ZERO; SCALARS]; + for ((s, nonce), scalar) in + s.iter_mut().zip(deterministic_nonces.iter()).zip(self.scalars.iter()) + { + if let Some(scalar) = scalar { + *s = (c * scalar.deref()) + nonce; + } else { + assert_eq!(*nonce, C::F::ZERO); + } + } + deterministic_nonces.zeroize(); + self.s = Some(s); + + (c * params.secret_share().deref()) + nonces[0].deref() + } + + #[must_use] + fn verify(&self, _group_key: C::G, _nonces: &[Vec], sum: C::F) -> Option { + // We drop the nonces argument as we've already incorporated them into R + let R = self.R.unwrap(); + let mpc_scalar_index = self + .scalars + .iter() + .position(Option::is_none) + .expect("constructed algorithm doesn't have a multi-party scalar"); + let mut s = self.s.unwrap(); + s[mpc_scalar_index] = sum; + + let outputs = self.outputs.unwrap(); + let sig = GeneralizedSchnorr { R, s }; + let mut transcript = self.proof_transcript.clone(); + if sig.verify(&mut transcript, self.matrix, outputs) { + return Some((outputs, transcript, sig)); + } + None + } + + fn verify_share( + &self, + verification_share: C::G, + nonces: &[Vec], + share: C::F, + ) -> Result, ()> { + let mpc_scalar_index = self + .scalars + .iter() + .position(Option::is_none) + .expect("constructed algorithm doesn't have a multi-party scalar"); + + let c = self.c.unwrap(); + let outputs = self.output_shares[&verification_share.to_bytes().as_ref().to_vec()]; + + // Since we need to append multiple statements, we need a random weight + // Use the MPC transcript, which should be binding to all context and valid as a deterministic + // weight + // The one item it hasn't bound is the scalar share, so bind that + let mut transcript = self.mpc_transcript.clone(); + transcript.append_message(b"share", share.to_repr()); + + let mut statements = vec![]; + for ((row, nonce), output) in self.matrix.iter().zip(nonces[0].iter()).zip(outputs.iter()) { + let weight = ::hash_to_F( + b"generalized_schnorr-mpc-verify_share", + transcript.challenge(b"verify_share").as_ref(), + ); + statements.extend(&[ + (weight, *nonce), + (weight * c, *output), + (weight * -share, row[mpc_scalar_index]), + ]); + } + Ok(statements) + } +} + +impl + GeneralizedSchnorr +{ + /// Prove a Generalized Schnorr statement via MPC. + /// + /// Creates a machine which returns the outputs, the proof, and the mutated proof transcript. + /// + /// Only one scalar in the witness is allowed to be shared among the RPC participants. The rest + /// must be known to all parties. The nonces blinding these known-by-all scalars will be + /// deterministically derived from the transcript. While this enables recovery of those values by + /// anyone with the transcript, that's presumed to solely be the participants (who already have + /// knowledge of the scalars). Please encrypt the transcript at the communication layer if you + /// need it to be private. + /// + /// Returns None if there isn't exactly one scalar in the witness is missing. + pub fn multiparty_prove( + mpc_transcript: T, + proof_transcript: T, + matrix: [[C::G; SCALARS]; OUTPUTS], + scalars: [Option>; SCALARS], + ) -> Option> { + if scalars.iter().filter(|scalar| scalar.is_none()).count() != 1 { + None?; + } + + Some(GeneralizedSchnorrAlgorithm { + mpc_transcript, + proof_transcript, + matrix, + scalars, + output_shares: HashMap::new(), + outputs: None, + R: None, + c: None, + s: None, + }) + } +} diff --git a/crypto/generalized-schnorr/src/tests.rs b/crypto/generalized-schnorr/src/tests/mod.rs similarity index 97% rename from crypto/generalized-schnorr/src/tests.rs rename to crypto/generalized-schnorr/src/tests/mod.rs index a9b70b64..4d91a134 100644 --- a/crypto/generalized-schnorr/src/tests.rs +++ b/crypto/generalized-schnorr/src/tests/mod.rs @@ -10,6 +10,9 @@ use ciphersuite::{ use crate::GeneralizedSchnorr; +#[cfg(feature = "mpc")] +mod mpc; + #[test] fn test() { const OUTPUTS: usize = 3; diff --git a/crypto/generalized-schnorr/src/tests/mpc.rs b/crypto/generalized-schnorr/src/tests/mpc.rs new file mode 100644 index 00000000..83e1539c --- /dev/null +++ b/crypto/generalized-schnorr/src/tests/mpc.rs @@ -0,0 +1,64 @@ +use zeroize::Zeroizing; +use rand_core::OsRng; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{ + group::{ff::Field, Group}, + Ciphersuite, +}; + +use frost::{ + curve::Ed25519, + tests::{key_gen, algorithm_machines, sign}, +}; + +use crate::GeneralizedSchnorr; + +#[test] +fn mpc_test() { + const OUTPUTS: usize = 3; + const SCALARS: usize = 2; + const SCALARS_PLUS_TWO: usize = SCALARS + 2; + + let matrix = [ + [ + ::G::random(&mut OsRng), + ::G::random(&mut OsRng), + ], + [ + ::G::random(&mut OsRng), + ::G::random(&mut OsRng), + ], + [ + ::G::random(&mut OsRng), + ::G::random(&mut OsRng), + ], + ]; + + let keys = key_gen::<_, Ed25519>(&mut OsRng); + let other_scalar = Zeroizing::new(::F::random(&mut OsRng)); + + let algorithm = + GeneralizedSchnorr::::multiparty_prove( + RecommendedTranscript::new(b"Generalized Schnorr MPC Test"), + RecommendedTranscript::new(b"Generalized Schnorr MPC Proof Test"), + matrix, + [None, Some(other_scalar)], + ) + .unwrap(); + + let (outputs, _, proof) = sign( + &mut OsRng, + &algorithm, + keys.clone(), + algorithm_machines(&mut OsRng, &algorithm, &keys), + &[], + ); + + assert!(proof.verify( + &mut RecommendedTranscript::new(b"Generalized Schnorr MPC Proof Test"), + matrix, + outputs + )); +} diff --git a/crypto/transcript/src/lib.rs b/crypto/transcript/src/lib.rs index 3956f51d..4fb42f01 100644 --- a/crypto/transcript/src/lib.rs +++ b/crypto/transcript/src/lib.rs @@ -91,6 +91,11 @@ where /// A simple transcript format constructed around the specified hash algorithm. #[derive(Clone, Debug)] pub struct DigestTranscript(D); +impl PartialEq for DigestTranscript { + fn eq(&self, other: &Self) -> bool { + self.clone().challenge(b"") == other.clone().challenge(b"") + } +} impl DigestTranscript { fn append(&mut self, kind: DigestTranscriptMember, value: &[u8]) {