Skip to content

eddsa: add support for point precomputation #262

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ On an Intel Core i7 4790K @ 4.0GHz I'm getting the following performance:
SECP112r2: 28 0.00015s 6697.11 0.00015s 6479.98 0.00028s 3524.72 0.00058s 1716.16
SECP128r1: 32 0.00018s 5497.65 0.00019s 5272.89 0.00036s 2747.39 0.00072s 1396.16
SECP160r1: 42 0.00025s 3949.32 0.00026s 3894.45 0.00046s 2153.85 0.00102s 985.07
Ed25519: 64 0.00166s 600.71 0.00131s 761.86 0.00432s 231.37 0.00445s 224.59
Ed448: 114 0.00473s 211.38 0.00406s 246.25 0.01299s 76.96 0.01293s 77.32
Ed25519: 64 0.00076s 1324.48 0.00042s 2405.01 0.00109s 918.05 0.00344s 290.50
Ed448: 114 0.00176s 569.53 0.00115s 870.94 0.00282s 355.04 0.01024s 97.69

ecdh ecdh/s
NIST192p: 0.00104s 964.89
Expand Down Expand Up @@ -154,8 +154,8 @@ On the same machine I'm getting the following performance with `gmpy2`:
SECP112r2: 28 0.00009s 11322.97 0.00009s 10857.71 0.00017s 5748.77 0.00032s 3094.28
SECP128r1: 32 0.00010s 10078.39 0.00010s 9665.27 0.00019s 5200.58 0.00036s 2760.88
SECP160r1: 42 0.00015s 6875.51 0.00015s 6647.35 0.00029s 3422.41 0.00057s 1768.35
Ed25519: 64 0.00070s 1423.69 0.00057s 1756.70 0.00195s 511.92 0.00194s 516.64
Ed448: 114 0.00149s 670.07 0.00126s 790.52 0.00434s 230.58 0.00438s 228.50
Ed25519: 64 0.00030s 3322.56 0.00018s 5568.63 0.00046s 2165.35 0.00153s 654.02
Ed448: 114 0.00060s 1680.53 0.00039s 2567.40 0.00096s 1036.67 0.00350s 285.62

ecdh ecdh/s
NIST192p: 0.00050s 1985.70
Expand Down
10 changes: 8 additions & 2 deletions src/ecdsa/eddsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def _sha512(data):

curve_ed25519 = ellipticcurve.CurveEdTw(_p, _a, _d, _h, _sha512)
generator_ed25519 = ellipticcurve.PointEdwards(
curve_ed25519, _Gx, _Gy, 1, _Gx * _Gy % _p, _r
curve_ed25519, _Gx, _Gy, 1, _Gx * _Gy % _p, _r, generator=True
)


Expand Down Expand Up @@ -76,7 +76,7 @@ def _shake256(data):

curve_ed448 = ellipticcurve.CurveEdTw(_p, _a, _d, _h, _shake256)
generator_ed448 = ellipticcurve.PointEdwards(
curve_ed448, _Gx, _Gy, 1, _Gx * _Gy % _p, _r
curve_ed448, _Gx, _Gy, 1, _Gx * _Gy % _p, _r, generator=True
)


Expand Down Expand Up @@ -116,6 +116,12 @@ def __ne__(self, other):
def point(self):
return self.__point

@point.setter
def point(self, other):
if self.__point != other:
raise ValueError("Can't change the coordinates of the point")
self.__point = other

def public_point(self):
return self.__point

