diff --git a/speed.py b/speed.py index 3f05309b..a74fcd7f 100644 --- a/speed.py +++ b/speed.py @@ -20,16 +20,19 @@ 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")) - 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)) diff --git a/src/ecdsa/curves.py b/src/ecdsa/curves.py index b61f4b45..2d6877f1 100644 --- a/src/ecdsa/curves.py +++ b/src/ecdsa/curves.py @@ -2,6 +2,7 @@ from . import der, ecdsa from .util import orderlen +from .ellipticcurve import PointJacobi # orderlen was defined in this module previously, so keep it in __all__, @@ -30,22 +31,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/ecdsa.py b/src/ecdsa/ecdsa.py index 213c67d8..3cfc5d18 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. """ @@ -88,36 +92,46 @@ 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) 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 verifies(self, hash, signature): """Verify that signature is a valid signature of hash. diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index e78314cc..3ea87b01 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. # @@ -57,11 +58,307 @@ 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 + + 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 + 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().scale() + + while i < order: + i *= 2 + self.__precompute[i] = doubler + doubler = doubler.double().scale() + + 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): + 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 + + def curve(self): + return self.__curve + + def x(self): + """ + 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. + """ + 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 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. + """ + 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 scale(self): + """ + Return point scaled so that z == 1. + + Modifies point in place, returns self. + """ + if self.__z == 1: + return self + 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""" + 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): + """Create from a Weierstrass form point""" + return PointJacobi(point.curve(), point.x(), point.y(), 1, + point.order(), generator) + + 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 = 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 + + if not Y3: + return INFINITY + + 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**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 not Y3: + 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 + X3 = (D-B-C)%p + Y3 = ((Y2-Y1)*(B-X3)-Y1*(C-B)) %p + Z3 = Z1*(X2-X1) %p + if not Y3: + 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**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 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.__y == 0: + return other + if other is 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() + 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 not Y3: + return INFINITY + + return PointJacobi(self.__curve, X3, Y3, Z3) + + 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 INFINITY + if other == 1: + return self + if self.__precompute: + return self._mul_precompute(other) + # 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) + self = self.scale() + 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.""" @@ -79,6 +376,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: @@ -98,6 +397,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/keys.py b/src/ecdsa/keys.py index 8c006b51..d4b76e8a 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -1,5 +1,65 @@ -import binascii +""" +Primary classes for performing signing and verification operations. + +.. glossary:: + + raw encoding + Conversion of public, private keys and signatures (which in + mathematical sense are integers or pairs of integers) to strings of + bytes that does not use any special tags or encoding rules. + For any given curve, all keys of the same type or signatures will be + encoded to byte strings of the same length. In more formal sense, + the integers are encoded as big-endian, constant length byte strings, + where the string length is determined by the curve order (e.g. + for NIST256p the order is 256 bits long, so the private key will be 32 + bytes long while public key will be 64 bytes long). The encoding of a + single integer is zero-padded on the left if the numerical value is + low. In case of public keys and signatures, which are comprised of two + integers, the integers are simply concatenated. + + uncompressed + The most common formatting specified in PKIX standards. Specified in + X9.62 and SEC1 standards. The only difference between it and + :term:`raw encoding` is the prepending of a 0x04 byte. Thus an + uncompressed NIST256p public key encoding will be 65 bytes long. + + compressed + The public point representation that uses half of bytes of the + uncompressed encoding (rounded up). It uses the first byte of the + encoding to specify the sign of the y coordinate and encodes the + x coordinate as-is. The first byte of the encoding is equal to + 0x02 or 0x03. Compressed encoding of NIST256p public key will be 33 + bytes long. + + hybrid + A combination of uncompressed and compressed encodings. Both x and y + coordinate is stored just as in :term:`compressed` encoding, but the + first byte reflects the sign of the y coordinate. The first byte of the + encoding will be equal to 0x06 or 0x7. Hybrid encoding of NIST256p + public key will be 65 bytes long. + PEM + The acronym stands for Privacy Enhanced Email, but currently it is used + primarily as the way to encode :term:`DER` objects into text that can + be either easily copy-pasted or transferred over email. + It uses headers like `-----BEGIN -----` and footers + like `-----END -----` to separate multiple + types of objects in the same file or the object from the surrounding + comments. The actual object stored is base64 encoded. + + DER + Distinguished Encoding Rules, the way to encode :term:`ASN.1` objects + deterministically and uniquely into byte strings. + + ASN.1 + Abstract Syntax Notation 1 is a standard description language for + specifying serialisation and deserialisation of data structures in a + portable and cross-platform way. +""" + +import binascii +from hashlib import sha1 +from six import PY3, b from . import ecdsa from . import der from . import rfc6979 @@ -10,8 +70,6 @@ from .util import string_to_number, number_to_string, randrange from .util import sigencode_string, sigdecode_string from .util import oid_ecPublicKey, encoded_oid_ecPublicKey, MalformedSignature -from six import PY3, b -from hashlib import sha1 __all__ = ["BadSignatureError", "BadDigestError", "VerifyingKey", "SigningKey", @@ -19,34 +77,104 @@ class BadSignatureError(Exception): + """ + Raised when verification of signature failed. + + Will be raised irrespective of reason of the failure: + + * the calculated or provided hash does not match the signature + * the signature does not match the curve/public key + * the encoding of the signature is malformed + * the size of the signature does not match the curve of the VerifyingKey + """ + pass class BadDigestError(Exception): + """Raised in case the selected hash is too large for the curve.""" + pass class MalformedPointError(AssertionError): + """Raised in case the encoding of private or public key is malformed.""" + pass -class VerifyingKey: +class VerifyingKey(object): + """ + Class for handling keys that can verify signatures (public keys). + + :ivar curve: The Curve over which all the cryptographic operations will + take place + :ivar default_hashfunc: the function that will be used for hashing the + data. Should implement the same API as hashlib.sha1 + :ivar pubkey: the ecdsa.ecdsa.Public_key holding the actual public key + """ + def __init__(self, _error__please_use_generate=None): + """Unsupported, please use one of the classmethods to initialise.""" if not _error__please_use_generate: raise TypeError("Please use VerifyingKey.generate() to " "construct me") + self.curve = None + self.default_hashfunc = None + self.pubkey = None @classmethod - def from_public_point(klass, point, curve=NIST192p, hashfunc=sha1): - self = klass(_error__please_use_generate=True) + def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1, + validate_point=True): + """ + Initialise the object from a Point object. + + This is a low-level method, generally you will not want to use it. + + :param point: The point to wrap around, the actual public key + :type point: ecdsa.ellipticcurve.Point + :param curve: The curve on which the point needs to reside, defaults + to NIST192p + :type curve: ecdsa.curves.Curve + :param hashfunc: The default hash function that will be used for + 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 + """ + self = cls(_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) + 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 + def precompute(self): + self.pubkey.point = ellipticcurve.PointJacobi.from_weierstrass( + self.pubkey.point, True) + @staticmethod - def _from_raw_encoding(string, curve, validate_point): + def _from_raw_encoding(string, curve): + """ + Decode public point from raw encoding. + + raw encoding is the same as the uncompressed encoding, but without + the 0x04 byte at the beginning. + """ order = curve.order # real assert, from_string() should not call us with different length assert len(string) == curve.verifying_key_length @@ -58,13 +186,12 @@ 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") @@ -82,17 +209,16 @@ 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): + """Decode public point from hybrid encoding.""" # 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 - 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 \ @@ -103,36 +229,107 @@ def _from_hybrid(cls, string, curve, validate_point): return point @classmethod - def from_string(klass, string, curve=NIST192p, hashfunc=sha1, + def from_string(cls, string, curve=NIST192p, hashfunc=sha1, validate_point=True): + """ + Initialise the object from byte encoding of public key. + + The method does accept and automatically detect the type of point + encoding used. It supports the :term:`raw encoding`, + :term:`uncompressed`, :term:`compressed` and :term:`hybrid` encodings. + + Note, while the method is named "from_string" it's a misnomer from + Python 2 days when there were no binary strings. In Python 3 the + input needs to be a bytes-like object. + + :param string: :term:`raw encoding` of the public key + :type string: bytes-like object + :param curve: the curve on which the public key is expected to lie + :type curve: ecdsa.curves.Curve + :param hashfunc: The default hash function that will be used for + verification, needs to implement the same interface as hashlib.sha1 + :type hashfunc: callable + :param validate_point: whether to verify that the point lies on the + 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 + """ sig_len = len(string) if sig_len == curve.verifying_key_length: - point = klass._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 = klass._from_hybrid(string, curve, validate_point) + point = cls._from_hybrid(string, curve, validate_point) elif string[:1] == b('\x04'): - point = klass._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 = klass._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 klass.from_public_point(point, curve, hashfunc) + return cls.from_public_point(point, curve, hashfunc, + validate_point) @classmethod - def from_pem(klass, string): - return klass.from_der(der.unpem(string)) + def from_pem(cls, string): + """ + Initialise from public key stored in :term:`PEM` format. + + The PEM header of the key should be `BEGIN PUBLIC KEY`. + + See the `from_der()` method for details of the format supported. + + Note: only a single PEM object encoding is supported in provided + string. + + :param string: text with PEM-encoded public ECDSA key + :type string: str + + :return: Initialised VerifyingKey object + :rtype: VerifyingKey + """ + return cls.from_der(der.unpem(string)) @classmethod - def from_der(klass, string): + def from_der(cls, string): + """ + Initialise the key stored in :term:`DER` format. + + The expected format of the key is the SubjectPublicKeyInfo structure + from RFC5912 (for RSA keys, it's known as the PKCS#1 format):: + + SubjectPublicKeyInfo {PUBLIC-KEY: IOSet} ::= SEQUENCE { + algorithm AlgorithmIdentifier {PUBLIC-KEY, {IOSet}}, + subjectPublicKey BIT STRING + } + + Note: only public EC keys are supported by this method. The + SubjectPublicKeyInfo.algorithm.algorithm field must specify + id-ecPublicKey (see RFC3279). + + Only the named curve encoding is supported, thus the + SubjectPublicKeyInfo.algorithm.parameters field needs to be an + object identifier. A sequence in that field indicates an explicit + parameter curve encoding, this format is not supported. A NULL object + in that field indicates an "implicitlyCA" encoding, where the curve + parameters come from CA certificate, those, again, are not supported. + + :param string: binary string with the DER encoding of public ECDSA key + :type string: bytes-like object + + :return: Initialised VerifyingKey object + :rtype: VerifyingKey + """ # [[oid_ecPublicKey,oid_curve], point_str_bitstring] s1, empty = der.remove_sequence(string) if empty != b(""): @@ -153,29 +350,79 @@ def from_der(klass, string): if empty != b(""): raise der.UnexpectedDER("trailing junk after pubkey pointstring: %s" % binascii.hexlify(empty)) - # the point encoding is padded with a zero byte - # raw encoding of point is invalid in DER files - if not point_str.startswith(b("\x00")) or \ - len(point_str[1:]) == curve.verifying_key_length: + # the bitsting encoding is padded with a zero byte + # to signify zero bits to truncate in last byte + # since the value of it is a X9.62 encoding of a public key, it will + # always have a zero bit truncation + if not point_str.startswith(b("\x00")): raise der.UnexpectedDER("Malformed encoding of public point") - return klass.from_string(point_str[1:], curve) + return cls.from_string(point_str[1:], curve) @classmethod def from_public_key_recovery(cls, signature, data, curve, hashfunc=sha1, - sigdecode=sigdecode_string): - # Given a signature and corresponding message this function - # returns a list of verifying keys for this signature and message - + sigdecode=sigdecode_string): + """ + Return keys that can be used as verifiers of the provided signature. + + Tries to recover the public key that can be used to verify the + signature, usually returns two keys like that. + + :param signature: the byte string with the encoded signature + :type signature: bytes-like object + :param data: the data to be hashed for signature verification + :type data: bytes-like object + :param curve: the curve over which the signature was performed + :type curve: ecdsa.curves.Curve + :param hashfunc: The default hash function that will be used for + verification, needs to implement the same interface as hashlib.sha1 + :type hashfunc: callable + :param sigdecode: Callable to define the way the signature needs to + be decoded to an object, needs to handle `signature` as the + first parameter, the curve order (an int) as the second and return + a tuple with two integers, "r" as the first one and "s" as the + second one. See `ecdsa.util.sigdecode_string` and + `ecdsa.util.sigdecode_der` for examples. + :type sigdecode: callable + + :return: Initialised VerifyingKey objects + :rtype: list of VerifyingKey + """ digest = hashfunc(data).digest() return cls.from_public_key_recovery_with_digest( signature, digest, curve, hashfunc=hashfunc, sigdecode=sigdecode) @classmethod - def from_public_key_recovery_with_digest(klass, signature, digest, curve, hashfunc=sha1, sigdecode=sigdecode_string): - # Given a signature and corresponding digest this function - # returns a list of verifying keys for this signature and message - + def from_public_key_recovery_with_digest( + cls, signature, digest, curve, + hashfunc=sha1, sigdecode=sigdecode_string): + """ + Return keys that can be used as verifiers of the provided signature. + + Tries to recover the public key that can be used to verify the + signature, usually returns two keys like that. + + :param signature: the byte string with the encoded signature + :type signature: bytes-like object + :param digest: the hash value of the message signed by the signature + :type digest: bytes-like object + :param curve: the curve over which the signature was performed + :type curve: ecdsa.curves.Curve + :param hashfunc: The default hash function that will be used for + verification, needs to implement the same interface as hashlib.sha1 + :type hashfunc: callable + :param sigdecode: Callable to define the way the signature needs to + be decoded to an object, needs to handle `signature` as the + first parameter, the curve order (an int) as the second and return + a tuple with two integers, "r" as the first one and "s" as the + second one. See `ecdsa.util.sigdecode_string` and + `ecdsa.util.sigdecode_der` for examples. + :type sigdecode: callable + + + :return: Initialised VerifyingKey object + :rtype: VerifyingKey + """ generator = curve.generator r, s = sigdecode(signature, generator.order()) sig = ecdsa.Signature(r, s) @@ -184,16 +431,19 @@ def from_public_key_recovery_with_digest(klass, signature, digest, curve, hashfu pks = sig.recover_public_keys(digest_as_number, generator) # Transforms the ecdsa.Public_key object into a VerifyingKey - verifying_keys = [klass.from_public_point(pk.point, curve, hashfunc) for pk in pks] + verifying_keys = [cls.from_public_point(pk.point, curve, hashfunc) + for pk in pks] return verifying_keys def _raw_encode(self): + """Convert the public key to the :term:`raw encoding`.""" order = self.pubkey.order x_str = number_to_string(self.pubkey.point.x(), order) y_str = number_to_string(self.pubkey.point.y(), order) return x_str + y_str def _compressed_encode(self): + """Encode the public point into the compressed form.""" order = self.pubkey.order x_str = number_to_string(self.pubkey.point.x(), order) if self.pubkey.point.y() & 1: @@ -202,6 +452,7 @@ def _compressed_encode(self): return b('\x02') + x_str def _hybrid_encode(self): + """Encode the public point into the hybrid form.""" raw_enc = self._raw_encode() if self.pubkey.point.y() & 1: return b('\x07') + raw_enc @@ -209,9 +460,24 @@ def _hybrid_encode(self): return b('\x06') + raw_enc def to_string(self, encoding="raw"): - # VerifyingKey.from_string(vk.to_string()) == vk as long as the - # curves are the same: the curve itself is not included in the - # serialized form + """ + Convert the public key to a byte string. + + The method by default uses the :term:`raw encoding` (specified + by `encoding="raw"`. It can also output keys in :term:`uncompressed`, + :term:`compressed` and :term:`hybrid` formats. + + Remember that the curve identification is not part of the encoding + so to decode the point using VerifyingKey.from_string(), curve + needs to be specified. + + Note: while the method is called "to_string", it's a misnomer from + Python 2 days when character strings and byte strings shared type. + On Python 3 the returned type will be `bytes`. + + :return: raw encoding of the public key (public point) on the curve + :rtype: bytes + """ assert encoding in ("raw", "uncompressed", "compressed", "hybrid") if encoding == "raw": return self._raw_encode() @@ -222,21 +488,112 @@ def to_string(self, encoding="raw"): else: return self._compressed_encode() - def to_pem(self): - return der.topem(self.to_der(), "PUBLIC KEY") + def to_pem(self, point_encoding="uncompressed"): + """ + Convert the public key to the :term:`PEM` format. + + The PEM header of the key will be `BEGIN PUBLIC KEY`. + + The format of the key is described in the `from_der()` method. + This method supports only "named curve" encoding of keys. + + :param str point_encoding: specification of the encoding format + of public keys. "uncompressed" is most portable, "compressed" is + smallest, "hybrid" is uncommon and unsupported by most + implementations, is as big as "uncompressed". "raw" will create + malformed files. + + :return: portable encoding of the public key + :rtype: str + """ + return der.topem(self.to_der(point_encoding), "PUBLIC KEY") def to_der(self, point_encoding="uncompressed"): + """ + Convert the public key to the :term:`DER` format. + + The format of the key is described in the `from_der()` method. + This method supports only "named curve" encoding of keys. + + :param str point_encoding: specification of the encoding format + of public keys. "uncompressed" is most portable, "compressed" is + smallest, "hybrid" is uncommon and unsupported by most + implementations, is as big as "uncompressed". "raw" will create + malformed files. + + :return: DER encoding of the public key + :rtype: bytes + """ point_str = b("\x00") + self.to_string(point_encoding) return der.encode_sequence(der.encode_sequence(encoded_oid_ecPublicKey, self.curve.encoded_oid), der.encode_bitstring(point_str)) - def verify(self, signature, data, hashfunc=None, sigdecode=sigdecode_string): + def verify(self, signature, data, hashfunc=None, + sigdecode=sigdecode_string): + """ + Verify a signature made over provided data. + + Will hash `data` to verify the signature. + + By default expects signature in raw encoding. Can also be used to + verify signatures in ASN.1 DER encoding by using `sigdecode_der` + as the `sigdecode` parameter. + + :param signature: encoding of the signature + :type signature: bytes like object + :param data: data signed by the `signature`, will be hashed using + `hashfunc`, if specified, or default hash function + :type data: bytes like object + :param hashfunc: The default hash function that will be used for + verification, needs to implement the same interface as hashlib.sha1 + :type hashfunc: callable + :param sigdecode: Callable to define the way the signature needs to + be decoded to an object, needs to handle `signature` as the + first parameter, the curve order (an int) as the second and return + a tuple with two integers, "r" as the first one and "s" as the + second one. See `ecdsa.util.sigdecode_string` and + `ecdsa.util.sigdecode_der` for examples. + :type sigdecode: callable + + :raises BadSignatureError: if the signature is invalid or malformed + :raises BadDigestError: if the provided hash is too big for the curve + associated with this VerifyingKey + + :return: True if the verification was successful + :rtype: bool + """ hashfunc = hashfunc or self.default_hashfunc digest = hashfunc(data).digest() return self.verify_digest(signature, digest, sigdecode) def verify_digest(self, signature, digest, sigdecode=sigdecode_string): + """ + Verify a signature made over provided hash value. + + By default expects signature in raw encoding. Can also be used to + verify signatures in ASN.1 DER encoding by using `sigdecode_der` + as the `sigdecode` parameter. + + :param signature: encoding of the signature + :type signature: bytes like object + :param digest: raw hash value that the signature authenticates. + :type digest: bytes like object + :param sigdecode: Callable to define the way the signature needs to + be decoded to an object, needs to handle `signature` as the + first parameter, the curve order (an int) as the second and return + a tuple with two integers, "r" as the first one and "s" as the + second one. See `ecdsa.util.sigdecode_string` and + `ecdsa.util.sigdecode_der` for examples. + :type sigdecode: callable + + :raises BadSignatureError: if the signature is invalid or malformed + :raises BadDigestError: if the provided hash is too big for the curve + associated with this VerifyingKey + + :return: True if the verification was successful + :rtype: bool + """ if len(digest) > self.curve.baselen: raise BadDigestError("this curve (%s) is too short " "for your digest (%d)" % (self.curve.name, @@ -252,24 +609,79 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string): raise BadSignatureError("Signature verification failed") -class SigningKey: +class SigningKey(object): + """ + Class for handling keys that can create signatures (private keys). + + :ivar curve: The Curve over which all the cryptographic operations will + take place + :ivar default_hashfunc: the function that will be used for hashing the + data. Should implement the same API as hashlib.sha1 + :ivar baselen: the length of a :term:`raw encoding` of private key + :ivar verifying_key: the ecdsa.keys.VerifyingKey holding the public key + associated with this private key + :ivar privkey: the ecdsa.ecdsa.Private_key holding the actual private key + """ + def __init__(self, _error__please_use_generate=None): + """Unsupported, please use one of the classmethods to initialise.""" if not _error__please_use_generate: raise TypeError("Please use SigningKey.generate() to construct me") + self.curve = None + self.default_hashfunc = None + self.baselen = None + self.verifying_key = None + self.privkey = None @classmethod - def generate(klass, curve=NIST192p, entropy=None, hashfunc=sha1): - secexp = randrange(curve.order, entropy) - return klass.from_secret_exponent(secexp, curve, hashfunc) + def generate(cls, curve=NIST192p, entropy=None, hashfunc=sha1): + """ + Generate a random private key. + + :param curve: The curve on which the point needs to reside, defaults + to NIST192p + :type curve: ecdsa.curves.Curve + :param entropy: Source of randomness for generating the private keys, + should provide cryptographically secure random numbers if the keys + need to be secure. Uses os.urandom() by default. + :type entropy: callable + :param hashfunc: The default hash function that will be used for + signing, needs to implement the same interface + as hashlib.sha1 + :type hashfunc: callable - # to create a signing key from a short (arbitrary-length) seed, convert - # that seed into an integer with something like - # secexp=util.randrange_from_seed__X(seed, curve.order), and then pass - # that integer into SigningKey.from_secret_exponent(secexp, curve) + :return: Initialised SigningKey object + :rtype: SigningKey + """ + secexp = randrange(curve.order, entropy) + return cls.from_secret_exponent(secexp, curve, hashfunc) @classmethod - def from_secret_exponent(klass, secexp, curve=NIST192p, hashfunc=sha1): - self = klass(_error__please_use_generate=True) + def from_secret_exponent(cls, secexp, curve=NIST192p, hashfunc=sha1): + """ + Create a private key from a random integer. + + Note: it's a low level method, it's recommended to use the + SigningKey.generate() method to create private keys. + + :param int secexp: secret multiplier (the actual private key in ECDSA). + Needs to be an integer between 1 and the curve order. + :param curve: The curve on which the point needs to reside + :type curve: ecdsa.curves.Curve + :param hashfunc: The default hash function that will be used for + signing, needs to implement the same interface + as hashlib.sha1 + :type hashfunc: callable + + :raises MalformedPointError: when the provided secexp is too large + or too small for the curve selected + :raises RuntimeError: if the generation of public key from private + key failed + + :return: Initialised SigningKey object + :rtype: SigningKey + """ + self = cls(_error__please_use_generate=True) self.curve = curve self.default_hashfunc = hashfunc self.baselen = curve.baselen @@ -279,36 +691,130 @@ 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 = ecdsa.Public_key(curve.generator, pubkey_point) - pubkey.order = n + pubkey_point = pubkey_point.scale() self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, - hashfunc) + hashfunc, False) + pubkey = self.verifying_key.pubkey self.privkey = ecdsa.Private_key(pubkey, secexp) self.privkey.order = n return self @classmethod - def from_string(klass, string, curve=NIST192p, hashfunc=sha1): + def from_string(cls, string, curve=NIST192p, hashfunc=sha1): + """ + Decode the private key from :term:`raw encoding`. + + Note: the name of this method is a misnomer coming from days of + Python 2, when binary strings and character strings shared a type. + In Python 3, the expected type is `bytes`. + + :param string: the raw encoding of the private key + :type string: bytes like object + :param curve: The curve on which the point needs to reside + :type curve: ecdsa.curves.Curve + :param hashfunc: The default hash function that will be used for + signing, needs to implement the same interface + as hashlib.sha1 + :type hashfunc: callable + + :raises MalformedPointError: if the length of encoding doesn't match + the provided curve or the encoded values is too large + :raises RuntimeError: if the generation of public key from private + key failed + + :return: Initialised SigningKey object + :rtype: SigningKey + """ 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) + return cls.from_secret_exponent(secexp, curve, hashfunc) @classmethod - def from_pem(klass, string, hashfunc=sha1): - # the privkey pem file has two sections: "EC PARAMETERS" and "EC - # PRIVATE KEY". The first is redundant. + def from_pem(cls, string, hashfunc=sha1): + """ + Initialise from key stored in :term:`PEM` format. + + Note, the only PEM format supported is the un-encrypted RFC5915 + (the sslay format) supported by OpenSSL, the more common PKCS#8 format + is NOT supported (see: + https://github.com/warner/python-ecdsa/issues/113 ) + + `openssl ec -in pkcs8.pem -out sslay.pem` can be used to + convert PKCS#8 file to this legacy format. + + The legacy format files have the header with the string + `BEGIN EC PRIVATE KEY`. + Encrypted files (ones that include the string `Proc-Type: 4,ENCRYPTED` + right after the PEM header are not supported. + + See `from_der()` for ANS.1 syntax of the objects in this files. + + :param string: text with PEM-encoded private ECDSA key + :type string: str + + :raises MalformedPointError: if the length of encoding doesn't match + the provided curve or the encoded values is too large + :raises RuntimeError: if the generation of public key from private + key failed + :raises UnexpectedDER: if the encoding of the PEM file is incorrect + + :return: Initialised VerifyingKey object + :rtype: VerifyingKey + """ + # the privkey pem may have multiple sections, commonly it also has + # "EC PARAMETERS", we need just "EC PRIVATE KEY". if PY3 and isinstance(string, str): string = string.encode() privkey_pem = string[string.index(b("-----BEGIN EC PRIVATE KEY-----")):] - return klass.from_der(der.unpem(privkey_pem), hashfunc) + return cls.from_der(der.unpem(privkey_pem), hashfunc) @classmethod - def from_der(klass, string, hashfunc=sha1): - # SEQ([int(1), octetstring(privkey),cont[0], oid(secp224r1), - # cont[1],bitstring]) + def from_der(cls, string, hashfunc=sha1): + """ + Initialise from key stored in :term:`DER` format. + + Note, the only DER format supported is the RFC5915 + (the sslay format) supported by OpenSSL, the more common PKCS#8 format + is NOT supported (see: + https://github.com/warner/python-ecdsa/issues/113 ) + + `openssl ec -in pkcs8.pem -outform der -out sslay.der` can be + used to convert PKCS#8 file to this legacy format. + + The encoding of the ASN.1 object in those files follows following + syntax specified in RFC5915:: + + ECPrivateKey ::= SEQUENCE { + version INTEGER { ecPrivkeyVer1(1) }} (ecPrivkeyVer1), + privateKey OCTET STRING, + parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + publicKey [1] BIT STRING OPTIONAL + } + + The only format supported for the `parameters` field is the named + curve method. Explicit encoding of curve parameters is not supported. + + While parameters field is defined as optional, this implementation + requires its presence for correct parsing of the keys. + + publicKey field is ignored completely (errors, if any, in it will + be undetected). + + :param string: binary string with DER-encoded private ECDSA key + :type string: bytes like object + + :raises MalformedPointError: if the length of encoding doesn't match + the provided curve or the encoded values is too large + :raises RuntimeError: if the generation of public key from private + key failed + :raises UnexpectedDER: if the encoding of the DER file is incorrect + + :return: Initialised VerifyingKey object + :rtype: VerifyingKey + """ s, empty = der.remove_sequence(string) if empty != b(""): raise der.UnexpectedDER("trailing junk after DER privkey: %s" % @@ -342,18 +848,54 @@ def from_der(klass, string, hashfunc=sha1): # our from_string method likes fixed-length privkey strings if len(privkey_str) < curve.baselen: privkey_str = b("\x00") * (curve.baselen - len(privkey_str)) + privkey_str - return klass.from_string(privkey_str, curve, hashfunc) + return cls.from_string(privkey_str, curve, hashfunc) def to_string(self): + """ + Convert the private key to :term:`raw encoding`. + + Note: while the method is named "to_string", its name comes from + Python 2 days, when binary and character strings used the same type. + The type used in Python 3 is `bytes`. + + :return: raw encoding of private key + :rtype: bytes + """ secexp = self.privkey.secret_multiplier s = number_to_string(secexp, self.privkey.order) return s - def to_pem(self): + def to_pem(self, point_encoding="uncompressed"): + """ + Convert the private key to the :term:`PEM` format. + + See `from_pem` method for format description. + + Only the named curve format is supported. + The public key will be included, the `point_encoding` specifies the + public point encoding format. + + The PEM header will specify `BEGIN EC PRIVATE KEY` + + :return: PEM encoded private key + :rtype: str + """ # TODO: "BEGIN ECPARAMETERS" - return der.topem(self.to_der(), "EC PRIVATE KEY") + return der.topem(self.to_der(point_encoding), "EC PRIVATE KEY") def to_der(self, point_encoding="uncompressed"): + """ + Convert the private key to the :term:`DER` format. + + See `from_der` method for format specification. + + Only the named curve format is supported. + The public key will be included, the `point_encoding` specifies the + public point encoding format + + :return: DER encoded private key + :rtype: bytes + """ # SEQ([int(1), octetstring(privkey),cont[0], oid(secp224r1), # cont[1],bitstring]) encoded_vk = b("\x00") + \ @@ -365,11 +907,50 @@ def to_der(self, point_encoding="uncompressed"): ) def get_verifying_key(self): + """ + Return the VerifyingKey associated with this private key. + + Equivalent to reading the `verifying_key` field of an instance. + + :return: a public key that can be used to verify the signatures made + with this SigningKey + :rtype: VerifyingKey + """ return self.verifying_key def sign_deterministic(self, data, hashfunc=None, sigencode=sigencode_string, extra_entropy=b''): + """ + Create signature over data using the deterministic RFC6679 algorithm. + + The data will be hashed using the `hashfunc` function before signing. + + This is the recommended method for performing signatures when hashing + of data is necessary. + + :param data: data to be hashed and computed signature over + :type data: bytes like object + :param hashfunc: hash function to use for computing the signature, + if unspecified, the default hash function selected during + object initialisation will be used (see + `VerifyingKey.default_hashfunc`). The object needs to implement + the same interface as hashlib.sha1. + :type hashfunc: callable + :param sigencode: function used to encode the signature. + The function needs to accept three parameters: the two integers + that are the signature and the order of the curve over which the + signature was computed. It needs to return an encoded signature. + See `ecdsa.util.sigencode_string` and `ecdsa.util.sigencode_der` + as examples of such functions. + :type sigencode: callable + :param extra_entropy: additional data that will be fed into the random + number generator used in the RFC6979 process. Entirely optional. + :type extra_entropy: bytes like object + + :return: encoded signature over `data` + :rtype: bytes or sigencode function dependant type + """ hashfunc = hashfunc or self.default_hashfunc digest = hashfunc(data).digest() @@ -381,9 +962,36 @@ def sign_digest_deterministic(self, digest, hashfunc=None, sigencode=sigencode_string, extra_entropy=b''): """ - Calculates 'k' from data itself, removing the need for strong - random generator and producing deterministic (reproducible) signatures. - See RFC 6979 for more details. + Create signature for digest using the deterministic RFC6679 algorithm. + + `digest` should be the output of cryptographically secure hash function + like SHA256 or SHA-3-256. + + This is the recommended method for performing signatures when no + hashing of data is necessary. + + :param digest: hash of data that will be signed + :type digest: bytes like object + :param hashfunc: hash function to use for computing the random "k" + value from RFC6979 process, + if unspecified, the default hash function selected during + object initialisation will be used (see + `VerifyingKey.default_hashfunc`). The object needs to implement + the same interface as hashlib.sha1. + :type hashfunc: callable + :param sigencode: function used to encode the signature. + The function needs to accept three parameters: the two integers + that are the signature and the order of the curve over which the + signature was computed. It needs to return an encoded signature. + See `ecdsa.util.sigencode_string` and `ecdsa.util.sigencode_der` + as examples of such functions. + :type sigencode: callable + :param extra_entropy: additional data that will be fed into the random + number generator used in the RFC6979 process. Entirely optional. + :type extra_entropy: bytes like object + + :return: encoded signature for the `digest` hash + :rtype: bytes or sigencode function dependant type """ secexp = self.privkey.secret_multiplier @@ -403,24 +1011,89 @@ def simple_r_s(r, s, order): return sigencode(r, s, order) - def sign(self, data, entropy=None, hashfunc=None, sigencode=sigencode_string, k=None): + def sign(self, data, entropy=None, hashfunc=None, + sigencode=sigencode_string, k=None): """ - hashfunc= should behave like hashlib.sha1 . The output length of the - hash (in bytes) must not be longer than the length of the curve order - (rounded up to the nearest byte), so using SHA256 with nist256p is - ok, but SHA256 with nist192p is not. (In the 2**-96ish unlikely event - of a hash output larger than the curve order, the hash will - effectively be wrapped mod n). + Create signature over data using the probabilistic ECDSA algorithm. - Use hashfunc=hashlib.sha1 to match openssl's -ecdsa-with-SHA1 mode, - or hashfunc=hashlib.sha256 for openssl-1.0.0's -ecdsa-with-SHA256. - """ + This method uses the standard ECDSA algorithm that requires a + cryptographically secure random number generator. + + It's recommended to use the `sign_deterministic()` method instead of + this one. + :param data: data that will be hashed for signing + :type data: bytes like object + :param callable entropy: randomness source, os.urandom by default + :param hashfunc: hash function to use for hashing the provided `data`. + If unspecified the default hash function selected during + object initialisation will be used (see + `VerifyingKey.default_hashfunc`). + Should behave like hashlib.sha1. The output length of the + hash (in bytes) must not be longer than the length of the curve + order (rounded up to the nearest byte), so using SHA256 with + NIST256p is ok, but SHA256 with NIST192p is not. (In the 2**-96ish + unlikely event of a hash output larger than the curve order, the + hash will effectively be wrapped mod n). + Use hashfunc=hashlib.sha1 to match openssl's -ecdsa-with-SHA1 mode, + or hashfunc=hashlib.sha256 for openssl-1.0.0's -ecdsa-with-SHA256. + :type hashfunc: callable + :param sigencode: function used to encode the signature. + The function needs to accept three parameters: the two integers + that are the signature and the order of the curve over which the + signature was computed. It needs to return an encoded signature. + See `ecdsa.util.sigencode_string` and `ecdsa.util.sigencode_der` + as examples of such functions. + :type sigencode: callable + :param int k: a pre-selected nonce for calculating the signature. + In typical use cases, it should be set to None (the default) to + allow its generation from entropy source. + + :raises RSZeroError: in the unlikely event when "r" parameter or + "s" parameter is equal 0 as that would leak the key. Calee should + try a better entropy source or different 'k' in such case. + + :return: encoded signature of the hash of `data` + :rtype: bytes or sigencode function dependant type + """ hashfunc = hashfunc or self.default_hashfunc h = hashfunc(data).digest() return self.sign_digest(h, entropy, sigencode, k) - def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, k=None): + def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, + k=None): + """ + Create signature over digest using the probabilistic ECDSA algorithm. + + This method uses the standard ECDSA algorithm that requires a + cryptographically secure random number generator. + + This method does not hash the input. + + It's recommended to use the `sign_digest_deterministic()` method + instead of this one. + + :param digest: hash value that will be signed + :type digest: bytes like object + :param callable entropy: randomness source, os.urandom by default + :param sigencode: function used to encode the signature. + The function needs to accept three parameters: the two integers + that are the signature and the order of the curve over which the + signature was computed. It needs to return an encoded signature. + See `ecdsa.util.sigencode_string` and `ecdsa.util.sigencode_der` + as examples of such functions. + :type sigencode: callable + :param int k: a pre-selected nonce for calculating the signature. + In typical use cases, it should be set to None (the default) to + allow its generation from entropy source. + + :raises RSZeroError: in the unlikely event when "r" parameter or + "s" parameter is equal 0 as that would leak the key. Calee should + try a better entropy source in such case. + + :return: encoded signature for the `digest` hash + :rtype: bytes or sigencode function dependant type + """ if len(digest) > self.curve.baselen: raise BadDigestError("this curve (%s) is too short " "for your digest (%d)" % (self.curve.name, @@ -430,15 +1103,27 @@ def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, k=None): return sigencode(r, s, self.privkey.order) def sign_number(self, number, entropy=None, k=None): - # returns a pair of numbers + """ + Sign an integer directly. + + Note, this is a low level method, usually you will want to use + sign_deterministic() or sign_digest_deterministic(). + + :param int number: number to sign using the probabilistic ECDSA + algorithm. + :param callable entropy: entropy source, os.urandom by default + :param int k: pre-selected nonce for signature operation. If unset + it will be selected at random using the entropy source. + + :raises RSZeroError: in the unlikely event when "r" parameter or + "s" parameter is equal 0 as that would leak the key. Calee should + try a different 'k' in such case. + + :return: the "r" and "s" parameters of the signature + :rtype: tuple of ints + """ order = self.privkey.order - # privkey.sign() may raise RuntimeError in the amazingly unlikely - # (2**-192) event that r=0 or s=0, because that would leak the key. - # We could re-try with a different 'k', but we couldn't test that - # code, so I choose to allow the signature to fail instead. - # If k is set, it is used directly. In other cases - # it is generated using entropy function if k is not None: _k = k else: 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): diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py new file mode 100644 index 00000000..94a483c6 --- /dev/null +++ b/src/ecdsa/test_jacobi.py @@ -0,0 +1,70 @@ + +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())) + 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)) diff --git a/src/ecdsa/util.py b/src/ecdsa/util.py index 18b26ea1..2094b2ac 100644 --- a/src/ecdsa/util.py +++ b/src/ecdsa/util.py @@ -4,8 +4,8 @@ import math import binascii from hashlib import sha256 -from . import der from six import PY3, int2byte, b, next +from . import der # RFC5480: # The "unrestricted" algorithm identifier is: @@ -212,6 +212,16 @@ def sigencode_strings(r, s, order): def sigencode_string(r, s, order): + """ + Encode the signature to raw format. + + :param int r: first parametr of the signature + :param int s: second parameter of the signature + :param int order: the order of curve over which the signature was computed + + :return: DER encoding of ECDSA signature + :rtype: bytes + """ # for any given curve, the size of the signature numbers is # fixed, so just use simple concatenation r_str, s_str = sigencode_strings(r, s, order) @@ -219,6 +229,26 @@ def sigencode_string(r, s, order): def sigencode_der(r, s, order): + """ + Encode the signature into the ECDSA-Sig-Value structure. + + Encodes the signature to the following ASN.1 structure:: + + Ecdsa-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER + } + + It's expected that this function will be used as a `sigencode=` parameter + in SigningKey.sign() method. + + :param int r: first parametr of the signature + :param int s: second parameter of the signature + :param int order: the order of curve over which the signature was computed + + :return: DER encoding of ECDSA signature + :rtype: bytes + """ return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) @@ -244,44 +274,113 @@ def sigencode_der_canonize(r, s, order): class MalformedSignature(Exception): + """ + Raised by decoding functions when the signature is malformed. + + Malformed in this context means that the relevant strings or integers + do not match what a signature over provided curve would create. + """ + pass def sigdecode_string(signature, order): + """ + Decoder for raw format of ECDSA signatures. + + raw encoding is a simple concatenation of the two integers that comprise + the signature, with each encoded using the same amount of bytes depending + on curve size/order. + + It's expected that this function will be used as as the `sigdecode=` + parameter to the `VerifyingKey.verify()` method. + + :param signature: encoded signature + :type signature: bytes like object + :param order: order of the curve over which the signature was computed + :type order: int + + :raises MalformedSignature: when the encoding of the signature is invalid + + :return: tuple with decoded 'r' and 's' values of signature + :rtype: tuple of ints + """ l = orderlen(order) if not len(signature) == 2 * l: raise MalformedSignature( - "Invalid length of signature, expected {0} bytes long, " - "provided string is {1} bytes long" - .format(2 * l, len(signature))) + "Invalid length of signature, expected {0} bytes long, " + "provided string is {1} bytes long" + .format(2 * l, len(signature))) r = string_to_number_fixedlen(signature[:l], order) s = string_to_number_fixedlen(signature[l:], order) return r, s def sigdecode_strings(rs_strings, order): + """ + Decode the signature from two strings. + + First string needs to encode 'r', second needs to encode the + 's' parameter of ECDSA signature. + + It's expected that this function will be used as as the `sigdecode=` + parameter to the `VerifyingKey.verify()` method. + + :param list rs_strings: list of two bytes-like objects, each encoding one + parameter of signature + :param int order: order of the curve over which the signature was computed + + :raises MalformedSignature: when the encoding of the signature is invalid + + :return: tuple with decoded 'r' and 's' values of signature + :rtype: tuple of ints + """ if not len(rs_strings) == 2: raise MalformedSignature( - "Invalid number of strings provided: {0}, expected 2" - .format(len(rs_strings))) + "Invalid number of strings provided: {0}, expected 2" + .format(len(rs_strings))) (r_str, s_str) = rs_strings l = orderlen(order) if not len(r_str) == l: raise MalformedSignature( - "Invalid length of first string ('r' parameter), " - "expected {0} bytes long, provided string is {1} bytes long" - .format(l, len(r_str))) + "Invalid length of first string ('r' parameter), " + "expected {0} bytes long, provided string is {1} bytes long" + .format(l, len(r_str))) if not len(s_str) == l: raise MalformedSignature( - "Invalid length of second string ('s' parameter), " - "expected {0} bytes long, provided string is {1} bytes long" - .format(l, len(s_str))) + "Invalid length of second string ('s' parameter), " + "expected {0} bytes long, provided string is {1} bytes long" + .format(l, len(s_str))) r = string_to_number_fixedlen(r_str, order) s = string_to_number_fixedlen(s_str, order) return r, s def sigdecode_der(sig_der, order): + """ + Decoder for DER format of ECDSA signatures. + + DER format of signature is one that uses the :term:`ASN.1` :term:`DER` + rules to encode it as a sequence of two integers:: + + Ecdsa-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER + } + + It's expected that this function will be used as as the `sigdecode=` + parameter to the `VerifyingKey.verify()` method. + + :param sig_der: encoded signature + :type sig_der: bytes like object + :param order: order of the curve over which the signature was computed + :type order: int + + :raises UnexpectedDER: when the encoding of signature is invalid + + :return: tuple with decoded 'r' and 's' values of signature + :rtype: tuple of ints + """ # return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) rs_strings, empty = der.remove_sequence(sig_der) if empty != b(""):