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}