Skip to content
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

Group trait overhaul #52

Merged
merged 9 commits into from
Jan 18, 2022
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ p256 = [
"once_cell",
"p256_",
]
ristretto255 = []
ristretto255 = ["generic-array/more_lengths"]
ristretto255_fiat_u32 = ["curve25519-dalek/fiat_u32_backend", "ristretto255"]
ristretto255_fiat_u64 = ["curve25519-dalek/fiat_u64_backend", "ristretto255"]
ristretto255_simd = ["curve25519-dalek/simd_backend", "ristretto255"]
Expand Down
4 changes: 2 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ pub enum Error {
ProofVerificationError,
/// Encountered insufficient bytes when attempting to deserialize
SizeError,
/// Encountered a zero scalar
ZeroScalarError,
/// Encountered an invalid scalar
ScalarError,
}

#[cfg(feature = "std")]
Expand Down
110 changes: 56 additions & 54 deletions src/group/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,82 @@
// License, Version 2.0 found in the LICENSE-APACHE file in the root directory
// of this source tree.

use core::ops::Add;
use core::convert::TryFrom;

use digest::core_api::BlockSizeUser;
use digest::core_api::{Block, BlockSizeUser};
use digest::{Digest, FixedOutputReset};
use generic_array::sequence::Concat;
use generic_array::typenum::{Unsigned, U1, U2};
use generic_array::typenum::{IsLess, NonZero, Unsigned, U65536};
use generic_array::{ArrayLength, GenericArray};

use crate::util::i2osp;
use crate::{Error, Result};

// Computes ceil(x / y)
fn div_ceil(x: usize, y: usize) -> usize {
let additive = (x % y != 0) as usize;
x / y + additive
}

fn xor<L: ArrayLength<u8>>(x: GenericArray<u8, L>, y: GenericArray<u8, L>) -> GenericArray<u8, L> {
x.into_iter().zip(y).map(|(x1, x2)| x1 ^ x2).collect()
}

