Skip to content

Commit

Permalink
Add Generalized Schnorr MPC
Browse files Browse the repository at this point in the history
  • Loading branch information
kayabaNerve committed Apr 27, 2024
1 parent d1b614a commit 027f204
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crypto/generalized-schnorr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"]
5 changes: 4 additions & 1 deletion crypto/generalized-schnorr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ use ciphersuite::{
};
use multiexp::{multiexp_vartime, BatchVerifier};

#[cfg(feature = "mpc")]
mod mpc;

#[cfg(test)]
mod tests;

Expand Down Expand Up @@ -123,7 +126,7 @@ impl<C: Ciphersuite, const OUTPUTS: usize, const SCALARS: usize, const SCALARS_P
});

let c = Self::challenge(transcript, matrix, outputs, R);
(outputs, Self { R, s: core::array::from_fn(|i| c * scalars[i].deref() + nonces[i].deref()) })
(outputs, Self { R, s: core::array::from_fn(|i| (c * scalars[i].deref()) + nonces[i].deref()) })
}

/// Return the series of pairs whose products sum to zero for a valid signature.
Expand Down
291 changes: 291 additions & 0 deletions crypto/generalized-schnorr/src/mpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
use core::{ops::Deref, fmt::Debug};
use std::{io, collections::HashMap};

use rand_core::{RngCore, CryptoRng};

use zeroize::{Zeroize, Zeroizing};

use transcript::Transcript;

use ciphersuite::{
group::{
ff::{Field, PrimeField},
Group, GroupEncoding,
},
Ciphersuite,
};

use frost::{
dkg::lagrange,
curve::Curve,
FrostError, Participant, ThresholdKeys, ThresholdView,
algorithm::{WriteAddendum, Algorithm},
};

use crate::GeneralizedSchnorr;

// The column of generators for the multi-party shared scalar scaled by the local share.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct OutputShare<C: Ciphersuite, const OUTPUTS: usize>([C::G; OUTPUTS]);
impl<C: Ciphersuite, const OUTPUTS: usize> WriteAddendum for OutputShare<C, OUTPUTS> {
fn write<W: io::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<Zeroizing<C::F>>; SCALARS],
output_shares: HashMap<Vec<u8>, [C::G; OUTPUTS]>,
outputs: Option<[C::G; OUTPUTS]>,
R: Option<[C::G; OUTPUTS]>,
c: Option<C::F>,
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<C> for GeneralizedSchnorrAlgorithm<C, T, OUTPUTS, SCALARS, SCALARS_PLUS_TWO>
{
type Transcript = T;
type Addendum = OutputShare<C, OUTPUTS>;
type Signature = ([C::G; OUTPUTS], T, GeneralizedSchnorr<C, OUTPUTS, SCALARS, SCALARS_PLUS_TWO>);

fn transcript(&mut self) -> &mut Self::Transcript {
&mut self.mpc_transcript
}

fn nonces(&self) -> Vec<Vec<C::G>> {
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<R: RngCore + CryptoRng>(
&mut self,
_: &mut R,
keys: &ThresholdKeys<C>,
) -> 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<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
let mut outputs = [C::G::identity(); OUTPUTS];
for output in &mut outputs {
*output = <C as Curve>::read_G(reader)?;
}
Ok(OutputShare(outputs))
}

fn process_addendum(
&mut self,
view: &ThresholdView<C>,
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::<C::F>(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<C>,
nonce_sums: &[Vec<C::G>],
nonces: Vec<Zeroizing<C::F>>,
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(|_| {
<C as Ciphersuite>::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::<C, OUTPUTS, SCALARS, SCALARS_PLUS_TWO>::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<C::G>], sum: C::F) -> Option<Self::Signature> {
// 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<C::G>],
share: C::F,
) -> Result<Vec<(C::F, C::G)>, ()> {
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 = <C as Ciphersuite>::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<C: Curve, const OUTPUTS: usize, const SCALARS: usize, const SCALARS_PLUS_TWO: usize>
GeneralizedSchnorr<C, OUTPUTS, SCALARS, SCALARS_PLUS_TWO>
{
/// 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<T: Sync + Clone + PartialEq + Debug + Transcript>(
mpc_transcript: T,
proof_transcript: T,
matrix: [[C::G; SCALARS]; OUTPUTS],
scalars: [Option<Zeroizing<C::F>>; SCALARS],
) -> Option<GeneralizedSchnorrAlgorithm<C, T, OUTPUTS, SCALARS, SCALARS_PLUS_TWO>> {
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,
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use ciphersuite::{

use crate::GeneralizedSchnorr;

#[cfg(feature = "mpc")]
mod mpc;

#[test]
fn test() {
const OUTPUTS: usize = 3;
Expand Down
Loading

0 comments on commit 027f204

Please sign in to comment.