Skip to content

Various fixes and improvements to hash2curve #1813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions elliptic-curve/src/hash2curve/group_digest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use super::{ExpandMsg, FromOkm, MapToCurve, hash_to_field};
use crate::{CurveArithmetic, ProjectivePoint, Result};
use group::cofactor::CofactorGroup;
use hybrid_array::typenum::Unsigned;

/// Adds hashing arbitrary byte sequences to a valid group element
pub trait GroupDigest: CurveArithmetic
Expand All @@ -12,6 +13,11 @@ where
/// The field element representation for a group value with multiple elements
type FieldElement: FromOkm + MapToCurve<Output = ProjectivePoint<Self>> + Default + Copy;

/// The target security level in bytes:
/// <https://www.rfc-editor.org/rfc/rfc9380.html#section-8.9-2.2>
/// <https://www.rfc-editor.org/rfc/rfc9380.html#name-target-security-levels>
type K: Unsigned;

/// Computes the hash to curve routine.
///
/// From <https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html>:
Expand Down
14 changes: 11 additions & 3 deletions elliptic-curve/src/hash2curve/hash2field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

mod expand_msg;

use core::num::NonZeroUsize;

pub use expand_msg::{xmd::*, xof::*, *};

use crate::{Error, Result};
use hybrid_array::{Array, ArraySize, typenum::Unsigned};
use hybrid_array::{
Array, ArraySize,
typenum::{NonZero, Unsigned},
};

