Skip to content

Commit 357fb84

Browse files
authored
Merge pull request #262 from tomato42/edwards-precompute
eddsa: add support for point precomputation
2 parents c7b5e06 + 67b1688 commit 357fb84

File tree

5 files changed

+125
-15
lines changed

5 files changed

+125
-15
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ On an Intel Core i7 4790K @ 4.0GHz I'm getting the following performance:
110110
SECP112r2: 28 0.00015s 6697.11 0.00015s 6479.98 0.00028s 3524.72 0.00058s 1716.16
111111
SECP128r1: 32 0.00018s 5497.65 0.00019s 5272.89 0.00036s 2747.39 0.00072s 1396.16
112112
SECP160r1: 42 0.00025s 3949.32 0.00026s 3894.45 0.00046s 2153.85 0.00102s 985.07
113-
Ed25519: 64 0.00166s 600.71 0.00131s 761.86 0.00432s 231.37 0.00445s 224.59
114-
Ed448: 114 0.00473s 211.38 0.00406s 246.25 0.01299s 76.96 0.01293s 77.32
113+
Ed25519: 64 0.00076s 1324.48 0.00042s 2405.01 0.00109s 918.05 0.00344s 290.50
114+
Ed448: 114 0.00176s 569.53 0.00115s 870.94 0.00282s 355.04 0.01024s 97.69
115115
116116
ecdh ecdh/s
117117
NIST192p: 0.00104s 964.89
@@ -154,8 +154,8 @@ On the same machine I'm getting the following performance with `gmpy2`:
154154
SECP112r2: 28 0.00009s 11322.97 0.00009s 10857.71 0.00017s 5748.77 0.00032s 3094.28
155155
SECP128r1: 32 0.00010s 10078.39 0.00010s 9665.27 0.00019s 5200.58 0.00036s 2760.88
156156
SECP160r1: 42 0.00015s 6875.51 0.00015s 6647.35 0.00029s 3422.41 0.00057s 1768.35
157-
Ed25519: 64 0.00070s 1423.69 0.00057s 1756.70 0.00195s 511.92 0.00194s 516.64
158-
Ed448: 114 0.00149s 670.07 0.00126s 790.52 0.00434s 230.58 0.00438s 228.50
157+
Ed25519: 64 0.00030s 3322.56 0.00018s 5568.63 0.00046s 2165.35 0.00153s 654.02
158+
Ed448: 114 0.00060s 1680.53 0.00039s 2567.40 0.00096s 1036.67 0.00350s 285.62
159159
160160
ecdh ecdh/s
161161
NIST192p: 0.00050s 1985.70

src/ecdsa/eddsa.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def _sha512(data):
4343

4444
curve_ed25519 = ellipticcurve.CurveEdTw(_p, _a, _d, _h, _sha512)
4545
generator_ed25519 = ellipticcurve.PointEdwards(
46-
curve_ed25519, _Gx, _Gy, 1, _Gx * _Gy % _p, _r
46+
curve_ed25519, _Gx, _Gy, 1, _Gx * _Gy % _p, _r, generator=True
4747
)
4848

4949

@@ -76,7 +76,7 @@ def _shake256(data):
7676

7777
curve_ed448 = ellipticcurve.CurveEdTw(_p, _a, _d, _h, _shake256)
7878
generator_ed448 = ellipticcurve.PointEdwards(
79-
curve_ed448, _Gx, _Gy, 1, _Gx * _Gy % _p, _r
79+
curve_ed448, _Gx, _Gy, 1, _Gx * _Gy % _p, _r, generator=True
8080
)
8181

8282

@@ -116,6 +116,12 @@ def __ne__(self, other):
116116
def point(self):
117117
return self.__point
118118

119+
@point.setter
120+
def point(self, other):
121+
if self.__point != other:
122+
raise ValueError("Can't change the coordinates of the point")
123+
self.__point = other
124+
119125
def public_point(self):
120126
return self.__point
121127

