diff --git a/Cargo.lock b/Cargo.lock index 16bdcacb..d10974f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1387,6 +1387,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "cxx" version = "1.0.86" @@ -2601,6 +2614,19 @@ dependencies = [ "sp-api", ] +[[package]] +name = "frost-schnorrkel" +version = "0.1.0" +dependencies = [ + "ciphersuite", + "group", + "modular-frost", + "rand_core 0.6.4", + "schnorr-signatures", + "schnorrkel 0.10.2", + "zeroize", +] + [[package]] name = "fs2" version = "0.4.3" @@ -7107,6 +7133,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "schnorrkel" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844b7645371e6ecdf61ff246ba1958c29e802881a749ae3fb1993675d210d28d" +dependencies = [ + "arrayref", + "arrayvec 0.7.2", + "curve25519-dalek-ng", + "merlin 3.0.0", + "rand_core 0.6.4", + "serde_bytes", + "sha2 0.9.9", + "subtle-ng", + "zeroize", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -7369,6 +7412,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "serde_bytes" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718dc5fff5b36f99093fc49b280cfc96ce6fc824317783bff5a1fed0c7a64819" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.152" @@ -7752,7 +7804,7 @@ dependencies = [ "rand 0.7.3", "regex", "scale-info", - "schnorrkel", + "schnorrkel 0.9.1", "secp256k1", "secrecy", "serde", @@ -7905,7 +7957,7 @@ dependencies = [ "merlin 2.0.1", "parity-scale-codec", "parking_lot 0.12.1", - "schnorrkel", + "schnorrkel 0.9.1", "serde", "sp-core", "sp-externalities", @@ -8344,7 +8396,7 @@ checksum = "49eee6965196b32f882dd2ee85a92b1dbead41b04e53907f269de3b0dc04733c" dependencies = [ "hmac 0.11.0", "pbkdf2 0.8.0", - "schnorrkel", + "schnorrkel 0.9.1", "sha2 0.9.9", "zeroize", ] @@ -8414,6 +8466,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "svm-rs" version = "0.2.18" diff --git a/Cargo.toml b/Cargo.toml index 7cdec0c1..520f1b5e 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..533c07a3 --- /dev/null +++ b/crypto/schnorrkel/Cargo.toml @@ -0,0 +1,28 @@ +[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" + +group = "0.12" + +ciphersuite = { path = "../ciphersuite", version = "0.1", features = ["std", "ristretto"] } +schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "0.2" } +frost = { path = "../frost", package = "modular-frost", version = "0.5", features = ["ristretto"] } + +schnorrkel = "0.10" + +[dev-dependencies] +frost = { path = "../frost", package = "modular-frost", version = "0.5", features = ["ristretto", "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/src/lib.rs b/crypto/schnorrkel/src/lib.rs new file mode 100644 index 00000000..b2476aef --- /dev/null +++ b/crypto/schnorrkel/src/lib.rs @@ -0,0 +1,133 @@ +use std::io::{self, Read}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::Zeroizing; + +use group::{ff::PrimeField, GroupEncoding}; +use ciphersuite::{Ciphersuite, Ristretto}; +use schnorr::SchnorrSignature; +use frost::{ + ThresholdKeys, ThresholdView, FrostError, + algorithm::{IetfTranscript, Hram, Algorithm, Schnorr}, +}; + +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() + } +} + +#[derive(Clone)] +pub struct Schnorrkel { + context: &'static [u8], + schnorr: Schnorr, + msg: Option>, +} + +impl Schnorrkel { + pub fn new(context: &'static [u8]) -> Schnorrkel { + Schnorrkel { context, schnorr: Schnorr::new(), msg: None } + } +} + +impl Algorithm for Schnorrkel { + type Transcript = IetfTranscript; + 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, + _: u16, + _: (), + ) -> Result<(), FrostError> { + Ok(()) + } + + fn sign_share( + &mut self, + params: &ThresholdView, + nonce_sums: &[Vec<::G>], + nonces: Vec::F>>, + msg: &[u8], + ) -> ::F { + 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: ::G, + nonces: &[Vec<::G>], + sum: ::F, + ) -> 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: ::G, + nonces: &[Vec<::G>], + share: ::F, + ) -> Result::F, ::G)>, ()> { + 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..8b36ce42 --- /dev/null +++ b/crypto/schnorrkel/src/tests.rs @@ -0,0 +1,14 @@ +use rand_core::OsRng; + +use frost::tests::{key_gen, algorithm_machines, sign}; + +use crate::Schnorrkel; + +#[test] +fn test() { + let keys = key_gen(&mut OsRng); + const CONTEXT: &[u8] = b"FROST Schnorrkel Test"; + let machines = algorithm_machines(&mut OsRng, Schnorrkel::new(CONTEXT), &keys); + const MSG: &[u8] = b"Hello, World!"; + sign(&mut OsRng, Schnorrkel::new(CONTEXT), keys, machines, MSG); +}