Skip to content

Commit 302fb0a

Browse files
committed
feat: add traits for group digest
Signed-off-by: Michael Lodder <[email protected]>
1 parent a6f22ef commit 302fb0a

File tree

11 files changed

+324
-83
lines changed

11 files changed

+324
-83
lines changed

elliptic-curve/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ zeroize = { version = "1", default-features = false }
2929

3030
# optional dependencies
3131
base64ct = { version = "1", optional = true, default-features = false }
32-
digest = { version = "0.9", optional = true, default-features = false }
32+
digest_traits = { version = "0.9", optional = true, default-features = false, package = "digest" }
3333
ff = { version = "0.11", optional = true, default-features = false }
3434
group = { version = "0.11", optional = true, default-features = false }
3535
hex-literal = { version = "0.3", optional = true }
@@ -47,10 +47,10 @@ alloc = ["der/alloc", "sec1/alloc", "zeroize/alloc"] # todo: use weak activation
4747
arithmetic = ["ff", "group"]
4848
bits = ["arithmetic", "ff/bits"]
4949
dev = ["arithmetic", "hex-literal", "pem", "pkcs8"]
50+
digest = ["digest_traits", "ff", "group"]
5051
ecdh = ["arithmetic"]
5152
hazmat = []
5253
jwk = ["alloc", "base64ct/alloc", "serde", "serde_json", "zeroize/alloc"]
53-
osswu = ["ff"]
5454
pem = ["alloc", "arithmetic", "pem-rfc7468/alloc", "pkcs8", "sec1/pem"]
5555
pkcs8 = ["sec1/pkcs8"]
5656
std = ["alloc", "rand_core/std"]

elliptic-curve/src/group_digest.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use crate::hash2field::{hash_to_field, ExpandMsg, FromOkm};
2+
use crate::map2curve::MapToCurve;
3+
use group::cofactor::CofactorGroup;
4+
5+
/// Adds hashing arbitrary byte sequences to a valid group element
6+
pub trait GroupDigest {
7+
/// The field element representation for a group value with multiple elements
8+
type FieldElement: FromOkm + Default + Copy;
9+
/// The resulting group element
10+
type Output: CofactorGroup<Subgroup = Self::Output>
11+
+ MapToCurve<FieldElement = Self::FieldElement, Output = Self::Output>;
12+
13+
/// Computes the hash to curve routine according to
14+
/// <https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html>
15+
/// which says
16+
/// Uniform encoding from byte strings to points in G.
17+
/// That is, the distribution of its output is statistically close
18+
/// to uniform in G.
19+
/// This function is suitable for most applications requiring a random
20+
/// oracle returning points in G assuming a cryptographically secure
21+
/// hash function is used.
22+
///
23+
/// Examples
24+
///
25+
/// Using a fixed size hash function
26+
///
27+
/// ```ignore
28+
/// let pt = ProjectivePoint::hash_from_bytes::<hash2field::ExpandMsgXmd<sha2::Sha256>>(b"test data", b"CURVE_XMD:SHA-256_SSWU_RO_");
29+
/// ```
30+
///
31+
/// Using an extendable output function
32+
///
33+
/// ```ignore
34+
/// let pt = ProjectivePoint::hash_from_bytes::<hash2field::ExpandMsgXof<sha3::Shake256>>(b"test data", b"CURVE_XOF:SHAKE-256_SSWU_RO_");
35+
/// ```
36+
///
37+
fn hash_from_bytes<X: ExpandMsg>(msg: &[u8], dst: &'static [u8]) -> Self::Output {
38+
let mut u = [Self::FieldElement::default(), Self::FieldElement::default()];
39+
hash_to_field::<X, _>(msg, dst, &mut u);
40+
let q0 = Self::Output::map_to_curve(u[0]);
41+
let q1 = Self::Output::map_to_curve(u[1]);
42+
// Ideally we could add and then clear cofactor once
43+
// thus saving a call but the field elements may not
44+
// add properly due to the underlying implementation
45+
// which could result in an incorrect subgroup
46+
q0.clear_cofactor() + q1.clear_cofactor()
47+
}
48+
49+
/// Computes the encode to curve routine according to
50+
/// <https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html>
51+
/// which says
52+
/// Nonuniform encoding from byte strings to
53+
/// points in G. That is, the distribution of its output is not
54+
/// uniformly random in G: the set of possible outputs of
55+
/// encode_to_curve is only a fraction of the points in G, and some
56+
/// points in this set are more likely to be output than others.
57+
fn encode_from_bytes<X: ExpandMsg>(msg: &[u8], dst: &'static [u8]) -> Self::Output {
58+
let mut u = [Self::FieldElement::default()];
59+
hash_to_field::<X, _>(msg, dst, &mut u);
60+
let q0 = Self::Output::map_to_curve(u[0]);
61+
q0.clear_cofactor()
62+
}
63+
}

