From 08fd434b92f6ad5ceac53adb8d91ae001cce59ca Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 00:24:59 +0200 Subject: [PATCH 01/23] remove unused code leftover after compressed points implementation --- src/ecdsa/keys.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 9e289935..b2872a35 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -223,9 +223,6 @@ def to_pem(self): return der.topem(self.to_der(), "PUBLIC KEY") def to_der(self, point_encoding="uncompressed"): - order = self.pubkey.order - x_str = number_to_string(self.pubkey.point.x(), order) - y_str = number_to_string(self.pubkey.point.y(), order) point_str = b("\x00") + self.to_string(point_encoding) return der.encode_sequence(der.encode_sequence(encoded_oid_ecPublicKey, self.curve.encoded_oid), From 35ea44ca675e27762f14e9004c2a86eccd04caa9 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 01:04:17 +0200 Subject: [PATCH 02/23] export user-visible exceptions in primary module since MalformedPointError and UnexpectedDER are user-visible (thrown by methods that users are expected to use), export them in the root of the module hierarchy --- README.md | 4 ++-- src/ecdsa/__init__.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ca993d7f..36324362 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ is to call `s=sk.to_string()`, and then re-create it with `SigningKey.from_string(s, curve)` . This short form does not record the curve, so you must be sure to tell from_string() the same curve you used for the original key. The short form of a NIST192p-based signing key is just 24 -bytes long. If the point encoding is invalid or it does not lie on the -specified curve, `from_string()` will raise MalformedPointError. +bytes long. If a point encoding is invalid or it does not lie on the specified +curve, `from_string()` will raise MalformedPointError. ```python from ecdsa import SigningKey, NIST384p diff --git a/src/ecdsa/__init__.py b/src/ecdsa/__init__.py index d896bbc1..1075810c 100644 --- a/src/ecdsa/__init__.py +++ b/src/ecdsa/__init__.py @@ -1,5 +1,7 @@ -from .keys import SigningKey, VerifyingKey, BadSignatureError, BadDigestError +from .keys import SigningKey, VerifyingKey, BadSignatureError, BadDigestError,\ + MalformedPointError from .curves import NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1 +from .der import UnexpectedDER # This code comes from http://github.com/warner/python-ecdsa from ._version import get_versions @@ -10,5 +12,6 @@ "test_pyecdsa", "util", "six"] _hush_pyflakes = [SigningKey, VerifyingKey, BadSignatureError, BadDigestError, + MalformedPointError, UnexpectedDER, NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1] del _hush_pyflakes From 0e907c08dc21d8ed3b67872e7450450fce8136d5 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 01:32:44 +0200 Subject: [PATCH 03/23] make sure assert is not optimised out as assert will be optimised out if the module is compiled with optimisations on, we can't use them for checking user-provided data use an exception that inherits from it, so that existing code will behave as expected --- .coveragerc | 6 +- src/ecdsa/keys.py | 21 +++-- src/ecdsa/test_pyecdsa.py | 170 +++++++++++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 13 deletions(-) diff --git a/.coveragerc b/.coveragerc index 5549ac31..64fdd3e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,9 +4,5 @@ include = src/ecdsa/* omit = - src/ecdsa/six.py src/ecdsa/_version.py - src/ecdsa/test_ecdsa.py - src/ecdsa/test_ellipticcurve.py - src/ecdsa/test_numbertheory.py - src/ecdsa/test_pyecdsa.py + src/ecdsa/test_* diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index b2872a35..8c006b51 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -48,12 +48,14 @@ def from_public_point(klass, point, curve=NIST192p, hashfunc=sha1): @staticmethod def _from_raw_encoding(string, curve, validate_point): order = curve.order - assert (len(string) == curve.verifying_key_length), \ - (len(string), curve.verifying_key_length) + # real assert, from_string() should not call us with different length + assert len(string) == curve.verifying_key_length xs = string[:curve.baselen] ys = string[curve.baselen:] - assert len(xs) == curve.baselen, (len(xs), curve.baselen) - assert len(ys) == curve.baselen, (len(ys), curve.baselen) + if len(xs) != curve.baselen: + raise MalformedPointError("Unexpected length of encoded x") + if len(ys) != curve.baselen: + 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): @@ -86,6 +88,7 @@ def _from_compressed(string, curve, validate_point): @classmethod def _from_hybrid(cls, string, curve, validate_point): + # real assert, from_string() should not call us with different types assert string[:1] in (b('\x06'), b('\x07')) # primarily use the uncompressed as it's easiest to handle @@ -271,7 +274,10 @@ def from_secret_exponent(klass, secexp, curve=NIST192p, hashfunc=sha1): self.default_hashfunc = hashfunc self.baselen = curve.baselen n = curve.order - assert 1 <= secexp < n + if not 1 <= secexp < n: + raise MalformedPointError( + "Invalid value for secexp, expected integer between 1 and {0}" + .format(n)) pubkey_point = curve.generator * secexp pubkey = ecdsa.Public_key(curve.generator, pubkey_point) pubkey.order = n @@ -283,7 +289,10 @@ def from_secret_exponent(klass, secexp, curve=NIST192p, hashfunc=sha1): @classmethod def from_string(klass, string, curve=NIST192p, hashfunc=sha1): - assert len(string) == curve.baselen, (len(string), curve.baselen) + if len(string) != curve.baselen: + raise MalformedPointError( + "Invalid length of private key, received {0}, expected {1}" + .format(len(string), curve.baselen)) secexp = string_to_number(string) return klass.from_secret_exponent(secexp, curve, hashfunc) diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index bcca3ce8..36b989df 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -14,17 +14,19 @@ from six import b, print_, binary_type from .keys import SigningKey, VerifyingKey -from .keys import BadSignatureError, MalformedPointError +from .keys import BadSignatureError, MalformedPointError, BadDigestError from . import util from .util import sigencode_der, sigencode_strings from .util import sigdecode_der, sigdecode_strings -from .util import number_to_string +from .util import number_to_string, encoded_oid_ecPublicKey, \ + MalformedSignature from .curves import Curve, UnknownCurveError from .curves import NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, \ SECP256k1, curves from .ellipticcurve import Point from . import der from . import rfc6979 +from . import ecdsa class SubprocessError(Exception): @@ -275,6 +277,47 @@ def order(self): pub2 = VerifyingKey.from_pem(pem) self.assertTruePubkeysEqual(pub1, pub2) + def test_vk_from_der_garbage_after_curve_oid(self): + type_oid_der = encoded_oid_ecPublicKey + curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + \ + b('garbage') + enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) + point_der = der.encode_bitstring(b'\x00\xff') + to_decode = der.encode_sequence(enc_type_der, point_der) + + with self.assertRaises(der.UnexpectedDER): + VerifyingKey.from_der(to_decode) + + def test_vk_from_der_invalid_key_type(self): + type_oid_der = der.encode_oid(*(1, 2, 3)) + curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) + point_der = der.encode_bitstring(b'\x00\xff') + to_decode = der.encode_sequence(enc_type_der, point_der) + + with self.assertRaises(der.UnexpectedDER): + VerifyingKey.from_der(to_decode) + + def test_vk_from_der_garbage_after_point_string(self): + type_oid_der = encoded_oid_ecPublicKey + curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) + point_der = der.encode_bitstring(b'\x00\xff') + b('garbage') + to_decode = der.encode_sequence(enc_type_der, point_der) + + with self.assertRaises(der.UnexpectedDER): + VerifyingKey.from_der(to_decode) + + def test_vk_from_der_invalid_bitstring(self): + type_oid_der = encoded_oid_ecPublicKey + curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) + point_der = der.encode_bitstring(b'\x08\xff') + to_decode = der.encode_sequence(enc_type_der, point_der) + + with self.assertRaises(der.UnexpectedDER): + VerifyingKey.from_der(to_decode) + def test_signature_strings(self): priv1 = SigningKey.generate() pub1 = priv1.get_verifying_key() @@ -298,6 +341,86 @@ def test_signature_strings(self): self.assertEqual(type(sig_der), binary_type) self.assertTrue(pub1.verify(sig_der, data, sigdecode=sigdecode_der)) + def test_sig_decode_strings_with_invalid_count(self): + with self.assertRaises(MalformedSignature): + sigdecode_strings([b('one'), b('two'), b('three')], 0xff) + + def test_sig_decode_strings_with_wrong_r_len(self): + with self.assertRaises(MalformedSignature): + sigdecode_strings([b('one'), b('two')], 0xff) + + def test_sig_decode_strings_with_wrong_s_len(self): + with self.assertRaises(MalformedSignature): + sigdecode_strings([b('\xa0'), b('\xb0\xff')], 0xff) + + def test_verify_with_too_long_input(self): + sk = SigningKey.generate() + vk = sk.verifying_key + + with self.assertRaises(BadDigestError): + vk.verify_digest(None, b('\x00') * 128) + + def test_sk_from_secret_exponent_with_wrong_sec_exponent(self): + with self.assertRaises(MalformedPointError): + SigningKey.from_secret_exponent(0) + + def test_sk_from_string_with_wrong_len_string(self): + with self.assertRaises(MalformedPointError): + SigningKey.from_string(b('\x01')) + + def test_sk_from_der_with_junk_after_sequence(self): + ver_der = der.encode_integer(1) + to_decode = der.encode_sequence(ver_der) + b('garbage') + + with self.assertRaises(der.UnexpectedDER): + SigningKey.from_der(to_decode) + + def test_sk_from_der_with_wrong_version(self): + ver_der = der.encode_integer(0) + to_decode = der.encode_sequence(ver_der) + + with self.assertRaises(der.UnexpectedDER): + SigningKey.from_der(to_decode) + + def test_sk_from_der_invalid_const_tag(self): + ver_der = der.encode_integer(1) + privkey_der = der.encode_octet_string(b('\x00\xff')) + curve_oid_der = der.encode_oid(*(1, 2, 3)) + const_der = der.encode_constructed(1, curve_oid_der) + to_decode = der.encode_sequence(ver_der, privkey_der, const_der, + curve_oid_der) + + with self.assertRaises(der.UnexpectedDER): + SigningKey.from_der(to_decode) + + def test_sk_from_der_garbage_after_privkey_oid(self): + ver_der = der.encode_integer(1) + privkey_der = der.encode_octet_string(b('\x00\xff')) + curve_oid_der = der.encode_oid(*(1, 2, 3)) + b('garbage') + const_der = der.encode_constructed(0, curve_oid_der) + to_decode = der.encode_sequence(ver_der, privkey_der, const_der, + curve_oid_der) + + with self.assertRaises(der.UnexpectedDER): + SigningKey.from_der(to_decode) + + def test_sk_from_der_with_short_privkey(self): + ver_der = der.encode_integer(1) + privkey_der = der.encode_octet_string(b('\x00\xff')) + curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + const_der = der.encode_constructed(0, curve_oid_der) + to_decode = der.encode_sequence(ver_der, privkey_der, const_der, + curve_oid_der) + + sk = SigningKey.from_der(to_decode) + self.assertEqual(sk.privkey.secret_multiplier, 255) + + def test_sign_with_too_long_hash(self): + sk = SigningKey.from_secret_exponent(12) + + with self.assertRaises(BadDigestError): + sk.sign_digest(b('\xff') * 64) + def test_hashfunc(self): sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) data = b("security level is 128 bits") @@ -448,6 +571,49 @@ 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 + # that inconsistent values are detected + curve = Curve("test", ecdsa.curve_192, ecdsa.generator_192, (1, 2)) + curve.verifying_key_length = 16 + curve.baselen = 32 + + with self.assertRaises(MalformedPointError): + VerifyingKey.from_string(b('\x00')*16, curve) + + def test_from_string_with_invalid_curve_too_long_ver_key_len(self): + # both verifying_key_length and baselen are calculated internally + # by the Curve constructor, but since we depend on them verify + # that inconsistent values are detected + curve = Curve("test", ecdsa.curve_192, ecdsa.generator_192, (1, 2)) + curve.verifying_key_length = 16 + curve.baselen = 16 + + with self.assertRaises(MalformedPointError): + VerifyingKey.from_string(b('\x00')*16, curve) + @pytest.mark.parametrize("val,even", [(i, j) for i in range(256) for j in [True, False]]) From 83dab294681aff1dfa9b379bbf23fe2a5fb505de Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 01:54:48 +0200 Subject: [PATCH 04/23] deprecate unused methods There are few pure-maths methods that are unused by the library code, mark them deprecated so that we can remove them in the future --- src/ecdsa/numbertheory.py | 50 +++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/ecdsa/numbertheory.py b/src/ecdsa/numbertheory.py index d7b906e3..27a9b0f1 100644 --- a/src/ecdsa/numbertheory.py +++ b/src/ecdsa/numbertheory.py @@ -19,6 +19,7 @@ xrange = range import math +import warnings class Error(Exception): @@ -326,8 +327,13 @@ def factorization(n): return result -def phi(n): +def phi(n): # pragma: no cover """Return the Euler totient function of n.""" + # deprecated in 0.14 + warnings.warn("Function is unused by library code. If you use this code, " + "please open an issue in " + "https://github.com/warner/python-ecdsa", + DeprecationWarning) assert isinstance(n, integer_types) @@ -345,20 +351,30 @@ def phi(n): return result -def carmichael(n): +def carmichael(n): # pragma: no cover """Return Carmichael function of n. Carmichael(n) is the smallest integer x such that m**x = 1 mod n for all m relatively prime to n. """ + # deprecated in 0.14 + warnings.warn("Function is unused by library code. If you use this code, " + "please open an issue in " + "https://github.com/warner/python-ecdsa", + DeprecationWarning) return carmichael_of_factorized(factorization(n)) -def carmichael_of_factorized(f_list): +def carmichael_of_factorized(f_list): # pragma: no cover """Return the Carmichael function of a number that is represented as a list of (prime,exponent) pairs. """ + # deprecated in 0.14 + warnings.warn("Function is unused by library code. If you use this code, " + "please open an issue in " + "https://github.com/warner/python-ecdsa", + DeprecationWarning) if len(f_list) < 1: return 1 @@ -370,9 +386,14 @@ def carmichael_of_factorized(f_list): return result -def carmichael_of_ppower(pp): +def carmichael_of_ppower(pp): # pragma: no cover """Carmichael function of the given power of the given prime. """ + # deprecated in 0.14 + warnings.warn("Function is unused by library code. If you use this code, " + "please open an issue in " + "https://github.com/warner/python-ecdsa", + DeprecationWarning) p, a = pp if p == 2 and a > 2: @@ -381,9 +402,14 @@ def carmichael_of_ppower(pp): return (p - 1) * p**(a - 1) -def order_mod(x, m): +def order_mod(x, m): # pragma: no cover """Return the order of x in the multiplicative group mod m. """ + # deprecated in 0.14 + warnings.warn("Function is unused by library code. If you use this code, " + "please open an issue in " + "https://github.com/warner/python-ecdsa", + DeprecationWarning) # Warning: this implementation is not very clever, and will # take a long time if m is very large. @@ -401,9 +427,14 @@ def order_mod(x, m): return result -def largest_factor_relatively_prime(a, b): +def largest_factor_relatively_prime(a, b): # pragma: no cover """Return the largest factor of a relatively prime to b. """ + # deprecated in 0.14 + warnings.warn("Function is unused by library code. If you use this code, " + "please open an issue in " + "https://github.com/warner/python-ecdsa", + DeprecationWarning) while 1: d = gcd(a, b) @@ -418,10 +449,15 @@ def largest_factor_relatively_prime(a, b): return a -def kinda_order_mod(x, m): +def kinda_order_mod(x, m): # pragma: no cover """Return the order of x in the multiplicative group mod m', where m' is the largest factor of m relatively prime to x. """ + # deprecated in 0.14 + warnings.warn("Function is unused by library code. If you use this code, " + "please open an issue in " + "https://github.com/warner/python-ecdsa", + DeprecationWarning) return order_mod(x, largest_factor_relatively_prime(m, x)) From 1fd82ca1d5104344bf224ff73768aa03ccb1c4f3 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 16:58:07 +0200 Subject: [PATCH 05/23] add implementation of EC using Jacobi representation --- src/ecdsa/ellipticcurve.py | 125 ++++++++++++++++++++++++++++++++++++- src/ecdsa/test_jacobi.py | 59 +++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/ecdsa/test_jacobi.py diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index f1751418..eaa624c2 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -57,11 +57,130 @@ 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**2) - (x**3 + 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) + +class PointJacobi(object): + """Point on an elliptic curve. Uses Jacobi representation""" + 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 self.__y == 0 and other == INFINITY: + return True + if isinstance(other, Point): + return self.to_weierstrass() == 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 x coordinate in Weierstrass form""" + p = self.__curve.p() + z = numbertheory.inverse_mod(self.__z, p) + return self.__x * z**2 % p + + def y(self): + """Return y coordinate in Weierstrass form""" + p = self.__curve.p() + z = numbertheory.inverse_mod(self.__z, p) + return self.__y * z**3 % p + + def to_weierstrass(self): + """Return point in Weierstrass 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_weierstrass(point): + """Create from a Weierstrass form point""" + return PointJacobi(point.curve(), point.x(), point.y(), 1, + point.order()) + + def double(self): + """Add a point to itself.""" + if not self.__y: + return PointJacobi(self.__curve, 0, 0, 0) + + p = self.__curve.p() + a = self.__curve.a() + x, y, z = self.__x, self.__y, self.__z + ysq = y**2 % p + s = 4 * x * ysq % p + m = (3 * x**2 + a * pow(z, 4, p)) % p + nx = (m**2 - 2 * s) % p + ny = (m * (s - nx) - 8 * ysq**2) % p + nz = 2 * y * z % p + return PointJacobi(self.__curve, nx, ny, nz, self.__order) + + 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_weierstrass(other) + if self.__curve != other.__curve: + raise ValueError("The other point is on different curve") + + p = self.__curve.p() + s_x, s_y, s_z = self.__x, self.__y, self.__z + o_x, o_y, o_z = other.__x, other.__y, other.__z + o_z2 = o_z**2 % p + s_z2 = s_z**2 % p + u1 = s_x * o_z2 % p + u2 = o_x * s_z2 % p + s1 = s_y * o_z2 * o_z % p + s2 = o_y * s_z2 * s_z % p + + if u1 == u2: + if s1 != s2: + return PointJacobi(self.__curve, 0, 0, 1) + return self.double() + + h = u2 - u1 + r = s2 - s1 + h2 = h**2 % p + h3 = h * h2 % p + u1h2 = u1 * h2 % p + nx = (r ** 2 - h3 - 2 * u1h2) % p + ny = (r * (u1h2 - nx) - s1 * h3) % p + nz = h * s_z * o_z % p + return PointJacobi(self.__curve, nx, ny, nz) + + def __rmul__(self, other): + """Multiply point by an integer.""" + return self * other + + def __mul__(self, other): + """Multiply point by an integer.""" + if self.__y == 0 or other == 0: + return PointJacobi(self.__curve, 0, 0, 1) + if other == 1: + return self + if self.__order: + other = other % self.__order + + if other % 2: + return (self * (other // 2)).double() + self + return (self * (other // 2)).double() + + 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.""" @@ -79,6 +198,8 @@ def __init__(self, curve, x, y, order=None): def __eq__(self, other): """Return True if the points are identical, False otherwise.""" + if isinstance(other, PointJacobi): + return self == other.to_weierstrass() if self.__curve == other.__curve \ and self.__x == other.__x \ and self.__y == other.__y: @@ -95,6 +216,8 @@ def __add__(self, other): return self if self == INFINITY: return other + if not isinstance(other, Point): + return NotImplemented assert self.__curve == other.__curve if self.__x == other.__x: if (self.__y + other.__y) % self.__curve.p() == 0: diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py new file mode 100644 index 00000000..a54ccbc4 --- /dev/null +++ b/src/ecdsa/test_jacobi.py @@ -0,0 +1,59 @@ + +try: + import unittest2 as unittest +except ImportError: + import unittest + +from .ellipticcurve import Point, PointJacobi +from .ecdsa import generator_256, curve_256 + +class TestJacobi(unittest.TestCase): + def test_conversion(self): + pj = PointJacobi.from_weierstrass(generator_256) + pw = pj.to_weierstrass() + + self.assertEqual(generator_256, pw) + + def test_single_double(self): + pj = PointJacobi.from_weierstrass(generator_256) + pw = generator_256.double() + + pj = pj.double() + + self.assertEqual(pj.x(), pw.x()) + self.assertEqual(pj.y(), pj.y()) + + def test_multiply_by_one(self): + pj = PointJacobi.from_weierstrass(generator_256) + pw = generator_256 * 1 + + pj = pj * 1 + + self.assertEqual(pj.x(), pw.x()) + self.assertEqual(pj.y(), pj.y()) + + def test_multiply_by_two(self): + pj = PointJacobi.from_weierstrass(generator_256) + pw = generator_256 * 2 + + pj = pj * 2 + + self.assertEqual(pj.x(), pw.x()) + self.assertEqual(pj.y(), pj.y()) + + def test_compare_double_with_multiply(self): + pj = PointJacobi.from_weierstrass(generator_256) + dbl = pj.double() + mlpl = pj * 2 + + self.assertEqual(dbl, mlpl) + + def test_simple_multiplications(self): + for i in range(1, 256): + pj = PointJacobi.from_weierstrass(generator_256) + pw = generator_256 * i + + pj = pj * i + + self.assertEqual((pj.x(), pj.y()), (pw.x(), pw.y())) + From 7e8476f08145bfe1c588f1ad96214a6ab2f82205 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 19:22:58 +0200 Subject: [PATCH 06/23] recursion-less scalar multiplication --- src/ecdsa/ellipticcurve.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index eaa624c2..a703c191 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -173,12 +173,26 @@ def __mul__(self, other): return PointJacobi(self.__curve, 0, 0, 1) if other == 1: return self - if self.__order: - other = other % self.__order - - if other % 2: - return (self * (other // 2)).double() + self - return (self * (other // 2)).double() + # 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() + if e & (i // 2) != 0: + result = result + self + i = i // 2 + return result class Point(object): From fefe893408d74faa59fb4167fca2ccabe10686c5 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 19:23:21 +0200 Subject: [PATCH 07/23] faster point addition --- src/ecdsa/ellipticcurve.py | 104 +++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 23 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index a703c191..9fa5216d 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -127,6 +127,55 @@ def double(self): nz = 2 * y * z % p return PointJacobi(self.__curve, nx, ny, nz, self.__order) + def _add_with_z_1(self, X1, Y1, X2, Y2): + """add points when both Z1 and Z2 equal 1""" + p = self.__curve.p() + H = X2-X1 + HH = H**2 + I = 4*HH % p + J = H*I + r = 2*(Y2-Y1) + V = X1*I + X3 = (r**2-J-2*V)%p + Y3 = (r*(V-X3)-2*Y1*J)%p + Z3 = 2*H%p + if Y3 == 0 or Z3 == 0: + 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""" + p = self.__curve.p() + A = (X2-X1)**2%p + B = X1*A%p + C = X2*A + D = (Y2-Y1)**2 %p + X3 = (D-B-C)%p + Y3 = ((Y2-Y1)*(B-X3)-Y1*(C-B)) %p + Z3 = Z1*(X2-X1) %p + if Y3 == 0 or Z3 == 0: + 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""" + p = self.__curve.p() + Z1Z1 = Z1**2 % p + U2 = X2*Z1Z1 + S2 = Y2*Z1*Z1Z1 + H = U2-X1 + HH = H**2%p + I = 4*HH%p + J = H*I + r = 2*(S2-Y1) + V = X1*I + X3 = (r**2-J-2*V)%p + Y3 = (r*(V-X3)-2*Y1*J)%p + Z3 = ((Z1+H)**2-Z1Z1-HH)%p + if Y3 == 0 or Z3 == 0: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + def __add__(self, other): """Add two points on elliptic curve.""" if self == INFINITY: @@ -139,29 +188,38 @@ def __add__(self, other): raise ValueError("The other point is on different curve") p = self.__curve.p() - s_x, s_y, s_z = self.__x, self.__y, self.__z - o_x, o_y, o_z = other.__x, other.__y, other.__z - o_z2 = o_z**2 % p - s_z2 = s_z**2 % p - u1 = s_x * o_z2 % p - u2 = o_x * s_z2 % p - s1 = s_y * o_z2 * o_z % p - s2 = o_y * s_z2 * s_z % p - - if u1 == u2: - if s1 != s2: - return PointJacobi(self.__curve, 0, 0, 1) - return self.double() - - h = u2 - u1 - r = s2 - s1 - h2 = h**2 % p - h3 = h * h2 % p - u1h2 = u1 * h2 % p - nx = (r ** 2 - h3 - 2 * u1h2) % p - ny = (r * (u1h2 - nx) - s1 * h3) % p - nz = h * s_z * o_z % p - return PointJacobi(self.__curve, nx, ny, nz) + 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 or Z2 == 1: + if Z1 == 1: + return self._add_with_z2_1(X2, Y2, Z2, X1, Y1) + 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**2 % p + Z2Z2 = Z2**2 % p + U1 = X1*Z2Z2 % p + U2 = X2*Z1Z1 % p + S1 = Y1*Z2*Z2Z2 %p + S2 = Y2*Z1*Z1Z1 % p + H = U2-U1 + I = (2*H)**2 % p + J = H*I % p + r = 2*(S2-S1) % p + V = U1*I + X3 = (r**2-J-2*V) % p + Y3 = (r*(V-X3)-2*S1*J) % p + Z3 = ((Z1+Z2)**2-Z1Z1-Z2Z2)*H % p + + if Y3 == 0 or Z3 == 0: + return INFINITY + + return PointJacobi(self.__curve, X3, Y3, Z3) def __rmul__(self, other): """Multiply point by an integer.""" From 412cb404d7bb2a9f4471dc981c1d7c8e20bb0fed Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 19:53:42 +0200 Subject: [PATCH 08/23] precompute for generators --- src/ecdsa/curves.py | 13 +++++++------ src/ecdsa/ellipticcurve.py | 36 +++++++++++++++++++++++++++++++++--- src/ecdsa/test_jacobi.py | 11 +++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/ecdsa/curves.py b/src/ecdsa/curves.py index 5ff53ad9..3bfc8a68 100644 --- a/src/ecdsa/curves.py +++ b/src/ecdsa/curves.py @@ -1,6 +1,7 @@ from __future__ import division from . import der, ecdsa +from .ellipticcurve import PointJacobi class UnknownCurveError(Exception): @@ -26,22 +27,22 @@ def __init__(self, name, curve, generator, oid, openssl_name=None): self.encoded_oid = der.encode_oid(*oid) NIST192p = Curve("NIST192p", ecdsa.curve_192, - ecdsa.generator_192, + PointJacobi.from_weierstrass(ecdsa.generator_192, True), (1, 2, 840, 10045, 3, 1, 1), "prime192v1") NIST224p = Curve("NIST224p", ecdsa.curve_224, - ecdsa.generator_224, + PointJacobi.from_weierstrass(ecdsa.generator_224, True), (1, 3, 132, 0, 33), "secp224r1") NIST256p = Curve("NIST256p", ecdsa.curve_256, - ecdsa.generator_256, + PointJacobi.from_weierstrass(ecdsa.generator_256, True), (1, 2, 840, 10045, 3, 1, 7), "prime256v1") NIST384p = Curve("NIST384p", ecdsa.curve_384, - ecdsa.generator_384, + PointJacobi.from_weierstrass(ecdsa.generator_384, True), (1, 3, 132, 0, 34), "secp384r1") NIST521p = Curve("NIST521p", ecdsa.curve_521, - ecdsa.generator_521, + PointJacobi.from_weierstrass(ecdsa.generator_521, True), (1, 3, 132, 0, 35), "secp521r1") SECP256k1 = Curve("SECP256k1", ecdsa.curve_secp256k1, - ecdsa.generator_secp256k1, + PointJacobi.from_weierstrass(ecdsa.generator_secp256k1, True), (1, 3, 132, 0, 10), "secp256k1") curves = [NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1] diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 9fa5216d..ff6fcd7e 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -65,12 +65,24 @@ def __str__(self): class PointJacobi(object): """Point on an elliptic curve. Uses Jacobi representation""" - def __init__(self, curve, x, y, z, order=None): + def __init__(self, curve, x, y, z, order=None, generator=False): self.__curve = curve self.__x = x self.__y = y self.__z = z self.__order = order + self.__precompute={} + if generator: + assert order + i = 1 + doubler = PointJacobi(curve, x, y, z, order) + self.__precompute[i] = doubler + doubler = doubler.double().normalise() + + while i < order: + i *= 2 + self.__precompute[i] = doubler + doubler = doubler.double().normalise() def __eq__(self, other): """Compare two points with each-other.""" @@ -98,6 +110,10 @@ def y(self): z = numbertheory.inverse_mod(self.__z, p) return self.__y * z**3 % p + def normalise(self): + """Return point with z == 1.""" + return self.from_weierstrass(self.to_weierstrass()) + def to_weierstrass(self): """Return point in Weierstrass form""" p = self.__curve.p() @@ -106,10 +122,10 @@ def to_weierstrass(self): self.__y * z**3 % p, self.__order) @staticmethod - def from_weierstrass(point): + def from_weierstrass(point, generator=False): """Create from a Weierstrass form point""" return PointJacobi(point.curve(), point.x(), point.y(), 1, - point.order()) + point.order(), generator) def double(self): """Add a point to itself.""" @@ -225,12 +241,26 @@ def __rmul__(self, other): """Multiply point by an integer.""" return self * other + def _mul_precompute(self, other): + i = 1 + result = None + while i <= other: + if i & other: + if result: + result = result + self.__precompute[i] + else: + result = self.__precompute[i] + i *= 2 + return result + def __mul__(self, other): """Multiply point by an integer.""" if self.__y == 0 or other == 0: return PointJacobi(self.__curve, 0, 0, 1) if other == 1: return self + if self.__precompute: + return self._mul_precompute(other) # makes vulnerable to Minerva #if self.__order: # other = other % self.__order diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index a54ccbc4..94a483c6 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -56,4 +56,15 @@ def test_simple_multiplications(self): pj = pj * i self.assertEqual((pj.x(), pj.y()), (pw.x(), pw.y())) + self.assertEqual(pj, PointJacobi.from_weierstrass(pw)) + def test_precompute(self): + precomp = PointJacobi.from_weierstrass(generator_256, True) + pj = PointJacobi.from_weierstrass(generator_256) + + + for i in range(255): + print(i) + a = precomp * (2 << i) + b = pj * (2 << i) + self.assertEqual(precomp * (2 << i), pj * (2 << i)) From 92c7422d851e0b654a31bdc0fd3dc415a3b8f45d Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 6 Oct 2019 20:26:52 +0200 Subject: [PATCH 09/23] use sliding window for precomputation --- src/ecdsa/ellipticcurve.py | 43 +++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index ff6fcd7e..07c79cbc 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -36,6 +36,20 @@ from six import python_2_unicode_compatible from . import numbertheory +import binascii + + +def orderlen(order): + return (1+len("%x" % order))//2 # bytes + + +def number_to_string(num, order): + l = orderlen(order) + fmt_str = "%0" + str(2 * l) + "x" + string = binascii.unhexlify((fmt_str % num).encode()) + assert len(string) == l, (len(string), l) + return string + @python_2_unicode_compatible class CurveFp(object): @@ -73,16 +87,9 @@ def __init__(self, curve, x, y, z, order=None, generator=False): self.__order = order self.__precompute={} if generator: - assert order - i = 1 - doubler = PointJacobi(curve, x, y, z, order) - self.__precompute[i] = doubler - doubler = doubler.double().normalise() - - while i < order: - i *= 2 - self.__precompute[i] = doubler - doubler = doubler.double().normalise() + point = PointJacobi(curve, x, y, z, order) + for i in range(1, 256): + self.__precompute[i] = (point * i).normalise() def __eq__(self, other): """Compare two points with each-other.""" @@ -244,13 +251,15 @@ def __rmul__(self, other): def _mul_precompute(self, other): i = 1 result = None - while i <= other: - if i & other: - if result: - result = result + self.__precompute[i] - else: - result = self.__precompute[i] - i *= 2 + bit_mask = number_to_string(other, other) + bit_mask = bytearray(bit_mask) + result = self.__precompute[bit_mask[0]] + for b in bit_mask[1:]: + for _ in range(8): + result = result.double() + if b: + result = result + self.__precompute[b] + return result def __mul__(self, other): From e3d40aaa69efcf1521278007e879be9e6f7e8c64 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:09:31 +0200 Subject: [PATCH 10/23] more efficient doubling algorithm --- src/ecdsa/ellipticcurve.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 07c79cbc..2441550d 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -119,6 +119,8 @@ def y(self): def normalise(self): """Return point with z == 1.""" + if self.__z == 1: + return self return self.from_weierstrass(self.to_weierstrass()) def to_weierstrass(self): @@ -141,14 +143,24 @@ def double(self): p = self.__curve.p() a = self.__curve.a() - x, y, z = self.__x, self.__y, self.__z - ysq = y**2 % p - s = 4 * x * ysq % p - m = (3 * x**2 + a * pow(z, 4, p)) % p - nx = (m**2 - 2 * s) % p - ny = (m * (s - nx) - 8 * ysq**2) % p - nz = 2 * y * z % p - return PointJacobi(self.__curve, nx, ny, nz, self.__order) + + X1, Y1, Z1 = self.__x, self.__y, self.__z + + # after: + # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl + + XX = X1**2%p + YY = Y1**2%p + YYYY = YY**2%p + ZZ = Z1**2%p + S = 2*((X1+YY)**2-XX-YYYY)%p + M = (3*XX+a*ZZ**2)%p + T = (M**2-2*S)%p + X3 = T + Y3 = (M*(S-T)-8*YYYY)%p + Z3 = ((Y1+Z1)**2-YY-ZZ)%p + + 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""" @@ -283,12 +295,13 @@ def leftmost_bit(x): e = other i = leftmost_bit(e) + self = self.normalise() result = self while i > 1: result = result.double() - if e & (i // 2) != 0: - result = result + self i = i // 2 + if e & i != 0: + result = result + self return result From 4863faf4f49b59f65643df2728e65e03ba6d7cd1 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:09:42 +0200 Subject: [PATCH 11/23] references for algorithms --- src/ecdsa/ellipticcurve.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 2441550d..47ce1cda 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -164,6 +164,8 @@ def double(self): 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**2 @@ -180,6 +182,8 @@ def _add_with_z_1(self, X1, Y1, X2, Y2): 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 @@ -194,6 +198,8 @@ def _add_with_z_eq(self, X1, Y1, Z1, X2, Y2): 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**2 % p U2 = X2*Z1Z1 From c29d2f12ae5988154f6feb74bdce2a097d32aa46 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:12:03 +0200 Subject: [PATCH 12/23] faster inverse modulo turns out the divmod() call is very expensive --- src/ecdsa/numbertheory.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/ecdsa/numbertheory.py b/src/ecdsa/numbertheory.py index 27a9b0f1..1a34c8b2 100644 --- a/src/ecdsa/numbertheory.py +++ b/src/ecdsa/numbertheory.py @@ -207,27 +207,15 @@ def square_root_mod_prime(a, p): def inverse_mod(a, m): - """Inverse of a mod m.""" - - if a < 0 or m <= a: - a = a % m - - # From Ferguson and Schneier, roughly: - - c, d = a, m - uc, vc, ud, vd = 1, 0, 0, 1 - while c != 0: - q, c, d = divmod(d, c) + (c,) - uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc - - # At this point, d is the GCD, and ud*a+vd*m = d. - # If d == 1, this means that ud is a inverse. - - assert d == 1 - if ud > 0: - return ud - else: - return ud + m + """Inverse of a mod m.""" + 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 + return lm % m def gcd2(a, b): From 5951a940b34aea962ae08f2963e41ee87ed86eb3 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:20:03 +0200 Subject: [PATCH 13/23] 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 8c006b51..eb552245 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -39,6 +39,8 @@ def __init__(self, _error__please_use_generate=None): @classmethod def from_public_point(klass, point, curve=NIST192p, hashfunc=sha1): self = klass(_error__please_use_generate=True) + if not isinstance(point, ellipticcurve.PointJacobi): + point = ellipticcurve.PointJacobi.from_weierstrass(point) self.curve = curve self.default_hashfunc = hashfunc self.pubkey = ecdsa.Public_key(curve.generator, point) @@ -279,6 +281,7 @@ def from_secret_exponent(klass, 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.normalise() pubkey = ecdsa.Public_key(curve.generator, pubkey_point) pubkey.order = n self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, From 640e8c6b1c8d0db352ad635ad5b267190dc88ea9 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:21:13 +0200 Subject: [PATCH 14/23] 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 a7b93614..4cb7e67a 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -87,12 +87,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 0979126b643b06fb277f334cdf225c87d6d5c6dc Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 00:22:40 +0200 Subject: [PATCH 15/23] we're so fast we need wider output :) --- speed.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/speed.py b/speed.py index 3f05309b..686ebf52 100644 --- a/speed.py +++ b/speed.py @@ -31,5 +31,7 @@ def do(setup_statements, statement): import ecdsa c = getattr(ecdsa, curve) sig = ecdsa.SigningKey.generate(c).sign(six.b("msg")) - print("%9s: siglen=%3d, keygen=%.3fs, sign=%.3fs, verify=%.3fs" \ - % (curve, len(sig), keygen, sign, verf)) + print("%9s: siglen=%3d, keygen=%.4fs, %5.1f/s, " + "sign=%.4fs, %5.1f/s, verify=%.4fs, %5.1f/s" \ + % (curve, len(sig), keygen, 1.0/keygen, sign, 1.0/sign, + verf, 1.0/verf)) From 23b4e69ed2bc87d0c3932b48b5c739e6a49588af Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 7 Oct 2019 01:02:09 +0200 Subject: [PATCH 16/23] more documentation --- src/ecdsa/ellipticcurve.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 47ce1cda..dcbc05f3 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. # @@ -78,8 +79,21 @@ def __str__(self): class PointJacobi(object): - """Point on an elliptic curve. Uses Jacobi representation""" + """ + Point on an elliptic curve. Uses Jacobi representation + + In Jacobian coordinates, there are three parameters, X, Y and Z. + They correspond to Weierstrass representation points 'x' and 'y' like so: + + x = X / Z² + y = Y / Z³ + """ 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 @@ -106,19 +120,31 @@ def curve(self): return self.__curve def x(self): - """Return x coordinate in Weierstrass form""" + """ + Return x coordinate in Weierstrass form. + + This method should be used only when the 'y' coordinate is not needed. + It's computationally more efficient to use `to_weierstrass()` and then + call x() and y() on the returned instance. + """ p = self.__curve.p() z = numbertheory.inverse_mod(self.__z, p) return self.__x * z**2 % p def y(self): - """Return y coordinate in Weierstrass form""" + """ + Return y coordinate in Weierstrass form. + + This method should be used only when the 'x' coordinate is not needed. + It's computationally more efficient to use `to_weierstrass()` and then + call x() and y() on the returned instance. + """ p = self.__curve.p() z = numbertheory.inverse_mod(self.__z, p) return self.__y * z**3 % p def normalise(self): - """Return point with z == 1.""" + """Return point scaled so that z == 1.""" if self.__z == 1: return self return self.from_weierstrass(self.to_weierstrass()) From 4d9502b631698a621cdd9f922cab420d3a714757 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 9 Oct 2019 22:55:24 +0200 Subject: [PATCH 17/23] unify INFINITY --- src/ecdsa/ellipticcurve.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index dcbc05f3..cd08349e 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -107,7 +107,7 @@ def __init__(self, curve, x, y, z, order=None, generator=False): def __eq__(self, other): """Compare two points with each-other.""" - if self.__y == 0 and other == INFINITY: + if (not self.__y or not self.__z) and other is INFINITY: return True if isinstance(other, Point): return self.to_weierstrass() == other @@ -165,7 +165,7 @@ def from_weierstrass(point, generator=False): def double(self): """Add a point to itself.""" if not self.__y: - return PointJacobi(self.__curve, 0, 0, 0) + return INFINITY p = self.__curve.p() a = self.__curve.a() @@ -186,6 +186,9 @@ def double(self): Y3 = (M*(S-T)-8*YYYY)%p Z3 = ((Y1+Z1)**2-YY-ZZ)%p + if not Y3: + return INFINITY + return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) def _add_with_z_1(self, X1, Y1, X2, Y2): @@ -202,7 +205,7 @@ def _add_with_z_1(self, X1, Y1, X2, Y2): X3 = (r**2-J-2*V)%p Y3 = (r*(V-X3)-2*Y1*J)%p Z3 = 2*H%p - if Y3 == 0 or Z3 == 0: + if not Y3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -218,7 +221,7 @@ def _add_with_z_eq(self, X1, Y1, Z1, X2, Y2): X3 = (D-B-C)%p Y3 = ((Y2-Y1)*(B-X3)-Y1*(C-B)) %p Z3 = Z1*(X2-X1) %p - if Y3 == 0 or Z3 == 0: + if not Y3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -239,15 +242,15 @@ def _add_with_z2_1(self, X1, Y1, Z1, X2, Y2): X3 = (r**2-J-2*V)%p Y3 = (r*(V-X3)-2*Y1*J)%p Z3 = ((Z1+H)**2-Z1Z1-HH)%p - if Y3 == 0 or Z3 == 0: + if not Y3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) def __add__(self, other): """Add two points on elliptic curve.""" - if self == INFINITY: + if self.__y == 0: return other - if other == INFINITY: + if other is INFINITY: return self if isinstance(other, Point): other = PointJacobi.from_weierstrass(other) @@ -283,7 +286,7 @@ def __add__(self, other): Y3 = (r*(V-X3)-2*S1*J) % p Z3 = ((Z1+Z2)**2-Z1Z1-Z2Z2)*H % p - if Y3 == 0 or Z3 == 0: + if not Y3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3) @@ -309,7 +312,7 @@ def _mul_precompute(self, other): def __mul__(self, other): """Multiply point by an integer.""" if self.__y == 0 or other == 0: - return PointJacobi(self.__curve, 0, 0, 1) + return INFINITY if other == 1: return self if self.__precompute: From 3d93ebb0f80d2621d4ac07e3c3ceedf825108fd6 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 9 Oct 2019 22:59:17 +0200 Subject: [PATCH 18/23] speed up scaling, name it in standard way in curve arithmetic it's called scaling, not normalisation also change one cubing into multiplication --- src/ecdsa/ellipticcurve.py | 29 ++++++++++++++++++++--------- src/ecdsa/keys.py | 2 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index cd08349e..9915b8b9 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -103,7 +103,7 @@ def __init__(self, curve, x, y, z, order=None, generator=False): if generator: point = PointJacobi(curve, x, y, z, order) for i in range(1, 256): - self.__precompute[i] = (point * i).normalise() + self.__precompute[i] = (point * i).scale() def __eq__(self, other): """Compare two points with each-other.""" @@ -143,18 +143,29 @@ def y(self): z = numbertheory.inverse_mod(self.__z, p) return self.__y * z**3 % p - def normalise(self): - """Return point scaled so that z == 1.""" + def scale(self): + """ + Return point scaled so that z == 1. + + Modifies point in place, returns self. + """ if self.__z == 1: return self - return self.from_weierstrass(self.to_weierstrass()) + p = self.__curve.p() + z_inv = numbertheory.inverse_mod(self.__z, p) + zz_inv = z_inv ** 2 % p + self.__x = self.__x * zz_inv % p + self.__y = self.__y * zz_inv * z_inv % p + self.__z = 1 + return self def to_weierstrass(self): """Return point in Weierstrass 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_weierstrass(point, generator=False): @@ -330,7 +341,7 @@ def leftmost_bit(x): e = other i = leftmost_bit(e) - self = self.normalise() + self = self.scale() result = self while i > 1: result = result.double() diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index eb552245..9735891e 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -281,7 +281,7 @@ def from_secret_exponent(klass, 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.normalise() + pubkey_point = pubkey_point.scale() pubkey = ecdsa.Public_key(curve.generator, pubkey_point) pubkey.order = n self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, From 39bca7312deae536b22b144df49641bc40bacf3c Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Oct 2019 00:03:20 +0200 Subject: [PATCH 19/23] 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 | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 9915b8b9..4cc3ea19 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -110,8 +110,21 @@ def __eq__(self, other): if (not self.__y or not self.__z) and other is INFINITY: return True if isinstance(other, Point): - return self.to_weierstrass() == 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 + x1, y1, z1 = self.__x, self.__y, self.__z + p = self.__curve.p() + + zz1 = z1 ** 2 % p + zz2 = z2 ** 2 % p + + # compare the fractions by bringing them to 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 From 8b5401e8ae7414b6c67c53e85db544d59093e557 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Oct 2019 00:04:21 +0200 Subject: [PATCH 20/23] don't re-scale whole point when just one coordinate is needed --- src/ecdsa/ellipticcurve.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 4cc3ea19..206cfa39 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -140,6 +140,8 @@ def x(self): It's computationally more efficient to use `to_weierstrass()` 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 @@ -152,6 +154,8 @@ def y(self): It's computationally more efficient to use `to_weierstrass()` 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 From b728b9c5a47cfeaff312fba09a07a0c16a3dc676 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Oct 2019 00:04:37 +0200 Subject: [PATCH 21/23] microoptimisation for double() --- src/ecdsa/ellipticcurve.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 206cfa39..78c38508 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -210,14 +210,14 @@ def double(self): S = 2*((X1+YY)**2-XX-YYYY)%p M = (3*XX+a*ZZ**2)%p T = (M**2-2*S)%p - X3 = T + #X3 = T Y3 = (M*(S-T)-8*YYYY)%p Z3 = ((Y1+Z1)**2-YY-ZZ)%p if not Y3: return INFINITY - return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) + 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""" From 2a7e3b9cdd18a898ad46dfe1e3b78f6cecb0f5ea Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Oct 2019 00:05:47 +0200 Subject: [PATCH 22/23] don't create Public_key object twice since Public_key verifies correctness of point, it's expensive to create, do it just once by reusing one created for VerifyingKey --- src/ecdsa/keys.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 9735891e..2bfe8062 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -282,10 +282,9 @@ def from_secret_exponent(klass, secexp, curve=NIST192p, hashfunc=sha1): .format(n)) pubkey_point = curve.generator * secexp pubkey_point = pubkey_point.scale() - pubkey = ecdsa.Public_key(curve.generator, pubkey_point) - pubkey.order = n self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, hashfunc) + pubkey = self.verifying_key.pubkey self.privkey = ecdsa.Private_key(pubkey, secexp) self.privkey.order = n return self From 0699935f535df46beba21042303253b66016c480 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Oct 2019 09:02:01 +0200 Subject: [PATCH 23/23] Revert "use sliding window for precomputation" This reverts commit 92c7422d851e0b654a31bdc0fd3dc415a3b8f45d. --- src/ecdsa/ellipticcurve.py | 43 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 78c38508..730b53e4 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -37,20 +37,6 @@ from six import python_2_unicode_compatible from . import numbertheory -import binascii - - -def orderlen(order): - return (1+len("%x" % order))//2 # bytes - - -def number_to_string(num, order): - l = orderlen(order) - fmt_str = "%0" + str(2 * l) + "x" - string = binascii.unhexlify((fmt_str % num).encode()) - assert len(string) == l, (len(string), l) - return string - @python_2_unicode_compatible class CurveFp(object): @@ -101,9 +87,16 @@ def __init__(self, curve, x, y, z, order=None, generator=False): self.__order = order self.__precompute={} if generator: - point = PointJacobi(curve, x, y, z, order) - for i in range(1, 256): - self.__precompute[i] = (point * i).scale() + assert order + i = 1 + doubler = PointJacobi(curve, x, y, z, order) + self.__precompute[i] = doubler + doubler = doubler.double().scale() + + while i < order: + i *= 2 + self.__precompute[i] = doubler + doubler = doubler.double().scale() def __eq__(self, other): """Compare two points with each-other.""" @@ -326,15 +319,13 @@ def __rmul__(self, other): def _mul_precompute(self, other): i = 1 result = None - bit_mask = number_to_string(other, other) - bit_mask = bytearray(bit_mask) - result = self.__precompute[bit_mask[0]] - for b in bit_mask[1:]: - for _ in range(8): - result = result.double() - if b: - result = result + self.__precompute[b] - + while i <= other: + if i & other: + if result: + result = result + self.__precompute[i] + else: + result = self.__precompute[i] + i *= 2 return result def __mul__(self, other):