monero_ed25519/
point.rs

1use subtle::{Choice, ConstantTimeEq, ConditionallySelectable};
2use zeroize::Zeroize;
3
4use sha3::{Digest as _, Keccak256};
5
6use crate::CompressedPoint;
7
8/// A decompressed point on the Ed25519 elliptic curve.
9#[derive(Clone, Copy, Eq, Debug, Zeroize)]
10pub struct Point(curve25519_dalek::EdwardsPoint);
11
12impl ConstantTimeEq for Point {
13  fn ct_eq(&self, other: &Self) -> Choice {
14    self.0.ct_eq(&other.0)
15  }
16}
17
18impl ConditionallySelectable for Point {
19  fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
20    Self(<_>::conditional_select(&a.0, &b.0, choice))
21  }
22}
23
24impl PartialEq for Point {
25  /// This defers to `ConstantTimeEq::ct_eq`.
26  fn eq(&self, other: &Self) -> bool {
27    bool::from(self.ct_eq(other))
28  }
29}
30
31impl Point {
32  /// Sample a biased point via a hash function.
33  ///
34  /// This is comparable to Monero's `hash_to_ec` function.
35  ///
36  /// This achieves parity with https://github.com/monero-project/monero
37  ///   /blob/389e3ba1df4a6df4c8f9d116aa239d4c00f5bc78/src/crypto/crypto.cpp#L611, inlining the
38  /// `ge_fromfe_frombytes_vartime` function (https://github.com/monero-project/monero
39  ///   /blob/389e3ba1df4a6df4c8f9d116aa239d4c00f5bc78/src/crypto/crypto-ops.c#L2309). This
40  /// implementation runs in constant time.
41  ///
42  /// According to the original authors
43  /// (<https://web.archive.org/web/20201028121818/https://cryptonote.org/whitepaper.pdf>), this
44  /// would implement <https://arxiv.org/abs/0706.1448>. Shen Noether also describes the algorithm
45  /// (<https://web.getmonero.org/resources/research-lab/pubs/ge_fromfe.pdf>), yet without
46  /// reviewing its security and in a very straight-forward fashion.
47  ///
48  /// In reality, this implements Elligator 2 as detailed in
49  /// "Elligator: Elliptic-curve points indistinguishable from uniform random strings"
50  /// (<https://eprint.iacr.org/2013/325>). Specifically, Section 5.5 details the application of
51  /// Elligator 2 to Curve25519, after which the result is mapped to Ed25519.
52  ///
53  /// As this only applies Elligator 2 once, it's limited to a subset of points where a certain
54  /// derivative of their `u` coordinates (in Montgomery form) are quadratic residues. It's biased
55  /// accordingly. The yielded points SHOULD still have uniform relations to each other however.
56  pub fn biased_hash(bytes: [u8; 32]) -> Self {
57    use crypto_bigint::{Encoding as _, modular::constant_mod::*, U256, impl_modulus, const_residue};
58
59    const MODULUS_STR: &str = "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed";
60    impl_modulus!(Two25519, U256, MODULUS_STR);
61
62    type Two25519Residue = Residue<Two25519, { U256::LIMBS }>;
63
64    /*
65      Curve25519 is a Montgomery curve with equation `v^2 = u^3 + 486662 u^2 + u`.
66
67      A Curve25519 point `(u, v)` may be mapped to an Ed25519 point `(x, y)` with the map
68      `(sqrt(-(A + 2)) u / v, (u - 1) / (u + 1))`.
69    */
70    const A_U256: U256 = U256::from_u64(486_662);
71    const A: Two25519Residue = const_residue!(A_U256, Two25519);
72    const NEGATIVE_A: Two25519Residue = A.neg();
73
74    // Sample a uniform field element
75    /*
76      This isn't a wide reduction, implying it'd be biased, yet the bias should only be negligible
77      due to the shape of the prime number. All elements within the prime field field have a
78      `2 / 2^{256}` chance of being selected, except for the first 19 which have a `3 / 2^256`
79      chance of being selected. In order for this 'third chance' (the bias) to be relevant, the
80      hash function would have to output a number greater than or equal to:
81
82        0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffda
83
84      which is of negligible probability.
85    */
86    let r = Two25519Residue::new(&U256::from_le_bytes(Keccak256::digest(bytes).into()));
87
88    // Per Section 5.5, take `u = 2`. This is the smallest quadratic non-residue in the field
89    let r_square = r.square();
90    let ur_square = r_square + r_square;
91
92    /*
93      We know this is non-zero as:
94
95      ```sage
96      p = 2**255 - 19
97      Mod((p - 1) * inverse_mod(2, p), p).is_square() == False
98      ```
99    */
100    let one_plus_ur_square = Two25519Residue::ONE + ur_square;
101    let (one_plus_ur_square_inv, _value_was_zero) = one_plus_ur_square.invert();
102    let upsilon = NEGATIVE_A * one_plus_ur_square_inv;
103    /*
104      Quoting section 5.5,
105      "then \epsilon = 1 and x = \upsilon. Otherwise \epsilon = -1, x = \upsilon u r^2"
106
107      Whereas in the specification present in Section 5.2, the expansion of the `u` coordinate when
108      `\epsilon = -1` is `-\upsilon - A`. Per Section 5.2, in the "Second case",
109      `= -\upsilon - A = \upsilon u r^2`. These two values are equivalent, yet the negation and
110      subtract outperform a multiplication.
111    */
112    let other_candidate = -upsilon - A;
113
114    // RFC-8032 provides `sqrt8k5`
115    fn is_quadratic_residue_8_mod_5(value: &Two25519Residue) -> Choice {
116      // (p + 3) // 8
117      const SQRT_EXP: U256 = Two25519::MODULUS.shr_vartime(3).wrapping_add(&U256::ONE);
118      // 2^{(p - 1) // 4}
119      const Z: Two25519Residue =
120        Two25519Residue::ONE.add(&Two25519Residue::ONE).pow(&Two25519::MODULUS.shr_vartime(2));
121      let y = value.pow(&SQRT_EXP);
122      let other_candidate = y * Z;
123      // If `value` is a quadratic residue, one of these will be its square root
124      y.square().ct_eq(value) | other_candidate.square().ct_eq(value)
125    }
126
127    /*
128      Check if `\upsilon` is a valid `u` coordinate by checking for a solution for the square root
129      of `\upsilon^3 + A \upsilon^2 + \upsilon`.
130    */
131    let epsilon = is_quadratic_residue_8_mod_5(&(((upsilon + A) * upsilon.square()) + upsilon));
132    let u = Two25519Residue::conditional_select(&other_candidate, &upsilon, epsilon);
133
134    // Map from Curve25519 to Ed25519
135    /*
136      Elligator 2's specification in section 5.2 says to choose the negative square root as the
137      `v` coordinate if `\upsilon` was chosen (as signaled by `\epsilon = 1`). The following
138      chooses the odd `y` coordinate if `\upsilon` was chosen, which is functionally equivalent.
139    */
140    let res = curve25519_dalek::MontgomeryPoint(u.retrieve().to_le_bytes())
141      .to_edwards(epsilon.unwrap_u8())
142      .expect("neither Elligator 2 candidate was a square");
143
144    // Ensure this point lies within the prime-order subgroup
145    Self::from(res.mul_by_cofactor())
146  }
147
148  /// Compress a point to a `CompressedPoint`.
149  pub fn compress(self) -> CompressedPoint {
150    CompressedPoint::from(self.0.compress().to_bytes())
151  }
152
153  /// Create a `Point` from a `curve25519_dalek::EdwardsPoint`.
154  ///
155  /// This is hidden as it is not part of our API commitment. No guarantees are made for it.
156  #[doc(hidden)]
157  pub fn from(point: curve25519_dalek::EdwardsPoint) -> Self {
158    Self(point)
159  }
160
161  /// Create a `curve25519_dalek::EdwardsPoint` from a `Point`.
162  ///
163  /// This is hidden as it is not part of our API commitment. No guarantees are made for it.
164  #[doc(hidden)]
165  pub fn into(self) -> curve25519_dalek::EdwardsPoint {
166    self.0
167  }
168
169  /// Interpret a point as a key image.
170  ///
171  /// This is hidden as it is not part of our API commitment. No guarantees are made for it.
172  #[doc(hidden)]
173  pub fn key_image(self) -> Option<curve25519_dalek::EdwardsPoint> {
174    use curve25519_dalek::traits::IsIdentity as _;
175    if self.0.is_identity() || (!self.0.is_torsion_free()) {
176      None?;
177    }
178    Some(self.0)
179  }
180}