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`.
This commit is contained in:
Luke Parker
2025-09-03 12:25:37 -04:00
parent 215e41fdb6
commit a141deaf36
124 changed files with 1003 additions and 1211 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "modular-frost"
version = "0.10.1"
version = "0.11.0"
description = "Modular implementation of FROST over ff/group"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost"
@@ -29,7 +29,7 @@ hex = { version = "0.4", default-features = false, features = ["std"], optional
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false, features = ["std", "recommended"] }
dalek-ff-group = { path = "../dalek-ff-group", version = "0.4", default-features = false, features = ["std"], optional = true }
dalek-ff-group = { path = "../dalek-ff-group", version = "0.5", default-features = false, features = ["std"], optional = true }
minimal-ed448 = { path = "../ed448", version = "0.4", default-features = false, features = ["std"], optional = true }
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }

View File

@@ -1,24 +1,24 @@
use ciphersuite::{digest::Digest, FromUniformBytes, Ciphersuite};
use subtle::CtOption;
use zeroize::Zeroize;
use ciphersuite::{
digest::Digest, group::GroupEncoding, FromUniformBytes, WrappedGroup, WithPreferredHash,
};
use dalek_ff_group::Scalar;
use crate::{curve::Curve, algorithm::Hram};
macro_rules! dalek_curve {
(
$feature: literal,
$Curve: ident,
$Hram: ident,
$CONTEXT: literal,
$chal: literal
) => {
pub use dalek_ff_group::$Curve;
impl Curve for $Curve {
const CONTEXT: &'static [u8] = $CONTEXT;
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
let mut digest = <Self as Ciphersuite>::H::new();
let mut digest = <Self as WithPreferredHash>::H::new();
digest.update(Self::CONTEXT);
digest.update(dst);
digest.update(msg);
@@ -31,8 +31,12 @@ macro_rules! dalek_curve {
pub struct $Hram;
impl Hram<$Curve> for $Hram {
#[allow(non_snake_case)]
fn hram(R: &<$Curve as Ciphersuite>::G, A: &<$Curve as Ciphersuite>::G, m: &[u8]) -> Scalar {
let mut hash = <$Curve as Ciphersuite>::H::new();
fn hram(
R: &<$Curve as WrappedGroup>::G,
A: &<$Curve as WrappedGroup>::G,
m: &[u8],
) -> Scalar {
let mut hash = <$Curve as WithPreferredHash>::H::new();
if $chal.len() != 0 {
hash.update($CONTEXT);
hash.update($chal);
@@ -46,8 +50,31 @@ macro_rules! dalek_curve {
};
}
#[cfg(feature = "ristretto")]
dalek_curve!("ristretto", Ristretto, IetfRistrettoHram, b"FROST-RISTRETTO255-SHA512-v1", b"chal");
/*
FROST defined Ristretto as using SHA2-512, while Blake2b512 is considered "preferred" by
`dalek-ff-group`. We define our own ciphersuite for it accordingly.
*/
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub struct Ristretto;
impl WrappedGroup for Ristretto {
type F = Scalar;
type G = dalek_ff_group::RistrettoPoint;
fn generator() -> Self::G {
dalek_ff_group::Ristretto::generator()
}
}
impl ciphersuite::Id for Ristretto {
const ID: &[u8] = b"FROST-RISTRETTO255";
}
impl WithPreferredHash for Ristretto {
type H = <Ed25519 as WithPreferredHash>::H;
}
impl ciphersuite::GroupCanonicalEncoding for Ristretto {
fn from_canonical_bytes(bytes: &<Self::G as GroupEncoding>::Repr) -> CtOption<Self::G> {
dalek_ff_group::Ristretto::from_canonical_bytes(bytes)
}
}
dalek_curve!(Ristretto, IetfRistrettoHram, b"FROST-RISTRETTO255-SHA512-v1", b"chal");
#[cfg(feature = "ed25519")]
dalek_curve!("ed25519", Ed25519, IetfEd25519Hram, b"FROST-ED25519-SHA512-v1", b"");
pub use dalek_ff_group::Ed25519;
dalek_curve!(Ed25519, IetfEd25519Hram, b"FROST-ED25519-SHA512-v1", b"");

View File

@@ -1,6 +1,6 @@
pub use ciphersuite::{digest::Digest, group::GroupEncoding, FromUniformBytes, Ciphersuite};
use minimal_ed448::{Scalar, Point};
pub use minimal_ed448::Ed448;
pub use ciphersuite::{digest::Digest, group::GroupEncoding, FromUniformBytes, WithPreferredHash};
use minimal_ed448::Scalar;
pub use minimal_ed448::Point as Ed448;
use crate::{curve::Curve, algorithm::Hram};
@@ -9,7 +9,7 @@ const CONTEXT: &[u8] = b"FROST-ED448-SHAKE256-v1";
impl Curve for Ed448 {
const CONTEXT: &'static [u8] = CONTEXT;
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
let mut digest = <Self as Ciphersuite>::H::new();
let mut digest = <Self as WithPreferredHash>::H::new();
digest.update(Self::CONTEXT);
digest.update(dst);
digest.update(msg);
@@ -22,8 +22,8 @@ impl Curve for Ed448 {
pub(crate) struct Ietf8032Ed448Hram;
impl Ietf8032Ed448Hram {
#[allow(non_snake_case)]
pub(crate) fn hram(context: &[u8], R: &Point, A: &Point, m: &[u8]) -> Scalar {
let mut digest = <Ed448 as Ciphersuite>::H::new();
pub(crate) fn hram(context: &[u8], R: &Ed448, A: &Ed448, m: &[u8]) -> Scalar {
let mut digest = <Ed448 as WithPreferredHash>::H::new();
digest.update(b"SigEd448");
digest.update([0, u8::try_from(context.len()).unwrap()]);
digest.update(context);
@@ -39,7 +39,7 @@ impl Ietf8032Ed448Hram {
pub struct IetfEd448Hram;
impl Hram<Ed448> for IetfEd448Hram {
#[allow(non_snake_case)]
fn hram(R: &Point, A: &Point, m: &[u8]) -> Scalar {
fn hram(R: &Ed448, A: &Ed448, m: &[u8]) -> Scalar {
Ietf8032Ed448Hram::hram(&[], R, A, m)
}
}

View File

@@ -7,7 +7,7 @@ use ciphersuite::{
ff::{Field, PrimeField},
GroupEncoding,
},
Ciphersuite,
WrappedGroup,
};
use elliptic_curve::{
@@ -20,7 +20,7 @@ use elliptic_curve::{
use crate::{curve::Curve, algorithm::Hram};
#[allow(non_snake_case)]
fn hash_to_F<C: Ciphersuite<F: PrimeField<Repr = GenericArray<u8, U32>>>>(
fn hash_to_F<C: WrappedGroup<F: PrimeField<Repr = GenericArray<u8, U32>>>>(
dst: &[u8],
msg: &[u8],
) -> C::F {
@@ -112,10 +112,10 @@ macro_rules! kp_curve {
impl Hram<$Curve> for $Hram {
#[allow(non_snake_case)]
fn hram(
R: &<$Curve as Ciphersuite>::G,
A: &<$Curve as Ciphersuite>::G,
R: &<$Curve as WrappedGroup>::G,
A: &<$Curve as WrappedGroup>::G,
m: &[u8],
) -> <$Curve as Ciphersuite>::F {
) -> <$Curve as WrappedGroup>::F {
<$Curve as Curve>::hash_to_F(
b"chal",
&[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat(),
@@ -132,7 +132,7 @@ kp_curve!("p256", P256, IetfP256Hram, b"FROST-P256-SHA256-v1");
kp_curve!("secp256k1", Secp256k1, IetfSecp256k1Hram, b"FROST-secp256k1-SHA256-v1");
#[cfg(test)]
fn test_oversize_dst<C: Ciphersuite<F: PrimeField<Repr = GenericArray<u8, U32>>>>() {
fn test_oversize_dst<C: WrappedGroup<F: PrimeField<Repr = GenericArray<u8, U32>>>>() {
use sha2::Digest;
// The draft specifies DSTs >255 bytes should be hashed into a 32-byte DST

View File

@@ -6,21 +6,16 @@ use rand_core::{RngCore, CryptoRng};
use zeroize::{Zeroize, Zeroizing};
use subtle::ConstantTimeEq;
pub use ciphersuite::{
digest::Digest,
group::{
ff::{Field, PrimeField},
Group,
},
Ciphersuite,
use ciphersuite::group::{
ff::{Field, PrimeField},
Group,
};
pub use ciphersuite::{digest::Digest, WrappedGroup, GroupIo, Ciphersuite};
#[cfg(any(feature = "ristretto", feature = "ed25519"))]
mod dalek;
#[cfg(feature = "ristretto")]
pub use dalek::{Ristretto, IetfRistrettoHram};
#[cfg(feature = "ed25519")]
pub use dalek::{Ed25519, IetfEd25519Hram};
#[cfg(any(feature = "ristretto", feature = "ed25519"))]
pub use dalek::*;
#[cfg(any(feature = "secp256k1", feature = "p256"))]
mod kp256;
@@ -38,11 +33,11 @@ pub(crate) use ed448::Ietf8032Ed448Hram;
/// FROST Ciphersuite.
///
/// This exclude the signing algorithm specific H2, making this solely the curve, its associated
/// This excludes the signing algorithm specific H2, making this solely the curve, its associated
/// hash function, and the functions derived from it.
pub trait Curve: Ciphersuite {
pub trait Curve: GroupIo + Ciphersuite {
/// Context string for this curve.
const CONTEXT: &'static [u8];
const CONTEXT: &[u8];
/// Hash the given dst and data to a byte vector. Used to instantiate H4 and H5.
fn hash(dst: &[u8], data: &[u8]) -> impl AsRef<[u8]> {
@@ -121,7 +116,7 @@ pub trait Curve: Ciphersuite {
/// Read a point from a reader, rejecting identity.
#[allow(non_snake_case)]
fn read_G<R: Read>(reader: &mut R) -> io::Result<Self::G> {
let res = <Self as Ciphersuite>::read_G(reader)?;
let res = <Self as GroupIo>::read_G(reader)?;
if res.is_identity().into() {
Err(io::Error::other("identity point"))?;
}

View File

@@ -11,10 +11,9 @@ use zeroize::{Zeroize, Zeroizing};
use transcript::Transcript;
use ciphersuite::group::{
ff::{Field, PrimeField},
GroupEncoding,
};
use ciphersuite::group::{ff::PrimeField, GroupEncoding};
#[cfg(any(test, feature = "tests"))]
use ciphersuite::group::ff::Field;
use multiexp::BatchVerifier;
use crate::{

View File

@@ -1,6 +1,6 @@
use rand_core::OsRng;
use ciphersuite::Ciphersuite;
use ciphersuite::GroupIo;
use schnorr::SchnorrSignature;

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use rand_core::{RngCore, CryptoRng};
use ciphersuite::Ciphersuite;
use ciphersuite::{GroupIo, Id};
pub use dkg_recovery::recover_key;
use crate::{
@@ -28,7 +28,7 @@ pub const PARTICIPANTS: u16 = 5;
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
/// Create a key, for testing purposes.
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
pub fn key_gen<R: RngCore + CryptoRng, C: GroupIo + Id>(
rng: &mut R,
) -> HashMap<Participant, ThresholdKeys<C>> {
let res = dkg_dealer::key_gen::<R, C>(rng, THRESHOLD, PARTICIPANTS).unwrap();