From 67b1688bcbf282c4a0d5da2f04e269a521de4ead Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Fri, 13 Aug 2021 17:50:06 +0200 Subject: [PATCH] eddsa: add support for point precomputation --- README.md | 8 ++--- src/ecdsa/eddsa.py | 10 ++++-- src/ecdsa/ellipticcurve.py | 73 +++++++++++++++++++++++++++++++++++--- src/ecdsa/keys.py | 18 +++++++--- src/ecdsa/test_eddsa.py | 31 ++++++++++++++++ 5 files changed, 125 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ccea21cc..340cb493 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/ecdsa/eddsa.py b/src/ecdsa/eddsa.py index 4cc2a7f3..82be0389 100644 --- a/src/ecdsa/eddsa.py +++ b/src/ecdsa/eddsa.py @@ -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 ) @@ -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 ) @@ -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 diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 9edd9abc..5306eca2 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -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. """ @@ -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( @@ -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 @@ -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 @@ -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 @@ -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() diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 04555ee6..7b2a43b1 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -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 diff --git a/src/ecdsa/test_eddsa.py b/src/ecdsa/test_eddsa.py index f3b09e7e..374eb3d5 100644 --- a/src/ecdsa/test_eddsa.py +++ b/src/ecdsa/test_eddsa.py @@ -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, @@ -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