Skip to content

Commit

Permalink
feat: add traits for group digest
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Lodder <redmike7@gmail.com>
  • Loading branch information
mikelodder7 committed Jan 6, 2022
1 parent a6f22ef commit c59e45e
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 82 deletions.
4 changes: 2 additions & 2 deletions elliptic-curve/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ zeroize = { version = "1", default-features = false }

# optional dependencies
base64ct = { version = "1", optional = true, default-features = false }
digest = { version = "0.9", optional = true, default-features = false }
digest_traits = { version = "0.9", optional = true, default-features = false, package = "digest" }
ff = { version = "0.11", optional = true, default-features = false }
group = { version = "0.11", optional = true, default-features = false }
hex-literal = { version = "0.3", optional = true }
Expand All @@ -47,10 +47,10 @@ alloc = ["der/alloc", "sec1/alloc", "zeroize/alloc"] # todo: use weak activation
arithmetic = ["ff", "group"]
bits = ["arithmetic", "ff/bits"]
dev = ["arithmetic", "hex-literal", "pem", "pkcs8"]
digest = ["digest_traits", "ff"]
ecdh = ["arithmetic"]
hazmat = []
jwk = ["alloc", "base64ct/alloc", "serde", "serde_json", "zeroize/alloc"]
osswu = ["ff"]
pem = ["alloc", "arithmetic", "pem-rfc7468/alloc", "pkcs8", "sec1/pem"]
pkcs8 = ["sec1/pkcs8"]
std = ["alloc", "rand_core/std"]
Expand Down
59 changes: 59 additions & 0 deletions elliptic-curve/src/group_digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use crate::hash2field::{hash_to_field, ExpandMsg, FromOkm};
use crate::map2curve::MapToCurve;
use group::cofactor::CofactorGroup;

/// Adds hashing arbitrary byte sequences to a valid group element
pub trait GroupDigest {
/// The field element representation for a group value with multiple elements
type FieldElement: FromOkm + Default + Copy;
/// The resulting group element
type Output: CofactorGroup<Subgroup = Self::Output>
+ MapToCurve<FieldElement = Self::FieldElement, Output = Self::Output>;

/// Computes the hash to curve routine according to
/// <https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html>
/// which says
/// Uniform encoding from byte strings to points in G.
/// That is, the distribution of its output is statistically close
/// to uniform in G.
/// This function is suitable for most applications requiring a random
/// oracle returning points in G assuming a cryptographically secure
/// hash function is used.
///
/// Examples
///
/// Using a fixed size hash function
///
/// let pt = ProjectivePoint::hash_from_bytes::<hash2field::ExpandMsgXmd<sha2::Sha256>>(b"test data", b"CURVE_XMD:SHA-256_SSWU_RO_");
///
/// Using an extendable output function
///
/// let pt = ProjectivePoint::hash_from_bytes::<hash2field::ExpandMsgXof<sha3::Shake256>>(b"test data", b"CURVE_XOF:SHAKE-256_SSWU_RO_");
///
fn hash_from_bytes<X: ExpandMsg>(msg: &[u8], dst: &'static [u8]) -> Self::Output {
let mut u = [Self::FieldElement::default(), Self::FieldElement::default()];
hash_to_field::<X, _>(msg, dst, &mut u);
let q0 = Self::Output::map_to_curve(u[0]);
let q1 = Self::Output::map_to_curve(u[1]);
// Ideally we could add and then clear cofactor once
// thus saving a call but the field elements may not
// add properly due to the underlying implementation
// which could result in an incorrect subgroup
q0.clear_cofactor() + q1.clear_cofactor()
}

/// Computes the encode to curve routine according to
/// <https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html>
/// which says
/// Nonuniform encoding from byte strings to
/// points in G. That is, the distribution of its output is not
/// uniformly random in G: the set of possible outputs of
/// encode_to_curve is only a fraction of the points in G, and some
/// points in this set are more likely to be output than others.
fn encode_from_bytes<X: ExpandMsg>(msg: &[u8], dst: &'static [u8]) -> Self::Output {
let mut u = [Self::FieldElement::default()];
hash_to_field::<X, _>(msg, dst, &mut u);
let q0 = Self::Output::map_to_curve(u[0]);
q0.clear_cofactor()
}
}
30 changes: 15 additions & 15 deletions elliptic-curve/src/hash2field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@ mod expand_msg;
mod expand_msg_xmd;
mod expand_msg_xof;

use core::convert::TryFrom;
pub use expand_msg::*;
pub use expand_msg_xmd::*;
pub use expand_msg_xof::*;
use generic_array::{typenum::Unsigned, ArrayLength, GenericArray};