src/ecdsa/ellipticcurve.py

+68-5
Original file line numberDiff line numberDiff line change
@@ -1272,7 +1272,7 @@ class PointEdwards(AbstractPoint):
12721272
x*y = T / Z
12731273
"""
12741274

1275-
def __init__(self, curve, x, y, z, t, order=None):
1275+
def __init__(self, curve, x, y, z, t, order=None, generator=False):
12761276
"""
12771277
Initialise a point that uses the extended coordinates internally.
12781278
"""
@@ -1284,6 +1284,8 @@ def __init__(self, curve, x, y, z, t, order=None):
12841284
else: # pragma: no branch
12851285
self.__coords = (x, y, z, t)
12861286
self.__order = order
1287+
self.__generator = generator
1288+
self.__precompute = []
12871289

12881290
@classmethod
12891291
def from_bytes(
@@ -1311,8 +1313,9 @@ def from_bytes(
13111313
supported
13121314
:param int order: the point order, must be non zero when using
13131315
generator=True
1314-
:param bool generator: Ignored, may be used in the future
1315-
to precompute point multiplication table.
1316+
:param bool generator: Flag to mark the point as a curve generator,
1317+
this will cause the library to pre-compute some values to
1318+
make repeated usages of the point much faster
13161319
13171320
:raises MalformedPointError: if the public point does not lay on the
13181321
curve or the encoding is invalid
@@ -1324,9 +1327,46 @@ def from_bytes(
13241327
curve, data, validate_encoding, valid_encodings
13251328
)
13261329
return PointEdwards(
1327-
curve, coord_x, coord_y, 1, coord_x * coord_y, order
1330+
curve, coord_x, coord_y, 1, coord_x * coord_y, order, generator
13281331
)
13291332

1333+
def _maybe_precompute(self):
1334+
if not self.__generator or self.__precompute:
1335+
return self.__precompute
1336+
1337+
# since this code will execute just once, and it's fully deterministic,
1338+
# depend on atomicity of the last assignment to switch from empty
1339+
# self.__precompute to filled one and just ignore the unlikely
1340+
# situation when two threads execute it at the same time (as it won't
1341+
# lead to inconsistent __precompute)
1342+
order = self.__order
1343+
assert order
1344+
precompute = []
1345+
i = 1
1346+
order *= 2
1347+
coord_x, coord_y, coord_z, coord_t = self.__coords
1348+
prime = self.__curve.p()
1349+
1350+
doubler = PointEdwards(
1351+
self.__curve, coord_x, coord_y, coord_z, coord_t, order
1352+
)
1353+
# for "protection" against Minerva we need 1 or 2 more bits depending
1354+
# on order bit size, but it's easier to just calculate one
1355+
# point more always
1356+
order *= 4
1357+
1358+
while i < order:
1359+
doubler = doubler.scale()
1360+
coord_x, coord_y = doubler.x(), doubler.y()
1361+
coord_t = coord_x * coord_y % prime
1362+
precompute.append((coord_x, coord_y, coord_t))
1363+
1364+
i *= 2
1365+
doubler = doubler.double()
1366+
1367+
self.__precompute = precompute
1368+
return self.__precompute
1369+
13301370
def x(self):
13311371
"""Return affine x coordinate."""
13321372
X1, _, Z1, _ = self.__coords
@@ -1482,6 +1522,27 @@ def __rmul__(self, other):
14821522
"""Multiply point by an integer."""
14831523
return self * other
14841524

1525+
def _mul_precompute(self, other):
1526+
"""Multiply point by integer with precomputation table."""
1527+
X3, Y3, Z3, T3, p, a = 0, 1, 1, 0, self.__curve.p(), self.__curve.a()
1528+
_add = self._add
1529+
for X2, Y2, T2 in self.__precompute:
1530+
rem = other % 4
1531+
if rem == 0 or rem == 2:
1532+
other //= 2
1533+
elif rem == 3:
1534+
other = (other + 1) // 2
1535+
X3, Y3, Z3, T3 = _add(X3, Y3, Z3, T3, -X2, Y2, 1, -T2, p, a)
1536+
else:
1537+
assert rem == 1
1538+
other = (other - 1) // 2
1539+
X3, Y3, Z3, T3 = _add(X3, Y3, Z3, T3, X2, Y2, 1, T2, p, a)
1540+
1541+
if not X3 or not T3:
1542+
return INFINITY
1543+
1544+
return PointEdwards(self.__curve, X3, Y3, Z3, T3, self.__order)
1545+
14851546
def __mul__(self, other):
14861547
"""Multiply point by an integer."""
14871548
X2, Y2, Z2, T2 = self.__coords
@@ -1490,8 +1551,10 @@ def __mul__(self, other):
14901551
if other == 1:
14911552
return self
14921553
if self.__order:
1493-
# order*2 as a protection for Minerva
1554+
# order*2 as a "protection" for Minerva
14941555
other = other % (self.__order * 2)
1556+
if self._maybe_precompute():
1557+
return self._mul_precompute(other)
14951558

14961559
X3, Y3, Z3, T3 = 0, 1, 1, 0 # INFINITY in extended coordinates
14971560
p, a = self.__curve.p(), self.__curve.a()

src/ecdsa/keys.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,20 @@ def precompute(self, lazy=False):
263263
use (when set to True)
264264
"""
265265
if isinstance(self.curve.curve, CurveEdTw):
266-
return
267-
self.pubkey.point = ellipticcurve.PointJacobi.from_affine(
268-
self.pubkey.point, True
269-
)
266+
pt = self.pubkey.point
267+
self.pubkey.point = ellipticcurve.PointEdwards(
268+
pt.curve(),
269+
pt.x(),
270+
pt.y(),
271+
1,
272+
pt.x() * pt.y(),
273+
self.curve.order,
274+
generator=True,
275+
)
276+
else:
277+
self.pubkey.point = ellipticcurve.PointJacobi.from_affine(
278+
self.pubkey.point, True
279+
)
270280
# as precomputation in now delayed to the time of first use of the
271281
# point and we were asked specifically to precompute now, make
272282
# sure the precomputation is performed now to preserve the behaviour

