From 0e876d4cd6604731bd0dcc23a14975a854483eaa Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 2 Dec 2019 18:04:16 +0100 Subject: [PATCH 01/34] explicit test cov for special case in verifies() --- src/ecdsa/test_ecdsa.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ecdsa/test_ecdsa.py b/src/ecdsa/test_ecdsa.py index 200cacef..26bcbddc 100644 --- a/src/ecdsa/test_ecdsa.py +++ b/src/ecdsa/test_ecdsa.py @@ -1,7 +1,7 @@ from __future__ import print_function import sys import hypothesis.strategies as st -from hypothesis import given, settings, note +from hypothesis import given, settings, note, example try: import unittest2 as unittest except ImportError: @@ -429,6 +429,7 @@ def st_random_gen_key_msg_nonce(draw): SIG_VER_SETTINGS = dict(HYP_SETTINGS) SIG_VER_SETTINGS["max_examples"] = 10 @settings(**SIG_VER_SETTINGS) +@example((generator_224, 4, 1, 1)) @given(st_random_gen_key_msg_nonce()) def test_sig_verify(args): """ From a93e33ddf9a9b483c22d77c6c73ebe3ec0dec308 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 9 Nov 2019 23:12:54 +0100 Subject: [PATCH 02/34] microoptimise contains_point do one multiplication less --- src/ecdsa/ellipticcurve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 9d5e6530..f56a5747 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -65,7 +65,7 @@ def b(self): def contains_point(self, x, y): """Is the point (x,y) on this curve?""" - return (y * y - (x * x * x + self.__a * x + self.__b)) % self.__p == 0 + return (y * y - ((x * x + self.__a) * x + self.__b)) % self.__p == 0 def __str__(self): return "CurveFp(p=%d, a=%d, b=%d)" % (self.__p, self.__a, self.__b) From 0133840152dada74616e257f0104374819823ea6 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 11 Nov 2019 01:03:50 +0100 Subject: [PATCH 03/34] prepare Point for Jacobi implementation make operations work correctly with PointJacobi in future since scaling Jacobi implementation to affine representation is expensive it's less computationally intensive to perform those operations using jacobi representation --- src/ecdsa/ellipticcurve.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index f56a5747..1125156a 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -101,6 +101,8 @@ def __add__(self, other): # X9.62 B.3: + if not isinstance(other, Point): + return NotImplemented if other == INFINITY: return self if self == INFINITY: From b70d7357639e90757356180921923963c6c5ba0a Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 16:58:07 +0200 Subject: [PATCH 04/34] add implementation of EC using Jacobi representation since Jacobi representation doesn't require calculation of modulo inverse for every addition or doubling, operations on it are much faster than with affine coordinates --- src/ecdsa/ellipticcurve.py | 253 +++++++++++++++++++++++++++++++++++ src/ecdsa/test_jacobi.py | 267 +++++++++++++++++++++++++++++++++++++ 2 files changed, 520 insertions(+) create mode 100644 src/ecdsa/test_jacobi.py diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 1125156a..859e8ccd 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -1,4 +1,5 @@ #! /usr/bin/env python +# -*- coding: utf-8 -*- # # Implementation of elliptic curves, for cryptographic applications. # @@ -70,6 +71,258 @@ def contains_point(self, x, y): def __str__(self): return "CurveFp(p=%d, a=%d, b=%d)" % (self.__p, self.__a, self.__b) + +class PointJacobi(object): + """ + Point on an elliptic curve. Uses Jacobi coordinates. + + In Jacobian coordinates, there are three parameters, X, Y and Z. + They correspond to affine parameters 'x' and 'y' like so: + + x = X / Z² + y = Y / Z³ + """ + def __init__(self, curve, x, y, z, order=None): + self.__curve = curve + self.__x = x + self.__y = y + self.__z = z + self.__order = order + + def __eq__(self, other): + """Compare two points with each-other.""" + if (not self.__y or not self.__z) and other is INFINITY: + return True + if isinstance(other, Point): + return self.to_affine() == other + return self.x() == other.x() and self.y() == other.y() + + def order(self): + return self.__order + + def curve(self): + return self.__curve + + def x(self): + """ + Return affine x coordinate. + + This method should be used only when the 'y' coordinate is not needed. + It's computationally more efficient to use `to_affine()` and then + call x() and y() on the returned instance. + """ + if self.__z == 1: + return self.__x + p = self.__curve.p() + z = numbertheory.inverse_mod(self.__z, p) + return self.__x * z**2 % p + + def y(self): + """ + Return affine y coordinate. + + This method should be used only when the 'x' coordinate is not needed. + It's computationally more efficient to use `to_affine()` and then + call x() and y() on the returned instance. + """ + if self.__z == 1: + return self.__y + p = self.__curve.p() + z = numbertheory.inverse_mod(self.__z, p) + return self.__y * z**3 % p + + def to_affine(self): + """Return point in affine form.""" + p = self.__curve.p() + z = numbertheory.inverse_mod(self.__z, p) + return Point(self.__curve, self.__x * z**2 % p, + self.__y * z**3 % p, self.__order) + + @staticmethod + def from_affine(point): + """Create from an affine point.""" + return PointJacobi(point.curve(), point.x(), point.y(), 1, + point.order()) + + # plese note that all the methods that use the equations from hyperelliptic + # are formatted in a way to maximise performance. + # Things that make code faster: multiplying instead of taking to the power + # (`xx = x * x; xxxx = xx * xx % p` is faster than `xxxx = x**4 % p` and + # `pow(x, 4, p)`), + # multiple assignments at the same time (`x1, x2 = self.x1, self.x2` is + # faster than `x1 = self.x1; x2 = self.x2`), + # similarly, sometimes the `% p` is skipped if it makes the calculation + # faster and the result of calculation is later reduced modulo `p` + + def double(self): + """Add a point to itself.""" + if not self.__y: + return INFINITY + + p = self.__curve.p() + a = self.__curve.a() + + X1, Y1, Z1 = self.__x, self.__y, self.__z + + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl + + XX, YY = X1 * X1 % p, Y1 * Y1 % p + if not YY: + return INFINITY + YYYY = YY * YY % p + ZZ = Z1 * Z1 % p + S = 2 * ((X1 + YY)**2 - XX - YYYY) % p + M = (3 * XX + a * ZZ * ZZ) % p + T = (M * M - 2 * S) % p + # X3 = T + Y3 = (M * (S - T) - 8 * YYYY) % p + Z3 = ((Y1 + Z1)**2 - YY - ZZ) % p + + return PointJacobi(self.__curve, T, Y3, Z3, self.__order) + + def _add_with_z_1(self, X1, Y1, X2, Y2): + """add points when both Z1 and Z2 equal 1""" + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-mmadd-2007-bl + p = self.__curve.p() + H = X2 - X1 + HH = H * H + I = 4 * HH % p + J = H * I + r = 2 * (Y2 - Y1) + if not H and not r: + return self.double() + V = X1 * I + X3 = (r**2 - J - 2 * V) % p + Y3 = (r * (V - X3) - 2 * Y1 * J) % p + Z3 = 2 * H % p + if not Y3 or not Z3: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + + def _add_with_z_eq(self, X1, Y1, Z1, X2, Y2): + """add points when Z1 == Z2""" + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-zadd-2007-m + p = self.__curve.p() + A = (X2 - X1)**2 % p + B = X1 * A % p + C = X2 * A + D = (Y2 - Y1)**2 % p + if not A and not D: + return self.double() + X3 = (D - B - C) % p + Y3 = ((Y2 - Y1) * (B - X3) - Y1 * (C - B)) % p + Z3 = Z1 * (X2 - X1) % p + if not Y3 or not Z3: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + + def _add_with_z2_1(self, X1, Y1, Z1, X2, Y2): + """add points when Z2 == 1""" + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-madd-2007-bl + p = self.__curve.p() + Z1Z1 = Z1 * Z1 % p + U2, S2 = X2 * Z1Z1 % p, Y2 * Z1 * Z1Z1 % p + H = (U2 - X1) % p + HH = H * H % p + I = 4 * HH % p + J = H * I + r = 2 * (S2 - Y1) % p + if not r and not H: + return self.double() + V = X1 * I + X3 = (r * r - J - 2 * V) % p + Y3 = (r * (V - X3) - 2 * Y1 * J) % p + Z3 = ((Z1 + H)**2 - Z1Z1 - HH) % p + if not Y3 or not Z3: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + + def __radd__(self, other): + return self + other + + def __add__(self, other): + """Add two points on elliptic curve.""" + if self == INFINITY: + return other + if other == INFINITY: + return self + if isinstance(other, Point): + other = PointJacobi.from_affine(other) + if self.__curve != other.__curve: + raise ValueError("The other point is on different curve") + + p = self.__curve.p() + X1, Y1, Z1 = self.__x, self.__y, self.__z + X2, Y2, Z2 = other.__x, other.__y, other.__z + if Z1 == Z2: + if Z1 == 1: + return self._add_with_z_1(X1, Y1, X2, Y2) + return self._add_with_z_eq(X1, Y1, Z1, X2, Y2) + if Z1 == 1: + return self._add_with_z2_1(X2, Y2, Z2, X1, Y1) + if Z2 == 1: + return self._add_with_z2_1(X1, Y1, Z1, X2, Y2) + + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-2007-bl + Z1Z1 = Z1 * Z1 % p + Z2Z2 = Z2 * Z2 % p + U1 = X1 * Z2Z2 % p + U2 = X2 * Z1Z1 % p + S1 = Y1 * Z2 * Z2Z2 % p + S2 = Y2 * Z1 * Z1Z1 % p + H = U2 - U1 + I = 4 * H * H % p + J = H * I % p + r = 2 * (S2 - S1) % p + if not H and not r: + return self.double() + V = U1 * I + X3 = (r * r - J - 2 * V) % p + Y3 = (r * (V - X3) - 2 * S1 * J) % p + Z3 = ((Z1 + Z2)**2 - Z1Z1 - Z2Z2) * H % p + + if not Y3 or not Z3: + return INFINITY + + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + + def __rmul__(self, other): + """Multiply point by an integer.""" + return self * other + + def __mul__(self, other): + """Multiply point by an integer.""" + if not self.__y or not other: + return INFINITY + if other == 1: + return self + # makes vulnerable to Minerva + # if self.__order: + # other = other % self.__order + + def leftmost_bit(x): + assert x > 0 + result = 1 + while result <= x: + result = 2 * result + return result // 2 + + e = other + i = leftmost_bit(e) + result = self + while i > 1: + result = result.double() + i = i // 2 + if e & i != 0: + result = result + self + return result + + class Point(object): """A point on an elliptic curve. Altering x and y is forbidding, but they can be read by the x() and y() methods.""" diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py new file mode 100644 index 00000000..63e8de28 --- /dev/null +++ b/src/ecdsa/test_jacobi.py @@ -0,0 +1,267 @@ + +try: + import unittest2 as unittest +except ImportError: + import unittest + +import hypothesis.strategies as st +from hypothesis import given, assume, settings, example + +from .ellipticcurve import Point, PointJacobi, INFINITY +from .ecdsa import generator_256, curve_256, generator_224 +from .numbertheory import inverse_mod + +class TestJacobi(unittest.TestCase): + def test___init__(self): + curve = object() + x = object() + y = object() + z = 1 + order = object() + pj = PointJacobi(curve, x, y, z, order) + + self.assertIs(pj.order(), order) + self.assertIs(pj.curve(), curve) + self.assertIs(pj.x(), x) + self.assertIs(pj.y(), y) + + def test_add_with_different_curves(self): + p_a = PointJacobi.from_affine(generator_256) + p_b = PointJacobi.from_affine(generator_224) + + with self.assertRaises(ValueError): + p_a + p_b + + def test_conversion(self): + pj = PointJacobi.from_affine(generator_256) + pw = pj.to_affine() + + self.assertEqual(generator_256, pw) + + def test_single_double(self): + pj = PointJacobi.from_affine(generator_256) + pw = generator_256.double() + + pj = pj.double() + + self.assertEqual(pj.x(), pw.x()) + self.assertEqual(pj.y(), pw.y()) + + def test_double_with_zero_point(self): + pj = PointJacobi(curve_256, 0, 0, 1) + + pj = pj.double() + + self.assertIs(pj, INFINITY) + + def test_double_with_zero_equivalent_point(self): + pj = PointJacobi(curve_256, 0, curve_256.p(), 1) + + pj = pj.double() + + self.assertIs(pj, INFINITY) + + def test_compare_with_affine_point(self): + pj = PointJacobi.from_affine(generator_256) + pa = pj.to_affine() + + self.assertEqual(pj, pa) + self.assertEqual(pa, pj) + + def test_add_with_affine_point(self): + pj = PointJacobi.from_affine(generator_256) + pa = pj.to_affine() + + s = pj + pa + + self.assertEqual(s, pj.double()) + + def test_radd_with_affine_point(self): + pj = PointJacobi.from_affine(generator_256) + pa = pj.to_affine() + + s = pa + pj + + self.assertEqual(s, pj.double()) + + def test_add_with_infinity(self): + pj = PointJacobi.from_affine(generator_256) + + s = pj + INFINITY + + self.assertEqual(s, pj) + + def test_add_zero_point_to_affine(self): + pa = PointJacobi.from_affine(generator_256).to_affine() + pj = PointJacobi(curve_256, 0, 0, 1) + + s = pj + pa + + self.assertIs(s, pa) + + def test_multiply_by_zero(self): + pj = PointJacobi.from_affine(generator_256) + + pj = pj * 0 + + self.assertIs(pj, INFINITY) + + def test_zero_point_multiply_by_one(self): + pj = PointJacobi(curve_256, 0, 0, 1) + + pj = pj * 1 + + self.assertIs(pj, INFINITY) + + def test_multiply_by_one(self): + pj = PointJacobi.from_affine(generator_256) + pw = generator_256 * 1 + + pj = pj * 1 + + self.assertEqual(pj.x(), pw.x()) + self.assertEqual(pj.y(), pw.y()) + + def test_multiply_by_two(self): + pj = PointJacobi.from_affine(generator_256) + pw = generator_256 * 2 + + pj = pj * 2 + + self.assertEqual(pj.x(), pw.x()) + self.assertEqual(pj.y(), pw.y()) + + def test_rmul_by_two(self): + pj = PointJacobi.from_affine(generator_256) + pw = generator_256 * 2 + + pj = 2 * pj + + self.assertEqual(pj, pw) + + def test_compare_non_zero_with_infinity(self): + pj = PointJacobi.from_affine(generator_256) + + self.assertNotEqual(pj, INFINITY) + + def test_compare_zero_point_with_infinity(self): + pj = PointJacobi(curve_256, 0, 0, 1) + + self.assertEqual(pj, INFINITY) + + def test_compare_double_with_multiply(self): + pj = PointJacobi.from_affine(generator_256) + dbl = pj.double() + mlpl = pj * 2 + + self.assertEqual(dbl, mlpl) + + @settings(max_examples=10) + @given(st.integers(min_value=0, max_value=generator_256.order())) + def test_multiplications(self, mul): + pj = PointJacobi.from_affine(generator_256) + pw = pj.to_affine() * mul + + pj = pj * mul + + self.assertEqual((pj.x(), pj.y()), (pw.x(), pw.y())) + + @settings(max_examples=10) + @given(st.integers(min_value=1, max_value=generator_256.order()), + st.integers(min_value=1, max_value=generator_256.order())) + @example(3, 3) + def test_add_scaled_points(self, a_mul, b_mul): + j_g = PointJacobi.from_affine(generator_256) + a = PointJacobi.from_affine(j_g * a_mul) + b = PointJacobi.from_affine(j_g * b_mul) + + c = a + b + + self.assertEqual(c, j_g * (a_mul + b_mul)) + + @settings(max_examples=10) + @given(st.integers(min_value=1, max_value=generator_256.order()), + st.integers(min_value=1, max_value=generator_256.order()), + st.integers(min_value=1, max_value=curve_256.p()-1)) + def test_add_one_scaled_point(self, a_mul, b_mul, new_z): + j_g = PointJacobi.from_affine(generator_256) + a = PointJacobi.from_affine(j_g * a_mul) + b = PointJacobi.from_affine(j_g * b_mul) + + p = curve_256.p() + + assume(inverse_mod(new_z, p)) + + new_zz = new_z * new_z % p + + b = PointJacobi( + curve_256, b.x() * new_zz % p, b.y() * new_zz * new_z % p, new_z) + + c = a + b + + self.assertEqual(c, j_g * (a_mul + b_mul)) + + @settings(max_examples=10) + @given(st.integers(min_value=1, max_value=generator_256.order()), + st.integers(min_value=1, max_value=generator_256.order()), + st.integers(min_value=1, max_value=curve_256.p()-1)) + @example(1, 1, 1) + @example(3, 3, 3) + @example(2, generator_256.order()-2, 1) + @example(2, generator_256.order()-2, 3) + def test_add_same_scale_points(self, a_mul, b_mul, new_z): + j_g = PointJacobi.from_affine(generator_256) + a = PointJacobi.from_affine(j_g * a_mul) + b = PointJacobi.from_affine(j_g * b_mul) + + p = curve_256.p() + + assume(inverse_mod(new_z, p)) + + new_zz = new_z * new_z % p + + a = PointJacobi( + curve_256, a.x() * new_zz % p, a.y() * new_zz * new_z % p, new_z) + b = PointJacobi( + curve_256, b.x() * new_zz % p, b.y() * new_zz * new_z % p, new_z) + + c = a + b + + self.assertEqual(c, j_g * (a_mul + b_mul)) + + @settings(max_examples=14) + @given(st.integers(min_value=1, max_value=generator_256.order()), + st.integers(min_value=1, max_value=generator_256.order()), + st.lists(st.integers(min_value=1, max_value=curve_256.p()-1), + min_size=2, max_size=2, unique=True)) + @example(2, 2, [2, 1]) + @example(2, 2, [2, 3]) + @example(2, generator_256.order()-2, [2, 3]) + @example(2, generator_256.order()-2, [2, 1]) + def test_add_different_scale_points(self, a_mul, b_mul, new_z): + j_g = PointJacobi.from_affine(generator_256) + a = PointJacobi.from_affine(j_g * a_mul) + b = PointJacobi.from_affine(j_g * b_mul) + + p = curve_256.p() + + assume(inverse_mod(new_z[0], p)) + assume(inverse_mod(new_z[1], p)) + + new_zz0 = new_z[0] * new_z[0] % p + new_zz1 = new_z[1] * new_z[1] % p + + a = PointJacobi( + curve_256, + a.x() * new_zz0 % p, + a.y() * new_zz0 * new_z[0] % p, + new_z[0]) + b = PointJacobi( + curve_256, + b.x() * new_zz1 % p, + b.y() * new_zz1 * new_z[1] % p, + new_z[1]) + + c = a + b + + self.assertEqual(c, j_g * (a_mul + b_mul)) From d1ded442907f44d5a48df90f3b3ea2cab5083818 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 19:53:42 +0200 Subject: [PATCH 05/34] precompute for generators --- src/ecdsa/ecdsa.py | 46 +++++++++++++++++++++----------------- src/ecdsa/ellipticcurve.py | 44 +++++++++++++++++++++++++++++++----- src/ecdsa/test_jacobi.py | 14 ++++++++++++ 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index fae9609a..6ca0dabf 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -268,7 +268,8 @@ def point_is_valid(generator, x, y): _Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811 curve_192 = ellipticcurve.CurveFp(_p, -3, _b) -generator_192 = ellipticcurve.Point(curve_192, _Gx, _Gy, _r) +generator_192 = ellipticcurve.PointJacobi( + curve_192, _Gx, _Gy, 1, _r, generator=True) # NIST Curve P-224: @@ -281,7 +282,8 @@ def point_is_valid(generator, x, y): _Gy = 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34 curve_224 = ellipticcurve.CurveFp(_p, -3, _b) -generator_224 = ellipticcurve.Point(curve_224, _Gx, _Gy, _r) +generator_224 = ellipticcurve.PointJacobi( + curve_224, _Gx, _Gy, 1, _r, generator=True) # NIST Curve P-256: _p = 115792089210356248762697446949407573530086143415290314195533631308867097853951 @@ -293,7 +295,8 @@ def point_is_valid(generator, x, y): _Gy = 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5 curve_256 = ellipticcurve.CurveFp(_p, -3, _b) -generator_256 = ellipticcurve.Point(curve_256, _Gx, _Gy, _r) +generator_256 = ellipticcurve.PointJacobi( + curve_256, _Gx, _Gy, 1, _r, generator=True) # NIST Curve P-384: _p = 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319 @@ -305,7 +308,8 @@ def point_is_valid(generator, x, y): _Gy = 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f curve_384 = ellipticcurve.CurveFp(_p, -3, _b) -generator_384 = ellipticcurve.Point(curve_384, _Gx, _Gy, _r) +generator_384 = ellipticcurve.PointJacobi( + curve_384, _Gx, _Gy, 1, _r, generator=True) # NIST Curve P-521: _p = 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151 @@ -317,7 +321,8 @@ def point_is_valid(generator, x, y): _Gy = 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650 curve_521 = ellipticcurve.CurveFp(_p, -3, _b) -generator_521 = ellipticcurve.Point(curve_521, _Gx, _Gy, _r) +generator_521 = ellipticcurve.PointJacobi( + curve_521, _Gx, _Gy, 1, _r, generator=True) # Certicom secp256-k1 _a = 0x0000000000000000000000000000000000000000000000000000000000000000 @@ -328,7 +333,8 @@ def point_is_valid(generator, x, y): _r = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 curve_secp256k1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_secp256k1 = ellipticcurve.Point(curve_secp256k1, _Gx, _Gy, _r) +generator_secp256k1 = ellipticcurve.PointJacobi( + curve_secp256k1, _Gx, _Gy, 1, _r, generator=True) # Brainpool P-160-r1 _a = 0x340E7BE2A280EB74E2BE61BADA745D97E8F7C300 @@ -339,8 +345,8 @@ def point_is_valid(generator, x, y): _q = 0xE95E4A5F737059DC60DF5991D45029409E60FC09 curve_brainpoolp160r1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_brainpoolp160r1 = ellipticcurve.Point( - curve_brainpoolp160r1, _Gx, _Gy, _q) +generator_brainpoolp160r1 = ellipticcurve.PointJacobi( + curve_brainpoolp160r1, _Gx, _Gy, 1, _q, generator=True) # Brainpool P-192-r1 _a = 0x6A91174076B1E0E19C39C031FE8685C1CAE040E5C69A28EF @@ -351,8 +357,8 @@ def point_is_valid(generator, x, y): _q = 0xC302F41D932A36CDA7A3462F9E9E916B5BE8F1029AC4ACC1 curve_brainpoolp192r1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_brainpoolp192r1 = ellipticcurve.Point( - curve_brainpoolp192r1, _Gx, _Gy, _q) +generator_brainpoolp192r1 = ellipticcurve.PointJacobi( + curve_brainpoolp192r1, _Gx, _Gy, 1, _q, generator=True) # Brainpool P-224-r1 _a = 0x68A5E62CA9CE6C1C299803A6C1530B514E182AD8B0042A59CAD29F43 @@ -363,8 +369,8 @@ def point_is_valid(generator, x, y): _q = 0xD7C134AA264366862A18302575D0FB98D116BC4B6DDEBCA3A5A7939F curve_brainpoolp224r1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_brainpoolp224r1 = ellipticcurve.Point( - curve_brainpoolp224r1, _Gx, _Gy, _q) +generator_brainpoolp224r1 = ellipticcurve.PointJacobi( + curve_brainpoolp224r1, _Gx, _Gy, 1, _q, generator=True) # Brainpool P-256-r1 _a = 0x7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9 @@ -375,8 +381,8 @@ def point_is_valid(generator, x, y): _q = 0xA9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7 curve_brainpoolp256r1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_brainpoolp256r1 = ellipticcurve.Point( - curve_brainpoolp256r1, _Gx, _Gy, _q) +generator_brainpoolp256r1 = ellipticcurve.PointJacobi( + curve_brainpoolp256r1, _Gx, _Gy, 1, _q, generator=True) # Brainpool P-320-r1 _a = 0x3EE30B568FBAB0F883CCEBD46D3F3BB8A2A73513F5EB79DA66190EB085FFA9F492F375A97D860EB4 @@ -387,8 +393,8 @@ def point_is_valid(generator, x, y): _q = 0xD35E472036BC4FB7E13C785ED201E065F98FCFA5B68F12A32D482EC7EE8658E98691555B44C59311 curve_brainpoolp320r1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_brainpoolp320r1 = ellipticcurve.Point( - curve_brainpoolp320r1, _Gx, _Gy, _q) +generator_brainpoolp320r1 = ellipticcurve.PointJacobi( + curve_brainpoolp320r1, _Gx, _Gy, 1, _q, generator=True) # Brainpool P-384-r1 _a = 0x7BC382C63D8C150C3C72080ACE05AFA0C2BEA28E4FB22787139165EFBA91F90F8AA5814A503AD4EB04A8C7DD22CE2826 @@ -399,8 +405,8 @@ def point_is_valid(generator, x, y): _q = 0x8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC3103B883202E9046565 curve_brainpoolp384r1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_brainpoolp384r1 = ellipticcurve.Point( - curve_brainpoolp384r1, _Gx, _Gy, _q) +generator_brainpoolp384r1 = ellipticcurve.PointJacobi( + curve_brainpoolp384r1, _Gx, _Gy, 1, _q, generator=True) # Brainpool P-512-r1 _a = 0x7830A3318B603B89E2327145AC234CC594CBDD8D3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CA @@ -411,5 +417,5 @@ def point_is_valid(generator, x, y): _q = 0xAADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA92619418661197FAC10471DB1D381085DDADDB58796829CA90069 curve_brainpoolp512r1 = ellipticcurve.CurveFp(_p, _a, _b) -generator_brainpoolp512r1 = ellipticcurve.Point( - curve_brainpoolp512r1, _Gx, _Gy, _q) +generator_brainpoolp512r1 = ellipticcurve.PointJacobi( + curve_brainpoolp512r1, _Gx, _Gy, 1, _q, generator=True) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 859e8ccd..3d793763 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -82,12 +82,29 @@ class PointJacobi(object): x = X / Z² y = Y / Z³ """ - def __init__(self, curve, x, y, z, order=None): + def __init__(self, curve, x, y, z, order=None, generator=False): + """ + :param bool generator: the point provided is a curve generator, as + such, it will be commonly used with scalar multiplication. This will + cause to precompute multiplication table for it + """ self.__curve = curve self.__x = x self.__y = y self.__z = z self.__order = order + self.__precompute={} + if generator: + assert order + i = 1 + order *= 2 + doubler = PointJacobi(curve, x, y, z, order) + self.__precompute[i] = doubler + + while i < order: + i *= 2 + doubler = doubler.double().scale() + self.__precompute[i] = doubler def __eq__(self, other): """Compare two points with each-other.""" @@ -131,6 +148,10 @@ def y(self): z = numbertheory.inverse_mod(self.__z, p) return self.__y * z**3 % p + def scale(self): + """Return point scaled so that z == 1.""" + return self.from_affine(self.to_affine()) + def to_affine(self): """Return point in affine form.""" p = self.__curve.p() @@ -139,10 +160,10 @@ def to_affine(self): self.__y * z**3 % p, self.__order) @staticmethod - def from_affine(point): + def from_affine(point, generator=False): """Create from an affine point.""" return PointJacobi(point.curve(), point.x(), point.y(), 1, - point.order()) + point.order(), generator) # plese note that all the methods that use the equations from hyperelliptic # are formatted in a way to maximise performance. @@ -295,15 +316,26 @@ def __rmul__(self, other): """Multiply point by an integer.""" return self * other + def _mul_precompute(self, other): + i = 1 + result = INFINITY + while i <= other: + if i & other: + result = result + self.__precompute[i] + i *= 2 + return result + def __mul__(self, other): """Multiply point by an integer.""" if not self.__y or not other: return INFINITY if other == 1: return self - # makes vulnerable to Minerva - # if self.__order: - # other = other % self.__order + if self.__order: + # order*2 as a protection for Minerva + other = other % (self.__order*2) + if self.__precompute: + return self._mul_precompute(other) def leftmost_bit(x): assert x > 0 diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index 63e8de28..f6cc16df 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -165,6 +165,20 @@ def test_multiplications(self, mul): pj = pj * mul self.assertEqual((pj.x(), pj.y()), (pw.x(), pw.y())) + self.assertEqual(pj, pw) + + @settings(max_examples=10) + @given(st.integers(min_value=0, max_value=generator_256.order())) + @example(0) + @example(generator_256.order()) + def test_precompute(self, mul): + precomp = PointJacobi.from_affine(generator_256, True) + pj = PointJacobi.from_affine(generator_256) + + a = precomp * mul + b = pj * mul + + self.assertEqual(a, b) @settings(max_examples=10) @given(st.integers(min_value=1, max_value=generator_256.order()), From 5e1afee856df82fe9eb63d07d8205626418baeb7 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 01:37:11 +0100 Subject: [PATCH 06/34] curves: pep8 fixes --- src/ecdsa/curves.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ecdsa/curves.py b/src/ecdsa/curves.py index b10022f5..098a2911 100644 --- a/src/ecdsa/curves.py +++ b/src/ecdsa/curves.py @@ -63,36 +63,50 @@ def __repr__(self): SECP256k1 = Curve("SECP256k1", ecdsa.curve_secp256k1, ecdsa.generator_secp256k1, (1, 3, 132, 0, 10), "secp256k1") + + BRAINPOOLP160r1 = Curve("BRAINPOOLP160r1", ecdsa.curve_brainpoolp160r1, ecdsa.generator_brainpoolp160r1, (1, 3, 36, 3, 3, 2, 8, 1, 1, 1), "brainpoolP160r1") + + BRAINPOOLP192r1 = Curve("BRAINPOOLP192r1", ecdsa.curve_brainpoolp192r1, ecdsa.generator_brainpoolp192r1, (1, 3, 36, 3, 3, 2, 8, 1, 1, 3), "brainpoolP192r1") + + BRAINPOOLP224r1 = Curve("BRAINPOOLP224r1", ecdsa.curve_brainpoolp224r1, ecdsa.generator_brainpoolp224r1, (1, 3, 36, 3, 3, 2, 8, 1, 1, 5), "brainpoolP224r1") + + BRAINPOOLP256r1 = Curve("BRAINPOOLP256r1", ecdsa.curve_brainpoolp256r1, ecdsa.generator_brainpoolp256r1, (1, 3, 36, 3, 3, 2, 8, 1, 1, 7), "brainpoolP256r1") + + BRAINPOOLP320r1 = Curve("BRAINPOOLP320r1", ecdsa.curve_brainpoolp320r1, ecdsa.generator_brainpoolp320r1, (1, 3, 36, 3, 3, 2, 8, 1, 1, 9), "brainpoolP320r1") + + BRAINPOOLP384r1 = Curve("BRAINPOOLP384r1", ecdsa.curve_brainpoolp384r1, ecdsa.generator_brainpoolp384r1, (1, 3, 36, 3, 3, 2, 8, 1, 1, 11), "brainpoolP384r1") + + BRAINPOOLP512r1 = Curve("BRAINPOOLP512r1", ecdsa.curve_brainpoolp512r1, ecdsa.generator_brainpoolp512r1, From 0cb3b1cc9698bc6880e1b0fa054afa28a5d69f3e Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:20:03 +0200 Subject: [PATCH 07/34] ensure that jacobi implementation is used for user methods --- src/ecdsa/keys.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 7dd2c111..cf87e014 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -168,6 +168,8 @@ def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1): :rtype: VerifyingKey """ self = cls(_error__please_use_generate=True) + if not isinstance(point, ellipticcurve.PointJacobi): + point = ellipticcurve.PointJacobi.from_affine(point) self.curve = curve self.default_hashfunc = hashfunc self.pubkey = ecdsa.Public_key(curve.generator, point) @@ -723,6 +725,7 @@ def from_secret_exponent(cls, secexp, curve=NIST192p, hashfunc=sha1): "Invalid value for secexp, expected integer between 1 and {0}" .format(n)) pubkey_point = curve.generator * secexp + pubkey_point = pubkey_point.scale() self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, hashfunc) From a8317c6407c9d3fbb04b21c446854f512aae874a Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:21:13 +0200 Subject: [PATCH 08/34] use jacobi implementation also for pubkey recovery --- src/ecdsa/ecdsa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index 6ca0dabf..a9b7d8f3 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -88,12 +88,12 @@ def recover_public_keys(self, hash, generator): y = beta if beta % 2 == 0 else curve.p() - beta # Compute the public key - R1 = ellipticcurve.Point(curve, x, y, n) + R1 = ellipticcurve.PointJacobi(curve, x, y, 1, n) Q1 = numbertheory.inverse_mod(r, n) * (s * R1 + (-e % n) * generator) Pk1 = Public_key(generator, Q1) # And the second solution - R2 = ellipticcurve.Point(curve, x, -y, n) + R2 = ellipticcurve.PointJacobi(curve, x, -y, 1, n) Q2 = numbertheory.inverse_mod(r, n) * (s * R2 + (-e % n) * generator) Pk2 = Public_key(generator, Q2) From 5d210e38ba8fbe003483fe27db2d7840097a4aeb Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 9 Oct 2019 22:59:17 +0200 Subject: [PATCH 09/34] speed up scaling also change one cubing into multiplication --- src/ecdsa/ellipticcurve.py | 24 ++++++++++++++++++------ src/ecdsa/keys.py | 3 ++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 3d793763..60d36ac1 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -149,15 +149,26 @@ def y(self): return self.__y * z**3 % p def scale(self): - """Return point scaled so that z == 1.""" - return self.from_affine(self.to_affine()) + """ + Return point scaled so that z == 1. + + Modifies point in place, returns self. + """ + p = self.__curve.p() + z_inv = numbertheory.inverse_mod(self.__z, p) + zz_inv = z_inv * z_inv % p + self.__x = self.__x * zz_inv % p + self.__y = self.__y * zz_inv * z_inv % p + self.__z = 1 + return self def to_affine(self): """Return point in affine form.""" - p = self.__curve.p() - z = numbertheory.inverse_mod(self.__z, p) - return Point(self.__curve, self.__x * z**2 % p, - self.__y * z**3 % p, self.__order) + if not self.__y or not self.__z: + return INFINITY + self.scale() + return Point(self.__curve, self.__x, + self.__y, self.__order) @staticmethod def from_affine(point, generator=False): @@ -346,6 +357,7 @@ def leftmost_bit(x): e = other i = leftmost_bit(e) + self = self.scale() result = self while i > 1: result = result.double() diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index cf87e014..e1b10f96 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -725,7 +725,8 @@ def from_secret_exponent(cls, secexp, curve=NIST192p, hashfunc=sha1): "Invalid value for secexp, expected integer between 1 and {0}" .format(n)) pubkey_point = curve.generator * secexp - pubkey_point = pubkey_point.scale() + if hasattr(pubkey_point, "scale"): + pubkey_point = pubkey_point.scale() self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, hashfunc) From dae2f334dc3418996d6e46c81bb25a9e32e926e1 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Oct 2019 00:03:20 +0200 Subject: [PATCH 10/34] implement equality without inverse_mod since inverse_mod is very computationally expensive (around 100 multiplications) it's cheaper to just bring the fractions to the same denominator --- src/ecdsa/ellipticcurve.py | 21 +++++++++++++++++++-- src/ecdsa/test_jacobi.py | 3 +++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 60d36ac1..f518c75c 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -110,9 +110,26 @@ def __eq__(self, other): """Compare two points with each-other.""" if (not self.__y or not self.__z) and other is INFINITY: return True + if self.__y and self.__z and other is INFINITY: + return False if isinstance(other, Point): - return self.to_affine() == other - return self.x() == other.x() and self.y() == other.y() + x2, y2, z2 = other.x(), other.y(), 1 + elif isinstance(other, PointJacobi): + x2, y2, z2 = other.__x, other.__y, other.__z + else: + return NotImplemented + if self.__curve != other.curve(): + return False + x1, y1, z1 = self.__x, self.__y, self.__z + p = self.__curve.p() + + zz1 = z1 * z1 % p + zz2 = z2 * z2 % p + + # compare the fractions by bringing them to the same denominator + # depend on short-circuit to save 4 multiplications in case of inequality + return (x1 * zz2 - x2 * zz1) % p == 0 and \ + (y1 * zz2 * z2 - y2 * zz1 * z1) % p == 0 def order(self): return self.__order diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index f6cc16df..9d463977 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -32,6 +32,9 @@ def test_add_with_different_curves(self): with self.assertRaises(ValueError): p_a + p_b + def test_compare_different_curves(self): + self.assertNotEqual(generator_256, generator_224) + def test_conversion(self): pj = PointJacobi.from_affine(generator_256) pw = pj.to_affine() From 08e357658dedca9a6e6d5b3b1fd9870a9a516a05 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Oct 2019 09:32:57 +0200 Subject: [PATCH 11/34] add ability to precompute for verification --- speed.py | 5 +++-- src/ecdsa/keys.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/speed.py b/speed.py index 5599896a..ea855e37 100644 --- a/speed.py +++ b/speed.py @@ -30,14 +30,15 @@ def do(setup_statements, statement): S3 = "msg = six.b('msg')" S4 = "sig = sk.sign(msg)" S5 = "vk = sk.get_verifying_key()" - S6 = "vk.verify(sig, msg)" + S6 = "vk.precompute()" + S7 = "vk.verify(sig, msg)" # We happen to know that .generate() also calculates the # verifying key, which is the time-consuming part. If the code # were changed to lazily calculate vk, we'd need to change this # benchmark to loop over S5 instead of S2 keygen = do([S1], S2) sign = do([S1,S2,S3], S4) - verf = do([S1,S2,S3,S4,S5], S6) + verf = do([S1,S2,S3,S4,S5,S6], S7) import ecdsa c = getattr(ecdsa, curve) sig = ecdsa.SigningKey.generate(c).sign(six.b("msg")) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index e1b10f96..e9d5b9b7 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -176,6 +176,10 @@ def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1): self.pubkey.order = curve.order return self + def precompute(self): + self.pubkey.point = ellipticcurve.PointJacobi.from_affine( + self.pubkey.point, True) + @staticmethod def _from_raw_encoding(string, curve, validate_point): """ From 6c7099d2d3e4aa93e5653699805e6e57c65d9e2c Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Fri, 11 Oct 2019 02:12:19 +0200 Subject: [PATCH 12/34] verify in one place, skip for self-created points when loading public keys, perform the point verification just once when loading private keys, do not verify the derived public point --- src/ecdsa/ecdsa.py | 30 +++++++++++++++++++++-------- src/ecdsa/keys.py | 47 +++++++++++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index a9b7d8f3..80b0bcd5 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -63,6 +63,10 @@ class RSZeroError(RuntimeError): pass +class InvalidPointError(RuntimeError): + pass + + class Signature(object): """ECDSA signature. """ @@ -99,25 +103,35 @@ def recover_public_keys(self, hash, generator): return [Pk1, Pk2] + class Public_key(object): """Public key for ECDSA. """ - def __init__(self, generator, point): - """generator is the Point that generates the group, - point is the Point that defines the public key. + def __init__(self, generator, point, verify=True): + """ + Low level ECDSA public key object. + + :param generator: the Point that generates the group + :param point: the Point that defines the public key + :param bool verify: if True check if point is valid point on curve + + :raises InvalidPointError: if the point parameters are invalid or + point does not lie on the curve """ self.curve = generator.curve() self.generator = generator self.point = point n = generator.order() - if not n: - raise RuntimeError("Generator point must have order.") - if not n * point == ellipticcurve.INFINITY: - raise RuntimeError("Generator point order is bad.") if point.x() < 0 or n <= point.x() or point.y() < 0 or n <= point.y(): - raise RuntimeError("Generator point has x or y out of range.") + raise InvalidPointError("Generator point has x or y out of range.") + if verify and not self.curve.contains_point(point.x(), point.y()): + raise InvalidPointError("Point does not lie on the curve") + if not n: + raise InvalidPointError("Generator point must have order.") + if verify and not n * point == ellipticcurve.INFINITY: + raise InvalidPointError("Generator point order is bad.") def __eq__(self, other): if isinstance(other, Public_key): diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index e9d5b9b7..bc8cf161 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -148,7 +148,8 @@ def __eq__(self, other): return NotImplemented @classmethod - def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1): + def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1, + validate_point=True): """ Initialise the object from a Point object. @@ -163,6 +164,12 @@ def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1): verification, needs to implement the same interface as hashlib.sha1 :type hashfunc: callable + :type bool validate_point: whether to check if the point lies on curve + should always be used if the public point is not a result + of our own calculation + + :raises MalformedPointError: if the public point does not lie on the + curve :return: Initialised VerifyingKey object :rtype: VerifyingKey @@ -172,7 +179,11 @@ def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1): point = ellipticcurve.PointJacobi.from_affine(point) self.curve = curve self.default_hashfunc = hashfunc - self.pubkey = ecdsa.Public_key(curve.generator, point) + try: + self.pubkey = ecdsa.Public_key(curve.generator, point, + validate_point) + except ecdsa.InvalidPointError: + raise MalformedPointError("Point does not lie on the curve") self.pubkey.order = curve.order return self @@ -181,7 +192,7 @@ def precompute(self): self.pubkey.point, True) @staticmethod - def _from_raw_encoding(string, curve, validate_point): + def _from_raw_encoding(string, curve): """ Decode public point from :term:`raw encoding`. @@ -199,13 +210,11 @@ def _from_raw_encoding(string, curve, validate_point): raise MalformedPointError("Unexpected length of encoded y") x = string_to_number(xs) y = string_to_number(ys) - if validate_point and not ecdsa.point_is_valid(curve.generator, x, y): - raise MalformedPointError("Point does not lie on the curve") - return ellipticcurve.Point(curve.curve, x, y, order) + return ellipticcurve.PointJacobi(curve.curve, x, y, 1, order) @staticmethod - def _from_compressed(string, curve, validate_point): + def _from_compressed(string, curve): """Decode public point from compressed encoding.""" if string[:1] not in (b('\x02'), b('\x03')): raise MalformedPointError("Malformed compressed point encoding") @@ -224,9 +233,7 @@ def _from_compressed(string, curve, validate_point): y = p - beta else: y = beta - if validate_point and not ecdsa.point_is_valid(curve.generator, x, y): - raise MalformedPointError("Point does not lie on curve") - return ellipticcurve.Point(curve.curve, x, y, order) + return ellipticcurve.PointJacobi(curve.curve, x, y, 1, order) @classmethod def _from_hybrid(cls, string, curve, validate_point): @@ -235,7 +242,7 @@ def _from_hybrid(cls, string, curve, validate_point): assert string[:1] in (b('\x06'), b('\x07')) # primarily use the uncompressed as it's easiest to handle - point = cls._from_raw_encoding(string[1:], curve, validate_point) + point = cls._from_raw_encoding(string[1:], curve) # but validate if it's self-consistent if we're asked to do that if validate_point and \ @@ -270,30 +277,33 @@ def from_string(cls, string, curve=NIST192p, hashfunc=sha1, provided curve or not, defaults to True :type validate_point: bool + :raises MalformedPointError: if the public point does not lie on the + curve or the encoding is invalid + :return: Initialised VerifyingKey object :rtype: VerifyingKey """ string = normalise_bytes(string) sig_len = len(string) if sig_len == curve.verifying_key_length: - point = cls._from_raw_encoding(string, curve, validate_point) + point = cls._from_raw_encoding(string, curve) elif sig_len == curve.verifying_key_length + 1: if string[:1] in (b('\x06'), b('\x07')): point = cls._from_hybrid(string, curve, validate_point) elif string[:1] == b('\x04'): - point = cls._from_raw_encoding(string[1:], curve, - validate_point) + point = cls._from_raw_encoding(string[1:], curve) else: raise MalformedPointError( "Invalid X9.62 encoding of the public point") elif sig_len == curve.baselen + 1: - point = cls._from_compressed(string, curve, validate_point) + point = cls._from_compressed(string, curve) else: raise MalformedPointError( "Length of string does not match lengths of " "any of the supported encodings of {0} " "curve.".format(curve.name)) - return cls.from_public_point(point, curve, hashfunc) + return cls.from_public_point(point, curve, hashfunc, + validate_point) @classmethod def from_pem(cls, string): @@ -731,9 +741,8 @@ def from_secret_exponent(cls, secexp, curve=NIST192p, hashfunc=sha1): pubkey_point = curve.generator * secexp if hasattr(pubkey_point, "scale"): pubkey_point = pubkey_point.scale() - self.verifying_key = VerifyingKey.from_public_point(pubkey_point, - curve, - hashfunc) + self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, + hashfunc, False) pubkey = self.verifying_key.pubkey self.privkey = ecdsa.Private_key(pubkey, secexp) self.privkey.order = n From 191898c5aae7bbbfb2d3d7ceaaec95f4e30724b0 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 02:16:29 +0100 Subject: [PATCH 13/34] add method that does multiplication and addition of two points --- src/ecdsa/ellipticcurve.py | 49 +++++++++++++++++++++++++ src/ecdsa/test_jacobi.py | 74 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index f518c75c..3fa28aaf 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -383,6 +383,55 @@ def leftmost_bit(x): result = result + self return result + def _mul_add_precompute(self, self_mul, other, other_mul): + i = 1 + result = INFINITY + m = max(self_mul, other_mul) + while i <= m: + if i & self_mul: + result = result + self.__precompute[i] + if i & other_mul: + result = result + other.__precompute[i] + i *= 2 + return result + + @staticmethod + def _leftmost_bit(x): + assert x > 0 + result = 1 + while result <= x: + result = 2 * result + return result // 2 + + def mul_add(self, self_mul, other, other_mul): + """ + Do two multiplications at the same time, add results. + + calculates self*self_mul + other*other_mul + """ + if other is INFINITY or other_mul == 0: + return self * self_mul + if self_mul == 0: + return other * other_mul + if self.__precompute and other.__precompute: + return self._mul_add_precompute(self_mul, other, other_mul) + + i = self._leftmost_bit(max(self_mul, other_mul))*2 + result = INFINITY + self = self.scale() + other = other.scale() + both = (self + other).scale() + while i > 1: + result = result.double() + i = i // 2 + if self_mul & i and other_mul & i: + result = result + both + elif self_mul & i: + result = result + self + elif other_mul & i: + result = result + other + return result + class Point(object): """A point on an elliptic curve. Altering x and y is forbidding, diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index 9d463977..6892bae1 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -35,6 +35,11 @@ def test_add_with_different_curves(self): def test_compare_different_curves(self): self.assertNotEqual(generator_256, generator_224) + def test_equality_with_non_point(self): + pj = PointJacobi.from_affine(generator_256) + + self.assertNotEqual(pj, "value") + def test_conversion(self): pj = PointJacobi.from_affine(generator_256) pw = pj.to_affine() @@ -71,6 +76,13 @@ def test_compare_with_affine_point(self): self.assertEqual(pj, pa) self.assertEqual(pa, pj) + def test_to_affine_with_zero_point(self): + pj = PointJacobi(curve_256, 0, 0, 1) + + pa = pj.to_affine() + + self.assertIs(pa, INFINITY) + def test_add_with_affine_point(self): pj = PointJacobi.from_affine(generator_256) pa = pj.to_affine() @@ -282,3 +294,65 @@ def test_add_different_scale_points(self, a_mul, b_mul, new_z): c = a + b self.assertEqual(c, j_g * (a_mul + b_mul)) + + def test_add_point_3_times(self): + j_g = PointJacobi.from_affine(generator_256) + + self.assertEqual(j_g * 3, j_g + j_g + j_g) + + def test_mul_add_inf(self): + j_g = PointJacobi.from_affine(generator_256) + + self.assertEqual(j_g, j_g.mul_add(1, INFINITY, 1)) + + def test_mul_add_same(self): + j_g = PointJacobi.from_affine(generator_256) + + self.assertEqual(j_g * 2, j_g.mul_add(1, j_g, 1)) + + def test_mul_add_precompute(self): + j_g = PointJacobi.from_affine(generator_256, True) + b = PointJacobi.from_affine(j_g * 255, True) + + self.assertEqual(j_g * 256, j_g + b) + self.assertEqual(j_g * (5 + 255 * 7), j_g * 5 + b * 7) + self.assertEqual(j_g * (5 + 255 * 7), j_g.mul_add(5, b, 7)) + + def test_mul_add_precompute_large(self): + j_g = PointJacobi.from_affine(generator_256, True) + b = PointJacobi.from_affine(j_g * 255, True) + + self.assertEqual(j_g * 256, j_g + b) + self.assertEqual(j_g * (0xff00 + 255 * 0xf0f0), + j_g * 0xff00 + b * 0xf0f0) + self.assertEqual(j_g * (0xff00 + 255 * 0xf0f0), + j_g.mul_add(0xff00, b, 0xf0f0)) + + def test_mul_add_to_mul(self): + j_g = PointJacobi.from_affine(generator_256) + + a = j_g * 3 + b = j_g.mul_add(2, j_g, 1) + + self.assertEqual(a, b) + + def test_mul_add(self): + j_g = PointJacobi.from_affine(generator_256) + + w_a = generator_256 * 255 + w_b = generator_256 * (0xa8*0xf0) + j_b = j_g * 0xa8 + + ret = j_g.mul_add(255, j_b, 0xf0) + + self.assertEqual(ret.to_affine(), w_a + w_b) + + def test_mul_add_large(self): + j_g = PointJacobi.from_affine(generator_256) + b = PointJacobi.from_affine(j_g * 255) + + self.assertEqual(j_g * 256, j_g + b) + self.assertEqual(j_g * (0xff00 + 255 * 0xf0f0), + j_g * 0xff00 + b * 0xf0f0) + self.assertEqual(j_g * (0xff00 + 255 * 0xf0f0), + j_g.mul_add(0xff00, b, 0xf0f0)) From 304c5226a354032afa8a28bd2e211ca546dc5926 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 12 Oct 2019 13:27:41 +0200 Subject: [PATCH 14/34] use combined multiplication and addition in verification --- src/ecdsa/ecdsa.py | 5 ++++- src/ecdsa/ellipticcurve.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index 80b0bcd5..7239f2b7 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -158,7 +158,10 @@ def verifies(self, hash, signature): c = numbertheory.inverse_mod(s, n) u1 = (hash * c) % n u2 = (r * c) % n - xy = u1 * G + u2 * self.point + if hasattr(G, "mul_add"): + xy = G.mul_add(u1, self.point, u2) + else: + xy = u1 * G + u2 * self.point v = xy.x() % n return v == r diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 3fa28aaf..4a451832 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -413,6 +413,8 @@ def mul_add(self, self_mul, other, other_mul): return self * self_mul if self_mul == 0: return other * other_mul + if not isinstance(other, PointJacobi): + other = PointJacobi.from_affine(other) if self.__precompute and other.__precompute: return self._mul_add_precompute(self_mul, other, other_mul) From 90b356437a8258a385ddb6859d1c62658ce1c4d3 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 12 Oct 2019 13:55:12 +0200 Subject: [PATCH 15/34] use 2-ary NAF for multiplication with precomputation --- src/ecdsa/ellipticcurve.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 4a451832..58e47a07 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -93,18 +93,19 @@ def __init__(self, curve, x, y, z, order=None, generator=False): self.__y = y self.__z = z self.__order = order - self.__precompute={} + self.__precompute=[] if generator: assert order i = 1 order *= 2 doubler = PointJacobi(curve, x, y, z, order) - self.__precompute[i] = doubler + order *= 2 + self.__precompute.append(doubler) while i < order: i *= 2 doubler = doubler.double().scale() - self.__precompute[i] = doubler + self.__precompute.append(doubler) def __eq__(self, other): """Compare two points with each-other.""" @@ -345,12 +346,15 @@ def __rmul__(self, other): return self * other def _mul_precompute(self, other): - i = 1 result = INFINITY - while i <= other: - if i & other: - result = result + self.__precompute[i] - i *= 2 + for precomp in self.__precompute: + if other % 2: + if other % 4 >= 2: + other, result = (other + 1)//2, result + (-precomp) + else: + other, result = (other - 1)//2, result + precomp + else: + other //= 2 return result def __mul__(self, other): @@ -383,18 +387,6 @@ def leftmost_bit(x): result = result + self return result - def _mul_add_precompute(self, self_mul, other, other_mul): - i = 1 - result = INFINITY - m = max(self_mul, other_mul) - while i <= m: - if i & self_mul: - result = result + self.__precompute[i] - if i & other_mul: - result = result + other.__precompute[i] - i *= 2 - return result - @staticmethod def _leftmost_bit(x): assert x > 0 @@ -415,8 +407,6 @@ def mul_add(self, self_mul, other, other_mul): return other * other_mul if not isinstance(other, PointJacobi): other = PointJacobi.from_affine(other) - if self.__precompute and other.__precompute: - return self._mul_add_precompute(self_mul, other, other_mul) i = self._leftmost_bit(max(self_mul, other_mul))*2 result = INFINITY @@ -434,6 +424,10 @@ def mul_add(self, self_mul, other, other_mul): result = result + other return result + def __neg__(self): + return PointJacobi(self.__curve, self.__x, -self.__y, self.__z, + self.__order) + class Point(object): """A point on an elliptic curve. Altering x and y is forbidding, From d60ec5e9e2548e1ba1bb0d1f219ff5fc0f242965 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 12 Oct 2019 14:28:53 +0200 Subject: [PATCH 16/34] use 2-ary NAF for regular multiplication too --- src/ecdsa/ellipticcurve.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 58e47a07..2fe240f3 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -357,6 +357,20 @@ def _mul_precompute(self, other): other //= 2 return result + def _naf(self, mult): + ret = [] + while mult: + if mult % 2: + nd = mult % 4 + if nd >= 2: + nd = nd - 4 + ret += [nd] + mult -= nd + else: + ret += [0] + mult //= 2 + return ret + def __mul__(self, other): """Multiply point by an integer.""" if not self.__y or not other: @@ -369,21 +383,15 @@ def __mul__(self, other): if self.__precompute: return self._mul_precompute(other) - def leftmost_bit(x): - assert x > 0 - result = 1 - while result <= x: - result = 2 * result - return result // 2 - - e = other - i = leftmost_bit(e) self = self.scale() - result = self - while i > 1: + result = INFINITY + # since adding points when at least one of them is scaled + # is quicker, reverse the NAF order + for i in reversed(self._naf(other)): result = result.double() - i = i // 2 - if e & i != 0: + if i < 0: + result = result + (-self) + elif i > 0: result = result + self return result From a96af1482f88984a7c7d60154efe93e5658e806d Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 01:42:41 +0100 Subject: [PATCH 17/34] point_is_valid: speed up with Jacobi --- src/ecdsa/ecdsa.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index 7239f2b7..fb3f912a 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -270,7 +270,8 @@ def point_is_valid(generator, x, y): return False if not curve.contains_point(x, y): return False - if not n * ellipticcurve.Point(curve, x, y) == ellipticcurve.INFINITY: + if not n * ellipticcurve.PointJacobi(curve, x, y, 1)\ + == ellipticcurve.INFINITY: return False return True From 19962f1030f4d188f80b314b133b72d92a6e7412 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 02:56:45 +0100 Subject: [PATCH 18/34] speed up test suite make all test cases execute in less than 0.3s on i7 4790K --- src/ecdsa/test_ellipticcurve.py | 6 +++++- src/ecdsa/test_malformed_sigs.py | 5 ++++- src/ecdsa/test_numbertheory.py | 8 ++++++-- src/ecdsa/test_pyecdsa.py | 21 ++++++++++++--------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/ecdsa/test_ellipticcurve.py b/src/ecdsa/test_ellipticcurve.py index f0b35a86..2a3323fe 100644 --- a/src/ecdsa/test_ellipticcurve.py +++ b/src/ecdsa/test_ellipticcurve.py @@ -37,7 +37,11 @@ g_23 = Point(c_23, 13, 7, 7) -@settings(**HYP_SETTINGS) +HYP_SLOW_SETTINGS=dict(HYP_SETTINGS) +HYP_SLOW_SETTINGS["max_examples"]=10 + + +@settings(**HYP_SLOW_SETTINGS) @given(st.integers(min_value=1, max_value=r+1)) def test_p192_mult_tests(multiple): inv_m = inverse_mod(multiple, r) diff --git a/src/ecdsa/test_malformed_sigs.py b/src/ecdsa/test_malformed_sigs.py index 22ae4550..60da81ad 100644 --- a/src/ecdsa/test_malformed_sigs.py +++ b/src/ecdsa/test_malformed_sigs.py @@ -118,6 +118,9 @@ def st_fuzzed_sig(draw, keys_and_sigs): HealthCheck.filter_too_much, HealthCheck.too_slow] +slow_params = dict(params) +slow_params["max_examples"] = 10 + @settings(**params) @given(st_fuzzed_sig(keys_and_sigs)) @@ -158,7 +161,7 @@ def st_random_der_ecdsa_sig_value(draw): return verifying_key, sig -@settings(**params) +@settings(**slow_params) @given(st_random_der_ecdsa_sig_value()) def test_random_der_ecdsa_sig_value(params): """ diff --git a/src/ecdsa/test_numbertheory.py b/src/ecdsa/test_numbertheory.py index bf98dc15..33c61ee0 100644 --- a/src/ecdsa/test_numbertheory.py +++ b/src/ecdsa/test_numbertheory.py @@ -168,6 +168,10 @@ def st_comp_no_com_fac(draw): HYP_SETTINGS['deadline'] = 5000 +HYP_SLOW_SETTINGS=dict(HYP_SETTINGS) +HYP_SLOW_SETTINGS["max_examples"] = 10 + + class TestNumbertheory(unittest.TestCase): def test_gcd(self): assert gcd(3 * 5 * 7, 3 * 5 * 11, 3 * 5 * 13) == 3 * 5 @@ -178,7 +182,7 @@ def test_gcd(self): "Hypothesis 2.0.0 can't be made tolerant of hard to " "meet requirements (like `is_prime()`), the test " "case times-out on it") - @settings(**HYP_SETTINGS) + @settings(**HYP_SLOW_SETTINGS) @given(st_comp_with_com_fac()) def test_gcd_with_com_factor(self, numbers): n = gcd(numbers) @@ -190,7 +194,7 @@ def test_gcd_with_com_factor(self, numbers): "Hypothesis 2.0.0 can't be made tolerant of hard to " "meet requirements (like `is_prime()`), the test " "case times-out on it") - @settings(**HYP_SETTINGS) + @settings(**HYP_SLOW_SETTINGS) @given(st_comp_no_com_fac()) def test_gcd_with_uncom_factor(self, numbers): n = gcd(numbers) diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index 3d1247ab..e5b3e76a 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -12,6 +12,9 @@ from binascii import hexlify, unhexlify from hashlib import sha1, sha256, sha512 +from hypothesis import given +import hypothesis.strategies as st + from six import b, print_, binary_type from .keys import SigningKey, VerifyingKey from .keys import BadSignatureError, MalformedPointError, BadDigestError @@ -1025,17 +1028,17 @@ def test_trytryagain(self): self.assertEqual(("%x" % (tta("seed", NIST224p.order))).encode(), b("6fa59d73bf0446ae8743cf748fc5ac11d5585a90356417e97155c3bc")) - def test_randrange(self): + @given(st.integers(min_value=0, max_value=10**200)) + def test_randrange(self, i): # util.randrange does not provide long-term stability: we might # change the algorithm in the future. - for i in range(1000): - entropy = util.PRNG("seed-%d" % i) - for order in (2**8 - 2, 2**8 - 1, 2**8, - 2**16 - 1, 2**16 + 1, - ): - # that oddball 2**16+1 takes half our runtime - n = util.randrange(order, entropy=entropy) - self.assertTrue(1 <= n < order, (1, n, order)) + entropy = util.PRNG("seed-%d" % i) + for order in (2**8 - 2, 2**8 - 1, 2**8, + 2**16 - 1, 2**16 + 1, + ): + # that oddball 2**16+1 takes half our runtime + n = util.randrange(order, entropy=entropy) + self.assertTrue(1 <= n < order, (1, n, order)) def OFF_test_prove_uniformity(self): order = 2**8 - 2 From 48040d7bc7ea713e4810089ff482eb0036b17f51 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 03:14:47 +0100 Subject: [PATCH 19/34] complete doc for PointJacobi --- src/ecdsa/ellipticcurve.py | 47 ++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 2fe240f3..daf3b7fe 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -84,6 +84,17 @@ class PointJacobi(object): """ def __init__(self, curve, x, y, z, order=None, generator=False): """ + Initialise a point that uses Jacobi representation internally. + + :param CurveFp curve: curve on which the point resides + :param int x: the X parameter of Jacobi representation (equal to x when + converting from affine coordinates + :param int y: the Y parameter of Jacobi representation (equal to y when + converting from affine coordinates + :param int z: the Z parameter of Jacobi representation (equal to 1 when + converting from affine coordinates + :param int order: the point order, must be non zero when using + generator=True :param bool generator: the point provided is a curve generator, as such, it will be commonly used with scalar multiplication. This will cause to precompute multiplication table for it @@ -133,9 +144,14 @@ def __eq__(self, other): (y1 * zz2 * z2 - y2 * zz1 * z1) % p == 0 def order(self): + """Return the order of the point. + + None if it is undefined. + """ return self.__order def curve(self): + """Return curve over which the point is defined.""" return self.__curve def x(self): @@ -144,7 +160,8 @@ def x(self): This method should be used only when the 'y' coordinate is not needed. It's computationally more efficient to use `to_affine()` and then - call x() and y() on the returned instance. + call x() and y() on the returned instance. Or call `scale()` + and then x() and y() on the returned instance. """ if self.__z == 1: return self.__x @@ -158,7 +175,8 @@ def y(self): This method should be used only when the 'x' coordinate is not needed. It's computationally more efficient to use `to_affine()` and then - call x() and y() on the returned instance. + call x() and y() on the returned instance. Or call `scale()` + and then x() and y() on the returned instance. """ if self.__z == 1: return self.__y @@ -190,7 +208,12 @@ def to_affine(self): @staticmethod def from_affine(point, generator=False): - """Create from an affine point.""" + """Create from an affine point. + + :param bool generator: set to True to make the point to precalculate + multiplication table - useful for public point when verifying many + signatures (around 100 or so) or for generator points of a curve. + """ return PointJacobi(point.curve(), point.x(), point.y(), 1, point.order(), generator) @@ -292,6 +315,7 @@ def _add_with_z2_1(self, X1, Y1, Z1, X2, Y2): return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) def __radd__(self, other): + """Add other to self.""" return self + other def __add__(self, other): @@ -346,6 +370,7 @@ def __rmul__(self, other): return self * other def _mul_precompute(self, other): + """Multiply point by integer with precomputation table.""" result = INFINITY for precomp in self.__precompute: if other % 2: @@ -357,7 +382,9 @@ def _mul_precompute(self, other): other //= 2 return result - def _naf(self, mult): + @staticmethod + def _naf(mult): + """Calculate non-adjacent form of number.""" ret = [] while mult: if mult % 2: @@ -397,11 +424,12 @@ def __mul__(self, other): @staticmethod def _leftmost_bit(x): - assert x > 0 - result = 1 - while result <= x: - result = 2 * result - return result // 2 + """Return integer with the same magnitude as x but hamming weight of 1""" + assert x > 0 + result = 1 + while result <= x: + result = 2 * result + return result // 2 def mul_add(self, self_mul, other, other_mul): """ @@ -433,6 +461,7 @@ def mul_add(self, self_mul, other, other_mul): return result def __neg__(self): + """Return negated point.""" return PointJacobi(self.__curve, self.__x, -self.__y, self.__z, self.__order) From 58eef88e1f4deb70a2a343399e729e240fa366c9 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 03:33:10 +0100 Subject: [PATCH 20/34] faster double for scaled points --- src/ecdsa/ellipticcurve.py | 37 ++++++++++++++++++++++++++++--------- src/ecdsa/test_jacobi.py | 7 +++++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index daf3b7fe..14ca57e0 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -227,19 +227,28 @@ def from_affine(point, generator=False): # similarly, sometimes the `% p` is skipped if it makes the calculation # faster and the result of calculation is later reduced modulo `p` - def double(self): - """Add a point to itself.""" - if not self.__y: + def _double_with_z_1(self, X1, Y1): + """Add a point to itself with z == 1.""" + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-mdbl-2007-bl + p, a = self.__curve.p(), self.__curve.a() + XX, YY = X1 * X1 % p, Y1 * Y1 % p + if not YY: return INFINITY + YYYY = YY * YY % p + S = 2 * ((X1 + YY)**2 - XX - YYYY) % p + M = 3 * XX + a + T = (M * M - 2 * S) % p + # X3 = T + Y3 = (M * (S - T) - 8 * YYYY) % p + Z3 = 2 * Y1 % p + return PointJacobi(self.__curve, T, Y3, Z3, self.__order) - p = self.__curve.p() - a = self.__curve.a() - - X1, Y1, Z1 = self.__x, self.__y, self.__z - + def _double(self, X1, Y1, Z1): + """Add a point to itself, arbitrary z.""" # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl - + p, a = self.__curve.p(), self.__curve.a() XX, YY = X1 * X1 % p, Y1 * Y1 % p if not YY: return INFINITY @@ -254,6 +263,16 @@ def double(self): return PointJacobi(self.__curve, T, Y3, Z3, self.__order) + def double(self): + """Add a point to itself.""" + if not self.__y: + return INFINITY + + X1, Y1, Z1 = self.__x, self.__y, self.__z + if Z1 == 1: + return self._double_with_z_1(X1, Y1) + return self._double(X1, Y1, Z1) + def _add_with_z_1(self, X1, Y1, X2, Y2): """add points when both Z1 and Z2 equal 1""" # after: diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index 6892bae1..a5da1bc7 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -69,6 +69,13 @@ def test_double_with_zero_equivalent_point(self): self.assertIs(pj, INFINITY) + def test_double_with_zero_equivalent_point_non_1_z(self): + pj = PointJacobi(curve_256, 0, curve_256.p(), 2) + + pj = pj.double() + + self.assertIs(pj, INFINITY) + def test_compare_with_affine_point(self): pj = PointJacobi.from_affine(generator_256) pa = pj.to_affine() From ab07a0cd256ffc1bde4d7314989aa05e660c5862 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 03:35:23 +0100 Subject: [PATCH 21/34] move numeric code out of __add__ don't treat the universal code for point addition specially --- src/ecdsa/ellipticcurve.py | 53 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 14ca57e0..7323ce96 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -333,6 +333,33 @@ def _add_with_z2_1(self, X1, Y1, Z1, X2, Y2): return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + def _add_with_z_ne(self, X1, Y1, Z1, X2, Y2, Z2): + """add points with arbitrary z""" + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-2007-bl + p = self.__curve.p() + Z1Z1 = Z1 * Z1 % p + Z2Z2 = Z2 * Z2 % p + U1 = X1 * Z2Z2 % p + U2 = X2 * Z1Z1 % p + S1 = Y1 * Z2 * Z2Z2 % p + S2 = Y2 * Z1 * Z1Z1 % p + H = U2 - U1 + I = 4 * H * H % p + J = H * I % p + r = 2 * (S2 - S1) % p + if not H and not r: + return self.double() + V = U1 * I + X3 = (r * r - J - 2 * V) % p + Y3 = (r * (V - X3) - 2 * S1 * J) % p + Z3 = ((Z1 + Z2)**2 - Z1Z1 - Z2Z2) * H % p + + if not Y3 or not Z3: + return INFINITY + + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + def __radd__(self, other): """Add other to self.""" return self + other @@ -348,7 +375,6 @@ def __add__(self, other): if self.__curve != other.__curve: raise ValueError("The other point is on different curve") - p = self.__curve.p() X1, Y1, Z1 = self.__x, self.__y, self.__z X2, Y2, Z2 = other.__x, other.__y, other.__z if Z1 == Z2: @@ -359,30 +385,7 @@ def __add__(self, other): return self._add_with_z2_1(X2, Y2, Z2, X1, Y1) if Z2 == 1: return self._add_with_z2_1(X1, Y1, Z1, X2, Y2) - - # after: - # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-2007-bl - Z1Z1 = Z1 * Z1 % p - Z2Z2 = Z2 * Z2 % p - U1 = X1 * Z2Z2 % p - U2 = X2 * Z1Z1 % p - S1 = Y1 * Z2 * Z2Z2 % p - S2 = Y2 * Z1 * Z1Z1 % p - H = U2 - U1 - I = 4 * H * H % p - J = H * I % p - r = 2 * (S2 - S1) % p - if not H and not r: - return self.double() - V = U1 * I - X3 = (r * r - J - 2 * V) % p - Y3 = (r * (V - X3) - 2 * S1 * J) % p - Z3 = ((Z1 + Z2)**2 - Z1Z1 - Z2Z2) * H % p - - if not Y3 or not Z3: - return INFINITY - - return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + return self._add_with_z_ne(X1, Y1, Z1, X2, Y2, Z2) def __rmul__(self, other): """Multiply point by an integer.""" From d4c2387e10794e61beb4820cc362c5c4fdd3d0b9 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 03:44:11 +0100 Subject: [PATCH 22/34] use tuples for calculation of double --- src/ecdsa/ellipticcurve.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 7323ce96..1816ecf1 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -227,14 +227,13 @@ def from_affine(point, generator=False): # similarly, sometimes the `% p` is skipped if it makes the calculation # faster and the result of calculation is later reduced modulo `p` - def _double_with_z_1(self, X1, Y1): + def _double_with_z_1(self, X1, Y1, p, a): """Add a point to itself with z == 1.""" # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-mdbl-2007-bl - p, a = self.__curve.p(), self.__curve.a() XX, YY = X1 * X1 % p, Y1 * Y1 % p if not YY: - return INFINITY + return 0, 0, 1 YYYY = YY * YY % p S = 2 * ((X1 + YY)**2 - XX - YYYY) % p M = 3 * XX + a @@ -242,16 +241,17 @@ def _double_with_z_1(self, X1, Y1): # X3 = T Y3 = (M * (S - T) - 8 * YYYY) % p Z3 = 2 * Y1 % p - return PointJacobi(self.__curve, T, Y3, Z3, self.__order) + return T, Y3, Z3 - def _double(self, X1, Y1, Z1): + def _double(self, X1, Y1, Z1, p, a): """Add a point to itself, arbitrary z.""" + if Z1 == 1: + return self._double_with_z_1(X1, Y1, p, a) # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl - p, a = self.__curve.p(), self.__curve.a() XX, YY = X1 * X1 % p, Y1 * Y1 % p if not YY: - return INFINITY + return 0, 0, 1 YYYY = YY * YY % p ZZ = Z1 * Z1 % p S = 2 * ((X1 + YY)**2 - XX - YYYY) % p @@ -261,17 +261,22 @@ def _double(self, X1, Y1, Z1): Y3 = (M * (S - T) - 8 * YYYY) % p Z3 = ((Y1 + Z1)**2 - YY - ZZ) % p - return PointJacobi(self.__curve, T, Y3, Z3, self.__order) + return T, Y3, Z3 def double(self): """Add a point to itself.""" if not self.__y: return INFINITY + p, a = self.__curve.p(), self.__curve.a() + X1, Y1, Z1 = self.__x, self.__y, self.__z - if Z1 == 1: - return self._double_with_z_1(X1, Y1) - return self._double(X1, Y1, Z1) + + X3, Y3, Z3 = self._double(X1, Y1, Z1, p, a) + + if not Y3: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) def _add_with_z_1(self, X1, Y1, X2, Y2): """add points when both Z1 and Z2 equal 1""" From b0084d9540ee6fb470ccbb9e8b6bde7f9aa17e04 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 12 Nov 2019 03:56:07 +0100 Subject: [PATCH 23/34] use tuples for calculation of addition --- src/ecdsa/ellipticcurve.py | 64 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 1816ecf1..36da6862 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -278,49 +278,42 @@ def double(self): return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) - def _add_with_z_1(self, X1, Y1, X2, Y2): + def _add_with_z_1(self, X1, Y1, X2, Y2, p): """add points when both Z1 and Z2 equal 1""" # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-mmadd-2007-bl - p = self.__curve.p() H = X2 - X1 HH = H * H I = 4 * HH % p J = H * I r = 2 * (Y2 - Y1) if not H and not r: - return self.double() + return self._double_with_z_1(X1, Y1, p, self.__curve.a()) V = X1 * I X3 = (r**2 - J - 2 * V) % p Y3 = (r * (V - X3) - 2 * Y1 * J) % p Z3 = 2 * H % p - if not Y3 or not Z3: - return INFINITY - return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + return X3, Y3, Z3 - def _add_with_z_eq(self, X1, Y1, Z1, X2, Y2): + def _add_with_z_eq(self, X1, Y1, Z1, X2, Y2, p): """add points when Z1 == Z2""" # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-zadd-2007-m - p = self.__curve.p() A = (X2 - X1)**2 % p B = X1 * A % p C = X2 * A D = (Y2 - Y1)**2 % p if not A and not D: - return self.double() + return self._double(X1, Y1, Z1, p, self.__curve.a()) X3 = (D - B - C) % p Y3 = ((Y2 - Y1) * (B - X3) - Y1 * (C - B)) % p Z3 = Z1 * (X2 - X1) % p - if not Y3 or not Z3: - return INFINITY - return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + return X3, Y3, Z3 - def _add_with_z2_1(self, X1, Y1, Z1, X2, Y2): + def _add_with_z2_1(self, X1, Y1, Z1, X2, Y2, p): """add points when Z2 == 1""" # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-madd-2007-bl - p = self.__curve.p() Z1Z1 = Z1 * Z1 % p U2, S2 = X2 * Z1Z1 % p, Y2 * Z1 * Z1Z1 % p H = (U2 - X1) % p @@ -329,20 +322,17 @@ def _add_with_z2_1(self, X1, Y1, Z1, X2, Y2): J = H * I r = 2 * (S2 - Y1) % p if not r and not H: - return self.double() + return self._double_with_z_1(X2, Y2, p, self.__curve.a()) V = X1 * I X3 = (r * r - J - 2 * V) % p Y3 = (r * (V - X3) - 2 * Y1 * J) % p Z3 = ((Z1 + H)**2 - Z1Z1 - HH) % p - if not Y3 or not Z3: - return INFINITY - return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + return X3, Y3, Z3 - def _add_with_z_ne(self, X1, Y1, Z1, X2, Y2, Z2): + def _add_with_z_ne(self, X1, Y1, Z1, X2, Y2, Z2, p): """add points with arbitrary z""" # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-2007-bl - p = self.__curve.p() Z1Z1 = Z1 * Z1 % p Z2Z2 = Z2 * Z2 % p U1 = X1 * Z2Z2 % p @@ -354,21 +344,30 @@ def _add_with_z_ne(self, X1, Y1, Z1, X2, Y2, Z2): J = H * I % p r = 2 * (S2 - S1) % p if not H and not r: - return self.double() + return self._double(X1, Y1, Z1, p, self.__curve.a()) V = U1 * I X3 = (r * r - J - 2 * V) % p Y3 = (r * (V - X3) - 2 * S1 * J) % p Z3 = ((Z1 + Z2)**2 - Z1Z1 - Z2Z2) * H % p - if not Y3 or not Z3: - return INFINITY - - return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + return X3, Y3, Z3 def __radd__(self, other): """Add other to self.""" return self + other + def _add(self, X1, Y1, Z1, X2, Y2, Z2, p): + """add two points, select fastest method.""" + if Z1 == Z2: + if Z1 == 1: + return self._add_with_z_1(X1, Y1, X2, Y2, p) + return self._add_with_z_eq(X1, Y1, Z1, X2, Y2, p) + if Z1 == 1: + return self._add_with_z2_1(X2, Y2, Z2, X1, Y1, p) + if Z2 == 1: + return self._add_with_z2_1(X1, Y1, Z1, X2, Y2, p) + return self._add_with_z_ne(X1, Y1, Z1, X2, Y2, Z2, p) + def __add__(self, other): """Add two points on elliptic curve.""" if self == INFINITY: @@ -380,17 +379,14 @@ def __add__(self, other): if self.__curve != other.__curve: raise ValueError("The other point is on different curve") + p = self.__curve.p() X1, Y1, Z1 = self.__x, self.__y, self.__z X2, Y2, Z2 = other.__x, other.__y, other.__z - if Z1 == Z2: - if Z1 == 1: - return self._add_with_z_1(X1, Y1, X2, Y2) - return self._add_with_z_eq(X1, Y1, Z1, X2, Y2) - if Z1 == 1: - return self._add_with_z2_1(X2, Y2, Z2, X1, Y1) - if Z2 == 1: - return self._add_with_z2_1(X1, Y1, Z1, X2, Y2) - return self._add_with_z_ne(X1, Y1, Z1, X2, Y2, Z2) + X3, Y3, Z3 = self._add(X1, Y1, Z1, X2, Y2, Z2, p) + + if not Y3 or not Z3: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) def __rmul__(self, other): """Multiply point by an integer.""" From 272349fda0ea1c025e1f61332ab77e8b8abcffcb Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 13 Nov 2019 02:18:49 +0100 Subject: [PATCH 24/34] use tuples for calculation of multiplication since this avoids creating new PointJacobi after every addition it makes the signing about 20% faster --- src/ecdsa/ellipticcurve.py | 73 ++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 36da6862..862ad663 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -111,12 +111,12 @@ def __init__(self, curve, x, y, z, order=None, generator=False): order *= 2 doubler = PointJacobi(curve, x, y, z, order) order *= 2 - self.__precompute.append(doubler) + self.__precompute.append((doubler.x(), doubler.y())) while i < order: i *= 2 doubler = doubler.double().scale() - self.__precompute.append(doubler) + self.__precompute.append((doubler.x(), doubler.y())) def __eq__(self, other): """Compare two points with each-other.""" @@ -247,6 +247,8 @@ def _double(self, X1, Y1, Z1, p, a): """Add a point to itself, arbitrary z.""" if Z1 == 1: return self._double_with_z_1(X1, Y1, p, a) + if not Z1: + return 0, 0, 1 # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl XX, YY = X1 * X1 % p, Y1 * Y1 % p @@ -274,7 +276,7 @@ def double(self): X3, Y3, Z3 = self._double(X1, Y1, Z1, p, a) - if not Y3: + if not Y3 or not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -358,6 +360,10 @@ def __radd__(self, other): def _add(self, X1, Y1, Z1, X2, Y2, Z2, p): """add two points, select fastest method.""" + if not Y1 or not Z1: + return X2, Y2, Z2 + if not Y2 or not Z2: + return X1, Y1, Z1 if Z1 == Z2: if Z1 == 1: return self._add_with_z_1(X1, Y1, X2, Y2, p) @@ -394,16 +400,22 @@ def __rmul__(self, other): def _mul_precompute(self, other): """Multiply point by integer with precomputation table.""" - result = INFINITY - for precomp in self.__precompute: + X3, Y3, Z3, p = 0, 0, 1, self.__curve.p() + _add = self._add + for X2, Y2 in self.__precompute: if other % 2: if other % 4 >= 2: - other, result = (other + 1)//2, result + (-precomp) + other = (other + 1)//2 + X3, Y3, Z3 = _add(X3, Y3, Z3, X2, -Y2, 1, p) else: - other, result = (other - 1)//2, result + precomp + other = (other - 1)//2 + X3, Y3, Z3 = _add(X3, Y3, Z3, X2, Y2, 1, p) else: other //= 2 - return result + + if not Y3 or not Z3: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @staticmethod def _naf(mult): @@ -434,16 +446,24 @@ def __mul__(self, other): return self._mul_precompute(other) self = self.scale() - result = INFINITY + X2, Y2 = self.__x, self.__y + X3, Y3, Z3 = 0, 0, 1 + p, a = self.__curve.p(), self.__curve.a() + _double = self._double + _add = self._add # since adding points when at least one of them is scaled # is quicker, reverse the NAF order for i in reversed(self._naf(other)): - result = result.double() + X3, Y3, Z3 = _double(X3, Y3, Z3, p, a) if i < 0: - result = result + (-self) + X3, Y3, Z3 = _add(X3, Y3, Z3, X2, -Y2, 1, p) elif i > 0: - result = result + self - return result + X3, Y3, Z3 = _add(X3, Y3, Z3, X2, Y2, 1, p) + + if not Y3 or not Z3: + return INFINITY + + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @staticmethod def _leftmost_bit(x): @@ -467,21 +487,36 @@ def mul_add(self, self_mul, other, other_mul): if not isinstance(other, PointJacobi): other = PointJacobi.from_affine(other) + if self.__order: + self_mul = self_mul % self.__order + other_mul = other_mul % self.__order + i = self._leftmost_bit(max(self_mul, other_mul))*2 - result = INFINITY + X3, Y3, Z3 = 0, 0, 1 + p, a = self.__curve.p(), self.__curve.a() self = self.scale() + X1, Y1 = self.__x, self.__y other = other.scale() + X2, Y2 = other.__x, other.__y both = (self + other).scale() + X4, Y4 = both.__x, both.__y + _double = self._double + _add = self._add while i > 1: - result = result.double() + X3, Y3, Z3 = _double(X3, Y3, Z3, p, a) i = i // 2 + if self_mul & i and other_mul & i: - result = result + both + X3, Y3, Z3 = _add(X3, Y3, Z3, X4, Y4, 1, p) elif self_mul & i: - result = result + self + X3, Y3, Z3 = _add(X3, Y3, Z3, X1, Y1, 1, p) elif other_mul & i: - result = result + other - return result + X3, Y3, Z3 = _add(X3, Y3, Z3, X2, Y2, 1, p) + + if not Y3 or not Z3: + return INFINITY + + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) def __neg__(self): """Return negated point.""" From 6469c2ed378388c502ad915082747a7015bf8199 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 13 Nov 2019 02:31:07 +0100 Subject: [PATCH 25/34] update README with info about the Jacobi code performance --- README.md | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 561705f7..4b986210 100644 --- a/README.md +++ b/README.md @@ -53,24 +53,24 @@ On an Intel Core i7 4790K @ 4.0GHz I'm getting the following performance: ``` siglen keygen keygen/s sign sign/s verify verify/s - NIST192p: 48 0.01534s 65.18 0.00833s 120.05 0.01601s 62.48 - NIST224p: 56 0.02107s 47.46 0.01153s 86.74 0.02220s 45.05 - NIST256p: 64 0.02824s 35.40 0.01523s 65.66 0.02965s 33.73 - NIST384p: 96 0.06640s 15.06 0.03572s 27.99 0.06973s 14.34 - NIST521p: 132 0.13150s 7.60 0.07094s 14.10 0.13869s 7.21 - SECP256k1: 64 0.02807s 35.63 0.01525s 65.58 0.02964s 33.74 - BRAINPOOLP160r1: 40 0.01100s 90.88 0.00564s 177.45 0.01053s 94.92 - BRAINPOOLP192r1: 48 0.01633s 61.25 0.00833s 120.05 0.01591s 62.84 - BRAINPOOLP224r1: 56 0.02261s 44.23 0.01153s 86.76 0.02234s 44.77 - BRAINPOOLP256r1: 64 0.02997s 33.36 0.01532s 65.29 0.03084s 32.42 - BRAINPOOLP320r1: 80 0.04774s 20.95 0.02447s 40.86 0.04717s 21.20 - BRAINPOOLP384r1: 96 0.07007s 14.27 0.03566s 28.04 0.07056s 14.17 - BRAINPOOLP512r1: 128 0.13390s 7.47 0.06766s 14.78 0.13425s 7.45 + NIST192p: 48 0.00033s 3062.43 0.00036s 2748.08 0.00062s 1605.66 + NIST224p: 56 0.00041s 2424.07 0.00046s 2196.71 0.00083s 1205.81 + NIST256p: 64 0.00053s 1892.05 0.00058s 1735.23 0.00106s 944.82 + NIST384p: 96 0.00110s 904.98 0.00118s 847.82 0.00217s 460.26 + NIST521p: 132 0.00234s 428.24 0.00245s 408.54 0.00443s 225.95 + SECP256k1: 64 0.00053s 1891.93 0.00058s 1734.46 0.00109s 913.35 + BRAINPOOLP160r1: 40 0.00025s 3982.49 0.00029s 3490.15 0.00053s 1878.51 + BRAINPOOLP192r1: 48 0.00032s 3086.07 0.00036s 2761.68 0.00063s 1578.22 + BRAINPOOLP224r1: 56 0.00041s 2412.41 0.00046s 2185.52 0.00076s 1311.65 + BRAINPOOLP256r1: 64 0.00054s 1866.84 0.00058s 1719.45 0.00110s 906.85 + BRAINPOOLP320r1: 80 0.00077s 1306.97 0.00083s 1201.59 0.00158s 632.82 + BRAINPOOLP384r1: 96 0.00112s 892.44 0.00119s 841.48 0.00229s 436.71 + BRAINPOOLP512r1: 128 0.00214s 467.05 0.00226s 441.64 0.00422s 237.13 ``` For comparison, a highly optimised implementation (including curve-specific -assemply) like OpenSSL provides following performance numbers on the same -machine. Run `openssl speed` to reproduce it: +assembly) like OpenSSL provides following performance numbers on the same +machine. Run `openssl speed ecdsa` to reproduce it: ``` sign verify sign/s verify/s 192 bits ecdsa (nistp192) 0.0002s 0.0002s 4785.6 5380.7 @@ -78,6 +78,9 @@ machine. Run `openssl speed` to reproduce it: 256 bits ecdsa (nistp256) 0.0000s 0.0001s 45069.6 14166.6 384 bits ecdsa (nistp384) 0.0008s 0.0006s 1265.6 1648.1 521 bits ecdsa (nistp521) 0.0003s 0.0005s 3753.1 1819.5 + 256 bits ecdsa (brainpoolP256r1) 0.0003s 0.0003s 2983.5 3333.2 + 384 bits ecdsa (brainpoolP384r1) 0.0008s 0.0007s 1258.8 1528.1 + 512 bits ecdsa (brainpoolP512r1) 0.0015s 0.0012s 675.1 860.1 ``` Keys and signature can be serialized in different ways (see Usage, below). @@ -94,7 +97,9 @@ following lengths (in bytes): In 2006, Peter Pearson announced his pure-python implementation of ECDSA in a [message to sci.crypt][1], available from his [download site][2]. In 2010, Brian Warner wrote a wrapper around this code, to make it a bit easier and -safer to use. You are looking at the README for this wrapper. +safer to use. Hubert Kario then included an implementation of elliptic curve +cryptography that uses Jacobian coordinates internally, improving performance +about 20-fold. You are looking at the README for this wrapper. [1]: http://www.derkeiler.com/Newsgroups/sci.crypt/2006-01/msg00651.html [2]: http://webpages.charter.net/curryfans/peter/downloads.html @@ -105,7 +110,7 @@ To run the full test suite, do this: tox -e coverage -On an Intel Core i7 4790K @ 4.0GHz, the tests take about 150 seconds to execute. +On an Intel Core i7 4790K @ 4.0GHz, the tests take about 16 seconds to execute. The test suite uses [`hypothesis`](https://github.com/HypothesisWorks/hypothesis) so there is some inherent variability in the test suite execution time. From 7d9c556bcea2da386289c5435e9b28d4c716062c Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 13 Nov 2019 10:42:06 +0100 Subject: [PATCH 26/34] remove duplicated test cases looks like few merges/rebases didn't go exactly as planned and ended up duplicating test code, remove it --- src/ecdsa/test_pyecdsa.py | 140 +------------------------------------- 1 file changed, 1 insertion(+), 139 deletions(-) diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index e5b3e76a..bfd7b189 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -642,27 +642,6 @@ def test_not_lying_on_curve(self): with self.assertRaises(MalformedPointError): VerifyingKey.from_string(b('\x02') + enc) - def test_decoding_with_malformed_uncompressed(self): - enc = b('\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3' - '\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4' - 'z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*') - - with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b('\x02') + enc) - - def test_decoding_with_point_not_on_curve(self): - enc = b('\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3' - '\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4' - 'z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*') - - with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(enc[:47] + b('\x00')) - - def test_decoding_with_point_at_infinity(self): - # decoding it is unsupported, as it's not necessary to encode it - with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b('\x00')) - def test_from_string_with_invalid_curve_too_short_ver_key_len(self): # both verifying_key_length and baselen are calculated internally # by the Curve constructor, but since we depend on them verify @@ -1192,6 +1171,7 @@ def test_10(self): hash_func=sha512, expected=int("16200813020EC986863BEDFC1B121F605C1215645018AEA1A7B215A564DE9EB1B38A67AA1128B80CE391C4FB71187654AAA3431027BFC7F395766CA988C964DC56D", 16)) + class ECDH(unittest.TestCase): def _do(self, curve, generator, dA, x_qA, y_qA, dB, x_qB, y_qB, x_Z, y_Z): qA = dA * generator @@ -1375,121 +1355,3 @@ def test_brainpoolP512r1(self): y_Z=int("7DB71C3DEF63212841C463E881BDCF055523BD368240E6C3143BD8DEF8" "B3B3223B95E0F53082FF5E412F4222537A43DF1C6D25729DDB51620A83" "2BE6A26680A2", 16)) - - -class ECDH(unittest.TestCase): - def _do(self, curve, generator, dA, x_qA, y_qA, dB, x_qB, y_qB, x_Z, y_Z): - qA = dA * generator - qB = dB * generator - Z = dA * qB - self.assertEqual(Point(curve, x_qA, y_qA), qA) - self.assertEqual(Point(curve, x_qB, y_qB), qB) - self.assertTrue((dA * qB) == (dA * dB * generator) == (dB * dA * generator) == (dB * qA)) - self.assertEqual(Point(curve, x_Z, y_Z), Z) - - -class RFC6932(ECDH): - # https://tools.ietf.org/html/rfc6932#appendix-A.1 - - def test_brainpoolP224r1(self): - self._do( - curve=curve_brainpoolp224r1, - generator=BRAINPOOLP224r1.generator, - dA=int("7C4B7A2C8A4BAD1FBB7D79CC0955DB7C6A4660CA64CC4778159B495E", 16), - x_qA=int("B104A67A6F6E85E14EC1825E1539E8ECDBBF584922367DD88C6BDCF2", 16), - y_qA=int("46D782E7FDB5F60CD8404301AC5949C58EDB26BC68BA07695B750A94", 16), - dB=int("63976D4AAE6CD0F6DD18DEFEF55D96569D0507C03E74D6486FFA28FB", 16), - x_qB=int("2A97089A9296147B71B21A4B574E1278245B536F14D8C2B9D07A874E", 16), - y_qB=int("9B900D7C77A709A797276B8CA1BA61BB95B546FC29F862E44D59D25B", 16), - x_Z=int("312DFD98783F9FB77B9704945A73BEB6DCCBE3B65D0F967DCAB574EB", 16), - y_Z=int("6F800811D64114B1C48C621AB3357CF93F496E4238696A2A012B3C98", 16)) - - def test_brainpoolP256r1(self): - self._do( - curve=curve_brainpoolp256r1, - generator=BRAINPOOLP256r1.generator, - dA=int("041EB8B1E2BC681BCE8E39963B2E9FC415B05283313DD1A8BCC055F11AE49699", 16), - x_qA=int("78028496B5ECAAB3C8B6C12E45DB1E02C9E4D26B4113BC4F015F60C5CCC0D206", 16), - y_qA=int("A2AE1762A3831C1D20F03F8D1E3C0C39AFE6F09B4D44BBE80CD100987B05F92B", 16), - dB=int("06F5240EACDB9837BC96D48274C8AA834B6C87BA9CC3EEDD81F99A16B8D804D3", 16), - x_qB=int("8E07E219BA588916C5B06AA30A2F464C2F2ACFC1610A3BE2FB240B635341F0DB", 16), - y_qB=int("148EA1D7D1E7E54B9555B6C9AC90629C18B63BEE5D7AA6949EBBF47B24FDE40D", 16), - x_Z=int("05E940915549E9F6A4A75693716E37466ABA79B4BF2919877A16DD2CC2E23708", 16), - y_Z=int("6BC23B6702BC5A019438CEEA107DAAD8B94232FFBBC350F3B137628FE6FD134C", 16)) - - def test_brainpoolP384r1(self): - self._do( - curve=curve_brainpoolp384r1, - generator=BRAINPOOLP384r1.generator, - dA=int("014EC0755B78594BA47FB0A56F6173045B4331E74BA1A6F47322E70D79D828D97E095884CA72B73FDABD5910DF0FA76A", 16), - x_qA=int("45CB26E4384DAF6FB776885307B9A38B7AD1B5C692E0C32F0125332778F3B8D3F50CA358099B30DEB5EE69A95C058B4E", 16), - y_qA=int("8173A1C54AFFA7E781D0E1E1D12C0DC2B74F4DF58E4A4E3AF7026C5D32DC530A2CD89C859BB4B4B768497F49AB8CC859", 16), - dB=int("6B461CB79BD0EA519A87D6828815D8CE7CD9B3CAA0B5A8262CBCD550A015C90095B976F3529957506E1224A861711D54", 16), - x_qB=int("01BF92A92EE4BE8DED1A911125C209B03F99E3161CFCC986DC7711383FC30AF9CE28CA3386D59E2C8D72CE1E7B4666E8", 16), - y_qB=int("3289C4A3A4FEE035E39BDB885D509D224A142FF9FBCC5CFE5CCBB30268EE47487ED8044858D31D848F7A95C635A347AC", 16), - x_Z=int("04CC4FF3DCCCB07AF24E0ACC529955B36D7C807772B92FCBE48F3AFE9A2F370A1F98D3FA73FD0C0747C632E12F1423EC", 16), - y_Z=int("7F465F90BD69AFB8F828A214EB9716D66ABC59F17AF7C75EE7F1DE22AB5D05085F5A01A9382D05BF72D96698FE3FF64E", 16)) - - def test_brainpoolP512r1(self): - self._do( - curve=curve_brainpoolp512r1, - generator=BRAINPOOLP512r1.generator, - dA=int("636B6BE0482A6C1C41AA7AE7B245E983392DB94CECEA2660A379CFE159559E357581825391175FC195D28BAC0CF03A7841A383B95C262B983782874CCE6FE333", 16), - x_qA=int("0562E68B9AF7CBFD5565C6B16883B777FF11C199161ECC427A39D17EC2166499389571D6A994977C56AD8252658BA8A1B72AE42F4FB7532151AFC3EF0971CCDA", 16), - y_qA=int("A7CA2D8191E21776A89860AFBC1F582FAA308D551C1DC6133AF9F9C3CAD59998D70079548140B90B1F311AFB378AA81F51B275B2BE6B7DEE978EFC7343EA642E", 16), - dB=int("0AF4E7F6D52EDD52907BB8DBAB3992A0BB696EC10DF11892FF205B66D381ECE72314E6A6EA079CEA06961DBA5AE6422EF2E9EE803A1F236FB96A1799B86E5C8B", 16), - x_qB=int("5A7954E32663DFF11AE24712D87419F26B708AC2B92877D6BFEE2BFC43714D89BBDB6D24D807BBD3AEB7F0C325F862E8BADE4F74636B97EAACE739E11720D323", 16), - y_qB=int("96D14621A9283A1BED84DE8DD64836B2C0758B11441179DC0C54C0D49A47C03807D171DD544B72CAAEF7B7CE01C7753E2CAD1A861ECA55A71954EE1BA35E04BE", 16), - x_Z=int("1EE8321A4BBF93B9CF8921AB209850EC9B7066D1984EF08C2BB723236208AC8F1A483E79461A00E0D5F6921CE9D360502F85C812BEDEE23AC5B210E5811B191E", 16), - y_Z=int("2632095B7B936174B41FD2FAF369B1D18DCADEED7E410A7E251F0831097C50D02CFED02607B6A2D5ADB4C0006008562208631875B58B54ECDA5A4F9FE9EAABA6", 16)) - - -class RFC7027(ECDH): - # https://tools.ietf.org/html/rfc7027#appendix-A - - def test_brainpoolP256r1(self): - self._do( - curve=curve_brainpoolp256r1, - generator=BRAINPOOLP256r1.generator, - dA=int("81DB1EE100150FF2EA338D708271BE38300CB54241D79950F77B063039804F1D", 16), - x_qA=int("44106E913F92BC02A1705D9953A8414DB95E1AAA49E81D9E85F929A8E3100BE5", 16), - y_qA=int("8AB4846F11CACCB73CE49CBDD120F5A900A69FD32C272223F789EF10EB089BDC", 16), - dB=int("55E40BC41E37E3E2AD25C3C6654511FFA8474A91A0032087593852D3E7D76BD3", 16), - x_qB=int("8D2D688C6CF93E1160AD04CC4429117DC2C41825E1E9FCA0ADDD34E6F1B39F7B", 16), - y_qB=int("990C57520812BE512641E47034832106BC7D3E8DD0E4C7F1136D7006547CEC6A", 16), - x_Z=int("89AFC39D41D3B327814B80940B042590F96556EC91E6AE7939BCE31F3A18BF2B", 16), - y_Z=int("49C27868F4ECA2179BFD7D59B1E3BF34C1DBDE61AE12931648F43E59632504DE", 16)) - - def test_brainpoolP384r1(self): - self._do( - curve=curve_brainpoolp384r1, - generator=BRAINPOOLP384r1.generator, - dA=int("1E20F5E048A5886F1F157C74E91BDE2B98C8B52D58E5003D57053FC4B0BD65D6F15EB5D1EE1610DF870795143627D042", 16), - x_qA=int("68B665DD91C195800650CDD363C625F4E742E8134667B767B1B476793588F885AB698C852D4A6E77A252D6380FCAF068", 16), - y_qA=int("55BC91A39C9EC01DEE36017B7D673A931236D2F1F5C83942D049E3FA20607493E0D038FF2FD30C2AB67D15C85F7FAA59", 16), - dB=int("032640BC6003C59260F7250C3DB58CE647F98E1260ACCE4ACDA3DD869F74E01F8BA5E0324309DB6A9831497ABAC96670", 16), - x_qB=int("4D44326F269A597A5B58BBA565DA5556ED7FD9A8A9EB76C25F46DB69D19DC8CE6AD18E404B15738B2086DF37E71D1EB4", 16), - y_qB=int("62D692136DE56CBE93BF5FA3188EF58BC8A3A0EC6C1E151A21038A42E9185329B5B275903D192F8D4E1F32FE9CC78C48", 16), - x_Z=int("0BD9D3A7EA0B3D519D09D8E48D0785FB744A6B355E6304BC51C229FBBCE239BBADF6403715C35D4FB2A5444F575D4F42", 16), - y_Z=int("0DF213417EBE4D8E40A5F76F66C56470C489A3478D146DECF6DF0D94BAE9E598157290F8756066975F1DB34B2324B7BD", 16)) - - def test_brainpoolP512r1(self): - self._do( - curve=curve_brainpoolp512r1, - generator=BRAINPOOLP512r1.generator, - dA=int("16302FF0DBBB5A8D733DAB7141C1B45ACBC8715939677F6A56850A38BD87BD59B09E80279609FF333EB9D4C061231FB26F92EEB04982A5F1D1764CAD57665422", 16), - x_qA=int("0A420517E406AAC0ACDCE90FCD71487718D3B953EFD7FBEC5F7F27E28C6149999397E91E029E06457DB2D3E640668B392C2A7E737A7F0BF04436D11640FD09FD", 16), - y_qA=int("72E6882E8DB28AAD36237CD25D580DB23783961C8DC52DFA2EC138AD472A0FCEF3887CF62B623B2A87DE5C588301EA3E5FC269B373B60724F5E82A6AD147FDE7", 16), - dB=int("230E18E1BCC88A362FA54E4EA3902009292F7F8033624FD471B5D8ACE49D12CFABBC19963DAB8E2F1EBA00BFFB29E4D72D13F2224562F405CB80503666B25429", 16), - x_qB=int("9D45F66DE5D67E2E6DB6E93A59CE0BB48106097FF78A081DE781CDB31FCE8CCBAAEA8DD4320C4119F1E9CD437A2EAB3731FA9668AB268D871DEDA55A5473199F", 16), - y_qB=int("2FDC313095BCDD5FB3A91636F07A959C8E86B5636A1E930E8396049CB481961D365CC11453A06C719835475B12CB52FC3C383BCE35E27EF194512B71876285FA", 16), - x_Z=int("A7927098655F1F9976FA50A9D566865DC530331846381C87256BAF3226244B76D36403C024D7BBF0AA0803EAFF405D3D24F11A9B5C0BEF679FE1454B21C4CD1F", 16), - y_Z=int("7DB71C3DEF63212841C463E881BDCF055523BD368240E6C3143BD8DEF8B3B3223B95E0F53082FF5E412F4222537A43DF1C6D25729DDB51620A832BE6A26680A2", 16)) - - -def __main__(): - unittest.main() - - -if __name__ == "__main__": - __main__() From 53d87a4940ca5340a0d4014617f05bd7dcfde4a5 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 13 Nov 2019 11:00:36 +0100 Subject: [PATCH 27/34] coverage: exclude environment branches in test cases since some branching in hypothesis strategies and in handling different python, hypothesis, openssl and unittest versions is necessary, ignore them for branch coverage remove benchmarking code and dead code from test_pyecdsa.py (we have speed.py now) and exclude a disabled test case from coverage --- src/ecdsa/test_ecdsa.py | 2 +- src/ecdsa/test_ellipticcurve.py | 4 ++-- src/ecdsa/test_malformed_sigs.py | 8 ++++---- src/ecdsa/test_numbertheory.py | 6 +++--- src/ecdsa/test_pyecdsa.py | 26 +++++--------------------- 5 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/ecdsa/test_ecdsa.py b/src/ecdsa/test_ecdsa.py index 26bcbddc..824bed0f 100644 --- a/src/ecdsa/test_ecdsa.py +++ b/src/ecdsa/test_ecdsa.py @@ -15,7 +15,7 @@ HYP_SETTINGS = {} # old hypothesis doesn't have the "deadline" setting -if sys.version_info > (2, 7): +if sys.version_info > (2, 7): # pragma: no branch # SEC521p is slow, allow long execution for it HYP_SETTINGS["deadline"] = 5000 diff --git a/src/ecdsa/test_ellipticcurve.py b/src/ecdsa/test_ellipticcurve.py index 2a3323fe..6929b74d 100644 --- a/src/ecdsa/test_ellipticcurve.py +++ b/src/ecdsa/test_ellipticcurve.py @@ -9,14 +9,14 @@ try: from hypothesis import HealthCheck HC_PRESENT=True -except ImportError: +except ImportError: # pragma: no cover HC_PRESENT=False from .numbertheory import inverse_mod from .ellipticcurve import CurveFp, INFINITY, Point HYP_SETTINGS={} -if HC_PRESENT: +if HC_PRESENT: # pragma: no branch HYP_SETTINGS['suppress_health_check']=[HealthCheck.too_slow] HYP_SETTINGS['deadline'] = 5000 diff --git a/src/ecdsa/test_malformed_sigs.py b/src/ecdsa/test_malformed_sigs.py index 60da81ad..cdca8816 100644 --- a/src/ecdsa/test_malformed_sigs.py +++ b/src/ecdsa/test_malformed_sigs.py @@ -3,7 +3,7 @@ import hashlib try: from hashlib import algorithms_available -except ImportError: +except ImportError: # pragma: no cover algorithms_available = [ "md5", "sha1", "sha224", "sha256", "sha384", "sha512"] from functools import partial @@ -82,7 +82,7 @@ def st_fuzzed_sig(draw, keys_and_sigs): note("Remove bytes: {0}".format(to_remove)) # decide which bytes of the original signature should be changed - if sig: + if sig: # pragma: no branch xors = draw(st.dictionaries( st.integers(min_value=0, max_value=len(sig)-1), st.integers(min_value=1, max_value=255))) @@ -110,7 +110,7 @@ def st_fuzzed_sig(draw, keys_and_sigs): params = {} # not supported in hypothesis 2.0.0 -if sys.version_info >= (2, 7): +if sys.version_info >= (2, 7): # pragma: no branch from hypothesis import HealthCheck # deadline=5s because NIST521p are slow to verify params["deadline"] = 5000 @@ -180,7 +180,7 @@ def st_der_integer(*args, **kwargs): INTEGER. Parameters are passed to hypothesis.strategy.integer. """ - if "min_value" not in kwargs: + if "min_value" not in kwargs: # pragma: no branch kwargs["min_value"] = 0 return st.builds(encode_integer, st.integers(*args, **kwargs)) diff --git a/src/ecdsa/test_numbertheory.py b/src/ecdsa/test_numbertheory.py index 33c61ee0..4cec4fd6 100644 --- a/src/ecdsa/test_numbertheory.py +++ b/src/ecdsa/test_numbertheory.py @@ -12,7 +12,7 @@ try: from hypothesis import HealthCheck HC_PRESENT=True -except ImportError: +except ImportError: # pragma: no cover HC_PRESENT=False from .numbertheory import (SquareRootError, factorization, gcd, lcm, jacobi, inverse_mod, @@ -85,7 +85,7 @@ def st_two_nums_rel_prime(draw): @st.composite def st_primes(draw, *args, **kwargs): - if "min_value" not in kwargs: + if "min_value" not in kwargs: # pragma: no branch kwargs["min_value"] = 1 prime = draw(st.sampled_from(smallprimes) | st.integers(*args, **kwargs) @@ -161,7 +161,7 @@ def st_comp_no_com_fac(draw): HYP_SETTINGS = {} -if HC_PRESENT: +if HC_PRESENT: # pragma: no branch HYP_SETTINGS['suppress_health_check']=[HealthCheck.filter_too_much, HealthCheck.too_slow] # the factorization() sometimes takes a long time to finish diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index bfd7b189..7ae7d259 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -52,9 +52,6 @@ def run_openssl(cmd): return stdout.decode() -BENCH = False - - class ECDSA(unittest.TestCase): def test_basic(self): priv = SigningKey.generate() @@ -103,8 +100,6 @@ def test_lengths(self): self.assertEqual(len(pub.to_string()), default.verifying_key_length) sig = priv.sign(b("data")) self.assertEqual(len(sig), default.signature_length) - if BENCH: - print_() for curve in (NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, BRAINPOOLP160r1, BRAINPOOLP192r1, BRAINPOOLP224r1, BRAINPOOLP256r1, BRAINPOOLP320r1, BRAINPOOLP384r1, @@ -121,13 +116,6 @@ def test_lengths(self): sig = priv.sign(b("data")) sign_time = time.time() - start self.assertEqual(len(sig), curve.signature_length) - if BENCH: - start = time.time() - pub1.verify(sig, b("data")) - verify_time = time.time() - start - print_("%s: siglen=%d, keygen=%0.3fs, sign=%0.3f, verify=%0.3f" \ - % (curve.name, curve.signature_length, - keygen_time, sign_time, verify_time)) def test_serialize(self): seed = b("secret") @@ -177,10 +165,6 @@ def assertTruePrivkeysEqual(self, priv1, priv2): self.assertEqual(priv1.privkey.public_key.generator, priv2.privkey.public_key.generator) - def failIfPrivkeysEqual(self, priv1, priv2): - self.failIfEqual(priv1.privkey.secret_multiplier, - priv2.privkey.secret_multiplier) - def test_privkey_creation(self): s = b("all the entropy in the entire world, compressed into one line") @@ -729,9 +713,9 @@ def get_openssl_messagedigest_arg(self): # e.g. "OpenSSL 1.0.0 29 Mar 2010", or "OpenSSL 1.0.0a 1 Jun 2010", # or "OpenSSL 0.9.8o 01 Jun 2010" vs = v.split()[1].split(".") - if vs >= ["1", "0", "0"]: + if vs >= ["1", "0", "0"]: # pragma: no cover return "-SHA1" - else: + else: # pragma: no cover return "-ecdsa-with-SHA1" # sk: 1:OpenSSL->python 2:python->OpenSSL @@ -809,7 +793,7 @@ def do_test_from_openssl(self, curve): # OpenSSL: create sk, vk, sign. # Python: read vk(3), checksig(5), read sk(1), sign, check mdarg = self.get_openssl_messagedigest_arg() - if os.path.isdir("t"): + if os.path.isdir("t"): # pragma: no cover shutil.rmtree("t") os.mkdir("t") run_openssl("ecparam -name %s -genkey -out t/privkey.pem" % curvename) @@ -904,7 +888,7 @@ def do_test_to_openssl(self, curve): # Python: create sk, vk, sign. # OpenSSL: read vk(4), checksig(6), read sk(2), sign, check mdarg = self.get_openssl_messagedigest_arg() - if os.path.isdir("t"): + if os.path.isdir("t"): # pragma: no cover shutil.rmtree("t") os.mkdir("t") sk = SigningKey.generate(curve=curve) @@ -1019,7 +1003,7 @@ def test_randrange(self, i): n = util.randrange(order, entropy=entropy) self.assertTrue(1 <= n < order, (1, n, order)) - def OFF_test_prove_uniformity(self): + def OFF_test_prove_uniformity(self): # pragma: no cover order = 2**8 - 2 counts = dict([(i, 0) for i in range(1, order)]) assert 0 not in counts From 8aac4a4330beec1e178f41f3b7da65add83973e9 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 13 Nov 2019 11:02:48 +0100 Subject: [PATCH 28/34] test_keys.py: fix typo in test case name --- .coveragerc | 1 - src/ecdsa/test_keys.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 64fdd3e7..57098f9a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,4 +5,3 @@ include = src/ecdsa/* omit = src/ecdsa/_version.py - src/ecdsa/test_* diff --git a/src/ecdsa/test_keys.py b/src/ecdsa/test_keys.py index a6f23389..cde8d780 100644 --- a/src/ecdsa/test_keys.py +++ b/src/ecdsa/test_keys.py @@ -97,7 +97,7 @@ def test_bytes_compressed(self): self.assertEqual(self.vk.to_string(), vk.to_string()) - def test_bytearray_uncompressed(self): + def test_bytearray_compressed(self): vk = VerifyingKey.from_string(bytearray(b'\x02' + self.key_bytes[:24])) self.assertEqual(self.vk.to_string(), vk.to_string()) From b8fdfbeb67fbdc8e040d9aeedfb9974d9aafa843 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 16 Nov 2019 17:14:11 +0100 Subject: [PATCH 29/34] Public_key: fix check for x and y size the x and y needs to be on curve, so they need to be smaller than the curve's prime, not the base point order See Section 3.2.2.1 of SEC 1 v2 --- src/ecdsa/ecdsa.py | 10 ++++++---- src/ecdsa/test_pyecdsa.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index fb3f912a..a82da07d 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -112,7 +112,7 @@ def __init__(self, generator, point, verify=True): """ Low level ECDSA public key object. - :param generator: the Point that generates the group + :param generator: the Point that generates the group (the base point) :param point: the Point that defines the public key :param bool verify: if True check if point is valid point on curve @@ -124,8 +124,9 @@ def __init__(self, generator, point, verify=True): self.generator = generator self.point = point n = generator.order() - if point.x() < 0 or n <= point.x() or point.y() < 0 or n <= point.y(): - raise InvalidPointError("Generator point has x or y out of range.") + p = self.curve.p() + if not (0 <= point.x() < p) or not (0 <= point.y() < p): + raise InvalidPointError("The public point has x or y out of range.") if verify and not self.curve.contains_point(point.x(), point.y()): raise InvalidPointError("Point does not lie on the curve") if not n: @@ -266,7 +267,8 @@ def point_is_valid(generator, x, y): n = generator.order() curve = generator.curve() - if x < 0 or n <= x or y < 0 or n <= y: + p = curve.p() + if not (0 <= x < p) or not (0 <= y < p): return False if not curve.contains_point(x, y): return False diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index 7ae7d259..05f22e60 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -621,7 +621,7 @@ def test_decoding_with_point_at_infinity(self): VerifyingKey.from_string(b('\x00')) def test_not_lying_on_curve(self): - enc = number_to_string(NIST192p.order, NIST192p.order+1) + enc = number_to_string(NIST192p.curve.p(), NIST192p.curve.p()+1) with self.assertRaises(MalformedPointError): VerifyingKey.from_string(b('\x02') + enc) From 8ab416bd254ed9e80e4ad9ebc63ca6b081b0f5ac Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 16 Nov 2019 17:57:50 +0100 Subject: [PATCH 30/34] use curve cofactor for point verification since multiplying a point by the order is farily expensive, skipping it (when safe to do so) greatly increases performance does not increase the speed.py numbers as point verification happens outside the signing and verifying operations --- src/ecdsa/ecdsa.py | 35 ++++++++++++++++++++--------------- src/ecdsa/ellipticcurve.py | 23 +++++++++++++++++++---- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index a82da07d..42c49b37 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -131,7 +131,11 @@ def __init__(self, generator, point, verify=True): raise InvalidPointError("Point does not lie on the curve") if not n: raise InvalidPointError("Generator point must have order.") - if verify and not n * point == ellipticcurve.INFINITY: + # for curve parameters with base point with cofactor 1, all points + # that are on the curve are scalar multiples of the base point, so + # verifying that is not necessary. See Section 3.2.2.1 of SEC 1 v2 + if verify and self.curve.cofactor() != 1 and \ + not n * point == ellipticcurve.INFINITY: raise InvalidPointError("Generator point order is bad.") def __eq__(self, other): @@ -272,7 +276,8 @@ def point_is_valid(generator, x, y): return False if not curve.contains_point(x, y): return False - if not n * ellipticcurve.PointJacobi(curve, x, y, 1)\ + if curve.cofactor() != 1 and \ + not n * ellipticcurve.PointJacobi(curve, x, y, 1)\ == ellipticcurve.INFINITY: return False return True @@ -287,7 +292,7 @@ def point_is_valid(generator, x, y): _Gx = 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012 _Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811 -curve_192 = ellipticcurve.CurveFp(_p, -3, _b) +curve_192 = ellipticcurve.CurveFp(_p, -3, _b, 1) generator_192 = ellipticcurve.PointJacobi( curve_192, _Gx, _Gy, 1, _r, generator=True) @@ -301,7 +306,7 @@ def point_is_valid(generator, x, y): _Gx = 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21 _Gy = 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34 -curve_224 = ellipticcurve.CurveFp(_p, -3, _b) +curve_224 = ellipticcurve.CurveFp(_p, -3, _b, 1) generator_224 = ellipticcurve.PointJacobi( curve_224, _Gx, _Gy, 1, _r, generator=True) @@ -314,7 +319,7 @@ def point_is_valid(generator, x, y): _Gx = 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296 _Gy = 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5 -curve_256 = ellipticcurve.CurveFp(_p, -3, _b) +curve_256 = ellipticcurve.CurveFp(_p, -3, _b, 1) generator_256 = ellipticcurve.PointJacobi( curve_256, _Gx, _Gy, 1, _r, generator=True) @@ -327,7 +332,7 @@ def point_is_valid(generator, x, y): _Gx = 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7 _Gy = 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f -curve_384 = ellipticcurve.CurveFp(_p, -3, _b) +curve_384 = ellipticcurve.CurveFp(_p, -3, _b, 1) generator_384 = ellipticcurve.PointJacobi( curve_384, _Gx, _Gy, 1, _r, generator=True) @@ -340,7 +345,7 @@ def point_is_valid(generator, x, y): _Gx = 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66 _Gy = 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650 -curve_521 = ellipticcurve.CurveFp(_p, -3, _b) +curve_521 = ellipticcurve.CurveFp(_p, -3, _b, 1) generator_521 = ellipticcurve.PointJacobi( curve_521, _Gx, _Gy, 1, _r, generator=True) @@ -352,7 +357,7 @@ def point_is_valid(generator, x, y): _Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 _r = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 -curve_secp256k1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_secp256k1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_secp256k1 = ellipticcurve.PointJacobi( curve_secp256k1, _Gx, _Gy, 1, _r, generator=True) @@ -364,7 +369,7 @@ def point_is_valid(generator, x, y): _Gy = 0x1667CB477A1A8EC338F94741669C976316DA6321 _q = 0xE95E4A5F737059DC60DF5991D45029409E60FC09 -curve_brainpoolp160r1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_brainpoolp160r1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_brainpoolp160r1 = ellipticcurve.PointJacobi( curve_brainpoolp160r1, _Gx, _Gy, 1, _q, generator=True) @@ -376,7 +381,7 @@ def point_is_valid(generator, x, y): _Gy = 0x14B690866ABD5BB88B5F4828C1490002E6773FA2FA299B8F _q = 0xC302F41D932A36CDA7A3462F9E9E916B5BE8F1029AC4ACC1 -curve_brainpoolp192r1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_brainpoolp192r1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_brainpoolp192r1 = ellipticcurve.PointJacobi( curve_brainpoolp192r1, _Gx, _Gy, 1, _q, generator=True) @@ -388,7 +393,7 @@ def point_is_valid(generator, x, y): _Gy = 0x58AA56F772C0726F24C6B89E4ECDAC24354B9E99CAA3F6D3761402CD _q = 0xD7C134AA264366862A18302575D0FB98D116BC4B6DDEBCA3A5A7939F -curve_brainpoolp224r1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_brainpoolp224r1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_brainpoolp224r1 = ellipticcurve.PointJacobi( curve_brainpoolp224r1, _Gx, _Gy, 1, _q, generator=True) @@ -400,7 +405,7 @@ def point_is_valid(generator, x, y): _Gy = 0x547EF835C3DAC4FD97F8461A14611DC9C27745132DED8E545C1D54C72F046997 _q = 0xA9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7 -curve_brainpoolp256r1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_brainpoolp256r1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_brainpoolp256r1 = ellipticcurve.PointJacobi( curve_brainpoolp256r1, _Gx, _Gy, 1, _q, generator=True) @@ -412,7 +417,7 @@ def point_is_valid(generator, x, y): _Gy = 0x14FDD05545EC1CC8AB4093247F77275E0743FFED117182EAA9C77877AAAC6AC7D35245D1692E8EE1 _q = 0xD35E472036BC4FB7E13C785ED201E065F98FCFA5B68F12A32D482EC7EE8658E98691555B44C59311 -curve_brainpoolp320r1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_brainpoolp320r1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_brainpoolp320r1 = ellipticcurve.PointJacobi( curve_brainpoolp320r1, _Gx, _Gy, 1, _q, generator=True) @@ -424,7 +429,7 @@ def point_is_valid(generator, x, y): _Gy = 0x8ABE1D7520F9C2A45CB1EB8E95CFD55262B70B29FEEC5864E19C054FF99129280E4646217791811142820341263C5315 _q = 0x8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC3103B883202E9046565 -curve_brainpoolp384r1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_brainpoolp384r1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_brainpoolp384r1 = ellipticcurve.PointJacobi( curve_brainpoolp384r1, _Gx, _Gy, 1, _q, generator=True) @@ -436,6 +441,6 @@ def point_is_valid(generator, x, y): _Gy = 0x7DDE385D566332ECC0EABFA9CF7822FDF209F70024A57B1AA000C55B881F8111B2DCDE494A5F485E5BCA4BD88A2763AED1CA2B2FA8F0540678CD1E0F3AD80892 _q = 0xAADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA92619418661197FAC10471DB1D381085DDADDB58796829CA90069 -curve_brainpoolp512r1 = ellipticcurve.CurveFp(_p, _a, _b) +curve_brainpoolp512r1 = ellipticcurve.CurveFp(_p, _a, _b, 1) generator_brainpoolp512r1 = ellipticcurve.PointJacobi( curve_brainpoolp512r1, _Gx, _Gy, 1, _q, generator=True) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 862ad663..2e8cb411 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -41,11 +41,19 @@ @python_2_unicode_compatible class CurveFp(object): """Elliptic Curve over the field of integers modulo a prime.""" - def __init__(self, p, a, b): - """The curve of points satisfying y^2 = x^3 + a*x + b (mod p).""" + def __init__(self, p, a, b, h=None): + """ + The curve of points satisfying y^2 = x^3 + a*x + b (mod p). + + h is an integer that is the cofactor of the elliptic curve domain + parameters; it is the number of points satisfying the elliptic curve + equation divided by the order of the base point. It is used for selection + of efficient algorithm for public point verification. + """ self.__p = p self.__a = a self.__b = b + self.__h = h def __eq__(self, other): if isinstance(other, CurveFp): @@ -64,12 +72,16 @@ def a(self): def b(self): return self.__b + def cofactor(self): + return self.__h + def contains_point(self, x, y): """Is the point (x,y) on this curve?""" return (y * y - ((x * x + self.__a) * x + self.__b)) % self.__p == 0 def __str__(self): - return "CurveFp(p=%d, a=%d, b=%d)" % (self.__p, self.__a, self.__b) + return "CurveFp(p=%d, a=%d, b=%d, h=%d)" % ( + self.__p, self.__a, self.__b, self.__h) class PointJacobi(object): @@ -536,7 +548,10 @@ def __init__(self, curve, x, y, order=None): # self.curve is allowed to be None only for INFINITY: if self.__curve: assert self.__curve.contains_point(x, y) - if order: + # for curves with cofactor 1, all points that are on the curve are scalar + # multiples of the base point, so performing multiplication is not + # necessary to verify that. See Section 3.2.2.1 of SEC 1 v2 + if curve and curve.cofactor() != 1 and order: assert self * order == INFINITY def __eq__(self, other): From f7910cf6fd1c67fe3ea69dbd94926534c2945dda Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 23 Nov 2019 15:50:27 +0100 Subject: [PATCH 31/34] use gmp2 for faster arithmetic --- .travis.yml | 15 ++++++- README.md | 29 ++++++++++-- src/ecdsa/ellipticcurve.py | 76 +++++++++++++++++++++++--------- src/ecdsa/numbertheory.py | 32 +++++++++----- src/ecdsa/test_ecdsa.py | 2 +- src/ecdsa/test_jacobi.py | 48 ++++++++++---------- src/ecdsa/test_malformed_sigs.py | 2 +- tox.ini | 15 ++++++- 8 files changed, 158 insertions(+), 61 deletions(-) diff --git a/.travis.yml b/.travis.yml index d22b6145..0da41db3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,12 @@ dist: trusty sudo: false language: python cache: pip +addons: + apt_packages: + # needed for gmpy2 + - libgmp-dev + - libmpfr-dev + - libmpc-dev before_cache: - rm -f $HOME/.cache/pip/log/debug.log # place the slowest (instrumental and py2.6) first @@ -17,6 +23,8 @@ matrix: env: TOX_ENV=py27 - python: 2.7 env: TOX_ENV=py27_old_six + - python: 2.7 + env: TOX_ENV=gmppy27 - python: 3.3 env: TOX_ENV=py33 - python: 3.4 @@ -33,6 +41,10 @@ matrix: env: TOX_ENV=py38 dist: xenial sudo: true + - python: 3.8 + env: TOX_ENV=gmppy38 + dist: xenial + sudo: true - python: nightly env: TOX_ENV=py dist: xenial @@ -75,11 +87,12 @@ install: else travis_retry pip install -r build-requirements.txt; fi + - if [[ $TOX_ENV =~ gmp ]]; then travis_retry pip install gmpy2; fi - if [[ $INSTRUMENTAL ]]; then travis_retry pip install instrumental; fi - pip list script: - if [[ $TOX_ENV ]]; then tox -e $TOX_ENV; fi - - tox -e speed + - if [[ $TOX_ENV =~ gmp ]]; then tox -e speedgmp2; else tox -e speed; fi - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then git checkout $PR_FIRST^ diff --git a/README.md b/README.md index 4b986210..7df12035 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,11 @@ curves over prime fields. This library uses only Python and the 'six' package. It is compatible with Python 2.6, 2.7 and 3.3+. It also supports execution on the alternative -implementations like pypy and pypy3 +implementations like pypy and pypy3. + +If `gmpy2` is installed, it will be used for faster arithmetic. +`gmpy2` can be installed after this library is installed, `python-ecdsa` will +detect presence of `gmpy2` on start-up and use it automatically. To run the OpenSSL compatibility tests, the 'openssl' tool must be in your `PATH`. This release has been tested successfully against OpenSSL 0.9.8o, @@ -67,10 +71,29 @@ On an Intel Core i7 4790K @ 4.0GHz I'm getting the following performance: BRAINPOOLP384r1: 96 0.00112s 892.44 0.00119s 841.48 0.00229s 436.71 BRAINPOOLP512r1: 128 0.00214s 467.05 0.00226s 441.64 0.00422s 237.13 ``` +To test performance with `gmpy2` loaded, use `tox -e speedgmp2`. +On the same machine I'm getting the following performance with `gmpy2`: +``` + siglen keygen keygen/s sign sign/s verify verify/s + NIST192p: 48 0.00016s 6180.12 0.00017s 5846.30 0.00033s 3029.51 + NIST224p: 56 0.00021s 4861.86 0.00021s 4662.63 0.00042s 2366.47 + NIST256p: 64 0.00023s 4343.93 0.00024s 4152.79 0.00047s 2148.83 + NIST384p: 96 0.00040s 2507.97 0.00041s 2435.99 0.00079s 1260.01 + NIST521p: 132 0.00088s 1135.13 0.00089s 1121.94 0.00139s 721.07 + SECP256k1: 64 0.00023s 4360.83 0.00024s 4147.61 0.00044s 2254.53 + BRAINPOOLP160r1: 40 0.00014s 7261.37 0.00015s 6824.47 0.00031s 3248.21 + BRAINPOOLP192r1: 48 0.00016s 6219.18 0.00017s 5862.93 0.00034s 2933.74 + BRAINPOOLP224r1: 56 0.00021s 4876.48 0.00022s 4640.40 0.00041s 2413.48 + BRAINPOOLP256r1: 64 0.00023s 4397.89 0.00024s 4178.48 0.00044s 2272.76 + BRAINPOOLP320r1: 80 0.00031s 3246.64 0.00032s 3138.38 0.00063s 1593.14 + BRAINPOOLP384r1: 96 0.00040s 2491.04 0.00041s 2421.67 0.00079s 1262.64 + BRAINPOOLP512r1: 128 0.00062s 1618.30 0.00063s 1577.42 0.00125s 799.29 +``` For comparison, a highly optimised implementation (including curve-specific -assembly) like OpenSSL provides following performance numbers on the same -machine. Run `openssl speed ecdsa` to reproduce it: +assembly for some curves), like the one in OpenSSL, provides following +performance numbers on the same machine. +Run `openssl speed ecdsa` to reproduce it: ``` sign verify sign/s verify/s 192 bits ecdsa (nistp192) 0.0002s 0.0002s 4785.6 5380.7 diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 2e8cb411..54c22305 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -35,25 +35,50 @@ from __future__ import division +try: + from gmpy2 import mpz + GMPY2=True +except ImportError: + GMPY2=False + from six import python_2_unicode_compatible from . import numbertheory + @python_2_unicode_compatible class CurveFp(object): """Elliptic Curve over the field of integers modulo a prime.""" - def __init__(self, p, a, b, h=None): - """ - The curve of points satisfying y^2 = x^3 + a*x + b (mod p). - - h is an integer that is the cofactor of the elliptic curve domain - parameters; it is the number of points satisfying the elliptic curve - equation divided by the order of the base point. It is used for selection - of efficient algorithm for public point verification. - """ - self.__p = p - self.__a = a - self.__b = b - self.__h = h + + if GMPY2: + def __init__(self, p, a, b, h=None): + """ + The curve of points satisfying y^2 = x^3 + a*x + b (mod p). + + h is an integer that is the cofactor of the elliptic curve domain + parameters; it is the number of points satisfying the elliptic curve + equation divided by the order of the base point. It is used for selection + of efficient algorithm for public point verification. + """ + self.__p = mpz(p) + self.__a = mpz(a) + self.__b = mpz(b) + # h is not used in calculations and it can be None, so don't use + # gmpy with it + self.__h = h + else: + def __init__(self, p, a, b, h=None): + """ + The curve of points satisfying y^2 = x^3 + a*x + b (mod p). + + h is an integer that is the cofactor of the elliptic curve domain + parameters; it is the number of points satisfying the elliptic curve + equation divided by the order of the base point. It is used for selection + of efficient algorithm for public point verification. + """ + self.__p = p + self.__a = a + self.__b = b + self.__h = h def __eq__(self, other): if isinstance(other, CurveFp): @@ -112,10 +137,16 @@ def __init__(self, curve, x, y, z, order=None, generator=False): cause to precompute multiplication table for it """ self.__curve = curve - self.__x = x - self.__y = y - self.__z = z - self.__order = order + if GMPY2: + self.__x = mpz(x) + self.__y = mpz(y) + self.__z = mpz(z) + self.__order = order and mpz(order) + else: + self.__x = x + self.__y = y + self.__z = z + self.__order = order self.__precompute=[] if generator: assert order @@ -542,9 +573,14 @@ class Point(object): def __init__(self, curve, x, y, order=None): """curve, x, y, order; order (optional) is the order of this point.""" self.__curve = curve - self.__x = x - self.__y = y - self.__order = order + if GMPY2: + self.__x = x and mpz(x) + self.__y = y and mpz(y) + self.__order = order and mpz(order) + else: + self.__x = x + self.__y = y + self.__order = order # self.curve is allowed to be None only for INFINITY: if self.__curve: assert self.__curve.contains_point(x, y) diff --git a/src/ecdsa/numbertheory.py b/src/ecdsa/numbertheory.py index a84da53b..19abf204 100644 --- a/src/ecdsa/numbertheory.py +++ b/src/ecdsa/numbertheory.py @@ -17,6 +17,11 @@ xrange except NameError: xrange = range +try: + from gmpy2 import powmod + GMPY2=True +except ImportError: + GMPY2=False import math import warnings @@ -201,19 +206,26 @@ def square_root_mod_prime(a, p): raise RuntimeError("No b found.") -def inverse_mod(a, m): - """Inverse of a mod m.""" +if GMPY2: + def inverse_mod(a, m): + """Inverse of a mod m.""" + if a == 0: + return 0 + return powmod(a, -1, m) +else: + def inverse_mod(a, m): + """Inverse of a mod m.""" - if a == 0: - return 0 + if a == 0: + return 0 - lm, hm = 1, 0 - low, high = a % m, m - while low > 1: - r = high // low - lm, low, hm, high = hm - lm * r, high - low * r, lm, low + lm, hm = 1, 0 + low, high = a % m, m + while low > 1: + r = high // low + lm, low, hm, high = hm - lm * r, high - low * r, lm, low - return lm % m + return lm % m try: diff --git a/src/ecdsa/test_ecdsa.py b/src/ecdsa/test_ecdsa.py index 824bed0f..71c68913 100644 --- a/src/ecdsa/test_ecdsa.py +++ b/src/ecdsa/test_ecdsa.py @@ -417,7 +417,7 @@ def st_random_gen_key_msg_nonce(draw): name = draw(st.sampled_from(sorted(name_gen.keys()))) note("Generator used: {0}".format(name)) generator = name_gen[name] - order = generator.order() + order = int(generator.order()) key = draw(st.integers(min_value=1, max_value=order)) msg = draw(st.integers(min_value=1, max_value=order)) diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index a5da1bc7..35e52421 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -14,16 +14,16 @@ class TestJacobi(unittest.TestCase): def test___init__(self): curve = object() - x = object() - y = object() + x = 2 + y = 3 z = 1 - order = object() + order = 4 pj = PointJacobi(curve, x, y, z, order) - self.assertIs(pj.order(), order) + self.assertEqual(pj.order(), order) self.assertIs(pj.curve(), curve) - self.assertIs(pj.x(), x) - self.assertIs(pj.y(), y) + self.assertEqual(pj.x(), x) + self.assertEqual(pj.y(), y) def test_add_with_different_curves(self): p_a = PointJacobi.from_affine(generator_256) @@ -179,7 +179,7 @@ def test_compare_double_with_multiply(self): self.assertEqual(dbl, mlpl) @settings(max_examples=10) - @given(st.integers(min_value=0, max_value=generator_256.order())) + @given(st.integers(min_value=0, max_value=int(generator_256.order()))) def test_multiplications(self, mul): pj = PointJacobi.from_affine(generator_256) pw = pj.to_affine() * mul @@ -190,9 +190,9 @@ def test_multiplications(self, mul): self.assertEqual(pj, pw) @settings(max_examples=10) - @given(st.integers(min_value=0, max_value=generator_256.order())) + @given(st.integers(min_value=0, max_value=int(generator_256.order()))) @example(0) - @example(generator_256.order()) + @example(int(generator_256.order())) def test_precompute(self, mul): precomp = PointJacobi.from_affine(generator_256, True) pj = PointJacobi.from_affine(generator_256) @@ -203,8 +203,8 @@ def test_precompute(self, mul): self.assertEqual(a, b) @settings(max_examples=10) - @given(st.integers(min_value=1, max_value=generator_256.order()), - st.integers(min_value=1, max_value=generator_256.order())) + @given(st.integers(min_value=1, max_value=int(generator_256.order())), + st.integers(min_value=1, max_value=int(generator_256.order()))) @example(3, 3) def test_add_scaled_points(self, a_mul, b_mul): j_g = PointJacobi.from_affine(generator_256) @@ -216,9 +216,9 @@ def test_add_scaled_points(self, a_mul, b_mul): self.assertEqual(c, j_g * (a_mul + b_mul)) @settings(max_examples=10) - @given(st.integers(min_value=1, max_value=generator_256.order()), - st.integers(min_value=1, max_value=generator_256.order()), - st.integers(min_value=1, max_value=curve_256.p()-1)) + @given(st.integers(min_value=1, max_value=int(generator_256.order())), + st.integers(min_value=1, max_value=int(generator_256.order())), + st.integers(min_value=1, max_value=int(curve_256.p()-1))) def test_add_one_scaled_point(self, a_mul, b_mul, new_z): j_g = PointJacobi.from_affine(generator_256) a = PointJacobi.from_affine(j_g * a_mul) @@ -238,13 +238,13 @@ def test_add_one_scaled_point(self, a_mul, b_mul, new_z): self.assertEqual(c, j_g * (a_mul + b_mul)) @settings(max_examples=10) - @given(st.integers(min_value=1, max_value=generator_256.order()), - st.integers(min_value=1, max_value=generator_256.order()), - st.integers(min_value=1, max_value=curve_256.p()-1)) + @given(st.integers(min_value=1, max_value=int(generator_256.order())), + st.integers(min_value=1, max_value=int(generator_256.order())), + st.integers(min_value=1, max_value=int(curve_256.p()-1))) @example(1, 1, 1) @example(3, 3, 3) - @example(2, generator_256.order()-2, 1) - @example(2, generator_256.order()-2, 3) + @example(2, int(generator_256.order()-2), 1) + @example(2, int(generator_256.order()-2), 3) def test_add_same_scale_points(self, a_mul, b_mul, new_z): j_g = PointJacobi.from_affine(generator_256) a = PointJacobi.from_affine(j_g * a_mul) @@ -266,14 +266,14 @@ def test_add_same_scale_points(self, a_mul, b_mul, new_z): self.assertEqual(c, j_g * (a_mul + b_mul)) @settings(max_examples=14) - @given(st.integers(min_value=1, max_value=generator_256.order()), - st.integers(min_value=1, max_value=generator_256.order()), - st.lists(st.integers(min_value=1, max_value=curve_256.p()-1), + @given(st.integers(min_value=1, max_value=int(generator_256.order())), + st.integers(min_value=1, max_value=int(generator_256.order())), + st.lists(st.integers(min_value=1, max_value=int(curve_256.p()-1)), min_size=2, max_size=2, unique=True)) @example(2, 2, [2, 1]) @example(2, 2, [2, 3]) - @example(2, generator_256.order()-2, [2, 3]) - @example(2, generator_256.order()-2, [2, 1]) + @example(2, int(generator_256.order()-2), [2, 3]) + @example(2, int(generator_256.order()-2), [2, 1]) def test_add_different_scale_points(self, a_mul, b_mul, new_z): j_g = PointJacobi.from_affine(generator_256) a = PointJacobi.from_affine(j_g * a_mul) diff --git a/src/ecdsa/test_malformed_sigs.py b/src/ecdsa/test_malformed_sigs.py index cdca8816..c1dca44a 100644 --- a/src/ecdsa/test_malformed_sigs.py +++ b/src/ecdsa/test_malformed_sigs.py @@ -144,7 +144,7 @@ def st_random_der_ecdsa_sig_value(draw): """ name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs)) note("Configuration: {0}".format(name)) - order = verifying_key.curve.order + order = int(verifying_key.curve.order) # the encode_integer doesn't suport negative numbers, would be nice # to generate them too, but we have coverage for remove_integer() diff --git a/tox.ini b/tox.ini index 0b3bcdad..08366c28 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py26, py27, py33, py34, py35, py36, py37, py38, py, pypy, pypy3 +envlist = py26, py27, py33, py34, py35, py36, py37, py38, py, pypy, pypy3, gmppy27, gmppy38 [testenv] deps = @@ -12,6 +12,9 @@ deps = py{26}: hypothesis<3 py{26,27,34,35,36,37,38,py,py3}: pytest py{27,34,35,36,37,38,py,py3}: hypothesis + gmppy{27,38}: gmpy2 + gmppy{27,38}: pytest + gmppy{27,38}: hypothesis # six==1.9.0 comes from setup.py install_requires py27_old_six: six==1.9.0 py27_old_six: pytest @@ -25,6 +28,12 @@ commands = coverage run --branch -m pytest {posargs:src/ecdsa} [testenv:py27_old_six] basepython = python2.7 +[testenv:gmppy27] +basepython=python2.7 + +[testenv:gmppy38] +basepython=python3.8 + [testenv:coverage] sitepackages=True commands = coverage run --branch -m pytest --hypothesis-show-statistics {posargs:src/ecdsa} @@ -32,6 +41,10 @@ commands = coverage run --branch -m pytest --hypothesis-show-statistics {posargs [testenv:speed] commands = {envpython} speed.py +[testenv:speedgmp2] +deps = gmpy2 +commands = {envpython} speed.py + [testenv:codechecks] basepython = python3 deps = From 4403ec3cd2dec60537f5ea69fd902d1b5a6e4e61 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 26 Nov 2019 21:03:38 +0100 Subject: [PATCH 32/34] be explicit about the optional dependency on gmpy2 --- README.md | 16 ++++++++++++++++ setup.py | 3 +++ 2 files changed, 19 insertions(+) diff --git a/README.md b/README.md index 7df12035..b4baabbf 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,22 @@ To run the OpenSSL compatibility tests, the 'openssl' tool must be in your `PATH`. This release has been tested successfully against OpenSSL 0.9.8o, 1.0.0a, 1.0.2f and 1.1.1d (among others). + +## Installation + +This library is available on PyPI, it's recommended to install it using `pip`: + +``` +pip install ecdsa +``` + +In case higher performance is wanted and using native code is not a problem, +it's possible to specify installation together with `gmpy2`: + +``` +pip install ecdsa[gmpy2] +``` + ## Speed The following table shows how long this library takes to generate keypairs diff --git a/setup.py b/setup.py index 13c91646..713f76d8 100755 --- a/setup.py +++ b/setup.py @@ -41,4 +41,7 @@ "Programming Language :: Python :: 3.8", ], install_requires=['six>=1.9.0'], + extras_require={ + 'gmpy2': 'gmpy2', + }, ) From b1568d4a69c8868333e0de37365196079d535dfb Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 26 Nov 2019 23:36:10 +0100 Subject: [PATCH 33/34] also allow the older gmpy since on older distros like CentOS 6 there is python-gmpy but not python-gmpy2, support gmpy too --- .travis.yml | 19 ++++++++++++++----- README.md | 16 ++++++++++++---- setup.py | 1 + src/ecdsa/ellipticcurve.py | 15 ++++++++++----- src/ecdsa/numbertheory.py | 25 +++++++++++++++++++++++++ tox.ini | 25 ++++++++++++++++++------- 6 files changed, 80 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0da41db3..7f66c986 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ language: python cache: pip addons: apt_packages: - # needed for gmpy2 + # needed for gmpy and gmpy2 - libgmp-dev - libmpfr-dev - libmpc-dev @@ -24,7 +24,9 @@ matrix: - python: 2.7 env: TOX_ENV=py27_old_six - python: 2.7 - env: TOX_ENV=gmppy27 + env: TOX_ENV=gmpypy27 + - python: 2.7 + env: TOX_ENV=gmpy2py27 - python: 3.3 env: TOX_ENV=py33 - python: 3.4 @@ -42,7 +44,11 @@ matrix: dist: xenial sudo: true - python: 3.8 - env: TOX_ENV=gmppy38 + env: TOX_ENV=gmpypy38 + dist: xenial + sudo: true + - python: 3.8 + env: TOX_ENV=gmpy2py38 dist: xenial sudo: true - python: nightly @@ -87,12 +93,15 @@ install: else travis_retry pip install -r build-requirements.txt; fi - - if [[ $TOX_ENV =~ gmp ]]; then travis_retry pip install gmpy2; fi + - if [[ $TOX_ENV =~ gmpy2 ]]; then travis_retry pip install gmpy2; fi + - if [[ $TOX_ENV =~ gmpyp ]]; then travis_retry pip install gmpy; fi - if [[ $INSTRUMENTAL ]]; then travis_retry pip install instrumental; fi - pip list script: - if [[ $TOX_ENV ]]; then tox -e $TOX_ENV; fi - - if [[ $TOX_ENV =~ gmp ]]; then tox -e speedgmp2; else tox -e speed; fi + - if [[ $TOX_ENV =~ gmpy2 ]]; then tox -e speedgmpy2; fi + - if [[ $TOX_ENV =~ gmpyp ]]; then tox -e speedgmpy; fi + - if ! [[ $TOX_ENV =~ gmpy ]]; then tox -e speed; fi - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then git checkout $PR_FIRST^ diff --git a/README.md b/README.md index b4baabbf..84e7dc0a 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,10 @@ This library uses only Python and the 'six' package. It is compatible with Python 2.6, 2.7 and 3.3+. It also supports execution on the alternative implementations like pypy and pypy3. -If `gmpy2` is installed, it will be used for faster arithmetic. -`gmpy2` can be installed after this library is installed, `python-ecdsa` will -detect presence of `gmpy2` on start-up and use it automatically. +If `gmpy2` or `gmpy` is installed, they will be used for faster arithmetic. +Either of them can be installed after this library is installed, +`python-ecdsa` will detect their presence on start-up and use them +automatically. To run the OpenSSL compatibility tests, the 'openssl' tool must be in your `PATH`. This release has been tested successfully against OpenSSL 0.9.8o, @@ -58,6 +59,11 @@ it's possible to specify installation together with `gmpy2`: pip install ecdsa[gmpy2] ``` +or (slower, legacy option): +``` +pip install ecdsa[gmpy] +``` + ## Speed The following table shows how long this library takes to generate keypairs @@ -87,7 +93,7 @@ On an Intel Core i7 4790K @ 4.0GHz I'm getting the following performance: BRAINPOOLP384r1: 96 0.00112s 892.44 0.00119s 841.48 0.00229s 436.71 BRAINPOOLP512r1: 128 0.00214s 467.05 0.00226s 441.64 0.00422s 237.13 ``` -To test performance with `gmpy2` loaded, use `tox -e speedgmp2`. +To test performance with `gmpy2` loaded, use `tox -e speedgmpy2`. On the same machine I'm getting the following performance with `gmpy2`: ``` siglen keygen keygen/s sign sign/s verify verify/s @@ -106,6 +112,8 @@ On the same machine I'm getting the following performance with `gmpy2`: BRAINPOOLP512r1: 128 0.00062s 1618.30 0.00063s 1577.42 0.00125s 799.29 ``` +(there's also `gmpy` version, execute it using `tox -e speedgmpy`) + For comparison, a highly optimised implementation (including curve-specific assembly for some curves), like the one in OpenSSL, provides following performance numbers on the same machine. diff --git a/setup.py b/setup.py index 713f76d8..0800d71e 100755 --- a/setup.py +++ b/setup.py @@ -43,5 +43,6 @@ install_requires=['six>=1.9.0'], extras_require={ 'gmpy2': 'gmpy2', + 'gmpy': 'gmpy', }, ) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 54c22305..ec4625ae 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -37,9 +37,14 @@ try: from gmpy2 import mpz - GMPY2=True + GMPY=True except ImportError: - GMPY2=False + try: + from gmpy import mpz + GMPY=True + except ImportError: + GMPY=False + from six import python_2_unicode_compatible from . import numbertheory @@ -49,7 +54,7 @@ class CurveFp(object): """Elliptic Curve over the field of integers modulo a prime.""" - if GMPY2: + if GMPY: def __init__(self, p, a, b, h=None): """ The curve of points satisfying y^2 = x^3 + a*x + b (mod p). @@ -137,7 +142,7 @@ def __init__(self, curve, x, y, z, order=None, generator=False): cause to precompute multiplication table for it """ self.__curve = curve - if GMPY2: + if GMPY: self.__x = mpz(x) self.__y = mpz(y) self.__z = mpz(z) @@ -573,7 +578,7 @@ class Point(object): def __init__(self, curve, x, y, order=None): """curve, x, y, order; order (optional) is the order of this point.""" self.__curve = curve - if GMPY2: + if GMPY: self.__x = x and mpz(x) self.__y = y and mpz(y) self.__order = order and mpz(order) diff --git a/src/ecdsa/numbertheory.py b/src/ecdsa/numbertheory.py index 19abf204..88f7cbbc 100644 --- a/src/ecdsa/numbertheory.py +++ b/src/ecdsa/numbertheory.py @@ -20,8 +20,14 @@ try: from gmpy2 import powmod GMPY2=True + GMPY=False except ImportError: GMPY2=False + try: + from gmpy import mpz + GMPY=True + except ImportError: + GMPY=False import math import warnings @@ -212,6 +218,25 @@ def inverse_mod(a, m): if a == 0: return 0 return powmod(a, -1, m) +elif GMPY: + def inverse_mod(a, m): + """Inverse of a mod m.""" + # while libgmp likely does support inverses modulo, it is accessible + # only using the native `pow()` function, and `pow()` sanity checks + # the parameters before passing them on to underlying implementation + # on Python2 + if a == 0: + return 0 + a = mpz(a) + m = mpz(m) + + lm, hm = mpz(1), mpz(0) + low, high = a % m, m + while low > 1: + r = high // low + lm, low, hm, high = hm - lm * r, high - low * r, lm, low + + return lm % m else: def inverse_mod(a, m): """Inverse of a mod m.""" diff --git a/tox.ini b/tox.ini index 08366c28..1416479a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py26, py27, py33, py34, py35, py36, py37, py38, py, pypy, pypy3, gmppy27, gmppy38 +envlist = py26, py27, py33, py34, py35, py36, py37, py38, py, pypy, pypy3, gmpy2py27, gmpy2py38, gmpypy27, gmpypy38 [testenv] deps = @@ -12,9 +12,10 @@ deps = py{26}: hypothesis<3 py{26,27,34,35,36,37,38,py,py3}: pytest py{27,34,35,36,37,38,py,py3}: hypothesis - gmppy{27,38}: gmpy2 - gmppy{27,38}: pytest - gmppy{27,38}: hypothesis + gmpy2py{27,38}: gmpy2 + gmpypy{27,38}: gmpy + gmpy{2py27,2py38,py27,py38}: pytest + gmpy{2py27,2py38,py27,py38}: hypothesis # six==1.9.0 comes from setup.py install_requires py27_old_six: six==1.9.0 py27_old_six: pytest @@ -28,10 +29,16 @@ commands = coverage run --branch -m pytest {posargs:src/ecdsa} [testenv:py27_old_six] basepython = python2.7 -[testenv:gmppy27] +[testenv:gmpypy27] basepython=python2.7 -[testenv:gmppy38] +[testenv:gmpypy38] +basepython=python3.8 + +[testenv:gmpy2py27] +basepython=python2.7 + +[testenv:gmpy2py38] basepython=python3.8 [testenv:coverage] @@ -41,7 +48,11 @@ commands = coverage run --branch -m pytest --hypothesis-show-statistics {posargs [testenv:speed] commands = {envpython} speed.py -[testenv:speedgmp2] +[testenv:speedgmpy] +deps = gmpy +commands = {envpython} speed.py + +[testenv:speedgmpy2] deps = gmpy2 commands = {envpython} speed.py From a67da69a9f91e4e16594d704ed91b659bb1afdc8 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 30 Nov 2019 01:17:52 +0100 Subject: [PATCH 34/34] ensure old versions of gmpy(2) work --- .travis.yml | 4 ++++ tox.ini | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7f66c986..2b516ae2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,10 @@ matrix: env: TOX_ENV=py26 - python: 2.7 env: TOX_ENV=py27 + - python: 2.7 + env: TOX_ENV=py27_old_gmpy + - python: 2.7 + env: TOX_ENV=py27_old_gmpy2 - python: 2.7 env: TOX_ENV=py27_old_six - python: 2.7 diff --git a/tox.ini b/tox.ini index 1416479a..99ee89bc 100644 --- a/tox.ini +++ b/tox.ini @@ -20,12 +20,26 @@ deps = py27_old_six: six==1.9.0 py27_old_six: pytest py27_old_six: hypothesis +# those are the oldest versions of gmpy and gmpy2 on PyPI (i.e. oldest we can +# actually test), older versions may work, but are not easy to test + py27_old_gmpy: gmpy==1.15 + py27_old_gmpy: pytest + py27_old_gmpy: hypothesis + py27_old_gmpy2: gmpy2==2.0.1 + py27_old_gmpy2: pytest + py27_old_gmpy2: hypothesis py: pytest py: hypothesis py{33}: wheel<0.30 coverage commands = coverage run --branch -m pytest {posargs:src/ecdsa} +[testenv:py27_old_gmpy] +basepython = python2.7 + +[testenv:py27_old_gmpy2] +basepython = python2.7 + [testenv:py27_old_six] basepython = python2.7