diff --git a/.travis.yml b/.travis.yml index f2cbba52..1b15378e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,7 +60,7 @@ before_install: TRAVIS_COMMIT_RANGE=$PR_FIRST^..$TRAVIS_COMMIT fi # sanity check current commit - - git rev-parse HEAD + - BRANCH=$(git rev-parse HEAD) - echo "TRAVIS_COMMIT_RANGE=$TRAVIS_COMMIT_RANGE" - git fetch origin master:refs/remotes/origin/master @@ -78,7 +78,6 @@ install: script: - if [[ $TOX_ENV ]]; then tox -e $TOX_ENV; fi - tox -e speed - - cp diff-instrumental.py diff-instrumental-2.py - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then git checkout $PR_FIRST^ @@ -86,8 +85,9 @@ script: files="$(ls src/ecdsa/test*.py | grep -v test_malformed_sigs.py)" instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` $files instrumental -f .instrumental.cov -s - instrumental -f .instrumental.cov -s | python diff-instrumental-2.py --save .diff-instrumental - git checkout $TRAVIS_COMMIT + instrumental -f .instrumental.cov -s | python diff-instrumental.py --save .diff-instrumental + git checkout $BRANCH + files="$(ls src/ecdsa/test*.py | grep -v test_malformed_sigs.py)" instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` $files instrumental -f .instrumental.cov -sr fi @@ -98,11 +98,11 @@ script: instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` $files instrumental -f .instrumental.cov -s # just log the values when merging - instrumental -f .instrumental.cov -s | python diff-instrumental-2.py + instrumental -f .instrumental.cov -s | python diff-instrumental.py fi - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then - instrumental -f .instrumental.cov -s | python diff-instrumental-2.py --read .diff-instrumental --fail-under 70 --max-difference -0.1 + instrumental -f .instrumental.cov -s | python diff-instrumental.py --read .diff-instrumental --fail-under 70 --max-difference -0.1 fi after_success: - if [[ -z $INSTRUMENTAL ]]; then coveralls; fi diff --git a/src/ecdsa/_compat.py b/src/ecdsa/_compat.py new file mode 100644 index 00000000..143c867e --- /dev/null +++ b/src/ecdsa/_compat.py @@ -0,0 +1,38 @@ +""" +Common functions for providing cross-python version compatibility. +""" +import sys +from six import integer_types + + +def str_idx_as_int(string, index): + """Take index'th byte from string, return as integer""" + val = string[index] + if isinstance(val, integer_types): + return val + return ord(val) + + +if sys.version_info < (3, 0): + def normalise_bytes(buffer_object): + """Cast the input into array of bytes.""" + return buffer(buffer_object) + + def hmac_compat(ret): + return ret + +else: + if sys.version_info < (3, 4): + # on python 3.3 hmac.hmac.update() accepts only bytes, on newer + # versions it does accept memoryview() also + def hmac_compat(data): + if not isinstance(data, bytes): + return bytes(data) + return data + else: + def hmac_compat(data): + return data + + def normalise_bytes(buffer_object): + """Cast the input into array of bytes.""" + return memoryview(buffer_object).cast('B') diff --git a/src/ecdsa/der.py b/src/ecdsa/der.py index 516451ca..fb2a7ad2 100644 --- a/src/ecdsa/der.py +++ b/src/ecdsa/der.py @@ -3,7 +3,8 @@ import binascii import base64 import warnings -from six import int2byte, b, integer_types, text_type +from six import int2byte, b, text_type +from ._compat import str_idx_as_int class UnexpectedDER(Exception): @@ -20,7 +21,7 @@ def encode_integer(r): if len(h) % 2: h = b("0") + h s = binascii.unhexlify(h) - num = s[0] if isinstance(s[0], integer_types) else ord(s[0]) + num = str_idx_as_int(s, 0) if num <= 0x7f: return b("\x02") + int2byte(len(s)) + s else: @@ -83,7 +84,7 @@ def encode_bitstring(s, unused=_sentry): if unused: if not s: raise ValueError("unused is non-zero but s is empty") - last = s[-1] if isinstance(s[-1], integer_types) else ord(s[-1]) + last = str_idx_as_int(s, -1) if last & (2 ** unused - 1): raise ValueError("unused bits must be zeros in DER") encoded_unused = int2byte(unused) @@ -121,7 +122,7 @@ def encode_number(n): def remove_constructed(string): - s0 = string[0] if isinstance(string[0], integer_types) else ord(string[0]) + s0 = str_idx_as_int(string, 0) if (s0 & 0xe0) != 0xa0: raise UnexpectedDER("wanted type 'constructed tag' (0xa0-0xbf), " "got 0x%02x" % s0) @@ -135,9 +136,8 @@ def remove_constructed(string): def remove_sequence(string): if not string: raise UnexpectedDER("Empty string does not encode a sequence") - if not string.startswith(b("\x30")): - n = string[0] if isinstance(string[0], integer_types) else \ - ord(string[0]) + if string[:1] != b"\x30": + n = str_idx_as_int(string, 0) raise UnexpectedDER("wanted type 'sequence' (0x30), got 0x%02x" % n) length, lengthlength = read_length(string[1:]) if length > len(string) - 1 - lengthlength: @@ -147,8 +147,8 @@ def remove_sequence(string): def remove_octet_string(string): - if not string.startswith(b("\x04")): - n = string[0] if isinstance(string[0], integer_types) else ord(string[0]) + if string[:1] != b"\x04": + n = str_idx_as_int(string, 0) raise UnexpectedDER("wanted type 'octetstring' (0x04), got 0x%02x" % n) length, llen = read_length(string[1:]) body = string[1+llen:1+llen+length] @@ -157,8 +157,8 @@ def remove_octet_string(string): def remove_object(string): - if not string.startswith(b("\x06")): - n = string[0] if isinstance(string[0], integer_types) else ord(string[0]) + if string[:1] != b"\x06": + n = str_idx_as_int(string, 0) raise UnexpectedDER("wanted type 'object' (0x06), got 0x%02x" % n) length, lengthlength = read_length(string[1:]) body = string[1+lengthlength:1+lengthlength+length] @@ -180,9 +180,8 @@ def remove_integer(string): if not string: raise UnexpectedDER("Empty string is an invalid encoding of an " "integer") - if not string.startswith(b("\x02")): - n = string[0] if isinstance(string[0], integer_types) \ - else ord(string[0]) + if string[:1] != b"\x02": + n = str_idx_as_int(string, 0) raise UnexpectedDER("wanted type 'integer' (0x02), got 0x%02x" % n) length, llen = read_length(string[1:]) if length > len(string) - 1 - llen: @@ -191,16 +190,14 @@ def remove_integer(string): raise UnexpectedDER("0-byte long encoding of integer") numberbytes = string[1+llen:1+llen+length] rest = string[1+llen+length:] - msb = numberbytes[0] if isinstance(numberbytes[0], integer_types) \ - else ord(numberbytes[0]) + msb = str_idx_as_int(numberbytes, 0) if not msb < 0x80: raise UnexpectedDER("Negative integers are not supported") # check if the encoding is the minimal one (DER requirement) if length > 1 and not msb: # leading zero byte is allowed if the integer would have been # considered a negative number otherwise - smsb = numberbytes[1] if isinstance(numberbytes[1], integer_types) \ - else ord(numberbytes[1]) + smsb = str_idx_as_int(numberbytes, 1) if smsb < 0x80: raise UnexpectedDER("Invalid encoding of integer, unnecessary " "zero padding bytes") @@ -215,7 +212,7 @@ def read_number(string): if llen > len(string): raise UnexpectedDER("ran out of length bytes") number = number << 7 - d = string[llen] if isinstance(string[llen], integer_types) else ord(string[llen]) + d = str_idx_as_int(string, llen) number += (d & 0x7f) llen += 1 if not d & 0x80: @@ -238,7 +235,7 @@ def encode_length(l): def read_length(string): if not string: raise UnexpectedDER("Empty string can't encode valid length value") - num = string[0] if isinstance(string[0], integer_types) else ord(string[0]) + num = str_idx_as_int(string, 0) if not (num & 0x80): # short form return (num & 0x7f), 1 @@ -250,7 +247,7 @@ def read_length(string): if llen > len(string)-1: raise UnexpectedDER("Length of length longer than provided buffer") # verify that the encoding is minimal possible (DER requirement) - msb = string[1] if isinstance(string[1], integer_types) else ord(string[1]) + msb = str_idx_as_int(string, 1) if not msb or llen == 1 and msb < 0x80: raise UnexpectedDER("Not minimal encoding of length") return int(binascii.hexlify(string[1:1+llen]), 16), 1+llen @@ -301,8 +298,8 @@ def remove_bitstring(string, expect_unused=_sentry): warnings.warn("Legacy call convention used, expect_unused= needs to be" " specified", DeprecationWarning) - num = string[0] if isinstance(string[0], integer_types) else ord(string[0]) - if not string.startswith(b("\x03")): + num = str_idx_as_int(string, 0) + if string[:1] != b"\x03": raise UnexpectedDER("wanted bitstring (0x03), got 0x%02x" % num) length, llen = read_length(string[1:]) if not length: @@ -310,8 +307,7 @@ def remove_bitstring(string, expect_unused=_sentry): body = string[1+llen:1+llen+length] rest = string[1+llen+length:] if expect_unused is not _sentry: - unused = body[0] if isinstance(body[0], integer_types) \ - else ord(body[0]) + unused = str_idx_as_int(body, 0) if not 0 <= unused <= 7: raise UnexpectedDER("Invalid encoding of unused bits") if expect_unused is not None and expect_unused != unused: @@ -320,8 +316,7 @@ def remove_bitstring(string, expect_unused=_sentry): if unused: if not body: raise UnexpectedDER("Invalid encoding of empty bit string") - last = body[-1] if isinstance(body[-1], integer_types) else \ - ord(body[-1]) + last = str_idx_as_int(body, -1) # verify that all the unused bits are set to zero (DER requirement) if last & (2 ** unused - 1): raise UnexpectedDER("Non zero padding bits in bit string") diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 4f926429..50630152 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -55,6 +55,15 @@ Abstract Syntax Notation 1 is a standard description language for specifying serialisation and deserialisation of data structures in a portable and cross-platform way. + + bytes-like object + All the types that implement the buffer protocol. That includes + ``str`` (only on python2), ``bytes``, ``bytesarray``, ``array.array` + and ``memoryview`` of those objects. + Please note that ``array.array` serialisation (converting it to byte + string) is endianess dependant! Signature computed over ``array.array`` + of integers on a big-endian system will not be verified on a + little-endian system and vice-versa. """ import binascii @@ -70,6 +79,7 @@ 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 ._compat import normalise_bytes __all__ = ["BadSignatureError", "BadDigestError", "VerifyingKey", "SigningKey", @@ -231,8 +241,8 @@ def from_string(cls, string, curve=NIST192p, hashfunc=sha1, 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 string: single point encoding of the public key + :type string: :term:`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 @@ -245,6 +255,7 @@ def from_string(cls, string, curve=NIST192p, hashfunc=sha1, :return: Initialised VerifyingKey object :rtype: VerifyingKey """ + string = normalise_bytes(string) sig_len = len(string) if sig_len == curve.verifying_key_length: point = cls._from_raw_encoding(string, curve, validate_point) @@ -317,16 +328,17 @@ def from_der(cls, string): :return: Initialised VerifyingKey object :rtype: VerifyingKey """ + string = normalise_bytes(string) # [[oid_ecPublicKey,oid_curve], point_str_bitstring] s1, empty = der.remove_sequence(string) - if empty != b(""): + if empty != b"": raise der.UnexpectedDER("trailing junk after DER pubkey: %s" % binascii.hexlify(empty)) 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(""): + if empty != b"": raise der.UnexpectedDER("trailing junk after DER pubkey objects: %s" % binascii.hexlify(empty)) if not oid_pk == oid_ecPublicKey: @@ -334,7 +346,7 @@ def from_der(cls, string): "encoding: {0!r}".format(oid_pk)) curve = find_curve(oid_curve) point_str, empty = der.remove_bitstring(point_str_bitstring, 0) - if empty != b(""): + if empty != b"": raise der.UnexpectedDER("trailing junk after pubkey pointstring: %s" % binascii.hexlify(empty)) # raw encoding of point is invalid in DER files @@ -371,6 +383,7 @@ def from_public_key_recovery(cls, signature, data, curve, hashfunc=sha1, :return: Initialised VerifyingKey objects :rtype: list of VerifyingKey """ + data = normalise_bytes(data) digest = hashfunc(data).digest() return cls.from_public_key_recovery_with_digest( signature, digest, curve, hashfunc=hashfunc, @@ -411,6 +424,7 @@ def from_public_key_recovery_with_digest( r, s = sigdecode(signature, generator.order()) sig = ecdsa.Signature(r, s) + digest = normalise_bytes(digest) digest_as_number = string_to_number(digest) pks = sig.recover_public_keys(digest_as_number, generator) @@ -531,7 +545,7 @@ def verify(self, signature, data, hashfunc=None, as the `sigdecode` parameter. :param signature: encoding of the signature - :type signature: bytes like object + :type signature: sigdecode method dependant :param data: data signed by the `signature`, will be hashed using `hashfunc`, if specified, or default hash function :type data: bytes like object @@ -553,6 +567,10 @@ def verify(self, signature, data, hashfunc=None, :return: True if the verification was successful :rtype: bool """ + # signature doesn't have to be a bytes-like-object so don't normalise + # it, the decoders will do that + data = normalise_bytes(data) + hashfunc = hashfunc or self.default_hashfunc digest = hashfunc(data).digest() return self.verify_digest(signature, digest, sigdecode) @@ -567,7 +585,7 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string): as the `sigdecode` parameter. :param signature: encoding of the signature - :type signature: bytes like object + :type signature: sigdecode method dependant :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 @@ -585,6 +603,9 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string): :return: True if the verification was successful :rtype: bool """ + # signature doesn't have to be a bytes-like-object so don't normalise + # it, the decoders will do that + digest = normalise_bytes(digest) if len(digest) > self.curve.baselen: raise BadDigestError("this curve (%s) is too short " "for your digest (%d)" % (self.curve.name, @@ -716,6 +737,7 @@ def from_string(cls, string, curve=NIST192p, hashfunc=sha1): :return: Initialised SigningKey object :rtype: SigningKey """ + string = normalise_bytes(string) if len(string) != curve.baselen: raise MalformedPointError( "Invalid length of private key, received {0}, expected {1}" @@ -808,6 +830,7 @@ def from_der(cls, string, hashfunc=sha1): :return: Initialised VerifyingKey object :rtype: VerifyingKey """ + string = normalise_bytes(string) s, empty = der.remove_sequence(string) if empty != b(""): raise der.UnexpectedDER("trailing junk after DER privkey: %s" % @@ -951,6 +974,8 @@ def sign_deterministic(self, data, hashfunc=None, :rtype: bytes or sigencode function dependant type """ hashfunc = hashfunc or self.default_hashfunc + data = normalise_bytes(data) + extra_entropy = normalise_bytes(extra_entropy) digest = hashfunc(data).digest() return self.sign_digest_deterministic( @@ -993,6 +1018,9 @@ def sign_digest_deterministic(self, digest, hashfunc=None, :rtype: bytes or sigencode function dependant type """ secexp = self.privkey.secret_multiplier + hashfunc = hashfunc or self.default_hashfunc + digest = normalise_bytes(digest) + extra_entropy = normalise_bytes(extra_entropy) def simple_r_s(r, s, order): return r, s, order @@ -1056,6 +1084,7 @@ def sign(self, data, entropy=None, hashfunc=None, :rtype: bytes or sigencode function dependant type """ hashfunc = hashfunc or self.default_hashfunc + data = normalise_bytes(data) h = hashfunc(data).digest() return self.sign_digest(h, entropy, sigencode, k) @@ -1094,6 +1123,7 @@ def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, :return: encoded signature for the `digest` hash :rtype: bytes or sigencode function dependant type """ + digest = normalise_bytes(digest) if len(digest) > self.curve.baselen: raise BadDigestError("this curve (%s) is too short " "for your digest (%d)" % (self.curve.name, diff --git a/src/ecdsa/rfc6979.py b/src/ecdsa/rfc6979.py index 290dcae7..a4893812 100644 --- a/src/ecdsa/rfc6979.py +++ b/src/ecdsa/rfc6979.py @@ -12,7 +12,7 @@ import hmac from binascii import hexlify from .util import number_to_string, number_to_string_crop, bit_length -from six import b +from ._compat import hmac_compat # bit_length was defined in this module previously so keep it for backwards @@ -54,24 +54,33 @@ def generate_k(order, secexp, hash_func, data, retry_gen=0, extra_entropy=b''): qlen = bit_length(order) holen = hash_func().digest_size rolen = (qlen + 7) / 8 - bx = number_to_string(secexp, order) + bits2octets(data, order) + \ - extra_entropy + bx = (hmac_compat(number_to_string(secexp, order)), + hmac_compat(bits2octets(data, order)), + hmac_compat(extra_entropy)) # Step B - v = b('\x01') * holen + v = b'\x01' * holen # Step C - k = b('\x00') * holen + k = b'\x00' * holen # Step D - k = hmac.new(k, v + b('\x00') + bx, hash_func).digest() + k = hmac.new(k, digestmod=hash_func) + k.update(v + b'\x00') + for i in bx: + k.update(i) + k = k.digest() # Step E v = hmac.new(k, v, hash_func).digest() # Step F - k = hmac.new(k, v + b('\x01') + bx, hash_func).digest() + k = hmac.new(k, digestmod=hash_func) + k.update(v + b'\x01') + for i in bx: + k.update(i) + k = k.digest() # Step G v = hmac.new(k, v, hash_func).digest() @@ -79,7 +88,7 @@ def generate_k(order, secexp, hash_func, data, retry_gen=0, extra_entropy=b''): # Step H while True: # Step H1 - t = b('') + t = b'' # Step H2 while len(t) < rolen: @@ -89,11 +98,10 @@ def generate_k(order, secexp, hash_func, data, retry_gen=0, extra_entropy=b''): # Step H3 secret = bits2int(t, qlen) - if secret >= 1 and secret < order: + if 1 <= secret < order: if retry_gen <= 0: return secret - else: - retry_gen -= 1 + retry_gen -= 1 - k = hmac.new(k, v + b('\x00'), hash_func).digest() + k = hmac.new(k, v + b'\x00', hash_func).digest() v = hmac.new(k, v, hash_func).digest() diff --git a/src/ecdsa/test_der.py b/src/ecdsa/test_der.py index c163519f..a44befb1 100644 --- a/src/ecdsa/test_der.py +++ b/src/ecdsa/test_der.py @@ -10,6 +10,8 @@ from six import b import pytest import warnings +from ._compat import str_idx_as_int + class TestRemoveInteger(unittest.TestCase): # DER requires the integers to be 0-padded only if they would be @@ -229,3 +231,14 @@ def test_invalid_encoding_of_empty_string(self): def test_invalid_padding_bits(self): with self.assertRaises(UnexpectedDER): remove_bitstring(b'\x03\x02\x01\xff', None) + + +class TestStrIdxAsInt(unittest.TestCase): + def test_str(self): + self.assertEqual(115, str_idx_as_int('str', 0)) + + def test_bytes(self): + self.assertEqual(115, str_idx_as_int(b'str', 0)) + + def test_bytearray(self): + self.assertEqual(115, str_idx_as_int(bytearray(b'str'), 0)) diff --git a/src/ecdsa/test_keys.py b/src/ecdsa/test_keys.py new file mode 100644 index 00000000..3f1dffe4 --- /dev/null +++ b/src/ecdsa/test_keys.py @@ -0,0 +1,310 @@ +try: + import unittest2 as unittest +except ImportError: + import unittest + +try: + buffer +except NameError: + buffer = memoryview + +import array +import six +import sys +import pytest +import hashlib + +from .keys import VerifyingKey, SigningKey +from .der import unpem +from .util import sigencode_string, sigencode_der, sigencode_strings, \ + sigdecode_string, sigdecode_der, sigdecode_strings + + +class TestVerifyingKeyFromString(unittest.TestCase): + """ + Verify that ecdsa.keys.VerifyingKey.from_string() can be used with + bytes-like objects + """ + + @classmethod + def setUpClass(cls): + cls.key_bytes = (b'\x04L\xa2\x95\xdb\xc7Z\xd7\x1f\x93\nz\xcf\x97\xcf' + b'\xd7\xc2\xd9o\xfe8}X!\xae\xd4\xfah\xfa^\rpI\xba\xd1' + b'Y\xfb\x92xa\xebo+\x9cG\xfav\xca') + cls.vk = VerifyingKey.from_string(cls.key_bytes) + + def test_bytes(self): + self.assertIsNotNone(self.vk) + self.assertIsInstance(self.vk, VerifyingKey) + self.assertEqual( + self.vk.pubkey.point.x(), + 105419898848891948935835657980914000059957975659675736097) + self.assertEqual( + self.vk.pubkey.point.y(), + 4286866841217412202667522375431381222214611213481632495306) + + def test_bytes_memoryview(self): + vk = VerifyingKey.from_string(buffer(self.key_bytes)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytearray(self): + vk = VerifyingKey.from_string(bytearray(self.key_bytes)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytesarray_memoryview(self): + vk = VerifyingKey.from_string(buffer(bytearray(self.key_bytes))) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_array_array_of_bytes(self): + arr = array.array('B', self.key_bytes) + vk = VerifyingKey.from_string(arr) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_array_array_of_bytes_memoryview(self): + arr = array.array('B', self.key_bytes) + vk = VerifyingKey.from_string(buffer(arr)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_array_array_of_ints(self): + arr = array.array('I', self.key_bytes) + vk = VerifyingKey.from_string(arr) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_array_array_of_ints_memoryview(self): + arr = array.array('I', self.key_bytes) + vk = VerifyingKey.from_string(buffer(arr)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytes_uncompressed(self): + vk = VerifyingKey.from_string(b'\x04' + self.key_bytes) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytearray_uncompressed(self): + vk = VerifyingKey.from_string(bytearray(b'\x04' + self.key_bytes)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytes_compressed(self): + vk = VerifyingKey.from_string(b'\x02' + self.key_bytes[:24]) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytearray_uncompressed(self): + vk = VerifyingKey.from_string(bytearray(b'\x02' + self.key_bytes[:24])) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + +class TestVerifyingKeyFromDer(unittest.TestCase): + """ + Verify that ecdsa.keys.VerifyingKey.from_der() can be used with + bytes-like objects. + """ + @classmethod + def setUpClass(cls): + prv_key_str = ( + "-----BEGIN EC PRIVATE KEY-----\n" + "MF8CAQEEGF7IQgvW75JSqULpiQQ8op9WH6Uldw6xxaAKBggqhkjOPQMBAaE0AzIA\n" + "BLiBd9CE7xf15FY5QIAoNg+fWbSk1yZOYtoGUdzkejWkxbRc9RWTQjqLVXucIJnz\n" + "bA==\n" + "-----END EC PRIVATE KEY-----\n") + key_str = ( + "-----BEGIN PUBLIC KEY-----\n" + "MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEuIF30ITvF/XkVjlAgCg2D59ZtKTX\n" + "Jk5i2gZR3OR6NaTFtFz1FZNCOotVe5wgmfNs\n" + "-----END PUBLIC KEY-----\n") + cls.key_bytes = unpem(key_str) + assert isinstance(cls.key_bytes, bytes) + cls.vk = VerifyingKey.from_pem(key_str) + + def test_bytes(self): + vk = VerifyingKey.from_der(self.key_bytes) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytes_memoryview(self): + vk = VerifyingKey.from_der(buffer(self.key_bytes)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytearray(self): + vk = VerifyingKey.from_der(bytearray(self.key_bytes)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_bytesarray_memoryview(self): + vk = VerifyingKey.from_der(buffer(bytearray(self.key_bytes))) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_array_array_of_bytes(self): + arr = array.array('B', self.key_bytes) + vk = VerifyingKey.from_der(arr) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + def test_array_array_of_bytes_memoryview(self): + arr = array.array('B', self.key_bytes) + vk = VerifyingKey.from_der(buffer(arr)) + + self.assertEqual(self.vk.to_string(), vk.to_string()) + + +# test VerifyingKey.verify() +prv_key_str = ( + "-----BEGIN EC PRIVATE KEY-----\n" + "MF8CAQEEGF7IQgvW75JSqULpiQQ8op9WH6Uldw6xxaAKBggqhkjOPQMBAaE0AzIA\n" + "BLiBd9CE7xf15FY5QIAoNg+fWbSk1yZOYtoGUdzkejWkxbRc9RWTQjqLVXucIJnz\n" + "bA==\n" + "-----END EC PRIVATE KEY-----\n") +key_bytes = unpem(prv_key_str) +assert isinstance(key_bytes, bytes) +sk = SigningKey.from_der(key_bytes) +vk = sk.verifying_key + +data = (b"some string for signing" + b"contents don't really matter" + b"but do include also some crazy values: " + b"\x00\x01\t\r\n\x00\x00\x00\xff\xf0") +assert len(data) % 4 == 0 +sha1 = hashlib.sha1() +sha1.update(data) +data_hash = sha1.digest() +assert isinstance(data_hash, bytes) +sig_raw = sk.sign(data, sigencode=sigencode_string) +assert isinstance(sig_raw, bytes) +sig_der = sk.sign(data, sigencode=sigencode_der) +assert isinstance(sig_der, bytes) +sig_strings = sk.sign(data, sigencode=sigencode_strings) +assert isinstance(sig_strings[0], bytes) + +verifiers = [] +for modifier, fun in [ + ("bytes", lambda x: x), + ("bytes memoryview", lambda x: buffer(x)), + ("bytearray", lambda x: bytearray(x)), + ("bytearray memoryview", lambda x: buffer(bytearray(x))), + ("array.array of bytes", lambda x: array.array('B', x)), + ("array.array of bytes memoryview", lambda x: buffer(array.array('B', x))), + ("array.array of ints", lambda x: array.array('I', x)), + ("array.array of ints memoryview", lambda x: buffer(array.array('I', x))) + ]: + if "ints" in modifier: + conv = lambda x: x + else: + conv = fun + for sig_format, signature, decoder, mod_apply in [ + ("raw", sig_raw, sigdecode_string, lambda x: conv(x)), + ("der", sig_der, sigdecode_der, lambda x: conv(x)), + ("strings", sig_strings, sigdecode_strings, lambda x: + tuple(conv(i) for i in x)) + ]: + for method_name, vrf_mthd, vrf_data in [ + ("verify", vk.verify, data), + ("verify_digest", vk.verify_digest, data_hash) + ]: + verifiers.append(pytest.param( + signature, decoder, mod_apply, fun, vrf_mthd, vrf_data, + id="{2}-{0}-{1}".format(modifier, sig_format, method_name))) + +@pytest.mark.parametrize( + "signature,decoder,mod_apply,fun,vrf_mthd,vrf_data", + verifiers) +def test_VerifyingKey_verify( + signature, decoder, mod_apply, fun, vrf_mthd, vrf_data): + sig = mod_apply(signature) + + assert vrf_mthd(sig, fun(vrf_data), sigdecode=decoder) + + +# test SigningKey.from_string() +prv_key_bytes = (b'^\xc8B\x0b\xd6\xef\x92R\xa9B\xe9\x89\x04<\xa2' + b'\x9fV\x1f\xa5%w\x0e\xb1\xc5') +assert len(prv_key_bytes) == 24 +converters = [] +for modifier, convert in [ + ("bytes", lambda x: x), + ("bytes memoryview", buffer), + ("bytearray", bytearray), + ("bytearray memoryview", lambda x: buffer(bytearray(x))), + ("array.array of bytes", lambda x: array.array('B', x)), + ("array.array of bytes memoryview", + lambda x: buffer(array.array('B', x))), + ("array.array of ints", lambda x: array.array('I', x)), + ("array.array of ints memoryview", + lambda x: buffer(array.array('I', x))) + ]: + converters.append(pytest.param( + convert, + id=modifier)) + +@pytest.mark.parametrize("convert", converters) +def test_SigningKey_from_string(convert): + key = convert(prv_key_bytes) + sk = SigningKey.from_string(key) + + assert sk.to_string() == prv_key_bytes + + +# test SigningKey.from_der() +prv_key_str = ( + "-----BEGIN EC PRIVATE KEY-----\n" + "MF8CAQEEGF7IQgvW75JSqULpiQQ8op9WH6Uldw6xxaAKBggqhkjOPQMBAaE0AzIA\n" + "BLiBd9CE7xf15FY5QIAoNg+fWbSk1yZOYtoGUdzkejWkxbRc9RWTQjqLVXucIJnz\n" + "bA==\n" + "-----END EC PRIVATE KEY-----\n") +key_bytes = unpem(prv_key_str) +assert isinstance(key_bytes, bytes) + +# last two converters are for array.array of ints, those require input +# that's multiple of 4, which no curve we support produces +@pytest.mark.parametrize("convert", converters[:-2]) +def test_SigningKey_from_der(convert): + key = convert(key_bytes) + sk = SigningKey.from_der(key) + + assert sk.to_string() == prv_key_bytes + + +# test SigningKey.sign_deterministic() +extra_entropy=b'\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11' + +@pytest.mark.parametrize("convert", converters) +def test_SigningKey_sign_deterministic(convert): + sig = sk.sign_deterministic( + convert(data), + extra_entropy=convert(extra_entropy)) + + vk.verify(sig, data) + + +# test SigningKey.sign_digest_deterministic() +@pytest.mark.parametrize("convert", converters) +def test_SigningKey_sign_digest_deterministic(convert): + sig = sk.sign_digest_deterministic( + convert(data_hash), + extra_entropy=convert(extra_entropy)) + + vk.verify(sig, data) + + +@pytest.mark.parametrize("convert", converters) +def test_SigningKey_sign(convert): + sig = sk.sign(convert(data)) + + vk.verify(sig, data) + + +@pytest.mark.parametrize("convert", converters) +def test_SigningKey_sign_digest(convert): + sig = sk.sign_digest(convert(data_hash)) + + vk.verify(sig, data) diff --git a/src/ecdsa/util.py b/src/ecdsa/util.py index d229ed25..4769e312 100644 --- a/src/ecdsa/util.py +++ b/src/ecdsa/util.py @@ -6,6 +6,7 @@ from hashlib import sha256 from six import PY3, int2byte, b, next from . import der +from ._compat import normalise_bytes # RFC5480: # The "unrestricted" algorithm identifier is: @@ -312,6 +313,7 @@ def sigdecode_string(signature, order): :return: tuple with decoded 'r' and 's' values of signature :rtype: tuple of ints """ + signature = normalise_bytes(signature) l = orderlen(order) if not len(signature) == 2 * l: raise MalformedSignature( @@ -347,6 +349,8 @@ def sigdecode_strings(rs_strings, order): "Invalid number of strings provided: {0}, expected 2" .format(len(rs_strings))) (r_str, s_str) = rs_strings + r_str = normalise_bytes(r_str) + s_str = normalise_bytes(s_str) l = orderlen(order) if not len(r_str) == l: raise MalformedSignature( @@ -388,14 +392,15 @@ def sigdecode_der(sig_der, order): :return: tuple with decoded 'r' and 's' values of signature :rtype: tuple of ints """ + sig_der = normalise_bytes(sig_der) # return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) rs_strings, empty = der.remove_sequence(sig_der) - if empty != b(""): + if empty != b"": raise der.UnexpectedDER("trailing junk after DER sig: %s" % binascii.hexlify(empty)) r, rest = der.remove_integer(rs_strings) s, empty = der.remove_integer(rest) - if empty != b(""): + if empty != b"": raise der.UnexpectedDER("trailing junk after DER numbers: %s" % binascii.hexlify(empty)) return r, s