Expand Down
73 changes: 68 additions & 5 deletions src/ecdsa/ellipticcurve.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,7 @@ class PointEdwards(AbstractPoint):
x*y = T / Z
"""

def __init__(self, curve, x, y, z, t, order=None):
def __init__(self, curve, x, y, z, t, order=None, generator=False):
"""
Initialise a point that uses the extended coordinates internally.
"""
Expand All @@ -1284,6 +1284,8 @@ def __init__(self, curve, x, y, z, t, order=None):
else: # pragma: no branch
self.__coords = (x, y, z, t)
self.__order = order
self.__generator = generator
self.__precompute = []

@classmethod
def from_bytes(
Expand Down Expand Up @@ -1311,8 +1313,9 @@ def from_bytes(
supported
:param int order: the point order, must be non zero when using
generator=True
:param bool generator: Ignored, may be used in the future
to precompute point multiplication table.
:param bool generator: Flag to mark the point as a curve generator,
this will cause the library to pre-compute some values to
make repeated usages of the point much faster

:raises MalformedPointError: if the public point does not lay on the
curve or the encoding is invalid
Expand All @@ -1324,9 +1327,46 @@ def from_bytes(
curve, data, validate_encoding, valid_encodings
)
return PointEdwards(
curve, coord_x, coord_y, 1, coord_x * coord_y, order
curve, coord_x, coord_y, 1, coord_x * coord_y, order, generator
)

def _maybe_precompute(self):
if not self.__generator or self.__precompute:
return self.__precompute

# since this code will execute just once, and it's fully deterministic,
# depend on atomicity of the last assignment to switch from empty
# self.__precompute to filled one and just ignore the unlikely
# situation when two threads execute it at the same time (as it won't
# lead to inconsistent __precompute)
order = self.__order
assert order
precompute = []
i = 1
order *= 2
coord_x, coord_y, coord_z, coord_t = self.__coords
prime = self.__curve.p()

doubler = PointEdwards(
self.__curve, coord_x, coord_y, coord_z, coord_t, order
)
# for "protection" against Minerva we need 1 or 2 more bits depending
# on order bit size, but it's easier to just calculate one
# point more always
order *= 4

while i < order:
doubler = doubler.scale()
coord_x, coord_y = doubler.x(), doubler.y()
coord_t = coord_x * coord_y % prime
precompute.append((coord_x, coord_y, coord_t))

i *= 2
doubler = doubler.double()

self.__precompute = precompute
return self.__precompute

def x(self):
"""Return affine x coordinate."""
X1, _, Z1, _ = self.__coords
Expand Down Expand Up @@ -1482,6 +1522,27 @@ def __rmul__(self, other):
"""Multiply point by an integer."""
return self * other

def _mul_precompute(self, other):
"""Multiply point by integer with precomputation table."""
X3, Y3, Z3, T3, p, a = 0, 1, 1, 0, self.__curve.p(), self.__curve.a()
_add = self._add
for X2, Y2, T2 in self.__precompute:
rem = other % 4
if rem == 0 or rem == 2:
other //= 2
elif rem == 3:
other = (other + 1) // 2
X3, Y3, Z3, T3 = _add(X3, Y3, Z3, T3, -X2, Y2, 1, -T2, p, a)
else:
assert rem == 1
other = (other - 1) // 2
X3, Y3, Z3, T3 = _add(X3, Y3, Z3, T3, X2, Y2, 1, T2, p, a)

if not X3 or not T3:
return INFINITY

return PointEdwards(self.__curve, X3, Y3, Z3, T3, self.__order)

def __mul__(self, other):
"""Multiply point by an integer."""
X2, Y2, Z2, T2 = self.__coords
Expand All @@ -1490,8 +1551,10 @@ def __mul__(self, other):
if other == 1:
return self
if self.__order:
# order*2 as a protection for Minerva
# order*2 as a "protection" for Minerva
other = other % (self.__order * 2)
if self._maybe_precompute():
return self._mul_precompute(other)

X3, Y3, Z3, T3 = 0, 1, 1, 0 # INFINITY in extended coordinates
p, a = self.__curve.p(), self.__curve.a()
Expand Down
18 changes: 14 additions & 4 deletions src/ecdsa/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,20 @@ def precompute(self, lazy=False):
use (when set to True)
"""
if isinstance(self.curve.curve, CurveEdTw):
return
self.pubkey.point = ellipticcurve.PointJacobi.from_affine(
self.pubkey.point, True
)
pt = self.pubkey.point
self.pubkey.point = ellipticcurve.PointEdwards(
pt.curve(),
pt.x(),
pt.y(),
1,
pt.x() * pt.y(),
self.curve.order,
generator=True,
)
else:
self.pubkey.point = ellipticcurve.PointJacobi.from_affine(
self.pubkey.point, True
)
# as precomputation in now delayed to the time of first use of the
# point and we were asked specifically to precompute now, make
# sure the precomputation is performed now to preserve the behaviour
Expand Down
31 changes: 31 additions & 0 deletions src/ecdsa/test_eddsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,25 @@ def test_invalid_signature_length(self):

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

def test_changing_public_key(self):
key = PublicKey(generator_ed25519, b"\x01" * 32)

g = key.point

new_g = PointEdwards(curve_ed25519, g.x(), g.y(), 1, g.x() * g.y())

key.point = new_g

self.assertEqual(g, key.point)

def test_changing_public_key_to_different_point(self):
key = PublicKey(generator_ed25519, b"\x01" * 32)

with self.assertRaises(ValueError) as e:
key.point = generator_ed25519

self.assertIn("coordinates", str(e.exception))

def test_invalid_s_value(self):
key = PublicKey(
generator_ed25519,
Expand Down Expand Up @@ -651,6 +670,18 @@ def test_ed448_encode_decode(multiple):
assert a == b


@settings(**HYP_SETTINGS)
@example(1)
@example(2)
@given(st.integers(min_value=1, max_value=int(generator_ed25519.order()) - 1))
def test_ed25519_mul_precompute_vs_naf(multiple):
"""Compare multiplication with and without precomputation."""
g = generator_ed25519
new_g = PointEdwards(curve_ed25519, g.x(), g.y(), 1, g.x() * g.y())

assert g * multiple == multiple * new_g


# Test vectors from RFC 8032
TEST_VECTORS = [
# TEST 1
Expand Down