diff --git a/elliptic-curve/Cargo.toml b/elliptic-curve/Cargo.toml index 3249ec1f2..0f48d46ab 100644 --- a/elliptic-curve/Cargo.toml +++ b/elliptic-curve/Cargo.toml @@ -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"] +hash2curve = ["digest", "ff", "group"] 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"] diff --git a/elliptic-curve/src/hash2curve.rs b/elliptic-curve/src/hash2curve.rs new file mode 100644 index 000000000..5b81eca8f --- /dev/null +++ b/elliptic-curve/src/hash2curve.rs @@ -0,0 +1,14 @@ +/// Traits for handling hash to curve +mod group_digest; +/// Traits for mapping an isogeny to another curve +/// +mod isogeny; +/// Traits for mapping field elements to points on the curve +mod map2curve; +/// Optimized simplified Shallue-van de Woestijne-Ulas methods +mod osswu; + +pub use group_digest::*; +pub use isogeny::*; +pub use map2curve::*; +pub use osswu::*; diff --git a/elliptic-curve/src/hash2curve/group_digest.rs b/elliptic-curve/src/hash2curve/group_digest.rs new file mode 100644 index 000000000..736f7c935 --- /dev/null +++ b/elliptic-curve/src/hash2curve/group_digest.rs @@ -0,0 +1,69 @@ +use super::MapToCurve; +use crate::hash2field::{hash_to_field, ExpandMsg, FromOkm}; +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 + + MapToCurve; + + /// Computes the hash to curve routine according to + /// + /// 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 + /// + /// ```ignore + /// let pt = ProjectivePoint::hash_from_bytes::>(b"test data", b"CURVE_XMD:SHA-256_SSWU_RO_"); + /// ``` + /// + /// Using an extendable output function + /// + /// ```ignore + /// let pt = ProjectivePoint::hash_from_bytes::>(b"test data", b"CURVE_XOF:SHAKE-256_SSWU_RO_"); + /// ``` + /// + fn hash_from_bytes(msg: &[u8], dst: &'static [u8]) -> Self::Output { + let mut u = [Self::FieldElement::default(), Self::FieldElement::default()]; + hash_to_field::(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. + // This is caused curve coefficients being different than + // what is usually implemented. + // FieldElement expects the `a` and `b` to be the original values + // isogenies are different with curves like k256 and bls12-381. + // This problem doesn't manifest for curves with no isogeny like p256. + // For k256 and p256 clear_cofactor doesn't do anything anyway so it will be a no-op. + q0.clear_cofactor() + q1.clear_cofactor() + } + + /// Computes the encode to curve routine according to + /// + /// 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(msg: &[u8], dst: &'static [u8]) -> Self::Output { + let mut u = [Self::FieldElement::default()]; + hash_to_field::(msg, dst, &mut u); + let q0 = Self::Output::map_to_curve(u[0]); + q0.clear_cofactor() + } +} diff --git a/elliptic-curve/src/hash2curve/isogeny.rs b/elliptic-curve/src/hash2curve/isogeny.rs new file mode 100644 index 000000000..abf1da27c --- /dev/null +++ b/elliptic-curve/src/hash2curve/isogeny.rs @@ -0,0 +1,53 @@ +use core::ops::{AddAssign, Mul}; +use ff::Field; +use generic_array::{typenum::Unsigned, ArrayLength, GenericArray}; + +/// The coefficients for mapping from one isogenous curve to another +pub struct IsogenyCoefficients> { + /// The coefficients for the x numerator + pub xnum: &'static [F], + /// The coefficients for the x denominator + pub xden: &'static [F], + /// The coefficients for the y numerator + pub ynum: &'static [F], + /// The coefficients for the x denominator + pub yden: &'static [F], +} + +/// The Isogeny methods to map to another curve +pub trait Isogeny: Field + AddAssign + Mul { + /// The maximum number of coefficients + type Degree: ArrayLength; + /// The isogeny coefficients + const COEFFICIENTS: IsogenyCoefficients; + + /// Map from the isogeny points to the main curve + fn isogeny(x: Self, y: Self) -> (Self, Self) { + let mut xs = GenericArray::::default(); + xs[0] = Self::one(); + xs[1] = x; + xs[2] = x.square(); + for i in 3..Self::Degree::to_usize() { + xs[i] = xs[i - 1] * x; + } + let x_num = Self::compute_iso(&xs, Self::COEFFICIENTS.xnum); + let x_den = Self::compute_iso(&xs, Self::COEFFICIENTS.xden) + .invert() + .unwrap(); + let y_num = Self::compute_iso(&xs, Self::COEFFICIENTS.ynum) * y; + let y_den = Self::compute_iso(&xs, Self::COEFFICIENTS.yden) + .invert() + .unwrap(); + + (x_num * x_den, y_num * y_den) + } + + /// Compute the ISO transform + fn compute_iso(xxs: &[Self], k: &[Self]) -> Self { + let mut xx = Self::zero(); + for (xi, ki) in xxs.iter().zip(k.iter()) { + xx += *xi * ki; + } + xx + } +} diff --git a/elliptic-curve/src/hash2curve/map2curve.rs b/elliptic-curve/src/hash2curve/map2curve.rs new file mode 100644 index 000000000..90310166e --- /dev/null +++ b/elliptic-curve/src/hash2curve/map2curve.rs @@ -0,0 +1,12 @@ +/// Trait for converting field elements into a point +/// via a mapping method like Simplified Shallue-van de Woestijne-Ulas +/// or Elligator +pub trait MapToCurve { + /// The input values representing x and y + type FieldElement; + /// The output point + type Output; + + /// Map a field element into a point + fn map_to_curve(u: Self::FieldElement) -> Self::Output; +} diff --git a/elliptic-curve/src/osswu.rs b/elliptic-curve/src/hash2curve/osswu.rs similarity index 97% rename from elliptic-curve/src/osswu.rs rename to elliptic-curve/src/hash2curve/osswu.rs index ae8637512..e10652142 100644 --- a/elliptic-curve/src/osswu.rs +++ b/elliptic-curve/src/hash2curve/osswu.rs @@ -43,8 +43,7 @@ pub trait OsswuMap: Field + Sgn0 { let mut tv2 = tv3.square(); // tv3^2 let mut xd = tv2 + tv3; // tv3^2 + tv3 let x1n = Self::PARAMS.map_b * (xd + Self::one()); // B * (xd + 1) - let a_neg = -Self::PARAMS.map_a; - xd *= a_neg; // -A * xd + xd *= -Self::PARAMS.map_a; // -A * xd let tv = Self::PARAMS.z * Self::PARAMS.map_a; xd.conditional_assign(&tv, xd.is_zero()); diff --git a/elliptic-curve/src/hash2field.rs b/elliptic-curve/src/hash2field.rs index a68a06edc..bdf30b9a5 100644 --- a/elliptic-curve/src/hash2field.rs +++ b/elliptic-curve/src/hash2field.rs @@ -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: Sized { +pub trait FromOkm { + /// The number of bytes needed to convert to a scalar + type Length: ArrayLength; + /// Convert a byte sequence into a scalar - fn from_okm(data: &[u8; L]) -> Self; + fn from_okm(data: &GenericArray) -> Self; } /// Convert an arbitrary byte sequence according to /// -pub fn hash_to_field( - data: &[u8], - domain: &[u8], -) -> [T; COUNT] +pub fn hash_to_field(data: &[u8], domain: &'static [u8], out: &mut [T]) where - E: ExpandMsg, - T: FromOkm + 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::::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 } diff --git a/elliptic-curve/src/hash2field/expand_msg.rs b/elliptic-curve/src/hash2field/expand_msg.rs index ba08f98f7..9adfab766 100644 --- a/elliptic-curve/src/hash2field/expand_msg.rs +++ b/elliptic-curve/src/hash2field/expand_msg.rs @@ -1,5 +1,74 @@ +use digest::{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 { - /// 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> { + /// > 255 + Hashed(GenericArray), + /// <= 255 + Array(&'static [u8]), +} + +impl> Domain { + pub fn xof(dst: &'static [u8]) -> Self + where + X: Default + ExtendableOutputDirty + Update, + { + if dst.len() > MAX_DST_LEN { + let mut data = GenericArray::::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(dst: &'static [u8]) -> Self + where + X: Digest, + { + 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(), + } + } } diff --git a/elliptic-curve/src/hash2field/expand_msg_xmd.rs b/elliptic-curve/src/hash2field/expand_msg_xmd.rs index e99f8b6e0..be599fcea 100644 --- a/elliptic-curve/src/hash2field/expand_msg_xmd.rs +++ b/elliptic-curve/src/hash2field/expand_msg_xmd.rs @@ -1,72 +1,103 @@ -use super::ExpandMsg; -use core::marker::PhantomData; +use super::{Domain, ExpandMsg}; use digest::{ - generic_array::{typenum::Unsigned, GenericArray}, + generic_array::{ + typenum::{IsLess, IsLessOrEqual, Unsigned, U256}, + GenericArray, + }, BlockInput, Digest, }; -use subtle::{Choice, ConditionallySelectable}; /// Placeholder type for implementing expand_message_xmd based on a hash function -#[derive(Debug)] -pub struct ExpandMsgXmd { - phantom: PhantomData, +pub struct ExpandMsgXmd +where + HashT: Digest + BlockInput, + HashT::OutputSize: IsLess, + HashT::OutputSize: IsLessOrEqual, +{ + b_0: GenericArray, + b_vals: GenericArray, + domain: Domain, + index: usize, + offset: usize, + ell: usize, +} + +impl ExpandMsgXmd +where + HashT: Digest + BlockInput, + HashT::OutputSize: IsLess, + HashT::OutputSize: IsLessOrEqual, +{ + 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::::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 ExpandMsg for ExpandMsgXmd +impl ExpandMsg for ExpandMsgXmd where HashT: Digest + BlockInput, + HashT::OutputSize: IsLess, + HashT::OutputSize: IsLessOrEqual, { - 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; - if ell > 255 { - panic!("ell was too big in expand_message_xmd"); - } + 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::(dst); let b_0 = HashT::new() .chain(GenericArray::::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::::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 } } diff --git a/elliptic-curve/src/hash2field/expand_msg_xof.rs b/elliptic-curve/src/hash2field/expand_msg_xof.rs index 0ec1df024..371482ae4 100644 --- a/elliptic-curve/src/hash2field/expand_msg_xof.rs +++ b/elliptic-curve/src/hash2field/expand_msg_xof.rs @@ -1,27 +1,33 @@ use super::ExpandMsg; -use core::marker::PhantomData; -use digest::{ExtendableOutput, Update, XofReader}; +use crate::hash2field::Domain; +use digest::{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 { - phantom: PhantomData, +pub struct ExpandMsgXof +where + HashT: Default + ExtendableOutput + ExtendableOutputDirty + Update, +{ + reader: ::Reader, } /// ExpandMsgXof implements expand_message_xof for the ExpandMsg trait -impl ExpandMsg for ExpandMsgXof +impl ExpandMsg for ExpandMsgXof 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::::xof::(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); } } diff --git a/elliptic-curve/src/lib.rs b/elliptic-curve/src/lib.rs index d6b637f0b..b67121f20 100644 --- a/elliptic-curve/src/lib.rs +++ b/elliptic-curve/src/lib.rs @@ -94,17 +94,16 @@ pub mod ecdh; #[cfg(feature = "jwk")] mod jwk; -/// Optimized simplified Shallue-van de Woestijne-Ulas methods -#[cfg(feature = "osswu")] -#[cfg_attr(docsrs, doc(cfg(feature = "osswu")))] -pub mod osswu; - -/// Traits for computing hash to field as described in -/// -#[cfg(feature = "digest")] -#[cfg_attr(docsrs, doc(cfg(feature = "digest")))] +/// Traits for hashing to field elements +#[cfg(feature = "hash2curve")] +#[cfg_attr(docsrs, doc(cfg(feature = "hash2curve")))] pub mod hash2field; +/// Traits for hashing byte sequences to curve points +#[cfg(feature = "hash2curve")] +#[cfg_attr(docsrs, doc(cfg(feature = "hash2curve")))] +pub mod hash2curve; + pub use crate::{ error::{Error, Result}, point::{