[Pkg-freeipa-devel] python-jwcrypto: Changes to 'upstream'

Timo Aaltonen tjaalton at moszumanska.debian.org
Mon Sep 19 13:24:26 UTC 2016


 .travis.yml                |   36 +-
 Makefile                   |   17 -
 docs/source/conf.py        |    6 
 docs/source/jwk.rst        |   28 +
 docs/source/jwt.rst        |   32 +
 jwcrypto/common.py         |    4 
 jwcrypto/jwe.py            |  722 ++++++++++++++++++++++++++++++++++++---------
 jwcrypto/jwk.py            |  278 +++++++++++++----
 jwcrypto/jws.py            |   83 ++---
 jwcrypto/jwt.py            |  357 +++++++++++++++++++++-
 jwcrypto/tests-cookbook.py |  635 ++++++++++++++++++++++++++-------------
 jwcrypto/tests.py          |  221 +++++++++----
 setup.py                   |    7 
 tox.ini                    |   29 +
 14 files changed, 1886 insertions(+), 569 deletions(-)

New commits:
commit b81d7ea6459b2ec42da519ba19f983e2f4416fbb
Author: Simo Sorce <simo at redhat.com>
Date:   Wed Aug 31 15:40:52 2016 -0400

    Security Release 0.3.2
    
    Signed-off-by: Simo Sorce <simo at redhat.com>

diff --git a/docs/source/conf.py b/docs/source/conf.py
index a27982e..8cc4cf2 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -55,7 +55,7 @@ copyright = u'2016, JWCrypto Contributors'
 # The short X.Y version.
 version = '0.3'
 # The full version, including alpha/beta/rc tags.
