diff --git a/curve25519-dalek/Cargo.toml b/curve25519-dalek/Cargo.toml index dc3b2b4e..e0c0a057 100644 --- a/curve25519-dalek/Cargo.toml +++ b/curve25519-dalek/Cargo.toml @@ -27,13 +27,14 @@ rustdoc-args = [ "--html-in-header", "docs/assets/rustdoc-include-katex-header.html", "--cfg", "docsrs", ] -features = ["serde", "rand_core", "digest", "legacy_compatibility", "group-bits"] +features = ["serde", "rand_core", "elligator2", "digest", "legacy_compatibility", "group-bits"] [dev-dependencies] sha2 = { version = "0.10", default-features = false } bincode = "1" criterion = { version = "0.5", features = ["html_reports"] } hex = "0.4.2" +json = "0.12.4" rand = "0.8" rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } @@ -68,6 +69,8 @@ precomputed-tables = [] legacy_compatibility = [] group = ["dep:group", "rand_core"] group-bits = ["group", "ff/bits"] +elligator2 = [] +digest = ["dep:digest", "elligator2"] [target.'cfg(all(not(curve25519_dalek_backend = "fiat"), not(curve25519_dalek_backend = "serial"), target_arch = "x86_64"))'.dependencies] curve25519-dalek-derive = { version = "0.1", path = "../curve25519-dalek-derive" } diff --git a/curve25519-dalek/README.md b/curve25519-dalek/README.md index c918d695..c028a6e1 100644 --- a/curve25519-dalek/README.md +++ b/curve25519-dalek/README.md @@ -56,6 +56,7 @@ curve25519-dalek = ">= 4.0, < 4.2" | `serde` | | Enables `serde` serialization/deserialization for all the point and scalar types. | | `legacy_compatibility`| | Enables `Scalar::from_bits`, which allows the user to build unreduced scalars whose arithmetic is broken. Do not use this unless you know what you're doing. | | `group` | | Enables external `group` and `ff` crate traits | +| `elligator2` | | Enables elligator2 functionality for supported types. This allows curve points to be encoded to uniform random representatives, and 32 byte values to be mapped (back) to curve points. | To disable the default features when using `curve25519-dalek` as a dependency, add `default-features = false` to the dependency in your `Cargo.toml`. To diff --git a/curve25519-dalek/src/backend/serial/u32/constants.rs b/curve25519-dalek/src/backend/serial/u32/constants.rs index 03e094e4..c1a1f2b3 100644 --- a/curve25519-dalek/src/backend/serial/u32/constants.rs +++ b/curve25519-dalek/src/backend/serial/u32/constants.rs @@ -71,12 +71,14 @@ pub(crate) const SQRT_M1: FieldElement2625 = FieldElement2625::from_limbs([ pub(crate) const APLUS2_OVER_FOUR: FieldElement2625 = FieldElement2625::from_limbs([121666, 0, 0, 0, 0, 0, 0, 0, 0, 0]); +#[cfg(feature = "elligator2")] /// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation /// for Curve25519 in its Montgomery form. (This is used internally within the /// Elligator map.) pub(crate) const MONTGOMERY_A: FieldElement2625 = FieldElement2625::from_limbs([486662, 0, 0, 0, 0, 0, 0, 0, 0, 0]); +#[cfg(feature = "elligator2")] /// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the /// Elligator map.) pub(crate) const MONTGOMERY_A_NEG: FieldElement2625 = FieldElement2625::from_limbs([ diff --git a/curve25519-dalek/src/backend/serial/u32/field.rs b/curve25519-dalek/src/backend/serial/u32/field.rs index 7319288a..a2ffe264 100644 --- a/curve25519-dalek/src/backend/serial/u32/field.rs +++ b/curve25519-dalek/src/backend/serial/u32/field.rs @@ -601,4 +601,24 @@ impl FieldElement2625 { } FieldElement2625::reduce(coeffs) } + + /// Returns 1 if self is greater than the other and 0 otherwise + // implementation based on C libgmp -> mpn_sub_n + pub(crate) fn gt(&self, other: &Self) -> Choice { + let mut _ul = 0_u32; + let mut _vl = 0_u32; + let mut _rl = 0_u32; + + let mut cy = 0_u32; + for i in 0..10 { + _ul = self.0[i]; + _vl = other.0[i]; + + let (_sl, _cy1) = _ul.overflowing_sub(_vl); + let (_rl, _cy2) = _sl.overflowing_sub(cy); + cy = _cy1 as u32 | _cy2 as u32; + } + + Choice::from((cy != 0_u32) as u8) + } } diff --git a/curve25519-dalek/src/backend/serial/u64/constants.rs b/curve25519-dalek/src/backend/serial/u64/constants.rs index baeb1dd5..cd9520e3 100644 --- a/curve25519-dalek/src/backend/serial/u64/constants.rs +++ b/curve25519-dalek/src/backend/serial/u64/constants.rs @@ -98,11 +98,13 @@ pub(crate) const SQRT_M1: FieldElement51 = FieldElement51::from_limbs([ pub(crate) const APLUS2_OVER_FOUR: FieldElement51 = FieldElement51::from_limbs([121666, 0, 0, 0, 0]); +#[cfg(feature = "elligator2")] /// `MONTGOMERY_A` is equal to 486662, which is a constant of the curve equation /// for Curve25519 in its Montgomery form. (This is used internally within the /// Elligator map.) pub(crate) const MONTGOMERY_A: FieldElement51 = FieldElement51::from_limbs([486662, 0, 0, 0, 0]); +#[cfg(feature = "elligator2")] /// `MONTGOMERY_A_NEG` is equal to -486662. (This is used internally within the /// Elligator map.) pub(crate) const MONTGOMERY_A_NEG: FieldElement51 = FieldElement51::from_limbs([ diff --git a/curve25519-dalek/src/backend/serial/u64/field.rs b/curve25519-dalek/src/backend/serial/u64/field.rs index 1263d23e..673f5f92 100644 --- a/curve25519-dalek/src/backend/serial/u64/field.rs +++ b/curve25519-dalek/src/backend/serial/u64/field.rs @@ -572,4 +572,24 @@ impl FieldElement51 { square } + + /// Returns 1 if self is greater than the other and 0 otherwise + // implementation based on C libgmp -> mpn_sub_n + pub(crate) fn gt(&self, other: &Self) -> Choice { + let mut _ul = 0_u64; + let mut _vl = 0_u64; + let mut _rl = 0_u64; + + let mut cy = 0_u64; + for i in 0..5 { + _ul = self.0[i]; + _vl = other.0[i]; + + let (_sl, _cy1) = _ul.overflowing_sub(_vl); + let (_rl, _cy2) = _sl.overflowing_sub(cy); + cy = _cy1 as u64 | _cy2 as u64; + } + + Choice::from((cy != 0_u64) as u8) + } } diff --git a/curve25519-dalek/src/edwards.rs b/curve25519-dalek/src/edwards.rs index 856fac12..7392a571 100644 --- a/curve25519-dalek/src/edwards.rs +++ b/curve25519-dalek/src/edwards.rs @@ -103,6 +103,8 @@ use core::ops::{Mul, MulAssign}; use cfg_if::cfg_if; +#[cfg(feature = "digest")] +use crate::elligator2::map_to_point; #[cfg(feature = "digest")] use digest::{generic_array::typenum::U64, Digest}; @@ -596,15 +598,67 @@ impl EdwardsPoint { let sign_bit = (res[31] & 0x80) >> 7; - let fe = FieldElement::from_bytes(&res); - - let M1 = crate::montgomery::elligator_encode(&fe); - let E1_opt = M1.to_edwards(sign_bit); + let fe1 = map_to_point(&res); + let E1_opt = fe1.to_edwards(sign_bit); E1_opt .expect("Montgomery conversion to Edwards point in Elligator failed") .mul_by_cofactor() } + + #[cfg(elligator2)] + /// Build an [`EdwardsPoint`] using the birational mapping from (the + /// extended `(u, v)` form of) a montgomery point. + pub fn from_uv(u: &[u8; 32], v: &[u8; 32]) -> EdwardsPoint { + let u_fe = FieldElement::from_bytes(u); + let v_fe = FieldElement::from_bytes(v); + let (x, y) = Self::new_edwards_point(&u_fe, &v_fe); + Self::from_xy(x, y) + } + + #[cfg(elligator2)] + fn new_edwards_point(u: &FieldElement, v: &FieldElement) -> (FieldElement, FieldElement) { + // Per RFC 7748: (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1)) + + let two = &FieldElement::ONE + &FieldElement::ONE; + let (_, sqrt_neg_a_plus_two) = + FieldElement::sqrt_ratio_i(&(&MONTGOMERY_A_NEG + &two), &FieldElement::ONE); + + let mut x = &(u * &v.invert()) * &sqrt_neg_a_plus_two; + + let u_plus_one = u + &FieldElement::ONE; + let u_minus_one = u - &FieldElement::ONE; + + let mut y = &u_minus_one * &u_plus_one.invert(); + + // This mapping is undefined when t == 0 or s == -1, i.e., when the + // denominator of either of the above rational functions is zero. + // Implementations MUST detect exceptional cases and return the value + // (v, w) = (0, 1), which is the identity point on all twisted Edwards + // curves. + let result_undefined = v.is_zero() | u_plus_one.is_zero(); + x.conditional_assign(&FieldElement::ZERO, result_undefined); + y.conditional_assign(&FieldElement::ONE, result_undefined); + + // Convert from Edwards (x, y) to extended (x, y, z, t) coordinates. + // new_edwards_from_xy(x, y) + + (x, y) + } + + #[cfg(elligator2)] + fn from_xy(x: &FieldElement, y: &FieldElement) -> EdwardsPoint { + // Yeah yeah yeah, no where better to put this. :( + let z = FieldElement::ONE; + let t = x * y; + + EdwardsPoint { + X: *x, + Y: *y, + Z: z, + T: t, + } + } } // ------------------------------------------------------------------------ diff --git a/curve25519-dalek/src/elligator2.rs b/curve25519-dalek/src/elligator2.rs new file mode 100644 index 00000000..085cdbfa --- /dev/null +++ b/curve25519-dalek/src/elligator2.rs @@ -0,0 +1,1091 @@ +// -*- mode: rust; -*- + +//! Functions mapping curve points to representative values +//! (indistinguishable from random), and back again. +use crate::constants::{MONTGOMERY_A, MONTGOMERY_A_NEG}; +use crate::field::FieldElement; +use crate::montgomery::MontgomeryPoint; +use crate::EdwardsPoint; + +use subtle::{ + Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq, ConstantTimeGreater, + CtOption, +}; + +/// (p - 1) / 2 = 2^254 - 10 +pub(crate) const DIVIDE_MINUS_P_1_2_BYTES: [u8; 32] = [ + 0xf6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, +]; + +/// Gets the public representative for a key pair using the private key +pub fn representative_from_privkey(privkey: &[u8; 32]) -> Option<[u8; 32]> { + let pubkey = EdwardsPoint::mul_base_clamped(*privkey).to_montgomery(); + let v_in_sqrt = v_in_sqrt(privkey); + point_to_representative(&pubkey, v_in_sqrt.into()).into() +} + +/// This function is used to map a curve point (i.e. an x25519 public key) +/// to a point that is effectively indistinguishable from random noise. +/// +/// This operation may fail because not all curve points are capable of being +/// hidden. On failure, this function will return None. +/// +/// This implementation is adapted from both: +/// - [kleshni C implementation](https://github.com/Kleshni/Elligator-2) +/// - [agl/ed25519 golang forks](https://gitlab.com/yawning/edwards25519-extra) +/// +/// +/// Note: the output field elements of this function are uniformly +/// distributed among the nonnegative field elements, but only if the +/// input points are also uniformly distributed among all points of +/// the curve. In particular, if the inputs are only selected from +/// members of the prime order group, then the outputs are +/// distinguishable from random. +/// +/// # Inputs +/// +/// * `point`: the \\(u\\)-coordinate of a point on the curve. Not all +/// points map to field elements. +/// +/// * `v_in_sqrt`: true if the \\(v\\)-coordinate of the point is negative. +/// +/// # Returns +/// +/// Either `None`, if the point couldn't be mapped, or `Some(fe)` such +/// that `fe` is nonnegative and `map_to_point(&fe) == point`. +/// +/// [elligator paper](https://elligator.cr.yp.to/elligator-20130828.pdf) +/// [elligator site](https://elligator.org/) +/// +pub fn point_to_representative(point: &MontgomeryPoint, v_in_sqrt: bool) -> CtOption<[u8; 32]> { + let divide_minus_p_1_2 = FieldElement::from_bytes(&DIVIDE_MINUS_P_1_2_BYTES); + + // a := point + let a = &FieldElement::from_bytes(&point.0); + let mut a_neg = *a; + a_neg.negate(); + + let is_encodable = is_encodable(a); + + // Calculate r1 = sqrt(-a/(2*(a+A))) + let (_r1_sqrt, r1) = FieldElement::sqrt_ratio_i( + &a_neg, + &(&(a + &MONTGOMERY_A) * &(&FieldElement::ONE + &FieldElement::ONE)), + ); + + // Calculate r0 = sqrt(-(a+A)/(2a)) + let (_r0_sqrt, r0) = FieldElement::sqrt_ratio_i( + &(&a_neg - &MONTGOMERY_A), + &(a * &(&FieldElement::ONE + &FieldElement::ONE)), + ); + + // if v_in_sqrt root=r0 otherwise r1 + let mut b = FieldElement::conditional_select(&r1, &r0, Choice::from(v_in_sqrt as u8)); + + // If root > (p - 1) / 2, root := -root + let negate = divide_minus_p_1_2.ct_gt(&b); + FieldElement::conditional_negate(&mut b, negate); + + CtOption::new(b.as_bytes(), is_encodable) +} + +/// Determines whether a point is encodable as a representative. Approximately +/// 50% of points are not encodable. +#[inline] +fn is_encodable(u: &FieldElement) -> Choice { + let b0 = u + &MONTGOMERY_A; + let b1 = &(&(&b0.square().square() * &b0.square()) * &b0) * u; // b1 = u * (u + A)^7 + let c = b1.pow_p58(); + + let b2 = &(&b0.square().square().square() * &b0.square().square()) * &b0.square(); // (u + A)^14 + let mut chi = &(&c.square().square() * &u.square()) * &b2; // chi = -c^4 * u^2 * (u + A)^14 + chi.negate(); + + let chi_bytes = chi.as_bytes(); + + // chi[1] is either 0 or 0xff + chi_bytes[1].ct_eq(&0_u8) +} + +/// `high_y` - Montgomery points can have have two different values for a +/// single X coordinate (based on sign). The Kleshni implementation determines +/// which sign value to use with this function. +/// +/// If the point is 0, this should be ignored. +#[inline] +pub(crate) fn high_y(d: &FieldElement) -> Choice { + let d_sq = &d.square(); + let au = &MONTGOMERY_A * &d; + + let inner = &(d_sq + &au) + &FieldElement::ONE; + let eps = d * &inner; /* eps = d^3 + Ad^2 + d */ + + let (eps_is_sq, _) = FieldElement::sqrt_ratio_i(&eps, &FieldElement::ONE); + + eps_is_sq +} + +/// Determines if `V <= (p - 1)/2` for a Scalar (e.g an x25519 private key) and +/// returns a [`Choice`] indicating the result. +/// +/// Note: When using the elligator2 transformations on x25519 keypairs this +/// requires the use of the clamped scalar_base_mult of the private key to +/// get the edwards representation of the public key, otherwise we need to know +/// the correct sign value for the edwards conversion or the derivation of +/// the `V` value will be broken. As noted in [`EdwardsPoint::to_montgomery`], +/// the sign information about the X coordinate is lost on conversion so we +/// have to use the edwards point derived from the private key to guarantee the +/// correct value here. +/// +/// Alternatively you can keep track of the public key and sign bit manually +/// and construct an EdwardsPoint for which [`v_in_sqrt_pubkey_edwards`] will +/// give you the same result. +/// +// As an interface, using the private key should work just fine. This allows +// us to match the existing [`PublicKey`] generation interface for the +// [`PublicRepresentative`] in the [`x25519_dalek`] crate. AFAIK there is no +// need for anyone with only the public key to be able to generate the +// representative. +pub fn v_in_sqrt(key_input: &[u8; 32]) -> Choice { + let mut masked_pk = *key_input; + masked_pk[0] &= 0xf8; + masked_pk[31] &= 0x7f; + masked_pk[31] |= 0x40; + + let pubkey = EdwardsPoint::mul_base_clamped(masked_pk); + v_in_sqrt_pubkey_edwards(&pubkey) +} + +/// Determines if `V <= (p - 1)/2` for an EdwardsPoint (e.g an x25519 public key) +/// and returns a [`Choice`] indicating the result. +pub fn v_in_sqrt_pubkey_edwards(pubkey: &EdwardsPoint) -> Choice { + let divide_minus_p_1_2 = FieldElement::from_bytes(&DIVIDE_MINUS_P_1_2_BYTES); + + // sqrtMinusAPlus2 is sqrt(-(486662+2)) + let (_, sqrt_minus_a_plus_2) = FieldElement::sqrt_ratio_i( + &(&MONTGOMERY_A_NEG - &(&FieldElement::ONE + &FieldElement::ONE)), + &FieldElement::ONE, + ); + + // inv1 = 1/((A.Z - A.y) * A.X) + let inv1 = (&(&pubkey.Z - &pubkey.Y) * &pubkey.X).invert(); + + // t0 = A.Y + A.Z + let t0 = &pubkey.Y + &pubkey.Z; + + // v = t0 * inv1 * A.Z * sqrtMinusAPlus2 + let v = &(&t0 * &inv1) * &(&pubkey.Z * &sqrt_minus_a_plus_2); + + // is v <= (q-1)/2 ? + divide_minus_p_1_2.ct_gt(&v) +} + +// ---------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- + +#[allow(unused, non_snake_case)] +/// Perform the Elligator2 mapping to Curve25519 in accordance with RFC9380. +/// +/// Calculates a point on elliptic curve E (Curve25519) from an element of +/// the finite field F over which E is defined. See section 6.7.1 of the +/// RFC. +/// +/// The input r and outputs u and v are elements of the field F. The +/// affine coordinates (u, v) specify a point on an elliptic curve +/// defined over F. Note, however, that the point (u, v) is not a +/// uniformly random point. +/// +/// Input: +/// * r -> an element of field F. +/// +/// Output: +/// * Q - a point in `(u,v)` for on the Montgomery elliptic curve. +/// +/// See +pub fn map_to_curve(r: &[u8; 32]) -> ([u8; 32], [u8; 32]) { + let fe = FieldElement::from_bytes(r); + let (x, y) = map_fe_to_curve(&fe); + (x.as_bytes(), y.as_bytes()) +} + +pub(crate) fn map_fe_to_curve(r: &FieldElement) -> (FieldElement, FieldElement) { + let zero = FieldElement::ZERO; + let one = FieldElement::ONE; + let mut minus_one = FieldElement::ONE; + minus_one.negate(); + + // Exceptional case 2u^2 == -1 + let mut tv1 = r.square2(); + tv1.conditional_assign(&zero, tv1.ct_eq(&minus_one)); + + let d_1 = &one + &tv1; /* 1 + 2u^2 */ + let d = &MONTGOMERY_A_NEG * &(d_1.invert()); /* d = -A/(1+2u^2) */ + + let inner = &(&d.square() + &(&d * &MONTGOMERY_A)) + &one; + let gx1 = &d * &inner; /* gx1 = d^3 + Ad^2 + d */ + let gx2 = &gx1 * &tv1; + + let eps_is_sq = high_y(&d); + + // complete X + /* A_temp = 0, or A if nonsquare*/ + let a_temp = FieldElement::conditional_select(&MONTGOMERY_A, &zero, eps_is_sq); + let mut x = &d + &a_temp; /* d, or d+A if nonsquare */ + x.conditional_negate(!eps_is_sq); /* d, or -d-A if nonsquare */ + + // complete Y + let y2 = FieldElement::conditional_select(&gx2, &gx1, eps_is_sq); + let (_, mut y) = FieldElement::sqrt_ratio_i(&y2, &one); + y.conditional_negate(eps_is_sq ^ y.is_negative()); + + (x, y) +} + +#[allow(unused, non_snake_case)] +/// Perform the Elligator2 mapping to a [`MontgomeryPoint`]. +/// +/// Calculates a point on elliptic curve E (Curve25519) from an element of +/// the finite field F over which E is defined. See section 6.7.1 of the +/// RFC. +/// +/// The input u and output P are elements of the field F. Note, however, that +/// the output point P is not a uniformly random point. +/// +/// Input: +/// * u -> an element of field F. +/// +/// Output: +/// * P - a point on the Montgomery elliptic curve. +/// +/// See +pub fn map_to_point(r: &[u8; 32]) -> MontgomeryPoint { + let r_0 = FieldElement::from_bytes(r); + let (p, _) = map_fe_to_curve(&r_0); + MontgomeryPoint(p.as_bytes()) +} + +// ------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------ + +#[cfg(test)] +#[cfg(feature = "elligator2")] +#[cfg(feature = "alloc")] +mod test { + use super::*; + + const MASK_UNSET_BYTE: u8 = 0x3f; + + //////////////////////////////////////////////////////////// + // Ntor tests // + //////////////////////////////////////////////////////////// + + #[test] + #[cfg(feature = "elligator2")] + fn repres_from_pubkey_kleshni() { + // testcases from kleshni + for (i, testcase) in encoding_testcases().iter().enumerate() { + let point: [u8; 32] = hex::decode(testcase.point).unwrap().try_into().unwrap(); + + let edw_point = MontgomeryPoint(point) + .to_edwards(testcase.high_y as u8) + .unwrap(); + let v_in_sqrt = v_in_sqrt_pubkey_edwards(&edw_point); + + let repres: Option<[u8; 32]> = + point_to_representative(&MontgomeryPoint(point), v_in_sqrt.into()).into(); + if testcase.representative.is_some() { + assert_eq!( + testcase.representative.unwrap(), + hex::encode(repres.unwrap()), + "[good case] kleshni ({i}) bad pubkey from true representative" + ); + } else { + assert!( + repres.is_none(), + "[good case] kleshni ({i}) expected none got repres {}", + hex::encode(repres.unwrap()) + ); + } + } + } + + #[test] + #[cfg(feature = "elligator2")] + /// Ensure private keys generate the expected representatives. These tests + /// are generated from agl/ed25519 to ensure compatibility. + fn repres_from_privkey_agl() { + for (i, vector) in ntor_valid_test_vectors().iter().enumerate() { + let privkey: [u8; 32] = hex::decode(vector[0]).unwrap().try_into().unwrap(); + let true_repres = vector[2]; + + let repres_res = representative_from_privkey(&privkey); + assert!( + Into::::into(repres_res.is_some()), + "failed to get representative when we should have gotten one :(" + ); + let mut repres = repres_res.unwrap(); + + repres[31] &= MASK_UNSET_BYTE; + assert_eq!( + true_repres, + hex::encode(repres), + "[good case] agl/ed25519 ({i}) bad representative from privkey" + ); + } + } + + #[test] + #[cfg(feature = "elligator2")] + fn pubkey_from_repres() { + // testcases from kleshni + for (i, testcase) in decoding_testcases().iter().enumerate() { + let repres: [u8; 32] = hex::decode(testcase.representative) + .unwrap() + .try_into() + .unwrap(); + + let point = MontgomeryPoint::from_representative(&MontgomeryPoint(repres)); + assert_eq!( + testcase.point, + hex::encode(point.to_bytes()), + "[good case] kleshni ({i}) bad representative from point" + ); + } + + // testcases from golang agl/ed25519 + for (i, vector) in ntor_valid_test_vectors().iter().enumerate() { + let true_pubkey = vector[1]; + let repres: [u8; 32] = hex::decode(vector[2]).unwrap().try_into().unwrap(); + + // ensure that the representative can be reversed to recover the + // original public key. + let pubkey = MontgomeryPoint::from_representative(&MontgomeryPoint(repres)); + assert_eq!( + true_pubkey, + hex::encode(pubkey.to_bytes()), + "[good case] agl/ed25519 ({i}) bad pubkey from true representative" + ); + } + } + + #[test] + #[cfg(feature = "elligator2")] + fn non_representable_points() { + for (i, key) in ntor_invalid_keys().iter().enumerate() { + let privkey: [u8; 32] = hex::decode(key).unwrap().try_into().unwrap(); + + // ensure that the representative can be reversed to recover the + // original public key. + let res: Option<[u8; 32]> = representative_from_privkey(&privkey).into(); + assert!( + res.is_none(), + "[bad case] agl/ed25519 ({i}) expected None, got Some({})", + hex::encode(res.unwrap()) + ); + } + } + + /// returns a set of keypair encodings generated by (a fork of) the golang + /// implementation agl/ed25519 which is used by the mainstream obfs4 + /// library. Each set of three strings is + /// + /// 1) the generated private key scalar + /// 2) the associated public key point + /// 3) the successfully created representative + /// + /// All of these are valid keypairs and our public key to + /// representative implementation (and repres-to-pubkey) should match and + /// handle these cases. + /// + fn ntor_valid_test_vectors() -> [[&'static str; 3]; 20] { + [ + [ + "eec0c0e43a2f693557dac4938c9a0f44be8bf7999399f26a24e5eab3267517c8", + "309d1f477c62df666f47b87930d1883c072d007a169c03d1c231efe2e51cae1f", + "bfd7e6dc33b735403cf6c7235513463843db8e1d2c16e62f0d5cacc8a3817515", + ], + [ + "d27f87a4850f85ef5211094eb417bc8fb9441dd8eedba8def6fd040da93fdf94", + "bb6fe9e93c929e104a6b9f956c5de1fdc977899a781d50e76dd8f8852f19e635", + "420c98e6ac9cabaccf54e02034916df64a45ad1e7799b5d2ab0403073c6f6a21", + ], + [ + "54b0d4e7110fb3a6ca5424fa7ffdc7cc599f9280df9759d1eb5d04186a4e82dd", + "f305e32fbd38dd1e6b04ba32620c6b8db121ed3216f7118875580bd454eb077d", + "a2b1a54463ad048ea9780fe2f92e0517636d2cd537d77a18cb6be03f1f991c04", + ], + [ + "9ce200c8a0c3e617c7c5605dc60d1ce67e30a608c492143d643880f91594a6dd", + "56a2e451811eb62c78090c3d076f4b179b2e9baa4d80188a3db319301031191b", + "c16f22f4899aae477d37c250164d10c9c898a820bf790b1532c3bc379b8d733e", + ], + [ + "98f09f9dedc813654e4ba28c9cd545587b20c6133603f13d8d4da2b67b4eab8c", + "210d599d6b8d1659e4a6eb98fdebd1a7024b22ba148af2a603ab807735af6a57", + "fc4ad471aff8243195ab6778054b0b243f93b4d31d5ac3a5bda9354dc3def735", + ], + [ + "fa850ae6663a8c7b84c71c6391e0b02df2d6adbc30a03b961c4b496b9979cf9d", + "7fc7a4a8ae33cd045b064d1618689a7e16c87ce611f8f519433b10134dc57b04", + "062d292892515a6a9e71e1430cc593e5cf90e4c18d7c0c0eaae7a07768e6f713", + ], + [ + "c19e91e47db473ff36714a8490665eaf6e57d228ef116e7771febce437c9e4ef", + "5ca47c5affeaeac3d67ef6564f0faa0bd575857eb4e315b57458959b0034f75c", + "f22ceea9e0e3826a308fe60df0edf4b82177e3704750f7af6e41aa5c5d806f16", + ], + [ + "012fb2f110c7da5d56bb86e28e23dff1d860bb6ed11e145be9344ba756dd4e6e", + "badb58d30c1a8b6bbc0ee97f627272aa1c5625a1cadb7bf75fa274f76c081964", + "62cf0d3d7c67715a7cc982c9186ccd4d151b64b2cef862433a963b7b2c13f033", + ], + [ + "fc9fec244ec6064f1060aabad079d04d1bb180239b07d4d02383ecbb05a7095e", + "54fe5cc959be48e5a0251026ac4cd0f5a424b8f5c157a7c59857d2ab11521e4c", + "0326b2e14412ef42fdd0144f96f6b47a5e0954aa110ca7ba124d1aca82269e1f", + ], + [ + "1cb297f54fd1b320995f885591abdd5b84ff35f55ed22f85475a00c0c97b648a", + "2b0472ba2ffd8af768c2b6dab2b6db21c0ca153537d4b3c638a7cf7708c99e2d", + "08bba17691f09c899bba92f8950dec551065249f77e59d46bf113c56fc54a20f", + ], + [ + "4871c2e7ada1911544713cbf15e0553d4972cc9867a4e67a667643b8e7a77d30", + "d73258438f51a0f603180aa349e3276fd623bcce61ff9d2dbb9fd9a9aaf53e40", + "a5ad91e362963312d95659b2e97bfcfddca573841f864cf8a8e0e075a72e4d1c", + ], + [ + "52959827c6c7d3e3b6b50089db8a9524243ae7cee3f9ad7eff477587683af118", + "031d982caf72aa660a68d79439cedd0e0c47d09f9b6c90d1a0c322291591616e", + "8dd9423dcc8d08c951330954c99eb508395ac26a3e6d864df907ad1b23fd6a37", + ], + [ + "12c77ad0259120089f5bfb841999e9e17acf71a00ef7891366e3f286c0122790", + "ce7cb3c1789b119987d36ed73dd40db07099ae9eca8b8ee53f63f610728d7600", + "8cdcc4d90a94f8148619bc7a32d230e8bbd4416051f53da3ebda320a40904128", + ], + [ + "db46139a816b40cc15be3b82cd799f025cfa0817552bf959386e0a264184a419", + "03778d7eee1973fbe8a45670bc2172a8fbc8e0627cf81ad932f7bbedc1aca177", + "f3382b89603501ac8a5038aeda9422af88a2557f6a831b20062518733d59382d", + ], + // The following key sets have a v_in_sqrt value of 0 + [ + "51a8b03750f4b34922d3588b73a556a09594b7b014d2b7cefa26bb8c5eee6d2e", + "e8db45df5146d3745eae81e722af5e030a9a999a3e0d588f84514b125029b81b", + "f859efb4704b0b4c71959b02976de3ea9746f01e5adacddddba10ee08178cd1d", + ], + [ + "d5a46dfdf2c66a76c312e83954fc0cf54f75ff14c2d6febfeb08153e34f54cfd", + "08a6adca98cbee754c3cdb103b5d9f69e61b87c64339947cbbaa1a956df5b745", + "139b06604063d7942540fbd33de329a2f733c65a89fd68151743896744397c2b", + ], + [ + "a06917dc2988e4b51559ab26e25fd920e8fec2f8f2fe0f4c3c725dce06de7867", + "868603c764dff5f6db6f963237731452c469dfa2c8c5b682cfec85fc38661415", + "2bdd5f3dcaeefa352f200306be3471ad90a0a0ac4b6abba44230e284a852b813", + ], + [ + "7acad18a021a568d2abaf079d046f5eb55e081a32b00d4f6c77f8b8c9afed866", + "8e0f52904421469a46b2d636b9d17595573071d16ebff280fc88829b5ef8bd4f", + "abc0de8594713993eab06144fe9b6d7bd04227c62bda19ef984008a93161fb33", + ], + [ + "c547b93c519a1c0b40b71fe7b08e13b38639564e9317f6b58c5f99d5ad82471a", + "687fe9c6fe84e94ef0f7344abdac92dfd60dabc6156c1a3eea19d0d06705b461", + "8b0ea3b2215bf829aedd250c629557ca646515861aa0b7fc881c50d622f9ac38", + ], + [ + "77e48dfa107bbfdda73f50ec2c38347e4fcc9c38866adb75488a2143993a058f", + "7d124e12af90216f26ce3198f6b02e76faf990dd248cdb246dd80d4e1fef3d4d", + "128481624af3015c6226428a247514370800f212a7a06c90dfe4f1aa672d3b3e", + ], + ] + } + + /// returns a set of private keys generated by (a fork of) the golang + /// implementation agl/ed25519 which is used by the mainstream obfs4 + /// library. + /// + /// All of these fail to generate a keypair capable of using elligator2 + /// to encode of the public key. + /// + #[cfg(feature = "elligator2")] + fn ntor_invalid_keys() -> [&'static str; 16] { + [ + "fd0d592d67038a6253331547949d70eb7a6b6c4c4831a1bc235554765b20f845", + "9576aa473a2b73b174e753ada7d55bc34ae8c05c1b1d6077f9a098984df9cf52", + "5b26538e0663a453994cf49d5614926786a0549c0123fa9afc7be99e04afc9ea", + "d4e58db7420267ad43e72e1fe0dee1acb9ca547e258e22370d76c0945f4b1a1e", + "01b047693c6b5212607387df88d1a427d2f24bd24157058705fc090e22778a64", + "d2d363fad3ea3aceb8262adc1184f32c6f63911e6bc008c87f366f9c2b60753c", + "75f703e3095343cf5d3ccd05a3fea06f82509a26c391a0365a1f1ca5e1d5f82f", + "1f6986c9146f2d4b1907a812e7e7254c82e121776d58fd9c4a5f6741a326e2ef", + "fe8465f11b40d52dc845df0a1639398b1726409d3b52526082c3e67c52f06842", + "4546f8e010e0d3be7ab3e97480cecc411b054013960ff4032b3d3e5afe1565e0", + "fc0f99f041118fd44c685b87b143b48d6d8c820d9f07235eeb7f7a9468dc0ca0", + "847c1d842a2e91521d1a63e191d3d2389e54c73378130f376d013781730ace3e", + "7d8a740b09f477588bc9a9020b2444ee384718f889403c02f3a61e69f55e6f7f", + "0549080985424df8f9401be8802e8c6a4c02737c474ac76c9d3f7968a3fca40a", + "53cd411b966503129365517e5c1b04e9f52a248cc57599af9ed58c9ff5b774d4", + "015aebbe87a5e94c5d8d3179583cf46d02af06c8f99a6d4ce8a3a4b1de097cd2", + ] + } + + const ENCODING_TESTS_COUNT: usize = 6; + struct EncodingTestCase { + high_y: bool, + point: &'static str, + representative: Option<&'static str>, + } + + fn encoding_testcases() -> [EncodingTestCase; ENCODING_TESTS_COUNT] { + [ + // A not encodable point with both "high_y" values + EncodingTestCase { + point: "e6f66fdf6e230c603c5e6e59a254ea1476a13eb9511b9549846781e12e52230a", + high_y: false, + representative: None, + }, + EncodingTestCase { + point: "e6f66fdf6e230c603c5e6e59a254ea1476a13eb9511b9549846781e12e52230a", + high_y: true, + representative: None, + }, + // An encodable point with both "high_y" values + EncodingTestCase { + point: "33951964003c940878063ccfd0348af42150ca16d2646f2c5856e8338377d800", + high_y: false, + representative: Some( + "999b591b6697d074f266192277d554dec3c24c2ef6108101f63d94f7fff3a013", + ), + }, + EncodingTestCase { + point: "33951964003c940878063ccfd0348af42150ca16d2646f2c5856e8338377d800", + high_y: true, + representative: Some( + "bd3d2a7ed1c8a100a977f8d992e33aaa6f630d55089770ea469101d7fd73d13d", + // "999b591b6697d074f266192277d554dec3c24c2ef6108101f63d94f7fff3a013", + ), + }, + // 0 with both "high_y" values + EncodingTestCase { + point: "0000000000000000000000000000000000000000000000000000000000000000", + high_y: false, + representative: Some( + "0000000000000000000000000000000000000000000000000000000000000000", + ), + }, + EncodingTestCase { + point: "0000000000000000000000000000000000000000000000000000000000000000", + high_y: true, + representative: Some( + "0000000000000000000000000000000000000000000000000000000000000000", + ), + }, + ] + } + + const DECODING_TESTS_COUNT: usize = 7; + struct DecodingTestCase { + representative: &'static str, + point: &'static str, + } + + fn decoding_testcases() -> [DecodingTestCase; DECODING_TESTS_COUNT] { + [ + // A small representative with false "high_y" property + DecodingTestCase { + representative: "e73507d38bae63992b3f57aac48c0abc14509589288457995a2b4ca3490aa207", + point: "1e8afffed6bf53fe271ad572473262ded8faec68e5e67ef45ebb82eeba52604f", + }, + // A small representative with true "high_y" property + DecodingTestCase { + representative: "95a16019041dbefed9832048ede11928d90365f24a38aa7aef1b97e23954101b", + point: "794f05ba3e3a72958022468c88981e0be5782be1e1145ce2c3c6fde16ded5363", + }, + // The last representative returning true: (p - 1) / 2 + DecodingTestCase { + representative: "f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f", + point: "9cdb525555555555555555555555555555555555555555555555555555555555", + }, + // The first representative returning false: (p + 1) / 2 + DecodingTestCase { + representative: "f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f", + point: "9cdb525555555555555555555555555555555555555555555555555555555555", + }, + // A large representative with false "high_y" property + DecodingTestCase { + representative: "179f24730ded2ce3173908ec61964653b8027e383f40346c1c9b4d2bdb1db76c", + point: "10745497d35c6ede6ea6b330546a6fcbf15c903a7be28ae69b1ca14e0bf09b60", + }, + // A large representative with true "high_y" property + DecodingTestCase { + representative: "8a2f286180c3d8630b5f5a3c7cc027a55e0d3ffb3b1b990c5c7bb4c3d1f91b6f", + point: "6d3187192afc3bcc05a497928816e3e2336dc539aa7fc296a9ee013f560db843", + }, + // 0 + DecodingTestCase { + representative: "0000000000000000000000000000000000000000000000000000000000000000", + point: "0000000000000000000000000000000000000000000000000000000000000000", + }, + ] + } +} + +#[cfg(test)] +#[cfg(feature = "elligator2")] +mod rfc9380 { + use super::*; + + use hex::FromHex; + use std::string::String; + + #[test] + fn map_to_curve_test_go_ed25519_extra() { + for i in 0..CURVE25519_ELL2.len() { + let testcase = &CURVE25519_ELL2[i]; + + let u = testcase[0].must_from_be(); + let mut clamped = u.clone(); + clamped[31] &= 63; + + // map point to curve + let (q_x, _) = map_fe_to_curve(&FieldElement::from_bytes(&clamped)); + + // check resulting point + assert_eq!( + q_x.encode_be(), + testcase[1], + "({i}) incorrect x curve25519 ELL2\n" + ); + } + } + + #[test] + fn map_to_curve_test_curve25519() { + for i in 0..curve25519_XMD_SHA512_ELL2_NU.len() { + let testcase = &curve25519_XMD_SHA512_ELL2_NU[i]; + let u = FieldElement::from_bytes(&testcase.u_0.must_from_le()); + + // map point to curve + let (q_x, q_y) = map_fe_to_curve(&u); + + // check resulting point + assert_eq!( + q_x.encode_le(), + testcase.Q_x, + "({i}) incorrect Q0_x curve25519 NU\n{:?}", + testcase + ); + assert_eq!( + q_y.encode_le(), + testcase.Q_y, + "({i}) incorrect Q0_y curve25519 NU\n{:?}", + testcase + ); + } + for i in 0..curve25519_XMD_SHA512_ELL2_RO.len() { + let testcase = &curve25519_XMD_SHA512_ELL2_RO[i]; + let u0 = FieldElement::from_bytes(&testcase.u_0.must_from_le()); + let u1 = FieldElement::from_bytes(&testcase.u_1.must_from_le()); + + // map points to curve + let (q0_x, q0_y) = map_fe_to_curve(&u0); + let (q1_x, q1_y) = map_fe_to_curve(&u1); + + // check resulting points + assert_eq!( + q0_x.encode_le(), + testcase.Q0_x, + "({i}) incorrect Q0_x curve25519 RO\n{:?}", + testcase + ); + assert_eq!( + q0_y.encode_le(), + testcase.Q0_y, + "({i}) incorrect Q0_y curve25519 RO\n{:?}", + testcase + ); + assert_eq!( + q1_x.encode_le(), + testcase.Q1_x, + "({i}) incorrect Q1_x curve25519 RO\n{:?}", + testcase + ); + assert_eq!( + q1_y.encode_le(), + testcase.Q1_y, + "({i}) incorrect Q1_y curve25519 RO\n{:?}", + testcase + ); + } + } + + #[test] + fn map_to_curve_test_edwards25519() { + for i in 0..edwards25519_XMD_SHA512_ELL2_NU.len() { + let testcase = &curve25519_XMD_SHA512_ELL2_NU[i]; + let u = FieldElement::from_bytes(&testcase.u_0.must_from_le()); + + // map point to curve + let (q_x, q_y) = map_fe_to_curve(&u); + + // check resulting point + assert_eq!( + q_x.encode_le(), + testcase.Q_x, + "({i}) incorrect Q0_x curve25519 NU\n{:?}", + testcase + ); + assert_eq!( + q_y.encode_le(), + testcase.Q_y, + "({i}) incorrect Q0_y curve25519 NU\n{:?}", + testcase + ); + } + for i in 0..edwards25519_XMD_SHA512_ELL2_RO.len() { + let testcase = &curve25519_XMD_SHA512_ELL2_RO[i]; + let u0 = FieldElement::from_bytes(&testcase.u_0.must_from_le()); + let u1 = FieldElement::from_bytes(&testcase.u_1.must_from_le()); + + // map points to curve + let (q0_x, q0_y) = map_fe_to_curve(&u0); + let (q1_x, q1_y) = map_fe_to_curve(&u1); + + // check resulting points + assert_eq!( + q0_x.encode_le(), + testcase.Q0_x, + "({i}) incorrect Q0_x curve25519 RO\n{:?}", + testcase + ); + assert_eq!( + q0_y.encode_le(), + testcase.Q0_y, + "({i}) incorrect Q0_y curve25519 RO\n{:?}", + testcase + ); + assert_eq!( + q1_x.encode_le(), + testcase.Q1_x, + "({i}) incorrect Q1_x curve25519 RO\n{:?}", + testcase + ); + assert_eq!( + q1_y.encode_le(), + testcase.Q1_y, + "({i}) incorrect Q1_y curve25519 RO\n{:?}", + testcase + ); + } + } + + /// Example test cases found in gitlab.com/yawning/edwards25519-extra + /// + /// 1. representative + /// 2. associated point + /// + /// These test cases need the upper two bits cleared to be properly mapped. + const CURVE25519_ELL2: [[&'static str; 2]; 14] = [ + [ + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ], + [ + "0000000000000000000000000000000000000000000000000000000000000040", + "0000000000000000000000000000000000000000000000000000000000000000", + ], + [ + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000000", + ], + [ + "00000000000000000000000000000000000000000000000000000000000000c0", + "0000000000000000000000000000000000000000000000000000000000000000", + ], + [ + "673a505e107189ee54ca93310ac42e4545e9e59050aaac6f8b5f64295c8ec02f", + "242ae39ef158ed60f20b89396d7d7eef5374aba15dc312a6aea6d1e57cacf85e", + ], + [ + "922688fa428d42bc1fa8806998fbc5959ae801817e85a42a45e8ec25a0d7545a", + "696f341266c64bcfa7afa834f8c34b2730be11c932e08474d1a22f26ed82410b", + ], + [ + "0d3b0eb88b74ed13d5f6a130e03c4ad607817057dc227152827c0506a538bbba", + "0b00df174d9fb0b6ee584d2cf05613130bad18875268c38b377e86dfefef177f", + ], + [ + "01a3ea5658f4e00622eeacf724e0bd82068992fae66ed2b04a8599be16662ef5", + "7ae4c58bc647b5646c9f5ae4c2554ccbf7c6e428e7b242a574a5a9c293c21f7e", + ], + [ + "69599ab5a829c3e9515128d368da7354a8b69fcee4e34d0a668b783b6cae550f", + "09024abaaef243e3b69366397e8dfc1fdc14a0ecc7cf497cbe4f328839acce69", + ], + [ + "9172922f96d2fa41ea0daf961857056f1656ab8406db80eaeae76af58f8c9f50", + "beab745a2a4b4e7f1a7335c3ffcdbd85139f3a72b667a01ee3e3ae0e530b3372", + ], + [ + "6850a20ac5b6d2fa7af7042ad5be234d3311b9fb303753dd2b610bd566983281", + "1287388eb2beeff706edb9cf4fcfdd35757f22541b61528570b86e8915be1530", + ], + [ + "84417826c0e80af7cb25a73af1ba87594ff7048a26248b5757e52f2824e068f1", + "51acd2e8910e7d28b4993db7e97e2b995005f26736f60dcdde94bdf8cb542251", + ], + [ + "b0fbe152849f49034d2fa00ccc7b960fad7b30b6c4f9f2713eb01c147146ad31", + "98508bb3590886af3be523b61c3d0ce6490bb8b27029878caec57e4c750f993d", + ], + [ + "a0ca9ff75afae65598630b3b93560834c7f4dd29a557aa29c7becd49aeef3753", + "3c5fad0516bb8ec53da1c16e910c23f792b971c7e2a0ee57d57c32e3655a646b", + ], + ]; + + // J.4.1. curve25519_XMD:SHA-512_ELL2_RO_ + // + // Random Oracle Curve25519 SHA512 Elligator2 + // + // suite = curve25519_XMD:SHA-512_ELL2_RO_ + // dst = QUUX-V01-CS02-with-curve25519_XMD:SHA-512_ELL2_RO_ + // + #[allow(non_upper_case_globals)] + const curve25519_XMD_SHA512_ELL2_RO: [xmd_sha512_25519_ro_testcase; 5] = [ + xmd_sha512_25519_ro_testcase { + u_0: "49bed021c7a3748f09fa8cdfcac044089f7829d3531066ac9e74e0994e05bc7d", + u_1: "5c36525b663e63389d886105cee7ed712325d5a97e60e140aba7e2ce5ae851b6", + Q0_x: "16b3d86e056b7970fa00165f6f48d90b619ad618791661b7b5e1ec78be10eac1", + Q0_y: "4ab256422d84c5120b278cbdfc4e1facc5baadffeccecf8ee9bf3946106d50ca", + Q1_x: "7ec29ddbf34539c40adfa98fcb39ec36368f47f30e8f888cc7e86f4d46e0c264", + Q1_y: "10d1abc1cae2d34c06e247f2141ba897657fb39f1080d54f09ce0af128067c74", + }, + xmd_sha512_25519_ro_testcase { + u_0: "6412b7485ba26d3d1b6c290a8e1435b2959f03721874939b21782df17323d160", + u_1: "24c7b46c1c6d9a21d32f5707be1380ab82db1054fde82865d5c9e3d968f287b2", + Q0_x: "71de3dadfe268872326c35ac512164850860567aea0e7325e6b91a98f86533ad", + Q0_y: "26a08b6e9a18084c56f2147bf515414b9b63f1522e1b6c5649f7d4b0324296ec", + Q1_x: "5704069021f61e41779e2ba6b932268316d6d2a6f064f997a22fef16d1eaeaca", + Q1_y: "50483c7540f64fb4497619c050f2c7fe55454ec0f0e79870bb44302e34232210", + }, + xmd_sha512_25519_ro_testcase { + u_0: "5e123990f11bbb5586613ffabdb58d47f64bb5f2fa115f8ea8df0188e0c9e1b5", + u_1: "5e8553eb00438a0bb1e7faa59dec6d8087f9c8011e5fb8ed9df31cb6c0d4ac19", + Q0_x: "7a94d45a198fb5daa381f45f2619ab279744efdd8bd8ed587fc5b65d6cea1df0", + Q0_y: "67d44f85d376e64bb7d713585230cdbfafc8e2676f7568e0b6ee59361116a6e1", + Q1_x: "30506fb7a32136694abd61b6113770270debe593027a968a01f271e146e60c18", + Q1_y: "7eeee0e706b40c6b5174e551426a67f975ad5a977ee2f01e8e20a6d612458c3b", + }, + xmd_sha512_25519_ro_testcase { + u_0: "20f481e85da7a3bf60ac0fb11ed1d0558fc6f941b3ac5469aa8b56ec883d6d7d", + u_1: "017d57fd257e9a78913999a23b52ca988157a81b09c5442501d07fed20869465", + Q0_x: "02d606e2699b918ee36f2818f2bc5013e437e673c9f9b9cdc15fd0c5ee913970", + Q0_y: "29e9dc92297231ef211245db9e31767996c5625dfbf92e1c8107ef887365de1e", + Q1_x: "38920e9b988d1ab7449c0fa9a6058192c0c797bb3d42ac345724341a1aa98745", + Q1_y: "24dcc1be7c4d591d307e89049fd2ed30aae8911245a9d8554bf6032e5aa40d3d", + }, + xmd_sha512_25519_ro_testcase { + u_0: "005fe8a7b8fef0a16c105e6cadf5a6740b3365e18692a9c05bfbb4d97f645a6a", + u_1: "1347edbec6a2b5d8c02e058819819bee177077c9d10a4ce165aab0fd0252261a", + Q0_x: "36b4df0c864c64707cbf6cf36e9ee2c09a6cb93b28313c169be29561bb904f98", + Q0_y: "6cd59d664fb58c66c892883cd0eb792e52055284dac3907dd756b45d15c3983d", + Q1_x: "3fa114783a505c0b2b2fbeef0102853c0b494e7757f2a089d0daae7ed9a0db2b", + Q1_y: "76c0fe7fec932aaafb8eefb42d9cbb32eb931158f469ff3050af15cfdbbeff94", + }, + ]; + + // J.4.2. curve25519_XMD:SHA-512_ELL2_NU_ + // + // Nonuniform Encoding Curve25519 SHA512 Elligator2 + // + // suite: curve25519_XMD:SHA-512_ELL2_NU_ + // dst: QUUX-V01-CS02-with-curve25519_XMD:SHA-512_ELL2_NU_ + // + #[allow(non_upper_case_globals)] + const curve25519_XMD_SHA512_ELL2_NU: [xmd_sha512_25519_nu_testcase; 5] = [ + xmd_sha512_25519_nu_testcase { + u_0: "608d892b641f0328523802a6603427c26e55e6f27e71a91a478148d45b5093cd", + Q_x: "51125222da5e763d97f3c10fcc92ea6860b9ccbbd2eb1285728f566721c1e65b", + Q_y: "343d2204f812d3dfc5304a5808c6c0d81a903a5d228b342442aa3c9ba5520a3d", + }, + xmd_sha512_25519_nu_testcase { + u_0: "46f5b22494bfeaa7f232cc8d054be68561af50230234d7d1d63d1d9abeca8da5", + Q_x: "7d56d1e08cb0ccb92baf069c18c49bb5a0dcd927eff8dcf75ca921ef7f3e6eeb", + Q_y: "404d9a7dc25c9c05c44ab9a94590e7c3fe2dcec74533a0b24b188a5d5dacf429", + }, + xmd_sha512_25519_nu_testcase { + u_0: "235fe40c443766ce7e18111c33862d66c3b33267efa50d50f9e8e5d252a40aaa", + Q_x: "3fbe66b9c9883d79e8407150e7c2a1c8680bee496c62fabe4619a72b3cabe90f", + Q_y: "08ec476147c9a0a3ff312d303dbbd076abb7551e5fce82b48ab14b433f8d0a7b", + }, + xmd_sha512_25519_nu_testcase { + u_0: "001e92a544463bda9bd04ddbe3d6eed248f82de32f522669efc5ddce95f46f5b", + Q_x: "227e0bb89de700385d19ec40e857db6e6a3e634b1c32962f370d26f84ff19683", + Q_y: "5f86ff3851d262727326a32c1bf7655a03665830fa7f1b8b1e5a09d85bc66e4a", + }, + xmd_sha512_25519_nu_testcase { + u_0: "1a68a1af9f663592291af987203393f707305c7bac9c8d63d6a729bdc553dc19", + Q_x: "3bcd651ee54d5f7b6013898aab251ee8ecc0688166fce6e9548d38472f6bd196", + Q_y: "1bb36ad9197299f111b4ef21271c41f4b7ecf5543db8bb5931307ebdb2eaa465", + }, + ]; + + // J.5.1. edwards25519_XMD:SHA-512_ELL2_RO_ + // + // Random Oracle Edwards 25519 SHA512 Elligator2 + // + // suite: edwards25519_XMD:SHA-512_ELL2_RO_ + // dst: QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_RO_ + // + #[allow(non_upper_case_globals)] + const edwards25519_XMD_SHA512_ELL2_RO: [xmd_sha512_25519_ro_testcase; 5] = [ + xmd_sha512_25519_ro_testcase { + u_0: "03fef4813c8cb5f98c6eef88fae174e6e7d5380de2b007799ac7ee712d203f3a", + u_1: "780bdddd137290c8f589dc687795aafae35f6b674668d92bf92ae793e6a60c75", + Q0_x: "6549118f65bb617b9e8b438decedc73c496eaed496806d3b2eb9ee60b88e09a7", + Q0_y: "7315bcc8cf47ed68048d22bad602c6680b3382a08c7c5d3f439a973fb4cf9feb", + Q1_x: "31dcfc5c58aa1bee6e760bf78cbe71c2bead8cebb2e397ece0f37a3da19c9ed2", + Q1_y: "7876d81474828d8a5928b50c82420b2bd0898d819e9550c5c82c39fc9bafa196", + }, + xmd_sha512_25519_ro_testcase { + u_0: "5081955c4141e4e7d02ec0e36becffaa1934df4d7a270f70679c78f9bd57c227", + u_1: "005bdc17a9b378b6272573a31b04361f21c371b256252ae5463119aa0b925b76", + Q0_x: "5c1525bd5d4b4e034512949d187c39d48e8cd84242aa4758956e4adc7d445573", + Q0_y: "2bf426cf7122d1a90abc7f2d108befc2ef415ce8c2d09695a7407240faa01f29", + Q1_x: "37b03bba828860c6b459ddad476c83e0f9285787a269df2156219b7e5c86210c", + Q1_y: "285ebf5412f84d0ad7bb4e136729a9ffd2195d5b8e73c0dc85110ce06958f432", + }, + xmd_sha512_25519_ro_testcase { + u_0: "285ebaa3be701b79871bcb6e225ecc9b0b32dff2d60424b4c50642636a78d5b3", + u_1: "2e253e6a0ef658fedb8e4bd6a62d1544fd6547922acb3598ec6b369760b81b31", + Q0_x: "3ac463dd7fddb773b069c5b2b01c0f6b340638f54ee3bd92d452fcec3015b52d", + Q0_y: "7b03ba1e8db9ec0b390d5c90168a6a0b7107156c994c674b61fe696cbeb46baf", + Q1_x: "0757e7e904f5e86d2d2f4acf7e01c63827fde2d363985aa7432106f1b3a444ec", + Q1_y: "50026c96930a24961e9d86aa91ea1465398ff8e42015e2ec1fa397d416f6a1c0", + }, + xmd_sha512_25519_ro_testcase { + u_0: "4fedd25431c41f2a606952e2945ef5e3ac905a42cf64b8b4d4a83c533bf321af", + u_1: "02f20716a5801b843987097a8276b6d869295b2e11253751ca72c109d37485a9", + Q0_x: "703e69787ea7524541933edf41f94010a201cc841c1cce60205ec38513458872", + Q0_y: "32bb192c4f89106466f0874f5fd56a0d6b6f101cb714777983336c159a9bec75", + Q1_x: "0c9077c5c31720ed9413abe59bf49ce768506128d810cb882435aa90f713ef6b", + Q1_y: "7d5aec5210db638c53f050597964b74d6dda4be5b54fa73041bf909ccb3826cb", + }, + xmd_sha512_25519_ro_testcase { + u_0: "6e34e04a5106e9bd59f64aba49601bf09d23b27f7b594e56d5de06df4a4ea33b", + u_1: "1c1c2cb59fc053f44b86c5d5eb8c1954b64976d0302d3729ff66e84068f5fd96", + Q0_x: "21091b2e3f9258c7dfa075e7ae513325a94a3d8a28e1b1cb3b5b6f5d65675592", + Q0_y: "41a33d324c89f570e0682cdf7bdb78852295daf8084c669f2cc9692896ab5026", + Q1_x: "4c07ec48c373e39a23bd7954f9e9b66eeab9e5ee1279b867b3d5315aa815454f", + Q1_y: "67ccac7c3cb8d1381242d8d6585c57eabaddbb5dca5243a68a8aeb5477d94b3a", + }, + ]; + + // J.5.2. edwards25519_XMD:SHA-512_ELL2_NU_ + // + // Nonuniform Encoding Edwards 25519 SHA512 Elligator2 + // + // suite: edwards25519_XMD:SHA-512_ELL2_NU_ + // dst: QUUX-V01-CS02-with-edwards25519_XMD:SHA-512_ELL2_NU_ + // + #[allow(non_upper_case_globals)] + const edwards25519_XMD_SHA512_ELL2_NU: [xmd_sha512_25519_nu_testcase; 5] = [ + xmd_sha512_25519_nu_testcase { + u_0: "7f3e7fb9428103ad7f52db32f9df32505d7b427d894c5093f7a0f0374a30641d", + Q_x: "42836f691d05211ebc65ef8fcf01e0fb6328ec9c4737c26050471e50803022eb", + Q_y: "22cb4aaa555e23bd460262d2130d6a3c9207aa8bbb85060928beb263d6d42a95", + }, + xmd_sha512_25519_nu_testcase { + u_0: "09cfa30ad79bd59456594a0f5d3a76f6b71c6787b04de98be5cd201a556e253b", + Q_x: "333e41b61c6dd43af220c1ac34a3663e1cf537f996bab50ab66e33c4bd8e4e19", + Q_y: "51b6f178eb08c4a782c820e306b82c6e273ab22e258d972cd0c511787b2a3443", + }, + xmd_sha512_25519_nu_testcase { + u_0: "475ccff99225ef90d78cc9338e9f6a6bb7b17607c0c4428937de75d33edba941", + Q_x: "55186c242c78e7d0ec5b6c9553f04c6aeef64e69ec2e824472394da32647cfc6", + Q_y: "5b9ea3c265ee42256a8f724f616307ef38496ef7eba391c08f99f3bea6fa88f0", + }, + xmd_sha512_25519_nu_testcase { + u_0: "049a1c8bd51bcb2aec339f387d1ff51428b88d0763a91bcdf6929814ac95d03d", + Q_x: "024b6e1621606dca8071aa97b43dce4040ca78284f2a527dcf5d0fbfac2b07e7", + Q_y: "5102353883d739bdc9f8a3af650342b171217167dcce34f8db57208ec1dfdbf2", + }, + xmd_sha512_25519_nu_testcase { + u_0: "3cb0178a8137cefa5b79a3a57c858d7eeeaa787b2781be4a362a2f0750d24fa0", + Q_x: "3e6368cff6e88a58e250c54bd27d2c989ae9b3acb6067f2651ad282ab8c21cd9", + Q_y: "38fb39f1566ca118ae6c7af42810c0bb9767ae5960abb5a8ca792530bfb9447d", + }, + ]; + + #[allow(non_camel_case_types, non_snake_case)] + #[derive(Debug)] + struct xmd_sha512_25519_ro_testcase { + u_0: &'static str, + u_1: &'static str, + // Output + Q0_x: &'static str, + Q0_y: &'static str, + Q1_x: &'static str, + Q1_y: &'static str, + } + + #[allow(non_camel_case_types, non_snake_case)] + #[derive(Debug)] + struct xmd_sha512_25519_nu_testcase { + u_0: &'static str, + // output + Q_x: &'static str, + Q_y: &'static str, + } + + trait FromByteString { + fn must_from_le(&self) -> [u8; 32]; + fn must_from_be(&self) -> [u8; 32]; + } + + impl<'a> FromByteString for &'a str { + fn must_from_le(&self) -> [u8; 32] { + let mut u = <[u8; 32]>::from_hex(self).unwrap(); + u.reverse(); + u + } + fn must_from_be(&self) -> [u8; 32] { + <[u8; 32]>::from_hex(self).unwrap() + } + } + + trait ToByteString { + fn encode_le(&self) -> String; + fn encode_be(&self) -> String; + } + + impl ToByteString for FieldElement { + fn encode_le(&self) -> String { + let mut b = self.as_bytes(); + b.reverse(); + hex::encode(&b) + } + + fn encode_be(&self) -> String { + hex::encode(self.as_bytes()) + } + } + + impl ToByteString for [u8; 32] { + fn encode_le(&self) -> String { + let mut b = self.clone(); + b.reverse(); + hex::encode(&b) + } + + fn encode_be(&self) -> String { + hex::encode(self) + } + } +} diff --git a/curve25519-dalek/src/field.rs b/curve25519-dalek/src/field.rs index 68c9c8b8..0a4a428b 100644 --- a/curve25519-dalek/src/field.rs +++ b/curve25519-dalek/src/field.rs @@ -30,7 +30,7 @@ use cfg_if::cfg_if; use subtle::Choice; use subtle::ConditionallyNegatable; use subtle::ConditionallySelectable; -use subtle::ConstantTimeEq; +use subtle::{ConstantTimeEq, ConstantTimeGreater}; use crate::backend; use crate::constants; @@ -217,7 +217,7 @@ impl FieldElement { /// Raise this field element to the power (p-5)/8 = 2^252 -3. #[rustfmt::skip] // keep alignment of explanatory comments #[allow(clippy::let_and_return)] - fn pow_p58(&self) -> FieldElement { + pub(crate) fn pow_p58(&self) -> FieldElement { // The bits of (p-5)/8 are 101111.....11. // // nonzero bits of exponent @@ -305,6 +305,18 @@ impl FieldElement { } } +impl ConstantTimeGreater for FieldElement { + /// Test equality between two `FieldElement`s. Since the + /// internal representation is not canonical, the field elements + /// are normalized to wire format before comparison. + /// + /// If self > other return Choice(1), otherwise return Choice(0) + /// + fn ct_gt(&self, other: &FieldElement) -> Choice { + self.gt(other) + } +} + #[cfg(test)] mod test { use crate::field::*; diff --git a/curve25519-dalek/src/lib.rs b/curve25519-dalek/src/lib.rs index fecfe888..9792daf3 100644 --- a/curve25519-dalek/src/lib.rs +++ b/curve25519-dalek/src/lib.rs @@ -95,6 +95,11 @@ pub mod traits; // Finite field arithmetic mod p = 2^255 - 19 pub(crate) mod field; +// elligator2 implementation for encoding/decoding points to representatives and +// vice versa. +#[cfg(feature = "elligator2")] +pub mod elligator2; + // Arithmetic backends (using u32, u64, etc) live here #[cfg(docsrs)] pub mod backend; diff --git a/curve25519-dalek/src/montgomery.rs b/curve25519-dalek/src/montgomery.rs index 2be35cdc..1f235274 100644 --- a/curve25519-dalek/src/montgomery.rs +++ b/curve25519-dalek/src/montgomery.rs @@ -54,16 +54,18 @@ use core::{ ops::{Mul, MulAssign}, }; -use crate::constants::{APLUS2_OVER_FOUR, MONTGOMERY_A, MONTGOMERY_A_NEG}; +use crate::constants::APLUS2_OVER_FOUR; use crate::edwards::{CompressedEdwardsY, EdwardsPoint}; use crate::field::FieldElement; use crate::scalar::{clamp_integer, Scalar}; - use crate::traits::Identity; +#[cfg(feature = "elligator2")] +use crate::elligator2; + use subtle::Choice; +use subtle::ConditionallySelectable; use subtle::ConstantTimeEq; -use subtle::{ConditionallyNegatable, ConditionallySelectable}; #[cfg(feature = "zeroize")] use zeroize::Zeroize; @@ -244,35 +246,12 @@ impl MontgomeryPoint { CompressedEdwardsY(y_bytes).decompress() } -} - -/// Perform the Elligator2 mapping to a Montgomery point. -/// -/// See -// -// TODO Determine how much of the hash-to-group API should be exposed after the CFRG -// draft gets into a more polished/accepted state. -#[allow(unused)] -pub(crate) fn elligator_encode(r_0: &FieldElement) -> MontgomeryPoint { - let one = FieldElement::ONE; - let d_1 = &one + &r_0.square2(); /* 2r^2 */ - let d = &MONTGOMERY_A_NEG * &(d_1.invert()); /* A/(1+2r^2) */ - - let d_sq = &d.square(); - let au = &MONTGOMERY_A * &d; - - let inner = &(d_sq + &au) + &one; - let eps = &d * &inner; /* eps = d^3 + Ad^2 + d */ - - let (eps_is_sq, _eps) = FieldElement::sqrt_ratio_i(&eps, &one); - - let zero = FieldElement::ZERO; - let Atemp = FieldElement::conditional_select(&MONTGOMERY_A, &zero, eps_is_sq); /* 0, or A if nonsquare*/ - let mut u = &d + &Atemp; /* d, or d+A if nonsquare */ - u.conditional_negate(!eps_is_sq); /* d, or -d-A if nonsquare */ - - MontgomeryPoint(u.as_bytes()) + #[cfg(feature = "elligator2")] + /// This decodes an elligator2 hidden point to a curve point on Curve25519. + pub fn from_representative(&self) -> MontgomeryPoint { + elligator2::map_to_point(&self.0) + } } /// A `ProjectivePoint` holds a point on the projective line @@ -641,20 +620,20 @@ mod test { #[test] #[cfg(feature = "alloc")] + #[cfg(feature = "elligator2")] fn montgomery_elligator_correct() { let bytes: Vec = (0u8..32u8).collect(); let bits_in: [u8; 32] = (&bytes[..]).try_into().expect("Range invariant broken"); - let fe = FieldElement::from_bytes(&bits_in); - let eg = elligator_encode(&fe); - assert_eq!(eg.to_bytes(), ELLIGATOR_CORRECT_OUTPUT); + let eg = elligator2::map_to_point(&bits_in); + assert_eq!(eg.0, ELLIGATOR_CORRECT_OUTPUT); } #[test] + #[cfg(feature = "elligator2")] fn montgomery_elligator_zero_zero() { let zero = [0u8; 32]; - let fe = FieldElement::from_bytes(&zero); - let eg = elligator_encode(&fe); - assert_eq!(eg.to_bytes(), zero); + let eg = elligator2::map_to_point(&zero); + assert_eq!(eg.0, zero); } } diff --git a/ed25519-dalek/src/signing.rs b/ed25519-dalek/src/signing.rs index 8999f50d..3d911dd3 100644 --- a/ed25519-dalek/src/signing.rs +++ b/ed25519-dalek/src/signing.rs @@ -774,7 +774,7 @@ impl<'d> Deserialize<'d> for SigningKey { )); } - SigningKey::try_from(bytes).map_err(serde::de::Error::custom) + Ok(SigningKey::from(bytes)) } } diff --git a/x25519-dalek/Cargo.toml b/x25519-dalek/Cargo.toml index 4169c55a..33712967 100644 --- a/x25519-dalek/Cargo.toml +++ b/x25519-dalek/Cargo.toml @@ -35,7 +35,7 @@ rustdoc-args = [ "--html-in-header", "docs/assets/rustdoc-include-katex-header.html", "--cfg", "docsrs", ] -features = ["getrandom", "reusable_secrets", "serde", "static_secrets"] +features = ["getrandom", "reusable_secrets", "serde", "static_secrets", "elligator2"] [dependencies] curve25519-dalek = { version = "4", path = "../curve25519-dalek", default-features = false } @@ -61,3 +61,4 @@ alloc = ["curve25519-dalek/alloc", "serde?/alloc", "zeroize?/alloc"] precomputed-tables = ["curve25519-dalek/precomputed-tables"] reusable_secrets = [] static_secrets = [] +elligator2 = ["curve25519-dalek/elligator2"] diff --git a/x25519-dalek/src/x25519.rs b/x25519-dalek/src/x25519.rs index 3b96f9cf..cb18c1b0 100644 --- a/x25519-dalek/src/x25519.rs +++ b/x25519-dalek/src/x25519.rs @@ -375,3 +375,137 @@ pub fn x25519(k: [u8; 32], u: [u8; 32]) -> [u8; 32] { pub const X25519_BASEPOINT_BYTES: [u8; 32] = [ 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; + +/// [`PublicKey`] transformation to a format indistinguishable from uniform +/// random. Requires feature `elligator2`. +/// +/// This allows public keys to be sent over an insecure channel without +/// revealing that an x25519 public key is being shared. +/// +/// # Example +#[cfg_attr(feature = "elligator2", doc = "```")] +#[cfg_attr(not(feature = "elligator2"), doc = "```ignore")] +/// use rand_core::OsRng; +/// use rand_core::RngCore; +/// +/// use x25519_dalek::x25519; +/// use x25519_dalek::EphemeralSecret; +/// use x25519_dalek::{PublicKey, PublicRepresentative}; +/// +/// // ~50% of points are not encodable as elligator representatives, but we +/// // want to ensure we select a keypair that is. +/// fn get_representable_ephemeral() -> EphemeralSecret { +/// for i in 0_u8..255 { +/// let secret = EphemeralSecret::random_from_rng(&mut OsRng); +/// match Option::::from(&secret) { +/// Some(_) => return secret, +/// None => continue, +/// } +/// } +/// panic!("we should definitely have found a key by now") +/// } +/// +/// // Generate Alice's key pair. +/// let alice_secret = get_representable_ephemeral(); +/// let alice_representative = Option::::from(&alice_secret).unwrap(); +/// +/// // Generate Bob's key pair. +/// let bob_secret = get_representable_ephemeral(); +/// let bob_representative = Option::::from(&bob_secret).unwrap(); +/// +/// // Alice and Bob should now exchange their representatives and reveal the +/// // public key from the other person. +/// let bob_public = PublicKey::from(&bob_representative); +/// +/// let alice_public = PublicKey::from(&alice_representative); +/// +/// // Once they've done so, they may generate a shared secret. +/// let alice_shared = alice_secret.diffie_hellman(&bob_public); +/// let bob_shared = bob_secret.diffie_hellman(&alice_public); +/// +/// assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); +/// ``` +#[cfg(feature = "elligator2")] +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +pub struct PublicRepresentative([u8; 32]); + +#[cfg(feature = "elligator2")] +impl PublicRepresentative { + /// View this public representative as a byte array. + #[inline] + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Extract this representative's bytes for serialization. + #[inline] + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +#[cfg(feature = "elligator2")] +impl AsRef<[u8]> for PublicRepresentative { + /// View this shared secret key as a byte array. + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +#[cfg(feature = "elligator2")] +impl From<[u8; 32]> for PublicRepresentative { + /// Build a Elligator2 Public key Representative from bytes + fn from(r: [u8; 32]) -> PublicRepresentative { + PublicRepresentative(r) + } +} + +#[cfg(feature = "elligator2")] +impl<'a> From<&'a [u8; 32]> for PublicRepresentative { + /// Build a Elligator2 Public key Representative from bytes by reference + fn from(r: &'a [u8; 32]) -> PublicRepresentative { + PublicRepresentative(*r) + } +} + +#[cfg(feature = "elligator2")] +impl<'a> From<&'a EphemeralSecret> for Option { + /// Given an x25519 [`EphemeralSecret`] key, compute its corresponding [`PublicRepresentative`]. + fn from(secret: &'a EphemeralSecret) -> Option { + let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0); + let res: Option<[u8; 32]> = repres; + Some(PublicRepresentative(res?)) + } +} + +#[cfg(feature = "reusable_secrets")] +#[cfg(feature = "elligator2")] +impl<'a> From<&'a ReusableSecret> for Option { + /// Given an x25519 [`ReusableSecret`] key, compute its corresponding [`PublicRepresentative`]. + fn from(secret: &'a ReusableSecret) -> Option { + let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0); + let res: Option<[u8; 32]> = repres; + Some(PublicRepresentative(res?)) + } +} + +#[cfg(feature = "static_secrets")] +#[cfg(feature = "elligator2")] +impl<'a> From<&'a StaticSecret> for Option { + /// Given an x25519 [`StaticSecret`] key, compute its corresponding [`PublicRepresentative`]. + fn from(secret: &'a StaticSecret) -> Option { + let repres = curve25519_dalek::elligator2::representative_from_privkey(&secret.0); + let res: Option<[u8; 32]> = repres; + Some(PublicRepresentative(res?)) + } +} + +#[cfg(feature = "elligator2")] +impl<'a> From<&'a PublicRepresentative> for PublicKey { + /// Given an elligator2 [`PublicRepresentative`], compute its corresponding [`PublicKey`]. + fn from(representative: &'a PublicRepresentative) -> PublicKey { + let point = curve25519_dalek::elligator2::map_to_point(&representative.0); + PublicKey(point) + } +}