Skip to content

Commit f7910cf

Browse files
committed
use gmp2 for faster arithmetic
1 parent 8ab416b commit f7910cf

File tree

8 files changed

+158
-61
lines changed

8 files changed

+158
-61
lines changed

.travis.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ dist: trusty
44
sudo: false
55
language: python
66
cache: pip
7+
addons:
8+
apt_packages:
9+
# needed for gmpy2
10+
- libgmp-dev
11+
- libmpfr-dev
12+
- libmpc-dev
713
before_cache:
814
- rm -f $HOME/.cache/pip/log/debug.log
915
# place the slowest (instrumental and py2.6) first
@@ -17,6 +23,8 @@ matrix:
1723
env: TOX_ENV=py27
1824
- python: 2.7
1925
env: TOX_ENV=py27_old_six
26+
- python: 2.7
27+
env: TOX_ENV=gmppy27
2028
- python: 3.3
2129
env: TOX_ENV=py33
2230
- python: 3.4
@@ -33,6 +41,10 @@ matrix:
3341
env: TOX_ENV=py38
3442
dist: xenial
3543
sudo: true
44+
- python: 3.8
45+
env: TOX_ENV=gmppy38
46+
dist: xenial
47+
sudo: true
3648
- python: nightly
3749
env: TOX_ENV=py
3850
dist: xenial
@@ -75,11 +87,12 @@ install:
7587
else
7688
travis_retry pip install -r build-requirements.txt;
7789
fi
90+
- if [[ $TOX_ENV =~ gmp ]]; then travis_retry pip install gmpy2; fi
7891
- if [[ $INSTRUMENTAL ]]; then travis_retry pip install instrumental; fi
7992
- pip list
8093
script:
8194
- if [[ $TOX_ENV ]]; then tox -e $TOX_ENV; fi
82-
- tox -e speed
95+
- if [[ $TOX_ENV =~ gmp ]]; then tox -e speedgmp2; else tox -e speed; fi
8396
- |
8497
if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then
8598
git checkout $PR_FIRST^

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ curves over prime fields.
3232

3333
This library uses only Python and the 'six' package. It is compatible with
3434
Python 2.6, 2.7 and 3.3+. It also supports execution on the alternative
35-
implementations like pypy and pypy3
35+
implementations like pypy and pypy3.
36+
37+
If `gmpy2` is installed, it will be used for faster arithmetic.
38+
`gmpy2` can be installed after this library is installed, `python-ecdsa` will
39+
detect presence of `gmpy2` on start-up and use it automatically.
3640

3741
To run the OpenSSL compatibility tests, the 'openssl' tool must be in your
3842
`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:
6771
BRAINPOOLP384r1: 96 0.00112s 892.44 0.00119s 841.48 0.00229s 436.71
6872
BRAINPOOLP512r1: 128 0.00214s 467.05 0.00226s 441.64 0.00422s 237.13
6973
```
74+
To test performance with `gmpy2` loaded, use `tox -e speedgmp2`.
75+
On the same machine I'm getting the following performance with `gmpy2`:
76+
```
77+
siglen keygen keygen/s sign sign/s verify verify/s
78+
NIST192p: 48 0.00016s 6180.12 0.00017s 5846.30 0.00033s 3029.51
79+
NIST224p: 56 0.00021s 4861.86 0.00021s 4662.63 0.00042s 2366.47
80+
NIST256p: 64 0.00023s 4343.93 0.00024s 4152.79 0.00047s 2148.83
81+
NIST384p: 96 0.00040s 2507.97 0.00041s 2435.99 0.00079s 1260.01
82+
NIST521p: 132 0.00088s 1135.13 0.00089s 1121.94 0.00139s 721.07
83+
SECP256k1: 64 0.00023s 4360.83 0.00024s 4147.61 0.00044s 2254.53
84+
BRAINPOOLP160r1: 40 0.00014s 7261.37 0.00015s 6824.47 0.00031s 3248.21
85+
BRAINPOOLP192r1: 48 0.00016s 6219.18 0.00017s 5862.93 0.00034s 2933.74
86+
BRAINPOOLP224r1: 56 0.00021s 4876.48 0.00022s 4640.40 0.00041s 2413.48
87+
BRAINPOOLP256r1: 64 0.00023s 4397.89 0.00024s 4178.48 0.00044s 2272.76
88+
BRAINPOOLP320r1: 80 0.00031s 3246.64 0.00032s 3138.38 0.00063s 1593.14
89+
BRAINPOOLP384r1: 96 0.00040s 2491.04 0.00041s 2421.67 0.00079s 1262.64
90+
BRAINPOOLP512r1: 128 0.00062s 1618.30 0.00063s 1577.42 0.00125s 799.29
91+
```
7092