elliptic-curve/src/hash2field.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,32 @@ mod expand_msg;
22
mod expand_msg_xmd;
33
mod expand_msg_xof;
44

5-
use core::convert::TryFrom;
65
pub use expand_msg::*;
76
pub use expand_msg_xmd::*;
87
pub use expand_msg_xof::*;
8+
use generic_array::{typenum::Unsigned, ArrayLength, GenericArray};
99

1010
/// The trait for helping to convert to a scalar
11-
pub trait FromOkm<const L: usize>: Sized {
11+
pub trait FromOkm {
12+
/// The number of bytes needed to convert to a scalar
13+
type Length: ArrayLength<u8>;
14+
1215
/// Convert a byte sequence into a scalar
13-
fn from_okm(data: &[u8; L]) -> Self;
16+
fn from_okm(data: &GenericArray<u8, Self::Length>) -> Self;
1417
}
1518

1619
/// Convert an arbitrary byte sequence according to
1720
/// <https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3>
18-
pub fn hash_to_field<E, T, const L: usize, const COUNT: usize, const OUT: usize>(
19-
data: &[u8],
20-
domain: &[u8],
21-
) -> [T; COUNT]
21+
pub fn hash_to_field<E, T>(data: &[u8], domain: &'static [u8], out: &mut [T])
2222
where
23-
E: ExpandMsg<OUT>,
24-
T: FromOkm<L> + Default + Copy,
23+
E: ExpandMsg,
24+
T: FromOkm + Default,
2525
{
26-
let random_bytes = E::expand_message(data, domain);
27-
let mut out = [T::default(); COUNT];
28-
for i in 0..COUNT {
29-
let u = <[u8; L]>::try_from(&random_bytes[(L * i)..L * (i + 1)]).expect("not enough bytes");
30-
out[i] = T::from_okm(&u);
26+
let len_in_bytes = T::Length::to_usize() * out.len();
27+
let mut tmp = GenericArray::<u8, <T as FromOkm>::Length>::default();
28+
let mut expander = E::expand_message(data, domain, len_in_bytes);
29+
for o in out.iter_mut() {
30+
expander.fill_bytes(&mut tmp);
31+
*o = T::from_okm(&tmp);
3132
}
32-
out
3333
}
Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
1+
use digest_traits::{Digest, ExtendableOutputDirty, Update, XofReader};
2+
use generic_array::{ArrayLength, GenericArray};
3+
4+
/// Salt when the DST is too long
5+
const OVERSIZE_DST_SALT: &[u8] = b"H2C-OVERSIZE-DST-";
6+
/// Maximum domain separation tag length
7+
const MAX_DST_LEN: usize = 255;
8+
19
/// Trait for types implementing expand_message interface for hash_to_field
2-
pub trait ExpandMsg<const OUT: usize> {
3-
/// Expands `msg` to the required number of bytes in `buf`
4-
fn expand_message(msg: &[u8], dst: &[u8]) -> [u8; OUT];
10+
pub trait ExpandMsg {
11+
/// Expands `msg` to the required number of bytes
12+
/// Returns an expander that can be used to call `read` until enough
13+
/// bytes have been consumed
14+
fn expand_message(msg: &[u8], dst: &'static [u8], len_in_bytes: usize) -> Self;
15+
16+
/// Fill the array with the expanded bytes
17+
fn fill_bytes(&mut self, okm: &mut [u8]);
18+
}
19+
20+
/// The domain separation tag
21+
///
22+
/// Implements [section 5.4.3 of `draft-irtf-cfrg-hash-to-curve-13`][dst].
23+
///
24+
/// [dst]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-13#section-5.4.3
25+
pub(crate) enum Domain<L: ArrayLength<u8>> {
26+
/// > 255
27+
Hashed(GenericArray<u8, L>),
28+
/// <= 255
29+
Array(&'static [u8]),
30+
}
31+
32+
impl<L: ArrayLength<u8>> Domain<L> {
33+
pub fn xof<X>(dst: &'static [u8]) -> Self
34+
where
35+
X: Default + ExtendableOutputDirty + Update,
36+
{
37+
if dst.len() > MAX_DST_LEN {
38+
let mut data = GenericArray::<u8, L>::default();
39+
X::default()
40+
.chain(OVERSIZE_DST_SALT)
41+
.chain(dst)
42+
.finalize_xof_dirty()
43+
.read(&mut data);
44+
Self::Hashed(data)
45+
} else {
46+
Self::Array(dst)
47+
}
48+
}
49+
50+
pub fn xmd<X>(dst: &'static [u8]) -> Self
51+
where
52+
X: Digest<OutputSize = L>,
53+
{
54+
if dst.len() > MAX_DST_LEN {
55+
Self::Hashed(X::new().chain(OVERSIZE_DST_SALT).chain(dst).finalize())
56+
} else {
57+
Self::Array(dst)
58+
}
59+
}
60+
61+
pub fn data(&self) -> &[u8] {
62+
match self {
63+
Self::Hashed(d) => &d[..],
64+
Self::Array(d) => *d,
65+
}
66+
}
67+
68+
pub fn len(&self) -> usize {
69+
match self {
70+
Self::Hashed(_) => L::to_usize(),
71+
Self::Array(d) => d.len(),
72+
}
73+
}
574
}
Lines changed: 65 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,94 @@
1-
use super::ExpandMsg;
2-
use core::marker::PhantomData;
3-
use digest::{
1+
use super::{Domain, ExpandMsg};
2+
use digest_traits::{
43
generic_array::{typenum::Unsigned, GenericArray},
54
BlockInput, Digest,
65
};
7-
use subtle::{Choice, ConditionallySelectable};
86

97
/// Placeholder type for implementing expand_message_xmd based on a hash function
10-
#[derive(Debug)]
11-
pub struct ExpandMsgXmd<HashT> {
12-
phantom: PhantomData<HashT>,
8+
pub struct ExpandMsgXmd<HashT>
9+
where
10+
HashT: Digest + BlockInput,
11+
{
12+
b_0: GenericArray<u8, HashT::OutputSize>,
13+
b_vals: GenericArray<u8, HashT::OutputSize>,
14+
domain: Domain<HashT::OutputSize>,
15+
index: usize,
16+
offset: usize,
17+
ell: usize,
18+
}
19+
20+
impl<HashT> ExpandMsgXmd<HashT>
21+
where
22+
HashT: Digest + BlockInput,
23+
{
24+
fn next(&mut self) -> bool {
25+
if self.index < self.ell {
26+
self.index += 1;
27+
self.offset = 0;
28+
// b_0 XOR b_(idx - 1)
29+
let mut tmp = GenericArray::<u8, HashT::OutputSize>::default();
30+
self.b_0
31+
.iter()
32+
.zip(&self.b_vals[..])
33+
.enumerate()
34+
.for_each(|(j, (b0val, bi1val))| tmp[j] = b0val ^ bi1val);
35+
self.b_vals = HashT::new()
36+
.chain(tmp)
37+
.chain([self.index as u8])
38+
.chain(self.domain.data())
39+
.chain([self.domain.len() as u8])
40+
.finalize();
41+
true
42+
} else {
43+
false
44+
}
45+
}
1346
}
1447

1548
/// ExpandMsgXmd implements expand_message_xmd for the ExpandMsg trait
16-
impl<HashT, const LEN_IN_BYTES: usize> ExpandMsg<LEN_IN_BYTES> for ExpandMsgXmd<HashT>
49+
impl<HashT> ExpandMsg for ExpandMsgXmd<HashT>
1750
where
1851
HashT: Digest + BlockInput,
1952
{
20-
fn expand_message(msg: &[u8], dst: &[u8]) -> [u8; LEN_IN_BYTES] {
53+
fn expand_message(msg: &[u8], dst: &'static [u8], len_in_bytes: usize) -> Self {
2154
let b_in_bytes = HashT::OutputSize::to_usize();
22-
let ell = (LEN_IN_BYTES + b_in_bytes - 1) / b_in_bytes;
55+
let ell = (len_in_bytes + b_in_bytes - 1) / b_in_bytes;
2356
if ell > 255 {
2457
panic!("ell was too big in expand_message_xmd");
2558
}
59+
let domain = Domain::xmd::<HashT>(dst);
2660
let b_0 = HashT::new()
2761
.chain(GenericArray::<u8, HashT::BlockSize>::default())
2862
.chain(msg)
29-
.chain([(LEN_IN_BYTES >> 8) as u8, LEN_IN_BYTES as u8, 0u8])
30-
.chain(dst)
31-
.chain([dst.len() as u8])
63+
.chain([(len_in_bytes >> 8) as u8, len_in_bytes as u8, 0u8])
64+
.chain(domain.data())
65+
.chain([domain.len() as u8])
3266
.finalize();
3367

34-
let mut b_vals = HashT::new()
68+
let b_vals = HashT::new()
3569
.chain(&b_0[..])
3670
.chain([1u8])
37-
.chain(dst)
38-
.chain([dst.len() as u8])
71+
.chain(domain.data())
72+
.chain([domain.len() as u8])
3973
.finalize();
4074

41-
let mut buf = [0u8; LEN_IN_BYTES];
42-
let mut offset = 0;
75+
Self {
76+
b_0,
77+
b_vals,
78+
domain,
79+
index: 1,
80+
offset: 0,
81+
ell,
82+
}
83+
}
4384

44-
for i in 1..ell {
45-
// b_0 XOR b_(idx - 1)
46-
let mut tmp = GenericArray::<u8, HashT::OutputSize>::default();
47-
b_0.iter()
48-
.zip(&b_vals[..])
49-
.enumerate()
50-
.for_each(|(j, (b0val, bi1val))| tmp[j] = b0val ^ bi1val);
51-
for b in b_vals {
52-
buf[offset % LEN_IN_BYTES].conditional_assign(
53-
&b,
54-
Choice::from(if offset < LEN_IN_BYTES { 1 } else { 0 }),
55-
);
56-
offset += 1;
85+
fn fill_bytes(&mut self, okm: &mut [u8]) {
86+
for b in okm {
87+
if self.offset == self.b_vals.len() && !self.next() {
88+
return;
5789
}
58-
b_vals = HashT::new()
59-
.chain(tmp)
60-
.chain([(i + 1) as u8])
61-
.chain(dst)
62-
.chain([dst.len() as u8])
63-
.finalize();
64-
}
65-
for b in b_vals {
66-
buf[offset % LEN_IN_BYTES]
67-
.conditional_assign(&b, Choice::from(if offset < LEN_IN_BYTES { 1 } else { 0 }));
68-
offset += 1;
90+
*b = self.b_vals[self.offset];
91+
self.offset += 1;
6992
}
70-
buf
7193
}
7294
}
Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
use super::ExpandMsg;
2-
use core::marker::PhantomData;
3-
use digest::{ExtendableOutput, Update, XofReader};
2+
use crate::hash2field::Domain;
3+
use digest_traits::{ExtendableOutput, ExtendableOutputDirty, Update, XofReader};
4+
use generic_array::typenum::U32;
45

56
/// Placeholder type for implementing expand_message_xof based on an extendable output function
6-
#[derive(Debug)]
7-
pub struct ExpandMsgXof<HashT> {
8-
phantom: PhantomData<HashT>,
7+
pub struct ExpandMsgXof<HashT>
8+
where
9+
HashT: Default + ExtendableOutput + ExtendableOutputDirty + Update,
10+
{
11+
reader: <HashT as ExtendableOutput>::Reader,
912
}
1013

1114
/// ExpandMsgXof implements expand_message_xof for the ExpandMsg trait
12-
impl<HashT, const LEN_IN_BYTES: usize> ExpandMsg<LEN_IN_BYTES> for ExpandMsgXof<HashT>
15+
impl<HashT> ExpandMsg for ExpandMsgXof<HashT>
1316
where
14-
HashT: Default + ExtendableOutput + Update,
17+
HashT: Default + ExtendableOutput + ExtendableOutputDirty + Update,
1518
{
16-
fn expand_message(msg: &[u8], dst: &[u8]) -> [u8; LEN_IN_BYTES] {
17-
let mut buf = [0u8; LEN_IN_BYTES];
18-
let mut r = HashT::default()
19+
fn expand_message(msg: &[u8], dst: &'static [u8], len_in_bytes: usize) -> Self {
20+
let domain = Domain::<U32>::xof::<HashT>(dst);
21+
let reader = HashT::default()
1922
.chain(msg)
20-
.chain([(LEN_IN_BYTES >> 8) as u8, LEN_IN_BYTES as u8])
21-
.chain(dst)
22-
.chain([dst.len() as u8])
23+
.chain([(len_in_bytes >> 8) as u8, len_in_bytes as u8])
24+
.chain(domain.data())
25+
.chain([domain.len() as u8])
2326
.finalize_xof();
24-
r.read(&mut buf);
25-
buf
27+
Self { reader }
28+
}
29+
30+
fn fill_bytes(&mut self, okm: &mut [u8]) {
31+
self.reader.read(okm);
2632
}
2733
}

0 commit comments

Comments
 (0)