-release = '0.3.1'
+release = '0.3.2'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/setup.py b/setup.py
index be4197a..088971a 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ from setuptools import setup
 
 setup(
     name = 'jwcrypto',
-    version = '0.3.1',
+    version = '0.3.2',
     license = 'LGPLv3+',
     maintainer = 'JWCrypto Project Contributors',
     maintainer_email = 'simo at redhat.com',

commit 70539b7056dc4924f601e5e243c7378c75c6dccc
Author: Simo Sorce <simo at redhat.com>
Date:   Mon Aug 29 12:23:19 2016 -0400

    CVE-2016-6298: Million Messages Attack mitigation
    
    [Backport]
    
    RFC 3218 describes an oracle attack called Million Messages Attack
    against RSA with PKCS1 v1.5 padding.
    
    Depending on how JWEs are used a server may become an Oracle, and the
    mitigation presecribed in RFC 3218 2.3.2 need to be implemented.
    
    Many thanks to Dennis Detering for his responsible disclosure and help
    verifying the mitigation approach.
    
    Signed-off-by: Simo Sorce <simo at redhat.com>

diff --git a/jwcrypto/jwe.py b/jwcrypto/jwe.py
index 0ef4f75..d3330c5 100644
--- a/jwcrypto/jwe.py
+++ b/jwcrypto/jwe.py
@@ -187,6 +187,23 @@ class _Rsa15(_RSA):
     def name(self):
         return 'RSA1_5'
 
+    def unwrap(self, key, keylen, ek, headers):
+        self._check_key(key)
+        # Address MMA attack by implementing RFC 3218 - 2.3.2. Random Filling
+        # provides a random cek that will cause the decryption engine to
+        # run to the end, but will fail decryption later.
+
+        # always generate a random cek so we spend roughly the
+        # same time as in the exception side of the branch
+        cek = os.urandom(keylen)
+        try:
+            cek = super(_Rsa15, self).unwrap(key, keylen, ek, headers)
+            # always raise so we always run through the exception handling
+            # code in all cases
+            raise Exception('Dummy')
+        except Exception:  # pylint: disable=broad-except
+            return cek
+
 
 class _RsaOaep(_RSA):
     def __init__(self):

commit d8c4783fe099f56ae8c0ecb87048ded99ba72020
Author: Simo Sorce <simo at redhat.com>
Date:   Fri Aug 19 11:25:23 2016 -0400

    New version 0.3.1
    
    Signed-off-by: Simo Sorce <simo at redhat.com>

diff --git a/docs/source/conf.py b/docs/source/conf.py
index e8d0dda..a27982e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -55,7 +55,7 @@ copyright = u'2016, JWCrypto Contributors'
 # The short X.Y version.
 version = '0.3'
 # The full version, including alpha/beta/rc tags.
-release = '0.3.0'
+release = '0.3.1'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/setup.py b/setup.py
index b4f668d..be4197a 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ from setuptools import setup
 
 setup(
     name = 'jwcrypto',
-    version = '0.3.0',
+    version = '0.3.1',
     license = 'LGPLv3+',
     maintainer = 'JWCrypto Project Contributors',
     maintainer_email = 'simo at redhat.com',

commit 364121b41f3c1571d74c16cc1e465dc72f4d668d
Author: Simo Sorce <simo at redhat.com>
Date:   Fri Aug 19 10:25:33 2016 -0400

    Update docs and project URL
    
    Version is now at 0.3.0, docs were still sayion 0.1.0.
    Also update setup.py to the correct URL for the project.
    Change classifiers and other code to properly test with 3.4 and 3.5,
    drop 3.3 as it is untested at this point.
    
    Signed-off-by: Simo Sorce <simo at redhat.com>
    Reviewed-by: Christian Heimes <cheimes at redhat.com>
    Closes #51

diff --git a/.travis.yml b/.travis.yml
index 715a256..f17d9f3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,23 +1,33 @@
+sudo: false
+
 language: python
 
-python:
-  - "2.7"
+cache: pip
 
-branches:
-  only:
-    - master
+matrix:
+  include:
+    - python: 2.7
+      env: TOXENV=py27
+    - python: 3.4
+      env: TOXENV=py34
+    - python: 3.5
+      env: TOXENV=py35
+    - python: 3.5
+      env: TOXENV=doc
+    - python: 3.5
+      env: TOXENV=sphinx
+    - python: 3.5
+      env: TOXENV=lint
+    - python: 2.7
+      env: TOXENV=pep8py2
+    - python: 3.5
+      env: TOXENV=pep8py3
 
 install:
+  - pip install --upgrade pip setuptools
+  - pip --version
   - pip install tox
+  - tox --version
 
 script:
   - tox
-
-env:
-  - TOXENV=pep8
-  - TOXENV=py3pep8
-  - TOXENV=lint
-  - TOXENV=py27
-  - TOXENV=py34
-  - TOXENV=doc
-  - TOXENV=sphinx
diff --git a/Makefile b/Makefile
index 3e9604b..5163d78 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,8 @@ lint:
 
 pep8:
 	# Check style consistency
-	tox -e pep8
+	tox -e pep8py2
+	tox -e pep8py3
 
 clean:
 	rm -fr build dist *.egg-info
@@ -19,7 +20,8 @@ cscope:
 test:
 	rm -f .coverage
 	tox -e py27
-	tox -e py34
+	tox -e py34 --skip-missing-interpreter
+	tox -e py35 --skip-missing-interpreter
 
 DOCS_DIR = docs
 .PHONY: docs
diff --git a/docs/source/conf.py b/docs/source/conf.py
index a0ac453..e8d0dda 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -46,16 +46,16 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'JWCrypto'
-copyright = u'2015, JWCrypto Contributors'
+copyright = u'2016, JWCrypto Contributors'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.1'
+version = '0.3'
 # The full version, including alpha/beta/rc tags.
-release = '0.1.0'
+release = '0.3.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/setup.py b/setup.py
index 1d51f15..b4f668d 100755
--- a/setup.py
+++ b/setup.py
@@ -10,12 +10,13 @@ setup(
     license = 'LGPLv3+',
     maintainer = 'JWCrypto Project Contributors',
     maintainer_email = 'simo at redhat.com',
-    url='https://github.com/simo5/jwcrypto',
+    url='https://github.com/latchset/jwcrypto',
     packages = ['jwcrypto'],
     description = 'Implementation of JOSE Web standards',
     classifiers = [
         'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
         'Intended Audience :: Developers',
         'Topic :: Security',
         'Topic :: Software Development :: Libraries :: Python Modules'
diff --git a/tox.ini b/tox.ini
index a83a397..b1689b7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,6 @@
 [tox]
-envlist = py27,py34,doc,sphinx
+envlist = lint,py27,py34,py35,pep8py2,pep8py3,doc,sphinx
+skip_missing_interpreters = true
 
 [testenv]
 setenv =
@@ -10,8 +11,8 @@ deps =
     -r{toxinidir}/requirements.txt
 sitepackages = True
 commands =
-    coverage run -m pytest --capture=no --strict {posargs}
-    coverage report -m
+    {envpython} -m coverage run -m pytest --capture=no --strict {posargs}
+    {envpython} -m coverage report -m
 
 [testenv:lint]
 basepython = python2.7
@@ -20,25 +21,25 @@ deps =
     -r{toxinidir}/requirements.txt
 sitepackages = True
 commands =
-    pylint -d c,r,i,W0613 -r n -f colorized --notes= --disable=star-args ./jwcrypto
+    {envpython} -m pylint -d c,r,i,W0613 -r n -f colorized --notes= --disable=star-args ./jwcrypto
 
-[testenv:pep8]
+[testenv:pep8py2]
 basepython = python2.7
 deps =
     flake8
     flake8-import-order
     pep8-naming
 commands =
-    flake8 {posargs} jwcrypto
+    {envpython} -m flake8 {posargs} jwcrypto
 
-[testenv:py3pep8]
-basepython = python3.4
+[testenv:pep8py3]
+basepython = python3
 deps =
     flake8
     flake8-import-order
     pep8-naming
 commands =
-    flake8 {posargs} jwcrypto
+    {envpython} -m flake8 {posargs} jwcrypto
 
 [testenv:doc]
 deps =

commit 92731158a194a4f971ca5cb70d8da5d6f0e7b726
Author: Simo Sorce <simo at redhat.com>
Date:   Mon Aug 15 14:14:56 2016 -0400

    Fix key generation
    
    The new code was throwing away any additional parameter for the key (like the
    'kid') when generating a new key.
    
    Signed-off-by: Simo Sorce <simo at redhat.com>
    Reviewed-by: Christian Heimes <cheimes at redhat.com>
    Fixes #47
    Closes #48

diff --git a/jwcrypto/jwk.py b/jwcrypto/jwk.py
index 6374801..50163ca 100644
--- a/jwcrypto/jwk.py
+++ b/jwcrypto/jwk.py
@@ -217,8 +217,7 @@ class JWK(object):
         gen(kwargs)
         return obj
 
-    def generate_key(self, **kwargs):
-        params = kwargs.copy()
+    def generate_key(self, **params):
         try:
             kty = params['generate']
             del params['generate']
@@ -252,26 +251,30 @@ class JWK(object):
             size = params['size']
             del params['size']
         key = rsa.generate_private_key(pubexp, size, default_backend())
-        self._import_pyca_pri_rsa(key)
+        self._import_pyca_pri_rsa(key, **params)
 
-    def _import_pyca_pri_rsa(self, key):
+    def _import_pyca_pri_rsa(self, key, **params):
         pn = key.private_numbers()
-        params = {'kty': 'RSA'}
-        params['n'] = self._encode_int(pn.public_numbers.n)
-        params['e'] = self._encode_int(pn.public_numbers.e)
-        params['d'] = self._encode_int(pn.d)
-        params['p'] = self._encode_int(pn.p)
-        params['q'] = self._encode_int(pn.q)
-        params['dp'] = self._encode_int(pn.dmp1)
-        params['dq'] = self._encode_int(pn.dmq1)
-        params['qi'] = self._encode_int(pn.iqmp)
+        params.update(
+            kty='RSA',
+            n=self._encode_int(pn.public_numbers.n),
+            e=self._encode_int(pn.public_numbers.e),
+            d=self._encode_int(pn.d),
+            p=self._encode_int(pn.p),
+            q=self._encode_int(pn.q),
+            dp=self._encode_int(pn.dmp1),
+            dq=self._encode_int(pn.dmq1),
+            qi=self._encode_int(pn.iqmp)
+        )
         self.import_key(**params)
 
-    def _import_pyca_pub_rsa(self, key):
+    def _import_pyca_pub_rsa(self, key, **params):
         pn = key.public_numbers()
-        params = {'kty': 'RSA'}
-        params['n'] = self._encode_int(pn.n)
-        params['e'] = self._encode_int(pn.e)
+        params.update(
+            kty='RSA',
+            n=self._encode_int(pn.n),
+            e=self._encode_int(pn.e)
+        )
         self.import_key(**params)
 
     def _get_curve_by_name(self, name):
@@ -296,23 +299,27 @@ class JWK(object):
             del params['crv']
         curve_name = self._get_curve_by_name(curve)
         key = ec.generate_private_key(curve_name, default_backend())
-        self._import_pyca_pri_ec(key)
+        self._import_pyca_pri_ec(key, **params)
 
-    def _import_pyca_pri_ec(self, key):
+    def _import_pyca_pri_ec(self, key, **params):
         pn = key.private_numbers()
-        params = {'kty': 'EC'}
-        params['crv'] = JWKpycaCurveMap[key.curve.name]
-        params['x'] = self._encode_int(pn.public_numbers.x)
-        params['y'] = self._encode_int(pn.public_numbers.y)
-        params['d'] = self._encode_int(pn.private_value)
+        params.update(
+            kty='EC',
+            crv=JWKpycaCurveMap[key.curve.name],
+            x=self._encode_int(pn.public_numbers.x),
+            y=self._encode_int(pn.public_numbers.y),
+            d=self._encode_int(pn.private_value)
+        )
         self.import_key(**params)
 
-    def _import_pyca_pub_ec(self, key):
+    def _import_pyca_pub_ec(self, key, **params):
         pn = key.public_numbers()
-        params = {'kty': 'EC'}
-        params['crv'] = JWKpycaCurveMap[key.curve.name]
-        params['x'] = self._encode_int(pn.x)
-        params['y'] = self._encode_int(pn.y)
+        params.update(
+            kty='EC',
+            crv=JWKpycaCurveMap[key.curve.name],
+            x=self._encode_int(pn.x),
+            y=self._encode_int(pn.y),
+        )
         self.import_key(**params)
 
     def import_key(self, **kwargs):
diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py
index 68487c2..c09f529 100644
--- a/jwcrypto/tests.py
+++ b/jwcrypto/tests.py
@@ -198,6 +198,8 @@ class TestJWK(unittest.TestCase):
         jwk.JWK.generate(kty='oct', size=256)
         jwk.JWK.generate(kty='RSA', size=4096)
         jwk.JWK.generate(kty='EC', curve='P-521')
+        k = jwk.JWK.generate(kty='oct', size=256, kid='MySymmetricKey')
+        self.assertEqual(k.key_id, 'MySymmetricKey')
 
     def test_export_public_keys(self):
         k = jwk.JWK(**RSAPrivateKey)

commit 5b6568ed91c4a5f78cad758f9d9b375ef0a521b8
Author: Simo Sorce <simo at redhat.com>
Date:   Wed Aug 3 14:58:46 2016 -0400

    Fix typo
    
    Signed-off-by: Simo Sorce <simo at redhat.com>

diff --git a/jwcrypto/jwe.py b/jwcrypto/jwe.py
index 9468f4f..0ef4f75 100644
--- a/jwcrypto/jwe.py
+++ b/jwcrypto/jwe.py
@@ -1012,7 +1012,7 @@ class JWE(object):
          representation, otherwise generates a standard JSON format.
 
         :raises InvalidJWEOperation: if the object cannot serialized
-         with the compact representation and `compat` is True.
+         with the compact representation and `compact` is True.
         :raises InvalidJWEOperation: if no recipients have been added
          to the object.
         """

commit c08e26fd6170b966abe3c3abe3da0465267920b3
Author: Simo Sorce <simo at redhat.com>
Date:   Tue Aug 2 09:04:35 2016 -0400

    New version 0.3.0
    
    Signed-off-by: Simo Sorce <simo at redhat.com>

diff --git a/setup.py b/setup.py
index 0d8ac1b..1d51f15 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ from setuptools import setup
 
 setup(
     name = 'jwcrypto',
-    version = '0.2.1',
+    version = '0.3.0',
     license = 'LGPLv3+',
     maintainer = 'JWCrypto Project Contributors',
     maintainer_email = 'simo at redhat.com',

commit 9494134bec476c30b6f4764010c295cfad1cbb50
Author: Simo Sorce <simo at redhat.com>
Date:   Thu Jul 14 21:58:40 2016 -0400

    Add thumbprint support to JWK
    
    Signed-off-by: Simo Sorce <simo at redhat.com>
    
    Closes #39

diff --git a/jwcrypto/common.py b/jwcrypto/common.py
index b86de09..9882787 100644
--- a/jwcrypto/common.py
+++ b/jwcrypto/common.py
@@ -31,7 +31,7 @@ def base64url_decode(payload):
 def json_encode(string):
     if isinstance(string, bytes):
         string = string.decode('utf-8')
-    return json.dumps(string, separators=(',', ':'))
+    return json.dumps(string, separators=(',', ':'), sort_keys=True)
 
 
 def json_decode(string):
diff --git a/jwcrypto/jwk.py b/jwcrypto/jwk.py
index 0f3d358..6374801 100644
--- a/jwcrypto/jwk.py
+++ b/jwcrypto/jwk.py
@@ -5,6 +5,7 @@ import os
 from binascii import hexlify, unhexlify
 
 from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives.asymmetric import ec
 from cryptography.hazmat.primitives.asymmetric import rsa
 
@@ -554,6 +555,20 @@ class JWK(object):
         obj.import_from_pyca(key)
         return obj
 
+    def thumbprint(self, hashalg=hashes.SHA256()):
+        """Returns the key thumbprint as specified by RFC 7638.
+
+        :param hashalg: A hash function (defaults to SHA256)
+        """
+
+        t = {'kty': self._params['kty']}
+        for name, val in iteritems(JWKValuesRegistry[t['kty']]):
+            if val[2] == 'Required':
+                t[name] = self._key[name]
+        digest = hashes.Hash(hashalg, backend=default_backend())
+        digest.update(bytes(json_encode(t).encode('utf8')))
+        return base64url_encode(digest.finalize())
+
 
 class _JWKkeys(set):
 
diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py
index e4f1c70..68487c2 100644
--- a/jwcrypto/tests.py
+++ b/jwcrypto/tests.py
@@ -35,7 +35,9 @@ PublicKeys = {"keys": [
                     "nqDKgw",
                "e": "AQAB",
                "alg": "RS256",
-               "kid": "2011-04-29"}]}
+               "kid": "2011-04-29"}],
+              "thumbprints": ["cn-I_WNMClehiVp51i_0VpOENW1upEerA8sEam5hn-s",
+                              "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"]}
 
 # RFC 7517 - A.2
 PrivateKeys = {"keys": [
@@ -256,6 +258,13 @@ class TestJWK(unittest.TestCase):
         ks3 = jwk.JWKSet.from_json(ks.export())
         self.assertEqual(len(ks), len(ks3))
 
+    def test_thumbprint(self):
+        for i in range(0, len(PublicKeys['keys'])):
+            k = jwk.JWK(**PublicKeys['keys'][i])
+            self.assertEqual(
+                k.thumbprint(),
+                PublicKeys['thumbprints'][i])
+
 
 # RFC 7515 - A.1
 A1_protected = \

commit 9d5f953f8b0e2bab826cae55fcee46f25ef3c0bd
Author: Simo Sorce <simo at redhat.com>
Date:   Thu Jul 14 21:17:41 2016 -0400

    Enforce minimum required attributes for Keys
    
    Signed-off-by: Simo Sorce <simo at redhat.com>

diff --git a/jwcrypto/jwk.py b/jwcrypto/jwk.py
index acdd8c5..0f3d358 100644
--- a/jwcrypto/jwk.py
+++ b/jwcrypto/jwk.py
@@ -23,19 +23,21 @@ JWKTypesRegistry = {'EC': 'Elliptic Curve',
 # RFC 7518 - 7.5
 # It is part of the JWK Parameters Registry, but we want a more
 # specific map for internal usage
-JWKValuesRegistry = {'EC': {'crv': ('Curve', 'Public'),
-                            'x': ('X Coordinate', 'Public'),
-                            'y': ('Y Coordinate', 'Public'),
-                            'd': ('ECC Private Key', 'Private')},
-                     'RSA': {'n': ('Modulus', 'Public'),
-                             'e': ('Exponent', 'Public'),
-                             'd': ('Private Exponent', 'Private'),
-                             'p': ('First Prime Factor', 'Private'),
-                             'q': ('Second Prime Factor', 'Private'),
-                             'dp': ('First Factor CRT Exponent', 'Private'),
-                             'dq': ('Second Factor CRT Exponent', 'Private'),
-                             'qi': ('First CRT Coefficient', 'Private')},
-                     'oct': {'k': ('Key Value', 'Private')}}
+JWKValuesRegistry = {'EC': {'crv': ('Curve', 'Public', 'Required'),
+                            'x': ('X Coordinate', 'Public', 'Required'),
+                            'y': ('Y Coordinate', 'Public', 'Required'),
+                            'd': ('ECC Private Key', 'Private', None)},
+                     'RSA': {'n': ('Modulus', 'Public', 'Required'),
+                             'e': ('Exponent', 'Public', 'Required'),
+                             'd': ('Private Exponent', 'Private', None),
+                             'p': ('First Prime Factor', 'Private', None),
+                             'q': ('Second Prime Factor', 'Private', None),
+                             'dp': ('First Factor CRT Exponent', 'Private',
+                                    None),
+                             'dq': ('Second Factor CRT Exponent', 'Private',
+                                    None),
+                             'qi': ('First CRT Coefficient', 'Private', None)},
+                     'oct': {'k': ('Key Value', 'Private', 'Required')}}
 """Registry of valid key values"""
 
 JWKParamsRegistry = {'kty': ('Key Type', 'Public', ),
@@ -331,6 +333,10 @@ class JWK(object):
                 while name in names:
                     names.remove(name)
 
+        for name, val in iteritems(JWKValuesRegistry[kty]):
+            if val[2] == 'Required' and name not in self._key:
+                raise InvalidJWKValue('Missing required value %s' % name)
+
         # Unknown key parameters are allowed
         # Let's just store them out of the way
         for name in names:

commit 906e9a25a857573899f8a4851ea7e8c1241a81b7
Author: Simo Sorce <simo at redhat.com>
Date:   Thu Jul 14 22:25:53 2016 -0400

    The Pbes2 algorithm accepts only bytes or string
    
    The Pbes2 algorithm is supposed to get a low entropy password so we break
    the interface so that JWK keys are not accepted at all.
    
    This is in order to avoid potential confusion in users where a low entropy
    password is coerced into an "oct" key and then potentially used as a high
    entropy symmetric key with other algorithms yielding very poor security.
    
    By making password mutually eclusive with keys we try to avoid this mistakes
    by users.
    
    Signed-off-by: Simo Sorce <simo at redhat.com>
    Closes #40

diff --git a/jwcrypto/jwe.py b/jwcrypto/jwe.py
index e524198..9468f4f 100644
--- a/jwcrypto/jwe.py
+++ b/jwcrypto/jwe.py
@@ -156,6 +156,8 @@ class _RSA(_RawKeyMgmt):
         self.padfn = padfn
 
     def _check_key(self, key):
+        if not isinstance(key, JWK):
+            raise ValueError('key is not a JWK object')
         if key.key_type != 'RSA':
             raise InvalidJWEKeyType('RSA', key.key_type)
 
@@ -215,6 +217,8 @@ class _AesKw(_RawKeyMgmt):
         self.keysize = keysize // 8
 
     def _get_key(self, key, op):
+        if not isinstance(key, JWK):
+            raise ValueError('key is not a JWK object')
         if key.key_type != 'oct':
             raise InvalidJWEKeyType('oct', key.key_type)
         rk = base64url_decode(key.get_op_key(op))
@@ -309,6 +313,8 @@ class _AesGcmKw(_RawKeyMgmt):
         self.keysize = keysize // 8
 
     def _get_key(self, key, op):
+        if not isinstance(key, JWK):
+            raise ValueError('key is not a JWK object')
         if key.key_type != 'oct':
             raise InvalidJWEKeyType('oct', key.key_type)
         rk = base64url_decode(key.get_op_key(op))
@@ -390,9 +396,10 @@ class _Pbes2HsAesKw(_RawKeyMgmt):
         self.keysize = keysize // 8
 
     def _get_key(self, alg, key, p2s, p2c):
-        if key.key_type != 'oct':
-            raise InvalidJWEKeyType('oct', key.key_type)
-        plain = base64url_decode(key.get_op_key('encrypt'))
+        if isinstance(key, bytes):
+            plain = key
+        else:
+            plain = key.encode('utf8')
         salt = bytes(self.name.encode('utf8')) + b'\x00' + p2s
 
         if self.hashsize == 256:
@@ -468,6 +475,8 @@ class _Direct(_RawKeyMgmt):
         return 'dir'
 
     def _check_key(self, key):
+        if not isinstance(key, JWK):
+            raise ValueError('key is not a JWK object')
         if key.key_type != 'oct':
             raise InvalidJWEKeyType('oct', key.key_type)
 
@@ -501,6 +510,8 @@ class _EcdhEs(_RawKeyMgmt):
         self.keydatalen = keydatalen
 
     def _check_key(self, key):
+        if not isinstance(key, JWK):
+            raise ValueError('key is not a JWK object')
         if key.key_type != 'EC':
             raise InvalidJWEKeyType('EC', key.key_type)
 
@@ -943,12 +954,11 @@ class JWE(object):
     def add_recipient(self, key, header=None):
         """Encrypt the plaintext with the given key.
 
-        :param key: A JWK key of appropriate type for the 'alg' provided
-         in the JOSE Headers.
+        :param key: A JWK key or password of appropriate type for the 'alg'
+         provided in the JOSE Headers.
         :param header: A JSON string representing the per-recipient header.
 
         :raises ValueError: if the plaintext is missing or not of type bytes.
-        :raises ValueError: if the key is not a JWK object.
         :raises ValueError: if the compression type is unknown.
         :raises InvalidJWAAlgorithm: if the 'alg' provided in the JOSE
          headers is missing or unknown, or otherwise not implemented.
@@ -957,8 +967,6 @@ class JWE(object):
             raise ValueError('Missing plaintext')
         if not isinstance(self.plaintext, bytes):
             raise ValueError("Plaintext must be 'bytes'")
-        if not isinstance(key, JWK):
-            raise ValueError('key is not a JWK object')
 
         jh = self._get_jose_header(header)
         alg, enc = self._get_alg_enc_from_headers(jh)
@@ -1115,14 +1123,14 @@ class JWE(object):
         """Decrypt a JWE token.
 
         :param key: The (:class:`jwcrypto.jwk.JWK`) decryption key.
+        :param key: A (:class:`jwcrypto.jwk.JWK`) decryption key or a password
+         string (optional).
 
         :raises InvalidJWEOperation: if the key is not a JWK object.
         :raises InvalidJWEData: if the ciphertext can't be decrypted or
          the object is otherwise malformed.
         """
 
-        if not isinstance(key, JWK):
-            raise ValueError('key is not a JWK object')
         if 'ciphertext' not in self.objects:
             raise InvalidJWEOperation("No available ciphertext")
         self.decryptlog = list()
@@ -1151,7 +1159,8 @@ class JWE(object):
 
         :param raw_jwe: a 'raw' JWE token (JSON Encoded or Compact
          notation) string.
-        :param key: A (:class:`jwcrypto.jwk.JWK`) decryption key (optional).
+        :param key: A (:class:`jwcrypto.jwk.JWK`) decryption key or a password
+         string (optional).
          If a key is provided a decryption step will be attempted after
          the object is successfully deserialized.
 
diff --git a/jwcrypto/tests-cookbook.py b/jwcrypto/tests-cookbook.py
index a7dd9ec..40b8d36 100644
--- a/jwcrypto/tests-cookbook.py
+++ b/jwcrypto/tests-cookbook.py
@@ -1140,17 +1140,17 @@ class Cookbook08JWETests(unittest.TestCase):
 
     def test_5_3_encryption(self):
         plaintext = Payload_plaintext_5_3_1
-        password = jwk.JWK(kty="oct", use="enc",
-                           k=base64url_encode(Password_5_3_1.decode('utf8')))
+        password = Password_5_3_1
+        unicodepwd = Password_5_3_1.decode('utf8')
         e = jwe.JWE(plaintext, json_encode(JWE_Protected_Header_no_p2x))
         e.add_recipient(password)
         e.serialize(compact=True)
         enc = e.serialize()
-        e.deserialize(enc, password)
+        e.deserialize(enc, unicodepwd)
         self.assertEqual(e.payload, plaintext)
         e.deserialize(JWE_compact_5_3_5, password)
         self.assertEqual(e.payload, plaintext)
-        e.deserialize(json_encode(JWE_general_5_3_5), password)
+        e.deserialize(json_encode(JWE_general_5_3_5), unicodepwd)
         self.assertEqual(e.payload, plaintext)
         e.deserialize(json_encode(JWE_flattened_5_3_5), password)
         self.assertEqual(e.payload, plaintext)

commit 562cdbbf28180a08d384243d6a6faf82b01a2a32
Author: Simo Sorce <simo at redhat.com>
Date:   Thu Jul 14 11:49:10 2016 -0400

    Add ECDH-ES Key Wrapping Algorithms
    
    Signed-off-by: Simo Sorce <simo at redhat.com>
    
    Closes #3
    Closes #38

diff --git a/jwcrypto/jwe.py b/jwcrypto/jwe.py
index f7684a1..e524198 100644
--- a/jwcrypto/jwe.py
+++ b/jwcrypto/jwe.py
@@ -1,14 +1,17 @@
 # Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
 
 import os
+import struct
 import zlib
 
 from binascii import hexlify, unhexlify
 
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import constant_time, hashes, hmac
+from cryptography.hazmat.primitives.asymmetric import ec
 from cryptography.hazmat.primitives.asymmetric import padding
 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
 from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
 from cryptography.hazmat.primitives.padding import PKCS7
 
@@ -487,6 +490,126 @@ class _Direct(_RawKeyMgmt):
         return cek
 
 
+class _EcdhEs(_RawKeyMgmt):
+
+    @property
+    def name(self):
+        return 'ECDH-ES'
+
+    def __init__(self, keydatalen=None):
+        self.backend = default_backend()
+        self.keydatalen = keydatalen
+
+    def _check_key(self, key):
+        if key.key_type != 'EC':
+            raise InvalidJWEKeyType('EC', key.key_type)
+
+    def _derive(self, privkey, pubkey, alg, keydatalen, headers):
+        # OtherInfo is defined in NIST SP 56A 5.8.1.2.1
+
+        # AlgorithmID
+        otherinfo = struct.pack('>I', len(alg))
+        otherinfo += bytes(alg.encode('utf8'))
+
+        # PartyUInfo
+        apu = base64url_decode(headers['apu']) if 'apu' in headers else b''
+        otherinfo += struct.pack('>I', len(apu))
+        otherinfo += apu
+
+        # PartyVInfo
+        apv = base64url_decode(headers['apv']) if 'apv' in headers else b''
+        otherinfo += struct.pack('>I', len(apv))
+        otherinfo += apv
+
+        # SuppPubInfo
+        otherinfo += struct.pack('>I', keydatalen)
+
+        # no SuppPrivInfo
+
+        shared_key = privkey.exchange(ec.ECDH(), pubkey)
+        ckdf = ConcatKDFHash(algorithm=hashes.SHA256(),
+                             length=keydatalen // 8,
+                             otherinfo=otherinfo,
+                             backend=self.backend)
+        return ckdf.derive(shared_key)
+
+    def wrap(self, key, keylen, cek, headers):
+        self._check_key(key)
+        if self.keydatalen is None:
+            if cek is not None:
+                raise InvalidJWEOperation('ECDH-ES cannot use an existing CEK')
+            keydatalen = keylen * 8
+            alg = headers['enc']
+        else:
+            keydatalen = self.keydatalen
+            alg = headers['alg']
+
+        epk = JWK.generate(kty=key.key_type, crv=key.key_curve)
+        dk = self._derive(epk.get_op_key('unwrapKey'),
+                          key.get_op_key('wrapKey'),
+                          alg, keydatalen, headers)
+
+        if self.keydatalen is None:
+            ret = {'cek': dk}
+        else:
+            aeskw = _AesKw(keydatalen)
+            kek = JWK(kty="oct", use="enc", k=base64url_encode(dk))
+            ret = aeskw.wrap(kek, keydatalen // 8, cek, headers)
+
+        ret['header'] = {'epk': json_decode(epk.export_public())}
+        return ret
+
+    def unwrap(self, key, keylen, ek, headers):
+        if 'epk' not in headers:
+            raise InvalidJWEData('Invalid Header, missing "epk" parameter')
+        self._check_key(key)
+        if self.keydatalen is None:
+            keydatalen = keylen * 8
+            alg = headers['enc']
+        else:
+            keydatalen = self.keydatalen
+            alg = headers['alg']
+
+        epk = JWK(**headers['epk'])
+        dk = self._derive(key.get_op_key('unwrapKey'),
+                          epk.get_op_key('wrapKey'),
+                          alg, keydatalen, headers)
+        if self.keydatalen is None:
+            return dk
+        else:
+            aeskw = _AesKw(keydatalen)
+            kek = JWK(kty="oct", use="enc", k=base64url_encode(dk))
+            cek = aeskw.unwrap(kek, keydatalen // 8, ek, headers)
+            return cek
+
+
+class _EcdhEsAes128Kw(_EcdhEs):
+    def __init__(self):
+        super(_EcdhEsAes128Kw, self).__init__(128)
+
+    @property
+    def name(self):
+        return 'ECDH-ES+A128KW'
+
+
+class _EcdhEsAes192Kw(_EcdhEs):
+    def __init__(self):
+        super(_EcdhEsAes192Kw, self).__init__(192)
+
+    @property
+    def name(self):
+        return 'ECDH-ES+A192KW'
+
+
+class _EcdhEsAes256Kw(_EcdhEs):
+    def __init__(self):
+        super(_EcdhEsAes256Kw, self).__init__(256)
+
+    @property
+    def name(self):
+        return 'ECDH-ES+A256KW'
+
+
 class _RawJWE(object):
 
     def encrypt(self, k, a, m):
@@ -689,6 +812,10 @@ class JWE(object):
         'A192KW': _A192KW,
         'A256KW': _A256KW,
         'dir': _Direct,
+        'ECDH-ES': _EcdhEs,
+        'ECDH-ES+A128KW': _EcdhEsAes128Kw,
+        'ECDH-ES+A192KW': _EcdhEsAes192Kw,
+        'ECDH-ES+A256KW': _EcdhEsAes256Kw,
         'A128GCMKW': _A128GcmKw,
         'A192GCMKW': _A192GcmKw,
         'A256GCMKW': _A256GcmKw,
diff --git a/jwcrypto/jwk.py b/jwcrypto/jwk.py
index 73c4b43..acdd8c5 100644
--- a/jwcrypto/jwk.py
+++ b/jwcrypto/jwk.py
@@ -410,6 +410,13 @@ class JWK(object):
         """
         return self._params.get('kid', None)
 
+    @property
+    def key_curve(self):
+        """The Curve Name."""
+        if self._params['kty'] != 'EC':
+            raise InvalidJWKType('Not an EC key')
+        return self._key['crv']
+
     def get_curve(self, arg):
         """Gets the Elliptic Curve associated with the key.
 
diff --git a/jwcrypto/tests-cookbook.py b/jwcrypto/tests-cookbook.py
index a86a416..a7dd9ec 100644
--- a/jwcrypto/tests-cookbook.py
+++ b/jwcrypto/tests-cookbook.py
@@ -692,9 +692,113 @@ JWE_flattened_5_3_5 = {
     "ciphertext": JWE_Ciphertext_5_3_4,
     "tag": JWE_Authentication_Tag_5_3_4}
 



More information about the Pkg-freeipa-devel mailing list