From 88d80b8b6e1c92cb554149005bd83c26903071e6 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Fri, 27 Sep 2019 20:12:24 +0200 Subject: [PATCH 1/3] documentation for VerifyingKey object also do minor cleanup with initialisers and imports --- src/ecdsa/keys.py | 409 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 375 insertions(+), 34 deletions(-) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 979473fd..ff138a14 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -1,5 +1,60 @@ -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 + :term:`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 :term:`uncompressed` and :term:`compressed` encodings. + Both x and y coordinates are 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 ASN.1 objects + deterministically and uniquely into byte strings. +""" +import binascii +from hashlib import sha1 +from six import PY3, b from . import ecdsa from . import der from . import rfc6979 @@ -10,8 +65,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,26 +72,75 @@ 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 ecdsa.curves.Curve 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 + :vartype default_hashfunc: callable + :ivar pubkey: the actual public key + :vartype pubkey: ecdsa.ecdsa.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): + """ + 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 + + :return: Initialised VerifyingKey object + :rtype: VerifyingKey + """ + self = cls(_error__please_use_generate=True) self.curve = curve self.default_hashfunc = hashfunc self.pubkey = ecdsa.Public_key(curve.generator, point) @@ -47,6 +149,12 @@ def from_public_point(klass, point, curve=NIST192p, hashfunc=sha1): @staticmethod def _from_raw_encoding(string, curve, validate_point): + """ + 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 @@ -65,6 +173,7 @@ def _from_raw_encoding(string, curve, validate_point): @staticmethod def _from_compressed(string, curve, validate_point): + """Decode public point from compressed encoding.""" if string[:1] not in (b('\x02'), b('\x03')): raise MalformedPointError("Malformed compressed point encoding") @@ -88,6 +197,7 @@ def _from_compressed(string, curve, validate_point): @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')) @@ -103,36 +213,105 @@ 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 + + :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, validate_point) 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, + validate_point) 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, validate_point) 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) @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 :func:`~VerifyingKey.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(""): @@ -156,24 +335,73 @@ def from_der(klass, string): # raw encoding of point is invalid in DER files if len(point_str) == curve.verifying_key_length: raise der.UnexpectedDER("Malformed encoding of public point") - return klass.from_string(point_str, curve) + return cls.from_string(point_str, 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 :func:`ecdsa.util.sigdecode_string` and + :func:`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 :func:`ecdsa.util.sigdecode_string` and + :func:`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) @@ -182,16 +410,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: @@ -200,6 +431,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 @@ -207,9 +439,25 @@ 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 :func:`~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: :term:`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() @@ -220,10 +468,42 @@ 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 + :func:`~VerifyingKey.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, it is as big as "uncompressed". + + :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 + :func:`~VerifyingKey.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, it is as big as "uncompressed". + + :return: DER encoding of the public key + :rtype: bytes + """ if point_encoding == "raw": raise ValueError("raw point_encoding not allowed in DER") point_str = self.to_string(point_encoding) @@ -233,12 +513,73 @@ def to_der(self, point_encoding="uncompressed"): # bit string der.encode_bitstring(point_str, 0)) - 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 :term:`raw encoding`. Can also be used + to verify signatures in ASN.1 DER encoding by using + :func:`ecdsa.util.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 :func:`ecdsa.util.sigdecode_string` and + :func:`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 :term:`raw encoding`. Can also be used + to verify signatures in ASN.1 DER encoding by using + :func:`ecdsa.util.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 :func:`ecdsa.util.sigdecode_string` and + :func:`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, @@ -254,7 +595,7 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string): raise BadSignatureError("Signature verification failed") -class SigningKey: +class SigningKey(object): def __init__(self, _error__please_use_generate=None): if not _error__please_use_generate: raise TypeError("Please use SigningKey.generate() to construct me") From 4c63e5bbbf262bd2234522b5d79e98d7e50dcc94 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 28 Sep 2019 00:47:24 +0200 Subject: [PATCH 2/3] SigningKey documentation use canonical name for first parameter in classmethods minor fixes with formatting --- src/ecdsa/keys.py | 418 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 376 insertions(+), 42 deletions(-) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index ff138a14..f0f24953 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -596,23 +596,78 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string): class SigningKey(object): + """ + Class for handling keys that can create signatures (private keys). + + :ivar ecdsa.curves.Curve 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 int baselen: the length of a :term:`raw encoding` of private key + :ivar ecdsa.keys.VerifyingKey verifying_key: the public key + associated with this private key + :ivar ecdsa.ecdsa.Private_key privkey: 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 + :func:`~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 @@ -631,27 +686,123 @@ def from_secret_exponent(klass, secexp, curve=NIST192p, hashfunc=sha1): 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 :func:`~SigningKey.from_der` for ASN.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" % @@ -685,18 +836,56 @@ 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 :func:`~SigningKey.from_pem` method for format description. + + Only the named curve format is supported. + The public key will be included in generated string. + + The PEM header will specify ``BEGIN EC PRIVATE KEY`` + + :param str point_encoding: format to use for encoding public point + + :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 :func:`~SigningKey.from_der` method for format specification. + + Only the named curve format is supported. + The public key will be included in the generated string. + + :param str point_encoding: format to use for encoding public point + + :return: DER encoded private key + :rtype: bytes + """ # SEQ([int(1), octetstring(privkey),cont[0], oid(secp224r1), # cont[1],bitstring]) if point_encoding == "raw": @@ -712,11 +901,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() @@ -728,9 +956,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 @@ -750,24 +1005,90 @@ 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 :func:`~SigningKey.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 an 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 + :func:`~SigningKey.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 an 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, @@ -777,15 +1098,28 @@ 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 + :func:`~SigningKey.sign_deterministic` or + :func:`~SigningKey.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: From 81391a0f4d7716bf58e72fa214ad7fb8a2091f6e Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 28 Sep 2019 00:43:33 +0200 Subject: [PATCH 3/3] docs for signature (de)coder functions --- src/ecdsa/keys.py | 7 ++- src/ecdsa/util.py | 130 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 124 insertions(+), 13 deletions(-) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index f0f24953..4f926429 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -48,8 +48,13 @@ comments. The actual object stored is base64 encoded. DER - Distinguished Encoding Rules, the way to encode ASN.1 objects + 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 diff --git a/src/ecdsa/util.py b/src/ecdsa/util.py index 18b26ea1..d229ed25 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,20 @@ def sigencode_strings(r, s, order): def sigencode_string(r, s, order): + """ + Encode the signature to raw format (:term:`raw encoding`) + + It's expected that this function will be used as a `sigencode=` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param int r: first parameter of the signature + :param int s: second parameter of the signature + :param int order: the order of the curve over which the signature was + computed + + :return: raw 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 +233,27 @@ def sigencode_string(r, s, order): def sigencode_der(r, s, order): + """ + Encode the signature into the ECDSA-Sig-Value structure using :term:`DER`. + + Encodes the signature to the following :term:`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 :func:`ecdsa.keys.SigningKey.sign` method. + + :param int r: first parameter of the signature + :param int s: second parameter of the signature + :param int order: the order of the 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 +279,115 @@ 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. Either + because the byte strings have incorrect lengths or because the encoded + values are too large. + """ + pass def sigdecode_string(signature, order): + """ + Decoder for :term:`raw encoding` 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 the `sigdecode=` + parameter to the :func:`ecdsa.keys.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 be a big endian encoding of 'r', second needs to + be a big endian encoding of the 's' parameter of an ECDSA signature. + + It's expected that this function will be used as the `sigdecode=` + parameter to the :func:`ecdsa.keys.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 :func:`ecdsa.keys.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(""):