From b046e1ffbcec17c2d2d5b2e917bacf20f4367a71 Mon Sep 17 00:00:00 2001 From: Ganna Starovoytova Date: Mon, 1 Sep 2025 14:13:38 +0200 Subject: [PATCH] Adds to DER format of ECDSA signature with extended ECDSA-Sig-Value and ECDSA-Full-R. --- src/ecdsa/der.py | 58 +++++- src/ecdsa/ecdsa.py | 21 ++- src/ecdsa/keys.py | 36 +++- src/ecdsa/test_der.py | 68 +++++++ src/ecdsa/test_keys.py | 1 + src/ecdsa/test_malformed_sigs.py | 221 ++++++++++++++++++++++- src/ecdsa/test_pyecdsa.py | 184 ++++++++++++++++++- src/ecdsa/util.py | 300 ++++++++++++++++++++++++++++++- 8 files changed, 870 insertions(+), 19 deletions(-) diff --git a/src/ecdsa/der.py b/src/ecdsa/der.py index 7a06b681..5e4a93c6 100644 --- a/src/ecdsa/der.py +++ b/src/ecdsa/der.py @@ -151,6 +151,20 @@ def encode_number(n): return b"".join([int2byte(d) for d in b128_digits]) +def encode_boolean(b): + """ + Encodes BOOLEAN acording to ASN.1 DER format. + The ASN.1 BOOLEAN type has two possible values: TRUE and FALSE. + True is encoded as ff", False is encoded as a zero. + + :param boolean b: the boolean value to be encoded + :return: a byte string + :rtype: bytes + """ + + return b"\x01" + encode_length(1) + (b"\xff" if b else b"\x00") + + def is_sequence(string): return string and string[:1] == b"\x30" @@ -234,6 +248,43 @@ def remove_octet_string(string): return body, rest +def remove_boolean(string): + """ + Removes the ASN.1 BOOLEAN type. + For BOOLEAN types, in DER FALSE is always encoded as zero + and TRUE is always encoded as ff. + + :param bytes string: the boolean value to be encoded + :return: a boolean value and the rest of the string + :rtype: tuple(boolean, bytes) + """ + if not string: + raise UnexpectedDER( + "Empty string is an invalid " "encoding of a boolean" + ) + if string[:1] != b"\x01": + n = str_idx_as_int(string, 0) + raise UnexpectedDER("wanted type 'boolean' (0x01), got 0x%02x" % n) + length, lengthlength = read_length(string[1:]) + body = string[1 + lengthlength : 1 + lengthlength + length] + rest = string[1 + lengthlength + length :] + if not body: + raise UnexpectedDER("Empty object identifier") + if length != 1: + raise UnexpectedDER( + "The contents octets of boolean shall consist of a single octet." + ) + if body == b"\x00": + return False, rest + # the workaround due to instrumental, that + # saves the binary data as UF-8 string + # (0xff is an invalid start byte) + num = int(binascii.hexlify(body), 16) + if num == 0xFF: + return True, rest + raise UnexpectedDER("Invalid encoding of BOOLEAN.") + + def remove_object(string): if not string: raise UnexpectedDER( @@ -292,8 +343,7 @@ def remove_integer(string): smsb = str_idx_as_int(numberbytes, 1) if smsb < 0x80: raise UnexpectedDER( - "Invalid encoding of integer, unnecessary " - "zero padding bytes" + "Invalid encoding of integer, unnecessary zero padding bytes" ) return int(binascii.hexlify(numberbytes), 16), rest @@ -393,8 +443,8 @@ def remove_bitstring(string, expect_unused=_sentry): raise UnexpectedDER("Empty string does not encode a bitstring") if expect_unused is _sentry: warnings.warn( - "Legacy call convention used, expect_unused= needs to be" - " specified", + "Legacy call convention used, " + "expect_unused= needs to be specified", DeprecationWarning, ) num = str_idx_as_int(string, 0) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index f7109659..723de648 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -64,6 +64,7 @@ modified as part of the python-ecdsa package. """ +import sys import warnings from six import int2byte from . import ellipticcurve @@ -192,10 +193,22 @@ def verifies(self, hash, signature): n = G.order() r = signature.r s = signature.s - if r < 1 or r > n - 1: - return False if s < 1 or s > n - 1: return False + + if sys.version_info < (3, 0): # pragma: no branch + # memoryview was introduced in py 2.7 + byte_objects = set((bytearray, bytes)) + else: + byte_objects = set((bytearray, bytes, memoryview)) + if type(r) in byte_objects: + point = ellipticcurve.AbstractPoint.from_bytes( + self.generator.curve(), r + ) + r = point[0] % n + + if r < 1 or r > n - 1: + return False c = numbertheory.inverse_mod(s, n) u1 = (hash * c) % n u2 = (r * c) % n @@ -231,7 +244,7 @@ def __ne__(self, other): """Return False if the points are identical, True otherwise.""" return not self == other - def sign(self, hash, random_k): + def sign(self, hash, random_k, accelerate=False): """Return a signature for the provided hash, using the provided random nonce. It is absolutely vital that random_k be an unpredictable number in the range [1, self.public_key.point.order()-1]. If @@ -267,6 +280,8 @@ def sign(self, hash, random_k): ) % n if s == 0: raise RSZeroError("amazingly unlucky random number s") + if accelerate: + return Signature(p1, s) return Signature(r, s) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index f74252c7..c1b652d3 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -1318,6 +1318,7 @@ def sign_deterministic( hashfunc=None, sigencode=sigencode_string, extra_entropy=b"", + accelerate=False, ): """ Create signature over data. @@ -1354,6 +1355,10 @@ def sign_deterministic( number generator used in the RFC6979 process. Entirely optional. Ignored with EdDSA. :type extra_entropy: :term:`bytes-like object` + :param accelerate: an indicator for ECDSA sign operation to return + an ECPoint instead of a number of "r" parameter. + Applicable only for ECDSA key. + :type accelerate: boolean :return: encoded signature over `data` :rtype: bytes or sigencode function dependent type @@ -1373,6 +1378,7 @@ def sign_deterministic( sigencode=sigencode, extra_entropy=extra_entropy, allow_truncate=True, + accelerate=accelerate, ) def sign_digest_deterministic( @@ -1382,6 +1388,7 @@ def sign_digest_deterministic( sigencode=sigencode_string, extra_entropy=b"", allow_truncate=False, + accelerate=False, ): """ Create signature for digest using the deterministic RFC6979 algorithm. @@ -1417,6 +1424,10 @@ def sign_digest_deterministic( 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. + :param accelerate: an indicator for ECDSA sign operation to return + an ECPoint instead of a number of "r" parameter. + Applicable only for ECDSA key. + :type accelerate: boolean :return: encoded signature for the `digest` hash :rtype: bytes or sigencode function dependent type @@ -1447,6 +1458,7 @@ def simple_r_s(r, s, order): sigencode=simple_r_s, k=k, allow_truncate=allow_truncate, + accelerate=accelerate, ) break except RSZeroError: @@ -1462,6 +1474,7 @@ def sign( sigencode=sigencode_string, k=None, allow_truncate=True, + accelerate=False, ): """ Create signature over data. @@ -1525,6 +1538,10 @@ def sign( leak the key. Caller should try a better entropy source, retry with different ``k``, or use the :func:`~SigningKey.sign_deterministic` in such case. + :param accelerate: an indicator for ECDSA sign operation to return + an ECPoint instead of a number of "r" parameter. + Applicable only for ECDSA key. + :type accelerate: boolean :return: encoded signature of the hash of `data` :rtype: bytes or sigencode function dependent type @@ -1534,7 +1551,9 @@ def sign( if isinstance(self.curve.curve, CurveEdTw): return self.sign_deterministic(data) h = hashfunc(data).digest() - return self.sign_digest(h, entropy, sigencode, k, allow_truncate) + return self.sign_digest( + h, entropy, sigencode, k, allow_truncate, accelerate + ) def sign_digest( self, @@ -1543,6 +1562,7 @@ def sign_digest( sigencode=sigencode_string, k=None, allow_truncate=False, + accelerate=False, ): """ Create signature over digest using the probabilistic ECDSA algorithm. @@ -1579,6 +1599,10 @@ def sign_digest( leak the key. Caller should try a better entropy source, retry with different 'k', or use the :func:`~SigningKey.sign_digest_deterministic` in such case. + :param accelerate: an indicator for ECDSA sign operation to return + an ECPoint instead of a number of "r" parameter. + Applicable only for ECDSA key. + :type accelerate: boolean :return: encoded signature for the `digest` hash :rtype: bytes or sigencode function dependent type @@ -1591,10 +1615,10 @@ def sign_digest( self.curve, allow_truncate, ) - r, s = self.sign_number(number, entropy, k) + r, s = self.sign_number(number, entropy, k, accelerate) return sigencode(r, s, self.privkey.order) - def sign_number(self, number, entropy=None, k=None): + def sign_number(self, number, entropy=None, k=None, accelerate=False): """ Sign an integer directly. @@ -1613,6 +1637,10 @@ def sign_number(self, number, entropy=None, k=None): leak the key. Caller should try a better entropy source, retry with different 'k', or use the :func:`~SigningKey.sign_digest_deterministic` in such case. + :param accelerate: an indicator for ECDSA sign operation to return + an ECPoint instead of a number of "r" parameter. + Applicable only for ECDSA key. + :type accelerate: boolean :return: the "r" and "s" parameters of the signature :rtype: tuple of ints @@ -1627,5 +1655,5 @@ def sign_number(self, number, entropy=None, k=None): _k = randrange(order, entropy) assert 1 <= _k < order - sig = self.privkey.sign(number, _k) + sig = self.privkey.sign(number, _k, accelerate) return sig.r, sig.s diff --git a/src/ecdsa/test_der.py b/src/ecdsa/test_der.py index b0955431..32bb92e2 100644 --- a/src/ecdsa/test_der.py +++ b/src/ecdsa/test_der.py @@ -14,6 +14,7 @@ from ._compat import str_idx_as_int from .curves import NIST256p, NIST224p from .der import ( + remove_boolean, remove_integer, UnexpectedDER, read_length, @@ -26,6 +27,7 @@ remove_octet_string, remove_sequence, encode_implicit, + encode_boolean, ) @@ -565,6 +567,72 @@ def test_with_wrong_length(self): self.assertIn("Length longer", str(e.exception)) +class TestEncodeBoolean(unittest.TestCase): + def test_simple_true(self): + der = encode_boolean(True) + self.assertEqual(len(der), 3) + self.assertEqual(der, b"\x01\x01\xff") + + def test_simle_false(self): + der = encode_boolean(False) + self.assertEqual(len(der), 3) + self.assertEqual(der, b"\x01\x01\x00") + + +class TestRemoveBoolean(unittest.TestCase): + def test_simple_false(self): + data = b"\x01\x01\x00" + body, rest = remove_boolean(data) + self.assertEqual(body, False) + self.assertEqual(rest, b"") + + def test_simple_true(self): + data = b"\x01\x01\xff" + body, rest = remove_boolean(data) + self.assertEqual(body, True) + self.assertEqual(rest, b"") + + def test_empty_string(self): + with self.assertRaises(UnexpectedDER) as e: + remove_boolean(b"") + + def test_with_wrong_tag(self): + data = b"\x02\x01\x00" + + with self.assertRaises(UnexpectedDER) as e: + remove_boolean(data) + + self.assertIn("wanted type 'boolean' (0x01)", str(e.exception)) + + def test_with_wrong_encoded_value(self): + data = b"\x01\x01\x01" + + with self.assertRaises(UnexpectedDER) as e: + remove_boolean(data) + + self.assertIn("Invalid encoding of BOOLEAN", str(e.exception)) + + def test_empty_body(self): + data = b"\x01\x01" + + with self.assertRaises(UnexpectedDER) as e: + remove_boolean(data) + + self.assertIn("Empty object identifier", str(e.exception)) + + def test_several_boolean_octets(self): + data = b"\x01\x02\x01\x01" + + with self.assertRaises(UnexpectedDER) as e: + remove_boolean(data) + + self.assertIn( + "The contents octets of boolean " + "shall consist of a single octet", + str(e.exception), + ) + + @st.composite def st_oid(draw, max_value=2**512, max_size=50): """ diff --git a/src/ecdsa/test_keys.py b/src/ecdsa/test_keys.py index 348475e2..d84a624c 100644 --- a/src/ecdsa/test_keys.py +++ b/src/ecdsa/test_keys.py @@ -1037,6 +1037,7 @@ def test_SigningKey_from_string(convert): 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]) diff --git a/src/ecdsa/test_malformed_sigs.py b/src/ecdsa/test_malformed_sigs.py index e5a87c28..d9a37fb1 100644 --- a/src/ecdsa/test_malformed_sigs.py +++ b/src/ecdsa/test_malformed_sigs.py @@ -2,6 +2,8 @@ import hashlib +from .errors import MalformedPointError + try: from hashlib import algorithms_available except ImportError: # pragma: no cover @@ -29,7 +31,13 @@ from .keys import SigningKey from .keys import BadSignatureError -from .util import sigencode_der, sigencode_string +from .util import ( + number_to_string, + sigdecode_der_extended, + sigencode_der, + sigencode_der_sig_value_a, + sigencode_string, +) from .util import sigdecode_der, sigdecode_string from .curves import curves, SECP112r2, SECP128r1 from .der import ( @@ -39,6 +47,8 @@ encode_oid, encode_sequence, encode_constructed, + encode_implicit, + encode_boolean, ) from .ellipticcurve import CurveEdTw @@ -224,6 +234,215 @@ def test_random_der_ecdsa_sig_value(params): verifying_key.verify(sig, example_data, sigdecode=sigdecode_der) +@st.composite +def st_random_der_ecdsa_sig_value_full_r(draw): # pragma: no cover + """ + Hypothesis strategy for selecting random values and encoding them + to ECDSA-Signature object with chosen ECDSA-Full-R:: + + ECDSA-Signature ::= CHOICE { + two-ints-plus ECDSA-Sig-Value, + point-int [0] ECDSA-Full-R, + ... -- Future representations may be added + } + + ECDSA-Full-R ::= SEQUENCE { + r ECPoint, + s INTEGER + } + + ECPoint ::= OCTET STRING + """ + name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs)) + note("Configuration: {0}".format(name)) + order = int(verifying_key.curve.order) + + x = draw(st.integers(min_value=0, max_value=verifying_key.curve.baselen)) + y = draw(st.integers(min_value=0, max_value=verifying_key.curve.baselen)) + prime = verifying_key.curve.curve.p() + x_str = number_to_string(x, prime) + y_str = number_to_string(y, prime) + r = x_str + y_str + s = draw( + st.integers(min_value=0, max_value=order << 4) + | st.integers(min_value=order >> 2, max_value=order + 1) + ) + sig = encode_sequence(encode_octet_string(r), encode_integer(s)) + # the ECDSA-FULL-R in ECDSA-Signature has a tag [0] + sig = encode_implicit(0, sig) + + return verifying_key, sig + + +@settings(**slow_params) +@given(st_random_der_ecdsa_sig_value_full_r()) +def test_random_der_ecdsa_sig_value_full_r(params): + """ + Check if random values encoded in ECDSA-Full-R structure are rejected + as signature. + """ + verifying_key, sig = params + + with pytest.raises(BadSignatureError): + verifying_key.verify( + sig, example_data, sigdecode=sigdecode_der_extended + ) + + +@st.composite +def st_random_der_ecdsa_sig_value_a(draw): # pragma: no cover + """ + Hypothesis strategy for selecting random values and encoding them + to ECDSA-Sig-Value object with coefficient 'a' specified:: + + ECDSA-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER, + a INTEGER OPTIONAL, + y CHOICE { b BOOLEAN, f FieldElement } OPTIONAL + } + """ + name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs)) + note("Configuration: {0}".format(name)) + order = int(verifying_key.curve.order) + + # the encode_integer doesn't support negative numbers, would be nice + # to generate them too, but we have coverage for remove_integer() + # verifying that it doesn't accept them, so meh. + # Test all numbers around the ones that can show up (around order) + # way smaller and slightly bigger + r = draw( + st.integers(min_value=0, max_value=order << 4) + | st.integers(min_value=order >> 2, max_value=order + 1) + ) + s = draw( + st.integers(min_value=0, max_value=order << 4) + | st.integers(min_value=order >> 2, max_value=order + 1) + ) + a = draw(st.integers(min_value=0, max_value=1)) + + sig = sigencode_der_sig_value_a(r, s, None, a) + + return verifying_key, sig + + +@settings(**slow_params) +@given(st_random_der_ecdsa_sig_value_a()) +def test_random_der_ecdsa_sig_value_a(params): + """ + Check if 'a' value encoded in ECDSA-Sig-Value structure is rejected + as signature. + """ + verifying_key, sig = params + + with pytest.raises(BadSignatureError): + verifying_key.verify( + sig, example_data, sigdecode=sigdecode_der_extended + ) + + +@st.composite +def st_random_der_ecdsa_sig_value_y_field_elem(draw): # pragma: no cover + """ + Hypothesis strategy for selecting random values and encoding them + to ECDSA-Signature object with chosen ECDSA-Sig-Value with 'y' being FieldElement:: + + ECDSA-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER, + a INTEGER OPTIONAL, + y CHOICE { b BOOLEAN, f FieldElement } OPTIONAL + } + """ + name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs)) + note("Configuration: {0}".format(name)) + order = int(verifying_key.curve.order) + + x = draw(st.integers(min_value=0, max_value=verifying_key.curve.baselen)) + y = draw(st.integers(min_value=0, max_value=verifying_key.curve.baselen)) + prime = verifying_key.curve.curve.p() + y_str = number_to_string(y, prime) + + s = draw( + st.integers(min_value=0, max_value=order << 4) + | st.integers(min_value=order >> 2, max_value=order + 1) + ) + sig = encode_sequence( + encode_integer(x), encode_integer(s), encode_octet_string(y_str) + ) + + return verifying_key, sig + + +@settings(**slow_params) +@given(st_random_der_ecdsa_sig_value_y_field_elem()) +def test_random_der_ecdsa_sig_value_y_field_elem(params): # pragma: no cover + """ + Check if 'y' value encoded as FieldElement ECDSA-Sig-Value + structure is rejected as signature. + """ + verifying_key, sig = params + # The random values are not creating a valid point every time, + # sometimes the test will fail with MalformedPointError. + # When the point is valid, the test will fail with BadSignatureError. + with pytest.raises((BadSignatureError, MalformedPointError)): + verifying_key.verify( + sig, example_data, sigdecode=sigdecode_der_extended + ) + + +@st.composite +def st_random_der_ecdsa_sig_value_y_boolean(draw): # pragma: no cover + """ + Hypothesis strategy for selecting random values and encoding them + to ECDSA-Signature object with chosen ECDSA-Sig-Value with 'y' being BOOLEAN:: + + ECDSA-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER, + a INTEGER OPTIONAL, + y CHOICE { b BOOLEAN, f FieldElement } OPTIONAL + } + """ + name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs)) + note("Configuration: {0}".format(name)) + order = int(verifying_key.curve.order) + b = False + x = draw(st.integers(min_value=0, max_value=verifying_key.curve.baselen)) + y = draw(st.integers(min_value=0, max_value=verifying_key.curve.baselen)) + if y & 1: + b = True + s = draw( + st.integers(min_value=0, max_value=order << 4) + | st.integers(min_value=order >> 2, max_value=order + 1) + ) + sig = encode_sequence( + encode_integer(x), encode_integer(s), encode_boolean(b) + ) + + return verifying_key, sig + + +@settings(**slow_params) +@given(st_random_der_ecdsa_sig_value_y_boolean()) +def test_random_der_ecdsa_sig_value_y_boolean(params): # pragma: no cover + """ + Check if 'y' value encoded as BOOLEAN ECDSA-Sig-Value + structure is rejected as signature. + """ + verifying_key, sig = params + + # The compressed point verification is more strict than 'raw', + # sometimes the random numbers are producing valid point, so + # the signature verification will fail (BadSignatureError), + # but sometimes the test will fail with trying to create a point + # (MalformedPointError). + with pytest.raises((BadSignatureError, MalformedPointError)): + verifying_key.verify( + sig, example_data, sigdecode=sigdecode_der_extended + ) + + def st_der_integer(*args, **kwargs): # pragma: no cover """ Hypothesis strategy that returns a random positive integer as DER diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index 799e9b74..99bf2fc1 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -1,5 +1,7 @@ from __future__ import with_statement, division, print_function +from ecdsa import ellipticcurve + try: import unittest2 as unittest except ImportError: @@ -22,12 +24,23 @@ from . import util from .util import ( sigencode_der, + sigencode_der_sig_value_a, sigencode_strings, sigencode_strings_canonize, sigencode_string_canonize, sigencode_der_canonize, + sigencode_der_full_r, + sigencode_der_sig_value_y_boolean, + sigencode_der_sig_value_y_field_elem, +) +from .util import ( + sigdecode_der, + sigdecode_strings, + sigdecode_string, + sigdecode_der_full_r, + sigdecode_der_extended, + sigdecode_der_ecdsa_sig_extended, ) -from .util import sigdecode_der, sigdecode_strings, sigdecode_string from .util import number_to_string, encoded_oid_ecPublicKey, MalformedSignature from .curves import Curve, UnknownCurveError from .curves import ( @@ -985,6 +998,144 @@ def test_from_string_with_invalid_curve_too_long_ver_key_len(self): with self.assertRaises(MalformedPointError): VerifyingKey.from_string(b"\x00" * 16, curve) + def test_sigencode_der_ecdsa_sig_extended(self): + priv1 = SigningKey.generate() + pub1 = priv1.get_verifying_key() + data = b"data" + sig = priv1.sign(data, sigencode=sigencode_der) + + self.assertEqual(type(sig), binary_type) + self.assertTrue( + pub1.verify(sig, data, sigdecode=sigdecode_der_ecdsa_sig_extended) + ) + + def test_sigencode_der_full_r(self): + priv1 = SigningKey.generate() + pub1 = priv1.get_verifying_key() + data = b"data" + sig = priv1.sign(data, sigencode=sigencode_der_full_r, accelerate=True) + + self.assertEqual(type(sig), binary_type) + self.assertTrue(pub1.verify(sig, data, sigdecode=sigdecode_der_full_r)) + + def test_sigencode_der_full_r_uncompressed(self): + priv1 = SigningKey.generate() + pub1 = priv1.get_verifying_key() + data = b"data" + sig = priv1.sign(data, sigencode=sigencode_der_full_r, accelerate=True) + r, s = sigdecode_der_full_r(sig, None) + point = ellipticcurve.Point.from_bytes( + priv1.privkey.public_key.generator.curve(), r + ) + sig = sigencode_der_full_r(point, s, None, encoding="uncompressed") + + self.assertEqual(type(sig), binary_type) + self.assertTrue(pub1.verify(sig, data, sigdecode=sigdecode_der_full_r)) + + def test_sigencode_der_full_r_compressed(self): + priv1 = SigningKey.generate() + pub1 = priv1.get_verifying_key() + data = b"data" + sig = priv1.sign(data, sigencode=sigencode_der_full_r, accelerate=True) + r, s = sigdecode_der_full_r(sig, None) + point = ellipticcurve.Point.from_bytes( + priv1.privkey.public_key.generator.curve(), r + ) + sig = sigencode_der_full_r(point, s, None, encoding="compressed") + + self.assertEqual(type(sig), binary_type) + self.assertTrue(pub1.verify(sig, data, sigdecode=sigdecode_der_full_r)) + + def test_sigencode_der_sig_value_with_y_boolean(self): + priv1 = SigningKey.generate() + pub1 = priv1.get_verifying_key() + data = b"data" + sig = priv1.sign( + data, sigencode=sigencode_der_sig_value_y_boolean, accelerate=True + ) + self.assertEqual(type(sig), binary_type) + self.assertTrue( + pub1.verify(sig, data, sigdecode=sigdecode_der_extended) + ) + + def test_sigencode_der_sig_value_with_y_field_elem(self): + priv1 = SigningKey.generate() + pub1 = priv1.get_verifying_key() + data = b"data" + sig = priv1.sign( + data, + sigencode=sigencode_der_sig_value_y_field_elem, + accelerate=True, + ) + self.assertEqual(type(sig), binary_type) + self.assertTrue( + pub1.verify(sig, data, sigdecode=sigdecode_der_extended) + ) + + def test_sigdecode_der_sig_value_a(self): + sig = sigencode_der_sig_value_a(1, 1, None, 1) + with self.assertRaises(der.UnexpectedDER) as e: + sigdecode_der_extended(sig, None) + self.assertIn( + "Only prime field curves are supported.", str(e.exception) + ) + + def test_sigdecode_full_r_wrong_tag(self): + r = der.encode_octet_string(b"1") + s = der.encode_integer(1) + value = der.encode_sequence(r, s) + value_w_tag = der.encode_implicit(1, value) + + with self.assertRaises(der.UnexpectedDER) as e: + sigdecode_der_full_r(value_w_tag, None) + self.assertIn("ECDSA-Full-R must be taged with [0]", str(e.exception)) + + def test_sigdecode_der_full_r_junk_after_tag(self): + r = der.encode_octet_string(b"1") + s = der.encode_integer(1) + value = der.encode_sequence(r, s) + value_w_tag = der.encode_implicit(0, value) + value = value_w_tag + b"garbage" + with self.assertRaises(der.UnexpectedDER) as e: + sigdecode_der_full_r(value, None) + self.assertIn("trailing junk after DER sig", str(e.exception)) + + def test_sigdecode_der_full_r_junk_after_sequence(self): + r = der.encode_octet_string(b"1") + s = der.encode_integer(1) + value = der.encode_sequence(r, s) + value += b"garbage" + value_w_tag = der.encode_implicit(0, value) + with self.assertRaises(der.UnexpectedDER) as e: + sigdecode_der_full_r(value_w_tag, None) + self.assertIn("trailing junk after DER sig", str(e.exception)) + + def test_sigdecode_der_full_r_junk_after_numbers(self): + r = der.encode_octet_string(b"1") + s = der.encode_integer(1) + value = der.encode_sequence(r, s, b"garbage") + value_w_tag = der.encode_implicit(0, value) + with self.assertRaises(der.UnexpectedDER) as e: + sigdecode_der_full_r(value_w_tag, None) + self.assertIn("trailing junk after DER numbers", str(e.exception)) + + def test_sigdecode_der_ecdsa_sig_junk_after_sequence(self): + r = der.encode_integer(1) + s = der.encode_integer(1) + value = der.encode_sequence(r, s) + value += b"garbage" + with self.assertRaises(der.UnexpectedDER) as e: + sigdecode_der_ecdsa_sig_extended(value, None) + self.assertIn("trailing junk after DER sig", str(e.exception)) + + def test_sigdecode_der_ecdsa_sig_junk_after_numbers(self): + r = der.encode_integer(1) + s = der.encode_integer(1) + value = der.encode_sequence(r, s, b"garbage") + with self.assertRaises(der.UnexpectedDER) as e: + sigdecode_der_ecdsa_sig_extended(value, None) + self.assertIn("trailing junk after DER numbers", str(e.exception)) + @pytest.mark.parametrize( "val,even", [(i, j) for i in range(256) for j in [True, False]] @@ -1874,6 +2025,10 @@ def test_constructed(self): x = der.encode_constructed(1, unhexlify(b"0102030a0b0c")) self.assertEqual(hexlify(x), b"a106" + b"0102030a0b0c") + def test_boolean(self): + self.assertEqual(der.encode_boolean(True), b"\x01\01\xff") + self.assertEqual(der.encode_boolean(False), b"\x01\01\x00") + class Util(unittest.TestCase): @pytest.mark.slow @@ -1940,6 +2095,30 @@ def OFF_test_prove_uniformity(self): # pragma: no cover for i in range(1, order): print("%3d: %s" % (i, "*" * (counts[i] // 100))) + def sigencode_der_boolean_true(self): # pragma: no cover + point = Point(None, 3, 3, 2) + sig_der = sigencode_der_sig_value_y_boolean(point, 1, 2) + sig, empty = der.remove_sequence(sig_der) + r, rest = der.remove_integer(sig) + s, rest = der.remove_integer(rest) + b, empty = der.remove_boolean(rest) + self.assertEqual(b, True) + self.assertEqual(r, 1) + self.assertEqual(s, 1) + self.assertEqual(empty, b"") + + def sigencode_der_boolean_false(self): # pragma: no cover + point = Point(None, 3, 2, 2) + sig_der = sigencode_der_sig_value_y_boolean(point, 1, 2) + sig, empty = der.remove_sequence(sig_der) + r, rest = der.remove_integer(sig) + s, rest = der.remove_integer(rest) + b, empty = der.remove_boolean(rest) + self.assertEqual(b, False) + self.assertEqual(r, 1) + self.assertEqual(s, 1) + self.assertEqual(empty, b"") + class RFC6979(unittest.TestCase): # https://tools.ietf.org/html/rfc6979#appendix-A.1 @@ -1949,7 +2128,8 @@ def _do(self, generator, secexp, hsh, hash_func, expected): def test_SECP256k1(self): """RFC doesn't contain test vectors for SECP256k1 used in bitcoin. - This vector has been computed by Golang reference implementation instead.""" + This vector has been computed by Golang reference implementation instead. + """ self._do( generator=SECP256k1.generator, secexp=int("9d0219792467d7d37b4d43298a7d0c05", 16), diff --git a/src/ecdsa/util.py b/src/ecdsa/util.py index 1aff5bf5..56573689 100644 --- a/src/ecdsa/util.py +++ b/src/ecdsa/util.py @@ -9,6 +9,16 @@ :func:`sigencode_string_canonize`, :func:`sigencode_der_canonize`, :func:`sigdecode_strings`, :func:`sigdecode_string`, and :func:`sigdecode_der` functions. +The methods :func:`sigencode_der_full_r`, +:func:`sigencode_der_sig_value_y_boolean`, +:func:`sigencode_der_sig_value_y_field_elem` are implemented according to +ASN.1 extension of signature structure of ECDSA. +The extentions aim to accelerate the computations, +but the accelerations are not implemented! +The functions must be used with :func:`~ecdsa.keys.SigningKey.sign` +function with accelarate=True parameter, to recieve the whole point, +instead of 'r' as integer. +For decoding such signatures :func:`sigdecode_der_extended` must be used. """ from __future__ import division @@ -117,13 +127,11 @@ def __call__(self, numbytes): return bytes(a) def block_generator(self, seed): - counter = 0 + c = 0 while True: - for byte in sha256( - ("prng-%d-%s" % (counter, seed)).encode() - ).digest(): + for byte in sha256(("prng-%d-%s" % (c, seed)).encode()).digest(): yield byte - counter += 1 + c += 1 def randrange_from_seed__overshoot_modulo(seed, order): @@ -304,6 +312,144 @@ def sigencode_der(r, s, order): return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) +def sigencode_der_sig_value_y_field_elem(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, + a INTEGER OPTIONAL, + y CHOICE { b BOOLEAN, f FieldElement } OPTIONAL + } + + with 'y' being a field element. + + It's expected that this function will be used as a ``sigencode=`` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param ECPoint r: point 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 + """ + y = r.y() + r = r.x() % order + f = number_to_string(y, order) + + return der.encode_sequence( + der.encode_integer(r), + der.encode_integer(s), + der.encode_octet_string(f), + ) + + +def sigencode_der_sig_value_y_boolean(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, + a INTEGER OPTIONAL, + y CHOICE { b BOOLEAN, f FieldElement } OPTIONAL + } + + with 'y' being a boolean. + + It's expected that this function will be used as a ``sigencode=`` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param ECPoint r: point 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 + """ + y = r.y() + r = r.x() % order + b = False + if y & 1: + b = True + return der.encode_sequence( + der.encode_integer(r), der.encode_integer(s), der.encode_boolean(b) + ) + + +def sigencode_der_sig_value_a(r, s, order, a): + """ + The function is used for testing purposes. The 'a' value is + applicable for binary curves, wich the current implementation of + python-ecdsa does not support. + 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, + a INTEGER OPTIONAL, + y CHOICE { b BOOLEAN, f FieldElement } OPTIONAL + } + + with 'a' specified. + :param int r: the integer used to create the signature + :param int s: second parameter of the signature + :param int order: the order of the curve over which the signature was + computed + :param int a: the coefficient to be encoded + + :return: DER encoding of ECDSA signature + :rtype: bytes + """ + return der.encode_sequence( + der.encode_integer(r), der.encode_integer(s), der.encode_integer(a) + ) + + +def sigencode_der_full_r(r, s, order, encoding="raw"): + """ + Encode the signature into the ECDSA-Sig-Value structure using :term:`DER`. + + Encodes the signature to the following :term:`ASN.1` structure:: + + ECDSA-Full-R ::= SEQUENCE { + r ECPoint, + s INTEGER + } + + Encodes the ECPoint with chosen encoding. + If no encoding was provided, the 'raw' encoding will be used. + + It's expected that this function will be used as a ``sigencode=`` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param ECPoint r: the point used to create 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 + """ + r = r.to_bytes(encoding=encoding) + sig = der.encode_sequence( + der.encode_octet_string(r), der.encode_integer(s) + ) + # the ECDSA-FULL-R in ECDSA-Signature has a tag [0] + sig = der.encode_implicit(0, sig) + return sig + + def _canonize(s, order): """ Internal function for ensuring that the ``s`` value of a signature is in @@ -531,3 +677,147 @@ def sigdecode_der(sig_der, order): "trailing junk after DER numbers: %s" % binascii.hexlify(empty) ) return r, s + + +def sigdecode_der_extended(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 one of the choices:: + + ECDSA-Signature ::= CHOICE { + two-ints-plus ECDSA-Sig-Value, + point-int [0] ECDSA-Full-R, + ... -- Future representations may be added + } + + 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 + """ + sig_der = normalise_bytes(sig_der) + # what signature is encoded depends on the tag presence + if not der.is_sequence(sig_der): + return sigdecode_der_full_r(sig_der, order) + return sigdecode_der_ecdsa_sig_extended(sig_der, order) + + +def sigdecode_der_full_r(sig_der, order): + """ + Decoder for DER format of ECDSA-Full-R signatures. + + DER format of ECDSA-Full-R signature includes the point R + represented as an octet string: + + ECDSA-Full-R ::= SEQUENCE { + r ECPoint, + s INTEGER + } + + ECPoint ::= OCTET STRING + + The function transforms the r octet string to integer. + + :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 + """ + + tag, body, empty = der.remove_implicit(sig_der) + if tag != 0: + raise der.UnexpectedDER("ECDSA-Full-R must be taged with [0]") + if empty != b"": + raise der.UnexpectedDER( + "trailing junk after DER sig: %s" % binascii.hexlify(empty) + ) + body, empty = der.remove_sequence(body) + if empty != b"": + raise der.UnexpectedDER( + "trailing junk after DER sig: %s" % binascii.hexlify(empty) + ) + r, rest = der.remove_octet_string(body) + s, empty = der.remove_integer(rest) + if empty != b"": + raise der.UnexpectedDER( + "trailing junk after DER numbers: %s" % binascii.hexlify(empty) + ) + return r, s + + +def sigdecode_der_ecdsa_sig_extended(sig_der, order): + """ + Decoder for DER format of ECDSA-Sig-Value signatures. + + DER format of ECDSA-Sig-Value signature includes the mandotary r + and s values, but also may has additional inforamtion: + + ECDSA-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER, + a INTEGER OPTIONAL, + y CHOICE { b BOOLEAN, f FieldElement } OPTIONAL + } + + FieldElement ::= OCTET STRING + + :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 + """ + + rs_strings, empty = der.remove_sequence(sig_der) + if empty != b"": + raise der.UnexpectedDER( + "trailing junk after DER sig: %s" % binascii.hexlify(empty) + ) + r, rest = der.remove_integer(rs_strings) + s, optional = der.remove_integer(rest) + empty = optional + r_point = None + + # check if "a" is present + if optional[:1] == b"\x02": + raise der.UnexpectedDER("Only prime field curves are supported.") + # check if "y" is present as boolean + if optional[:1] == b"\x01": + y, empty = der.remove_boolean(optional) + r_octet = number_to_string(r, order) + # In point compression True is odd b"\x03", False is even b"\x02" + if y: + r_point = b"\x03" + r_octet + else: + r_point = b"\x02" + r_octet + # or if "y" is present as FieldElement + elif optional[:1] == b"\x04": + y, empty = der.remove_octet_string(optional) + r_octet = number_to_string(r, order) + r_point = r_octet + y + if empty != b"": + raise der.UnexpectedDER( + "trailing junk after DER numbers: %s" % binascii.hexlify(empty) + ) + if r_point: + return r_point, s + return r, s