Files
serai/crypto/schnorr/src/lib.rs

137 lines
4.5 KiB
Rust
Raw Normal View History

2023-03-20 20:10:00 -04:00
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
2023-03-20 20:10:00 -04:00
use core::ops::Deref;
#[cfg(all(feature = "alloc", not(feature = "std")))]
extern crate alloc;
#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::vec::Vec;
#[allow(unused_imports)]
use std_shims::prelude::*;
use std_shims::io::{self, Read, Write};
#[cfg(feature = "alloc")]
use rand_core::{RngCore, CryptoRng};
use zeroize::{Zeroize, Zeroizing};
2023-03-07 03:46:16 -05:00
use ciphersuite::{
group::{
ff::{Field, PrimeField},
Group, GroupEncoding,
},
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
GroupIo,
};
#[cfg(feature = "alloc")]
use multiexp::{multiexp_vartime, BatchVerifier};
2023-03-20 20:10:00 -04:00
/// Half-aggregation from <https://eprint.iacr.org/2021/350>.
#[cfg(feature = "aggregate")]
pub mod aggregate;
#[cfg(test)]
mod tests;
/// A Schnorr signature of the form (R, s) where s = r + cx.
2023-03-02 08:45:09 -05:00
///
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
/// These are intended to be strict. It is generic over `GroupIo` which is for `PrimeGroup`s,
2023-03-02 08:45:09 -05:00
/// and mandates canonical encodings in its read function.
///
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
/// RFC 8032 has an alternative verification formula for Ed25519, `8R = 8s - 8cX`, which is
/// intended to handle torsioned nonces/public keys. Due to this library's strict requirements,
/// such signatures will not be verifiable with this library.
#[allow(non_snake_case)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
pub struct SchnorrSignature<C: GroupIo> {
pub R: C::G,
pub s: C::F,
}
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
impl<C: GroupIo> SchnorrSignature<C> {
/// Read a SchnorrSignature from something implementing Read.
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
Ok(SchnorrSignature { R: C::read_G(reader)?, s: C::read_F(reader)? })
}
/// Write a SchnorrSignature to something implementing Read.
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.R.to_bytes().as_ref())?;
writer.write_all(self.s.to_repr().as_ref())
}
2023-03-07 05:34:29 -05:00
/// Serialize a SchnorrSignature, returning a `Vec<u8>`.
#[cfg(feature = "alloc")]
pub fn serialize(&self) -> Vec<u8> {
let mut buf = vec![];
self.write(&mut buf).unwrap();
buf
}
/// Sign a Schnorr signature with the given nonce for the specified challenge.
2023-03-02 09:08:53 -05:00
///
/// This challenge must be properly crafted, which means being binding to the public key, nonce,
/// and any message. Failure to do so will let a malicious adversary to forge signatures for
/// different keys/messages.
#[allow(clippy::needless_pass_by_value)] // Prevents further-use of this single-use value
pub fn sign(
private_key: &Zeroizing<C::F>,
nonce: Zeroizing<C::F>,
challenge: C::F,
) -> SchnorrSignature<C> {
SchnorrSignature {
// Uses deref instead of * as * returns C::F yet deref returns &C::F, preventing a copy
R: C::generator() * nonce.deref(),
s: (challenge * private_key.deref()) + nonce.deref(),
}
}
/// Return the series of pairs whose products sum to zero for a valid signature.
/// This is intended to be used with a multiexp.
pub fn batch_statements(&self, public_key: C::G, challenge: C::F) -> [(C::F, C::G); 3] {
// s = r + ca
// sG == R + cA
// R + cA - sG == 0
[
// R
(C::F::ONE, self.R),
// cA
(challenge, public_key),
// -sG
(-self.s, C::generator()),
]
}
/// Verify a Schnorr signature for the given key with the specified challenge.
2023-03-02 09:08:53 -05:00
///
/// This challenge must be properly crafted, which means being binding to the public key, nonce,
/// and any message. Failure to do so will let a malicious adversary to forge signatures for
/// different keys/messages.
#[must_use]
pub fn verify(&self, public_key: C::G, challenge: C::F) -> bool {
let statements = self.batch_statements(public_key, challenge);
#[cfg(feature = "alloc")]
let res = multiexp_vartime(&statements);
#[cfg(not(feature = "alloc"))]
let res = statements.into_iter().map(|(scalar, point)| point * scalar).sum::<C::G>();
res.is_identity().into()
}
/// Queue a signature for batch verification.
2023-03-02 09:08:53 -05:00
///
/// This challenge must be properly crafted, which means being binding to the public key, nonce,
/// and any message. Failure to do so will let a malicious adversary to forge signatures for
/// different keys/messages.
#[cfg(feature = "alloc")]
pub fn batch_verify<R: RngCore + CryptoRng, I: Copy + Zeroize>(
&self,
rng: &mut R,
batch: &mut BatchVerifier<I, C::G>,
id: I,
public_key: C::G,
challenge: C::F,
) {
batch.queue(rng, id, self.batch_statements(public_key, challenge));
}
}