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]

Reply via email to