Skip to content

Commit fc5cb54

Browse files
authored
Merge pull request #168 from tomato42/large_hashes
Large hashes with small curves
2 parents aea736c + 73a245f commit fc5cb54

File tree

3 files changed

+80
-22
lines changed

3 files changed

+80
-22
lines changed

src/ecdsa/keys.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,6 @@ def verify(self, signature, data, hashfunc=None,
589589
:type sigdecode: callable
590590
591591
:raises BadSignatureError: if the signature is invalid or malformed
592-
:raises BadDigestError: if the provided hash is too big for the curve
593-
associated with this VerifyingKey
594592
595593
:return: True if the verification was successful
596594
:rtype: bool
@@ -601,9 +599,10 @@ def verify(self, signature, data, hashfunc=None,
601599

602600
hashfunc = hashfunc or self.default_hashfunc
603601
digest = hashfunc(data).digest()
604-
return self.verify_digest(signature, digest, sigdecode)
602+
return self.verify_digest(signature, digest, sigdecode, True)
605603

606-
def verify_digest(self, signature, digest, sigdecode=sigdecode_string):
604+
def verify_digest(self, signature, digest, sigdecode=sigdecode_string,
605+
allow_truncate=False):
607606
"""
608607
Verify a signature made over provided hash value.
609608
@@ -623,17 +622,23 @@ def verify_digest(self, signature, digest, sigdecode=sigdecode_string):
623622
second one. See :func:`ecdsa.util.sigdecode_string` and
624623
:func:`ecdsa.util.sigdecode_der` for examples.
625624
:type sigdecode: callable
625+
:param bool allow_truncate: if True, the provided digest can have
626+
bigger bit-size than the order of the curve, the extra bits (at
627+
the end of the digest) will be truncated. Use it when verifying
628+
SHA-384 output using NIST256p or in similar situations.
626629
627630
:raises BadSignatureError: if the signature is invalid or malformed
628-
:raises BadDigestError: if the provided hash is too big for the curve
629-
associated with this VerifyingKey
631+
:raises BadDigestError: if the provided digest is too big for the curve
632+
associated with this VerifyingKey and allow_truncate was not set
630633
631634
:return: True if the verification was successful
632635
:rtype: bool
633636
"""
634637
# signature doesn't have to be a bytes-like-object so don't normalise
635638
# it, the decoders will do that
636639
digest = normalise_bytes(digest)
640+
if allow_truncate:
641+
digest = digest[:self.curve.baselen]
637642
if len(digest) > self.curve.baselen:
638643
raise BadDigestError("this curve (%s) is too short "
639644
"for your digest (%d)" % (self.curve.name,
@@ -1017,11 +1022,11 @@ def sign_deterministic(self, data, hashfunc=None,
10171022

10181023
return self.sign_digest_deterministic(
10191024
digest, hashfunc=hashfunc, sigencode=sigencode,
1020-
extra_entropy=extra_entropy)
1025+
extra_entropy=extra_entropy, allow_truncate=True)
10211026

10221027
def sign_digest_deterministic(self, digest, hashfunc=None,
10231028
sigencode=sigencode_string,
1024-
extra_entropy=b''):
1029+
extra_entropy=b'', allow_truncate=False):
10251030
"""
10261031
Create signature for digest using the deterministic RFC6679 algorithm.
10271032
@@ -1050,6 +1055,10 @@ def sign_digest_deterministic(self, digest, hashfunc=None,
10501055
:param extra_entropy: additional data that will be fed into the random
10511056
number generator used in the RFC6979 process. Entirely optional.
10521057
:type extra_entropy: bytes like object
1058+
:param bool allow_truncate: if True, the provided digest can have
1059+
bigger bit-size than the order of the curve, the extra bits (at
1060+
the end of the digest) will be truncated. Use it when signing
1061+
SHA-384 output using NIST256p or in similar situations.
10531062
10541063
:return: encoded signature for the `digest` hash
10551064
:rtype: bytes or sigencode function dependant type
@@ -1068,7 +1077,10 @@ def simple_r_s(r, s, order):
10681077
self.curve.generator.order(), secexp, hashfunc, digest,
10691078
retry_gen=retry_gen, extra_entropy=extra_entropy)
10701079
try:
1071-
r, s, order = self.sign_digest(digest, sigencode=simple_r_s, k=k)
1080+
r, s, order = self.sign_digest(digest,
1081+
sigencode=simple_r_s,
1082+
k=k,
1083+
allow_truncate=allow_truncate)
10721084
break
10731085
except RSZeroError:
10741086
retry_gen += 1
@@ -1123,10 +1135,10 @@ def sign(self, data, entropy=None, hashfunc=None,
11231135
hashfunc = hashfunc or self.default_hashfunc
11241136
data = normalise_bytes(data)
11251137
h = hashfunc(data).digest()
1126-
return self.sign_digest(h, entropy, sigencode, k)
1138+
return self.sign_digest(h, entropy, sigencode, k, allow_truncate=True)
11271139

11281140
def sign_digest(self, digest, entropy=None, sigencode=sigencode_string,
1129-
k=None):
1141+
k=None, allow_truncate=False):
11301142
"""
11311143
Create signature over digest using the probabilistic ECDSA algorithm.
11321144
@@ -1152,6 +1164,10 @@ def sign_digest(self, digest, entropy=None, sigencode=sigencode_string,
11521164
:param int k: a pre-selected nonce for calculating the signature.
11531165
In typical use cases, it should be set to None (the default) to
11541166
allow its generation from an entropy source.
1167+
:param bool allow_truncate: if True, the provided digest can have
1168+
bigger bit-size than the order of the curve, the extra bits (at
1169+
the end of the digest) will be truncated. Use it when signing
1170+
SHA-384 output using NIST256p or in similar situations.
11551171
11561172
:raises RSZeroError: in the unlikely event when "r" parameter or
11571173
"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,
11611177
:rtype: bytes or sigencode function dependant type
11621178
"""
11631179
digest = normalise_bytes(digest)
1180+
if allow_truncate:
1181+
digest = digest[:self.curve.baselen]
11641182
if len(digest) > self.curve.baselen:
11651183
raise BadDigestError("this curve (%s) is too short "
11661184
"for your digest (%d)" % (self.curve.name,

src/ecdsa/test_pyecdsa.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import pytest
1212
from binascii import hexlify, unhexlify
1313
from hashlib import sha1, sha256, sha512
14+
import hashlib
15+
from functools import partial
1416

1517
from hypothesis import given
1618
import hypothesis.strategies as st
@@ -708,15 +710,15 @@ class OpenSSL(unittest.TestCase):
708710
run_openssl("ecparam -list_curves")
709711
.split('\n'))
710712

711-
def get_openssl_messagedigest_arg(self):
713+
def get_openssl_messagedigest_arg(self, hash_name):
712714
v = run_openssl("version")
713715
# e.g. "OpenSSL 1.0.0 29 Mar 2010", or "OpenSSL 1.0.0a 1 Jun 2010",
714716
# or "OpenSSL 0.9.8o 01 Jun 2010"
715717
vs = v.split()[1].split(".")
716718
if vs >= ["1", "0", "0"]: # pragma: no cover
717-
return "-SHA1"
719+
return "-{0}".format(hash_name)
718720
else: # pragma: no cover
719-
return "-ecdsa-with-SHA1"
721+
return "-ecdsa-with-{0}".format(hash_name)
720722

721723
# sk: 1:OpenSSL->python 2:python->OpenSSL
722724
# vk: 3:OpenSSL->python 4:python->OpenSSL
@@ -727,6 +729,11 @@ def get_openssl_messagedigest_arg(self):
727729
def test_from_openssl_nist192p(self):
728730
return self.do_test_from_openssl(NIST192p)
729731

732+
@pytest.mark.skipif("prime192v1" not in OPENSSL_SUPPORTED_CURVES,
733+
reason="system openssl does not support prime192v1")
734+
def test_from_openssl_nist192p_sha256(self):
735+
return self.do_test_from_openssl(NIST192p, "SHA256")
736+
730737
@pytest.mark.skipif("secp224r1" not in OPENSSL_SUPPORTED_CURVES,
731738
reason="system openssl does not support secp224r1")
732739
def test_from_openssl_nist224p(self):
@@ -737,6 +744,16 @@ def test_from_openssl_nist224p(self):
737744
def test_from_openssl_nist256p(self):
738745
return self.do_test_from_openssl(NIST256p)
739746

747+
@pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES,
748+
reason="system openssl does not support prime256v1")
749+
def test_from_openssl_nist256p_sha384(self):
750+
return self.do_test_from_openssl(NIST256p, "SHA384")
751+
752+
@pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES,
753+
reason="system openssl does not support prime256v1")
754+
def test_from_openssl_nist256p_sha512(self):
755+
return self.do_test_from_openssl(NIST256p, "SHA512")
756+
740757
@pytest.mark.skipif("secp384r1" not in OPENSSL_SUPPORTED_CURVES,
741758
reason="system openssl does not support secp384r1")
742759
def test_from_openssl_nist384p(self):
@@ -787,12 +804,12 @@ def test_from_openssl_brainpoolp384r1(self):
787804
def test_from_openssl_brainpoolp512r1(self):
788805
return self.do_test_from_openssl(BRAINPOOLP512r1)
789806

790-
def do_test_from_openssl(self, curve):
807+
def do_test_from_openssl(self, curve, hash_name="SHA1"):
791808
curvename = curve.openssl_name
792809
assert curvename
793810
# OpenSSL: create sk, vk, sign.
794811
# Python: read vk(3), checksig(5), read sk(1), sign, check
795-
mdarg = self.get_openssl_messagedigest_arg()
812+
mdarg = self.get_openssl_messagedigest_arg(hash_name)
796813
if os.path.isdir("t"): # pragma: no cover
797814
shutil.rmtree("t")
798815
os.mkdir("t")
@@ -809,19 +826,30 @@ def do_test_from_openssl(self, curve):
809826
with open("t/data.sig", "rb") as e:
810827
sig_der = e.read()
811828
self.assertTrue(vk.verify(sig_der, data, # 5
812-
hashfunc=sha1, sigdecode=sigdecode_der))
829+
hashfunc=partial(hashlib.new, hash_name),
830+
sigdecode=sigdecode_der))
813831

814832
with open("t/privkey.pem") as e:
815833
fp = e.read()
816834
sk = SigningKey.from_pem(fp) # 1
817-
sig = sk.sign(data)
818-
self.assertTrue(vk.verify(sig, data))
835+
sig = sk.sign(
836+
data,
837+
hashfunc=partial(hashlib.new, hash_name),
838+
)
839+
self.assertTrue(vk.verify(sig,
840+
data,
841+
hashfunc=partial(hashlib.new, hash_name)))
819842

820843
@pytest.mark.skipif("prime192v1" not in OPENSSL_SUPPORTED_CURVES,
821844
reason="system openssl does not support prime192v1")
822845
def test_to_openssl_nist192p(self):
823846
self.do_test_to_openssl(NIST192p)
824847

848+
@pytest.mark.skipif("prime192v1" not in OPENSSL_SUPPORTED_CURVES,
849+
reason="system openssl does not support prime192v1")
850+
def test_to_openssl_nist192p_sha256(self):
851+
self.do_test_to_openssl(NIST192p, "SHA256")
852+
825853
@pytest.mark.skipif("secp224r1" not in OPENSSL_SUPPORTED_CURVES,
826854
reason="system openssl does not support secp224r1")
827855
def test_to_openssl_nist224p(self):
@@ -832,6 +860,16 @@ def test_to_openssl_nist224p(self):
832860
def test_to_openssl_nist256p(self):
833861
self.do_test_to_openssl(NIST256p)
834862

863+
@pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES,
864+
reason="system openssl does not support prime256v1")
865+
def test_to_openssl_nist256p_sha384(self):
866+
self.do_test_to_openssl(NIST256p, "SHA384")
867+
868+
@pytest.mark.skipif("prime256v1" not in OPENSSL_SUPPORTED_CURVES,
869+
reason="system openssl does not support prime256v1")
870+
def test_to_openssl_nist256p_sha512(self):
871+
self.do_test_to_openssl(NIST256p, "SHA512")
872+
835873
@pytest.mark.skipif("secp384r1" not in OPENSSL_SUPPORTED_CURVES,
836874
reason="system openssl does not support secp384r1")
837875
def test_to_openssl_nist384p(self):
@@ -882,12 +920,12 @@ def test_to_openssl_brainpoolp384r1(self):
882920
def test_to_openssl_brainpoolp512r1(self):
883921
self.do_test_to_openssl(BRAINPOOLP512r1)
884922

885-
def do_test_to_openssl(self, curve):
923+
def do_test_to_openssl(self, curve, hash_name="SHA1"):
886924
curvename = curve.openssl_name
887925
assert curvename
888926
# Python: create sk, vk, sign.
889927
# OpenSSL: read vk(4), checksig(6), read sk(2), sign, check
890-
mdarg = self.get_openssl_messagedigest_arg()
928+
mdarg = self.get_openssl_messagedigest_arg(hash_name)
891929
if os.path.isdir("t"): # pragma: no cover
892930
shutil.rmtree("t")
893931
os.mkdir("t")
@@ -898,7 +936,8 @@ def do_test_to_openssl(self, curve):
898936
e.write(vk.to_der()) # 4
899937
with open("t/pubkey.pem", "wb") as e:
900938
e.write(vk.to_pem()) # 4
901-
sig_der = sk.sign(data, hashfunc=sha1, sigencode=sigencode_der)
939+
sig_der = sk.sign(data, hashfunc=partial(hashlib.new, hash_name),
940+
sigencode=sigencode_der)
902941

903942
with open("t/data.sig", "wb") as e:
904943
e.write(sig_der) # 6

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ basepython=python3.8
5757

5858
[testenv:coverage]
5959
sitepackages=True
60+
whitelist_externals=coverage
6061
commands = coverage run --branch -m pytest --hypothesis-show-statistics {posargs:src/ecdsa}
6162

6263
[testenv:speed]

0 commit comments

Comments
 (0)