diff --git a/Cargo.lock b/Cargo.lock index 3d3cb38e..7809352e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3080,6 +3080,20 @@ dependencies = [ "sp-api", ] +[[package]] +name = "frost-schnorrkel" +version = "0.1.0" +dependencies = [ + "ciphersuite", + "flexible-transcript", + "group 0.13.0", + "modular-frost", + "rand_core 0.6.4", + "schnorr-signatures", + "schnorrkel", + "zeroize", +] + [[package]] name = "fs-err" version = "2.9.0" diff --git a/Cargo.toml b/Cargo.toml index 0d343e20..c0621442 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "crypto/dleq", "crypto/dkg", "crypto/frost", + "crypto/schnorrkel", "coins/ethereum", "coins/monero/generators", diff --git a/crypto/schnorrkel/Cargo.toml b/crypto/schnorrkel/Cargo.toml new file mode 100644 index 00000000..bf611bc8 --- /dev/null +++ b/crypto/schnorrkel/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "frost-schnorrkel" +version = "0.1.0" +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" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +rand_core = "0.6" +zeroize = "^1.5" + +transcript = { package = "flexible-transcript", path = "../transcript", version = "0.3", features = ["merlin"] } + +group = "0.13" + +ciphersuite = { path = "../ciphersuite", version = "0.3", features = ["std", "ristretto"] } +schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "0.4" } +frost = { path = "../frost", package = "modular-frost", version = "0.7", features = ["ristretto"] } + +schnorrkel = "0.10" + +[dev-dependencies] +frost = { path = "../frost", package = "modular-frost", features = ["tests"] } diff --git a/crypto/schnorrkel/LICENSE b/crypto/schnorrkel/LICENSE new file mode 100644 index 00000000..e6bff13c --- /dev/null +++ b/crypto/schnorrkel/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypto/schnorrkel/README.md b/crypto/schnorrkel/README.md new file mode 100644 index 00000000..4f889dac --- /dev/null +++ b/crypto/schnorrkel/README.md @@ -0,0 +1,10 @@ +# 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 new file mode 100644 index 00000000..1db1ca5e --- /dev/null +++ b/crypto/schnorrkel/src/lib.rs @@ -0,0 +1,151 @@ +#![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. + 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 new file mode 100644 index 00000000..2b01ad43 --- /dev/null +++ b/crypto/schnorrkel/src/tests.rs @@ -0,0 +1,25 @@ +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 machines = algorithm_machines(&mut OsRng, Schnorrkel::new(CONTEXT), &keys); + let signature = sign(&mut OsRng, Schnorrkel::new(CONTEXT), keys, machines, MSG); + + let key = PublicKey::from_bytes(key.to_bytes().as_ref()).unwrap(); + key.verify(&mut SigningContext::new(CONTEXT).bytes(MSG), &signature).unwrap() +}