/// The trait for helping to convert to a scalar
pub trait FromOkm<const L: usize>: Sized {
pub trait FromOkm {
/// The number of bytes needed to convert to a scalar
type Length: ArrayLength<u8>;

/// Convert a byte sequence into a scalar
fn from_okm(data: &[u8; L]) -> Self;
fn from_okm(data: &GenericArray<u8, Self::Length>) -> Self;
}

/// Convert an arbitrary byte sequence according to
/// <https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3>
pub fn hash_to_field<E, T, const L: usize, const COUNT: usize, const OUT: usize>(
data: &[u8],
domain: &[u8],
) -> [T; COUNT]
pub fn hash_to_field<E, T>(data: &[u8], domain: &'static [u8], out: &mut [T])
where
E: ExpandMsg<OUT>,
T: FromOkm<L> + Default + Copy,
E: ExpandMsg,
T: FromOkm + Default,
{
let random_bytes = E::expand_message(data, domain);
let mut out = [T::default(); COUNT];
for i in 0..COUNT {
let u = <[u8; L]>::try_from(&random_bytes[(L * i)..L * (i + 1)]).expect("not enough bytes");
out[i] = T::from_okm(&u);
let len_in_bytes = T::Length::to_usize() * out.len();
let mut tmp = GenericArray::<u8, <T as FromOkm>::Length>::default();
let mut expander = E::expand_message(data, domain, len_in_bytes);
for o in out.iter_mut() {
expander.fill_bytes(&mut tmp);
*o = T::from_okm(&tmp);
}
out
}
75 changes: 72 additions & 3 deletions elliptic-curve/src/hash2field/expand_msg.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
use digest_traits::{Digest, ExtendableOutputDirty, Update, XofReader};
use generic_array::{ArrayLength, GenericArray};

/// Salt when the DST is too long
const OVERSIZE_DST_SALT: &[u8] = b"H2C-OVERSIZE-DST-";
/// Maximum domain separation tag length
const MAX_DST_LEN: usize = 255;

/// Trait for types implementing expand_message interface for hash_to_field
pub trait ExpandMsg<const OUT: usize> {
/// Expands `msg` to the required number of bytes in `buf`
fn expand_message(msg: &[u8], dst: &[u8]) -> [u8; OUT];
pub trait ExpandMsg {
/// Expands `msg` to the required number of bytes
/// Returns an expander that can be used to call `read` until enough
/// bytes have been consumed
fn expand_message(msg: &[u8], dst: &'static [u8], len_in_bytes: usize) -> Self;

/// Fill the array with the expanded bytes
fn fill_bytes(&mut self, okm: &mut [u8]);
}

/// The domain separation tag
///
/// Implements [section 5.4.3 of `draft-irtf-cfrg-hash-to-curve-13`][dst].
///
/// [dst]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-13#section-5.4.3
pub(crate) enum Domain<L: ArrayLength<u8>> {
/// > 255
Hashed(GenericArray<u8, L>),
/// <= 255
Array(&'static [u8]),
}

impl<L: ArrayLength<u8>> Domain<L> {
pub fn xof<X>(dst: &'static [u8]) -> Self
where
X: Default + ExtendableOutputDirty + Update,
{
if dst.len() > MAX_DST_LEN {
let mut data = GenericArray::<u8, L>::default();
X::default()
.chain(OVERSIZE_DST_SALT)
.chain(dst)
.finalize_xof_dirty()
.read(&mut data);
Self::Hashed(data)
} else {
Self::Array(dst)
}
}

pub fn xmd<X>(dst: &'static [u8]) -> Self
where
X: Digest<OutputSize = L>,
{
if dst.len() > MAX_DST_LEN {
Self::Hashed(X::new().chain(OVERSIZE_DST_SALT).chain(dst).finalize())
} else {
Self::Array(dst)
}
}

pub fn data(&self) -> &[u8] {
match self {
Self::Hashed(d) => &d[..],
Self::Array(d) => *d,
}
}

pub fn len(&self) -> usize {
match self {
Self::Hashed(_) => L::to_usize(),
Self::Array(d) => d.len(),
}
}
}
108 changes: 65 additions & 43 deletions elliptic-curve/src/hash2field/expand_msg_xmd.rs
Original file line number Diff line number Diff line change
@@ -1,72 +1,94 @@
use super::ExpandMsg;
use core::marker::PhantomData;
use digest::{
use super::{Domain, ExpandMsg};
use digest_traits::{
generic_array::{typenum::Unsigned, GenericArray},
BlockInput, Digest,
};
use subtle::{Choice, ConditionallySelectable};

/// Placeholder type for implementing expand_message_xmd based on a hash function
#[derive(Debug)]
pub struct ExpandMsgXmd<HashT> {
phantom: PhantomData<HashT>,
pub struct ExpandMsgXmd<HashT>
where
HashT: Digest + BlockInput,
{
b_0: GenericArray<u8, HashT::OutputSize>,
b_vals: GenericArray<u8, HashT::OutputSize>,
domain: Domain<HashT::OutputSize>,
index: usize,
offset: usize,
ell: usize,
}

impl<HashT> ExpandMsgXmd<HashT>
where
HashT: Digest + BlockInput,
{
fn next(&mut self) -> bool {
if self.index < self.ell {
self.index += 1;
self.offset = 0;
// b_0 XOR b_(idx - 1)
let mut tmp = GenericArray::<u8, HashT::OutputSize>::default();
self.b_0
.iter()
.zip(&self.b_vals[..])
.enumerate()
.for_each(|(j, (b0val, bi1val))| tmp[j] = b0val ^ bi1val);
self.b_vals = HashT::new()
.chain(tmp)
.chain([self.index as u8])
.chain(self.domain.data())
.chain([self.domain.len() as u8])
.finalize();
true
} else {
false
}
}
}

/// ExpandMsgXmd implements expand_message_xmd for the ExpandMsg trait
impl<HashT, const LEN_IN_BYTES: usize> ExpandMsg<LEN_IN_BYTES> for ExpandMsgXmd<HashT>
impl<HashT> ExpandMsg for ExpandMsgXmd<HashT>
where
HashT: Digest + BlockInput,
{
fn expand_message(msg: &[u8], dst: &[u8]) -> [u8; LEN_IN_BYTES] {
fn expand_message(msg: &[u8], dst: &'static [u8], len_in_bytes: usize) -> Self {
let b_in_bytes = HashT::OutputSize::to_usize();
let ell = (LEN_IN_BYTES + b_in_bytes - 1) / b_in_bytes;
let ell = (len_in_bytes + b_in_bytes - 1) / b_in_bytes;
if ell > 255 {
panic!("ell was too big in expand_message_xmd");
}
let domain = Domain::xmd::<HashT>(dst);
let b_0 = HashT::new()
.chain(GenericArray::<u8, HashT::BlockSize>::default())
.chain(msg)
.chain([(LEN_IN_BYTES >> 8) as u8, LEN_IN_BYTES as u8, 0u8])
.chain(dst)
.chain([dst.len() as u8])
.chain([(len_in_bytes >> 8) as u8, len_in_bytes as u8, 0u8])
.chain(domain.data())
.chain([domain.len() as u8])
.finalize();

let mut b_vals = HashT::new()
let b_vals = HashT::new()
.chain(&b_0[..])
.chain([1u8])
.chain(dst)
.chain([dst.len() as u8])
.chain(domain.data())
.chain([domain.len() as u8])
.finalize();

let mut buf = [0u8; LEN_IN_BYTES];
let mut offset = 0;
Self {
b_0,
b_vals,
domain,
index: 1,
offset: 0,
ell,
}
}

for i in 1..ell {
// b_0 XOR b_(idx - 1)
let mut tmp = GenericArray::<u8, HashT::OutputSize>::default();
b_0.iter()
.zip(&b_vals[..])
.enumerate()
.for_each(|(j, (b0val, bi1val))| tmp[j] = b0val ^ bi1val);
for b in b_vals {
buf[offset % LEN_IN_BYTES].conditional_assign(
&b,
Choice::from(if offset < LEN_IN_BYTES { 1 } else { 0 }),
);
offset += 1;
fn fill_bytes(&mut self, okm: &mut [u8]) {
for b in okm {
if self.offset == self.b_vals.len() && !self.next() {
return;
}
b_vals = HashT::new()
.chain(tmp)
.chain([(i + 1) as u8])
.chain(dst)
.chain([dst.len() as u8])
.finalize();
}
for b in b_vals {
buf[offset % LEN_IN_BYTES]
.conditional_assign(&b, Choice::from(if offset < LEN_IN_BYTES { 1 } else { 0 }));
offset += 1;
*b = self.b_vals[self.offset];
self.offset += 1;
}
buf
}
}
36 changes: 21 additions & 15 deletions elliptic-curve/src/hash2field/expand_msg_xof.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
use super::ExpandMsg;
use core::marker::PhantomData;
use digest::{ExtendableOutput, Update, XofReader};
use crate::hash2field::Domain;
use digest_traits::{ExtendableOutput, ExtendableOutputDirty, Update, XofReader};
use generic_array::typenum::U32;

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

/// ExpandMsgXof implements expand_message_xof for the ExpandMsg trait
impl<HashT, const LEN_IN_BYTES: usize> ExpandMsg<LEN_IN_BYTES> for ExpandMsgXof<HashT>
impl<HashT> ExpandMsg for ExpandMsgXof<HashT>
where
HashT: Default + ExtendableOutput + Update,
HashT: Default + ExtendableOutput + ExtendableOutputDirty + Update,
{
fn expand_message(msg: &[u8], dst: &[u8]) -> [u8; LEN_IN_BYTES] {
let mut buf = [0u8; LEN_IN_BYTES];
let mut r = HashT::default()
fn expand_message(msg: &[u8], dst: &'static [u8], len_in_bytes: usize) -> Self {
let domain = Domain::<U32>::xof::<HashT>(dst);
let reader = HashT::default()
.chain(msg)
.chain([(LEN_IN_BYTES >> 8) as u8, LEN_IN_BYTES as u8])
.chain(dst)
.chain([dst.len() as u8])
.chain([(len_in_bytes >> 8) as u8, len_in_bytes as u8])
.chain(domain.data())
.chain([domain.len() as u8])
.finalize_xof();
r.read(&mut buf);
buf
Self { reader }
}

fn fill_bytes(&mut self, okm: &mut [u8]) {
self.reader.read(okm);
}
}
Loading

0 comments on commit c59e45e

Please sign in to comment.