/// The trait for helping to convert to a field element.
pub trait FromOkm {
/// The number of bytes needed to convert to a field element.
type Length: ArraySize;
type Length: ArraySize + NonZero;

/// Convert a byte sequence into a field element.
fn from_okm(data: &Array<u8, Self::Length>) -> Self;
Expand All @@ -37,7 +42,10 @@ where
E: ExpandMsg<'a>,
T: FromOkm + Default,
{
let len_in_bytes = T::Length::to_usize().checked_mul(out.len()).ok_or(Error)?;
let len_in_bytes = T::Length::to_usize()
.checked_mul(out.len())
.and_then(NonZeroUsize::new)
.ok_or(Error)?;
let mut tmp = Array::<u8, <T as FromOkm>::Length>::default();
let mut expander = E::expand_message(data, domain, len_in_bytes)?;
for o in out.iter_mut() {
Expand Down
4 changes: 3 additions & 1 deletion elliptic-curve/src/hash2curve/hash2field/expand_msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
pub(super) mod xmd;
pub(super) mod xof;

use core::num::NonZero;

use crate::{Error, Result};
use digest::{Digest, ExtendableOutput, Update, XofReader};
use hybrid_array::typenum::{IsLess, U256};
Expand All @@ -28,7 +30,7 @@ pub trait ExpandMsg<'a> {
fn expand_message(
msgs: &[&[u8]],
dsts: &'a [&'a [u8]],
len_in_bytes: usize,
len_in_bytes: NonZero<usize>,
) -> Result<Self::Expander>;
}

Expand Down
52 changes: 33 additions & 19 deletions elliptic-curve/src/hash2curve/hash2field/expand_msg/xmd.rs
Original file line number Diff line number Diff line change
@@ -1,59 +1,69 @@
//! `expand_message_xmd` based on a hash function.

use core::marker::PhantomData;
use core::{marker::PhantomData, num::NonZero, ops::Mul};

use super::{Domain, ExpandMsg, Expander};
use crate::{Error, Result};
use digest::{
FixedOutput, HashMarker,
array::{
Array,
typenum::{IsLess, IsLessOrEqual, U256, Unsigned},
typenum::{IsGreaterOrEqual, IsLess, IsLessOrEqual, U2, U256, Unsigned},
},
core_api::BlockSizeUser,
};

/// Placeholder type for implementing `expand_message_xmd` based on a hash function
/// Implements `expand_message_xof` via the [`ExpandMsg`] trait:
/// <https://www.rfc-editor.org/rfc/rfc9380.html#name-expand_message_xmd>
///
/// `K` is the target security level in bytes:
/// <https://www.rfc-editor.org/rfc/rfc9380.html#section-8.9-2.2>
/// <https://www.rfc-editor.org/rfc/rfc9380.html#name-target-security-levels>
///
/// # Errors
/// - `dst.is_empty()`
/// - `len_in_bytes == 0`
/// - `len_in_bytes > u16::MAX`
/// - `len_in_bytes > 255 * HashT::OutputSize`
#[derive(Debug)]
pub struct ExpandMsgXmd<HashT>(PhantomData<HashT>)
pub struct ExpandMsgXmd<HashT, K>(PhantomData<(HashT, K)>)
where
HashT: BlockSizeUser + Default + FixedOutput + HashMarker,
HashT::OutputSize: IsLess<U256>,
HashT::OutputSize: IsLessOrEqual<HashT::BlockSize>;
HashT::OutputSize: IsLessOrEqual<HashT::BlockSize>,
K: Mul<U2>,
HashT::OutputSize: IsGreaterOrEqual<<K as Mul<U2>>::Output>;

/// ExpandMsgXmd implements expand_message_xmd for the ExpandMsg trait
impl<'a, HashT> ExpandMsg<'a> for ExpandMsgXmd<HashT>
impl<'a, HashT, K> ExpandMsg<'a> for ExpandMsgXmd<HashT, K>
where
HashT: BlockSizeUser + Default + FixedOutput + HashMarker,
// If `len_in_bytes` is bigger then 256, length of the `DST` will depend on
// the output size of the hash, which is still not allowed to be bigger then 256:
// If DST is larger than 255 bytes, the length of the computed DST will depend on the output
// size of the hash, which is still not allowed to be larger than 256:
// https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html#section-5.4.1-6
HashT::OutputSize: IsLess<U256>,
// Constraint set by `expand_message_xmd`:
// https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html#section-5.4.1-4
HashT::OutputSize: IsLessOrEqual<HashT::BlockSize>,
// The number of bits output by `HashT` MUST be larger or equal to `K * 2`:
// https://www.rfc-editor.org/rfc/rfc9380.html#section-5.3.1-2.1
K: Mul<U2>,
HashT::OutputSize: IsGreaterOrEqual<<K as Mul<U2>>::Output>,
{
type Expander = ExpanderXmd<'a, HashT>;

fn expand_message(
msgs: &[&[u8]],
dsts: &'a [&'a [u8]],
len_in_bytes: usize,
len_in_bytes: NonZero<usize>,
) -> Result<Self::Expander> {
if len_in_bytes == 0 {
let len_in_bytes_u16 = u16::try_from(len_in_bytes.get()).map_err(|_| Error)?;

// `255 * <b_in_bytes>` can not exceed `u16::MAX`
if len_in_bytes_u16 > 255 * HashT::OutputSize::to_u16() {
return Err(Error);
}

let len_in_bytes_u16 = u16::try_from(len_in_bytes).map_err(|_| Error)?;

let b_in_bytes = HashT::OutputSize::to_usize();
let ell = u8::try_from(len_in_bytes.div_ceil(b_in_bytes)).map_err(|_| Error)?;
let ell = u8::try_from(len_in_bytes.get().div_ceil(b_in_bytes)).map_err(|_| Error)?;

let domain = Domain::xmd::<HashT>(dsts)?;
let mut b_0 = HashT::default();
Expand Down Expand Up @@ -157,7 +167,7 @@ mod test {
use hex_literal::hex;
use hybrid_array::{
ArraySize,
typenum::{U32, U128},
typenum::{U4, U8, U32, U128},
};
use sha2::Sha256;

Expand Down Expand Up @@ -209,13 +219,17 @@ mod test {
) -> Result<()>
where
HashT: BlockSizeUser + Default + FixedOutput + HashMarker,
HashT::OutputSize: IsLess<U256> + IsLessOrEqual<HashT::BlockSize>,
HashT::OutputSize: IsLess<U256> + IsLessOrEqual<HashT::BlockSize> + Mul<U8>,
HashT::OutputSize: IsGreaterOrEqual<<U4 as Mul<U2>>::Output>,
{
assert_message::<HashT>(self.msg, domain, L::to_u16(), self.msg_prime);

let dst = [dst];
let mut expander =
ExpandMsgXmd::<HashT>::expand_message(&[self.msg], &dst, L::to_usize())?;
let mut expander = ExpandMsgXmd::<HashT, U4>::expand_message(
&[self.msg],
&dst,
NonZero::new(L::to_usize()).ok_or(Error)?,
)?;

let mut uniform_bytes = Array::<u8, L>::default();
expander.fill_bytes(&mut uniform_bytes);
Expand Down
76 changes: 49 additions & 27 deletions elliptic-curve/src/hash2curve/hash2field/expand_msg/xof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,38 @@

use super::{Domain, ExpandMsg, Expander};
use crate::{Error, Result};
use core::fmt;
use digest::{ExtendableOutput, Update, XofReader};
use hybrid_array::typenum::U32;

/// Placeholder type for implementing `expand_message_xof` based on an extendable output function
use core::{fmt, marker::PhantomData, num::NonZero, ops::Mul};
use digest::{ExtendableOutput, HashMarker, Update, XofReader};
use hybrid_array::{
ArraySize,
typenum::{IsLess, U2, U256},
};

/// Implements `expand_message_xof` via the [`ExpandMsg`] trait:
/// <https://www.rfc-editor.org/rfc/rfc9380.html#name-expand_message_xof>
///
/// `K` is the target security level in bytes:
/// <https://www.rfc-editor.org/rfc/rfc9380.html#section-8.9-2.2>
/// <https://www.rfc-editor.org/rfc/rfc9380.html#name-target-security-levels>
///
/// # Errors
/// - `dst.is_empty()`
/// - `len_in_bytes == 0`
/// - `len_in_bytes > u16::MAX`
pub struct ExpandMsgXof<HashT>
pub struct ExpandMsgXof<HashT, K>
where
HashT: Default + ExtendableOutput + Update,
HashT: Default + ExtendableOutput + Update + HashMarker,
K: Mul<U2>,
<K as Mul<U2>>::Output: ArraySize + IsLess<U256>,
{
reader: <HashT as ExtendableOutput>::Reader,
_k: PhantomData<K>,
}

impl<HashT> fmt::Debug for ExpandMsgXof<HashT>
impl<HashT, K> fmt::Debug for ExpandMsgXof<HashT, K>
where
HashT: Default + ExtendableOutput + Update,
HashT: Default + ExtendableOutput + Update + HashMarker,
K: Mul<U2>,
<K as Mul<U2>>::Output: ArraySize + IsLess<U256>,
<HashT as ExtendableOutput>::Reader: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand All @@ -31,25 +43,24 @@ where
}
}

/// ExpandMsgXof implements `expand_message_xof` for the [`ExpandMsg`] trait
impl<'a, HashT> ExpandMsg<'a> for ExpandMsgXof<HashT>
impl<'a, HashT, K> ExpandMsg<'a> for ExpandMsgXof<HashT, K>
where
HashT: Default + ExtendableOutput + Update,
HashT: Default + ExtendableOutput + Update + HashMarker,
// If DST is larger than 255 bytes, the length of the computed DST is calculated by `K * 2`.
// https://www.rfc-editor.org/rfc/rfc9380.html#section-5.3.1-2.1
K: Mul<U2>,
<K as Mul<U2>>::Output: ArraySize + IsLess<U256>,
{
type Expander = Self;

fn expand_message(
msgs: &[&[u8]],
dsts: &'a [&'a [u8]],
len_in_bytes: usize,
len_in_bytes: NonZero<usize>,
) -> Result<Self::Expander> {
if len_in_bytes == 0 {
return Err(Error);
}

let len_in_bytes = u16::try_from(len_in_bytes).map_err(|_| Error)?;
let len_in_bytes = u16::try_from(len_in_bytes.get()).map_err(|_| Error)?;

let domain = Domain::<U32>::xof::<HashT>(dsts)?;
let domain = Domain::<<K as Mul<U2>>::Output>::xof::<HashT>(dsts)?;
let mut reader = HashT::default();

for msg in msgs {
Expand All @@ -60,13 +71,18 @@ where
domain.update_hash(&mut reader);
reader.update(&[domain.len()]);
let reader = reader.finalize_xof();
Ok(Self { reader })
Ok(Self {
reader,
_k: PhantomData,
})
}
}

impl<HashT> Expander for ExpandMsgXof<HashT>
impl<HashT, K> Expander for ExpandMsgXof<HashT, K>
where
HashT: Default + ExtendableOutput + Update,
HashT: Default + ExtendableOutput + Update + HashMarker,
K: Mul<U2>,
<K as Mul<U2>>::Output: ArraySize + IsLess<U256>,
{
fn fill_bytes(&mut self, okm: &mut [u8]) {
self.reader.read(okm);
Expand All @@ -78,7 +94,10 @@ mod test {
use super::*;
use core::mem::size_of;
use hex_literal::hex;
use hybrid_array::{Array, ArraySize, typenum::U128};
use hybrid_array::{
Array, ArraySize,
typenum::{U16, U32, U128},
};
use sha3::Shake128;

fn assert_message(msg: &[u8], domain: &Domain<'_, U32>, len_in_bytes: u16, bytes: &[u8]) {
Expand Down Expand Up @@ -110,13 +129,16 @@ mod test {
#[allow(clippy::panic_in_result_fn)]
fn assert<HashT, L>(&self, dst: &'static [u8], domain: &Domain<'_, U32>) -> Result<()>
where
HashT: Default + ExtendableOutput + Update,
HashT: Default + ExtendableOutput + Update + HashMarker,
L: ArraySize,
{
assert_message(self.msg, domain, L::to_u16(), self.msg_prime);

let mut expander =
ExpandMsgXof::<HashT>::expand_message(&[self.msg], &[dst], L::to_usize())?;
let mut expander = ExpandMsgXof::<HashT, U16>::expand_message(
&[self.msg],
&[dst],
NonZero::new(L::to_usize()).ok_or(Error)?,
)?;

let mut uniform_bytes = Array::<u8, L>::default();
expander.fill_bytes(&mut uniform_bytes);
Expand Down