From 70dd6ecbd22ecc22c1827d7bd18e9952c2f4c8f1 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 30 Nov 2019 15:12:28 +0100 Subject: [PATCH 1/2] support large hashes with small curves While not recommended, it is well-defined, so allow use of large hashes with small curve (like SHA-512 with NIST256p) test against OpenSSL --- src/ecdsa/keys.py | 40 ++++++++++++++++++------- src/ecdsa/test_pyecdsa.py | 61 ++++++++++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index bc8cf161..af9762d2 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -589,8 +589,6 @@ def verify(self, signature, data, hashfunc=None, :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 @@ -601,9 +599,10 @@ def verify(self, signature, data, hashfunc=None, hashfunc = hashfunc or self.default_hashfunc digest = hashfunc(data).digest() - return self.verify_digest(signature, digest, sigdecode) + return self.verify_digest(signature, digest, sigdecode, True) - def verify_digest(self, signature, digest, sigdecode=sigdecode_string): + def verify_digest(self, signature, digest, sigdecode=sigdecode_string, + allow_truncate=False): """ Verify a signature made over provided hash value. @@ -623,10 +622,14 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string): second one. See :func:`ecdsa.util.sigdecode_string` and :func:`ecdsa.util.sigdecode_der` for examples. :type sigdecode: callable + :param bool allow_truncate: if True, the provided digest can have + bigger bit-size than the order of the curve, the extra bits (at + the end of the digest) will be truncated. Use it when verifying + SHA-384 output using NIST256p or in similar situations. :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 + :raises BadDigestError: if the provided digest is too big for the curve + associated with this VerifyingKey and allow_truncate was not set :return: True if the verification was successful :rtype: bool @@ -634,6 +637,8 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string): # 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 allow_truncate: + digest = digest[:self.curve.baselen] if len(digest) > self.curve.baselen: raise BadDigestError("this curve (%s) is too short " "for your digest (%d)" % (self.curve.name, @@ -1017,11 +1022,11 @@ def sign_deterministic(self, data, hashfunc=None, return self.sign_digest_deterministic( digest, hashfunc=hashfunc, sigencode=sigencode, - extra_entropy=extra_entropy) + extra_entropy=extra_entropy, allow_truncate=True) def sign_digest_deterministic(self, digest, hashfunc=None, sigencode=sigencode_string, - extra_entropy=b''): + extra_entropy=b'', allow_truncate=False): """ Create signature for digest using the deterministic RFC6679 algorithm. @@ -1050,6 +1055,10 @@ def sign_digest_deterministic(self, digest, hashfunc=None, :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 + :param bool allow_truncate: if True, the provided digest can have + bigger bit-size than the order of the curve, the extra bits (at + the end of the digest) will be truncated. Use it when signing + SHA-384 output using NIST256p or in similar situations. :return: encoded signature for the `digest` hash :rtype: bytes or sigencode function dependant type @@ -1068,7 +1077,10 @@ def simple_r_s(r, s, order): self.curve.generator.order(), secexp, hashfunc, digest, retry_gen=retry_gen, extra_entropy=extra_entropy) try: - r, s, order = self.sign_digest(digest, sigencode=simple_r_s, k=k) + r, s, order = self.sign_digest(digest, + sigencode=simple_r_s, + k=k, + allow_truncate=allow_truncate) break except RSZeroError: retry_gen += 1 @@ -1123,10 +1135,10 @@ def sign(self, data, entropy=None, hashfunc=None, hashfunc = hashfunc or self.default_hashfunc data = normalise_bytes(data) h = hashfunc(data).digest() - return self.sign_digest(h, entropy, sigencode, k) + return self.sign_digest(h, entropy, sigencode, k, allow_truncate=True) def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, - k=None): + k=None, allow_truncate=False): """ Create signature over digest using the probabilistic ECDSA algorithm. @@ -1152,6 +1164,10 @@ def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, :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. + :param bool allow_truncate: if True, the provided digest can have + bigger bit-size than the order of the curve, the extra bits (at + the end of the digest) will be truncated. Use it when signing + SHA-384 output using NIST256p or in similar situations. :raises RSZeroError: in the unlikely event when "r" parameter or "s" parameter is equal 0 as that would leak the key. Calee should @@ -1161,6 +1177,8 @@ def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, :rtype: bytes or sigencode function dependant type """ digest = normalise_bytes(digest) + if allow_truncate: + digest = digest[:self.curve.baselen] 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/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index 05f22e60..7e2a031c 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -11,6 +11,8 @@ import pytest from binascii import hexlify, unhexlify from hashlib import sha1, sha256, sha512 +import hashlib +from functools import partial from hypothesis import given import hypothesis.strategies as st @@ -708,15 +710,15 @@ class OpenSSL(unittest.TestCase): run_openssl("ecparam -list_curves") .split('\n')) - def get_openssl_messagedigest_arg(self): + def get_openssl_messagedigest_arg(self, hash_name): v = run_openssl("version") # e.g. "OpenSSL 1.0.0 29 Mar 2010", or "OpenSSL 1.0.0a 1 Jun 2010", # or "OpenSSL 0.9.8o 01 Jun 2010" vs = v.split()[1].split(".") if vs >= ["1", "0", "0"]: # pragma: no cover - return "-SHA1" + return "-{0}".format(hash_name) else: # pragma: no cover - return "-ecdsa-with-SHA1" + return "-ecdsa-with-{0}".format(hash_name) # sk: 1:OpenSSL->python 2:python->OpenSSL # vk: 3:OpenSSL->python 4:python->OpenSSL @@ -727,6 +729,11 @@ def get_openssl_messagedigest_arg(self): def test_from_openssl_nist192p(self): return self.do_test_from_openssl(NIST192p) + @pytest.mark.skipif("prime192v1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support prime192v1") + def test_from_openssl_nist192p_sha256(self): + return self.do_test_from_openssl(NIST192p, "SHA256") + @pytest.mark.skipif("secp224r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp224r1") def test_from_openssl_nist224p(self): @@ -737,6 +744,16 @@ def test_from_openssl_nist224p(self): def test_from_openssl_nist256p(self): return self.do_test_from_openssl(NIST256p) + @pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support prime256v1") + def test_from_openssl_nist256p_sha384(self): + return self.do_test_from_openssl(NIST256p, "SHA384") + + @pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support prime256v1") + def test_from_openssl_nist256p_sha512(self): + return self.do_test_from_openssl(NIST256p, "SHA512") + @pytest.mark.skipif("secp384r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp384r1") def test_from_openssl_nist384p(self): @@ -787,12 +804,12 @@ def test_from_openssl_brainpoolp384r1(self): def test_from_openssl_brainpoolp512r1(self): return self.do_test_from_openssl(BRAINPOOLP512r1) - def do_test_from_openssl(self, curve): + def do_test_from_openssl(self, curve, hash_name="SHA1"): curvename = curve.openssl_name assert curvename # OpenSSL: create sk, vk, sign. # Python: read vk(3), checksig(5), read sk(1), sign, check - mdarg = self.get_openssl_messagedigest_arg() + mdarg = self.get_openssl_messagedigest_arg(hash_name) if os.path.isdir("t"): # pragma: no cover shutil.rmtree("t") os.mkdir("t") @@ -809,19 +826,30 @@ def do_test_from_openssl(self, curve): with open("t/data.sig", "rb") as e: sig_der = e.read() self.assertTrue(vk.verify(sig_der, data, # 5 - hashfunc=sha1, sigdecode=sigdecode_der)) + hashfunc=partial(hashlib.new, hash_name), + sigdecode=sigdecode_der)) with open("t/privkey.pem") as e: fp = e.read() sk = SigningKey.from_pem(fp) # 1 - sig = sk.sign(data) - self.assertTrue(vk.verify(sig, data)) + sig = sk.sign( + data, + hashfunc=partial(hashlib.new, hash_name), + ) + self.assertTrue(vk.verify(sig, + data, + hashfunc=partial(hashlib.new, hash_name))) @pytest.mark.skipif("prime192v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime192v1") def test_to_openssl_nist192p(self): self.do_test_to_openssl(NIST192p) + @pytest.mark.skipif("prime192v1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support prime192v1") + def test_to_openssl_nist192p_sha256(self): + self.do_test_to_openssl(NIST192p, "SHA256") + @pytest.mark.skipif("secp224r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp224r1") def test_to_openssl_nist224p(self): @@ -832,6 +860,16 @@ def test_to_openssl_nist224p(self): def test_to_openssl_nist256p(self): self.do_test_to_openssl(NIST256p) + @pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support prime256v1") + def test_to_openssl_nist256p_sha384(self): + self.do_test_to_openssl(NIST256p, "SHA384") + + @pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support prime256v1") + def test_to_openssl_nist256p_sha512(self): + self.do_test_to_openssl(NIST256p, "SHA512") + @pytest.mark.skipif("secp384r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp384r1") def test_to_openssl_nist384p(self): @@ -882,12 +920,12 @@ def test_to_openssl_brainpoolp384r1(self): def test_to_openssl_brainpoolp512r1(self): self.do_test_to_openssl(BRAINPOOLP512r1) - def do_test_to_openssl(self, curve): + def do_test_to_openssl(self, curve, hash_name="SHA1"): curvename = curve.openssl_name assert curvename # Python: create sk, vk, sign. # OpenSSL: read vk(4), checksig(6), read sk(2), sign, check - mdarg = self.get_openssl_messagedigest_arg() + mdarg = self.get_openssl_messagedigest_arg(hash_name) if os.path.isdir("t"): # pragma: no cover shutil.rmtree("t") os.mkdir("t") @@ -898,7 +936,8 @@ def do_test_to_openssl(self, curve): e.write(vk.to_der()) # 4 with open("t/pubkey.pem", "wb") as e: e.write(vk.to_pem()) # 4 - sig_der = sk.sign(data, hashfunc=sha1, sigencode=sigencode_der) + sig_der = sk.sign(data, hashfunc=partial(hashlib.new, hash_name), + sigencode=sigencode_der) with open("t/data.sig", "wb") as e: e.write(sig_der) # 6 From 73a245f4562859c241943f8ae087c51b53895dc3 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sat, 30 Nov 2019 15:13:55 +0100 Subject: [PATCH 2/2] silence tox about commands external to testenv such use will not work in tox 4, so fix it before it breaks --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 99ee89bc..58280226 100644 --- a/tox.ini +++ b/tox.ini @@ -57,6 +57,7 @@ basepython=python3.8 [testenv:coverage] sitepackages=True +whitelist_externals=coverage commands = coverage run --branch -m pytest --hypothesis-show-statistics {posargs:src/ecdsa} [testenv:speed]