Replace Ciphersuite::hash_to_F

The prior-present `Ciphersuite::hash_to_F` was a sin. Implementations took a
DST, yet were not require to securely handle it. It was also biased towards the
requirements of `modular-frost` as `ciphersuite` was originally written all
those years ago, when `modular-frost` had needs exceeding what `ff`, `group`
satisfied.

Now, the hash is bound to produce an output which can be converted to a scalar
with `ff::FromUniformBytes`. A new `hash_to_F`, which accepts a single argument
of the value to hash (removing the potential to insecurely handle the DST by
removing the DST entirely). Due to `digest` yielding a `GenericArray`, yet
`FromUniformBytes` taking a `const usize`, the `ciphersuite` crate now defines
a `FromUniformBytes` trait taking an array (then implemented for all satisfiers
of `ff::FromUniformBytes`). In order to get the array type from the
`GenericArray`, the output of the hash, `digest` is updated to the `0.11`
release candidate which moves to `flexible-array` which solves that problem.

The existing, specific `hash_to_F` functions have been moved to `modular-frost`
as necessary.

`flexible-array` itself is patched to a fork due to
https://github.com/RustCrypto/hybrid-array/issues/131.
This commit is contained in:
Luke Parker
2025-08-29 05:04:03 -04:00
parent a4811c9a41
commit 90bc364f9f
37 changed files with 355 additions and 416 deletions

View File

@@ -1,9 +1,6 @@
use digest::Digest;
use ciphersuite::{digest::Digest, FromUniformBytes, Ciphersuite};
use dalek_ff_group::Scalar;
use ciphersuite::Ciphersuite;
use crate::{curve::Curve, algorithm::Hram};
macro_rules! dalek_curve {
@@ -20,6 +17,13 @@ macro_rules! dalek_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();
digest.update(Self::CONTEXT);
digest.update(dst);
digest.update(msg);
Self::F::from_uniform_bytes(&digest.finalize().into())
}
}
/// The challenge function for this ciphersuite.
@@ -30,11 +34,13 @@ macro_rules! dalek_curve {
fn hram(R: &<$Curve as Ciphersuite>::G, A: &<$Curve as Ciphersuite>::G, m: &[u8]) -> Scalar {
let mut hash = <$Curve as Ciphersuite>::H::new();
if $chal.len() != 0 {
hash.update(&[$CONTEXT.as_ref(), $chal].concat());
hash.update($CONTEXT);
hash.update($chal);
}
Scalar::from_hash(
hash.chain_update(&[&R.compress().to_bytes(), &A.compress().to_bytes(), m].concat()),
)
hash.update(R.compress().to_bytes());
hash.update(A.compress().to_bytes());
hash.update(m);
Scalar::from_uniform_bytes(&hash.finalize().into())
}
}
};

View File

