diff --git a/NEWS b/NEWS index 5f27c16..0d066f4 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,25 @@ +* Release 0.19.1 (13 Mar 2025) + +New API: +* ``der.remove_implitic`` and ``der.encode_implicit`` for decoding and + encoding DER IMPLICIT values with custom tag values and arbitrary + classes + +Bug fixes: +* Minor fixes around arithmetic with curves that have non-prime order + (useful for experimentation, not practical deployments) +* Fix arithmetic to work with curves that have (0, 0) on the curve +* Fix canonicalization of signatures when ``s`` is just slightly + above half of curve order + +Maintenance: +* Dropped official support for Python 3.5 (again, issues with CI, support + for Python 2.6 and Python 2.7 is unchanged) +* Officialy support Python 3.12 and 3.13 (add them to CI) +* Removal of few more unnecessary `six.b` literals (Alexandre Detiste) +* Fix typos in warning messages + + * Release 0.19.0 (08 Apr 2024) New API: diff --git a/PKG-INFO b/PKG-INFO index a9ec87a..3b4026c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: ecdsa -Version: 0.19.0 +Version: 0.19.2 Summary: ECDSA cryptographic signature library (pure python) Home-page: http://github.com/tlsfuzzer/python-ecdsa Author: Brian Warner @@ -11,7 +11,6 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 @@ -19,15 +18,19 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 -Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +Classifier: Programming Language :: Python :: 3.13 +Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.* Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: six>=1.9.0 Provides-Extra: gmpy2 +Requires-Dist: gmpy2; extra == "gmpy2" Provides-Extra: gmpy -License-File: LICENSE +Requires-Dist: gmpy; extra == "gmpy" # Pure-Python ECDSA and ECDH -[![Build Status](https://github.com/tlsfuzzer/python-ecdsa/workflows/GitHub%20CI/badge.svg?branch=master)](https://github.com/tlsfuzzer/python-ecdsa/actions?query=workflow%3A%22GitHub+CI%22+branch%3Amaster) +[![GitHub CI](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml/badge.svg)](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml) [![Documentation Status](https://readthedocs.org/projects/ecdsa/badge/?version=latest)](https://ecdsa.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/tlsfuzzer/python-ecdsa/badge.svg?branch=master)](https://coveralls.io/github/tlsfuzzer/python-ecdsa?branch=master) ![condition coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tomato42/9b6ca1f3410207fbeca785a178781651/raw/python-ecdsa-condition-coverage.json) @@ -72,7 +75,7 @@ curves over prime fields. ## Dependencies This library uses only Python and the 'six' package. It is compatible with -Python 2.6, 2.7, and 3.5+. It also supports execution on alternative +Python 2.6, 2.7, and 3.6+. It also supports execution on alternative implementations like pypy and pypy3. If `gmpy2` or `gmpy` is installed, they will be used for faster arithmetic. diff --git a/README.md b/README.md index 6c43f85..0bf02bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pure-Python ECDSA and ECDH -[![Build Status](https://github.com/tlsfuzzer/python-ecdsa/workflows/GitHub%20CI/badge.svg?branch=master)](https://github.com/tlsfuzzer/python-ecdsa/actions?query=workflow%3A%22GitHub+CI%22+branch%3Amaster) +[![GitHub CI](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml/badge.svg)](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml) [![Documentation Status](https://readthedocs.org/projects/ecdsa/badge/?version=latest)](https://ecdsa.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/tlsfuzzer/python-ecdsa/badge.svg?branch=master)](https://coveralls.io/github/tlsfuzzer/python-ecdsa?branch=master) ![condition coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tomato42/9b6ca1f3410207fbeca785a178781651/raw/python-ecdsa-condition-coverage.json) @@ -45,7 +45,7 @@ curves over prime fields. ## Dependencies This library uses only Python and the 'six' package. It is compatible with -Python 2.6, 2.7, and 3.5+. It also supports execution on alternative +Python 2.6, 2.7, and 3.6+. It also supports execution on alternative implementations like pypy and pypy3. If `gmpy2` or `gmpy` is installed, they will be used for faster arithmetic. diff --git a/debian/changelog b/debian/changelog index 31d77b3..70e3367 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,23 @@ +python-ecdsa (0.19.2-1) unstable; urgency=medium + + * Team upload. + * New upstream version 0.19.2 (Closes: #1132164) CVE-2026-33936 + * Disable useless Salsa CI jobs + * Bump Standards-Version to 4.7.3, drop Priority: tag + * Drop "Rules-Requires-Root: no": it is the default now + * Rewrite d/watch in v5 format + * Refresh remove_six.patch + + -- Alexandre Detiste Wed, 01 Apr 2026 10:19:06 +0200 + +python-ecdsa (0.19.1-1) unstable; urgency=medium + + * Team upload. + * New upstream release. + * Use pybuild-plugin-pyproject. + + -- Colin Watson Sun, 23 Mar 2025 16:27:42 +0000 + python-ecdsa (0.19.0-2) unstable; urgency=medium * Team upload. diff --git a/debian/control b/debian/control index 443ce15..e822992 100644 --- a/debian/control +++ b/debian/control @@ -1,11 +1,11 @@ Source: python-ecdsa Section: python -Priority: optional Maintainer: Debian Python Team Uploaders: Josue Ortega Build-Depends: debhelper-compat (= 13), dh-sequence-python3, + pybuild-plugin-pyproject, python3-setuptools, python3-all, python3-pytest, @@ -14,8 +14,7 @@ Build-Depends: Homepage: https://github.com/warner/python-ecdsa Vcs-Browser: https://salsa.debian.org/python-team/packages/python-ecdsa Vcs-Git: https://salsa.debian.org/python-team/packages/python-ecdsa.git -Rules-Requires-Root: no -Standards-Version: 4.6.1 +Standards-Version: 4.7.3 Package: python3-ecdsa Architecture: all diff --git a/debian/patches/00-remove-temp-test-dir.patch b/debian/patches/00-remove-temp-test-dir.patch index 550197b..6ffed87 100644 --- a/debian/patches/00-remove-temp-test-dir.patch +++ b/debian/patches/00-remove-temp-test-dir.patch @@ -8,12 +8,14 @@ Forwarded: not-needed dirty tree module at installation time --- - src/ecdsa/test_pyecdsa.py | 21 +++++++++++++++++++++ - 1 file changed, 21 insertions(+) + src/ecdsa/test_pyecdsa.py | 20 ++++++++++++++++++++ + 1 file changed, 20 insertions(+) +diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py +index 799e9b7..7a16d65 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py -@@ -99,6 +99,11 @@ +@@ -99,6 +99,11 @@ def run_openssl(cmd): class ECDSA(unittest.TestCase): @@ -25,7 +27,7 @@ dirty tree module at installation time def test_basic(self): priv = SigningKey.generate() pub = priv.get_verifying_key() -@@ -1768,6 +1773,10 @@ +@@ -1812,6 +1817,10 @@ class TooSmallCurve(unittest.TestCase): class DER(unittest.TestCase): @@ -36,7 +38,7 @@ dirty tree module at installation time def test_integer(self): self.assertEqual(der.encode_integer(0), b"\x02\x01\x00") self.assertEqual(der.encode_integer(1), b"\x02\x01\x01") -@@ -1832,6 +1841,10 @@ +@@ -1876,6 +1885,10 @@ class DER(unittest.TestCase): class Util(unittest.TestCase): @@ -47,7 +49,7 @@ dirty tree module at installation time @pytest.mark.slow def test_trytryagain(self): tta = util.randrange_from_seed__trytryagain -@@ -1898,6 +1911,9 @@ +@@ -1942,6 +1955,9 @@ class Util(unittest.TestCase): class RFC6979(unittest.TestCase): @@ -57,7 +59,7 @@ dirty tree module at installation time # https://tools.ietf.org/html/rfc6979#appendix-A.1 def _do(self, generator, secexp, hsh, hash_func, expected): actual = rfc6979.generate_k(generator.order(), secexp, hash_func, hsh) -@@ -2102,6 +2118,10 @@ +@@ -2144,6 +2160,10 @@ class RFC6979(unittest.TestCase): class ECDH(unittest.TestCase): diff --git a/debian/patches/remove-six.patch b/debian/patches/remove-six.patch index 3ea924e..ddcb185 100644 --- a/debian/patches/remove-six.patch +++ b/debian/patches/remove-six.patch @@ -1,41 +1,14 @@ From: shixuantong Date: Wed, 13 Apr 2022 00:04:12 +0800 Subject: [PATCH] drop six -Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 ---- - .travis.yml | 1 - - setup.py | 2 +- - speed.py | 7 +- - src/ecdsa/__init__.py | 5 - - src/ecdsa/_compat.py | 4 +- - src/ecdsa/curves.py | 3 +- - src/ecdsa/der.py | 37 ++-- - src/ecdsa/ecdsa.py | 7 +- - src/ecdsa/ellipticcurve.py | 2 - - src/ecdsa/keys.py | 9 +- - src/ecdsa/numbertheory.py | 13 +- - src/ecdsa/test_der.py | 39 ++--- - src/ecdsa/test_pyecdsa.py | 343 ++++++++++++++++--------------------- - src/ecdsa/util.py | 12 +- - tox.ini | 3 - - 15 files changed, 205 insertions(+), 282 deletions(-) +Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 ---- a/.travis.yml -+++ b/.travis.yml -@@ -87,7 +87,6 @@ - env: PATH=/c/Python38:/c/Python38/Scripts:$PATH - install: - - pip list -- - pip install six - - pip install -r build-requirements.txt - - pip list - script: --- a/setup.py +++ b/setup.py -@@ -43,6 +43,6 @@ - "Programming Language :: Python :: 3.11", +@@ -44,6 +44,6 @@ setup( "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], - install_requires=["six>=1.9.0"], + install_requires=[], @@ -50,7 +23,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 from .keys import ( SigningKey, VerifyingKey, -@@ -56,7 +53,6 @@ +@@ -56,7 +53,6 @@ __all__ = [ "numbertheory", "test_pyecdsa", "util", @@ -58,7 +31,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 ] _hush_pyflakes = [ -@@ -90,7 +86,7 @@ +@@ -90,7 +86,7 @@ _hush_pyflakes = [ SECP160r1, Ed25519, Ed448, @@ -69,7 +42,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 BRAINPOOLP224t1, --- a/src/ecdsa/_compat.py +++ b/src/ecdsa/_compat.py -@@ -4,13 +4,11 @@ +@@ -4,13 +4,11 @@ Common functions for providing cross-python version compatibility. import sys import re import binascii @@ -93,7 +66,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 from . import der, ecdsa, ellipticcurve, eddsa from .util import orderlen, number_to_string, string_to_number from ._compat import normalise_bytes, bit_length -@@ -269,7 +268,7 @@ +@@ -269,7 +268,7 @@ class Curve: and ``explicit`` :type valid_encodings: :term:`set-like object` """ @@ -104,7 +77,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 ec_param_index = string.find(b"-----BEGIN EC PARAMETERS-----") --- a/src/ecdsa/der.py +++ b/src/ecdsa/der.py -@@ -4,9 +4,10 @@ +@@ -4,9 +4,10 @@ import binascii import base64 import warnings from itertools import chain @@ -116,7 +89,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 class UnexpectedDER(Exception): pass -@@ -386,7 +387,7 @@ +@@ -461,7 +462,7 @@ def remove_bitstring(string, expect_unused=_sentry): def unpem(pem): @@ -127,7 +100,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 d = b"".join( --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py -@@ -65,12 +65,13 @@ +@@ -65,12 +65,13 @@ modified as part of the python-ecdsa package. """ import warnings @@ -144,7 +117,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 pass --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py -@@ -47,14 +47,12 @@ +@@ -47,14 +47,12 @@ except ImportError: # pragma: no branch GMPY = False @@ -161,7 +134,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 :term:`Short Weierstrass Elliptic Curve ` over a --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py -@@ -5,7 +5,6 @@ +@@ -5,7 +5,6 @@ Primary classes for performing signing and verification operations. import binascii from hashlib import sha1 import os @@ -169,7 +142,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 from . import ecdsa, eddsa from . import der, ssh from . import rfc6979 -@@ -963,7 +962,7 @@ +@@ -963,7 +962,7 @@ class SigningKey(object): :return: Initialised SigningKey object :rtype: SigningKey """ @@ -192,7 +165,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 try: xrange -@@ -218,11 +217,7 @@ +@@ -218,11 +219,7 @@ def square_root_mod_prime(a, p): assert d == p - 1 return (2 * a * pow(4 * a, (p - 5) // 8, p)) % p @@ -207,15 +180,15 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 f = (a, -b, 1) --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py -@@ -16,7 +16,6 @@ +@@ -16,7 +16,6 @@ from functools import partial from hypothesis import given, settings import hypothesis.strategies as st --from six import b, print_, binary_type +-from six import binary_type from .keys import SigningKey, VerifyingKey from .keys import BadSignatureError, MalformedPointError, BadDigestError from . import util -@@ -70,6 +69,7 @@ +@@ -70,6 +69,7 @@ from . import der from . import rfc6979 from . import ecdsa @@ -223,7 +196,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 class SubprocessError(Exception): pass -@@ -293,7 +293,7 @@ +@@ -293,7 +293,7 @@ class ECDSA(unittest.TestCase): priv1 = SigningKey.generate() pub1 = priv1.get_verifying_key() s1 = pub1.to_string() @@ -232,7 +205,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 self.assertEqual(len(s1), NIST192p.verifying_key_length) pub2 = VerifyingKey.from_string(s1) self.assertTruePubkeysEqual(pub1, pub2) -@@ -345,13 +345,13 @@ +@@ -345,13 +345,13 @@ class ECDSA(unittest.TestCase): priv1 = SigningKey.generate(curve=BRAINPOOLP512r1) pub1 = priv1.get_verifying_key() s1 = pub1.to_string() @@ -248,210 +221,9 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 pub2 = VerifyingKey.from_der(pub1_der) self.assertTruePubkeysEqual(pub1, pub2) -@@ -370,9 +370,7 @@ - - def test_vk_from_der_garbage_after_curve_oid(self): - type_oid_der = encoded_oid_ecPublicKey -- curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + b( -- "garbage" -- ) -+ curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + b"garbage" - enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) - point_der = der.encode_bitstring(b"\x00\xff", None) - to_decode = der.encode_sequence(enc_type_der, point_der) -@@ -775,10 +773,10 @@ - sk = SigningKey.from_secret_exponent(123456789) - vk = sk.verifying_key - -- exp = b( -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ exp = ( -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - self.assertEqual(vk.to_string(), exp) - self.assertEqual(vk.to_string("raw"), exp) -@@ -790,10 +788,10 @@ - sk = SigningKey.from_secret_exponent(123456789) - vk = sk.verifying_key - -- enc = b( -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - - from_raw = VerifyingKey.from_string(enc) -@@ -809,11 +807,11 @@ - self.assertEqual(from_uncompressed.pubkey.point, vk.pubkey.point) - - def test_uncompressed_decoding_as_only_alowed(self): -- enc = b( -- "\x04" -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x04" -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - vk = VerifyingKey.from_string(enc, valid_encodings=("uncompressed",)) - sk = SigningKey.from_secret_exponent(123456789) -@@ -821,10 +819,10 @@ - self.assertEqual(vk, sk.verifying_key) - - def test_raw_decoding_with_blocked_format(self): -- enc = b( -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - with self.assertRaises(MalformedPointError) as exp: - VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) -@@ -838,11 +836,11 @@ - self.assertIn("Only uncompressed, compressed", str(e.exception)) - - def test_uncompressed_decoding_with_blocked_format(self): -- enc = b( -- "\x04" -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x04" -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - with self.assertRaises(MalformedPointError) as exp: - VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) -@@ -850,11 +848,11 @@ - self.assertIn("Invalid X9.62 encoding", str(exp.exception)) - - def test_hybrid_decoding_with_blocked_format(self): -- enc = b( -- "\x06" -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x06" -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - with self.assertRaises(MalformedPointError) as exp: - VerifyingKey.from_string(enc, valid_encodings=("uncompressed",)) -@@ -862,11 +860,11 @@ - self.assertIn("Invalid X9.62 encoding", str(exp.exception)) - - def test_compressed_decoding_with_blocked_format(self): -- enc = b( -- "\x02" -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x02" -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - )[:25] - with self.assertRaises(MalformedPointError) as exp: - VerifyingKey.from_string(enc, valid_encodings=("hybrid", "raw")) -@@ -874,40 +872,40 @@ - self.assertIn("(hybrid, raw)", str(exp.exception)) - - def test_decoding_with_malformed_uncompressed(self): -- enc = b( -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - - with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b"\x02" + enc) - - def test_decoding_with_malformed_compressed(self): -- enc = b( -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - - with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b"\x01" + enc[:24]) - - def test_decoding_with_inconsistent_hybrid(self): -- enc = b( -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - - with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b"\x07" + enc) - - def test_decoding_with_point_not_on_curve(self): -- enc = b( -- "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -- "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -- "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" -+ enc = ( -+ b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" -+ b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" -+ b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" - ) - - with self.assertRaises(MalformedPointError): -@@ -1907,7 +1905,7 @@ - # this technique should use the full range - self.assertTrue(counts[order - 1]) - for i in range(1, order): -- print_("%3d: %s" % (i, "*" * (counts[i] // 100))) -+ print("%3d: %s" % (i, "*" * (counts[i] // 100))) - - - class RFC6979(unittest.TestCase): -@@ -1997,9 +1995,7 @@ - ), - secexp=int("09A4D6792295A7F730FC3F2B49CBC0F62E862272F", 16), - hsh=unhexlify( -- b( -- "AF2BDBE1AA9B6EC1E2ADE1D694F41FC71A831D0268E9891562113D8A62ADD1BF" -- ) -+ b"AF2BDBE1AA9B6EC1E2ADE1D694F41FC71A831D0268E9891562113D8A62ADD1BF" - ), - hash_func=hashlib.sha256, - expected=int("23AF4074C90A02B3FE61D286D5C87F425E6BDD81B", 16), --- a/src/ecdsa/util.py +++ b/src/ecdsa/util.py -@@ -18,10 +18,11 @@ +@@ -18,10 +18,11 @@ import math import binascii import sys from hashlib import sha256 @@ -464,7 +236,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 # RFC5480: # The "unrestricted" algorithm identifier is: -@@ -111,10 +112,7 @@ +@@ -111,10 +112,7 @@ class PRNG: def __call__(self, numbytes): a = [next(self.generator) for i in range(numbytes)] @@ -476,7 +248,7 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 def block_generator(self, seed): counter = 0 -@@ -185,7 +183,7 @@ +@@ -185,7 +183,7 @@ def randrange_from_seed__truncate_bits(seed, order, hashmod=sha256): base = "\x00" * (maxbytes - len(base)) + base topbits = 8 * maxbytes - bits if topbits: @@ -485,22 +257,3 @@ Forwarded: https://github.com/tlsfuzzer/python-ecdsa/pull/294 number = 1 + int(binascii.hexlify(base), 16) assert 1 <= number < order return number ---- a/tox.ini -+++ b/tox.ini -@@ -12,8 +12,6 @@ - gmpypy{27,39,310,311,312}: gmpy - gmpy{2py27,2py39,2py310,2py311,2py312,py27,py39,py310,py311,py312}: pytest - gmpy{2py27,2py39,2py310,2py311,2py312,py27,py39,py310,py311,py312}: hypothesis --# six==1.9.0 comes from setup.py install_requires -- py27_old_six: six==1.9.0 - py27_old_six: pytest - py27_old_six: hypothesis - # those are the oldest versions of gmpy and gmpy2 on PyPI (i.e. oldest we can -@@ -76,7 +74,6 @@ - hypothesis - pytest>=4.6.0 - coverage -- six - commands = - instrumental -t ecdsa -i '.*test_.*|.*_version|.*_compat|.*_sha3' {envbindir}/pytest {posargs:src/ecdsa} - instrumental -f .instrumental.cov -sr diff --git a/debian/rules b/debian/rules index b3011fb..c2d0d59 100755 --- a/debian/rules +++ b/debian/rules @@ -1,6 +1,7 @@ #!/usr/bin/make -f export PYBUILD_NAME=ecdsa +export PYBUILD_TEST_ARGS=-k 'not test_SigningKey_from_pem_pkcs8v2_EdDSA' %: dh $@ --buildsystem=pybuild diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml index 0c22dc4..c614482 100644 --- a/debian/salsa-ci.yml +++ b/debian/salsa-ci.yml @@ -1,3 +1,8 @@ +--- include: - - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml - - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml + +variables: + SALSA_CI_DISABLE_BLHC: 1 + SALSA_CI_DISABLE_BUILD_PACKAGE_ALL: 1 + SALSA_CI_DISABLE_BUILD_PACKAGE_ANY: 1 diff --git a/debian/watch b/debian/watch index 468a997..58a11c7 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,5 @@ -version=4 -opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ -https://pypi.debian.net/ecdsa/ecdsa-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) +Version: 5 + +Template: Pypi +Dist: ecdsa +Uversion-Mangle: auto diff --git a/docs/source/conf.py b/docs/source/conf.py index 0581c1c..d86ea49 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,9 @@ html_static_path = ["_static"] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"https://docs.python.org/": None} +intersphinx_mapping = { + "python": ("https://docs.python.org/", None), +} autodoc_default_options = { "undoc-members": True, diff --git a/setup.py b/setup.py index a9ae243..618f5e6 100755 --- a/setup.py +++ b/setup.py @@ -27,14 +27,14 @@ package_dir={"": "src"}, license="MIT", cmdclass=commands, - python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", + python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, " + "!=3.5.*", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -42,6 +42,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], install_requires=["six>=1.9.0"], extras_require={"gmpy2": "gmpy2", "gmpy": "gmpy"}, diff --git a/src/ecdsa.egg-info/PKG-INFO b/src/ecdsa.egg-info/PKG-INFO index a9ec87a..3b4026c 100644 --- a/src/ecdsa.egg-info/PKG-INFO +++ b/src/ecdsa.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: ecdsa -Version: 0.19.0 +Version: 0.19.2 Summary: ECDSA cryptographic signature library (pure python) Home-page: http://github.com/tlsfuzzer/python-ecdsa Author: Brian Warner @@ -11,7 +11,6 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 @@ -19,15 +18,19 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 -Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +Classifier: Programming Language :: Python :: 3.13 +Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.* Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: six>=1.9.0 Provides-Extra: gmpy2 +Requires-Dist: gmpy2; extra == "gmpy2" Provides-Extra: gmpy -License-File: LICENSE +Requires-Dist: gmpy; extra == "gmpy" # Pure-Python ECDSA and ECDH -[![Build Status](https://github.com/tlsfuzzer/python-ecdsa/workflows/GitHub%20CI/badge.svg?branch=master)](https://github.com/tlsfuzzer/python-ecdsa/actions?query=workflow%3A%22GitHub+CI%22+branch%3Amaster) +[![GitHub CI](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml/badge.svg)](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml) [![Documentation Status](https://readthedocs.org/projects/ecdsa/badge/?version=latest)](https://ecdsa.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/tlsfuzzer/python-ecdsa/badge.svg?branch=master)](https://coveralls.io/github/tlsfuzzer/python-ecdsa?branch=master) ![condition coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tomato42/9b6ca1f3410207fbeca785a178781651/raw/python-ecdsa-condition-coverage.json) @@ -72,7 +75,7 @@ curves over prime fields. ## Dependencies This library uses only Python and the 'six' package. It is compatible with -Python 2.6, 2.7, and 3.5+. It also supports execution on alternative +Python 2.6, 2.7, and 3.6+. It also supports execution on alternative implementations like pypy and pypy3. If `gmpy2` or `gmpy` is installed, they will be used for faster arithmetic. diff --git a/src/ecdsa/_version.py b/src/ecdsa/_version.py index cf329eb..de8e05b 100644 --- a/src/ecdsa/_version.py +++ b/src/ecdsa/_version.py @@ -8,11 +8,11 @@ version_json = ''' { - "date": "2024-04-08T20:59:55+0200", + "date": "2026-03-26T10:50:34+0100", "dirty": false, "error": null, - "full-revisionid": "be70016f8911f79e891a65dcfcb602e5ba866ed3", - "version": "0.19.0" + "full-revisionid": "bd66899550d7185939bf27b75713a2ac9325a9d3", + "version": "0.19.2" } ''' # END VERSION_JSON diff --git a/src/ecdsa/der.py b/src/ecdsa/der.py index b291485..5d35d69 100644 --- a/src/ecdsa/der.py +++ b/src/ecdsa/der.py @@ -16,6 +16,32 @@ def encode_constructed(tag, value): return int2byte(0xA0 + tag) + encode_length(len(value)) + value +def encode_implicit(tag, value, cls="context-specific"): + """ + Encode and IMPLICIT value using :term:`DER`. + + :param int tag: the tag value to encode, must be between 0 an 31 inclusive + :param bytes value: the data to encode + :param str cls: the class of the tag to encode: "application", + "context-specific", or "private" + :rtype: bytes + """ + if cls not in ("application", "context-specific", "private"): + raise ValueError("invalid tag class") + if tag > 31: + raise ValueError("Long tags not supported") + + if cls == "application": + tag_class = 0b01000000 + elif cls == "context-specific": + tag_class = 0b10000000 + else: + assert cls == "private" + tag_class = 0b11000000 + + return int2byte(tag_class + tag) + encode_length(len(value)) + value + + def encode_integer(r): assert r >= 0 # can't support negative numbers yet h = ("%x" % r).encode() @@ -137,6 +163,53 @@ def remove_constructed(string): ) tag = s0 & 0x1F length, llen = read_length(string[1:]) + if length > len(string) - 1 - llen: + raise UnexpectedDER("Length longer than the provided buffer") + body = string[1 + llen : 1 + llen + length] + rest = string[1 + llen + length :] + return tag, body, rest + + +def remove_implicit(string, exp_class="context-specific"): + """ + Removes an IMPLICIT tagged value from ``string`` following :term:`DER`. + + :param bytes string: a byte string that can have one or more + DER elements. + :param str exp_class: the expected tag class of the implicitly + encoded value. Possible values are: "context-specific", "application", + and "private". + :return: a tuple with first value being the tag without indicator bits, + second being the raw bytes of the value and the third one being + remaining bytes (or an empty string if there are none) + :rtype: tuple(int,bytes,bytes) + """ + if exp_class not in ("context-specific", "application", "private"): + raise ValueError("invalid `exp_class` value") + if exp_class == "application": + tag_class = 0b01000000 + elif exp_class == "context-specific": + tag_class = 0b10000000 + else: + assert exp_class == "private" + tag_class = 0b11000000 + tag_mask = 0b11000000 + + s0 = str_idx_as_int(string, 0) + + if (s0 & tag_mask) != tag_class: + raise UnexpectedDER( + "wanted class {0}, got 0x{1:02x} tag".format(exp_class, s0) + ) + if s0 & 0b00100000 != 0: + raise UnexpectedDER( + "wanted type primitive, got 0x{0:02x} tag".format(s0) + ) + + tag = s0 & 0x1F + length, llen = read_length(string[1:]) + if length > len(string) - 1 - llen: + raise UnexpectedDER("Length longer than the provided buffer") body = string[1 + llen : 1 + llen + length] rest = string[1 + llen + length :] return tag, body, rest @@ -160,6 +233,8 @@ def remove_octet_string(string): n = str_idx_as_int(string, 0) raise UnexpectedDER("wanted type 'octetstring' (0x04), got 0x%02x" % n) length, llen = read_length(string[1:]) + if length > len(string) - 1 - llen: + raise UnexpectedDER("Length longer than the provided buffer") body = string[1 + llen : 1 + llen + length] rest = string[1 + llen + length :] return body, rest diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index 9284ace..f710965 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -40,16 +40,16 @@ # Verifying a signature for a hash value: if pubkey.verifies( hash, signature ): - print_("Demo verification succeeded.") + print("Demo verification succeeded.") else: - print_("*** Demo verification failed.") + print("*** Demo verification failed.") # Verification fails if the hash value is modified: if pubkey.verifies( hash-1, signature ): - print_("**** Demo verification failed to reject tampered hash.") + print("**** Demo verification failed to reject tampered hash.") else: - print_("Demo verification correctly rejected tampered hash.") + print("Demo verification correctly rejected tampered hash.") Revision history: 2005.12.31 - Initial version. @@ -275,7 +275,7 @@ def int_to_string(x): # pragma: no cover # deprecated in 0.19 warnings.warn( "Function is unused in library code. If you use this code, " - "change to util.string_to_number.", + "change to util.number_to_string.", DeprecationWarning, ) assert x >= 0 @@ -296,7 +296,7 @@ def string_to_int(s): # pragma: no cover # deprecated in 0.19 warnings.warn( "Function is unused in library code. If you use this code, " - "change to util.number_to_string.", + "change to util.string_to_number.", DeprecationWarning, ) result = 0 diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index 18816a6..a982c1e 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -633,7 +633,7 @@ def __eq__(self, other): """ x1, y1, z1 = self.__coords if other is INFINITY: - return not y1 or not z1 + return not z1 if isinstance(other, Point): x2, y2, z2 = other.x(), other.y(), 1 elif isinstance(other, PointJacobi): @@ -723,11 +723,13 @@ def scale(self): def to_affine(self): """Return point in affine form.""" - _, y, z = self.__coords - if not y or not z: + _, _, z = self.__coords + p = self.__curve.p() + if not (z % p): return INFINITY self.scale() x, y, z = self.__coords + assert z == 1 return Point(self.__curve, x, y, self.__order) @staticmethod @@ -759,7 +761,7 @@ def _double_with_z_1(self, X1, Y1, p, a): # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-mdbl-2007-bl XX, YY = X1 * X1 % p, Y1 * Y1 % p if not YY: - return 0, 0, 1 + return 0, 0, 0 YYYY = YY * YY % p S = 2 * ((X1 + YY) ** 2 - XX - YYYY) % p M = 3 * XX + a @@ -773,13 +775,13 @@ def _double(self, X1, Y1, Z1, p, a): """Add a point to itself, arbitrary z.""" if Z1 == 1: return self._double_with_z_1(X1, Y1, p, a) - if not Y1 or not Z1: - return 0, 0, 1 + if not Z1: + return 0, 0, 0 # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl XX, YY = X1 * X1 % p, Y1 * Y1 % p if not YY: - return 0, 0, 1 + return 0, 0, 0 YYYY = YY * YY % p ZZ = Z1 * Z1 % p S = 2 * ((X1 + YY) ** 2 - XX - YYYY) % p @@ -795,14 +797,14 @@ def double(self): """Add a point to itself.""" X1, Y1, Z1 = self.__coords - if not Y1: + if not Z1: return INFINITY p, a = self.__curve.p(), self.__curve.a() X3, Y3, Z3 = self._double(X1, Y1, Z1, p, a) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -886,10 +888,10 @@ def __radd__(self, other): def _add(self, X1, Y1, Z1, X2, Y2, Z2, p): """add two points, select fastest method.""" - if not Y1 or not Z1: - return X2, Y2, Z2 - if not Y2 or not Z2: - return X1, Y1, Z1 + if not Z1: + return X2 % p, Y2 % p, Z2 % p + if not Z2: + return X1 % p, Y1 % p, Z1 % p if Z1 == Z2: if Z1 == 1: return self._add_with_z_1(X1, Y1, X2, Y2, p) @@ -917,7 +919,7 @@ def __add__(self, other): X3, Y3, Z3 = self._add(X1, Y1, Z1, X2, Y2, Z2, p) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -927,7 +929,7 @@ def __rmul__(self, other): def _mul_precompute(self, other): """Multiply point by integer with precomputation table.""" - X3, Y3, Z3, p = 0, 0, 1, self.__curve.p() + X3, Y3, Z3, p = 0, 0, 0, self.__curve.p() _add = self._add for X2, Y2 in self.__precompute: if other % 2: @@ -940,7 +942,7 @@ def _mul_precompute(self, other): else: other //= 2 - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -959,7 +961,7 @@ def __mul__(self, other): self = self.scale() X2, Y2, _ = self.__coords - X3, Y3, Z3 = 0, 0, 1 + X3, Y3, Z3 = 0, 0, 0 p, a = self.__curve.p(), self.__curve.a() _double = self._double _add = self._add @@ -972,7 +974,7 @@ def __mul__(self, other): elif i > 0: X3, Y3, Z3 = _add(X3, Y3, Z3, X2, Y2, 1, p) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -1001,7 +1003,7 @@ def mul_add(self, self_mul, other, other_mul): other_mul = other_mul % self.__order # (X3, Y3, Z3) is the accumulator - X3, Y3, Z3 = 0, 0, 1 + X3, Y3, Z3 = 0, 0, 0 p, a = self.__curve.p(), self.__curve.a() # as we have 6 unique points to work with, we can't scale all of them, @@ -1025,7 +1027,7 @@ def mul_add(self, self_mul, other, other_mul): # when the self and other sum to infinity, we need to add them # one by one to get correct result but as that's very unlikely to # happen in regular operation, we don't need to optimise this case - if not pApB_Y or not pApB_Z: + if not pApB_Z: return self * self_mul + other * other_mul # gmp object creation has cumulatively higher overhead than the @@ -1070,7 +1072,7 @@ def mul_add(self, self_mul, other, other_mul): assert B > 0 X3, Y3, Z3 = _add(X3, Y3, Z3, pApB_X, pApB_Y, pApB_Z, p) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -1154,6 +1156,8 @@ def __eq__(self, other): Note: only points that lay on the same curve can be equal. """ + if other is INFINITY: + return self.__x is None or self.__y is None if isinstance(other, Point): return ( self.__curve == other.__curve @@ -1220,17 +1224,22 @@ def leftmost_bit(x): # From X9.62 D.3.2: e3 = 3 * e - negative_self = Point(self.__curve, self.__x, -self.__y, self.__order) + negative_self = Point( + self.__curve, + self.__x, + (-self.__y) % self.__curve.p(), + self.__order, + ) i = leftmost_bit(e3) // 2 result = self - # print_("Multiplying %s by %d (e3 = %d):" % (self, other, e3)) + # print("Multiplying %s by %d (e3 = %d):" % (self, other, e3)) while i > 1: result = result.double() if (e3 & i) != 0 and (e & i) == 0: result = result + self if (e3 & i) == 0 and (e & i) != 0: result = result + negative_self - # print_(". . . i = %d, result = %s" % ( i, result )) + # print(". . . i = %d, result = %s" % ( i, result )) i = i // 2 return result @@ -1247,7 +1256,6 @@ def __str__(self): def double(self): """Return a new point that is twice the old.""" - if self == INFINITY: return INFINITY @@ -1261,6 +1269,9 @@ def double(self): * numbertheory.inverse_mod(2 * self.__y, p) ) % p + if not l: + return INFINITY + x3 = (l * l - 2 * self.__x) % p y3 = (l * (self.__x - x3) - self.__y) % p diff --git a/src/ecdsa/test_der.py b/src/ecdsa/test_der.py index 0c2dc4d..33a2dca 100644 --- a/src/ecdsa/test_der.py +++ b/src/ecdsa/test_der.py @@ -22,8 +22,10 @@ remove_object, encode_oid, remove_constructed, + remove_implicit, remove_octet_string, remove_sequence, + encode_implicit, ) @@ -396,6 +398,128 @@ def test_with_malformed_tag(self): self.assertIn("constructed tag", str(e.exception)) +class TestRemoveImplicit(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.exp_tag = 6 + cls.exp_data = b"\x0a\x0b" + # data with application tag class + cls.data_application = b"\x46\x02\x0a\x0b" + # data with context-specific tag class + cls.data_context_specific = b"\x86\x02\x0a\x0b" + # data with private tag class + cls.data_private = b"\xc6\x02\x0a\x0b" + + def test_simple(self): + tag, body, rest = remove_implicit(self.data_context_specific) + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_wrong_expected_class(self): + with self.assertRaises(ValueError) as e: + remove_implicit(self.data_context_specific, "foobar") + + self.assertIn("invalid `exp_class` value", str(e.exception)) + + def test_with_wrong_class(self): + with self.assertRaises(UnexpectedDER) as e: + remove_implicit(self.data_application) + + self.assertIn( + "wanted class context-specific, got 0x46 tag", str(e.exception) + ) + + def test_with_application_class(self): + tag, body, rest = remove_implicit(self.data_application, "application") + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_with_private_class(self): + tag, body, rest = remove_implicit(self.data_private, "private") + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_with_data_following(self): + extra_data = b"\x00\x01" + + tag, body, rest = remove_implicit( + self.data_context_specific + extra_data + ) + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, extra_data) + + def test_with_constructed(self): + data = b"\xa6\x02\x0a\x0b" + + with self.assertRaises(UnexpectedDER) as e: + remove_implicit(data) + + self.assertIn("wanted type primitive, got 0xa6 tag", str(e.exception)) + + def test_encode_decode(self): + data = b"some longish string" + + tag, body, rest = remove_implicit( + encode_implicit(6, data, "application"), "application" + ) + + self.assertEqual(tag, 6) + self.assertEqual(body, data) + self.assertEqual(rest, b"") + + +class TestEncodeImplicit(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.data = b"\x0a\x0b" + # data with application tag class + cls.data_application = b"\x46\x02\x0a\x0b" + # data with context-specific tag class + cls.data_context_specific = b"\x86\x02\x0a\x0b" + # data with private tag class + cls.data_private = b"\xc6\x02\x0a\x0b" + + def test_encode_with_default_class(self): + ret = encode_implicit(6, self.data) + + self.assertEqual(ret, self.data_context_specific) + + def test_encode_with_application_class(self): + ret = encode_implicit(6, self.data, "application") + + self.assertEqual(ret, self.data_application) + + def test_encode_with_context_specific_class(self): + ret = encode_implicit(6, self.data, "context-specific") + + self.assertEqual(ret, self.data_context_specific) + + def test_encode_with_private_class(self): + ret = encode_implicit(6, self.data, "private") + + self.assertEqual(ret, self.data_private) + + def test_encode_with_invalid_class(self): + with self.assertRaises(ValueError) as e: + encode_implicit(6, self.data, "foobar") + + self.assertIn("invalid tag class", str(e.exception)) + + def test_encode_with_too_large_tag(self): + with self.assertRaises(ValueError) as e: + encode_implicit(32, self.data) + + self.assertIn("Long tags not supported", str(e.exception)) + + class TestRemoveOctetString(unittest.TestCase): def test_simple(self): data = b"\x04\x03\xaa\xbb\xcc" @@ -476,3 +600,23 @@ def test_oids(ids): decoded_oid, rest = remove_object(encoded_oid) assert rest == b"" assert decoded_oid == ids + +def test_remove_octet_string_rejects_truncated_length(): + # OCTET STRING: declared length 4096, but only 3 bytes present + bad = b"\x04\x82\x10\x00" + b"ABC" + with pytest.raises(UnexpectedDER, match="Length longer than the provided buffer"): + remove_octet_string(bad) + +def test_remove_constructed_rejects_truncated_length(): + # Constructed tag: 0xA0 (context-specific constructed, tag=0) + # declared length 4096, but only 3 bytes present + bad = b"\xA0\x82\x10\x00" + b"ABC" + with pytest.raises(UnexpectedDER, match="Length longer than the provided buffer"): + remove_constructed(bad) + +def test_remove_implicit_rejects_truncated_length(): + # IMPLICIT primitive context-specific tag 0: 0x80 + # declared length 4096, but only 3 bytes present + bad = b"\x80\x82\x10\x00" + b"ABC" + with pytest.raises(UnexpectedDER, match="Length longer than the provided buffer"): + remove_implicit(bad) diff --git a/src/ecdsa/test_ellipticcurve.py b/src/ecdsa/test_ellipticcurve.py index 9bf0951..864cf10 100644 --- a/src/ecdsa/test_ellipticcurve.py +++ b/src/ecdsa/test_ellipticcurve.py @@ -83,6 +83,11 @@ def test_inequality_curves(self): c192 = CurveFp(p, -3, b) self.assertNotEqual(self.c_23, c192) + def test_inequality_curves_by_b_only(self): + a = CurveFp(23, 1, 0) + b = CurveFp(23, 1, 1) + self.assertNotEqual(a, b) + def test_usability_in_a_hashed_collection_curves(self): {self.c_23: None} @@ -184,6 +189,33 @@ def test_double(self): self.assertEqual(p3.x(), x3) self.assertEqual(p3.y(), y3) + def test_double_to_infinity(self): + p1 = Point(self.c_23, 11, 20) + p2 = p1.double() + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2.double() + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_add_self_to_infinity(self): + p1 = Point(self.c_23, 11, 20) + p2 = p1 + p1 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 + p2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_mul_to_infinity(self): + p1 = Point(self.c_23, 11, 20) + p2 = p1 * 2 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 * 2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + def test_multiply(self): x1, y1, m, x3, y3 = (3, 10, 2, 7, 12) p1 = Point(self.c_23, x1, y1) @@ -224,6 +256,12 @@ def test_inequality_points_diff_types(self): c = CurveFp(100, -3, 100) self.assertNotEqual(self.g_23, c) + def test_inequality_diff_y(self): + p1 = Point(self.c_23, 6, 4) + p2 = Point(self.c_23, 6, 19) + + self.assertNotEqual(p1, p2) + def test_to_bytes_from_bytes(self): p = Point(self.c_23, 3, 10) diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index 9a46afe..f811b92 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -14,7 +14,7 @@ import hypothesis.strategies as st from hypothesis import given, assume, settings, example -from .ellipticcurve import CurveFp, PointJacobi, INFINITY +from .ellipticcurve import CurveFp, PointJacobi, INFINITY, Point from .ecdsa import ( generator_256, curve_256, @@ -92,14 +92,21 @@ def test_double_with_zero_point(self): self.assertIs(pj, INFINITY) def test_double_with_zero_equivalent_point(self): - pj = PointJacobi(curve_256, 0, curve_256.p(), 1) + pj = PointJacobi(curve_256, 0, 0, 0) pj = pj.double() self.assertIs(pj, INFINITY) - def test_double_with_zero_equivalent_point_non_1_z(self): - pj = PointJacobi(curve_256, 0, curve_256.p(), 2) + def test_double_with_zero_equivalent_point_non_zero_z_non_zero_y(self): + pj = PointJacobi(curve_256, 0, 1, curve_256.p()) + + pj = pj.double() + + self.assertIs(pj, INFINITY) + + def test_double_with_zero_equivalent_point_non_zero_z(self): + pj = PointJacobi(curve_256, 0, 0, curve_256.p()) pj = pj.double() @@ -113,7 +120,7 @@ def test_compare_with_affine_point(self): self.assertEqual(pa, pj) def test_to_affine_with_zero_point(self): - pj = PointJacobi(curve_256, 0, 0, 1) + pj = PointJacobi(curve_256, 0, 0, 0) pa = pj.to_affine() @@ -144,7 +151,7 @@ def test_add_with_infinity(self): def test_add_zero_point_to_affine(self): pa = PointJacobi.from_affine(generator_256).to_affine() - pj = PointJacobi(curve_256, 0, 0, 1) + pj = PointJacobi(curve_256, 0, 0, 0) s = pj + pa @@ -195,8 +202,35 @@ def test_compare_non_zero_with_infinity(self): self.assertNotEqual(pj, INFINITY) + def test_compare_non_zero_bad_scale_with_infinity(self): + pj = PointJacobi(curve_256, 1, 1, 0) + self.assertEqual(pj, INFINITY) + + def test_eq_x_0_on_curve_with_infinity(self): + c_23 = CurveFp(23, 1, 1) + pj = PointJacobi(c_23, 0, 1, 1) + + self.assertTrue(c_23.contains_point(0, 1)) + + self.assertNotEqual(pj, INFINITY) + + def test_eq_y_0_on_curve_with_infinity(self): + c_23 = CurveFp(23, 1, 1) + pj = PointJacobi(c_23, 4, 0, 1) + + self.assertTrue(c_23.contains_point(4, 0)) + + self.assertNotEqual(pj, INFINITY) + + def test_eq_with_same_x_different_y(self): + c_23 = CurveFp(23, 1, 1) + p_a = PointJacobi(c_23, 0, 22, 1) + p_b = PointJacobi(c_23, 0, 1, 1) + + self.assertNotEqual(p_a, p_b) + def test_compare_zero_point_with_infinity(self): - pj = PointJacobi(curve_256, 0, 0, 1) + pj = PointJacobi(curve_256, 0, 0, 0) self.assertEqual(pj, INFINITY) @@ -579,6 +613,18 @@ def test_mul_add(self): self.assertEqual(ret.to_affine(), w_a + w_b) + def test_mul_add_zero(self): + j_g = PointJacobi.from_affine(generator_256) + + w_a = generator_256 * 255 + w_b = generator_256 * (0 * 0xA8) + + j_b = j_g * 0xA8 + + ret = j_g.mul_add(255, j_b, 0) + + self.assertEqual(ret.to_affine(), w_a + w_b) + def test_mul_add_large(self): j_g = PointJacobi.from_affine(generator_256) b = PointJacobi.from_affine(j_g * 255) @@ -619,6 +665,20 @@ def test_mul_add_with_doubled_negation_of_itself(self): self.assertEqual(j_g.mul_add(4, dbl_neg, 2), INFINITY) + @given( + st.integers(min_value=0, max_value=int(generator_112r2.order() - 1)), + st.integers(min_value=0, max_value=int(generator_112r2.order() - 1)), + st.integers(min_value=0, max_value=int(generator_112r2.order() - 1)), + ) + @example(693, 2, 3293) # values that will hit all the conditions for NAF + def test_mul_add_random(self, mul1, mul2, mul3): + p_a = PointJacobi.from_affine(generator_112r2) + p_b = generator_112r2 * mul2 + + res = p_a.mul_add(mul1, p_b, mul3) + + self.assertEqual(res, p_a * mul1 + p_b * mul3) + def test_equality(self): pj1 = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1) pj2 = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1) @@ -641,6 +701,78 @@ def test_add_with_point_at_infinity(self): self.assertEqual((x, y, z), (2, 3, 1)) + def test_double_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 11, 20, 1) + p2 = p.double() + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2.double() + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_double_to_x_0(self): + c_23_2 = CurveFp(23, 1, 2) + p = PointJacobi(c_23_2, 9, 2, 1) + p2 = p.double() + + self.assertEqual((p2.x(), p2.y()), (0, 18)) + + def test_mul_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 11, 20, 1) + p2 = p * 2 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 * 2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_add_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 11, 20, 1) + p2 = p + p + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 + p2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_mul_to_x_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 13 + self.assertEqual((p2.x(), p2.y()), (0, 22)) + + def test_mul_to_y_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 14 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + + def test_add_to_x_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 12 + p + self.assertEqual((p2.x(), p2.y()), (0, 22)) + + def test_add_to_y_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 13 + p + self.assertEqual((p2.x(), p2.y()), (4, 0)) + + def test_add_diff_z_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + c = p * 20 + p * 8 + self.assertIs(c, INFINITY) + def test_pickle(self): pj = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1) self.assertEqual(pickle.loads(pickle.dumps(pj)), pj) @@ -751,3 +883,52 @@ def interrupter(barrier_start, barrier_end, lock_exit): gen._PointJacobi__precompute, generator_112r2._PointJacobi__precompute, ) + + +class TestZeroCurve(unittest.TestCase): + """Tests with curve that has (0, 0) on the curve.""" + + def setUp(self): + self.curve = CurveFp(23, 1, 0) + + def test_zero_point_on_curve(self): + self.assertTrue(self.curve.contains_point(0, 0)) + + def test_double_to_0_0_point(self): + p = PointJacobi(self.curve, 1, 18, 1) + + d = p.double() + + self.assertNotEqual(d, INFINITY) + self.assertEqual((0, 0), (d.x(), d.y())) + + def test_double_to_0_0_point_with_non_one_z(self): + z = 2 + p = PointJacobi(self.curve, 1 * z**2, 18 * z**3, z) + + d = p.double() + + self.assertNotEqual(d, INFINITY) + self.assertEqual((0, 0), (d.x(), d.y())) + + def test_mul_to_0_0_point(self): + p = PointJacobi(self.curve, 11, 13, 1) + + d = p * 12 + + self.assertNotEqual(d, INFINITY) + self.assertEqual((0, 0), (d.x(), d.y())) + + def test_double_of_0_0_point(self): + p = PointJacobi(self.curve, 0, 0, 1) + + d = p.double() + + self.assertIs(d, INFINITY) + + def test_compare_to_old_implementation(self): + p = PointJacobi(self.curve, 11, 13, 1) + p_c = Point(self.curve, 11, 13) + + for i in range(24): + self.assertEqual(p * i, p_c * i) diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index 20201ba..799e9b7 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -1,4 +1,4 @@ -from __future__ import with_statement, division +from __future__ import with_statement, division, print_function try: import unittest2 as unittest @@ -16,7 +16,7 @@ from hypothesis import given, settings import hypothesis.strategies as st -from six import b, print_, binary_type +from six import binary_type from .keys import SigningKey, VerifyingKey from .keys import BadSignatureError, MalformedPointError, BadDigestError from . import util @@ -365,8 +365,8 @@ def test_sk_to_der_with_invalid_point_encoding(self): def test_vk_from_der_garbage_after_curve_oid(self): type_oid_der = encoded_oid_ecPublicKey - curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + b( - "garbage" + curve_oid_der = ( + der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + b"garbage" ) enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) point_der = der.encode_bitstring(b"\x00\xff", None) @@ -520,6 +520,23 @@ def test_sigencode_der_canonize(self): self.assertEqual(r, new_r) self.assertEqual(order - s, new_s) + def test_sigencode_der_canonize_with_close_to_half_order(self): + r = 13 + order = SECP112r1.order + s = order // 2 + 1 + + regular_encode = sigencode_der(r, s, order) + canonical_encode = sigencode_der_canonize(r, s, order) + + self.assertNotEqual(regular_encode, canonical_encode) + + new_r, new_s = sigdecode_der( + sigencode_der_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(order - s, new_s) + def test_sig_decode_strings_with_invalid_count(self): with self.assertRaises(MalformedSignature): sigdecode_strings([b"one", b"two", b"three"], 0xFF) @@ -770,10 +787,10 @@ def test_encoding(self): sk = SigningKey.from_secret_exponent(123456789) vk = sk.verifying_key - exp = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + exp = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) self.assertEqual(vk.to_string(), exp) self.assertEqual(vk.to_string("raw"), exp) @@ -785,10 +802,10 @@ def test_decoding(self): sk = SigningKey.from_secret_exponent(123456789) vk = sk.verifying_key - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) from_raw = VerifyingKey.from_string(enc) @@ -804,11 +821,11 @@ def test_decoding(self): self.assertEqual(from_uncompressed.pubkey.point, vk.pubkey.point) def test_uncompressed_decoding_as_only_alowed(self): - enc = b( - "\x04" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x04" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) vk = VerifyingKey.from_string(enc, valid_encodings=("uncompressed",)) sk = SigningKey.from_secret_exponent(123456789) @@ -816,10 +833,10 @@ def test_uncompressed_decoding_as_only_alowed(self): self.assertEqual(vk, sk.verifying_key) def test_raw_decoding_with_blocked_format(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) @@ -833,11 +850,11 @@ def test_decoding_with_unknown_format(self): self.assertIn("Only uncompressed, compressed", str(e.exception)) def test_uncompressed_decoding_with_blocked_format(self): - enc = b( - "\x04" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x04" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) @@ -845,23 +862,39 @@ def test_uncompressed_decoding_with_blocked_format(self): self.assertIn("Invalid X9.62 encoding", str(exp.exception)) def test_hybrid_decoding_with_blocked_format(self): - enc = b( - "\x06" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x06" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("uncompressed",)) self.assertIn("Invalid X9.62 encoding", str(exp.exception)) + def test_hybrid_decoding_with_inconsistent_encoding_and_no_validation( + self, + ): + sk = SigningKey.from_secret_exponent(123456789) + vk = sk.verifying_key + + enc = vk.to_string("hybrid") + self.assertEqual(enc[:1], b"\x06") + enc = b"\x07" + enc[1:] + + b = VerifyingKey.from_string( + enc, valid_encodings=("hybrid",), validate_point=False + ) + + self.assertEqual(vk, b) + def test_compressed_decoding_with_blocked_format(self): - enc = b( - "\x02" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x02" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" )[:25] with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("hybrid", "raw")) @@ -869,40 +902,51 @@ def test_compressed_decoding_with_blocked_format(self): self.assertIn("(hybrid, raw)", str(exp.exception)) def test_decoding_with_malformed_uncompressed(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): VerifyingKey.from_string(b"\x02" + enc) def test_decoding_with_malformed_compressed(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): VerifyingKey.from_string(b"\x01" + enc[:24]) def test_decoding_with_inconsistent_hybrid(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): VerifyingKey.from_string(b"\x07" + enc) + def test_decoding_with_inconsistent_hybrid_odd_point(self): + sk = SigningKey.from_secret_exponent(123456791) + vk = sk.verifying_key + + enc = vk.to_string("hybrid") + self.assertEqual(enc[:1], b"\x07") + enc = b"\x06" + enc[1:] + + with self.assertRaises(MalformedPointError): + b = VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) + def test_decoding_with_point_not_on_curve(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): @@ -1894,7 +1938,7 @@ def OFF_test_prove_uniformity(self): # pragma: no cover # this technique should use the full range self.assertTrue(counts[order - 1]) for i in range(1, order): - print_("%3d: %s" % (i, "*" * (counts[i] // 100))) + print("%3d: %s" % (i, "*" * (counts[i] // 100))) class RFC6979(unittest.TestCase): @@ -1981,9 +2025,7 @@ def test_1(self): ), secexp=int("09A4D6792295A7F730FC3F2B49CBC0F62E862272F", 16), hsh=unhexlify( - b( - "AF2BDBE1AA9B6EC1E2ADE1D694F41FC71A831D0268E9891562113D8A62ADD1BF" - ) + b"AF2BDBE1AA9B6EC1E2ADE1D694F41FC71A831D0268E9891562113D8A62ADD1BF" ), hash_func=hashlib.sha256, expected=int("23AF4074C90A02B3FE61D286D5C87F425E6BDD81B", 16), diff --git a/src/ecdsa/util.py b/src/ecdsa/util.py index 639bc0c..1aff5bf 100644 --- a/src/ecdsa/util.py +++ b/src/ecdsa/util.py @@ -304,6 +304,23 @@ def sigencode_der(r, s, order): return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) +def _canonize(s, order): + """ + Internal function for ensuring that the ``s`` value of a signature is in + the "canonical" format. + + :param int s: the second parameter of ECDSA signature + :param int order: the order of the curve over which the signatures was + computed + + :return: canonical value of s + :rtype: int + """ + if s > order // 2: + s = order - s + return s + + def sigencode_strings_canonize(r, s, order): """ Encode the signature to a pair of strings in a tuple @@ -326,8 +343,7 @@ def sigencode_strings_canonize(r, s, order): :return: raw encoding of ECDSA signature :rtype: tuple(bytes, bytes) """ - if s > order / 2: - s = order - s + s = _canonize(s, order) return sigencode_strings(r, s, order) @@ -350,8 +366,7 @@ def sigencode_string_canonize(r, s, order): :return: raw encoding of ECDSA signature :rtype: bytes """ - if s > order / 2: - s = order - s + s = _canonize(s, order) return sigencode_string(r, s, order) @@ -381,8 +396,7 @@ def sigencode_der_canonize(r, s, order): :return: DER encoding of ECDSA signature :rtype: bytes """ - if s > order / 2: - s = order - s + s = _canonize(s, order) return sigencode_der(r, s, order) diff --git a/tox.ini b/tox.ini index ea01c4b..19c0317 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,17 @@ [tox] -envlist = py26, py27, py35, py36, py37, py38, py39, py310, py311, py312, py, pypy, pypy3, gmpy2py27, gmpy2py39, gmpy2py310, gmpypy27, gmpypy39, gmpypy310, codechecks +envlist = py26, py27, py35, py36, py37, py38, py39, py310, py311, py312, py313, py, pypy, pypy3, gmpy2py27, gmpy2py39, gmpy2py310, gmpypy27, gmpypy39, gmpypy310, codechecks [testenv] deps = py{26}: unittest2 py{26}: hypothesis<3 - py{26,27,35,36,37,38,39,310,311,312,py,py3}: pytest - py{27,35,36,37,38,39,310,311,312,py,py3}: hypothesis - gmpy2py{27,39,310,311,312}: gmpy2 - gmpypy{27,39,310,311,312}: gmpy - gmpy{2py27,2py39,2py310,2py311,2py312,py27,py39,py310,py311,py312}: pytest - gmpy{2py27,2py39,2py310,2py311,2py312,py27,py39,py310,py311,py312}: hypothesis + py{26,27,35,36,37,38,39,310,311,312,313,py,py3}: pytest + py{27,35,36,37,38,39,310,311,312,313,py,py3}: hypothesis + gmpy2py{27,39,310,311,312,313}: gmpy2 + gmpypy{27,39,310,311,312,313}: gmpy + gmpy{2py27,2py39,2py310,2py311,2py312,2py313,py27,py39,py310,py311,py312,py313}: pytest + gmpy{2py27,2py39,2py310,2py311,2py312,2py313,py27,py39,py310,py311,py312,py313}: hypothesis # six==1.9.0 comes from setup.py install_requires py27_old_six: six==1.9.0 py27_old_six: pytest @@ -53,6 +53,9 @@ basepython=python3.11 [testenv:gmpypy312] basepython=python3.12 +[testenv:gmpypy313] +basepython=python3.13 + [testenv:gmpy2py27] basepython=python2.7 @@ -68,6 +71,9 @@ basepython=python3.11 [testenv:gmpy2py312] basepython=python3.12 +[testenv:gmpy2py313] +basepython=python3.13 + [testenv:instrumental] basepython = python2.7 deps =