7193
For comparison, a highly optimised implementation (including curve-specific
72-
assembly) like OpenSSL provides following performance numbers on the same
73-
machine. Run `openssl speed ecdsa` to reproduce it:
94+
assembly for some curves), like the one in OpenSSL, provides following
95+
performance numbers on the same machine.
96+
Run `openssl speed ecdsa` to reproduce it:
7497
```
7598
sign verify sign/s verify/s
7699
192 bits ecdsa (nistp192) 0.0002s 0.0002s 4785.6 5380.7

src/ecdsa/ellipticcurve.py

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,50 @@
3535

3636
from __future__ import division
3737

38+
try:
39+
from gmpy2 import mpz
40+
GMPY2=True
41+
except ImportError:
42+
GMPY2=False
43+
3844
from six import python_2_unicode_compatible
3945
from . import numbertheory
4046

47+
4148
@python_2_unicode_compatible
4249
class CurveFp(object):
4350
"""Elliptic Curve over the field of integers modulo a prime."""
44-
def __init__(self, p, a, b, h=None):
45-
"""
46-
The curve of points satisfying y^2 = x^3 + a*x + b (mod p).
47-
48-
h is an integer that is the cofactor of the elliptic curve domain
49-
parameters; it is the number of points satisfying the elliptic curve
50-
equation divided by the order of the base point. It is used for selection
51-
of efficient algorithm for public point verification.
52-
"""
53-
self.__p = p
54-
self.__a = a
55-
self.__b = b
56-
self.__h = h
51+
52+
if GMPY2:
53+
def __init__(self, p, a, b, h=None):
54+
"""
55+
The curve of points satisfying y^2 = x^3 + a*x + b (mod p).
56+
57+
h is an integer that is the cofactor of the elliptic curve domain
58+
parameters; it is the number of points satisfying the elliptic curve
59+
equation divided by the order of the base point. It is used for selection
60+
of efficient algorithm for public point verification.
61+
"""
62+
self.__p = mpz(p)
63+
self.__a = mpz(a)
64+
self.__b = mpz(b)
65+
# h is not used in calculations and it can be None, so don't use
66+
# gmpy with it
67+
self.__h = h
68+
else:
69+
def __init__(self, p, a, b, h=None):
70+
"""
71+
The curve of points satisfying y^2 = x^3 + a*x + b (mod p).
72+
73+
h is an integer that is the cofactor of the elliptic curve domain
74+
parameters; it is the number of points satisfying the elliptic curve
75+
equation divided by the order of the base point. It is used for selection
76+
of efficient algorithm for public point verification.
77+
"""
78+
self.__p = p
79+
self.__a = a
80+
self.__b = b
81+
self.__h = h
5782

5883
def __eq__(self, other):
5984
if isinstance(other, CurveFp):
@@ -112,10 +137,16 @@ def __init__(self, curve, x, y, z, order=None, generator=False):
112137
cause to precompute multiplication table for it
113138
"""
114139
self.__curve = curve
115-
self.__x = x
116-
self.__y = y
117-
self.__z = z
118-
self.__order = order
140+
if GMPY2:
141+
self.__x = mpz(x)
142+
self.__y = mpz(y)
143+
self.__z = mpz(z)
144+
self.__order = order and mpz(order)
145+
else:
146+
self.__x = x
147+
self.__y = y
148+
self.__z = z
149+
self.__order = order
119150
self.__precompute=[]
120151
if generator:
121152
assert order
@@ -542,9 +573,14 @@ class Point(object):
542573
def __init__(self, curve, x, y, order=None):
543574
"""curve, x, y, order; order (optional) is the order of this point."""
544575
self.__curve = curve
545-
self.__x = x
546-
self.__y = y
547-
self.__order = order
576+
if GMPY2:
577+
self.__x = x and mpz(x)
578+
self.__y = y and mpz(y)
579+
self.__order = order and mpz(order)
580+
else:
581+
self.__x = x
582+
self.__y = y
583+
self.__order = order
548584
# self.curve is allowed to be None only for INFINITY:
549585
if self.__curve:
550586
assert self.__curve.contains_point(x, y)

src/ecdsa/numbertheory.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
xrange
1818
except NameError:
1919
xrange = range
20+
try:
21+
from gmpy2 import powmod
22+
GMPY2=True
23+
except ImportError:
24+
GMPY2=False
2025

2126
import math
2227
import warnings
@@ -201,19 +206,26 @@ def square_root_mod_prime(a, p):
201206
raise RuntimeError("No b found.")
202207

203208

204-
def inverse_mod(a, m):
205-
"""Inverse of a mod m."""
209+
if GMPY2:
210+
def inverse_mod(a, m):
211+
"""Inverse of a mod m."""
212+
if a == 0:
213+
return 0
214+
return powmod(a, -1, m)
215+
else:
216+
def inverse_mod(a, m):
217+
"""Inverse of a mod m."""
206218

207-
if a == 0:
208-
return 0
219+
if a == 0:
220+
return 0
209221

210-
lm, hm = 1, 0
211-
low, high = a % m, m
212-
while low > 1:
213-
r = high // low
214-
lm, low, hm, high = hm - lm * r, high - low * r, lm, low
222+
lm, hm = 1, 0
223+
low, high = a % m, m
224+
while low > 1:
225+
r = high // low
226+
lm, low, hm, high = hm - lm * r, high - low * r, lm, low
215227

216-
return lm % m
228+
return lm % m
217229

218230

219231
try:

src/ecdsa/test_ecdsa.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ def st_random_gen_key_msg_nonce(draw):
417417
name = draw(st.sampled_from(sorted(name_gen.keys())))
418418
note("Generator used: {0}".format(name))
419419
generator = name_gen[name]
420-
order = generator.order()
420+
order = int(generator.order())
421421

422422
key = draw(st.integers(min_value=1, max_value=order))
423423
msg = draw(st.integers(min_value=1, max_value=order))

src/ecdsa/test_jacobi.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414
class TestJacobi(unittest.TestCase):
1515
def test___init__(self):
1616
curve = object()
17-
x = object()
18-
y = object()
17+
x = 2
18+
y = 3
1919
z = 1
20-
order = object()
20+
order = 4
2121
pj = PointJacobi(curve, x, y, z, order)
2222

23-
self.assertIs(pj.order(), order)
23+
self.assertEqual(pj.order(), order)
2424
self.assertIs(pj.curve(), curve)
25-
self.assertIs(pj.x(), x)
26-
self.assertIs(pj.y(), y)
25+
self.assertEqual(pj.x(), x)
26+
self.assertEqual(pj.y(), y)
2727

2828
def test_add_with_different_curves(self):
2929
p_a = PointJacobi.from_affine(generator_256)
@@ -179,7 +179,7 @@ def test_compare_double_with_multiply(self):
179179
self.assertEqual(dbl, mlpl)
180180

181181
@settings(max_examples=10)
182-
@given(st.integers(min_value=0, max_value=generator_256.order()))
182+
@given(st.integers(min_value=0, max_value=int(generator_256.order())))
183183
def test_multiplications(self, mul):
184184
pj = PointJacobi.from_affine(generator_256)
185185
pw = pj.to_affine() * mul
@@ -190,9 +190,9 @@ def test_multiplications(self, mul):
190190
self.assertEqual(pj, pw)
191191

192192
@settings(max_examples=10)
193-
@given(st.integers(min_value=0, max_value=generator_256.order()))
193+
@given(st.integers(min_value=0, max_value=int(generator_256.order())))
194194
@example(0)
195-
@example(generator_256.order())
195+
@example(int(generator_256.order()))
196196
def test_precompute(self, mul):
197197
precomp = PointJacobi.from_affine(generator_256, True)
198198
pj = PointJacobi.from_affine(generator_256)
@@ -203,8 +203,8 @@ def test_precompute(self, mul):
203203
self.assertEqual(a, b)
204204

205205
@settings(max_examples=10)
206-
@given(st.integers(min_value=1, max_value=generator_256.order()),
207-
st.integers(min_value=1, max_value=generator_256.order()))
206+
@given(st.integers(min_value=1, max_value=int(generator_256.order())),
207+
st.integers(min_value=1, max_value=int(generator_256.order())))
208208
@example(3, 3)
209209
def test_add_scaled_points(self, a_mul, b_mul):
210210
j_g = PointJacobi.from_affine(generator_256)
@@ -216,9 +216,9 @@ def test_add_scaled_points(self, a_mul, b_mul):
216216
self.assertEqual(c, j_g * (a_mul + b_mul))
217217

218218
@settings(max_examples=10)
219-
@given(st.integers(min_value=1, max_value=generator_256.order()),
220-
st.integers(min_value=1, max_value=generator_256.order()),
221-
st.integers(min_value=1, max_value=curve_256.p()-1))
219+
@given(st.integers(min_value=1, max_value=int(generator_256.order())),
220+
st.integers(min_value=1, max_value=int(generator_256.order())),
221+
st.integers(min_value=1, max_value=int(curve_256.p()-1)))
222222
def test_add_one_scaled_point(self, a_mul, b_mul, new_z):
223223
j_g = PointJacobi.from_affine(generator_256)
224224
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):
238238
self.assertEqual(c, j_g * (a_mul + b_mul))
239239

240240
@settings(max_examples=10)
241-
@given(st.integers(min_value=1, max_value=generator_256.order()),
242-
st.integers(min_value=1, max_value=generator_256.order()),
243-
st.integers(min_value=1, max_value=curve_256.p()-1))
241+
@given(st.integers(min_value=1, max_value=int(generator_256.order())),
242+
st.integers(min_value=1, max_value=int(generator_256.order())),
243+
st.integers(min_value=1, max_value=int(curve_256.p()-1)))
244244
@example(1, 1, 1)
245245
@example(3, 3, 3)
246-
@example(2, generator_256.order()-2, 1)
247-
@example(2, generator_256.order()-2, 3)
246+
@example(2, int(generator_256.order()-2), 1)
247+
@example(2, int(generator_256.order()-2), 3)
248248
def test_add_same_scale_points(self, a_mul, b_mul, new_z):
249249
j_g = PointJacobi.from_affine(generator_256)
250250
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):
266266
self.assertEqual(c, j_g * (a_mul + b_mul))
267267

268268
@settings(max_examples=14)
269-
@given(st.integers(min_value=1, max_value=generator_256.order()),
270-
st.integers(min_value=1, max_value=generator_256.order()),
271-
st.lists(st.integers(min_value=1, max_value=curve_256.p()-1),
269+
@given(st.integers(min_value=1, max_value=int(generator_256.order())),
270+
st.integers(min_value=1, max_value=int(generator_256.order())),
271+
st.lists(st.integers(min_value=1, max_value=int(curve_256.p()-1)),
272272
min_size=2, max_size=2, unique=True))
273273
@example(2, 2, [2, 1])
274274
@example(2, 2, [2, 3])
275-
@example(2, generator_256.order()-2, [2, 3])
276-
@example(2, generator_256.order()-2, [2, 1])
275+
@example(2, int(generator_256.order()-2), [2, 3])
276+
@example(2, int(generator_256.order()-2), [2, 1])
277277
def test_add_different_scale_points(self, a_mul, b_mul, new_z):
278278
j_g = PointJacobi.from_affine(generator_256)
279279
a = PointJacobi.from_affine(j_g * a_mul)

src/ecdsa/test_malformed_sigs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def st_random_der_ecdsa_sig_value(draw):
144144
"""
145145
name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs))
146146
note("Configuration: {0}".format(name))
147-
order = verifying_key.curve.order
147+
order = int(verifying_key.curve.order)
148148

149149
# the encode_integer doesn't suport negative numbers, would be nice
150150
# to generate them too, but we have coverage for remove_integer()

0 commit comments

Comments
 (0)