Skip to content

Commit 80c6f8a

Browse files
authored
Point generation in subgroup tests (#7)
change the point generation in subgroup tests for organization and incorporate changes from the upstream crate
1 parent b98883e commit 80c6f8a

File tree

9 files changed

+194
-53
lines changed

9 files changed

+194
-53
lines changed

curve25519-elligator2/Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ name = "curve25519-elligator2"
44
# - update CHANGELOG
55
# - update README if required by semver
66
# - if README was updated, also update module documentation in src/lib.rs
7-
version = "0.1.0-alpha.1"
7+
version = "0.1.0-alpha.2"
88
edition = "2021"
99
rust-version = "1.60.0"
1010
authors = ["Isis Lovecruft <[email protected]>",
@@ -77,3 +77,13 @@ digest = ["dep:digest", "elligator2"]
7777

7878
[target.'cfg(all(not(curve25519_dalek_backend = "fiat"), not(curve25519_dalek_backend = "serial"), target_arch = "x86_64"))'.dependencies]
7979
curve25519-dalek-derive = { version = "0.1.1" }
80+
81+
[lints.rust.unexpected_cfgs]
82+
level = "warn"
83+
check-cfg = [
84+
'cfg(allow_unused_unsafe)',
85+
'cfg(curve25519_dalek_backend, values("fiat", "serial", "simd"))',
86+
'cfg(curve25519_dalek_diagnostics, values("build"))',
87+
'cfg(curve25519_dalek_bits, values("32", "64"))',
88+
'cfg(nightly)',
89+
]

curve25519-elligator2/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use in the interim period. As such, this crate:
1414
To import `curve25519-elligator2`, add the following to the dependencies section of
1515
your project's `Cargo.toml`:
1616
```toml
17-
curve25519-elligator2 = "0.1.0-alpha.1"
17+
curve25519-elligator2 = "0.1.0-alpha.2"
1818
```
1919

2020

curve25519-elligator2/src/backend/serial/u32/scalar.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
1313
use core::fmt::Debug;
1414
use core::ops::{Index, IndexMut};
15+
use subtle::{Choice, ConditionallySelectable};
1516

1617
#[cfg(feature = "zeroize")]
1718
use zeroize::Zeroize;
@@ -196,10 +197,12 @@ impl Scalar29 {
196197
}
197198

198199
// conditionally add l if the difference is negative
199-
let underflow_mask = ((borrow >> 31) ^ 1).wrapping_sub(1);
200200
let mut carry: u32 = 0;
201201
for i in 0..9 {
202-
carry = (carry >> 29) + difference[i] + (constants::L[i] & underflow_mask);
202+
let underflow = Choice::from((borrow >> 31) as u8);
203+
204+
let addend = u32::conditional_select(&0, &constants::L[i], underflow);
205+
carry = (carry >> 29) + difference[i] + addend;
203206
difference[i] = carry & mask;
204207
}
205208

curve25519-elligator2/src/backend/serial/u64/scalar.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
1414
use core::fmt::Debug;
1515
use core::ops::{Index, IndexMut};
16+
use subtle::{Choice, ConditionallySelectable};
1617

1718
#[cfg(feature = "zeroize")]
1819
use zeroize::Zeroize;
@@ -185,10 +186,12 @@ impl Scalar52 {
185186
}
186187

187188
// conditionally add l if the difference is negative
188-
let underflow_mask = ((borrow >> 63) ^ 1).wrapping_sub(1);
189189
let mut carry: u64 = 0;
190190
for i in 0..5 {
191-
carry = (carry >> 52) + difference[i] + (constants::L[i] & underflow_mask);
191+
let underflow = Choice::from((borrow >> 63) as u8);
192+
193+
let addend = u64::conditional_select(&0, &constants::L[i], underflow);
194+
carry = (carry >> 52) + difference[i] + addend;
192195
difference[i] = carry & mask;
193196
}
194197

curve25519-elligator2/src/backend/vector/ifma/edwards.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ impl<'a> From<&'a edwards::EdwardsPoint> for NafLookupTable8<CachedPoint> {
249249
}
250250
}
251251

252-
#[cfg(target_feature = "avx512ifma,avx512vl")]
252+
#[cfg(all(target_feature = "avx512ifma", target_feature = "avx512vl"))]
253253
#[cfg(test)]
254254
mod test {
255255
use super::*;

curve25519-elligator2/src/backend/vector/ifma/field.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ impl<'a, 'b> Mul<&'b F51x4Reduced> for &'a F51x4Reduced {
629629
}
630630
}
631631

632-
#[cfg(target_feature = "avx512ifma,avx512vl")]
632+
#[cfg(all(target_feature = "avx512ifma", target_feature = "avx512vl"))]
633633
#[cfg(test)]
634634
mod test {
635635
use super::*;

curve25519-elligator2/src/elligator2/subgroup.rs

Lines changed: 167 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,43 @@
11
use super::*;
2-
use crate::{MontgomeryPoint, Scalar};
2+
use crate::scalar::test::BASEPOINT_ORDER_MINUS_ONE;
3+
use crate::{traits::IsIdentity, MontgomeryPoint};
34

4-
use rand::{thread_rng, Rng};
5+
use rand::Rng;
6+
use rand_core::{CryptoRng, RngCore};
7+
8+
// Generates a new Keypair using, and returns the public key representative
9+
// along, with its public key as a newly allocated edwards25519.Point.
10+
fn generate<R: RngCore + CryptoRng>(rng: &mut R) -> ([u8; 32], EdwardsPoint) {
11+
for _ in 0..63 {
12+
let y_sk = rng.gen::<[u8; 32]>();
13+
let y_sk_tweak = rng.next_u32() as u8;
14+
15+
let y_repr_bytes = match Randomized::to_representative(&y_sk, y_sk_tweak).into() {
16+
Some(r) => r,
17+
None => continue,
18+
};
19+
let y_pk = Randomized::mul_base_clamped(y_sk);
20+
21+
assert_eq!(
22+
MontgomeryPoint::from_representative::<Randomized>(&y_repr_bytes)
23+
.expect("failed to re-derive point from representative"),
24+
y_pk.to_montgomery()
25+
);
26+
27+
return (y_repr_bytes, y_pk);
28+
}
29+
panic!("failed to generate a valid keypair");
30+
}
31+
32+
/// Returns a new edwards25519.Point that is v multiplied by the subgroup order.
33+
///
34+
/// BASEPOINT_ORDER_MINUS_ONE is the same as scMinusOne in filippo.io/edwards25519.
35+
/// https://github.com/FiloSottile/edwards25519/blob/v1.0.0/scalar.go#L34
36+
fn scalar_mult_order(v: &EdwardsPoint) -> EdwardsPoint {
37+
// v * (L - 1) + v => v * L
38+
let p = v * BASEPOINT_ORDER_MINUS_ONE;
39+
p + v
40+
}
541

642
#[test]
743
#[cfg(feature = "elligator2")]
@@ -30,45 +66,6 @@ use rand::{thread_rng, Rng};
3066
// work around this by multiplying the point by L - 1, then adding the
3167
// point once to the product.
3268
fn pubkey_subgroup_check() {
33-
// This is the same as scMinusOne in filippo.io/edwards25519.
34-
// https://github.com/FiloSottile/edwards25519/blob/v1.0.0/scalar.go#L34
35-
let scalar_order_minus1 = Scalar::from_canonical_bytes([
36-
236_u8, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0,
37-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16,
38-
])
39-
.unwrap();
40-
41-
// Returns a new edwards25519.Point that is v multiplied by the subgroup order.
42-
let scalar_mult_order = |v: &EdwardsPoint| -> EdwardsPoint {
43-
// v * (L - 1) + v => v * L
44-
let p = v * scalar_order_minus1;
45-
p + v
46-
};
47-
48-
// Generates a new Keypair using, and returns the public key representative
49-
// along, with its public key as a newly allocated edwards25519.Point.
50-
let generate = || -> ([u8; 32], EdwardsPoint) {
51-
for _ in 0..63 {
52-
let y_sk = thread_rng().gen::<[u8; 32]>();
53-
let y_sk_tweak = thread_rng().gen::<u8>();
54-
55-
let y_repr_bytes = match Randomized::to_representative(&y_sk, y_sk_tweak).into() {
56-
Some(r) => r,
57-
None => continue,
58-
};
59-
let y_pk = Randomized::mul_base_clamped(y_sk);
60-
61-
assert_eq!(
62-
MontgomeryPoint::from_representative::<Randomized>(&y_repr_bytes)
63-
.expect("failed to re-derive point from representative"),
64-
y_pk.to_montgomery()
65-
);
66-
67-
return (y_repr_bytes, y_pk);
68-
}
69-
panic!("failed to generate a valid keypair");
70-
};
71-
7269
// These are all the points of low order that may result from
7370
// multiplying an Elligator-mapped point by L. We will test that all of
7471
// them are covered.
@@ -94,8 +91,10 @@ fn pubkey_subgroup_check() {
9491
// and break the loop when it reaches 8, so when representatives are
9592
// actually uniform we will usually run much fewer iterations.
9693
let mut num_covered = 0;
94+
95+
let mut rng = rand::thread_rng();
9796
for _ in 0..255 {
98-
let (repr, pk) = generate();
97+
let (repr, pk) = generate(&mut rng);
9998
let v = scalar_mult_order(&pk);
10099

101100
let b = v.compress().to_bytes();
@@ -131,3 +130,129 @@ fn pubkey_subgroup_check() {
131130
panic!("not all low order points were covered")
132131
}
133132
}
133+
134+
#[test]
135+
fn off_subgroup_check_edw() {
136+
let mut rng = rand::thread_rng();
137+
for _ in 0..100 {
138+
let (repr, pk) = generate(&mut rng);
139+
140+
// check if the generated public key is off the subgroup
141+
let v = scalar_mult_order(&pk);
142+
let pk_off = !v.is_identity();
143+
144+
// ---
145+
146+
// check if the public key derived from the representative (top bit 0)
147+
// is off the subgroup
148+
let mut yr_255 = repr;
149+
yr_255[31] &= 0xbf;
150+
let pk_255 = EdwardsPoint::from_representative::<RFC9380>(&yr_255)
151+
.expect("from_repr_255, should never fail");
152+
let v = scalar_mult_order(&pk_255);
153+
let off_255 = !v.is_identity();
154+
155+
// check if the public key derived from the representative (top two bits 0 - as
156+
// our representatives are) is off the subgroup.
157+
let mut yr_254 = repr;
158+
yr_254[31] &= 0x3f;
159+
let pk_254 = EdwardsPoint::from_representative::<RFC9380>(&yr_254)
160+
.expect("from_repr_254, should never fail");
161+
let v = scalar_mult_order(&pk_254);
162+
let off_254 = !v.is_identity();
163+
164+
println!("pk_gen: {pk_off}, pk_255: {off_255}, pk_254: {off_254}");
165+
}
166+
}
167+
168+
use crate::constants::BASEPOINT_ORDER_PRIVATE;
169+
170+
fn check(pk: MontgomeryPoint) -> bool {
171+
let z = pk * BASEPOINT_ORDER_PRIVATE;
172+
!z.is_identity()
173+
}
174+
175+
/// check a point in the group, assuming it is a representative and given a
176+
/// variant by which to convert it to a point.
177+
fn check_r<V: MapToPointVariant>(r: [u8; 32]) -> bool {
178+
let pk = MontgomeryPoint::from_representative::<V>(&r).expect("from_representative failed");
179+
check(pk)
180+
}
181+
182+
#[test]
183+
/// Somehow there should be a montgomery distinguisher where all real representatives
184+
/// map to the curve subgroup. For our keys this should only (consistently) happen
185+
/// for representatives with the top two bits cleared (i.e. 254 but representatives).
186+
fn off_subgroup_check_mgt() {
187+
let mut rng = rand::thread_rng();
188+
189+
for _ in 0..100 {
190+
let (mut repr, pk) = generate(&mut rng);
191+
repr[31] &= MASK_UNSET_BYTE;
192+
193+
let off_pk = check(pk.to_montgomery());
194+
195+
let off_rfc = check_r::<RFC9380>(repr);
196+
197+
let off_rand = check_r::<Randomized>(repr);
198+
199+
let (u, _v) = elligator_dir_map(repr);
200+
let u = MontgomeryPoint(u.as_bytes());
201+
let off = check(u);
202+
203+
println!("pk: {off_pk},\trfc: {off_rfc},\trand: {off_rand},\tcust: {off}");
204+
}
205+
}
206+
207+
#[test]
208+
/// Somehow there should be a montgomery distinguisher where all real representatives
209+
/// map to the curve subgroup. For our keys this should only (consistently) happen
210+
/// for representatives with the top two bits cleared (i.e. 254 but representatives).
211+
fn off_subgroup_check_custom() {
212+
let mut rng = rand::thread_rng();
213+
214+
for _ in 0..100 {
215+
let (mut repr, _) = generate(&mut rng);
216+
repr[31] &= MASK_UNSET_BYTE;
217+
218+
let (u, _v) = elligator_dir_map(repr);
219+
let u = MontgomeryPoint(u.as_bytes());
220+
let off = check(u);
221+
222+
println!("custom: {off}");
223+
}
224+
}
225+
226+
/// Direct elligator map translate as accurately as possible from `obfs4-subgroup-check.py`.
227+
fn elligator_dir_map(rb: [u8; 32]) -> (FieldElement, FieldElement) {
228+
let r = FieldElement::from_bytes(&rb);
229+
let two = &FieldElement::ONE + &FieldElement::ONE;
230+
let ufactor = &-&two * &SQRT_M1;
231+
let (_, vfactor) = FieldElement::sqrt_ratio_i(&ufactor, &FieldElement::ONE);
232+
233+
let u = r.square();
234+
let t1 = r.square2();
235+
let v = &t1 + &FieldElement::ONE;
236+
let t2 = v.square();
237+
let t3 = MONTGOMERY_A.square();
238+
let t3 = &t3 * &t1;
239+
let t3 = &t3 - &t2;
240+
let t3 = &t3 * &MONTGOMERY_A;
241+
let t1 = &t2 * &v;
242+
243+
let (is_sq, t1) = FieldElement::sqrt_ratio_i(&FieldElement::ONE, &(&t3 * &t1));
244+
let u = &u * &ufactor;
245+
let v = &r * &vfactor;
246+
let u = FieldElement::conditional_select(&u, &FieldElement::ONE, is_sq);
247+
let v = FieldElement::conditional_select(&v, &FieldElement::ONE, is_sq);
248+
let v = &v * &t3;
249+
let v = &v * &t1;
250+
let t1 = t1.square();
251+
let u = &u * &-&MONTGOMERY_A;
252+
let u = &u * &t3;
253+
let u = &u * &t2;
254+
let u = &u * &t1;
255+
let t1 = -&v;
256+
let v = FieldElement::conditional_select(&v, &t1, is_sq ^ v.is_negative());
257+
(u, v)
258+
}

curve25519-elligator2/src/montgomery.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ mod test {
508508
let mut csprng = rand_core::OsRng;
509509

510510
for _ in 0..100 {
511-
let p_edwards = rand_prime_order_point(&mut csprng);
511+
let p_edwards = rand_prime_order_point(csprng);
512512
let p_montgomery: MontgomeryPoint = p_edwards.to_montgomery();
513513

514514
let s: Scalar = Scalar::random(&mut csprng);
@@ -527,7 +527,7 @@ mod test {
527527

528528
for _ in 0..100 {
529529
// Make a random prime-order point P
530-
let p_edwards = rand_prime_order_point(&mut csprng);
530+
let p_edwards = rand_prime_order_point(csprng);
531531
let p_montgomery: MontgomeryPoint = p_edwards.to_montgomery();
532532

533533
// Make a random integer b

curve25519-elligator2/src/scalar.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1480,7 +1480,7 @@ pub(crate) mod test {
14801480
0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
14811481
];
14821482

1483-
const BASEPOINT_ORDER_MINUS_ONE: Scalar = Scalar {
1483+
pub(crate) const BASEPOINT_ORDER_MINUS_ONE: Scalar = Scalar {
14841484
bytes: [
14851485
0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9,
14861486
0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

0 commit comments

Comments
 (0)