/// Corresponds to the expand_message_xmd() function defined in
/// <https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-10.txt>
pub fn expand_message_xmd<
'a,
H: BlockSizeUser + Digest + FixedOutputReset,
L: ArrayLength<u8>,
M: IntoIterator<Item = &'a [u8]>,
D: ArrayLength<u8> + Add<U1>,
>(
msg: M,
dst: GenericArray<u8, D>,
pub fn expand_message_xmd<H: BlockSizeUser + Digest + FixedOutputReset, L: ArrayLength<u8>>(
msg: &[&[u8]],
dst: &[u8],
) -> Result<GenericArray<u8, L>>
where
<D as Add<U1>>::Output: ArrayLength<u8>,
// Constraint set by `expand_message_xmd`:
// https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-13.html#section-5.4.1-6
L: NonZero + IsLess<U65536>,
{
let digest_len = H::OutputSize::USIZE;
let ell = div_ceil(L::USIZE, digest_len);
if ell > 255 {
// DST, a byte string of at most 255 bytes.
let dst_len = u8::try_from(dst.len()).map_err(|_| Error::HashToCurveError)?;

// b_in_bytes, b / 8 for b the output size of H in bits.
let b_in_bytes = H::OutputSize::to_usize();

// 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
if b_in_bytes > H::BlockSize::USIZE {
return Err(Error::HashToCurveError);
}
let dst_prime = dst.concat(i2osp::<U1>(D::USIZE)?);
let z_pad = i2osp::<H::BlockSize>(0)?;
let l_i_b_str = i2osp::<U2>(L::USIZE)?;

let mut h = H::new();
// ell = ceil(len_in_bytes / b_in_bytes)
// ABORT if ell > 255
let ell = u8::try_from((L::USIZE + b_in_bytes - 1) / b_in_bytes)
.map_err(|_| Error::HashToCurveError)?;

let mut hash = H::new();

// b_0 = H(msg_prime)
// msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime
Digest::update(&mut h, z_pad);
for bytes in msg {
Digest::update(&mut h, bytes)
// Z_pad = I2OSP(0, s_in_bytes)
// s_in_bytes, the input block size of H, measured in bytes
Digest::update(&mut hash, Block::<H>::default());
for msg in msg {
Digest::update(&mut hash, msg);
}
Digest::update(&mut h, l_i_b_str);
Digest::update(&mut h, i2osp::<U1>(0)?);
Digest::update(&mut h, &dst_prime);
// l_i_b_str = I2OSP(len_in_bytes, 2)
Digest::update(&mut hash, L::U16.to_be_bytes());
Digest::update(&mut hash, [0]);
// DST_prime = DST || I2OSP(len(DST), 1)
Digest::update(&mut hash, dst);
Digest::update(&mut hash, [dst_len]);
let b_0 = hash.finalize_reset();

// b[0]
let b_0 = h.finalize_reset();
let mut b_i = GenericArray::default();

let mut uniform_bytes = GenericArray::default();

for (i, chunk) in (1..(ell + 1)).zip(uniform_bytes.chunks_mut(digest_len)) {
Digest::update(&mut h, xor(b_0.clone(), b_i.clone()));
Digest::update(&mut h, i2osp::<U1>(i)?);
Digest::update(&mut h, &dst_prime);
b_i = h.finalize_reset();
chunk.copy_from_slice(&b_i[..digest_len.min(chunk.len())]);
// b_1 = H(b_0 || I2OSP(1, 1) || DST_prime)
// for i in (2, ..., ell):
for (i, chunk) in (1..(ell + 1)).zip(uniform_bytes.chunks_mut(b_in_bytes)) {
// b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime)
Digest::update(&mut hash, xor(b_0.clone(), b_i.clone()));
Digest::update(&mut hash, [i]);
// DST_prime = DST || I2OSP(len(DST), 1)
Digest::update(&mut hash, dst);
Digest::update(&mut hash, [dst_len]);
b_i = hash.finalize_reset();
// uniform_bytes = b_1 || ... || b_ell
// return substr(uniform_bytes, 0, len_in_bytes)
chunk.copy_from_slice(&b_i[..b_in_bytes.min(chunk.len())]);
}

Ok(uniform_bytes)
Expand All @@ -81,7 +89,6 @@ where
#[cfg(test)]
mod tests {
use generic_array::typenum::{U128, U32};
use generic_array::GenericArray;

struct Params {
msg: &'static str,
Expand All @@ -91,6 +98,8 @@ mod tests {

#[test]
fn test_expand_message_xmd() {
const DST: [u8; 27] = *b"QUUX-V01-CS02-with-expander";

// Test vectors taken from Section K.1 of https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-10.txt
let test_vectors: alloc::vec::Vec<Params> = alloc::vec![
Params {
Expand Down Expand Up @@ -190,20 +199,13 @@ mod tests {
378fba044a31f5cb44583a892f5969dcd73b3fa128816e",
},
];
let dst = GenericArray::from(*b"QUUX-V01-CS02-with-expander");

for tv in test_vectors {
let uniform_bytes = match tv.len_in_bytes {
32 => super::expand_message_xmd::<sha2::Sha256, U32, _, _>(
Some(tv.msg.as_bytes()),
dst,
)
.map(|bytes| bytes.to_vec()),
128 => super::expand_message_xmd::<sha2::Sha256, U128, _, _>(
Some(tv.msg.as_bytes()),
dst,
)
.map(|bytes| bytes.to_vec()),
32 => super::expand_message_xmd::<sha2::Sha256, U32>(&[tv.msg.as_bytes()], &DST)
.map(|bytes| bytes.to_vec()),
128 => super::expand_message_xmd::<sha2::Sha256, U128>(&[tv.msg.as_bytes()], &DST)
.map(|bytes| bytes.to_vec()),
_ => unimplemented!(),
}
.unwrap();
Expand Down
135 changes: 48 additions & 87 deletions src/group/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,36 @@ use core::ops::{Add, Mul, Sub};

use digest::core_api::BlockSizeUser;
use digest::{Digest, FixedOutputReset};
use generic_array::typenum::U1;
use generic_array::{ArrayLength, GenericArray};
use rand_core::{CryptoRng, RngCore};
#[cfg(feature = "ristretto255")]
pub use ristretto::Ristretto255;
use subtle::ConstantTimeEq;
use zeroize::Zeroize;

use crate::{Error, Result};
use crate::voprf::Mode;
use crate::Result;

pub(crate) const STR_HASH_TO_SCALAR: [u8; 13] = *b"HashToScalar-";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if neither the p256 or ristretto255 feature is enabled, then these strings won't get used. Perhaps we could either just make them pub, or cfg-if on the features?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna handle this in a follow-up when we actually remove the P-256 implementation and replace it with a generic one over traits of elliptic-curve, then they are always used.

pub(crate) const STR_HASH_TO_GROUP: [u8; 12] = *b"HashToGroup-";

/// A prime-order subgroup of a base field (EC, prime-order field ...). This
/// subgroup is noted additively — as in the draft RFC — in this trait.
pub trait Group:
Copy
+ Sized
+ ConstantTimeEq
+ for<'a> Mul<&'a <Self as Group>::Scalar, Output = Self>
+ for<'a> Add<&'a Self, Output = Self>
{
pub trait Group {
/// The ciphersuite identifier as dictated by
/// <https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-05.txt>
const SUITE_ID: usize;
const SUITE_ID: u16;

/// transforms a password and domain separation tag (DST) into a curve point
fn hash_to_curve<H: BlockSizeUser + Digest + FixedOutputReset, D: ArrayLength<u8> + Add<U1>>(
msg: &[u8],
dst: GenericArray<u8, D>,
) -> Result<Self>
where
<D as Add<U1>>::Output: ArrayLength<u8>;
/// The type of group elements
type Elem: Copy
+ Sized
+ ConstantTimeEq
+ Zeroize
+ for<'a> Mul<&'a Self::Scalar, Output = Self::Elem>
+ for<'a> Add<&'a Self::Elem, Output = Self::Elem>;

/// Hashes a slice of pseudo-random bytes to a scalar
fn hash_to_scalar<
'a,
H: BlockSizeUser + Digest + FixedOutputReset,
D: ArrayLength<u8> + Add<U1>,
I: IntoIterator<Item = &'a [u8]>,
>(
input: I,
dst: GenericArray<u8, D>,
) -> Result<Self::Scalar>
where
<D as Add<U1>>::Output: ArrayLength<u8>;
/// The byte length necessary to represent group elements
type ElemLen: ArrayLength<u8> + 'static;

/// The type of base field scalars
type Scalar: Zeroize
Expand All @@ -67,79 +56,51 @@ pub trait Group:
+ for<'a> Add<&'a Self::Scalar, Output = Self::Scalar>
+ for<'a> Sub<&'a Self::Scalar, Output = Self::Scalar>
+ for<'a> Mul<&'a Self::Scalar, Output = Self::Scalar>;

/// The byte length necessary to represent scalars
type ScalarLen: ArrayLength<u8> + 'static;

/// Return a scalar from its fixed-length bytes representation, without
/// checking if the scalar is zero.
fn from_scalar_slice_unchecked(
scalar_bits: &GenericArray<u8, Self::ScalarLen>,
) -> Result<Self::Scalar>;
/// transforms a password and domain separation tag (DST) into a curve point
fn hash_to_curve<H: BlockSizeUser + Digest + FixedOutputReset>(
msg: &[&[u8]],
mode: Mode,
) -> Result<Self::Elem>;

/// Return a scalar from its fixed-length bytes representation. If the
/// scalar is zero, then return an error.
fn from_scalar_slice<'a>(
scalar_bits: impl Into<&'a GenericArray<u8, Self::ScalarLen>>,
) -> Result<Self::Scalar> {
let scalar = Self::from_scalar_slice_unchecked(scalar_bits.into())?;
if scalar.ct_eq(&Self::scalar_zero()).into() {
return Err(Error::ZeroScalarError);
}
Ok(scalar)
}
/// Hashes a slice of pseudo-random bytes to a scalar
fn hash_to_scalar<H: BlockSizeUser + Digest + FixedOutputReset>(
input: &[&[u8]],
mode: Mode,
) -> Result<Self::Scalar>;

/// picks a scalar at random
fn random_nonzero_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Scalar;
/// Serializes a scalar to bytes
fn scalar_as_bytes(scalar: Self::Scalar) -> GenericArray<u8, Self::ScalarLen>;
/// The multiplicative inverse of this scalar
fn scalar_invert(scalar: &Self::Scalar) -> Self::Scalar;
/// Get the base point for the group
fn base_elem() -> Self::Elem;

/// The byte length necessary to represent group elements
type ElemLen: ArrayLength<u8> + 'static;
/// Returns the identity group element
fn identity_elem() -> Self::Elem;

/// Return an element from its fixed-length bytes representation. This is
/// the unchecked version, which does not check for deserializing the
/// identity element
fn from_element_slice_unchecked(element_bits: &GenericArray<u8, Self::ElemLen>)
-> Result<Self>;
/// Serializes the `self` group element
fn serialize_elem(elem: Self::Elem) -> GenericArray<u8, Self::ElemLen>;

/// Return an element from its fixed-length bytes representation. If the
/// element is the identity element, return an error.
fn from_element_slice<'a>(
element_bits: impl Into<&'a GenericArray<u8, Self::ElemLen>>,
) -> Result<Self> {
let elem = Self::from_element_slice_unchecked(element_bits.into())?;

if Self::ct_eq(&elem, &<Self as Group>::identity()).into() {
// found the identity element
return Err(Error::PointError);
}

Ok(elem)
}

/// Serializes the `self` group element
fn to_arr(&self) -> GenericArray<u8, Self::ElemLen>;
fn deserialize_elem(element_bits: &GenericArray<u8, Self::ElemLen>) -> Result<Self::Elem>;

/// Get the base point for the group
fn base_point() -> Self;

/// Returns if the group element is equal to the identity (1)
fn is_identity(&self) -> bool {
self.ct_eq(&<Self as Group>::identity()).into()
}
/// picks a scalar at random
fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Scalar;

/// Returns the identity group element
fn identity() -> Self;
/// The multiplicative inverse of this scalar
fn invert_scalar(scalar: Self::Scalar) -> Self::Scalar;

/// Returns the scalar representing zero
fn scalar_zero() -> Self::Scalar;
#[cfg(test)]
fn zero_scalar() -> Self::Scalar;

/// Serializes a scalar to bytes
fn serialize_scalar(scalar: Self::Scalar) -> GenericArray<u8, Self::ScalarLen>;

/// Set the contents of self to the identity value
fn zeroize(&mut self) {
*self = <Self as Group>::identity();
}
/// Return a scalar from its fixed-length bytes representation. If the
/// scalar is zero or invalid, then return an error.
fn deserialize_scalar(scalar_bits: &GenericArray<u8, Self::ScalarLen>) -> Result<Self::Scalar>;
}

#[cfg(test)]
Expand Down
Loading