Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-josepy for openSUSE:Factory checked in at 2021-03-08 15:19:08 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-josepy (Old) and /work/SRC/openSUSE:Factory/.python-josepy.new.2378 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-josepy" Mon Mar 8 15:19:08 2021 rev:9 rq:877589 version:1.7.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-josepy/python-josepy.changes 2021-01-06 19:57:11.497173700 +0100 +++ /work/SRC/openSUSE:Factory/.python-josepy.new.2378/python-josepy.changes 2021-03-08 15:20:55.062099515 +0100 @@ -1,0 +2,7 @@ +Mon Mar 8 08:39:01 UTC 2021 - Dirk M??ller <dmuel...@suse.com> + +- update to 1.7.0: + * Dropped support for Python 2.7. + * Added support for EC keys. + +------------------------------------------------------------------- Old: ---- josepy-1.5.0.tar.gz josepy-1.5.0.tar.gz.asc New: ---- josepy-1.7.0.tar.gz josepy-1.7.0.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-josepy.spec ++++++ --- /var/tmp/diff_new_pack.W1NFzQ/_old 2021-03-08 15:20:55.778100039 +0100 +++ /var/tmp/diff_new_pack.W1NFzQ/_new 2021-03-08 15:20:55.778100039 +0100 @@ -18,8 +18,9 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define libname josepy +%global skip_python2 1 Name: python-%{libname} -Version: 1.5.0 +Version: 1.7.0 Release: 0 Summary: JOSE protocol implementation in Python License: Apache-2.0 ++++++ josepy-1.5.0.tar.gz -> josepy-1.7.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/.travis.yml new/josepy-1.7.0/.travis.yml --- old/josepy-1.5.0/.travis.yml 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/.travis.yml 2021-02-11 20:42:03.000000000 +0100 @@ -4,11 +4,10 @@ dist: xenial matrix: include: - - python: "2.7" - python: "3.6" - python: "3.7" - python: "3.8" - - python: "3.9-dev" + - python: "3.9" install: - pip install tox-travis codecov script: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/CHANGELOG.rst new/josepy-1.7.0/CHANGELOG.rst --- old/josepy-1.5.0/CHANGELOG.rst 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/CHANGELOG.rst 2021-02-11 20:42:03.000000000 +0100 @@ -1,6 +1,17 @@ Changelog ========= +1.7.0 (2021-02-11) +------------------ + +* Dropped support for Python 2.7. +* Added support for EC keys. + +1.6.0 (2021-01-26) +------------------ + +* Deprecated support for Python 2.7. + 1.5.0 (2020-11-03) ------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/PKG-INFO new/josepy-1.7.0/PKG-INFO --- old/josepy-1.5.0/PKG-INFO 2020-11-03 23:41:40.393079000 +0100 +++ new/josepy-1.7.0/PKG-INFO 2021-02-11 20:42:42.238660600 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: josepy -Version: 1.5.0 +Version: 1.7.0 Summary: JOSE protocol implementation in Python Home-page: https://github.com/certbot/josepy Author: Certbot Project @@ -26,8 +26,6 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 @@ -35,7 +33,7 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.* +Requires-Python: >=3.6 Provides-Extra: dev Provides-Extra: dev3 Provides-Extra: docs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/docs/conf.py new/josepy-1.7.0/docs/conf.py --- old/josepy-1.5.0/docs/conf.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/docs/conf.py 2021-02-11 20:42:03.000000000 +0100 @@ -63,9 +63,9 @@ # built documents. # # The short X.Y version. -version = u'1.5' +version = u'1.7' # The full version, including alpha/beta/rc tags. -release = u'1.5.0' +release = u'1.7.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/setup.py new/josepy-1.7.0/setup.py --- old/josepy-1.5.0/setup.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/setup.py 2021-02-11 20:42:03.000000000 +0100 @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -version = '1.5.0' +version = '1.7.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -55,14 +55,12 @@ author="Certbot Project", author_email='client-...@letsencrypt.org', license='Apache License 2.0', - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*', + python_requires='>=3.6', classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/__init__.py new/josepy-1.7.0/src/josepy/__init__.py --- old/josepy-1.5.0/src/josepy/__init__.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/__init__.py 2021-02-11 20:42:03.000000000 +0100 @@ -65,6 +65,9 @@ RS256, RS384, RS512, + ES256, + ES384, + ES512, ) from josepy.jwk import ( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/jwa.py new/josepy-1.7.0/src/josepy/jwa.py --- old/josepy-1.5.0/src/josepy/jwa.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/jwa.py 2021-02-11 20:42:03.000000000 +0100 @@ -5,12 +5,15 @@ """ import abc import logging +import math import cryptography.exceptions from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes # type: ignore from cryptography.hazmat.primitives import hmac # type: ignore -from cryptography.hazmat.primitives.asymmetric import padding # type: ignore +from cryptography.hazmat.primitives.asymmetric import padding, ec # type: ignore +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature from josepy import errors, interfaces, jwk @@ -75,7 +78,6 @@ class _JWAHS(JWASignature): - kty = jwk.JWKOct def __init__(self, name, hash_): @@ -100,7 +102,6 @@ class _JWARSA(object): - kty = jwk.JWKRSA padding = NotImplemented hash = NotImplemented @@ -163,15 +164,65 @@ self.hash = hash_() -class _JWAES(JWASignature): # pylint: disable=abstract-class-not-used +class _JWAEC(JWASignature): + kty = jwk.JWKEC - # TODO: implement ES signatures + def __init__(self, name, hash_): + super(_JWAEC, self).__init__(name) + self.hash = hash_() - def sign(self, key, msg): # pragma: no cover - raise NotImplementedError() + def sign(self, key, msg): + """Sign the ``msg`` using ``key``.""" + sig = self._sign(key, msg) + dr, ds = decode_dss_signature(sig) + return (dr.to_bytes(length=math.ceil(dr.bit_length() / 8), byteorder='big') + + ds.to_bytes(length=math.ceil(ds.bit_length() / 8), byteorder='big')) - def verify(self, key, msg, sig): # pragma: no cover - raise NotImplementedError() + def _sign(self, key, msg): + # If cryptography library supports new style api (v1.4 and later) + new_api = hasattr(key, 'sign') + try: + if new_api: + return key.sign(msg, ec.ECDSA(self.hash)) + signer = key.signer(ec.ECDSA(self.hash)) + except AttributeError as error: + logger.debug(error, exc_info=True) + raise errors.Error('Public key cannot be used for signing') + except ValueError as error: # digest too large + logger.debug(error, exc_info=True) + raise errors.Error(str(error)) + signer.update(msg) + try: + return signer.finalize() + except ValueError as error: + logger.debug(error, exc_info=True) + raise errors.Error(str(error)) + + def verify(self, key, msg, sig): + """Verify the ``msg` and ``sig`` using ``key``.""" + rlen = math.ceil(key.key_size / 8) + asn1sig = encode_dss_signature( + int.from_bytes(sig[0:rlen], byteorder='big'), + int.from_bytes(sig[rlen:], byteorder='big') + ) + return self._verify(key, msg, asn1sig) + + def _verify(self, key, msg, asn1sig): + # If cryptography library supports new style api (v1.4 and later) + new_api = hasattr(key, 'verify') + if not new_api: + verifier = key.verifier(asn1sig, ec.ECDSA(self.hash)) + verifier.update(msg) + try: + if new_api: + key.verify(asn1sig, msg, ec.ECDSA(self.hash)) + else: + verifier.verify() + except cryptography.exceptions.InvalidSignature as error: + logger.debug(error, exc_info=True) + return False + else: + return True #: HMAC using SHA-256 @@ -196,8 +247,8 @@ PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512)) #: ECDSA using P-256 and SHA-256 -ES256 = JWASignature.register(_JWAES('ES256')) +ES256 = JWASignature.register(_JWAEC('ES256', hashes.SHA256)) #: ECDSA using P-384 and SHA-384 -ES384 = JWASignature.register(_JWAES('ES384')) +ES384 = JWASignature.register(_JWAEC('ES384', hashes.SHA384)) #: ECDSA using P-521 and SHA-512 -ES512 = JWASignature.register(_JWAES('ES512')) +ES512 = JWASignature.register(_JWAEC('ES512', hashes.SHA512)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/jwa_test.py new/josepy-1.7.0/src/josepy/jwa_test.py --- old/josepy-1.5.0/src/josepy/jwa_test.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/jwa_test.py 2021-02-11 20:42:03.000000000 +0100 @@ -8,6 +8,9 @@ RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem') +EC_P256_KEY = test_util.load_ec_private_key('ec_p256_key.pem') +EC_P384_KEY = test_util.load_ec_private_key('ec_p384_key.pem') +EC_P521_KEY = test_util.load_ec_private_key('ec_p521_key.pem') class JWASignatureTest(unittest.TestCase): @@ -69,8 +72,7 @@ def test_sign_no_private_part(self): from josepy.jwa import RS256 - self.assertRaises( - errors.Error, RS256.sign, RSA512_KEY.public_key(), b'foo') + self.assertRaises(errors.Error, RS256.sign, RSA512_KEY.public_key(), b'foo') def test_sign_key_too_small(self): from josepy.jwa import RS256 @@ -130,6 +132,81 @@ self.assertTrue(all([ key.verifier.called, verifier.update.called, + verifier.verify.called])) + + +class JWAECTest(unittest.TestCase): + + def test_sign_no_private_part(self): + from josepy.jwa import ES256 + self.assertRaises( + errors.Error, ES256.sign, EC_P256_KEY.public_key(), b'foo') + + def test_es256_sign_and_verify(self): + from josepy.jwa import ES256 + message = b'foo' + signature = ES256.sign(EC_P256_KEY, message) + self.assertTrue(ES256.verify(EC_P256_KEY.public_key(), message, signature)) + + def test_es384_sign_and_verify(self): + from josepy.jwa import ES384 + message = b'foo' + signature = ES384.sign(EC_P384_KEY, message) + self.assertTrue(ES384.verify(EC_P384_KEY.public_key(), message, signature)) + + def test_verify_with_wrong_jwa(self): + from josepy.jwa import ES256, ES384 + message = b'foo' + signature = ES256.sign(EC_P256_KEY, message) + self.assertFalse(ES384.verify(EC_P384_KEY.public_key(), message, signature)) + + def test_verify_with_different_key(self): + from josepy.jwa import ES256 + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.backends import default_backend + + message = b'foo' + signature = ES256.sign(EC_P256_KEY, message) + different_key = ec.generate_private_key(ec.SECP256R1, default_backend()) + self.assertFalse(ES256.verify(different_key.public_key(), message, signature)) + + def test_sign_new_api(self): + from josepy.jwa import ES256 + key = mock.MagicMock() + with mock.patch("josepy.jwa.decode_dss_signature") as decode_patch: + decode_patch.return_value = (0, 0) + ES256.sign(key, "message") + self.assertTrue(key.sign.called) + + def test_sign_old_api(self): + from josepy.jwa import ES256 + key = mock.MagicMock(spec=[u'signer']) + signer = mock.MagicMock() + key.signer.return_value = signer + with mock.patch("josepy.jwa.decode_dss_signature") as decode_patch: + decode_patch.return_value = (0, 0) + ES256.sign(key, "message") + self.assertTrue(all([ + key.signer.called, + signer.update.called, + signer.finalize.called])) + + def test_verify_new_api(self): + from josepy.jwa import ES256 + key = mock.MagicMock() + ES256.verify(key, "message", "signature".encode()) + self.assertTrue(key.verify.called) + + def test_verify_old_api(self): + from josepy.jwa import ES256 + key = mock.MagicMock(spec=[u'verifier']) + verifier = mock.MagicMock() + key.verifier.return_value = verifier + key.key_size = 65 * 8 + ES256.verify(key, "message", "signature".encode()) + self.assertTrue(all([ + key.verifier.called, + verifier.update.called, verifier.verify.called])) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/jwk.py new/josepy-1.7.0/src/josepy/jwk.py --- old/josepy-1.5.0/src/josepy/jwk.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/jwk.py 2021-02-11 20:42:03.000000000 +0100 @@ -121,30 +121,6 @@ @JWK.register -class JWKES(JWK): # pragma: no cover - # pylint: disable=abstract-class-not-used - """ES JWK. - - .. warning:: This is not yet implemented! - - """ - typ = 'ES' - cryptography_key_types = ( - ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) - required = ('crv', JWK.type_field_name, 'x', 'y') - - def fields_to_partial_json(self): - raise NotImplementedError() - - @classmethod - def fields_from_json(cls, jobj): - raise NotImplementedError() - - def public_key(self): - raise NotImplementedError() - - -@JWK.register class JWKOct(JWK): """Symmetric JWK.""" typ = 'oct' @@ -194,6 +170,7 @@ :rtype: unicode """ + def _leading_zeros(arg): if len(arg) % 2: return '0' + arg @@ -275,3 +252,125 @@ } return dict((key, self._encode_param(value)) for key, value in six.iteritems(params)) + + +@JWK.register +class JWKEC(JWK): + """EC JWK. + + :ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` + or :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` wrapped + in :class:`~josepy.util.ComparableRSAKey` + + """ + typ = 'EC' + __slots__ = ('key',) + cryptography_key_types = ( + ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) + required = ('crv', JWK.type_field_name, 'x', 'y') + + def __init__(self, *args, **kwargs): + if 'key' in kwargs and not isinstance( + kwargs['key'], util.ComparableECKey): + kwargs['key'] = util.ComparableECKey(kwargs['key']) + super(JWKEC, self).__init__(*args, **kwargs) + + @classmethod + def _encode_param(cls, data): + """Encode Base64urlUInt. + :type data: long + :rtype: unicode + """ + + def _leading_zeros(arg): + if len(arg) % 2: + return '0' + arg + return arg + + return json_util.encode_b64jose(binascii.unhexlify( + _leading_zeros(hex(data)[2:].rstrip('L')))) + + @classmethod + def _decode_param(cls, data, name, valid_lengths): + """Decode Base64urlUInt.""" + try: + binary = json_util.decode_b64jose(data) + if len(binary) not in valid_lengths: + raise errors.DeserializationError( + 'Expected parameter "{name}" to be {valid_lengths} bytes ' + 'after base64-decoding; got {length} bytes instead'.format( + name=name, valid_lengths=valid_lengths, length=len(binary)) + ) + return int(binascii.hexlify(binary), 16) + except ValueError: # invalid literal for long() with base 16 + raise errors.DeserializationError() + + @classmethod + def _curve_name_to_crv(cls, curve_name): + if curve_name == 'secp256r1': + return 'P-256' + if curve_name == 'secp384r1': + return 'P-384' + if curve_name == 'secp521r1': + return 'P-521' + raise errors.SerializationError() + + @classmethod + def _crv_to_curve(cls, crv): + # crv is case-sensitive + if crv == 'P-256': + return ec.SECP256R1() + if crv == 'P-384': + return ec.SECP384R1() + if crv == 'P-521': + return ec.SECP521R1() + raise errors.DeserializationError() + + @classmethod + def _expected_length_for_curve(cls, curve): + if isinstance(curve, ec.SECP256R1): + return range(32, 33) + elif isinstance(curve, ec.SECP384R1): + return range(48, 49) + elif isinstance(curve, ec.SECP521R1): + return range(63, 67) + + def fields_to_partial_json(self): + params = {} + if isinstance(self.key._wrapped, ec.EllipticCurvePublicKey): + public = self.key.public_numbers() + elif isinstance(self.key._wrapped, ec.EllipticCurvePrivateKey): + private = self.key.private_numbers() + public = self.key.public_key().public_numbers() + params['d'] = private.private_value + else: + raise errors.SerializationError( + 'Supplied key is neither of type EllipticCurvePublicKey nor EllipticCurvePrivateKey') + params['x'] = public.x + params['y'] = public.y + params = {key: self._encode_param(value) for key, value in six.iteritems(params)} + params['crv'] = self._curve_name_to_crv(public.curve.name) + return params + + @classmethod + def fields_from_json(cls, jobj): + # pylint: disable=invalid-name + curve = cls._crv_to_curve(jobj['crv']) + expected_length = cls._expected_length_for_curve(curve) + x, y = (cls._decode_param(jobj[n], n, expected_length) for n in ('x', 'y')) + public_numbers = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=curve) + if 'd' not in jobj: # public key + key = public_numbers.public_key(default_backend()) + else: # private key + d = cls._decode_param(jobj['d'], 'd', expected_length) + key = ec.EllipticCurvePrivateNumbers(d, public_numbers).private_key( + default_backend()) + return cls(key=key) + + def public_key(self): + # Unlike RSAPrivateKey, EllipticCurvePrivateKey does not contain public_key() + if hasattr(self.key, 'public_key'): + key = self.key.public_key() + else: + key = self.key.public_numbers().public_key(default_backend()) + return type(self)(key=key) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/jwk_test.py new/josepy-1.7.0/src/josepy/jwk_test.py --- old/josepy-1.5.0/src/josepy/jwk_test.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/jwk_test.py 2021-02-11 20:42:03.000000000 +0100 @@ -7,6 +7,9 @@ DSA_PEM = test_util.load_vector('dsa512_key.pem') RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') +EC_P256_KEY = test_util.load_ec_private_key('ec_p256_key.pem') +EC_P384_KEY = test_util.load_ec_private_key('ec_p384_key.pem') +EC_P521_KEY = test_util.load_ec_private_key('ec_p521_key.pem') class JWKTest(unittest.TestCase): @@ -182,5 +185,126 @@ b"f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932") +class JWKECTest(unittest.TestCase, JWKTestBaseMixin): + """Tests for josepy.jwk.JWKEC.""" + # pylint: disable=too-many-instance-attributes + + thumbprint = (b'\x06\xceL\x1b\xa8\x8d\x86\x1flF\x99J\x8b\xe0$\t\xbbj' + b'\xd8\xf6O\x1ed\xdeR\x8f\x97\xff\xf6\xa2\x86\xd3') + + def setUp(self): + from josepy.jwk import JWKEC + self.jwk256 = JWKEC(key=EC_P256_KEY.public_key()) + self.jwk384 = JWKEC(key=EC_P384_KEY.public_key()) + self.jwk521 = JWKEC(key=EC_P521_KEY.public_key()) + self.jwk256_not_comparable = JWKEC(key=EC_P256_KEY.public_key()._wrapped) + self.jwk256json = { + 'kty': 'EC', + 'crv': 'P-256', + 'x': 'jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U', + 'y': 'EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc', + } + self.jwk384json = { + 'kty': 'EC', + 'crv': 'P-384', + 'x': 'tIhpNtEXkadUbrY84rYGgApFM1X_3l3EWQRuOP1IWtxlTftrZQwneJZF0k0eRn00', + 'y': 'KW2Gp-TThDXmZ-9MJPnD8hv-X130SVvfZRl1a04HPVwIbvLe87mvA_iuOa-myUyv', + } + self.jwk521json = { + 'kty': 'EC', + 'crv': 'P-521', + 'x': 'WR2XpwrMGY_XxTx99-k_ghk3Z4QPjmENzBE-Xll4KXAdwu2cwEy5ZgUU783OboMvYyGk3TMjZtwwsl33lpja0-w', + 'y': 'AYvZq3wByjt7nQd8nYMqhFNCL3j_-U6GPWZet1hYBY_XZHrC4yIV0R4JnssRAY9eqc1EElpCc4hziis1jiV1iR4W', + } + self.private = JWKEC(key=EC_P256_KEY) + self.private_json = { + 'd': 'xReNQBKqqTthG8oTmBdhp4EQYImSK1dVqfa2yyMn2rc', + 'x': 'jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U', + 'y': 'EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc', + 'crv': 'P-256', + 'kty': 'EC'} + self.jwk = self.private + + def test_init_auto_comparable(self): + self.assertTrue(isinstance( + self.jwk256_not_comparable.key, util.ComparableECKey)) + self.assertEqual(self.jwk256, self.jwk256_not_comparable) + + def test_encode_param_zero(self): + from josepy.jwk import JWKEC + # pylint: disable=protected-access + # TODO: move encode/decode _param to separate class + self.assertEqual('AA', JWKEC._encode_param(0)) + + def test_equals(self): + self.assertEqual(self.jwk256, self.jwk256) + self.assertEqual(self.jwk384, self.jwk384) + self.assertEqual(self.jwk521, self.jwk521) + + def test_not_equals(self): + self.assertNotEqual(self.jwk256, self.jwk384) + self.assertNotEqual(self.jwk256, self.jwk521) + self.assertNotEqual(self.jwk384, self.jwk256) + self.assertNotEqual(self.jwk384, self.jwk521) + self.assertNotEqual(self.jwk521, self.jwk256) + self.assertNotEqual(self.jwk521, self.jwk384) + + def test_load(self): + from josepy.jwk import JWKEC + self.assertEqual(self.private, JWKEC.load( + test_util.load_vector('ec_p256_key.pem'))) + + def test_public_key(self): + self.assertEqual(self.jwk256, self.private.public_key()) + + def test_to_partial_json(self): + self.assertEqual(self.jwk256.to_partial_json(), self.jwk256json) + self.assertEqual(self.jwk384.to_partial_json(), self.jwk384json) + self.assertEqual(self.jwk521.to_partial_json(), self.jwk521json) + self.assertEqual(self.private.to_partial_json(), self.private_json) + + def test_from_json(self): + from josepy.jwk import JWK + self.assertEqual( + self.jwk256, JWK.from_json(self.jwk256json)) + self.assertEqual( + self.jwk384, JWK.from_json(self.jwk384json)) + self.assertEqual( + self.jwk521, JWK.from_json(self.jwk521json)) + self.assertEqual( + self.private, JWK.from_json(self.private_json)) + + def test_from_json_missing_x_coordinate(self): + from josepy.jwk import JWK + del self.private_json['x'] + self.assertRaises(KeyError, JWK.from_json, self.private_json) + + def test_from_json_missing_y_coordinate(self): + from josepy.jwk import JWK + del self.private_json['y'] + self.assertRaises(KeyError, JWK.from_json, self.private_json) + + def test_from_json_hashable(self): + from josepy.jwk import JWK + hash(JWK.from_json(self.jwk256json)) + + def test_from_json_non_schema_errors(self): + # valid against schema, but still failing + from josepy.jwk import JWK + self.assertRaises(errors.DeserializationError, JWK.from_json, + {'kty': 'EC', 'crv': 'P-256', 'x': 'AQAB', + 'y': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk'}) + self.assertRaises(errors.DeserializationError, JWK.from_json, + {'kty': 'EC', 'crv': 'P-256', 'x': 'jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U', 'y': '1'}) + + def test_unknown_crv_name(self): + from josepy.jwk import JWK + self.assertRaises(errors.DeserializationError, JWK.from_json, + {'kty': 'EC', + 'crv': 'P-255', + 'x': 'jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U', + 'y': 'EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc'}) + + if __name__ == '__main__': unittest.main() # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/test_util.py new/josepy-1.7.0/src/josepy/test_util.py --- old/josepy-1.5.0/src/josepy/test_util.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/test_util.py 2021-02-11 20:42:03.000000000 +0100 @@ -11,6 +11,7 @@ from cryptography.hazmat.primitives import serialization from josepy import ComparableRSAKey, ComparableX509 +from josepy.util import ComparableECKey def vector_path(*names): @@ -68,6 +69,14 @@ load_vector(*names), password=None, backend=default_backend())) +def load_ec_private_key(*names): + """Load EC private key.""" + loader = _guess_loader(names[-1], serialization.load_pem_private_key, + serialization.load_der_private_key) + return ComparableECKey(loader( + load_vector(*names), password=None, backend=default_backend())) + + def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" loader = _guess_loader( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/testdata/README new/josepy-1.7.0/src/josepy/testdata/README --- old/josepy-1.5.0/src/josepy/testdata/README 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/testdata/README 2021-02-11 20:42:03.000000000 +0100 @@ -6,6 +6,10 @@ for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done + openssl ecparam -name prime256v1 -genkey -out ec_p256_key.pem + openssl ecparam -name secp384r1 -genkey -out ec_p384_key.pem + openssl ecparam -name secp521r1 -genkey -out ec_p521_key.pem + and for the CSR: openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/testdata/ec_p256_key.pem new/josepy-1.7.0/src/josepy/testdata/ec_p256_key.pem --- old/josepy-1.5.0/src/josepy/testdata/ec_p256_key.pem 1970-01-01 01:00:00.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/testdata/ec_p256_key.pem 2021-02-11 20:42:03.000000000 +0100 @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMUXjUASqqk7YRvKE5gXYaeBEGCJkitXVan2tssjJ9q3oAoGCCqGSM49 +AwEHoUQDQgAEjjQtV+fA7J/tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5UQ8DDz/zPs +9gqwcfqGUZKWxbEWgWXv7S8zRBEZuae8Jw== +-----END EC PRIVATE KEY----- \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/testdata/ec_p384_key.pem new/josepy-1.7.0/src/josepy/testdata/ec_p384_key.pem --- old/josepy-1.5.0/src/josepy/testdata/ec_p384_key.pem 1970-01-01 01:00:00.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/testdata/ec_p384_key.pem 2021-02-11 20:42:03.000000000 +0100 @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDBHGWgnk8VgEwuXadVIOaCXw+MJ7qVrSHSiAOZri9LExJGSioGpGtZa +fzqLGitkNAygBwYFK4EEACKhZANiAAS0iGk20ReRp1RutjzitgaACkUzVf/eXcRZ +BG44/Uha3GVN+2tlDCd4lkXSTR5GfTQpbYan5NOENeZn70wk+cPyG/5fXfRJW99l +GXVrTgc9XAhu8t7zua8D+K45r6bJTK8= +-----END EC PRIVATE KEY----- \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/testdata/ec_p521_key.pem new/josepy-1.7.0/src/josepy/testdata/ec_p521_key.pem --- old/josepy-1.5.0/src/josepy/testdata/ec_p521_key.pem 1970-01-01 01:00:00.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/testdata/ec_p521_key.pem 2021-02-11 20:42:03.000000000 +0100 @@ -0,0 +1,10 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIB9I82YbB0mhnHdWOnwP4Ag0Gl2x+tKYnfTWNEaqW0+cztSrI0OD/W +WtjJo+8fC+2QWGxV4l1sHpXebBnTG8NWS2OgBwYFK4EEACOhgYkDgYYABABZHZen +CswZj9fFPH336T+CGTdnhA+OYQ3MET5eWXgpcB3C7ZzATLlmBRTvzc5ugy9jIaTd +MyNm3DCyXfeWmNrT7AGL2at8Aco7e50HfJ2DKoRTQi94//lOhj1mXrdYWAWP12R6 +wuMiFdEeCZ7LEQGPXqnNRBJaQnOIc4orNY4ldYkeFg== +-----END EC PRIVATE KEY----- \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/util.py new/josepy-1.7.0/src/josepy/util.py --- old/josepy-1.5.0/src/josepy/util.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/util.py 2021-02-11 20:42:03.000000000 +0100 @@ -6,7 +6,8 @@ import OpenSSL import six -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec, rsa class abstractclassmethod(classmethod): @@ -36,6 +37,7 @@ :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. """ + def __init__(self, wrapped): assert isinstance(wrapped, OpenSSL.crypto.X509) or isinstance( wrapped, OpenSSL.crypto.X509Req) @@ -138,6 +140,34 @@ return hash((self.__class__, pub.n, pub.e)) +class ComparableECKey(ComparableKey): # pylint: disable=too-few-public-methods + """Wrapper for ``cryptography`` RSA keys. + Wraps around: + - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` + - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` + """ + + def __hash__(self): + # public_numbers() hasn't got stable hash! + # https://github.com/pyca/cryptography/issues/2143 + if isinstance(self._wrapped, ec.EllipticCurvePrivateKeyWithSerialization): + priv = self.private_numbers() + pub = priv.public_numbers + return hash((self.__class__, pub.curve.name, pub.x, pub.y, priv.private_value)) + elif isinstance(self._wrapped, ec.EllipticCurvePublicKeyWithSerialization): + pub = self.public_numbers() + return hash((self.__class__, pub.curve.name, pub.x, pub.y)) + + def public_key(self): + """Get wrapped public key.""" + # Unlike RSAPrivateKey, EllipticCurvePrivateKey does not have public_key() + if hasattr(self._wrapped, 'public_key'): + key = self._wrapped.public_key() + else: + key = self._wrapped.public_numbers().public_key(default_backend()) + return self.__class__(key) + + class ImmutableMap(Mapping, Hashable): # type: ignore # pylint: disable=too-few-public-methods """Immutable key to value mapping with attribute access.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy/util_test.py new/josepy-1.7.0/src/josepy/util_test.py --- old/josepy-1.5.0/src/josepy/util_test.py 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/src/josepy/util_test.py 2021-02-11 20:42:03.000000000 +0100 @@ -91,6 +91,52 @@ self.assertTrue(isinstance(self.key.public_key(), ComparableRSAKey)) +class ComparableECKeyTest(unittest.TestCase): + """Tests for josepy.util.ComparableECKey.""" + + def setUp(self): + # test_utl.load_ec_private_key return ComparableECKey + self.p256_key = test_util.load_ec_private_key('ec_p256_key.pem') + self.p256_key_same = test_util.load_ec_private_key('ec_p256_key.pem') + self.p384_key = test_util.load_ec_private_key('ec_p384_key.pem') + self.p521_key = test_util.load_ec_private_key('ec_p521_key.pem') + + def test_getattr_proxy(self): + self.assertEqual(256, self.p256_key.key_size) + + def test_eq(self): + self.assertEqual(self.p256_key, self.p256_key_same) + + def test_ne(self): + self.assertNotEqual(self.p256_key, self.p384_key) + self.assertNotEqual(self.p256_key, self.p521_key) + + def test_ne_different_types(self): + self.assertNotEqual(self.p256_key, 5) + + def test_ne_not_wrapped(self): + # pylint: disable=protected-access + self.assertNotEqual(self.p256_key, self.p256_key_same._wrapped) + + def test_ne_no_serialization(self): + from josepy.util import ComparableECKey + self.assertNotEqual(ComparableECKey(5), ComparableECKey(5)) + + def test_hash(self): + self.assertTrue(isinstance(hash(self.p256_key), int)) + self.assertEqual(hash(self.p256_key), hash(self.p256_key_same)) + self.assertNotEqual(hash(self.p256_key), hash(self.p384_key)) + self.assertNotEqual(hash(self.p256_key), hash(self.p521_key)) + + def test_repr(self): + self.assertTrue(repr(self.p256_key).startswith( + '<ComparableECKey(<cryptography.hazmat.')) + + def test_public_key(self): + from josepy.util import ComparableECKey + self.assertTrue(isinstance(self.p256_key.public_key(), ComparableECKey)) + + class ImmutableMapTest(unittest.TestCase): """Tests for josepy.util.ImmutableMap.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy.egg-info/PKG-INFO new/josepy-1.7.0/src/josepy.egg-info/PKG-INFO --- old/josepy-1.5.0/src/josepy.egg-info/PKG-INFO 2020-11-03 23:41:40.000000000 +0100 +++ new/josepy-1.7.0/src/josepy.egg-info/PKG-INFO 2021-02-11 20:42:42.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: josepy -Version: 1.5.0 +Version: 1.7.0 Summary: JOSE protocol implementation in Python Home-page: https://github.com/certbot/josepy Author: Certbot Project @@ -26,8 +26,6 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 @@ -35,7 +33,7 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.* +Requires-Python: >=3.6 Provides-Extra: dev Provides-Extra: dev3 Provides-Extra: docs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/src/josepy.egg-info/SOURCES.txt new/josepy-1.7.0/src/josepy.egg-info/SOURCES.txt --- old/josepy-1.5.0/src/josepy.egg-info/SOURCES.txt 2020-11-03 23:41:40.000000000 +0100 +++ new/josepy-1.7.0/src/josepy.egg-info/SOURCES.txt 2021-02-11 20:42:42.000000000 +0100 @@ -68,6 +68,9 @@ src/josepy/testdata/csr.der src/josepy/testdata/csr.pem src/josepy/testdata/dsa512_key.pem +src/josepy/testdata/ec_p256_key.pem +src/josepy/testdata/ec_p384_key.pem +src/josepy/testdata/ec_p521_key.pem src/josepy/testdata/rsa1024_key.pem src/josepy/testdata/rsa2048_cert.pem src/josepy/testdata/rsa2048_key.pem diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/josepy-1.5.0/tox.ini new/josepy-1.7.0/tox.ini --- old/josepy-1.5.0/tox.ini 2020-11-03 23:41:27.000000000 +0100 +++ new/josepy-1.7.0/tox.ini 2021-02-11 20:42:03.000000000 +0100 @@ -1,6 +1,5 @@ [tox] envlist = - py27 py3{6,7,8,9} [testenv]