src/ecdsa/test_eddsa.py

+31
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,25 @@ def test_invalid_signature_length(self):
580580

581581
self.assertIn("length", str(e.exception))
582582

583+
def test_changing_public_key(self):
584+
key = PublicKey(generator_ed25519, b"\x01" * 32)
585+
586+
g = key.point
587+
588+
new_g = PointEdwards(curve_ed25519, g.x(), g.y(), 1, g.x() * g.y())
589+
590+
key.point = new_g
591+
592+
self.assertEqual(g, key.point)
593+
594+
def test_changing_public_key_to_different_point(self):
595+
key = PublicKey(generator_ed25519, b"\x01" * 32)
596+
597+
with self.assertRaises(ValueError) as e:
598+
key.point = generator_ed25519
599+
600+
self.assertIn("coordinates", str(e.exception))
601+
583602
def test_invalid_s_value(self):
584603
key = PublicKey(
585604
generator_ed25519,
@@ -651,6 +670,18 @@ def test_ed448_encode_decode(multiple):
651670
assert a == b
652671

653672

673+
@settings(**HYP_SETTINGS)
674+
@example(1)
675+
@example(2)
676+
@given(st.integers(min_value=1, max_value=int(generator_ed25519.order()) - 1))
677+
def test_ed25519_mul_precompute_vs_naf(multiple):
678+
"""Compare multiplication with and without precomputation."""
679+
g = generator_ed25519
680+
new_g = PointEdwards(curve_ed25519, g.x(), g.y(), 1, g.x() * g.y())
681+
682+
assert g * multiple == multiple * new_g
683+
684+
654685
# Test vectors from RFC 8032
655686
TEST_VECTORS = [
656687
# TEST 1

0 commit comments

Comments
 (0)