@@ -1,11 +1,6 @@
use digest::Digest;
pub use ciphersuite::{digest::Digest, group::GroupEncoding, FromUniformBytes, Ciphersuite};
use minimal_ed448::{Scalar, Point};
pub use minimal_ed448::Ed448;
pub use ciphersuite::{
group::{ff::FromUniformBytes, GroupEncoding},
Ciphersuite,
};
use crate::{curve::Curve, algorithm::Hram};
@@ -13,6 +8,13 @@ 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();
digest.update(Self::CONTEXT);
digest.update(dst);
digest.update(msg);
Self::F::from_uniform_bytes(&digest.finalize().into())
}
}
// The RFC-8032 Ed448 challenge function.
@@ -21,20 +23,14 @@ pub(crate) struct Ietf8032Ed448Hram;
impl Ietf8032Ed448Hram {
#[allow(non_snake_case)]
pub(crate) fn hram(context: &[u8], R: &Point, A: &Point, m: &[u8]) -> Scalar {
Scalar::from_uniform_bytes(
&<[u8; 114]>::try_from(
<Ed448 as Ciphersuite>::H::digest(
[
&[b"SigEd448".as_ref(), &[0, u8::try_from(context.len()).unwrap()]].concat(),
context,
&[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat(),
]
.concat(),
)
.as_slice(),
)
.unwrap(),
)
let mut digest = <Ed448 as Ciphersuite>::H::new();
digest.update(b"SigEd448");
digest.update([0, u8::try_from(context.len()).unwrap()]);
digest.update(context);
digest.update(R.to_bytes());
digest.update(A.to_bytes());
digest.update(m);
Scalar::from_uniform_bytes(&digest.finalize().into())
}
}

View File

@@ -1,7 +1,85 @@
use ciphersuite::{group::GroupEncoding, Ciphersuite};
use core::convert::AsRef;
use sha2::{digest::Digest, Sha256};
use ciphersuite::{
group::{
ff::{Field, PrimeField},
GroupEncoding,
},
Ciphersuite,
};
use elliptic_curve::{
zeroize::Zeroize,
generic_array::{typenum::U32, GenericArray},
bigint::{NonZero, CheckedAdd, Encoding, U384},
hash2curve::{Expander, ExpandMsg, ExpandMsgXmd},
};
use crate::{curve::Curve, algorithm::Hram};
#[allow(non_snake_case)]
fn hash_to_F<C: Ciphersuite<F: PrimeField<Repr = GenericArray<u8, U32>>>>(
dst: &[u8],
msg: &[u8],
) -> C::F {
// While one of these two libraries does support directly hashing to the Scalar field, the
// other doesn't. While that's probably an oversight, this is a universally working method
// This method is from
// https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html
// Specifically, Section 5
// While that draft, overall, is intended for hashing to curves, that necessitates
// detailing how to hash to a finite field. The draft comments that its mechanism for
// doing so, which it uses to derive field elements, is also applicable to the scalar field
// The hash_to_field function is intended to provide unbiased values
// In order to do so, a wide reduction from an extra k bits is applied, minimizing bias to
// 2^-k
// k is intended to be the bits of security of the suite, which is 128 for secp256k1 and
// P-256
const K: usize = 128;
// L is the amount of bytes of material which should be used in the wide reduction
// The 256 is for the bit-length of the primes, rounded up to the nearest byte threshold
// This is a simplification of the formula from the end of section 5
const L: usize = (256 + K) / 8; // 48
// In order to perform this reduction, we need to use 48-byte numbers
// First, convert the modulus to a 48-byte number
// This is done by getting -1 as bytes, parsing it into a U384, and then adding back one
let mut modulus = [0; L];
// The byte repr of scalars will be 32 big-endian bytes
// Set the lower 32 bytes of our 48-byte array accordingly
modulus[16 ..].copy_from_slice(&(C::F::ZERO - C::F::ONE).to_repr());
// Use a checked_add + unwrap since this addition cannot fail (being a 32-byte value with
// 48-bytes of space)
// While a non-panicking saturating_add/wrapping_add could be used, they'd likely be less
// performant
let modulus = U384::from_be_slice(&modulus).checked_add(&U384::ONE).unwrap();
// The defined P-256 and secp256k1 ciphersuites both use expand_message_xmd
let mut wide = U384::from_be_bytes({
let mut bytes = [0; 48];
ExpandMsgXmd::<Sha256>::expand_message(&[msg], &[dst], 48).unwrap().fill_bytes(&mut bytes);
bytes
})
.rem(&NonZero::new(modulus).unwrap())
.to_be_bytes();
// Now that this has been reduced back to a 32-byte value, grab the lower 32-bytes
let mut array = *GenericArray::from_slice(&wide[16 ..]);
let res = C::F::from_repr(array).unwrap();
// Zeroize the temp values we can due to the possibility `hash_to_F` is being used for
// nonces
wide.zeroize();
array.zeroize();
res
}
macro_rules! kp_curve {
(
$feature: literal,
@@ -15,6 +93,17 @@ macro_rules! kp_curve {
impl Curve for $Curve {
const CONTEXT: &'static [u8] = $CONTEXT;
// These ciphersuites define their hash as SHA-512, yet FROST uses SHA-256
fn hash(dst: &[u8], data: &[u8]) -> impl AsRef<[u8]> {
sha2::Sha256::digest([Self::CONTEXT, dst, data].concat())
}
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
let dst = [Self::CONTEXT, dst].concat();
let dst = dst.as_slice();
hash_to_F::<Self>(dst, msg)
}
}
/// The challenge function for this ciphersuite.
@@ -41,3 +130,27 @@ kp_curve!("p256", P256, IetfP256Hram, b"FROST-P256-SHA256-v1");
#[cfg(feature = "secp256k1")]
kp_curve!("secp256k1", Secp256k1, IetfSecp256k1Hram, b"FROST-secp256k1-SHA256-v1");
#[cfg(test)]
fn test_oversize_dst<C: Ciphersuite<F: PrimeField<Repr = GenericArray<u8, U32>>>>() {
use sha2::Digest;
// The draft specifies DSTs >255 bytes should be hashed into a 32-byte DST
let oversize_dst = [0x00; 256];
let actual_dst = Sha256::digest([b"H2C-OVERSIZE-DST-".as_slice(), &oversize_dst].concat());
// Test the hash_to_F function handles this
// If it didn't, these would return different values
assert_eq!(hash_to_F::<C>(&oversize_dst, &[]), hash_to_F::<C>(&actual_dst, &[]));
}
#[cfg(feature = "secp256k1")]
#[test]
fn test_secp256k1() {
test_oversize_dst::<Secp256k1>();
}
#[cfg(feature = "p256")]
#[test]
fn test_p256() {
test_oversize_dst::<P256>();
}

View File

@@ -1,4 +1,4 @@
use core::ops::Deref;
use core::{ops::Deref, convert::AsRef};
use std::io::{self, Read};
use rand_core::{RngCore, CryptoRng};
@@ -6,9 +6,8 @@ use rand_core::{RngCore, CryptoRng};
use zeroize::{Zeroize, Zeroizing};
use subtle::ConstantTimeEq;
use digest::{Digest, Output};
pub use ciphersuite::{
digest::Digest,
group::{
ff::{Field, PrimeField},
Group,
@@ -46,24 +45,23 @@ pub trait Curve: Ciphersuite {
const CONTEXT: &'static [u8];
/// Hash the given dst and data to a byte vector. Used to instantiate H4 and H5.
fn hash(dst: &[u8], data: &[u8]) -> Output<Self::H> {
fn hash(dst: &[u8], data: &[u8]) -> impl AsRef<[u8]> {
Self::H::digest([Self::CONTEXT, dst, data].concat())
}
/// Field element from hash. Used during key gen and by other crates under Serai as a general
/// utility. Used to instantiate H1 and H3.
/// Field element from hash. Used to instantiate H1 and H3.
///
/// The `dst` MUST be prefixed by `Self::CONTEXT` by the implementor.
#[allow(non_snake_case)]
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
<Self as Ciphersuite>::hash_to_F(&[Self::CONTEXT, dst].concat(), msg)
}
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F;
/// Hash the message for the binding factor. H4 from the IETF draft.
fn hash_msg(msg: &[u8]) -> Output<Self::H> {
fn hash_msg(msg: &[u8]) -> impl AsRef<[u8]> {
Self::hash(b"msg", msg)
}
/// Hash the commitments for the binding factor. H5 from the IETF draft.
fn hash_commitments(commitments: &[u8]) -> Output<Self::H> {
fn hash_commitments(commitments: &[u8]) -> impl AsRef<[u8]> {
Self::hash(b"com", commitments)
}