diff --git a/build-requirements-3.4.txt b/build-requirements-3.4.txt index ce3e0fec..ee734d11 100644 --- a/build-requirements-3.4.txt +++ b/build-requirements-3.4.txt @@ -4,3 +4,4 @@ hypothesis pytest>=4.6.0 PyYAML<5.3 coverage +attrs<21 diff --git a/src/ecdsa/curves.py b/src/ecdsa/curves.py index 90cdd52e..7a4a530a 100644 --- a/src/ecdsa/curves.py +++ b/src/ecdsa/curves.py @@ -1,7 +1,9 @@ from __future__ import division -from . import der, ecdsa -from .util import orderlen +from six import PY2 +from . import der, ecdsa, ellipticcurve +from .util import orderlen, number_to_string, string_to_number +from ._compat import normalise_bytes # orderlen was defined in this module previously, so keep it in __all__, @@ -29,9 +31,15 @@ "BRAINPOOLP320r1", "BRAINPOOLP384r1", "BRAINPOOLP512r1", + "PRIME_FIELD_OID", + "CHARACTERISTIC_TWO_FIELD_OID", ] +PRIME_FIELD_OID = (1, 2, 840, 10045, 1, 1) +CHARACTERISTIC_TWO_FIELD_OID = (1, 2, 840, 10045, 1, 2) + + class UnknownCurveError(Exception): pass @@ -47,11 +55,203 @@ def __init__(self, name, curve, generator, oid, openssl_name=None): self.verifying_key_length = 2 * orderlen(curve.p()) self.signature_length = 2 * self.baselen self.oid = oid - self.encoded_oid = der.encode_oid(*oid) + if oid: + self.encoded_oid = der.encode_oid(*oid) + + def __eq__(self, other): + if isinstance(other, Curve): + return ( + self.curve == other.curve and self.generator == other.generator + ) + return NotImplemented + + def __ne__(self, other): + return not self == other def __repr__(self): return self.name + def to_der(self, encoding=None, point_encoding="uncompressed"): + """Serialise the curve parameters to binary string. + + :param str encoding: the format to save the curve parameters in. + Default is ``named_curve``, with fallback being the ``explicit`` + if the OID is not set for the curve. + :param str point_encoding: the point encoding of the generator when + explicit curve encoding is used. Ignored for ``named_curve`` + format. + + :return: DER encoded ECParameters structure + :rtype: bytes + """ + if encoding is None: + if self.oid: + encoding = "named_curve" + else: + encoding = "explicit" + + if encoding == "named_curve": + if not self.oid: + raise UnknownCurveError( + "Can't encode curve using named_curve encoding without " + "associated curve OID" + ) + return der.encode_oid(*self.oid) + + # encode the ECParameters sequence + curve_p = self.curve.p() + version = der.encode_integer(1) + field_id = der.encode_sequence( + der.encode_oid(*PRIME_FIELD_OID), der.encode_integer(curve_p) + ) + curve = der.encode_sequence( + der.encode_octet_string( + number_to_string(self.curve.a() % curve_p, curve_p) + ), + der.encode_octet_string( + number_to_string(self.curve.b() % curve_p, curve_p) + ), + ) + base = der.encode_octet_string(self.generator.to_bytes(point_encoding)) + order = der.encode_integer(self.generator.order()) + seq_elements = [version, field_id, curve, base, order] + if self.curve.cofactor(): + cofactor = der.encode_integer(self.curve.cofactor()) + seq_elements.append(cofactor) + + return der.encode_sequence(*seq_elements) + + def to_pem(self, encoding=None, point_encoding="uncompressed"): + """ + Serialise the curve parameters to the :term:`PEM` format. + + :param str encoding: the format to save the curve parameters in. + Default is ``named_curve``, with fallback being the ``explicit`` + if the OID is not set for the curve. + :param str point_encoding: the point encoding of the generator when + explicit curve encoding is used. Ignored for ``named_curve`` + format. + + :return: PEM encoded ECParameters structure + :rtype: str + """ + return der.topem( + self.to_der(encoding, point_encoding), "EC PARAMETERS" + ) + + @staticmethod + def from_der(data, valid_encodings=None): + """Decode the curve parameters from DER file. + + :param data: the binary string to decode the parameters from + :type data: :term:`bytes-like object` + :param valid_encodings: set of names of allowed encodings, by default + all (set by passing ``None``), supported ones are ``named_curve`` + and ``explicit`` + :type valid_encodings: :term:`set-like object` + """ + if not valid_encodings: + valid_encodings = set(("named_curve", "explicit")) + if not all(i in ["named_curve", "explicit"] for i in valid_encodings): + raise ValueError( + "Only named_curve and explicit encodings supported" + ) + data = normalise_bytes(data) + if not der.is_sequence(data): + if "named_curve" not in valid_encodings: + raise der.UnexpectedDER( + "named_curve curve parameters not allowed" + ) + oid, empty = der.remove_object(data) + if empty: + raise der.UnexpectedDER("Unexpected data after OID") + return find_curve(oid) + + if "explicit" not in valid_encodings: + raise der.UnexpectedDER("explicit curve parameters not allowed") + + seq, empty = der.remove_sequence(data) + if empty: + raise der.UnexpectedDER( + "Unexpected data after ECParameters structure" + ) + # decode the ECParameters sequence + version, rest = der.remove_integer(seq) + if version != 1: + raise der.UnexpectedDER("Unknown parameter encoding format") + field_id, rest = der.remove_sequence(rest) + curve, rest = der.remove_sequence(rest) + base_bytes, rest = der.remove_octet_string(rest) + order, rest = der.remove_integer(rest) + cofactor = None + if rest: + # the ASN.1 specification of ECParameters allows for future + # extensions of the sequence, so ignore the remaining bytes + cofactor, _ = der.remove_integer(rest) + + # decode the ECParameters.fieldID sequence + field_type, rest = der.remove_object(field_id) + if field_type == CHARACTERISTIC_TWO_FIELD_OID: + raise UnknownCurveError("Characteristic 2 curves unsupported") + if field_type != PRIME_FIELD_OID: + raise UnknownCurveError( + "Unknown field type: {0}".format(field_type) + ) + prime, empty = der.remove_integer(rest) + if empty: + raise der.UnexpectedDER( + "Unexpected data after ECParameters.fieldID.Prime-p element" + ) + + # decode the ECParameters.curve sequence + curve_a_bytes, rest = der.remove_octet_string(curve) + curve_b_bytes, rest = der.remove_octet_string(rest) + # seed can be defined here, but we don't parse it, so ignore `rest` + + curve_a = string_to_number(curve_a_bytes) + curve_b = string_to_number(curve_b_bytes) + + curve_fp = ellipticcurve.CurveFp(prime, curve_a, curve_b, cofactor) + + # decode the ECParameters.base point + + base = ellipticcurve.PointJacobi.from_bytes( + curve_fp, + base_bytes, + valid_encodings=("uncompressed", "compressed", "hybrid"), + order=order, + generator=True, + ) + tmp_curve = Curve("unknown", curve_fp, base, None) + + # if the curve matches one of the well-known ones, use the well-known + # one in preference, as it will have the OID and name associated + for i in curves: + if tmp_curve == i: + return i + return tmp_curve + + @classmethod + def from_pem(cls, string, valid_encodings=None): + """Decode the curve parameters from PEM file. + + :param str string: the text string to decode the parameters from + :param valid_encodings: set of names of allowed encodings, by default + all (set by passing ``None``), supported ones are ``named_curve`` + and ``explicit`` + :type valid_encodings: :term:`set-like object` + """ + if not PY2 and isinstance(string, str): # pragma: no branch + string = string.encode() + + ec_param_index = string.find(b"-----BEGIN EC PARAMETERS-----") + if ec_param_index == -1: + raise der.UnexpectedDER("EC PARAMETERS PEM header not found") + + return cls.from_der( + der.unpem(string[ec_param_index:]), valid_encodings + ) + # the SEC curves SECP112r1 = Curve( diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index e6d8931a..6d0bc304 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -49,6 +49,9 @@ from six import python_2_unicode_compatible from . import numbertheory +from ._compat import normalise_bytes +from .errors import MalformedPointError +from .util import orderlen, string_to_number, number_to_string @python_2_unicode_compatible @@ -98,10 +101,11 @@ def __eq__(self, other): only the prime and curve parameters are considered. """ if isinstance(other, CurveFp): + p = self.__p return ( self.__p == other.__p - and self.__a == other.__a - and self.__b == other.__b + and self.__a % p == other.__a % p + and self.__b % p == other.__b % p ) return NotImplemented @@ -137,7 +141,198 @@ def __str__(self): ) -class PointJacobi(object): +class AbstractPoint(object): + """Class for common methods of elliptic curve points.""" + + @staticmethod + def _from_raw_encoding(data, raw_encoding_length): + """ + Decode public point from :term:`raw encoding`. + + :term:`raw encoding` is the same as the :term:`uncompressed` encoding, + but without the 0x04 byte at the beginning. + """ + # real assert, from_bytes() should not call us with different length + assert len(data) == raw_encoding_length + xs = data[: raw_encoding_length // 2] + ys = data[raw_encoding_length // 2 :] + # real assert, raw_encoding_length is calculated by multiplying an + # integer by two so it will always be even + assert len(xs) == raw_encoding_length // 2 + assert len(ys) == raw_encoding_length // 2 + coord_x = string_to_number(xs) + coord_y = string_to_number(ys) + + return coord_x, coord_y + + @staticmethod + def _from_compressed(data, curve): + """Decode public point from compressed encoding.""" + if data[:1] not in (b"\x02", b"\x03"): + raise MalformedPointError("Malformed compressed point encoding") + + is_even = data[:1] == b"\x02" + x = string_to_number(data[1:]) + p = curve.p() + alpha = (pow(x, 3, p) + (curve.a() * x) + curve.b()) % p + try: + beta = numbertheory.square_root_mod_prime(alpha, p) + except numbertheory.SquareRootError as e: + raise MalformedPointError( + "Encoding does not correspond to a point on curve", e + ) + if is_even == bool(beta & 1): + y = p - beta + else: + y = beta + return x, y + + @classmethod + def _from_hybrid(cls, data, raw_encoding_length, validate_encoding): + """Decode public point from hybrid encoding.""" + # real assert, from_bytes() should not call us with different types + assert data[:1] in (b"\x06", b"\x07") + + # primarily use the uncompressed as it's easiest to handle + x, y = cls._from_raw_encoding(data[1:], raw_encoding_length) + + # but validate if it's self-consistent if we're asked to do that + if validate_encoding and ( + y & 1 + and data[:1] != b"\x07" + or (not y & 1) + and data[:1] != b"\x06" + ): + raise MalformedPointError("Inconsistent hybrid point encoding") + + return x, y + + @classmethod + def from_bytes( + cls, curve, data, validate_encoding=True, valid_encodings=None + ): + """ + Initialise the object from byte encoding of a point. + + 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: generally you will want to call the ``from_bytes()`` method of + either a child class, PointJacobi or Point. + + :param data: single point encoding of the public key + :type data: :term:`bytes-like object` + :param curve: the curve on which the public key is expected to lay + :type curve: ecdsa.ellipticcurve.CurveFp + :param validate_encoding: whether to verify that the encoding of the + point is self-consistent, defaults to True, has effect only + on ``hybrid`` encoding + :type validate_encoding: bool + :param valid_encodings: list of acceptable point encoding formats, + supported ones are: :term:`uncompressed`, :term:`compressed`, + :term:`hybrid`, and :term:`raw encoding` (specified with ``raw`` + name). All formats by default (specified with ``None``). + :type valid_encodings: :term:`set-like object` + + :raises MalformedPointError: if the public point does not lay on the + curve or the encoding is invalid + + :return: x and y coordinates of the encoded point + :rtype: tuple(int, int) + """ + if not valid_encodings: + valid_encodings = set( + ["uncompressed", "compressed", "hybrid", "raw"] + ) + if not all( + i in set(("uncompressed", "compressed", "hybrid", "raw")) + for i in valid_encodings + ): + raise ValueError( + "Only uncompressed, compressed, hybrid or raw encoding " + "supported." + ) + data = normalise_bytes(data) + key_len = len(data) + raw_encoding_length = 2 * orderlen(curve.p()) + if key_len == raw_encoding_length and "raw" in valid_encodings: + coord_x, coord_y = cls._from_raw_encoding( + data, raw_encoding_length + ) + elif key_len == raw_encoding_length + 1 and ( + "hybrid" in valid_encodings or "uncompressed" in valid_encodings + ): + if data[:1] in (b"\x06", b"\x07") and "hybrid" in valid_encodings: + coord_x, coord_y = cls._from_hybrid( + data, raw_encoding_length, validate_encoding + ) + elif data[:1] == b"\x04" and "uncompressed" in valid_encodings: + coord_x, coord_y = cls._from_raw_encoding( + data[1:], raw_encoding_length + ) + else: + raise MalformedPointError( + "Invalid X9.62 encoding of the public point" + ) + elif ( + key_len == raw_encoding_length // 2 + 1 + and "compressed" in valid_encodings + ): + coord_x, coord_y = cls._from_compressed(data, curve) + else: + raise MalformedPointError( + "Length of string does not match lengths of " + "any of the enabled ({0}) encodings of the " + "curve.".format(", ".join(valid_encodings)) + ) + return coord_x, coord_y + + def _raw_encode(self): + """Convert the point to the :term:`raw encoding`.""" + prime = self.curve().p() + x_str = number_to_string(self.x(), prime) + y_str = number_to_string(self.y(), prime) + return x_str + y_str + + def _compressed_encode(self): + """Encode the point into the compressed form.""" + prime = self.curve().p() + x_str = number_to_string(self.x(), prime) + if self.y() & 1: + return b"\x03" + x_str + return b"\x02" + x_str + + def _hybrid_encode(self): + """Encode the point into the hybrid form.""" + raw_enc = self._raw_encode() + if self.y() & 1: + return b"\x07" + raw_enc + return b"\x06" + raw_enc + + def to_bytes(self, encoding="raw"): + """ + Convert the point to a byte string. + + The method by default uses the :term:`raw encoding` (specified + by `encoding="raw"`. It can also output points in :term:`uncompressed`, + :term:`compressed`, and :term:`hybrid` formats. + + :return: :term:`raw encoding` of a public on the curve + :rtype: bytes + """ + assert encoding in ("raw", "uncompressed", "compressed", "hybrid") + if encoding == "raw": + return self._raw_encode() + elif encoding == "uncompressed": + return b"\x04" + self._raw_encode() + elif encoding == "hybrid": + return self._hybrid_encode() + else: + return self._compressed_encode() + + +class PointJacobi(AbstractPoint): """ Point on an elliptic curve. Uses Jacobi coordinates. @@ -165,6 +360,7 @@ def __init__(self, curve, x, y, z, order=None, generator=False): such, it will be commonly used with scalar multiplication. This will cause to precompute multiplication table generation for it """ + super(PointJacobi, self).__init__() self.__curve = curve if GMPY: # pragma: no branch self.__coords = (mpz(x), mpz(y), mpz(z)) @@ -175,6 +371,53 @@ def __init__(self, curve, x, y, z, order=None, generator=False): self.__generator = generator self.__precompute = [] + @classmethod + def from_bytes( + cls, + curve, + data, + validate_encoding=True, + valid_encodings=None, + order=None, + generator=False, + ): + """ + Initialise the object from byte encoding of a point. + + 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. + + :param data: single point encoding of the public key + :type data: :term:`bytes-like object` + :param curve: the curve on which the public key is expected to lay + :type curve: ecdsa.ellipticcurve.CurveFp + :param validate_encoding: whether to verify that the encoding of the + point is self-consistent, defaults to True, has effect only + on ``hybrid`` encoding + :type validate_encoding: bool + :param valid_encodings: list of acceptable point encoding formats, + supported ones are: :term:`uncompressed`, :term:`compressed`, + :term:`hybrid`, and :term:`raw encoding` (specified with ``raw`` + name). All formats by default (specified with ``None``). + :type valid_encodings: :term:`set-like object` + :param int order: the point order, must be non zero when using + generator=True + :param bool generator: the point provided is a curve generator, as + such, it will be commonly used with scalar multiplication. This + will cause to precompute multiplication table generation for it + + :raises MalformedPointError: if the public point does not lay on the + curve or the encoding is invalid + + :return: Point on curve + :rtype: PointJacobi + """ + coord_x, coord_y = super(PointJacobi, cls).from_bytes( + curve, data, validate_encoding, valid_encodings + ) + return PointJacobi(curve, coord_x, coord_y, 1, order, generator) + def _maybe_precompute(self): if not self.__generator or self.__precompute: return @@ -683,12 +926,13 @@ def __neg__(self): return PointJacobi(self.__curve, x, -y, z, self.__order) -class Point(object): +class Point(AbstractPoint): """A point on an elliptic curve. Altering x and y is forbidden, but they can be read by the x() and y() methods.""" def __init__(self, curve, x, y, order=None): """curve, x, y, order; order (optional) is the order of this point.""" + super(Point, self).__init__() self.__curve = curve if GMPY: self.__x = x and mpz(x) @@ -707,6 +951,49 @@ def __init__(self, curve, x, y, order=None): if curve and curve.cofactor() != 1 and order: assert self * order == INFINITY + @classmethod + def from_bytes( + cls, + curve, + data, + validate_encoding=True, + valid_encodings=None, + order=None, + ): + """ + Initialise the object from byte encoding of a point. + + 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. + + :param data: single point encoding of the public key + :type data: :term:`bytes-like object` + :param curve: the curve on which the public key is expected to lay + :type curve: ecdsa.ellipticcurve.CurveFp + :param validate_encoding: whether to verify that the encoding of the + point is self-consistent, defaults to True, has effect only + on ``hybrid`` encoding + :type validate_encoding: bool + :param valid_encodings: list of acceptable point encoding formats, + supported ones are: :term:`uncompressed`, :term:`compressed`, + :term:`hybrid`, and :term:`raw encoding` (specified with ``raw`` + name). All formats by default (specified with ``None``). + :type valid_encodings: :term:`set-like object` + :param int order: the point order, must be non zero when using + generator=True + + :raises MalformedPointError: if the public point does not lay on the + curve or the encoding is invalid + + :return: Point on curve + :rtype: Point + """ + coord_x, coord_y = super(Point, cls).from_bytes( + curve, data, validate_encoding, valid_encodings + ) + return Point(curve, coord_x, coord_y, order) + def __eq__(self, other): """Return True if the points are identical, False otherwise. diff --git a/src/ecdsa/errors.py b/src/ecdsa/errors.py new file mode 100644 index 00000000..0184c05b --- /dev/null +++ b/src/ecdsa/errors.py @@ -0,0 +1,4 @@ +class MalformedPointError(AssertionError): + """Raised in case the encoding of private or public key is malformed.""" + + pass diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index e0723540..29c77d13 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -77,8 +77,7 @@ from . import der from . import rfc6979 from . import ellipticcurve -from .curves import NIST192p, find_curve -from .numbertheory import square_root_mod_prime, SquareRootError +from .curves import NIST192p, Curve from .ecdsa import RSZeroError from .util import string_to_number, number_to_string, randrange from .util import sigencode_string, sigdecode_string, bit_length @@ -90,6 +89,8 @@ MalformedSignature, ) from ._compat import normalise_bytes +from .errors import MalformedPointError +from .ellipticcurve import PointJacobi __all__ = [ @@ -122,12 +123,6 @@ class BadDigestError(Exception): pass -class MalformedPointError(AssertionError): - """Raised in case the encoding of private or public key is malformed.""" - - pass - - def _truncate_and_convert_digest(digest, curve, allow_truncate): """Truncates and converts digest to an integer.""" if not allow_truncate: @@ -269,71 +264,6 @@ def precompute(self, lazy=False): if not lazy: self.pubkey.point * 2 - @staticmethod - def _from_raw_encoding(string, curve): - """ - Decode public point from :term:`raw encoding`. - - :term:`raw encoding` is the same as the :term:`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 - xs = string[: curve.verifying_key_length // 2] - ys = string[curve.verifying_key_length // 2 :] - # real assert, verifying_key_length is calculated by multiplying an - # integer by two so it will always be even - assert len(xs) == curve.verifying_key_length // 2 - assert len(ys) == curve.verifying_key_length // 2 - x = string_to_number(xs) - y = string_to_number(ys) - - return ellipticcurve.PointJacobi(curve.curve, x, y, 1, order) - - @staticmethod - 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") - - is_even = string[:1] == b("\x02") - x = string_to_number(string[1:]) - order = curve.order - p = curve.curve.p() - alpha = (pow(x, 3, p) + (curve.curve.a() * x) + curve.curve.b()) % p - try: - beta = square_root_mod_prime(alpha, p) - except SquareRootError as e: - raise MalformedPointError( - "Encoding does not correspond to a point on curve", e - ) - if is_even == bool(beta & 1): - y = p - beta - else: - y = beta - 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) - - # but validate if it's self-consistent if we're asked to do that - if validate_point and ( - point.y() & 1 - and string[:1] != b("\x07") - or (not point.y() & 1) - and string[:1] != b("\x06") - ): - raise MalformedPointError("Inconsistent hybrid point encoding") - - return point - @classmethod def from_string( cls, @@ -348,7 +278,7 @@ def from_string( 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. + :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 @@ -376,43 +306,22 @@ def from_string( :return: Initialised VerifyingKey object :rtype: VerifyingKey """ - if valid_encodings is None: - valid_encodings = set( - ["uncompressed", "compressed", "hybrid", "raw"] - ) - string = normalise_bytes(string) - sig_len = len(string) - if sig_len == curve.verifying_key_length and "raw" in valid_encodings: - point = cls._from_raw_encoding(string, curve) - elif sig_len == curve.verifying_key_length + 1 and ( - "hybrid" in valid_encodings or "uncompressed" in valid_encodings - ): - if ( - string[:1] in (b("\x06"), b("\x07")) - and "hybrid" in valid_encodings - ): - point = cls._from_hybrid(string, curve, validate_point) - elif string[:1] == b("\x04") and "uncompressed" in valid_encodings: - point = cls._from_raw_encoding(string[1:], curve) - else: - raise MalformedPointError( - "Invalid X9.62 encoding of the public point" - ) - elif ( - sig_len == curve.verifying_key_length // 2 + 1 - and "compressed" in valid_encodings - ): - point = cls._from_compressed(string, curve) - else: - raise MalformedPointError( - "Length of string does not match lengths of " - "any of the enabled ({1}) encodings of {0} " - "curve.".format(curve.name, ", ".join(valid_encodings)) - ) + point = PointJacobi.from_bytes( + curve.curve, + string, + validate_encoding=validate_point, + valid_encodings=valid_encodings, + ) return cls.from_public_point(point, curve, hashfunc, validate_point) @classmethod - def from_pem(cls, string, hashfunc=sha1, valid_encodings=None): + def from_pem( + cls, + string, + hashfunc=sha1, + valid_encodings=None, + valid_curve_encodings=None, + ): """ Initialise from public key stored in :term:`PEM` format. @@ -421,7 +330,7 @@ def from_pem(cls, string, hashfunc=sha1, valid_encodings=None): See the :func:`~VerifyingKey.from_der()` method for details of the format supported. - Note: only a single PEM object encoding is supported in provided + Note: only a single PEM object decoding is supported in provided string. :param string: text with PEM-encoded public ECDSA key @@ -431,6 +340,11 @@ def from_pem(cls, string, hashfunc=sha1, valid_encodings=None): :term:`hybrid`. To read malformed files, include :term:`raw encoding` with ``raw`` in the list. :type valid_encodings: :term:`set-like object + :param valid_curve_encodings: list of allowed encoding formats + for curve parameters. By default (``None``) all are supported: + ``named_curve`` and ``explicit``. + :type valid_curve_encodings: :term:`set-like object` + :return: Initialised VerifyingKey object :rtype: VerifyingKey @@ -439,10 +353,17 @@ def from_pem(cls, string, hashfunc=sha1, valid_encodings=None): der.unpem(string), hashfunc=hashfunc, valid_encodings=valid_encodings, + valid_curve_encodings=valid_curve_encodings, ) @classmethod - def from_der(cls, string, hashfunc=sha1, valid_encodings=None): + def from_der( + cls, + string, + hashfunc=sha1, + valid_encodings=None, + valid_curve_encodings=None, + ): """ Initialise the key stored in :term:`DER` format. @@ -472,6 +393,10 @@ def from_der(cls, string, hashfunc=sha1, valid_encodings=None): :term:`hybrid`. To read malformed files, include :term:`raw encoding` with ``raw`` in the list. :type valid_encodings: :term:`set-like object + :param valid_curve_encodings: list of allowed encoding formats + for curve parameters. By default (``None``) all are supported: + ``named_curve`` and ``explicit``. + :type valid_curve_encodings: :term:`set-like object` :return: Initialised VerifyingKey object :rtype: VerifyingKey @@ -488,18 +413,12 @@ def from_der(cls, string, hashfunc=sha1, valid_encodings=None): s2, point_str_bitstring = der.remove_sequence(s1) # s2 = oid_ecPublicKey,oid_curve oid_pk, rest = der.remove_object(s2) - oid_curve, empty = der.remove_object(rest) - if empty != b"": - raise der.UnexpectedDER( - "trailing junk after DER pubkey objects: %s" - % binascii.hexlify(empty) - ) if not oid_pk == oid_ecPublicKey: raise der.UnexpectedDER( "Unexpected object identifier in DER " "encoding: {0!r}".format(oid_pk) ) - curve = find_curve(oid_curve) + curve = Curve.from_der(rest, valid_curve_encodings) point_str, empty = der.remove_bitstring(point_str_bitstring, 0) if empty != b"": raise der.UnexpectedDER( @@ -622,30 +541,6 @@ def from_public_key_recovery_with_digest( ] return verifying_keys - def _raw_encode(self): - """Convert the public key to the :term:`raw encoding`.""" - order = self.curve.curve.p() - 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.curve.curve.p() - x_str = number_to_string(self.pubkey.point.x(), order) - if self.pubkey.point.y() & 1: - return b("\x03") + x_str - else: - 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 - else: - return b("\x06") + raw_enc - def to_string(self, encoding="raw"): """ Convert the public key to a byte string. @@ -667,16 +562,11 @@ def to_string(self, encoding="raw"): :rtype: bytes """ assert encoding in ("raw", "uncompressed", "compressed", "hybrid") - if encoding == "raw": - return self._raw_encode() - elif encoding == "uncompressed": - return b("\x04") + self._raw_encode() - elif encoding == "hybrid": - return self._hybrid_encode() - else: - return self._compressed_encode() + return self.pubkey.point.to_bytes(encoding) - def to_pem(self, point_encoding="uncompressed"): + def to_pem( + self, point_encoding="uncompressed", curve_parameters_encoding=None + ): """ Convert the public key to the :term:`PEM` format. @@ -690,6 +580,9 @@ def to_pem(self, point_encoding="uncompressed"): of public keys. "uncompressed" is most portable, "compressed" is smallest. "hybrid" is uncommon and unsupported by most implementations, it is as big as "uncompressed". + :param str curve_parameters_encoding: the encoding for curve parameters + to use, by default tries to use ``named_curve`` encoding, + if that is not possible, falls back to ``named_curve`` encoding. :return: portable encoding of the public key :rtype: bytes @@ -697,9 +590,14 @@ def to_pem(self, point_encoding="uncompressed"): .. warning:: The PEM is encoded to US-ASCII, it needs to be re-encoded if the system is incompatible (e.g. uses UTF-16) """ - return der.topem(self.to_der(point_encoding), "PUBLIC KEY") + return der.topem( + self.to_der(point_encoding, curve_parameters_encoding), + "PUBLIC KEY", + ) - def to_der(self, point_encoding="uncompressed"): + def to_der( + self, point_encoding="uncompressed", curve_parameters_encoding=None + ): """ Convert the public key to the :term:`DER` format. @@ -711,6 +609,9 @@ def to_der(self, point_encoding="uncompressed"): of public keys. "uncompressed" is most portable, "compressed" is smallest. "hybrid" is uncommon and unsupported by most implementations, it is as big as "uncompressed". + :param str curve_parameters_encoding: the encoding for curve parameters + to use, by default tries to use ``named_curve`` encoding, + if that is not possible, falls back to ``named_curve`` encoding. :return: DER encoding of the public key :rtype: bytes @@ -720,7 +621,8 @@ def to_der(self, point_encoding="uncompressed"): point_str = self.to_string(point_encoding) return der.encode_sequence( der.encode_sequence( - encoded_oid_ecPublicKey, self.curve.encoded_oid + encoded_oid_ecPublicKey, + self.curve.to_der(curve_parameters_encoding), ), # 0 is the number of unused bits in the # bit string @@ -977,7 +879,7 @@ def from_string(cls, string, curve=NIST192p, hashfunc=sha1): return cls.from_secret_exponent(secexp, curve, hashfunc) @classmethod - def from_pem(cls, string, hashfunc=sha1): + def from_pem(cls, string, hashfunc=sha1, valid_curve_encodings=None): """ Initialise from key stored in :term:`PEM` format. @@ -997,6 +899,11 @@ def from_pem(cls, string, hashfunc=sha1): :param string: text with PEM-encoded private ECDSA key :type string: str + :param valid_curve_encodings: list of allowed encoding formats + for curve parameters. By default (``None``) all are supported: + ``named_curve`` and ``explicit``. + :type valid_curve_encodings: :term:`set-like object` + :raises MalformedPointError: if the length of encoding doesn't match the provided curve or the encoded values is too large @@ -1017,10 +924,14 @@ def from_pem(cls, string, hashfunc=sha1): if private_key_index == -1: private_key_index = string.index(b"-----BEGIN PRIVATE KEY-----") - return cls.from_der(der.unpem(string[private_key_index:]), hashfunc) + return cls.from_der( + der.unpem(string[private_key_index:]), + hashfunc, + valid_curve_encodings, + ) @classmethod - def from_der(cls, string, hashfunc=sha1): + def from_der(cls, string, hashfunc=sha1, valid_curve_encodings=None): """ Initialise from key stored in :term:`DER` format. @@ -1041,8 +952,8 @@ def from_der(cls, string, hashfunc=sha1): `publicKey` field is ignored completely (errors, if any, in it will be undetected). - The only format supported for the `parameters` field is the named - curve method. Explicit encoding of curve parameters is not supported. + Two formats are supported for the `parameters` field: the named + curve and the explicit encoding of curve parameters. In the legacy ssleay format, this implementation requires the optional `parameters` field to get the curve name. In PKCS #8 format, the curve is part of the PrivateKeyAlgorithmIdentifier. @@ -1065,6 +976,10 @@ def from_der(cls, string, hashfunc=sha1): :param string: binary string with DER-encoded private ECDSA key :type string: bytes like object + :param valid_curve_encodings: list of allowed encoding formats + for curve parameters. By default (``None``) all are supported: + ``named_curve`` and ``explicit``. + :type valid_curve_encodings: :term:`set-like object` :raises MalformedPointError: if the length of encoding doesn't match the provided curve or the encoded values is too large @@ -1099,8 +1014,7 @@ def from_der(cls, string, hashfunc=sha1): sequence, s = der.remove_sequence(s) algorithm_oid, algorithm_identifier = der.remove_object(sequence) - curve_oid, empty = der.remove_object(algorithm_identifier) - curve = find_curve(curve_oid) + curve = Curve.from_der(algorithm_identifier, valid_curve_encodings) if algorithm_oid not in (oid_ecPublicKey, oid_ecDH, oid_ecMQV): raise der.UnexpectedDER( @@ -1142,13 +1056,7 @@ def from_der(cls, string, hashfunc=sha1): raise der.UnexpectedDER( "expected tag 0 in DER privkey, got %d" % tag ) - curve_oid, empty = der.remove_object(curve_oid_str) - if empty != b(""): - raise der.UnexpectedDER( - "trailing junk after DER privkey " - "curve_oid: %s" % binascii.hexlify(empty) - ) - curve = find_curve(curve_oid) + curve = Curve.from_der(curve_oid_str, valid_curve_encodings) # we don't actually care about the following fields # @@ -1184,7 +1092,12 @@ def to_string(self): s = number_to_string(secexp, self.privkey.order) return s - def to_pem(self, point_encoding="uncompressed", format="ssleay"): + def to_pem( + self, + point_encoding="uncompressed", + format="ssleay", + curve_parameters_encoding=None, + ): """ Convert the private key to the :term:`PEM` format. @@ -1198,6 +1111,11 @@ def to_pem(self, point_encoding="uncompressed", format="ssleay"): :param str point_encoding: format to use for encoding public point :param str format: either ``ssleay`` (default) or ``pkcs8`` + :param str curve_parameters_encoding: format of encoded curve + parameters, default depends on the curve, if the curve has + an associated OID, ``named_curve`` format will be used, + if no OID is associated with the curve, the fallback of + ``explicit`` parameters will be used. :return: PEM encoded private key :rtype: bytes @@ -1208,9 +1126,17 @@ def to_pem(self, point_encoding="uncompressed", format="ssleay"): # TODO: "BEGIN ECPARAMETERS" assert format in ("ssleay", "pkcs8") header = "EC PRIVATE KEY" if format == "ssleay" else "PRIVATE KEY" - return der.topem(self.to_der(point_encoding, format), header) + return der.topem( + self.to_der(point_encoding, format, curve_parameters_encoding), + header, + ) - def to_der(self, point_encoding="uncompressed", format="ssleay"): + def to_der( + self, + point_encoding="uncompressed", + format="ssleay", + curve_parameters_encoding=None, + ): """ Convert the private key to the :term:`DER` format. @@ -1221,6 +1147,11 @@ def to_der(self, point_encoding="uncompressed", format="ssleay"): :param str point_encoding: format to use for encoding public point :param str format: either ``ssleay`` (default) or ``pkcs8`` + :param str curve_parameters_encoding: format of encoded curve + parameters, default depends on the curve, if the curve has + an associated OID, ``named_curve`` format will be used, + if no OID is associated with the curve, the fallback of + ``explicit`` parameters will be used. :return: DER encoded private key :rtype: bytes @@ -1231,14 +1162,22 @@ def to_der(self, point_encoding="uncompressed", format="ssleay"): raise ValueError("raw encoding not allowed in DER") assert format in ("ssleay", "pkcs8") encoded_vk = self.get_verifying_key().to_string(point_encoding) - # the 0 in encode_bitstring specifies the number of unused bits - # in the `encoded_vk` string - ec_private_key = der.encode_sequence( + priv_key_elems = [ der.encode_integer(1), der.encode_octet_string(self.to_string()), - der.encode_constructed(0, self.curve.encoded_oid), - der.encode_constructed(1, der.encode_bitstring(encoded_vk, 0)), + ] + if format == "ssleay": + priv_key_elems.append( + der.encode_constructed( + 0, self.curve.to_der(curve_parameters_encoding) + ) + ) + # the 0 in encode_bitstring specifies the number of unused bits + # in the `encoded_vk` string + priv_key_elems.append( + der.encode_constructed(1, der.encode_bitstring(encoded_vk, 0)) ) + ec_private_key = der.encode_sequence(*priv_key_elems) if format == "ssleay": return ec_private_key @@ -1248,7 +1187,8 @@ def to_der(self, point_encoding="uncompressed", format="ssleay"): # top-level structure. der.encode_integer(1), der.encode_sequence( - der.encode_oid(*oid_ecPublicKey), self.curve.encoded_oid + der.encode_oid(*oid_ecPublicKey), + self.curve.to_der(curve_parameters_encoding), ), der.encode_octet_string(ec_private_key), ) diff --git a/src/ecdsa/test_curves.py b/src/ecdsa/test_curves.py new file mode 100644 index 00000000..69990c9a --- /dev/null +++ b/src/ecdsa/test_curves.py @@ -0,0 +1,310 @@ +try: + import unittest2 as unittest +except ImportError: + import unittest + +import base64 +import pytest +from .curves import Curve, NIST256p, curves, UnknownCurveError, PRIME_FIELD_OID +from .ellipticcurve import CurveFp, PointJacobi +from . import der +from .util import number_to_string + + +class TestParameterEncoding(unittest.TestCase): + @classmethod + def setUpClass(cls): + # minimal, but with cofactor (excludes seed when compared to + # OpenSSL output) + cls.base64_params = ( + "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////" + "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K" + "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd" + "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1" + "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=" + ) + + def test_from_pem(self): + pem_params = ( + "-----BEGIN EC PARAMETERS-----\n" + "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////\n" + "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K\n" + "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd\n" + "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1\n" + "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=\n" + "-----END EC PARAMETERS-----\n" + ) + curve = Curve.from_pem(pem_params) + + self.assertIs(curve, NIST256p) + + def test_from_pem_with_explicit_when_explicit_disabled(self): + pem_params = ( + "-----BEGIN EC PARAMETERS-----\n" + "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////\n" + "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K\n" + "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd\n" + "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1\n" + "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=\n" + "-----END EC PARAMETERS-----\n" + ) + with self.assertRaises(der.UnexpectedDER) as e: + Curve.from_pem(pem_params, ["named_curve"]) + + self.assertIn("explicit curve parameters not", str(e.exception)) + + def test_from_pem_with_named_curve_with_named_curve_disabled(self): + pem_params = ( + "-----BEGIN EC PARAMETERS-----\n" + "BggqhkjOPQMBBw==\n" + "-----END EC PARAMETERS-----\n" + ) + with self.assertRaises(der.UnexpectedDER) as e: + Curve.from_pem(pem_params, ["explicit"]) + + self.assertIn("named_curve curve parameters not", str(e.exception)) + + def test_from_pem_with_wrong_header(self): + pem_params = ( + "-----BEGIN PARAMETERS-----\n" + "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////\n" + "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K\n" + "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd\n" + "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1\n" + "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=\n" + "-----END PARAMETERS-----\n" + ) + with self.assertRaises(der.UnexpectedDER) as e: + Curve.from_pem(pem_params) + + self.assertIn("PARAMETERS PEM header", str(e.exception)) + + def test_to_pem(self): + pem_params = ( + b"-----BEGIN EC PARAMETERS-----\n" + b"BggqhkjOPQMBBw==\n" + b"-----END EC PARAMETERS-----\n" + ) + encoding = NIST256p.to_pem() + + self.assertEqual(pem_params, encoding) + + def test_compare_with_different_object(self): + self.assertNotEqual(NIST256p, 256) + + def test_named_curve_params_der(self): + encoded = NIST256p.to_der() + + # just the encoding of the NIST256p OID (prime256v1) + self.assertEqual(b"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07", encoded) + + def test_verify_that_default_is_named_curve_der(self): + encoded_default = NIST256p.to_der() + encoded_named = NIST256p.to_der("named_curve") + + self.assertEqual(encoded_default, encoded_named) + + def test_encoding_to_explicit_params(self): + encoded = NIST256p.to_der("explicit") + + self.assertEqual(encoded, bytes(base64.b64decode(self.base64_params))) + + def test_encoding_to_explicit_compressed_params(self): + encoded = NIST256p.to_der("explicit", "compressed") + + compressed_base_point = ( + "MIHAAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////" + "/////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6" + "k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEIQNrF9Hy4SxCR/i85uVjpEDydwN9" + "gS3rM6D0oTlF2JjClgIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVR" + "AgEB" + ) + + self.assertEqual( + encoded, bytes(base64.b64decode(compressed_base_point)) + ) + + def test_decoding_explicit_from_openssl(self): + # generated with openssl 1.1.1k using + # openssl ecparam -name P-256 -param_enc explicit -out /tmp/file.pem + p256_explicit = ( + "MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////" + "/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6" + "k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+" + "kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK" + "fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz" + "ucrC/GMlUQIBAQ==" + ) + + decoded = Curve.from_der(bytes(base64.b64decode(p256_explicit))) + + self.assertEqual(NIST256p, decoded) + + def test_decoding_well_known_from_explicit_params(self): + curve = Curve.from_der(bytes(base64.b64decode(self.base64_params))) + + self.assertIs(curve, NIST256p) + + def test_decoding_with_incorrect_valid_encodings(self): + with self.assertRaises(ValueError) as e: + Curve.from_der(b"", ["explicitCA"]) + + self.assertIn("Only named_curve", str(e.exception)) + + def test_compare_curves_with_different_generators(self): + curve_fp = CurveFp(23, 1, 7) + base_a = PointJacobi(curve_fp, 13, 3, 1, 9, generator=True) + base_b = PointJacobi(curve_fp, 1, 20, 1, 9, generator=True) + + curve_a = Curve("unknown", curve_fp, base_a, None) + curve_b = Curve("unknown", curve_fp, base_b, None) + + self.assertNotEqual(curve_a, curve_b) + + def test_default_encode_for_custom_curve(self): + curve_fp = CurveFp(23, 1, 7) + base_point = PointJacobi(curve_fp, 13, 3, 1, 9, generator=True) + + curve = Curve("unknown", curve_fp, base_point, None) + + encoded = curve.to_der() + + decoded = Curve.from_der(encoded) + + self.assertEqual(curve, decoded) + + expected = "MCECAQEwDAYHKoZIzj0BAQIBFzAGBAEBBAEHBAMEDQMCAQk=" + + self.assertEqual(encoded, bytes(base64.b64decode(expected))) + + def test_named_curve_encode_for_custom_curve(self): + curve_fp = CurveFp(23, 1, 7) + base_point = PointJacobi(curve_fp, 13, 3, 1, 9, generator=True) + + curve = Curve("unknown", curve_fp, base_point, None) + + with self.assertRaises(UnknownCurveError) as e: + curve.to_der("named_curve") + + self.assertIn("Can't encode curve", str(e.exception)) + + def test_try_decoding_binary_explicit(self): + sect113r1_explicit = ( + "MIGRAgEBMBwGByqGSM49AQIwEQIBcQYJKoZIzj0BAgMCAgEJMDkEDwAwiCUMpufH" + "/mSc6Fgg9wQPAOi+5NPiJgdEGIvg6ccjAxUAEOcjqxTWluZ2h1YVF1b+v4/LSakE" + "HwQAnXNhbzX0qxQH1zViwQ8ApSgwJ3lY7oTRMV7TGIYCDwEAAAAAAAAA2czsijnl" + "bwIBAg==" + ) + + with self.assertRaises(UnknownCurveError) as e: + Curve.from_der(base64.b64decode(sect113r1_explicit)) + + self.assertIn("Characteristic 2 curves unsupported", str(e.exception)) + + def test_decode_malformed_named_curve(self): + bad_der = der.encode_oid(*NIST256p.oid) + der.encode_integer(1) + + with self.assertRaises(der.UnexpectedDER) as e: + Curve.from_der(bad_der) + + self.assertIn("Unexpected data after OID", str(e.exception)) + + def test_decode_malformed_explicit_garbage_after_ECParam(self): + bad_der = bytes( + base64.b64decode(self.base64_params) + ) + der.encode_integer(1) + + with self.assertRaises(der.UnexpectedDER) as e: + Curve.from_der(bad_der) + + self.assertIn("Unexpected data after ECParameters", str(e.exception)) + + def test_decode_malformed_unknown_version_number(self): + bad_der = der.encode_sequence(der.encode_integer(2)) + + with self.assertRaises(der.UnexpectedDER) as e: + Curve.from_der(bad_der) + + self.assertIn("Unknown parameter encoding format", str(e.exception)) + + def test_decode_malformed_unknown_field_type(self): + curve_p = NIST256p.curve.p() + bad_der = der.encode_sequence( + der.encode_integer(1), + der.encode_sequence( + der.encode_oid(1, 2, 3), der.encode_integer(curve_p) + ), + der.encode_sequence( + der.encode_octet_string( + number_to_string(NIST256p.curve.a() % curve_p, curve_p) + ), + der.encode_octet_string( + number_to_string(NIST256p.curve.b(), curve_p) + ), + ), + der.encode_octet_string( + NIST256p.generator.to_bytes("uncompressed") + ), + der.encode_integer(NIST256p.generator.order()), + ) + + with self.assertRaises(UnknownCurveError) as e: + Curve.from_der(bad_der) + + self.assertIn("Unknown field type: (1, 2, 3)", str(e.exception)) + + def test_decode_malformed_garbage_after_prime(self): + curve_p = NIST256p.curve.p() + bad_der = der.encode_sequence( + der.encode_integer(1), + der.encode_sequence( + der.encode_oid(*PRIME_FIELD_OID), + der.encode_integer(curve_p), + der.encode_integer(1), + ), + der.encode_sequence( + der.encode_octet_string( + number_to_string(NIST256p.curve.a() % curve_p, curve_p) + ), + der.encode_octet_string( + number_to_string(NIST256p.curve.b(), curve_p) + ), + ), + der.encode_octet_string( + NIST256p.generator.to_bytes("uncompressed") + ), + der.encode_integer(NIST256p.generator.order()), + ) + + with self.assertRaises(der.UnexpectedDER) as e: + Curve.from_der(bad_der) + + self.assertIn("Prime-p element", str(e.exception)) + + +@pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) +def test_curve_params_encode_decode_named(curve): + ret = Curve.from_der(curve.to_der("named_curve")) + + assert curve == ret + + +@pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) +def test_curve_params_encode_decode_explicit(curve): + ret = Curve.from_der(curve.to_der("explicit")) + + assert curve == ret + + +@pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) +def test_curve_params_encode_decode_default(curve): + ret = Curve.from_der(curve.to_der()) + + assert curve == ret + + +@pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) +def test_curve_params_encode_decode_explicit_compressed(curve): + ret = Curve.from_der(curve.to_der("explicit", "compressed")) + + assert curve == ret diff --git a/src/ecdsa/test_keys.py b/src/ecdsa/test_keys.py index 31353f45..0cf7cc9a 100644 --- a/src/ecdsa/test_keys.py +++ b/src/ecdsa/test_keys.py @@ -14,7 +14,7 @@ import hashlib from .keys import VerifyingKey, SigningKey, MalformedPointError -from .der import unpem +from .der import unpem, UnexpectedDER from .util import ( sigencode_string, sigencode_der, @@ -153,6 +153,47 @@ def setUpClass(cls): cls.sk2 = SigningKey.generate(vk.curve) + def test_load_key_with_explicit_parameters(self): + pub_key_str = ( + "-----BEGIN PUBLIC KEY-----\n" + "MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA\n" + "AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////\n" + "///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd\n" + "NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5\n" + "RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA\n" + "//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABIr1UkgYs5jmbFc7it1/YI2X\n" + "T//IlaEjMNZft1owjqpBYH2ErJHk4U5Pp4WvWq1xmHwIZlsH7Ig4KmefCfR6SmU=\n" + "-----END PUBLIC KEY-----" + ) + pk = VerifyingKey.from_pem(pub_key_str) + + pk_exp = VerifyingKey.from_string( + b"\x04\x8a\xf5\x52\x48\x18\xb3\x98\xe6\x6c\x57\x3b\x8a\xdd\x7f" + b"\x60\x8d\x97\x4f\xff\xc8\x95\xa1\x23\x30\xd6\x5f\xb7\x5a\x30" + b"\x8e\xaa\x41\x60\x7d\x84\xac\x91\xe4\xe1\x4e\x4f\xa7\x85\xaf" + b"\x5a\xad\x71\x98\x7c\x08\x66\x5b\x07\xec\x88\x38\x2a\x67\x9f" + b"\x09\xf4\x7a\x4a\x65", + curve=NIST256p, + ) + self.assertEqual(pk, pk_exp) + + def test_load_key_with_explicit_with_explicit_disabled(self): + pub_key_str = ( + "-----BEGIN PUBLIC KEY-----\n" + "MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA\n" + "AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////\n" + "///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd\n" + "NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5\n" + "RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA\n" + "//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABIr1UkgYs5jmbFc7it1/YI2X\n" + "T//IlaEjMNZft1owjqpBYH2ErJHk4U5Pp4WvWq1xmHwIZlsH7Ig4KmefCfR6SmU=\n" + "-----END PUBLIC KEY-----" + ) + with self.assertRaises(UnexpectedDER): + VerifyingKey.from_pem( + pub_key_str, valid_curve_encodings=["named_curve"] + ) + def test_load_key_with_disabled_format(self): with self.assertRaises(MalformedPointError) as e: VerifyingKey.from_der(self.key_bytes, valid_encodings=["raw"]) @@ -263,6 +304,50 @@ def setUpClass(cls): ) cls.sk2 = SigningKey.from_pem(prv_key_str) + def test_decoding_explicit_curve_parameters(self): + prv_key_str = ( + "-----BEGIN PRIVATE KEY-----\n" + "MIIBeQIBADCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAAB\n" + "AAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA\n" + "///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMV\n" + "AMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg\n" + "9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8A\n" + "AAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBBG0wawIBAQQgIXtREfUmR16r\n" + "ZbmvDGD2lAEFPZa2DLPyz0czSja58yChRANCAASK9VJIGLOY5mxXO4rdf2CNl0//\n" + "yJWhIzDWX7daMI6qQWB9hKyR5OFOT6eFr1qtcZh8CGZbB+yIOCpnnwn0ekpl\n" + "-----END PRIVATE KEY-----\n" + ) + + sk = SigningKey.from_pem(prv_key_str) + + sk2 = SigningKey.from_string( + b"\x21\x7b\x51\x11\xf5\x26\x47\x5e\xab\x65\xb9\xaf\x0c\x60\xf6" + b"\x94\x01\x05\x3d\x96\xb6\x0c\xb3\xf2\xcf\x47\x33\x4a\x36\xb9" + b"\xf3\x20", + curve=NIST256p, + ) + + self.assertEqual(sk, sk2) + + def test_decoding_explicit_curve_parameters_with_explicit_disabled(self): + prv_key_str = ( + "-----BEGIN PRIVATE KEY-----\n" + "MIIBeQIBADCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAAB\n" + "AAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA\n" + "///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMV\n" + "AMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg\n" + "9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8A\n" + "AAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBBG0wawIBAQQgIXtREfUmR16r\n" + "ZbmvDGD2lAEFPZa2DLPyz0czSja58yChRANCAASK9VJIGLOY5mxXO4rdf2CNl0//\n" + "yJWhIzDWX7daMI6qQWB9hKyR5OFOT6eFr1qtcZh8CGZbB+yIOCpnnwn0ekpl\n" + "-----END PRIVATE KEY-----\n" + ) + + with self.assertRaises(UnexpectedDER): + SigningKey.from_pem( + prv_key_str, valid_curve_encodings=["named_curve"] + ) + def test_equality_on_signing_keys(self): sk = SigningKey.from_secret_exponent( self.sk1.privkey.secret_multiplier, self.sk1.curve diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index d1418c01..c1393bcc 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -745,6 +745,12 @@ def test_raw_decoding_with_blocked_format(self): self.assertIn("hybrid", str(exp.exception)) + def test_decoding_with_unknown_format(self): + with self.assertRaises(ValueError) as e: + VerifyingKey.from_string(b"", valid_encodings=("raw", "foobar")) + + self.assertIn("Only uncompressed, compressed", str(e.exception)) + def test_uncompressed_decoding_with_blocked_format(self): enc = b( "\x04" @@ -1313,6 +1319,17 @@ def do_test_to_openssl(self, curve, hash_name="SHA1"): % mdarg ) + with open("t/privkey-explicit.pem", "wb") as e: + e.write(sk.to_pem(curve_parameters_encoding="explicit")) + run_openssl( + "dgst %s -sign t/privkey-explicit.pem -out t/data.sig2 t/data.txt" + % mdarg + ) + run_openssl( + "dgst %s -verify t/pubkey.pem -signature t/data.sig2 t/data.txt" + % mdarg + ) + with open("t/privkey-p8.pem", "wb") as e: e.write(sk.to_pem(format="pkcs8")) run_openssl( @@ -1324,6 +1341,19 @@ def do_test_to_openssl(self, curve, hash_name="SHA1"): % mdarg ) + with open("t/privkey-p8-explicit.pem", "wb") as e: + e.write( + sk.to_pem(format="pkcs8", curve_parameters_encoding="explicit") + ) + run_openssl( + "dgst %s -sign t/privkey-p8-explicit.pem -out t/data.sig3 t/data.txt" + % mdarg + ) + run_openssl( + "dgst %s -verify t/pubkey.pem -signature t/data.sig3 t/data.txt" + % mdarg + ) + class TooSmallCurve(unittest.TestCase): OPENSSL_SUPPORTED_CURVES = set( diff --git a/tox.ini b/tox.ini index 742f3fbb..3ae7bd70 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ deps = py{33}: hypothesis<3.44 py{26}: unittest2 py{26}: hypothesis<3 + py{34}: attrs<21 py{26,27,34,35,36,37,38,39,py,py3}: pytest py{27,34,35,36,37,38,39,py,py3}: hypothesis gmpy2py{27,39}: gmpy2 @@ -96,6 +97,13 @@ commands = flake8 setup.py speed.py src black --check --line-length 79 . +[testenv:codeformat] +basepython = python3 +deps = + black==19.10b0 +commands = + black --line-length 79 . + [flake8] exclude = src/ecdsa/test*.py # We're just getting started. For now, ignore the following problems: