Skip to content

Commit 5e1bc8c

Browse files
author
Michael Rosenberg
committed
Defined map_to_curve_restricted; gave it input and output guarantees
1 parent 95bf940 commit 5e1bc8c

File tree

1 file changed

+42
-27
lines changed

1 file changed

+42
-27
lines changed

curve25519-dalek/src/lizard/lizard_ristretto.rs

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ impl RistrettoPoint {
3131
let digest = D::digest(data);
3232
fe_bytes[0..32].copy_from_slice(digest.as_slice());
3333
fe_bytes[8..24].copy_from_slice(data);
34-
// Clear the appropriate bits to make this a reduced positive field elem, and map to curve
35-
RistrettoPoint::map_pos_felem_to_curve(fe_bytes)
34+
// Clear the appropriate bits to be able to call map_to_curve_restricted
35+
fe_bytes[31] &= 0b00111111;
36+
fe_bytes[0] &= 0b11111110;
37+
38+
RistrettoPoint::map_to_curve_restricted(fe_bytes)
3639
}
3740

3841
/// Decode 16 bytes of data from a RistrettoPoint, using the Lizard method. Returns `None` if
@@ -180,37 +183,45 @@ impl RistrettoPoint {
180183
]
181184
}
182185

183-
/// Clears bits in `bytes` to make it a positive, reduced field element, then performs
184-
/// [`RistrettoPoint::map_to_curve`]). Specifically, clears the the bottom bit (bottom bit of
185-
/// `bytes[0]`) and second-to-top bit (second-to-top bit of `bytes[31]`). The first is to ensure
186-
/// we have a positive field element and the second is to ensure we are below the modulus
187-
/// (map-to-curve clears the topmost bit for us).
188-
pub fn map_pos_felem_to_curve(mut bytes: [u8; 32]) -> RistrettoPoint {
189-
bytes[0] &= 0b11111110;
190-
bytes[31] &= 0b10111111;
186+
/// Does the same as [`RistrettoPoint::map_to_curve`], but only operates on a restricted domain,
187+
/// namely `bytes` with the bottom bit and the top two bits unset. It is guaranteed that
188+
/// `map_to_curve_inverse(map_to_curve_restricted(bytes))` contains `bytes` in one of its first
189+
/// 8 return values (i.e., the positive ones).
190+
///
191+
/// # Panics
192+
/// Panics if the bottom bit (bottom bit of `bytes[0]`) or either of the top two bits (top two
193+
/// bits of `bytes[31]`) are set.
194+
pub fn map_to_curve_restricted(bytes: [u8; 32]) -> RistrettoPoint {
195+
// Ensure it's a positive field element
196+
assert_eq!(bytes[0] & 0b00000001, 0);
197+
// Ensure it's less than below 2^254 (which is less than p)
198+
assert_eq!(bytes[31] & 0b11000000, 0);
191199

192200
RistrettoPoint::map_to_curve(bytes)
193201
}
194202

195203
/// Interprets the given bytestring as a field element and computes the Ristretto Elligator map.
196-
/// This is the MAP function in
197-
/// [RFC 9496](https://www.rfc-editor.org/rfc/rfc9496.html#section-4.3.4-4).
198-
/// Note this clears the top bit (`bytes[31] & 0x80`).
204+
/// This is the [MAP](https://www.rfc-editor.org/rfc/rfc9496.html#section-4.3.4-4) function in
205+
/// RFC 9496.
199206
///
200207
/// # Warning
201208
///
202209
/// This function does not produce cryptographically random-looking Ristretto points. Use
203210
/// [`Self::hash_from_bytes`] for that. DO NOT USE THIS FUNCTION unless you really know what
204211
/// you're doing.
205-
pub fn map_to_curve(mut bytes: [u8; 32]) -> RistrettoPoint {
206-
bytes[31] &= 0b01111111;
207-
212+
pub fn map_to_curve(bytes: [u8; 32]) -> RistrettoPoint {
213+
// MAP must clear the top bit. This is done in `from_bytes` for every field backend, so we
214+
// don't have to do it ourselves here
208215
let fe = FieldElement::from_bytes(&bytes);
209216
RistrettoPoint::elligator_ristretto_flavor(&fe)
210217
}
211218

212219
/// Computes the possible bytestrings that could have produced this point via
213220
/// [`Self::map_to_curve`].
221+
///
222+
/// The first 8 return values are positive field elements (i.e., have the LSB (`bytes[0] & 1`)
223+
/// unset), if defined. The last 8 return values are negative field elements (i.e., have the LSB
224+
/// set), if defined.
214225
pub fn map_to_curve_inverse(&self) -> [CtOption<[u8; 32]>; 16] {
215226
// Compute the inverses
216227
let fes = self.elligator_ristretto_flavor_inverse();
@@ -365,7 +376,8 @@ mod test {
365376
}
366377
}
367378

368-
// Tests that map_to_curve_inverse ○ map_pos_felem_to_curve is the identity
379+
// Tests that map_to_curve_inverse ○ map_to_curve_restricted is the identity and has a non-None
380+
// return value in the first 8 elements
369381
#[test]
370382
fn map_pos_felem_to_curve_inverse() {
371383
let mut rng = rand::rng();
@@ -374,22 +386,25 @@ mod test {
374386
let mut input = [0u8; 32];
375387
rng.fill_bytes(&mut input);
376388

377-
// Map to Ristretto and invert it
378-
let pt = RistrettoPoint::map_pos_felem_to_curve(input);
379-
let inverses = pt.map_to_curve_inverse();
389+
// Clear bits so we can call map_to_curve_restricted
390+
input[31] &= 0b00111111;
391+
input[0] &= 0b11111110;
380392

381-
// map_pos_felem_to_curve masks the bottom bit and top two bits of `input`
382-
let mut expected_inverse = input;
383-
expected_inverse[31] &= 0b00111111;
384-
expected_inverse[0] &= 0b11111110;
393+
let pt = RistrettoPoint::map_to_curve_restricted(input);
394+
let inverses = pt.map_to_curve_inverse();
385395

386396
// Check that one of the valid inverses matches the input
387397
let mut found = false;
388-
for inv in inverses.into_iter() {
389-
if inv.is_some().into() && inv.unwrap() == expected_inverse {
398+
for (i, inv) in inverses.into_iter().enumerate() {
399+
if inv.is_some().into() && inv.unwrap() == input {
400+
// map_to_curve_inverse returns all the positive solutions in the first 8
401+
// values, and all the negative solution in the second 8 values
402+
if i >= 8 {
403+
panic!("input is in the latter 8 inverses ({input:02x?})");
404+
}
390405
// Per the README, the probability of finding two inverses is ~2^-122
391406
if found == true {
392-
panic!("found two inverses for input {:02x?}", input);
407+
panic!("found two inverses ({input:02x?})");
393408
}
394409

395410
found = true;

0 commit comments

Comments
 (0)