diff --git a/.github/workflows/crypto-tests.yml b/.github/workflows/crypto-tests.yml index 6007b5dd..8a01c004 100644 --- a/.github/workflows/crypto-tests.yml +++ b/.github/workflows/crypto-tests.yml @@ -34,4 +34,4 @@ jobs: -p dleq \ -p dkg \ -p modular-frost \ - -p frost-schnorrkel + -p generalized-schnorr diff --git a/Cargo.lock b/Cargo.lock index 22d7ce4e..2ea83a04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -417,25 +417,24 @@ dependencies = [ ] [[package]] -name = "frost-schnorrkel" -version = "0.1.2" +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generalized-schnorr" +version = "0.1.0" dependencies = [ "ciphersuite", "flexible-transcript", - "group", - "modular-frost", + "hex", + "multiexp", "rand_core", - "schnorr-signatures", - "schnorrkel", + "std-shims", "zeroize", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "generic-array" version = "0.14.7" @@ -763,8 +762,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", - "rand_chacha", "rand_core", ] @@ -823,24 +820,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "schnorrkel" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da18ffd9f2f5d01bc0b3050b37ce7728665f926b4dd1157fe3221b05737d924f" -dependencies = [ - "arrayref", - "arrayvec", - "curve25519-dalek", - "merlin", - "rand", - "rand_core", - "serde_bytes", - "sha2", - "subtle", - "zeroize", -] - [[package]] name = "sec1" version = "0.7.3" @@ -884,15 +863,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bytes" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" version = "1.0.198" diff --git a/Cargo.toml b/Cargo.toml index d89d3720..78b451fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ members = [ "crypto/dleq", "crypto/dkg", "crypto/frost", - "crypto/schnorrkel", + + "crypto/generalized-schnorr", "tests/no-std", ] diff --git a/crypto/generalized-schnorr/Cargo.toml b/crypto/generalized-schnorr/Cargo.toml new file mode 100644 index 00000000..4d54b7e7 --- /dev/null +++ b/crypto/generalized-schnorr/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "generalized-schnorr" +version = "0.1.0" +description = "Generalized Schnorr Protocols" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorr" +authors = ["Luke Parker "] +keywords = ["schnorr", "ff", "group"] +edition = "2021" +rust-version = "1.74" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +std-shims = { path = "../../common/std-shims", version = "^0.1.1", default-features = false } + +rand_core = { version = "0.6", default-features = false } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false } + +ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false, features = ["alloc"] } +multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["batch"] } + +[dev-dependencies] +hex = "0.4" + +rand_core = { version = "0.6", features = ["std"] } + +transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"] } + +ciphersuite = { path = "../ciphersuite", features = ["ed25519"] } + +[features] +std = ["std-shims/std", "rand_core/std", "zeroize/std", "ciphersuite/std", "multiexp/std"] +default = ["std"] diff --git a/crypto/schnorrkel/LICENSE b/crypto/generalized-schnorr/LICENSE similarity index 97% rename from crypto/schnorrkel/LICENSE rename to crypto/generalized-schnorr/LICENSE index e6bff13c..659881f1 100644 --- a/crypto/schnorrkel/LICENSE +++ b/crypto/generalized-schnorr/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Luke Parker +Copyright (c) 2024 Luke Parker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/crypto/generalized-schnorr/README.md b/crypto/generalized-schnorr/README.md new file mode 100644 index 00000000..e2c57cdc --- /dev/null +++ b/crypto/generalized-schnorr/README.md @@ -0,0 +1,19 @@ +# Schnorr Signatures + +A challenge (and therefore HRAm) agnostic Schnorr signature library. This is +intended to be used as a primitive by a variety of crates relying on Schnorr +signatures, voiding the need to constantly define a Schnorr signature struct +with associated functions. + +This library provides signatures of the `R, s` form. Batch verification is +supported via the multiexp crate. Half-aggregation, as defined in +, is also supported. + +This library was +[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf), +culminating in commit +[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06). +Any subsequent changes have not undergone auditing. + +This library is usable under no_std, via alloc, when the default features are +disabled. diff --git a/crypto/generalized-schnorr/src/lib.rs b/crypto/generalized-schnorr/src/lib.rs new file mode 100644 index 00000000..690e0b14 --- /dev/null +++ b/crypto/generalized-schnorr/src/lib.rs @@ -0,0 +1,183 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] + +use core::ops::Deref; +#[cfg(not(feature = "std"))] +#[macro_use] +extern crate alloc; +use std_shims::io::{self, Read, Write}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::{Zeroize, Zeroizing}; + +use transcript::Transcript; + +use ciphersuite::{ + group::{ + ff::{Field, PrimeField}, + Group, GroupEncoding, + }, + Ciphersuite, +}; +use multiexp::{multiexp_vartime, BatchVerifier}; + +#[cfg(test)] +mod tests; + +/// A Generalized Schnorr Proof. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct GeneralizedSchnorr< + C: Ciphersuite, + const OUTPUTS: usize, + const SCALARS: usize, + const SCALARS_PLUS_TWO: usize, +> { + pub R: [C::G; OUTPUTS], + pub s: [C::F; SCALARS], +} + +impl + GeneralizedSchnorr +{ + /// Read a GeneralizedSchnorr from something implementing Read. + pub fn read(reader: &mut R) -> io::Result { + let mut R = [C::G::identity(); OUTPUTS]; + for R in &mut R { + *R = C::read_G(reader)?; + } + let mut s = [C::F::ZERO; SCALARS]; + for s in &mut s { + *s = C::read_F(reader)?; + } + Ok(Self { R, s }) + } + + /// Write a GeneralizedSchnorr to something implementing Read. + pub fn write(&self, writer: &mut W) -> io::Result<()> { + for R in self.R { + writer.write_all(R.to_bytes().as_ref())?; + } + for s in self.s { + writer.write_all(s.to_repr().as_ref())?; + } + Ok(()) + } + + fn challenge( + transcript: &mut impl Transcript, + matrix: [[C::G; SCALARS]; OUTPUTS], + outputs: [C::G; OUTPUTS], + nonces: [C::G; OUTPUTS], + ) -> C::F { + transcript.domain_separate(b"generalized_schnorr"); + transcript.append_message( + b"scalars", + u32::try_from(SCALARS).expect("passed 2**32 scalars").to_le_bytes(), + ); + transcript.append_message( + b"outputs", + u32::try_from(OUTPUTS).expect("passed 2**32 outputs").to_le_bytes(), + ); + for row in matrix { + for generator in row { + transcript.append_message(b"generator", generator.to_bytes()); + } + } + for output in outputs { + transcript.append_message(b"output", output.to_bytes()); + } + for nonce in nonces { + transcript.append_message(b"nonce", nonce.to_bytes()); + } + C::hash_to_F(b"generalized_schnorr", transcript.challenge(b"c").as_ref()) + } + + /// Serialize a GeneralizedSchnorr, returning a `Vec`. + #[cfg(feature = "std")] + pub fn serialize(&self) -> Vec { + let mut buf = vec![]; + self.write(&mut buf).unwrap(); + buf + } + + /// Prove a Generalized Schnorr statement. + /// + /// Returns the outputs and the proof for them. + pub fn prove( + rng: &mut (impl RngCore + CryptoRng), + transcript: &mut impl Transcript, + matrix: [[C::G; SCALARS]; OUTPUTS], + scalars: [&Zeroizing; SCALARS], + ) -> ([C::G; OUTPUTS], Self) { + let outputs: [C::G; OUTPUTS] = core::array::from_fn(|i| { + matrix[i].iter().zip(scalars.iter()).map(|(generator, scalar)| *generator * ***scalar).sum() + }); + + let nonces: [Zeroizing; SCALARS] = + core::array::from_fn(|_| Zeroizing::new(C::F::random(&mut *rng))); + let R = core::array::from_fn(|i| { + matrix[i].iter().zip(nonces.iter()).map(|(generator, nonce)| *generator * **nonce).sum() + }); + + let c = Self::challenge(transcript, matrix, outputs, R); + (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. + /// + /// This is intended to be used with a multiexp for efficient batch verification. + fn batch_statements( + &self, + transcript: &mut impl Transcript, + matrix: [[C::G; SCALARS]; OUTPUTS], + outputs: [C::G; OUTPUTS], + ) -> [[(C::F, C::G); SCALARS_PLUS_TWO]; OUTPUTS] { + assert_eq!(SCALARS_PLUS_TWO, SCALARS + 2); + let c = Self::challenge(transcript, matrix, outputs, self.R); + core::array::from_fn(|i| { + core::array::from_fn(|j| { + if j == SCALARS { + (-C::F::ONE, self.R[i]) + } else if j == (SCALARS + 1) { + (-c, outputs[i]) + } else { + (self.s[j], matrix[i][j]) + } + }) + }) + } + + /// Verify a Generalized Schnorr proof. + #[must_use] + pub fn verify( + &self, + transcript: &mut impl Transcript, + matrix: [[C::G; SCALARS]; OUTPUTS], + outputs: [C::G; OUTPUTS], + ) -> bool { + for statements in self.batch_statements(transcript, matrix, outputs) { + if !bool::from(multiexp_vartime(statements.as_slice()).is_identity()) { + return false; + } + } + true + } + + /// Queue a proof for batch verification. + pub fn batch_verify( + &self, + rng: &mut R, + transcript: &mut impl Transcript, + batch: &mut BatchVerifier, + id: I, + matrix: [[C::G; SCALARS]; OUTPUTS], + outputs: [C::G; OUTPUTS], + ) { + for statements in self.batch_statements(transcript, matrix, outputs) { + batch.queue(rng, id, statements); + } + } +} diff --git a/crypto/generalized-schnorr/src/tests.rs b/crypto/generalized-schnorr/src/tests.rs new file mode 100644 index 00000000..a9b70b64 --- /dev/null +++ b/crypto/generalized-schnorr/src/tests.rs @@ -0,0 +1,48 @@ +use zeroize::Zeroizing; +use rand_core::OsRng; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{ + group::{ff::Field, Group}, + Ciphersuite, Ed25519, +}; + +use crate::GeneralizedSchnorr; + +#[test] +fn 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 (outputs, proof) = GeneralizedSchnorr::::prove( + &mut OsRng, + &mut RecommendedTranscript::new(b"Generalized Schnorr Test"), + matrix, + [ + &Zeroizing::new(::F::random(&mut OsRng)), + &Zeroizing::new(::F::random(&mut OsRng)), + ], + ); + assert!(proof.verify( + &mut RecommendedTranscript::new(b"Generalized Schnorr Test"), + matrix, + outputs + )); +} diff --git a/crypto/schnorrkel/Cargo.toml b/crypto/schnorrkel/Cargo.toml deleted file mode 100644 index f5819070..00000000 --- a/crypto/schnorrkel/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "frost-schnorrkel" -version = "0.1.2" -description = "modular-frost Algorithm compatible with Schnorrkel" -license = "MIT" -repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorrkel" -authors = ["Luke Parker "] -keywords = ["frost", "multisig", "threshold", "schnorrkel"] -edition = "2021" -rust-version = "1.74" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] - -[lints] -workspace = true - -[dependencies] -rand_core = "0.6" -zeroize = "^1.5" - -transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", features = ["merlin"] } - -group = "0.13" - -ciphersuite = { path = "../ciphersuite", version = "^0.4.1", features = ["std", "ristretto"] } -schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1" } -frost = { path = "../frost", package = "modular-frost", version = "^0.8.1", features = ["ristretto"] } - -schnorrkel = { version = "0.11" } - -[dev-dependencies] -frost = { path = "../frost", package = "modular-frost", features = ["tests"] } diff --git a/crypto/schnorrkel/README.md b/crypto/schnorrkel/README.md deleted file mode 100644 index 4f889dac..00000000 --- a/crypto/schnorrkel/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# FROST Schnorrkel - -A Schnorrkel algorithm for [modular-frost](https://docs.rs/modular-frost). - -While the Schnorrkel algorithm has not been audited, the underlying FROST -implementation was -[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf), -culminating in commit -[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06). -Any subsequent changes have not undergone auditing. diff --git a/crypto/schnorrkel/src/lib.rs b/crypto/schnorrkel/src/lib.rs deleted file mode 100644 index bb46bc02..00000000 --- a/crypto/schnorrkel/src/lib.rs +++ /dev/null @@ -1,153 +0,0 @@ -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![doc = include_str!("../README.md")] - -use std::io::{self, Read}; - -use rand_core::{RngCore, CryptoRng}; - -use zeroize::Zeroizing; - -use transcript::{Transcript, MerlinTranscript}; - -use group::{ff::PrimeField, GroupEncoding}; -use ciphersuite::{Ciphersuite, Ristretto}; -use schnorr::SchnorrSignature; - -use ::frost::{ - Participant, ThresholdKeys, ThresholdView, FrostError, - algorithm::{Hram, Algorithm, Schnorr}, -}; - -/// The [modular-frost](https://docs.rs/modular-frost) library. -pub mod frost { - pub use ::frost::*; -} - -use schnorrkel::{PublicKey, Signature, context::SigningTranscript, signing_context}; - -type RistrettoPoint = ::G; -type Scalar = ::F; - -#[cfg(test)] -mod tests; - -#[derive(Clone)] -struct SchnorrkelHram; -impl Hram for SchnorrkelHram { - #[allow(non_snake_case)] - fn hram(R: &RistrettoPoint, A: &RistrettoPoint, m: &[u8]) -> Scalar { - let ctx_len = - usize::try_from(u32::from_le_bytes(m[0 .. 4].try_into().expect("malformed message"))) - .unwrap(); - - let mut t = signing_context(&m[4 .. (4 + ctx_len)]).bytes(&m[(4 + ctx_len) ..]); - t.proto_name(b"Schnorr-sig"); - let convert = - |point: &RistrettoPoint| PublicKey::from_bytes(&point.to_bytes()).unwrap().into_compressed(); - t.commit_point(b"sign:pk", &convert(A)); - t.commit_point(b"sign:R", &convert(R)); - Scalar::from_repr(t.challenge_scalar(b"sign:c").to_bytes()).unwrap() - } -} - -/// FROST Schnorrkel algorithm. -#[derive(Clone)] -pub struct Schnorrkel { - context: &'static [u8], - schnorr: Schnorr, - msg: Option>, -} - -impl Schnorrkel { - /// Create a new algorithm with the specified context. - /// - /// If the context is greater than or equal to 4 GB in size, this will panic. - pub fn new(context: &'static [u8]) -> Schnorrkel { - Schnorrkel { - context, - schnorr: Schnorr::new(MerlinTranscript::new(b"FROST Schnorrkel")), - msg: None, - } - } -} - -impl Algorithm for Schnorrkel { - type Transcript = MerlinTranscript; - type Addendum = (); - type Signature = Signature; - - fn transcript(&mut self) -> &mut Self::Transcript { - self.schnorr.transcript() - } - - fn nonces(&self) -> Vec::G>> { - self.schnorr.nonces() - } - - fn preprocess_addendum( - &mut self, - _: &mut R, - _: &ThresholdKeys, - ) { - } - - fn read_addendum(&self, _: &mut R) -> io::Result { - Ok(()) - } - - fn process_addendum( - &mut self, - _: &ThresholdView, - _: Participant, - (): (), - ) -> Result<(), FrostError> { - Ok(()) - } - - fn sign_share( - &mut self, - params: &ThresholdView, - nonce_sums: &[Vec], - nonces: Vec>, - msg: &[u8], - ) -> Scalar { - self.msg = Some(msg.to_vec()); - self.schnorr.sign_share( - params, - nonce_sums, - nonces, - &[ - &u32::try_from(self.context.len()).expect("context exceeded 2^32 bytes").to_le_bytes(), - self.context, - msg, - ] - .concat(), - ) - } - - #[must_use] - fn verify( - &self, - group_key: RistrettoPoint, - nonces: &[Vec], - sum: Scalar, - ) -> Option { - let mut sig = (SchnorrSignature:: { R: nonces[0][0], s: sum }).serialize(); - sig[63] |= 1 << 7; - Some(Signature::from_bytes(&sig).unwrap()).filter(|sig| { - PublicKey::from_bytes(&group_key.to_bytes()) - .unwrap() - .verify(&mut signing_context(self.context).bytes(self.msg.as_ref().unwrap()), sig) - .is_ok() - }) - } - - fn verify_share( - &self, - verification_share: RistrettoPoint, - nonces: &[Vec], - share: Scalar, - ) -> Result, ()> { - self.schnorr.verify_share(verification_share, nonces, share) - } -} diff --git a/crypto/schnorrkel/src/tests.rs b/crypto/schnorrkel/src/tests.rs deleted file mode 100644 index 2f3c758b..00000000 --- a/crypto/schnorrkel/src/tests.rs +++ /dev/null @@ -1,26 +0,0 @@ -use rand_core::OsRng; - -use group::GroupEncoding; -use frost::{ - Participant, - tests::{key_gen, algorithm_machines, sign}, -}; - -use schnorrkel::{keys::PublicKey, context::SigningContext}; - -use crate::Schnorrkel; - -#[test] -fn test() { - const CONTEXT: &[u8] = b"FROST Schnorrkel Test"; - const MSG: &[u8] = b"Hello, World!"; - - let keys = key_gen(&mut OsRng); - let key = keys[&Participant::new(1).unwrap()].group_key(); - let algorithm = Schnorrkel::new(CONTEXT); - let machines = algorithm_machines(&mut OsRng, &algorithm, &keys); - let signature = sign(&mut OsRng, &algorithm, keys, machines, MSG); - - let key = PublicKey::from_bytes(key.to_bytes().as_ref()).unwrap(); - key.verify(&mut SigningContext::new(CONTEXT).bytes(MSG), &signature).unwrap() -}