[Pkg-freeipa-devel] [Git][freeipa-team/python-jwcrypto][upstream] 39 commits: Post release bump to 0.9.dev1
Timo Aaltonen (@tjaalton)
gitlab at salsa.debian.org
Mon Aug 16 07:34:25 BST 2021
Timo Aaltonen pushed to branch upstream at FreeIPA packaging / python-jwcrypto
Commits:
f0d93764 by Simo Sorce at 2020-08-24T14:51:12-04:00
Post release bump to 0.9.dev1
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
fa0b6430 by Simo Sorce at 2020-08-26T14:53:47-04:00
Make sure an empty dict is a valid JWT payload
Nowhere in the spec it says the payload must not be an empty dictionary.
It makes little sense to not have any claims, because then, what do you
claim? But that's a problem for the user, especially given the fact you
could already pas in the literal string '{}' and it would get signed.
However a completely empty string is invalid, at least one character,
even just a space is needed.
Fixes #187
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
787f69a8 by inyong_lee at 2020-08-31T09:38:35-04:00
RFC 8812 - Add Default allowed algorithms
- - - - -
0edf66d5 by Simo Sorce at 2020-12-04T09:17:32-05:00
Turn JWK into a dict-like object
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
62a70b01 by Simo Sorce at 2020-12-04T09:17:32-05:00
Go one step further and provide access as attrs
This is allowed only for offical JWK paramters as defined in the
standard, if custom paramters are added to the JWK they have to be
access via the dict like interface.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
37f195a4 by Simo Sorce at 2020-12-04T09:17:32-05:00
Drop support for py34 as it stopped working on F33
- - - - -
e0249b11 by Arumugam at 2020-12-04T10:10:19-05:00
Adding Power support(ppc64le) with ci and testing to the project for architecture independent
- - - - -
c2d281ab by Simo Sorce at 2020-12-14T15:59:54-05:00
Deprecate RSA1_5 and remove from defaults
RSA1_5 relies on the PKCS1 v 1.5 padding algorithm.
This algorithm has been shown time and again to be very difficult to
implement correctly because it requires a fully constant time
implementation including in application level error handling.
Python Cryptography which is our crypto engine documents that the
current API is not constant time.
This patch deprecates the algorithm and removes it from the default
allowed algorithms list. This means that code needs to be changed in
order to explicitly allow RSA1_5 going forward.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
874981e4 by Simo Sorce at 2020-12-17T08:26:11-05:00
Make PBES2 behave like all other algorithms
PBES2 accepts only a plaintext password or bytes which is different
from all other algorithms. Make it support passing in a symmetric key,
and add a jwk classmethod to create a symmetric key from a password.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
fc1e4c3f by Simo Sorce at 2021-01-12T09:54:23-05:00
Enforce protected header in compact serilization
As RFC 7515 Sections 7.10 states the compact serialization must carry
the algorithm in the protected header. Make sure we do not produce a
compact serialization without that.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
0e211fab by Simon Willison at 2021-01-25T08:33:32-05:00
Installation instructions + extra badges
I had to dig around to figure out how to install it.
- - - - -
08cae6a4 by Hielke at 2021-02-08T16:15:17-05:00
Added six as a dependency to avoid import error
- - - - -
e88326ef by Simo Sorce at 2021-02-09T12:19:22-05:00
Add Rust in travis so we can build cryptography
Not always needed but some images build it.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
058f510c by Simo Sorce at 2021-04-21T09:14:08-04:00
Fix importing Public EC keys from PEM files
This same issue was already handled for private keys, but somehow it was
mishandled in public keys.
Fixes #204
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
81288c78 by Simo Sorce at 2021-06-01T09:47:54-04:00
Release 0.9
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
dbb4bed2 by Simo Sorce at 2021-06-03T20:31:44-04:00
Move to Github Actions
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
78207bf8 by Simo Sorce at 2021-06-03T20:46:29-04:00
Create codeql-analysis.yml
- - - - -
69ca09a9 by Simo Sorce at 2021-06-04T14:44:25-04:00
Add back ppc64le CI
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
870a35c3 by Simo Sorce at 2021-06-04T15:34:18-04:00
Actually add the split out ppc64le action
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
756d781d by Simo Sorce at 2021-06-04T15:50:39-04:00
Replace travis badge with Actions badge
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
95751614 by Simo Sorce at 2021-06-09T12:36:27-04:00
Silence a test warning
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
38ecf42d by Simo Sorce at 2021-06-09T12:36:27-04:00
Remove the _params abstraction and simplify JWK
Go one step further than previously done and actually remove the _params
dict and use the object as a real dict as intended.
This should fix multiple issues with the current implementation
including odd behaviors repr() and other functions that were not
overriden properly.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
5f3c4156 by Simo Sorce at 2021-06-09T12:36:27-04:00
Fix crash in exception handler
Direct use of k['kid'] was causing the excpetion itself to
crash on keys that do not have a 'kid' with a keyError.
Always use the .get accessor to pull 'kid' so that None is
returned when not availble.
Fixes #209
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
015cf17a by Simo Sorce at 2021-06-09T12:36:27-04:00
Add test for Issue 209
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
6cd280cb by Simo Sorce at 2021-06-09T14:23:26-04:00
Fix keyset import with similar keys
Keys that are idential except for their key ID were filtered out in set
import. Change __hash__() to include the value of 'kid' as well.
Also add __eq__() function which should always be provided if __hash__()
is implemented.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
57a69e7b by Simo Sorce at 2021-06-09T14:23:26-04:00
Test fix for Issue 208
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
e056cfdc by Simo Sorce at 2021-06-09T14:28:40-04:00
Add __repr__() to mask keys
We do not want to risk private keys to be exposed by accident.
Return a repr that allows to diagnose key issues without disclosing
sensible data.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
1ccdaec0 by Simo Sorce at 2021-06-09T14:30:35-04:00
Release 0.9.1
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
56536cf6 by Simo Sorce at 2021-06-09T14:43:00-04:00
Post release bump to 1.0.dev1
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
82518c94 by Simo Sorce at 2021-06-09T15:02:52-04:00
Create SECURITY.md
- - - - -
6b50656d by Simo Sorce at 2021-06-09T15:19:34-04:00
Show Code Scan button and exclude trivial pulls
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
e8a8216a by Simo Sorce at 2021-07-13T17:51:11-04:00
Allow empty payloads in JWS tokens
Apparently Let's Encrypt's ACME Server accepts JWT tokens that are JWS
tokens with an empty payload.
Fixes #224
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
39a38a9b by Simo Sorce at 2021-07-13T17:51:11-04:00
Add tests to check empty payload support
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
1f5e7517 by Simo Sorce at 2021-07-13T17:53:03-04:00
Drop python2 compatibility
Python 2 is now EOL and python cryptography is going to drop support for
it shortly as well.
There is no reason to maintain compatiblity with it.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
1d8d7f69 by Simo Sorce at 2021-07-13T17:53:03-04:00
Fix python3 pylint issues
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
f7463e54 by Simo Sorce at 2021-07-13T17:54:15-04:00
Add explicit support to check 'typ' in JWT
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
3e983f2a by Simo Sorce at 2021-07-13T18:24:25-04:00
Drop support for importing old MutableMapping
Since we dropped support for older python versions this is not neeed
anymore and it is causing lint to complain.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
5792e6a0 by Simo Sorce at 2021-07-13T18:24:25-04:00
Disable annoying pep8 naming checks
This are normlly nice, but this code was fine till now and changin all
these names is just too much.
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
aa835cb1 by Simo Sorce at 2021-08-02T04:40:33-04:00
Version 1.0
Signed-off-by: Simo Sorce <simo at redhat.com>
- - - - -
19 changed files:
- + .github/workflows/build.yml
- + .github/workflows/codeql-analysis.yml
- + .github/workflows/ppc64le.yml
- − .travis.yml
- Makefile
- README.md
- + SECURITY.md
- docs/source/conf.py
- docs/source/index.rst
- jwcrypto/common.py
- jwcrypto/jwa.py
- jwcrypto/jwe.py
- jwcrypto/jwk.py
- jwcrypto/jws.py
- jwcrypto/jwt.py
- jwcrypto/tests-cookbook.py
- jwcrypto/tests.py
- setup.py
- tox.ini
Changes:
=====================================
.github/workflows/build.yml
=====================================
@@ -0,0 +1,100 @@
+{
+ "name": "Build",
+ "on": {
+ "push": {
+ "branches": [ "master" ]
+ },
+ "pull_request": null,
+ },
+ "jobs": {
+ "linux": {
+ "name": "CI on x86_64",
+ "runs-on": "ubuntu-latest",
+ "strategy": {
+ "fail-fast": false,
+ "matrix": {
+ "name": [
+ "python-36",
+ "python-37",
+ "python-38",
+ "python-39",
+ "doc",
+ "sphinx",
+ "lint",
+ "pep8",
+ ],
+ "include": [
+ {
+ "name": "python-36",
+ "python": "3.6",
+ "toxenv": "py36",
+ "arch": "x64",
+ },
+ {
+ "name": "python-37",
+ "python": "3.7",
+ "toxenv": "py37",
+ "arch": "x64",
+ },
+ {
+ "name": "python-38",
+ "python": "3.8",
+ "toxenv": "py38",
+ "arch": "x64",
+ },
+ {
+ "name": "python-39",
+ "python": "3.9",
+ "toxenv": "py39",
+ "arch": "x64",
+ },
+ {
+ "name": "doc",
+ "python": "3.9",
+ "toxenv": "doc",
+ "arch": "x64",
+ },
+ {
+ "name": "sphinx",
+ "python": "3.9",
+ "toxenv": "sphinx",
+ "arch": "x64",
+ },
+ {
+ "name": "lint",
+ "python": "3.9",
+ "toxenv": "lint",
+ "arch": "x64",
+ },
+ {
+ "name": "pep8",
+ "python": "3.9",
+ "toxenv": "pep8",
+ "arch": "x64",
+ },
+ ],
+ },
+ },
+ "steps": [
+ { "uses": "actions/checkout at v2" },
+ {
+ "uses": "actions/setup-python at v2",
+ "with": {
+ "python-version": "${{ matrix.python }}",
+ "architecture": "${{ matrix.arch }}"
+ },
+ },
+ { "run": "sudo apt-get install cargo" },
+ { "run": "pip --version" },
+ { "run": "pip install tox" },
+ { "run": "tox --version" },
+ {
+ "env": {
+ "TOXENV": "${{matrix.toxenv}}"
+ },
+ "run": "tox",
+ },
+ ],
+ },
+ },
+}
=====================================
.github/workflows/codeql-analysis.yml
=====================================
@@ -0,0 +1,74 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ master ]
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.txt'
+ schedule:
+ - cron: '31 10 * * 5'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'python' ]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+ # Learn more:
+ # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout at v2
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init at v1
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries at main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild at v1
+
+ # ℹ️ Command-line programs to run using the OS shell..
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze at v1
=====================================
.github/workflows/ppc64le.yml
=====================================
@@ -0,0 +1,42 @@
+{
+ "name": "ppc64le CI",
+ "on": {
+ "push": {
+ "branches": [ "master" ]
+ },
+ },
+ "jobs": {
+ "ppc64le": {
+ "name": "CI on ppc64le",
+ "runs-on": "ubuntu-20.04",
+ "strategy": {
+ "fail-fast": false,
+ "matrix": {
+ "name": [
+ "fedora-python",
+ ],
+ "include": [
+ {
+ "name": "fedora-python",
+ "toxenv": "py38",
+ "arch": "ppc64le",
+ },
+ ],
+ },
+ },
+ "steps": [
+ { "uses": "actions/checkout at v2" },
+ {
+ "uses": "uraimo/run-on-arch-action at v2.0.5",
+ "with": {
+ "arch": "${{matrix.arch}}",
+ "distro": "fedora_latest",
+ "install": "dnf install -y openssl-devel cargo python3-pip tox\n",
+ "env": "TOXENV: ${{matrix.toxenv}}",
+ "run": "tox",
+ },
+ },
+ ],
+ },
+ },
+}
=====================================
.travis.yml deleted
=====================================
@@ -1,35 +0,0 @@
-sudo: false
-
-language: python
-
-cache: pip
-
-matrix:
- include:
- - python: 2.7
- env: TOXENV=py27
- - python: 3.4
- env: TOXENV=py34
- - python: 3.5
- env: TOXENV=py35
- - python: 3.6
- env: TOXENV=py36
- - python: 3.6
- env: TOXENV=doc
- - python: 3.6
- env: TOXENV=sphinx
- - python: 3.6
- env: TOXENV=lint
- - python: 2.7
- env: TOXENV=pep8py2
- - python: 3.6
- env: TOXENV=pep8py3
-
-install:
- - pip install --upgrade pip setuptools
- - pip --version
- - pip install tox
- - tox --version
-
-script:
- - tox
=====================================
Makefile
=====================================
@@ -7,8 +7,7 @@ lint:
pep8:
# Check style consistency
- tox -e pep8py2
- tox -e pep8py3
+ tox -e pep8
clean:
rm -fr build dist *.egg-info
@@ -25,11 +24,10 @@ testlong:
test:
rm -f .coverage
- tox -e py27
- tox -e py34 --skip-missing-interpreter
- tox -e py35 --skip-missing-interpreter
tox -e py36 --skip-missing-interpreter
tox -e py37 --skip-missing-interpreter
+ tox -e py38 --skip-missing-interpreter
+ tox -e py39 --skip-missing-interpreter
DOCS_DIR = docs
.PHONY: docs
=====================================
README.md
=====================================
@@ -1,4 +1,8 @@
-[![Build Status](https://travis-ci.org/latchset/jwcrypto.svg?branch=master)](https://travis-ci.org/latchset/jwcrypto)
+[![PyPI](https://img.shields.io/pypi/v/jwcrypto.svg)](https://pypi.org/project/jwcrypto/)
+[![Changelog](https://img.shields.io/github/v/release/latchset/jwcrypto?label=changelog)](https://github.com/latchset/jwcrypto/releases)
+[![Build Status](https://github.com/latchset/jwcrypto/actions/workflows/build.yml/badge.svg)](https://github.com/latchset/jwcrypto/actions/workflows/build.yml)
+[![ppc64le Build](https://github.com/latchset/jwcrypto/actions/workflows/ppc64le.yml/badge.svg)](https://github.com/latchset/jwcrypto/actions/workflows/ppc64le.yml)
+[![Code Scan](https://github.com/latchset/jwcrypto/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/latchset/jwcrypto/actions/workflows/codeql-analysis.yml)
JWCrypto
========
@@ -12,7 +16,32 @@ An implementation of the JOSE Working Group documents:
- RFC 7520 - Examples of Protecting Content Using JSON Object Signing and
Encryption (JOSE)
+Installation
+============
+
+ pip install jwcrypto
+
Documentation
=============
http://jwcrypto.readthedocs.org
+
+Deprecation Notices
+===================
+
+2020.12.11: The RSA1_5 algorithm is now considered deprecated due to numerous
+implementation issues that make it a very problematic tool to use safely..
+The algorithm can still be used but requires explicitly allowing it on object
+instantiation. If your application depends on it there are examples of how to
+re-enable RSA1_5 usage in the tests files.
+
+Note: if you enable support for `RSA1_5` and the attacker can send you chosen
+ciphertext and is able to measure the processing times of your application,
+then your application will be vulnerable to a Bleichenbacher RSA padding
+oracle, allowing the so-called "Million messages attack". That attack allows
+to decrypt intercepted messages (even if they were encrypted with RSA-OAEP) or
+forge signatures (both RSA-PKCS#1 v1.5 and RSASSA-PSS).
+
+Given JWT is generally used in tokens to sign authorization assertions or to
+encrypt private key material, this is a particularly severe issue, and must
+not be underestimated.
=====================================
SECURITY.md
=====================================
@@ -0,0 +1,16 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 0.8 + | :white_check_mark: |
+| < 0.8 | :x: |
+
+## Reporting a Vulnerability
+
+Please contact simo at redhat.com if you have found a security vulnerability
+
+Expect a response within 2 business days (not on week ends or holidays).
+
+If the vulnerbaility is confirmed and accepted you will be given instruction on any embargo or disclosure timeline via email.
=====================================
docs/source/conf.py
=====================================
@@ -53,9 +53,9 @@ copyright = u'2016-2018, JWCrypto Contributors'
# built documents.
#
# The short X.Y version.
-version = '0.8'
+version = '1.0'
# The full version, including alpha/beta/rc tags.
-release = '0.8'
+release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
=====================================
docs/source/index.rst
=====================================
@@ -10,8 +10,7 @@ JWCrypto is an implementation of the Javascript Object Signing and
Encryption (JOSE) Web Standards as they are being developed in the
JOSE_ IETF Working Group and related technology.
-JWCrypto is Python2 and Python3 compatible and uses the Cryptography_
-package for all the crypto functions.
+JWCrypto uses the Cryptography_ package for all the crypto functions.
.. _JOSE: https://datatracker.ietf.org/wg/jose/charter/
.. _Cryptography: https://cryptography.io/
=====================================
jwcrypto/common.py
=====================================
@@ -4,10 +4,7 @@ import copy
import json
from base64 import urlsafe_b64decode, urlsafe_b64encode
from collections import namedtuple
-try:
- from collections.abc import MutableMapping
-except ImportError:
- from collections import MutableMapping
+from collections.abc import MutableMapping
# Padding stripping versions as described in
# RFC 7515 Appendix C
=====================================
jwcrypto/jwa.py
=====================================
@@ -1,8 +1,8 @@
# Copyright (C) 2016 JWCrypto Project Contributors - see LICENSE file
-import abc
import os
import struct
+from abc import ABCMeta, abstractmethod
from binascii import hexlify, unhexlify
from cryptography.exceptions import InvalidSignature
@@ -17,8 +17,6 @@ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.keywrap import aes_key_unwrap, aes_key_wrap
from cryptography.hazmat.primitives.padding import PKCS7
-import six
-
from jwcrypto.common import InvalidCEKeyLength
from jwcrypto.common import InvalidJWAAlgorithm
from jwcrypto.common import InvalidJWEKeyLength
@@ -31,33 +29,32 @@ from jwcrypto.jwk import JWK
# Implements RFC 7518 - JSON Web Algorithms (JWA)
- at six.add_metaclass(abc.ABCMeta)
-class JWAAlgorithm(object):
+class JWAAlgorithm(metaclass=ABCMeta):
- @abc.abstractproperty
+ @property
+ @abstractmethod
def name(self):
"""The algorithm Name"""
- pass
- @abc.abstractproperty
+ @property
+ @abstractmethod
def description(self):
"""A short description"""
- pass
- @abc.abstractproperty
+ @property
+ @abstractmethod
def keysize(self):
"""The actual/recommended/minimum key size"""
- pass
- @abc.abstractproperty
+ @property
+ @abstractmethod
def algorithm_usage_location(self):
"""One of 'alg', 'enc' or 'JWK'"""
- pass
- @abc.abstractproperty
+ @property
+ @abstractmethod
def algorithm_use(self):
"""One of 'sig', 'kex', 'enc'"""
- pass
def _bitsize(x):
@@ -161,7 +158,7 @@ class _RawNone(_RawJWS):
return ''
def verify(self, key, payload, signature):
- if key.key_type != 'oct' or key.get_op_key() != '':
+ if key['kty'] != 'oct' or key.get_op_key() != '':
raise InvalidSignature('The "none" signature cannot be verified')
@@ -353,8 +350,8 @@ class _RSA(_RawKeyMgmt):
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)
+ if key['kty'] != 'RSA':
+ raise InvalidJWEKeyType('RSA', key['kty'])
# FIXME: get key size and insure > 2048 bits
def wrap(self, key, bitsize, cek, headers):
@@ -441,8 +438,8 @@ class _AesKw(_RawKeyMgmt):
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)
+ if key['kty'] != 'oct':
+ raise InvalidJWEKeyType('oct', key['kty'])
rk = base64url_decode(key.get_op_key(op))
if _bitsize(rk) != self.keysize:
raise InvalidJWEKeyLength(self.keysize, _bitsize(rk))
@@ -503,8 +500,8 @@ class _AesGcmKw(_RawKeyMgmt):
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)
+ if key['kty'] != 'oct':
+ raise InvalidJWEKeyType('oct', key['kty'])
rk = base64url_decode(key.get_op_key(op))
if _bitsize(rk) != self.keysize:
raise InvalidJWEKeyLength(self.keysize, _bitsize(rk))
@@ -583,10 +580,15 @@ class _Pbes2HsAesKw(_RawKeyMgmt):
self.aeskwmap = {128: _A128KW, 192: _A192KW, 256: _A256KW}
def _get_key(self, alg, key, p2s, p2c):
- if isinstance(key, bytes):
- plain = key
+ if not isinstance(key, JWK):
+ # backwards compatiblity for old interface
+ if isinstance(key, bytes):
+ plain = key
+ else:
+ plain = key.encode('utf8')
else:
- plain = key.encode('utf8')
+ plain = base64url_decode(key.get_op_key())
+
salt = bytes(self.name.encode('utf8')) + b'\x00' + p2s
if self.hashsize == 256:
@@ -669,8 +671,8 @@ class _Direct(_RawKeyMgmt, JWAAlgorithm):
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)
+ if key['kty'] != 'oct':
+ raise InvalidJWEKeyType('oct', key['kty'])
def wrap(self, key, bitsize, cek, headers):
self._check_key(key)
@@ -706,12 +708,12 @@ class _EcdhEs(_RawKeyMgmt, JWAAlgorithm):
def _check_key(self, key):
if not isinstance(key, JWK):
raise ValueError('key is not a JWK object')
- if key.key_type not in ['EC', 'OKP']:
- raise InvalidJWEKeyType('EC or OKP', key.key_type)
- if key.key_type == 'OKP':
- if key.key_curve not in ['X25519', 'X448']:
+ if key['kty'] not in ['EC', 'OKP']:
+ raise InvalidJWEKeyType('EC or OKP', key['kty'])
+ if key['kty'] == 'OKP':
+ if key['crv'] not in ['X25519', 'X448']:
raise InvalidJWEKeyType('X25519 or X448',
- key.key_curve)
+ key['crv'])
def _derive(self, privkey, pubkey, alg, bitsize, headers):
# OtherInfo is defined in NIST SP 56A 5.8.1.2.1
@@ -759,7 +761,7 @@ class _EcdhEs(_RawKeyMgmt, JWAAlgorithm):
else:
alg = headers['alg']
- epk = JWK.generate(kty=key.key_type, crv=key.key_curve)
+ epk = JWK.generate(kty=key['kty'], crv=key['crv'])
dk = self._derive(epk.get_op_key('unwrapKey'),
key.get_op_key('wrapKey'),
alg, dk_size, headers)
@@ -835,13 +837,13 @@ class _EdDsa(_RawJWS, JWAAlgorithm):
def sign(self, key, payload):
- if key.key_curve in ['Ed25519', 'Ed448']:
+ if key['crv'] in ['Ed25519', 'Ed448']:
skey = key.get_op_key('sign')
return skey.sign(payload)
raise NotImplementedError
def verify(self, key, payload, signature):
- if key.key_curve in ['Ed25519', 'Ed448']:
+ if key['crv'] in ['Ed25519', 'Ed448']:
pkey = key.get_op_key('verify')
return pkey.verify(signature, payload)
raise NotImplementedError
@@ -1099,21 +1101,21 @@ class JWA(object):
try:
return cls.instantiate_alg(name, use='sig')
except KeyError:
- raise InvalidJWAAlgorithm(
- '%s is not a valid Signign algorithm name' % name)
+ raise InvalidJWAAlgorithm('%s is not a valid Signign algorithm'
+ ' name' % name) from None
@classmethod
def keymgmt_alg(cls, name):
try:
return cls.instantiate_alg(name, use='kex')
except KeyError:
- raise InvalidJWAAlgorithm(
- '%s is not a valid Key Management algorithm name' % name)
+ raise InvalidJWAAlgorithm('%s is not a valid Key Management'
+ ' algorithm name' % name) from None
@classmethod
def encryption_alg(cls, name):
try:
return cls.instantiate_alg(name, use='enc')
except KeyError:
- raise InvalidJWAAlgorithm(
- '%s is not a valid Encryption algorithm name' % name)
+ raise InvalidJWAAlgorithm('%s is not a valid Encryption'
+ ' algorithm name' % name) from None
=====================================
jwcrypto/jwe.py
=====================================
@@ -33,7 +33,7 @@ JWEHeaderRegistry = {
default_allowed_algs = [
# Key Management Algorithms
- 'RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256',
+ 'RSA-OAEP', 'RSA-OAEP-256',
'A128KW', 'A192KW', 'A256KW',
'dir',
'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
@@ -477,10 +477,10 @@ class JWE(object):
if 'header' in djwe:
o['header'] = json_encode(djwe['header'])
- except ValueError:
+ except ValueError as e:
c = raw_jwe.split('.')
if len(c) != 5:
- raise InvalidJWEData()
+ raise InvalidJWEData() from e
p = base64url_decode(c[0])
o['protected'] = p.decode('utf-8')
ekey = base64url_decode(c[1])
@@ -493,7 +493,7 @@ class JWE(object):
self.objects = o
except Exception as e: # pylint: disable=broad-except
- raise InvalidJWEData('Invalid format', repr(e))
+ raise InvalidJWEData('Invalid format', repr(e)) from e
if key:
self.decrypt(key)
=====================================
jwcrypto/jwk.py
=====================================
@@ -11,7 +11,7 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import rsa
-from six import iteritems
+from deprecated import deprecated
from jwcrypto.common import JWException
from jwcrypto.common import base64url_decode, base64url_encode
@@ -151,7 +151,7 @@ JWKParamsRegistry = {
"""Registry of valid key parameters"""
# RFC 7518 - 7.6 , RFC 8037 - 5
-# secp256k1 - https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms
+# RFC 8812 - 4.4
JWKEllipticCurveRegistry = {'P-256': 'P-256 curve',
'P-384': 'P-384 curve',
'P-521': 'P-521 curve',
@@ -261,10 +261,8 @@ class InvalidJWKValue(JWException):
on the key type or other constraints.
"""
- pass
-
-class JWK(object):
+class JWK(dict):
"""JSON Web Key object
This object represent a Key.
@@ -302,9 +300,7 @@ class JWK(object):
:raises InvalidJWKValue: if incorrect or inconsistent parameters
are provided.
"""
- self._params = dict()
- self._key = dict()
- self._unknown = dict()
+ super(JWK, self).__init__()
if 'generate' in kwargs:
self.generate_key(**kwargs)
@@ -318,8 +314,8 @@ class JWK(object):
try:
kty = kwargs['kty']
gen = getattr(obj, '_generate_%s' % kty)
- except (KeyError, AttributeError):
- raise InvalidJWKType(kty)
+ except (KeyError, AttributeError) as e:
+ raise InvalidJWKType(kty) from e
gen(kwargs)
return obj
@@ -328,8 +324,8 @@ class JWK(object):
try:
kty = params.pop('generate')
gen = getattr(self, '_generate_%s' % kty)
- except (KeyError, AttributeError):
- raise InvalidJWKType(kty)
+ except (KeyError, AttributeError) as e:
+ raise InvalidJWKType(kty) from e
gen(params)
@@ -341,8 +337,8 @@ class JWK(object):
try:
from jwcrypto.jwa import JWA
alg = JWA.instantiate_alg(params['alg'])
- except KeyError:
- raise ValueError("Invalid 'alg' parameter")
+ except KeyError as e:
+ raise ValueError("Invalid 'alg' parameter") from e
size = alg.keysize
return size
@@ -437,11 +433,12 @@ class JWK(object):
def _import_pyca_pub_ec(self, key, **params):
pn = key.public_numbers()
+ key_size = pn.curve.key_size
params.update(
kty='EC',
crv=JWKpycaCurveMap[key.curve.name],
- x=self._encode_int(pn.x),
- y=self._encode_int(pn.y),
+ x=self._encode_int(pn.x, key_size),
+ y=self._encode_int(pn.y, key_size),
)
self.import_key(**params)
@@ -450,13 +447,13 @@ class JWK(object):
raise InvalidJWKValue('Must specify "crv" for OKP key generation')
try:
key = _OKP_CURVES_TABLE[params['crv']].privkey.generate()
- except KeyError:
+ except KeyError as e:
raise InvalidJWKValue('"%s" is not a supported curve for the '
- 'OKP key type' % params['crv'])
+ 'OKP key type' % params['crv']) from e
self._import_pyca_pri_okp(key, **params)
def _okp_curve_from_pyca_key(self, key):
- for name, val in iteritems(_OKP_CURVES_TABLE):
+ for name, val in _OKP_CURVES_TABLE.items():
if isinstance(key, (val.pubkey, val.privkey)):
return name
raise InvalidJWKValue('Invalid OKP Key object %r' % key)
@@ -486,82 +483,90 @@ class JWK(object):
self.import_key(**params)
def import_key(self, **kwargs):
+ newkey = dict()
+ key_vals = 0
+
names = list(kwargs.keys())
for name in list(JWKParamsRegistry.keys()):
if name in kwargs:
- self._params[name] = kwargs[name]
+ newkey[name] = kwargs[name]
while name in names:
names.remove(name)
- kty = self._params.get('kty', None)
+ kty = newkey.get('kty')
if kty not in JWKTypesRegistry:
raise InvalidJWKType(kty)
for name in list(JWKValuesRegistry[kty].keys()):
if name in kwargs:
- self._key[name] = kwargs[name]
+ newkey[name] = kwargs[name]
+ key_vals += 1
while name in names:
names.remove(name)
- for name, val in iteritems(JWKValuesRegistry[kty]):
- if val.required and name not in self._key:
+ for name, val in JWKValuesRegistry[kty].items():
+ if val.required and name not in newkey:
raise InvalidJWKValue('Missing required value %s' % name)
- if val.type == ParmType.unsupported and name in self._key:
+ if val.type == ParmType.unsupported and name in newkey:
raise InvalidJWKValue('Unsupported parameter %s' % name)
- if val.type == ParmType.b64 and name in self._key:
+ if val.type == ParmType.b64 and name in newkey:
# Check that the value is base64url encoded
try:
- base64url_decode(self._key[name])
- except Exception: # pylint: disable=broad-except
+ base64url_decode(newkey[name])
+ except Exception as e: # pylint: disable=broad-except
raise InvalidJWKValue(
'"%s" is not base64url encoded' % name
- )
- if val[3] == ParmType.b64u and name in self._key:
+ ) from e
+ if val.type == ParmType.b64u and name in newkey:
# Check that the value is Base64urlUInt encoded
try:
- self._decode_int(self._key[name])
- except Exception: # pylint: disable=broad-except
+ self._decode_int(newkey[name])
+ except Exception as e: # pylint: disable=broad-except
raise InvalidJWKValue(
'"%s" is not Base64urlUInt encoded' % name
- )
+ ) from e
# Unknown key parameters are allowed
- # Let's just store them out of the way
for name in names:
- self._unknown[name] = kwargs[name]
+ newkey[name] = kwargs[name]
- if len(self._key) == 0:
+ if key_vals == 0:
raise InvalidJWKValue('No Key Values found')
# check key_ops
- if 'key_ops' in self._params:
- for ko in self._params['key_ops']:
+ if 'key_ops' in newkey:
+ for ko in newkey['key_ops']:
c = 0
- for cko in self._params['key_ops']:
+ for cko in newkey['key_ops']:
if ko == cko:
c += 1
if c != 1:
raise InvalidJWKValue('Duplicate values in "key_ops"')
# check use/key_ops consistency
- if 'use' in self._params and 'key_ops' in self._params:
+ if 'use' in newkey and 'key_ops' in newkey:
sigl = ['sign', 'verify']
encl = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey',
'deriveKey', 'deriveBits']
- if self._params['use'] == 'sig':
+ if newkey['use'] == 'sig':
for op in encl:
- if op in self._params['key_ops']:
+ if op in newkey['key_ops']:
raise InvalidJWKValue('Incompatible "use" and'
' "key_ops" values specified at'
' the same time')
- elif self._params['use'] == 'enc':
+ elif newkey['use'] == 'enc':
for op in sigl:
- if op in self._params['key_ops']:
+ if op in newkey['key_ops']:
raise InvalidJWKValue('Incompatible "use" and'
' "key_ops" values specified at'
' the same time')
+ self.clear()
+ # must set 'kty' as first item
+ self.__setitem__('kty', newkey['kty'])
+ self.update(newkey)
+
@classmethod
def from_json(cls, key):
"""Creates a RFC 7517 JWK from the standard JSON format.
@@ -572,7 +577,7 @@ class JWK(object):
try:
jkey = json_decode(key)
except Exception as e: # pylint: disable=broad-except
- raise InvalidJWKValue(e)
+ raise InvalidJWKValue from e
obj.import_key(**jkey)
return obj
@@ -607,22 +612,20 @@ class JWK(object):
if not self.has_public:
raise InvalidJWKType("No public key available")
pub = {}
- preg = JWKParamsRegistry
- for name in preg:
- if preg[name].public:
- if name in self._params:
- pub[name] = self._params[name]
- reg = JWKValuesRegistry[self._params['kty']]
- for param in reg:
- if reg[param].public:
- pub[param] = self._key[param]
+ reg = JWKParamsRegistry
+ for name in reg:
+ if reg[name].public:
+ if name in self.keys():
+ pub[name] = self.get(name)
+ reg = JWKValuesRegistry[self.get('kty')]
+ for name in reg:
+ if reg[name].public:
+ pub[name] = self.get(name)
return pub
def _export_all(self, as_dict=False):
d = dict()
- d.update(self._params)
- d.update(self._key)
- d.update(self._unknown)
+ d.update(self)
if as_dict is True:
return d
return json_encode(d)
@@ -648,48 +651,52 @@ class JWK(object):
@property
def has_public(self):
- """Whether this JWK has an asymmetric Public key."""
+ """Whether this JWK has an asymmetric Public key value."""
if self.is_symmetric:
return False
- reg = JWKValuesRegistry[self._params['kty']]
- for value in reg:
- if reg[value].public and value in self._key:
+ reg = JWKValuesRegistry[self.get('kty')]
+ for name in reg:
+ if reg[name].public and name in self.keys():
return True
+ return False
@property
def has_private(self):
- """Whether this JWK has an asymmetric key Private key."""
+ """Whether this JWK has an asymmetric Private key value."""
if self.is_symmetric:
return False
- reg = JWKValuesRegistry[self._params['kty']]
- for value in reg:
- if not reg[value].public and value in self._key:
+ reg = JWKValuesRegistry[self.get('kty')]
+ for name in reg:
+ if not reg[name].public and name in self.keys():
return True
return False
@property
def is_symmetric(self):
"""Whether this JWK is a symmetric key."""
- return self.key_type == 'oct'
+ return self.get('kty') == 'oct'
@property
+ @deprecated
def key_type(self):
"""The Key type"""
- return self._params.get('kty', None)
+ return self.get('kty')
@property
+ @deprecated
def key_id(self):
"""The Key ID.
Provided by the kid parameter if present, otherwise returns None..
"""
- return self._params.get('kid', None)
+ return self.get('kid')
@property
+ @deprecated
def key_curve(self):
"""The Curve Name."""
- if self._params['kty'] not in ['EC', 'OKP']:
+ if self.get('kty') not in ['EC', 'OKP']:
raise InvalidJWKType('Not an EC or OKP key')
- return self._key['crv']
+ return self.get('crv')
def get_curve(self, arg):
"""Gets the Elliptic Curve associated with the key.
@@ -699,20 +706,20 @@ class JWK(object):
:raises InvalidJWKType: the key is not an EC or OKP key.
:raises InvalidJWKValue: if the curve names is invalid.
"""
- k = self._key
- if self._params['kty'] not in ['EC', 'OKP']:
+ crv = self.get('crv')
+ if self.get('kty') not in ['EC', 'OKP']:
raise InvalidJWKType('Not an EC or OKP key')
- if arg and k['crv'] != arg:
+ if arg and crv != arg:
raise InvalidJWKValue('Curve requested is "%s", but '
- 'key curve is "%s"' % (arg, k['crv']))
+ 'key curve is "%s"' % (arg, crv))
- return self._get_curve_by_name(k['crv'])
+ return self._get_curve_by_name(crv)
def _check_constraints(self, usage, operation):
- use = self._params.get('use', None)
+ use = self.get('use')
if use and use != usage:
raise InvalidJWKUsage(usage, use)
- ops = self._params.get('key_ops', None)
+ ops = self.get('key_ops')
if ops:
if not isinstance(ops, list):
ops = [ops]
@@ -723,65 +730,72 @@ class JWK(object):
def _decode_int(self, n):
return int(hexlify(base64url_decode(n)), 16)
- def _rsa_pub(self, k):
- return rsa.RSAPublicNumbers(self._decode_int(k['e']),
- self._decode_int(k['n']))
-
- def _rsa_pri(self, k):
- return rsa.RSAPrivateNumbers(self._decode_int(k['p']),
- self._decode_int(k['q']),
- self._decode_int(k['d']),
- self._decode_int(k['dp']),
- self._decode_int(k['dq']),
- self._decode_int(k['qi']),
- self._rsa_pub(k))
-
- def _ec_pub(self, k, curve):
- return ec.EllipticCurvePublicNumbers(self._decode_int(k['x']),
- self._decode_int(k['y']),
- self.get_curve(curve))
-
- def _ec_pri(self, k, curve):
- return ec.EllipticCurvePrivateNumbers(self._decode_int(k['d']),
- self._ec_pub(k, curve))
-
- def _okp_pub(self, k):
+ def _rsa_pub(self):
+ e = self._decode_int(self.get('e'))
+ n = self._decode_int(self.get('n'))
+ return rsa.RSAPublicNumbers(e, n)
+
+ def _rsa_pri(self):
+ p = self._decode_int(self.get('p'))
+ q = self._decode_int(self.get('q'))
+ d = self._decode_int(self.get('d'))
+ dp = self._decode_int(self.get('dp'))
+ dq = self._decode_int(self.get('dq'))
+ qi = self._decode_int(self.get('qi'))
+ return rsa.RSAPrivateNumbers(p, q, d, dp, dq, qi, self._rsa_pub())
+
+ def _ec_pub(self, curve):
+ x = self._decode_int(self.get('x'))
+ y = self._decode_int(self.get('y'))
+ return ec.EllipticCurvePublicNumbers(x, y, self.get_curve(curve))
+
+ def _ec_pri(self, curve):
+ d = self._decode_int(self.get('d'))
+ return ec.EllipticCurvePrivateNumbers(d, self._ec_pub(curve))
+
+ def _okp_pub(self):
+ crv = self.get('crv')
try:
- pubkey = _OKP_CURVES_TABLE[k['crv']].pubkey
- except KeyError:
- raise InvalidJWKValue('Unknown curve "%s"' % k['crv'])
+ pubkey = _OKP_CURVES_TABLE[crv].pubkey
+ except KeyError as e:
+ raise InvalidJWKValue('Unknown curve "%s"' % crv) from e
- return pubkey.from_public_bytes(base64url_decode(k['x']))
+ x = base64url_decode(self.get('x'))
+ return pubkey.from_public_bytes(x)
- def _okp_pri(self, k):
+ def _okp_pri(self):
+ crv = self.get('crv')
try:
- privkey = _OKP_CURVES_TABLE[k['crv']].privkey
- except KeyError:
- raise InvalidJWKValue('Unknown curve "%s"' % k['crv'])
+ privkey = _OKP_CURVES_TABLE[crv].privkey
+ except KeyError as e:
+ raise InvalidJWKValue('Unknown curve "%s"' % crv) from e
- return privkey.from_private_bytes(base64url_decode(k['d']))
+ d = base64url_decode(self.get('d'))
+ return privkey.from_private_bytes(d)
def _get_public_key(self, arg=None):
- if self._params['kty'] == 'oct':
- return self._key['k']
- elif self._params['kty'] == 'RSA':
- return self._rsa_pub(self._key).public_key(default_backend())
- elif self._params['kty'] == 'EC':
- return self._ec_pub(self._key, arg).public_key(default_backend())
- elif self._params['kty'] == 'OKP':
- return self._okp_pub(self._key)
+ ktype = self.get('kty')
+ if ktype == 'oct':
+ return self.get('k')
+ elif ktype == 'RSA':
+ return self._rsa_pub().public_key(default_backend())
+ elif ktype == 'EC':
+ return self._ec_pub(arg).public_key(default_backend())
+ elif ktype == 'OKP':
+ return self._okp_pub()
else:
raise NotImplementedError
def _get_private_key(self, arg=None):
- if self._params['kty'] == 'oct':
- return self._key['k']
- elif self._params['kty'] == 'RSA':
- return self._rsa_pri(self._key).private_key(default_backend())
- elif self._params['kty'] == 'EC':
- return self._ec_pri(self._key, arg).private_key(default_backend())
- elif self._params['kty'] == 'OKP':
- return self._okp_pri(self._key)
+ ktype = self.get('kty')
+ if ktype == 'oct':
+ return self.get('k')
+ elif ktype == 'RSA':
+ return self._rsa_pri().private_key(default_backend())
+ elif ktype == 'EC':
+ return self._ec_pri(arg).private_key(default_backend())
+ elif ktype == 'OKP':
+ return self._okp_pri()
else:
raise NotImplementedError
@@ -801,13 +815,13 @@ class JWK(object):
:raises InvalidJWKUsage: if the use constraints do not permit
the operation.
"""
- validops = self._params.get('key_ops',
- list(JWKOperationsRegistry.keys()))
+ validops = self.get('key_ops',
+ list(JWKOperationsRegistry.keys()))
if validops is not list:
validops = [validops]
if operation is None:
- if self._params['kty'] == 'oct':
- return self._key['k']
+ if self.get('kty') == 'oct':
+ return self.get('k')
raise InvalidJWKOperation(operation, validops)
elif operation == 'sign':
self._check_constraints('sig', operation)
@@ -840,7 +854,7 @@ class JWK(object):
else:
raise InvalidJWKValue('Unknown key object %r' % key)
- def import_from_pem(self, data, password=None):
+ def import_from_pem(self, data, password=None, kid=None):
"""Imports a key from data loaded from a PEM file.
The key may be encrypted with a password.
Private keys (PKCS#8 format), public keys, and X509 certificate's
@@ -865,10 +879,13 @@ class JWK(object):
data, backend=default_backend())
key = cert.public_key()
except ValueError:
+ # pylint: disable=raise-missing-from
raise e
self.import_from_pyca(key)
- self._params['kid'] = self.thumbprint()
+ if kid is None:
+ kid = self.thumbprint()
+ self.__setitem__('kid', kid)
def export_to_pem(self, private_key=False, password=False):
"""Exports keys to a data buffer suitable to be stored as a PEM file.
@@ -929,14 +946,147 @@ class JWK(object):
:param hashalg: A hash function (defaults to SHA256)
"""
- t = {'kty': self._params['kty']}
- for name, val in iteritems(JWKValuesRegistry[t['kty']]):
+ t = {'kty': self.get('kty')}
+ for name, val in JWKValuesRegistry[t['kty']].items():
if val.required:
- t[name] = self._key[name]
+ t[name] = self.get(name)
digest = hashes.Hash(hashalg, backend=default_backend())
digest.update(bytes(json_encode(t).encode('utf8')))
return base64url_encode(digest.finalize())
+ # Methods to constrain what this dict allows
+ def __setitem__(self, item, value):
+ kty = self.get('kty')
+
+ if item == 'kty':
+ if kty is None:
+ if value not in JWKTypesRegistry:
+ raise InvalidJWKType(value)
+ super(JWK, self).__setitem__(item, value)
+ return
+ elif kty != value:
+ raise ValueError('Cannot change key type')
+
+ # Check if item is a key value and verify its format
+ if item in list(JWKValuesRegistry[kty].keys()):
+ if JWKValuesRegistry[kty][item].type == ParmType.b64:
+ try:
+ v = base64url_decode(value)
+ # empty values are also invalid except for the
+ # special case of 'oct' key where an empty value
+ # is used to indicate a 'None' key
+ if v == b'' and kty != 'oct' and item != 'k':
+ raise ValueError
+ except Exception as e: # pylint: disable=broad-except
+ raise InvalidJWKValue(
+ '"%s" is not base64url encoded' % item
+ ) from e
+ elif JWKValuesRegistry[kty][item].type == ParmType.b64u:
+ try:
+ self._decode_int(value)
+ except Exception as e: # pylint: disable=broad-except
+ raise InvalidJWKValue(
+ '"%s" is not Base64urlUInt encoded' % item
+ ) from e
+ super(JWK, self).__setitem__(item, value)
+ return
+
+ # If not a key param check if it is a know parameter
+ if item in list(JWKParamsRegistry.keys()):
+ super(JWK, self).__setitem__(item, value)
+ return
+
+ # if neither a key param nor a known parameter, check if we are
+ # trying to set a parameter for a different key type and refuse
+ # in this case.
+ for name in list(JWKTypesRegistry.keys()):
+ if name == kty:
+ continue
+ if item in list(JWKValuesRegistry[name].keys()):
+ raise KeyError("Cannot set '{}' on '{}' key type".format(
+ item, kty))
+
+ # ok if we've come this far it means we have an unknown parameter
+ super(JWK, self).__setitem__(item, value)
+
+ def update(self, *args, **kwargs):
+ for k, v in dict(*args, **kwargs).items():
+ self.__setitem__(k, v)
+
+ def setdefault(self, key, default=None):
+ if key not in self.keys():
+ self.__setitem__(key, default)
+ return self.get(key)
+
+ def __delitem__(self, item):
+ param = self.get(item)
+ if param is None:
+ raise KeyError(item)
+
+ if item == 'kty':
+ for name in list(JWKValuesRegistry[param].keys()):
+ if self.get(name) is not None:
+ raise KeyError("Cannot remove 'kty', values present")
+
+ super(JWK, self).__delitem__(item)
+
+ def __eq__(self, other):
+ if not isinstance(other, JWK):
+ return NotImplemented
+
+ return self.thumbprint() == other.thumbprint() and \
+ self.get('kid') == other.get('kid')
+
+ def __hash__(self):
+ return hash((self.thumbprint(), self.get('kid')))
+
+ def __getattr__(self, item):
+ try:
+ if item in JWKParamsRegistry.keys():
+ if item in self.keys():
+ return self.get(item)
+ kty = self.get('kty')
+ if kty is not None:
+ if item in list(JWKValuesRegistry[kty].keys()):
+ if item in self.keys():
+ return self.get(item)
+ raise KeyError
+ except KeyError:
+ raise AttributeError(item) from None
+
+ def __setattr__(self, item, value):
+ try:
+ if item in JWKParamsRegistry.keys():
+ self.__setitem__(item, value)
+ for name in list(JWKTypesRegistry.keys()):
+ if item in list(JWKValuesRegistry[name].keys()):
+ self.__setitem__(item, value)
+ super(JWK, self).__setattr__(item, value)
+ except KeyError:
+ raise AttributeError(item) from None
+
+ @classmethod
+ def from_password(cls, password):
+ """Creates a symmetric JWK key from a user password.
+
+ :param key: A password in utf8 format.
+ """
+ obj = cls()
+ params = {'kty': 'oct'}
+ try:
+ params['k'] = base64url_encode(password.encode('utf8'))
+ except Exception as e: # pylint: disable=broad-except
+ raise InvalidJWKValue from e
+ obj.import_key(**params)
+ return obj
+
+ # Prevent accidental disclosure of key material via repr()
+ def __repr__(self):
+ repr_dict = dict()
+ repr_dict['kid'] = self.get('kid', 'Missing Key ID')
+ repr_dict['thumbprint'] = self.thumbprint()
+ return json_encode(repr_dict)
+
class _JWKkeys(set):
@@ -971,15 +1121,20 @@ class JWKSet(dict):
return self['keys'].__contains__(key)
def __setitem__(self, key, val):
- if key == 'keys':
+ if key == 'keys' and not isinstance(val, _JWKkeys):
self['keys'].add(val)
else:
super(JWKSet, self).__setitem__(key, val)
def update(self, *args, **kwargs):
- for k, v in iteritems(dict(*args, **kwargs)):
+ for k, v in dict(*args, **kwargs).items():
self.__setitem__(k, v)
+ def setdefault(self, key, default=None):
+ if key not in self.keys():
+ self.__setitem__(key, default)
+ return self.get(key)
+
def add(self, elem):
self['keys'].add(elem)
@@ -993,7 +1148,7 @@ class JWKSet(dict):
a JSON object
"""
exp_dict = dict()
- for k, v in iteritems(self):
+ for k, v in self.items():
if k == 'keys':
keys = list()
for jwk in v:
@@ -1011,13 +1166,13 @@ class JWKSet(dict):
"""
try:
jwkset = json_decode(keyset)
- except Exception: # pylint: disable=broad-except
- raise InvalidJWKValue()
+ except Exception as e: # pylint: disable=broad-except
+ raise InvalidJWKValue from e
if 'keys' not in jwkset:
- raise InvalidJWKValue()
+ raise InvalidJWKValue
- for k, v in iteritems(jwkset):
+ for k, v in jwkset.items():
if k == 'keys':
for jwk in v:
self['keys'].add(JWK(**jwk))
@@ -1039,6 +1194,17 @@ class JWKSet(dict):
:param kid: the 'kid' key identifier.
"""
for jwk in self['keys']:
- if jwk.key_id == kid:
+ if jwk.get('kid') == kid:
return jwk
return None
+
+ def __repr__(self):
+ repr_dict = dict()
+ for k, v in self.items():
+ if k == 'keys':
+ keys = list()
+ for jwk in v:
+ keys.append(repr(jwk))
+ v = keys
+ repr_dict[k] = v
+ return json_encode(repr_dict)
=====================================
jwcrypto/jws.py
=====================================
@@ -30,7 +30,7 @@ default_allowed_algs = [
'RS256', 'RS384', 'RS512',
'ES256', 'ES384', 'ES512',
'PS256', 'PS384', 'PS512',
- 'EdDSA']
+ 'EdDSA', 'ES256K']
"""Default allowed algorithms"""
@@ -163,7 +163,7 @@ class JWSCore(object):
sigin = b'.'.join([self.protected.encode('utf-8'), payload])
self.engine.verify(self.key, sigin, signature)
except Exception as e: # pylint: disable=broad-except
- raise InvalidJWSSignature('Verification failed', repr(e))
+ raise InvalidJWSSignature('Verification failed') from e
return True
@@ -180,8 +180,7 @@ class JWS(object):
:param header_registry: Optional additions to the header registry
"""
self.objects = dict()
- if payload:
- self.objects['payload'] = payload
+ self.objects['payload'] = payload
self.verifylog = None
self._allowed_algs = None
self.header_registry = JWSEHeaderRegistry(JWSHeaderRegistry)
@@ -400,7 +399,8 @@ class JWS(object):
except ValueError:
c = raw_jws.split('.')
if len(c) != 3:
- raise InvalidJWSObject('Unrecognized representation')
+ raise InvalidJWSObject('Unrecognized'
+ ' representation') from None
p = base64url_decode(str(c[0]))
if len(p) > 0:
o['protected'] = p.decode('utf-8')
@@ -411,7 +411,7 @@ class JWS(object):
self.objects = o
except Exception as e: # pylint: disable=broad-except
- raise InvalidJWSObject('Invalid format', repr(e))
+ raise InvalidJWSObject('Invalid format') from e
if key:
self.verify(key, alg)
@@ -427,8 +427,7 @@ class JWS(object):
:param protected: The Protected Header (optional)
:param header: The Unprotected Header (optional)
- :raises InvalidJWSObject: if no payload has been set on the object,
- or invalid headers are provided.
+ :raises InvalidJWSObject: if invalid headers are provided.
:raises ValueError: if the key is not a :class:`JWK` object.
:raises ValueError: if the algorithm is missing or is not provided
by one of the headers.
@@ -436,9 +435,6 @@ class JWS(object):
unknown or otherwise not yet implemented.
"""
- if not self.objects.get('payload', None):
- raise InvalidJWSObject('Missing Payload')
-
b64 = True
p = dict()
@@ -481,7 +477,8 @@ class JWS(object):
raise ValueError('"alg" not specified')
c = JWSCore(
- alg, key, protected, self.objects['payload'], self.allowed_algs
+ alg, key, protected, self.objects.get('payload'),
+ self.allowed_algs
)
sig = c.sign()
@@ -531,10 +528,15 @@ class JWS(object):
if not self.objects.get('valid', False):
raise InvalidJWSSignature("No valid signature found")
if 'protected' in self.objects:
+ p = json_decode(self.objects['protected'])
+ if 'alg' not in p:
+ raise InvalidJWSOperation("Compact encoding must carry "
+ "'alg' in protected header")
protected = base64url_encode(self.objects['protected'])
else:
- protected = ''
- if self.objects.get('payload', False):
+ raise InvalidJWSOperation("Can't use compact encoding "
+ "without protected header")
+ if self.objects.get('payload'):
if self.objects.get('b64', True):
payload = base64url_encode(self.objects['payload'])
else:
@@ -553,11 +555,11 @@ class JWS(object):
else:
obj = self.objects
sig = dict()
- if self.objects.get('payload', False):
- if self.objects.get('b64', True):
- sig['payload'] = base64url_encode(self.objects['payload'])
- else:
- sig['payload'] = self.objects['payload']
+ payload = self.objects.get('payload', '')
+ if self.objects.get('b64', True):
+ sig['payload'] = base64url_encode(payload)
+ else:
+ sig['payload'] = payload
if 'signature' in obj:
if not obj.get('valid', False):
raise InvalidJWSSignature("No valid signature found")
@@ -585,11 +587,9 @@ class JWS(object):
@property
def payload(self):
- if 'payload' not in self.objects:
- raise InvalidJWSOperation("Payload not available")
if not self.is_valid:
raise InvalidJWSOperation("Payload not verified")
- return self.objects['payload']
+ return self.objects.get('payload')
def detach_payload(self):
self.objects.pop('payload', None)
=====================================
jwcrypto/jwt.py
=====================================
@@ -3,8 +3,6 @@
import time
import uuid
-from six import string_types
-
from jwcrypto.common import JWException, json_decode, json_encode
from jwcrypto.jwe import JWE
from jwcrypto.jwk import JWK, JWKSet
@@ -199,7 +197,7 @@ class JWT(object):
if check_claims is not None:
self._check_claims = check_claims
- if claims:
+ if claims is not None:
self.claims = claims
if jwt is not None:
@@ -306,17 +304,17 @@ class JWT(object):
def _check_string_claim(self, name, claims):
if name not in claims:
return
- if not isinstance(claims[name], string_types):
+ if not isinstance(claims[name], str):
raise JWTInvalidClaimFormat("Claim %s is not a StringOrURI type")
def _check_array_or_string_claim(self, name, claims):
if name not in claims:
return
if isinstance(claims[name], list):
- if any(not isinstance(claim, string_types) for claim in claims):
+ if any(not isinstance(claim, str) for claim in claims):
raise JWTInvalidClaimFormat(
"Claim %s contains non StringOrURI types" % (name, ))
- elif not isinstance(claims[name], string_types):
+ elif not isinstance(claims[name], str):
raise JWTInvalidClaimFormat(
"Claim %s is not a StringOrURI type" % (name, ))
@@ -325,9 +323,9 @@ class JWT(object):
return
try:
int(claims[name])
- except ValueError:
+ except ValueError as e:
raise JWTInvalidClaimFormat(
- "Claim %s is not an integer" % (name, ))
+ "Claim %s is not an integer" % (name, )) from e
def _check_exp(self, claim, limit, leeway):
if claim < limit - leeway:
@@ -347,6 +345,7 @@ class JWT(object):
self._check_integer_claim('nbf', claims)
self._check_integer_claim('iat', claims)
self._check_string_claim('jti', claims)
+ self._check_string_claim('typ', claims)
if self._check_claims is None:
if 'exp' in claims:
@@ -363,10 +362,11 @@ class JWT(object):
claims = json_decode(self.claims)
if not isinstance(claims, dict):
raise ValueError()
- except ValueError:
+ except ValueError as e:
if self._check_claims is not None:
- raise JWTInvalidClaimFormat(
- "Claims check requested but claims is not a json dict")
+ raise JWTInvalidClaimFormat("Claims check requested "
+ "but claims is not a json "
+ "dict") from e
return
self._check_default_claims(claims)
@@ -407,12 +407,28 @@ class JWT(object):
else:
self._check_nbf(claims[name], time.time(), self._leeway)
+ elif name == 'typ':
+ if value is not None:
+ if self.norm_typ(value) != self.norm_typ(claims[name]):
+ raise JWTInvalidClaimValue("Invalid '%s' value. '%s'"
+ " does not normalize to "
+ "'%s'" % (name,
+ claims[name],
+ value))
+
else:
if value is not None and value != claims[name]:
raise JWTInvalidClaimValue(
"Invalid '%s' value. Expected '%s' got '%s'" % (
name, value, claims[name]))
+ def norm_typ(self, val):
+ lc = val.lower()
+ if '/' in lc:
+ return lc
+ else:
+ return 'application/' + lc
+
def make_signed_token(self, key):
"""Signs the payload.
@@ -440,6 +456,8 @@ class JWT(object):
"""
t = JWE(self.claims, self.header)
+ if self._algs:
+ t.allowed_algs = self._algs
t.add_recipient(key)
self.token = t
@@ -489,7 +507,7 @@ class JWT(object):
self.deserializelog.append("Success")
break
except Exception as e: # pylint: disable=broad-except
- keyid = k.key_id
+ keyid = k.get('kid')
if keyid is None:
keyid = k.thumbprint()
self.deserializelog.append('Key [%s] failed: [%s]' % (
=====================================
jwcrypto/tests-cookbook.py
=====================================
@@ -1110,7 +1110,8 @@ class Cookbook08JWETests(unittest.TestCase):
plaintext = Payload_plaintext_5
protected = base64url_decode(JWE_Protected_Header_5_1_4)
rsa_key = jwk.JWK(**RSA_key_5_1_1)
- e = jwe.JWE(plaintext, protected)
+ e = jwe.JWE(plaintext, protected,
+ algs=jwe.default_allowed_algs + ['RSA1_5'])
e.add_recipient(rsa_key)
enc = e.serialize()
e.deserialize(enc, rsa_key)
=====================================
jwcrypto/tests.py
=====================================
@@ -19,6 +19,9 @@ from jwcrypto.common import JWSEHeaderParameter
from jwcrypto.common import base64url_decode, base64url_encode
from jwcrypto.common import json_decode, json_encode
+jwe_algs_and_rsa1_5 = jwe.default_allowed_algs + ['RSA1_5']
+jws_algs_and_rsa1_5 = jws.default_allowed_algs + ['RSA1_5']
+
# RFC 7517 - A.1
PublicKeys = {"keys": [
{"kty": "EC",
@@ -292,6 +295,20 @@ MCowBQYDK2VwAyEAlsRcb1mVVIUcDjNqZU27N+iPXihH1EQDa/O3utHLtqc=
-----END PUBLIC KEY-----
"""
+ECPublicPEM = b"""-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhvGzt82WMJxqTuXCZxnvwrx4enQj
+6xc+erlhbTq8gTMAJBzNRPbpuj4NOwTCwjohrtY0TAkthwTuixuojpGKmw==
+-----END PUBLIC KEY-----
+"""
+
+ECPublicJWK = {
+ "crv": "P-256",
+ "kid": "MWhDfZyDWdx6Fpk3N00ZMShuKhDRXw1fN4ZSfqzeAWY",
+ "kty": "EC",
+ "x": "hvGzt82WMJxqTuXCZxnvwrx4enQj6xc-erlhbTq8gTM",
+ "y": "ACQczUT26bo-DTsEwsI6Ia7WNEwJLYcE7osbqI6Rips"
+}
+
class TestJWK(unittest.TestCase):
def test_create_pubKeys(self):
@@ -321,7 +338,7 @@ class TestJWK(unittest.TestCase):
jwk.JWK.generate(kty='RSA', size=4096)
jwk.JWK.generate(kty='EC', curve='P-521')
k = jwk.JWK.generate(kty='oct', alg='A192KW', kid='MySymmetricKey')
- self.assertEqual(k.key_id, 'MySymmetricKey')
+ self.assertEqual(k['kid'], 'MySymmetricKey')
self.assertEqual(len(base64url_decode(k.get_op_key('encrypt'))), 24)
jwk.JWK.generate(kty='RSA', alg='RS256')
k = jwk.JWK.generate(kty='RSA', size=4096, alg='RS256')
@@ -332,7 +349,7 @@ class TestJWK(unittest.TestCase):
jk = k.export_public()
self.assertFalse('d' in json_decode(jk))
k2 = jwk.JWK(**json_decode(jk))
- self.assertEqual(k.key_id, k2.key_id)
+ self.assertEqual(k['kid'], k2['kid'])
def test_generate_oct_key(self):
key = jwk.JWK.generate(kty='oct', size=128)
@@ -364,13 +381,13 @@ class TestJWK(unittest.TestCase):
def test_import_pyca_keys(self):
rsa1 = rsa.generate_private_key(65537, 1024, default_backend())
krsa1 = jwk.JWK.from_pyca(rsa1)
- self.assertEqual(krsa1.key_type, 'RSA')
+ self.assertEqual(krsa1['kty'], 'RSA')
krsa2 = jwk.JWK.from_pyca(rsa1.public_key())
self.assertEqual(krsa1.get_op_key('verify').public_numbers().n,
krsa2.get_op_key('verify').public_numbers().n)
ec1 = ec.generate_private_key(ec.SECP256R1(), default_backend())
kec1 = jwk.JWK.from_pyca(ec1)
- self.assertEqual(kec1.key_type, 'EC')
+ self.assertEqual(kec1['kty'], 'EC')
kec2 = jwk.JWK.from_pyca(ec1.public_key())
self.assertEqual(kec1.get_op_key('verify').public_numbers().x,
kec2.get_op_key('verify').public_numbers().x)
@@ -392,10 +409,8 @@ class TestJWK(unittest.TestCase):
self.assertEqual(len(ks), 1)
k1 = ks.get_key(RSAPrivateKey['kid'])
k2 = ks2.get_key(RSAPrivateKey['kid'])
- # pylint: disable=protected-access
- self.assertEqual(k1._key, k2._key)
- # pylint: disable=protected-access
- self.assertEqual(k1._key['d'], RSAPrivateKey['d'])
+ self.assertEqual(k1, k2)
+ self.assertEqual(k1['d'], RSAPrivateKey['d'])
# test class method import too
ks3 = jwk.JWKSet.from_json(ks.export())
self.assertEqual(len(ks), len(ks3))
@@ -409,6 +424,19 @@ class TestJWK(unittest.TestCase):
num += 1
self.assertEqual(num, len(PrivateKeys['keys']))
+ def test_jwkset_issue_208(self):
+ ks = jwk.JWKSet()
+ key1 = RSAPrivateKey.copy()
+ key1['kid'] = 'kid_1'
+ ks.add(jwk.JWK(**key1))
+ key2 = RSAPrivateKey.copy()
+ key2['kid'] = 'kid_2'
+ ks.add(jwk.JWK(**key2))
+ ks2 = jwk.JWKSet()
+ ks2.import_keyset(ks.export())
+ self.assertEqual(len(ks['keys']), 2)
+ self.assertEqual(len(ks['keys']), len(ks2['keys']))
+
def test_thumbprint(self):
for i in range(0, len(PublicKeys['keys'])):
k = jwk.JWK(**PublicKeys['keys'][i])
@@ -427,7 +455,12 @@ class TestJWK(unittest.TestCase):
rsapub.public_numbers().n)
pubc = jwk.JWK.from_pem(PublicCert)
- self.assertEqual(pubc.key_id, PublicCertThumbprint)
+ self.assertEqual(pubc['kid'], PublicCertThumbprint)
+
+ def test_import_ec_from_pem(self):
+ pub_ec = jwk.JWK.from_pem(ECPublicPEM)
+ self.assertEqual(pub_ec.export_to_pem(), ECPublicPEM)
+ self.assertEqual(json_decode(pub_ec.export()), ECPublicJWK)
def test_export_symmetric(self):
key = jwk.JWK(**SymmetricKeys['keys'][0])
@@ -456,7 +489,7 @@ class TestJWK(unittest.TestCase):
pub = key.export_public()
pubkey = jwk.JWK(**json_decode(pub))
self.assertFalse(pubkey.has_private)
- self.assertEqual(prikey.key_id, pubkey.key_id)
+ self.assertEqual(prikey['kid'], pubkey['kid'])
def test_export_as_dict(self):
key = jwk.JWK(**SymmetricKeys['keys'][1])
@@ -535,6 +568,33 @@ class TestJWK(unittest.TestCase):
self.assertTrue(c.objects['valid'])
self.assertEqual(c.payload, payload)
+ def test_jwk_as_dict(self):
+ key = jwk.JWK(**PublicKeys['keys'][0])
+ self.assertEqual(key['kty'], 'EC')
+ self.assertEqual(key.kty, 'EC')
+ self.assertEqual(key.x, key['x'])
+ self.assertEqual(key.kid, '1')
+ key = jwk.JWK(**PublicKeys['keys'][1])
+ self.assertEqual(key['kty'], 'RSA')
+ self.assertEqual(key.n, key['n'])
+ with self.assertRaises(AttributeError):
+ # pylint: disable=pointless-statement
+ key.d
+ with self.assertRaises(AttributeError):
+ key.x = 'xyz'
+ with self.assertRaises(jwk.InvalidJWKValue):
+ key['n'] = '!!!'
+ with self.assertRaises(jwk.InvalidJWKValue):
+ key.e = '3'
+ key.unknown = '1'
+ key['unknown'] = 2
+ self.assertFalse(key.unknown == key['unknown'])
+
+ def test_jwk_from_password(self):
+ key = jwk.JWK.from_password('test password')
+ self.assertEqual(key['kty'], 'oct')
+ self.assertEqual(key['k'], 'dGVzdCBwYXNzd29yZA')
+
# RFC 7515 - A.1
A1_protected = \
@@ -867,15 +927,28 @@ class TestJWS(unittest.TestCase):
key = jwk.JWK(**PrivateKeys_secp256k1['keys'][0])
payload = bytes(bytearray(A1_payload))
jws_test = jws.JWS(payload)
- jws_test.allowed_algs = ['ES256K']
jws_test.add_signature(key, None, json_encode({"alg": "ES256K"}), None)
jws_test_serialization_compact = jws_test.serialize(compact=True)
jws_verify = jws.JWS()
- jws_verify.allowed_algs = ['ES256K']
jws_verify.deserialize(jws_test_serialization_compact)
jws_verify.verify(key.public())
self.assertEqual(jws_verify.payload, payload)
+ def test_jws_issue_224(self):
+ key = jwk.JWK().generate(kty='oct')
+
+ # Test Empty payload is supported for creating and verifying signatures
+ s = jws.JWS(payload='')
+ s.add_signature(key, None, json_encode({"alg": "HS256"}))
+ o1 = s.serialize(compact=True)
+ self.assertTrue('..' in o1)
+ o2 = json_decode(s.serialize())
+ self.assertEqual(o2['payload'], '')
+
+ t = jws.JWS()
+ t.deserialize(o1)
+ t.verify(key)
+
E_A1_plaintext = \
[84, 104, 101, 32, 116, 114, 117, 101, 32, 115, 105, 103, 110, 32,
@@ -1089,7 +1162,7 @@ X25519_Protected_Header_no_epk = {
class TestJWE(unittest.TestCase):
def check_enc(self, plaintext, protected, key, vector):
- e = jwe.JWE(plaintext, protected)
+ e = jwe.JWE(plaintext, protected, algs=jwe_algs_and_rsa1_5)
e.add_recipient(key)
# Encrypt and serialize using compact
enc = e.serialize()
@@ -1111,7 +1184,8 @@ class TestJWE(unittest.TestCase):
E_A3_ex['key'], E_A3_ex['vector'])
def test_A4(self):
- e = jwe.JWE(E_A4_ex['plaintext'], E_A4_ex['protected'])
+ e = jwe.JWE(E_A4_ex['plaintext'], E_A4_ex['protected'],
+ algs=jwe_algs_and_rsa1_5)
e.add_recipient(E_A4_ex['key1'], E_A4_ex['header1'])
e.add_recipient(E_A4_ex['key2'], E_A4_ex['header2'])
enc = e.serialize()
@@ -1122,7 +1196,7 @@ class TestJWE(unittest.TestCase):
e.deserialize(E_A4_ex['vector'], E_A4_ex['key2'])
def test_A5(self):
- e = jwe.JWE()
+ e = jwe.JWE(algs=jwe_algs_and_rsa1_5)
e.deserialize(E_A5_ex, E_A4_ex['key2'])
with self.assertRaises(jwe.InvalidJWEData):
e = jwe.JWE(algs=['A256KW'])
@@ -1246,10 +1320,10 @@ class TestMMA(unittest.TestCase):
print('Testing MMA timing attacks')
ok_cek = 0
- ok_e = jwe.JWE()
+ ok_e = jwe.JWE(algs=jwe_algs_and_rsa1_5)
ok_e.deserialize(MMA_vector_ok_cek)
ko_cek = 0
- ko_e = jwe.JWE()
+ ko_e = jwe.JWE(algs=jwe_algs_and_rsa1_5)
ko_e.deserialize(MMA_vector_ko_cek)
import time
@@ -1331,26 +1405,31 @@ class TestJWT(unittest.TestCase):
def test_A1(self):
key = jwk.JWK(**E_A2_key)
# first encode/decode ourselves
- t = jwt.JWT(A1_header, A1_claims)
+ t = jwt.JWT(A1_header, A1_claims,
+ algs=jwe_algs_and_rsa1_5)
t.make_encrypted_token(key)
token = t.serialize()
t.deserialize(token)
# then try the test vector
- t = jwt.JWT(jwt=A1_token, key=key, check_claims=False)
+ t = jwt.JWT(jwt=A1_token, key=key, check_claims=False,
+ algs=jwe_algs_and_rsa1_5)
# then try the test vector with explicit expiration date
- t = jwt.JWT(jwt=A1_token, key=key, check_claims={'exp': 1300819380})
+ t = jwt.JWT(jwt=A1_token, key=key, check_claims={'exp': 1300819380},
+ algs=jwe_algs_and_rsa1_5)
# Finally check it raises for expired tokens
- self.assertRaises(jwt.JWTExpired, jwt.JWT, jwt=A1_token, key=key)
+ self.assertRaises(jwt.JWTExpired, jwt.JWT, jwt=A1_token, key=key,
+ algs=jwe_algs_and_rsa1_5)
def test_A2(self):
sigkey = jwk.JWK(**A2_example['key'])
- touter = jwt.JWT(jwt=A2_token, key=E_A2_ex['key'])
+ touter = jwt.JWT(jwt=A2_token, key=E_A2_ex['key'],
+ algs=jwe_algs_and_rsa1_5)
tinner = jwt.JWT(jwt=touter.claims, key=sigkey, check_claims=False)
self.assertEqual(A1_claims, json_decode(tinner.claims))
with self.assertRaises(jwe.InvalidJWEData):
jwt.JWT(jwt=A2_token, key=E_A2_ex['key'],
- algs=['RSA_1_5', 'AES256GCM'])
+ algs=jws_algs_and_rsa1_5)
def test_decrypt_keyset(self):
key = jwk.JWK(kid='testkey', **E_A2_key)
@@ -1359,19 +1438,20 @@ class TestJWT(unittest.TestCase):
# encrypt a new JWT with kid
header = copy.copy(A1_header)
header['kid'] = 'testkey'
- t = jwt.JWT(header, A1_claims)
+ t = jwt.JWT(header, A1_claims, algs=jwe_algs_and_rsa1_5)
t.make_encrypted_token(key)
token = t.serialize()
# try to decrypt without a matching key
self.assertRaises(jwt.JWTMissingKey, jwt.JWT, jwt=token, key=keyset)
# now decrypt with key
keyset.add(key)
- jwt.JWT(jwt=token, key=keyset, check_claims={'exp': 1300819380})
+ jwt.JWT(jwt=token, key=keyset, algs=jwe_algs_and_rsa1_5,
+ check_claims={'exp': 1300819380})
# encrypt a new JWT with wrong kid
header = copy.copy(A1_header)
header['kid'] = '1'
- t = jwt.JWT(header, A1_claims)
+ t = jwt.JWT(header, A1_claims, algs=jwe_algs_and_rsa1_5)
t.make_encrypted_token(key)
token = t.serialize()
self.assertRaises(jwe.InvalidJWEData, jwt.JWT, jwt=token, key=keyset)
@@ -1379,33 +1459,37 @@ class TestJWT(unittest.TestCase):
keyset = jwk.JWKSet.from_json(json_encode(PrivateKeys))
# encrypt a new JWT with no kid
header = copy.copy(A1_header)
- t = jwt.JWT(header, A1_claims)
+ t = jwt.JWT(header, A1_claims, algs=jwe_algs_and_rsa1_5)
t.make_encrypted_token(key)
token = t.serialize()
# try to decrypt without a matching key
self.assertRaises(jwt.JWTMissingKey, jwt.JWT, jwt=token, key=keyset)
# now decrypt with key
keyset.add(key)
- jwt.JWT(jwt=token, key=keyset, check_claims={'exp': 1300819380})
+ jwt.JWT(jwt=token, key=keyset, algs=jwe_algs_and_rsa1_5,
+ check_claims={'exp': 1300819380})
def test_invalid_claim_type(self):
key = jwk.JWK(**E_A2_key)
claims = {"testclaim": "test"}
claims.update(A1_claims)
- t = jwt.JWT(A1_header, claims)
+ t = jwt.JWT(A1_header, claims, algs=jwe_algs_and_rsa1_5)
t.make_encrypted_token(key)
token = t.serialize()
# Wrong string
self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
- key=key, check_claims={"testclaim": "ijgi"})
+ key=key, algs=jwe_algs_and_rsa1_5,
+ check_claims={"testclaim": "ijgi"})
# Wrong type
self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
- key=key, check_claims={"testclaim": 123})
+ key=key, algs=jwe_algs_and_rsa1_5,
+ check_claims={"testclaim": 123})
# Correct
- jwt.JWT(jwt=token, key=key, check_claims={"testclaim": "test"})
+ jwt.JWT(jwt=token, key=key, algs=jwe_algs_and_rsa1_5,
+ check_claims={"testclaim": "test"})
def test_claim_params(self):
key = jwk.JWK(**E_A2_key)
@@ -1413,21 +1497,96 @@ class TestJWT(unittest.TestCase):
string_claims = '{"string_claim":"test"}'
string_header = '{"alg":"RSA1_5","enc":"A128CBC-HS256"}'
t = jwt.JWT(string_header, string_claims,
- default_claims=default_claims)
+ default_claims=default_claims,
+ algs=jwe_algs_and_rsa1_5)
t.make_encrypted_token(key)
token = t.serialize()
# Check default_claims
- jwt.JWT(jwt=token, key=key, check_claims={"iss": "test", "exp": None,
- "string_claim": "test"})
+ jwt.JWT(jwt=token, key=key, algs=jwe_algs_and_rsa1_5,
+ check_claims={"iss": "test", "exp": None,
+ "string_claim": "test"})
+
+ def test_claims_typ(self):
+ key = jwk.JWK().generate(kty='oct')
+ claims = '{"typ":"application/test"}'
+ string_header = '{"alg":"HS256"}'
+ t = jwt.JWT(string_header, claims)
+ t.make_signed_token(key)
+ token = t.serialize()
+
+ # Same typ w/o application prefix
+ jwt.JWT(jwt=token, key=key, check_claims={"typ": "test"})
+ self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+ key=key, check_claims={"typ": "wrong"})
+
+ # Same typ w/ application prefix
+ jwt.JWT(jwt=token, key=key, check_claims={"typ": "application/test"})
+ self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+ key=key, check_claims={"typ": "application/wrong"})
+
+ # check that a '/' in the name makes it not be matched with
+ # 'application/' prefix
+ claims = '{"typ":"diffmime/test"}'
+ t = jwt.JWT(string_header, claims)
+ t.make_signed_token(key)
+ token = t.serialize()
+ self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+ key=key, check_claims={"typ": "application/test"})
+ self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+ key=key, check_claims={"typ": "test"})
+
+ # finally make sure it doesn't raise if not checked.
+ jwt.JWT(jwt=token, key=key)
+
+ def test_empty_claims(self):
+ key = jwk.JWK().generate(kty='oct')
+
+ # empty dict is valid
+ t = jwt.JWT('{"alg":"HS256"}', {})
+ self.assertEqual('{}', t.claims)
+ t.make_signed_token(key)
+ token = t.serialize()
+
+ c = jwt.JWT()
+ c.deserialize(token, key)
+ self.assertEqual('{}', c.claims)
+
+ # empty string is also valid
+ t = jwt.JWT('{"alg":"HS256"}', '')
+ t.make_signed_token(key)
+ token = t.serialize()
+
+ # also a space is fine
+ t = jwt.JWT('{"alg":"HS256"}', ' ')
+ self.assertEqual(' ', t.claims)
+ t.make_signed_token(key)
+ token = t.serialize()
+
+ c = jwt.JWT()
+ c.deserialize(token, key)
+ self.assertEqual(' ', c.claims)
+
+ def test_Issue_209(self):
+ key = jwk.JWK(**A3_key)
+ t = jwt.JWT('{"alg":"ES256"}', {})
+ t.make_signed_token(key)
+ token = t.serialize()
+
+ ks = jwk.JWKSet()
+ ks.add(jwk.JWK().generate(kty='oct'))
+ ks.add(key)
+
+ # Make sure this one does not assert when cycling through
+ # the oct key before hitting the ES one
+ jwt.JWT(jwt=token, key=ks)
class ConformanceTests(unittest.TestCase):
def test_unknown_key_params(self):
key = jwk.JWK(kty='oct', k='secret', unknown='mystery')
- # pylint: disable=protected-access
- self.assertEqual('mystery', key._unknown['unknown'])
+ self.assertEqual('mystery', key.get('unknown'))
def test_key_ops_values(self):
self.assertRaises(jwk.InvalidJWKValue, jwk.JWK,
@@ -1526,6 +1685,24 @@ class ConformanceTests(unittest.TestCase):
token.deserialize(jwt=e)
json_decode(token.claims)
+ def test_no_default_rsa_1_5(self):
+ s = jws.JWS('test')
+ with self.assertRaisesRegex(jws.InvalidJWSOperation,
+ 'Algorithm not allowed'):
+ s.add_signature(A2_key, alg="RSA1_5")
+
+ def test_pbes2_hs256_aeskw(self):
+ enc = jwe.JWE(plaintext='plain',
+ protected={"alg": "PBES2-HS256+A128KW",
+ "enc": "A256CBC-HS512"})
+ key = jwk.JWK.from_password('password')
+ enc.add_recipient(key)
+ o = enc.serialize()
+ check = jwe.JWE()
+ check.deserialize(o)
+ check.decrypt(key)
+ self.assertEqual(check.payload, b'plain')
+
class JWATests(unittest.TestCase):
def test_jwa_create(self):
=====================================
setup.py
=====================================
@@ -6,7 +6,7 @@ from setuptools import setup
setup(
name = 'jwcrypto',
- version = '0.8',
+ version = '1.0',
license = 'LGPLv3+',
maintainer = 'JWCrypto Project Contributors',
maintainer_email = 'simo at redhat.com',
@@ -14,12 +14,10 @@ setup(
packages = ['jwcrypto'],
description = 'Implementation of JOSE Web standards',
classifiers = [
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
'Intended Audience :: Developers',
'Topic :: Security',
'Topic :: Software Development :: Libraries :: Python Modules'
@@ -27,5 +25,6 @@ setup(
data_files = [('share/doc/jwcrypto', ['LICENSE', 'README.md'])],
install_requires = [
'cryptography >= 2.3',
+ 'deprecated',
],
)
=====================================
tox.ini
=====================================
@@ -1,5 +1,5 @@
[tox]
-envlist = lint,py27,py34,py35,py36,py37,pep8py2,pep8py3,doc,sphinx
+envlist = lint,py36,py37,py38,py39,pep8,doc,sphinx
skip_missing_interpreters = true
[testenv]
@@ -8,43 +8,35 @@ setenv =
deps =
pytest
coverage
+ pip >= 19.1.1
#sitepackages = True
commands =
- {envpython} -bb -m coverage run -m pytest --capture=no --strict {posargs}
+ {envpython} -bb -m coverage run -m pytest --capture=no --strict-markers {posargs}
{envpython} -m coverage report -m
[testenv:lint]
-basepython = python2.7
+basepython = python3.9
deps =
pylint
#sitepackages = True
commands =
{envpython} -m pylint -d c,r,i,W0613 -r n -f colorized --notes= --disable=star-args ./jwcrypto
-[testenv:pep8py2]
-basepython = python2.7
-deps =
- flake8
- flake8-import-order
- pep8-naming
-commands =
- {envpython} -m flake8 {posargs} jwcrypto
-
-[testenv:pep8py3]
+[testenv:pep8]
basepython = python3
deps =
flake8
flake8-import-order
pep8-naming
commands =
- {envpython} -m flake8 {posargs} jwcrypto
+ {envpython} -m flake8 {posargs} --ignore=N802,N818 jwcrypto
[testenv:doc]
deps =
doc8
docutils
markdown
-basepython = python2.7
+basepython = python3
commands =
doc8 --allow-long-titles README.md
markdown_py README.md -f {toxworkdir}/README.md.html
View it on GitLab: https://salsa.debian.org/freeipa-team/python-jwcrypto/-/compare/4e08b6610fed7f97f8004f16aaa939b4d6662384...aa835cb132c39fdfa275eb1be07bb4c3c39daa4c
--
View it on GitLab: https://salsa.debian.org/freeipa-team/python-jwcrypto/-/compare/4e08b6610fed7f97f8004f16aaa939b4d6662384...aa835cb132c39fdfa275eb1be07bb4c3c39daa4c
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-freeipa-devel/attachments/20210816/2630adf4/attachment-0001.htm>
More information about the Pkg-freeipa-devel
mailing list