summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTimo Aaltonen <tjaalton@debian.org>2017-12-23 08:00:03 (GMT)
committerTimo Aaltonen <tjaalton@debian.org>2017-12-23 08:00:03 (GMT)
commita004b940311eef4d324179a3041bd8593716ecb7 (patch)
tree5aeaf168b1261a4937157ae980fcf6427c68b692
downloadpython-jwcrypto-a004b940311eef4d324179a3041bd8593716ecb7.zip
python-jwcrypto-a004b940311eef4d324179a3041bd8593716ecb7.tar.gz
python-jwcrypto-a004b940311eef4d324179a3041bd8593716ecb7.tar.bz2
Import python-jwcrypto_0.4.2.orig.tar.gz
[dgit import orig python-jwcrypto_0.4.2.orig.tar.gz]
-rw-r--r--.coveragerc23
-rw-r--r--.gitignore8
-rw-r--r--.travis.yml35
-rw-r--r--LICENSE165
-rw-r--r--MANIFEST.in2
-rw-r--r--Makefile36
-rw-r--r--README.md16
-rw-r--r--docs/Makefile177
-rw-r--r--docs/source/.static/.gitignore1
-rw-r--r--docs/source/conf.py263
-rw-r--r--docs/source/index.rst36
-rw-r--r--docs/source/jwe.rst69
-rw-r--r--docs/source/jwk.rst90
-rw-r--r--docs/source/jws.rst65
-rw-r--r--docs/source/jwt.rst48
-rw-r--r--jwcrypto/__init__.py0
-rw-r--r--jwcrypto/common.py106
-rw-r--r--jwcrypto/jwa.py1104
-rw-r--r--jwcrypto/jwe.py485
-rw-r--r--jwcrypto/jwk.py803
-rw-r--r--jwcrypto/jws.py505
-rw-r--r--jwcrypto/jwt.py496
-rw-r--r--jwcrypto/tests-cookbook.py1293
-rw-r--r--jwcrypto/tests.py1150
-rw-r--r--setup.cfg6
-rwxr-xr-xsetup.py29
-rw-r--r--tox.ini68
27 files changed, 7079 insertions, 0 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..c80ab62
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,23 @@
+[run]
+branch = True
+source =
+ jwcrypto
+ tests
+
+[paths]
+source =
+ jwcrypto
+ .tox/*/lib/python*/site-packages/jwcrypto
+
+[report]
+ignore_errors = False
+precision = 1
+exclude_lines =
+ pragma: no cover
+ raise AssertionError
+ raise NotImplementedError
+ if 0:
+ if False:
+ if __name__ == .__main__.:
+ if PY3
+ if not PY3
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..669c805
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+build/
+dist/
+*.pyc
+*.pyo
+cscope.out
+.tox
+.coverage
+*.egg-info
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..1eccbef
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,35 @@
+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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..65c5ca8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..46e1f5d
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include LICENSE README.md
+include tox.ini setup.cfg
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c52c43a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+all: lint pep8 docs test
+ echo "All tests passed"
+
+lint:
+ # Pylint checks
+ tox -e lint
+
+pep8:
+ # Check style consistency
+ tox -e pep8py2
+ tox -e pep8py3
+
+clean:
+ rm -fr build dist *.egg-info
+ find ./ -name '*.pyc' -exec rm -f {} \;
+
+cscope:
+ git ls-files | xargs pycscope
+
+testlong: export JWCRYPTO_TESTS_ENABLE_MMA=True
+testlong: export TOX_TESTENV_PASSENV=JWCRYPTO_TESTS_ENABLE_MMA
+testlong:
+ rm -f .coverage
+ tox -e py35
+
+test:
+ rm -f .coverage
+ tox -e py27
+ tox -e py34 --skip-missing-interpreter
+ tox -e py35 --skip-missing-interpreter
+
+DOCS_DIR = docs
+.PHONY: docs
+
+docs:
+ $(MAKE) -C $(DOCS_DIR) html
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..18500ba
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
+JWCrypto
+========
+
+An implementation of the JOSE Working Group documents:
+RFC 7515 - JSON Web Signature (JWS)
+RFC 7516 - JSON Web Encryption (JWE)
+RFC 7517 - JSON Web Key (JWK)
+RFC 7518 - JSON Web Algorithms (JWA)
+RFC 7519 - JSON Web Token (JWT)
+RFC 7520 - Examples of Protecting Content Using JSON Object Signing and
+Encryption (JOSE)
+
+Documentation
+=============
+
+http://jwcrypto.readthedocs.org
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..9d4d24b
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/JWCrypto.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/JWCrypto.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/JWCrypto"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/JWCrypto"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/source/.static/.gitignore b/docs/source/.static/.gitignore
new file mode 100644
index 0000000..1bb8bf6
--- /dev/null
+++ b/docs/source/.static/.gitignore
@@ -0,0 +1 @@
+# empty
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..c06c1b6
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,263 @@
+# -*- coding: utf-8 -*-
+#
+# JWCrypto documentation build configuration file, created by
+# sphinx-quickstart on Tue Apr 14 10:25:10 2015.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+sys.path.insert(0, os.path.abspath('../..'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['.templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'JWCrypto'
+copyright = u'2016-2017, 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.4'
+# The full version, including alpha/beta/rc tags.
+release = '0.4.2'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['.static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'JWCryptodoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'JWCrypto.tex', u'JWCrypto Documentation',
+ u'JWCrypto Contributors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'jwcrypto', u'JWCrypto Documentation',
+ [u'JWCrypto Contributors'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'JWCrypto', u'JWCrypto Documentation',
+ u'JWCrypto Contributors', 'JWCrypto', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+autoclass_content = 'both'
+autodoc_member_order = 'groupwise'
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000..e4adc21
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,36 @@
+.. JWCrypto documentation master file, created by
+ sphinx-quickstart on Tue Apr 14 10:25:10 2015.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to JWCrypto's documentation!
+====================================
+
+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.
+
+.. _JOSE: https://datatracker.ietf.org/wg/jose/charter/
+.. _Cryptography: https://cryptography.io/
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ jwk
+ jws
+ jwe
+ jwt
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/docs/source/jwe.rst b/docs/source/jwe.rst
new file mode 100644
index 0000000..855f0cb
--- /dev/null
+++ b/docs/source/jwe.rst
@@ -0,0 +1,69 @@
+JSON Web Encryption (JWE)
+=========================
+
+The jwe Module implements the `JSON Web Encryption`_ standard.
+A JSON Web Encryption is represented by a JWE object, related utility
+classes and functions are availbale in this module too.
+
+.. _JSON Web Encryption: https://tools.ietf.org/html/rfc7516
+
+Classes
+-------
+
+.. autoclass:: jwcrypto.jwe.JWE
+ :members:
+ :show-inheritance:
+
+Variables
+---------
+
+.. autodata:: jwcrypto.jwe.default_allowed_algs
+
+Exceptions
+----------
+
+.. autoclass:: jwcrypto.jwe.InvalidJWEOperation
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwe.InvalidJWEData
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwe.InvalidJWEKeyType
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwe.InvalidJWEKeyLength
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwe.InvalidCEKeyLength
+ :members:
+ :show-inheritance:
+
+Registries
+----------
+
+.. autodata:: jwcrypto.jwe.JWEHeaderRegistry
+ :annotation:
+
+Examples
+--------
+
+Encrypt a JWE token::
+ >>> from jwcrypto import jwk, jwe
+ >>> from jwcrypto.common import json_encode
+ >>> key = jwk.JWK.generate(kty='oct', size=256)
+ >>> payload = "My Encrypted message"
+ >>> jwetoken = jwe.JWE(payload.encode('utf-8'),
+ json_encode({"alg": "A256KW",
+ "enc": "A256CBC-HS512"}))
+ >>> jwetoken.add_recipient(key)
+ >>> enc = jwetoken.serialize()
+
+Decrypt a JWE token::
+ >>> jwetoken = jwe.JWE()
+ >>> jwetoken.deserialize(enc)
+ >>> jwetoken.decrypt(key)
+ >>> payload = jwetoken.payload
diff --git a/docs/source/jwk.rst b/docs/source/jwk.rst
new file mode 100644
index 0000000..c019333
--- /dev/null
+++ b/docs/source/jwk.rst
@@ -0,0 +1,90 @@
+JSON Web Key (JWK)
+==================
+
+The jwk Module implements the `JSON Web Key`_ standard.
+A JSON Web Key is represented by a JWK object, related utility classes and
+functions are availbale in this module too.
+
+.. _JSON Web Key: http://tools.ietf.org/html/rfc7517
+
+Classes
+-------
+
+.. autoclass:: jwcrypto.jwk.JWK
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwk.JWKSet
+ :members:
+ :show-inheritance:
+
+Exceptions
+----------
+
+.. autoclass:: jwcrypto.jwk.InvalidJWKType
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwk.InvalidJWKValue
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwk.InvalidJWKOperation
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jwk.InvalidJWKUsage
+ :members:
+ :show-inheritance:
+
+Registries
+----------
+
+.. autodata:: jwcrypto.jwk.JWKTypesRegistry
+ :annotation:
+
+.. autodata:: jwcrypto.jwk.JWKValuesRegistry
+ :annotation:
+
+.. autodata:: jwcrypto.jwk.JWKParamsRegistry
+ :annotation:
+
+.. autodata:: jwcrypto.jwk.JWKEllipticCurveRegistry
+ :annotation:
+
+.. autodata:: jwcrypto.jwk.JWKUseRegistry
+ :annotation:
+
+.. autodata:: jwcrypto.jwk.JWKOperationsRegistry
+ :annotation:
+
+Examples
+--------
+
+Create a 256bit symmetric key::
+ >>> from jwcrypto import jwk
+ >>> key = jwk.JWK.generate(kty='oct', size=256)
+
+Export the key with::
+ >>> key.export()
+ '{"k":"X6TBlwY2so8EwKZ2TFXM7XHSgWBKQJhcspzYydp5Y-o","kty":"oct"}'
+
+Create a 2048bit RSA keypair::
+ >>> jwk.JWK.generate(kty='RSA', size=2048)
+
+Create a P-256 EC keypair and export the public key::
+ >>> key = jwk.JWK.generate(kty='EC', crv='P-256')
+ >>> key.export(private_key=False)
+ '{"y":"VYlYwBfOTIICojCPfdUjnmkpN-g-lzZKxzjAoFmDRm8",
+ "x":"3mdE0rODWRju6qqU01Kw5oPYdNxBOMisFvJFH1vEu9Q",
+ "crv":"P-256","kty":"EC"}'
+
+Import a P-256 Public Key::
+ >>> expkey = {"y":"VYlYwBfOTIICojCPfdUjnmkpN-g-lzZKxzjAoFmDRm8",
+ "x":"3mdE0rODWRju6qqU01Kw5oPYdNxBOMisFvJFH1vEu9Q",
+ "crv":"P-256","kty":"EC"}
+ >>> key = jwk.JWK(**expkey)
+
+Import a Key from a PEM file::
+ >>> with open("public.pem", "rb") as pemfile:
+ >>> key = jwk.JWK.from_pem(pemfile.read())
diff --git a/docs/source/jws.rst b/docs/source/jws.rst
new file mode 100644
index 0000000..d7d0ef6
--- /dev/null
+++ b/docs/source/jws.rst
@@ -0,0 +1,65 @@
+JSON Web Signature (JWS)
+========================
+
+The jws Module implements the `JSON Web Signature`_ standard.
+A JSON Web Signature is represented by a JWS object, related utility
+classes and functions are available in this module too.
+
+.. _JSON Web Signature: http://tools.ietf.org/html/rfc7515
+
+Classes
+-------
+
+.. autoclass:: jwcrypto.jws.JWS
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jws.JWSCore
+ :members:
+ :show-inheritance:
+
+Variables
+---------
+
+.. autodata:: jwcrypto.jws.default_allowed_algs
+
+Exceptions
+----------
+
+.. autoclass:: jwcrypto.jws.InvalidJWSSignature
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jws.InvalidJWSObject
+ :members:
+ :show-inheritance:
+
+.. autoclass:: jwcrypto.jws.InvalidJWSOperation
+ :members:
+ :show-inheritance:
+
+Registries
+----------
+
+.. autodata:: jwcrypto.jws.JWSHeaderRegistry
+ :annotation:
+
+Examples
+--------
+
+Sign a JWS token::
+ >>> from jwcrypto import jwk, jws
+ >>> from jwcrypto.common import json_encode
+ >>> key = jwk.JWK.generate(kty='oct', size=256)
+ >>> payload = "My Integrity protected message"
+ >>> jwstoken = jws.JWS(payload.encode('utf-8'))
+ >>> jwstoken.add_signature(key, None,
+ json_encode({"alg": "HS256"}),
+ json_encode({"kid": key.thumbprint()}))
+ >>> sig = jwstoken.serialize()
+
+Verify a JWS token::
+ >>> jwstoken = jws.JWS()
+ >>> jwstoken.deserialize(sig)
+ >>> jwstoken.verify(key)
+ >>> payload = jwstoken.payload
diff --git a/docs/source/jwt.rst b/docs/source/jwt.rst
new file mode 100644
index 0000000..1d02e42
--- /dev/null
+++ b/docs/source/jwt.rst
@@ -0,0 +1,48 @@
+JSON Web Token (JWT)
+====================
+
+The jwt Module implements the `JSON Web Token`_ standard.
+A JSON Web Token is represented by a JWT object, related utility classes and
+functions are availbale in this module too.
+
+.. _JSON Web Token: http://tools.ietf.org/html/rfc7519
+
+Classes
+-------
+
+.. autoclass:: jwcrypto.jwt.JWT
+ :members:
+ :show-inheritance:
+
+Examples
+--------
+
+Create a symmetric key::
+ >>> from jwcrypto import jwt, jwk
+ >>> key = jwk.JWK(generate='oct', size=256)
+ >>> key.export()
+ '{"k":"Wal4ZHCBsml0Al_Y8faoNTKsXCkw8eefKXYFuwTBOpA","kty":"oct"}'
+
+Create a signed token with the generated key::
+ >>> Token = jwt.JWT(header={"alg": "HS256"},
+ claims={"info": "I'm a signed token"})
+ >>> Token.make_signed_token(key)
+ >>> Token.serialize()
+ u'eyJhbGciOiJIUzI1NiJ9.eyJpbmZvIjoiSSdtIGEgc2lnbmVkIHRva2VuIn0.rjnRMAKcaRamEHnENhg0_Fqv7Obo-30U4bcI_v-nfEM'
+
+Further encrypt the token with the same key::
+ >>> Etoken = jwt.JWT(header={"alg": "A256KW", "enc": "A256CBC-HS512"},
+ claims=Token.serialize())
+ >>> Etoken.make_encrypted_token(key)
+ >>> Etoken.serialize()
+ u'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0.ST5RmjqDLj696xo7YFTFuKUhcd3naCrm6yMjBM3cqWiFD6U8j2JIsbclsF7ryNg8Ktmt1kQJRKavV6DaTl1T840tP3sIs1qz.wSxVhZH5GyzbJnPBAUMdzQ.6uiVYwrRBzAm7Uge9rEUjExPWGbgerF177A7tMuQurJAqBhgk3_5vee5DRH84kHSapFOxcEuDdMBEQLI7V2E0F57-d01TFStHzwtgtSmeZRQ6JSIL5XlgJouwHfSxn9Z_TGl5xxq4TksORHED1vnRA.5jPyPWanJVqlOohApEbHmxi3JHp1MXbmvQe2_dVd8FI'
+
+Now decrypt and verify::
+ >>> from jwcrypto import jwt, jwk
+ >>> k = {"k": "Wal4ZHCBsml0Al_Y8faoNTKsXCkw8eefKXYFuwTBOpA", "kty": "oct"}
+ >>> key = jwk.JWK(**k)
+ >>> e = u'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0.ST5RmjqDLj696xo7YFTFuKUhcd3naCrm6yMjBM3cqWiFD6U8j2JIsbclsF7ryNg8Ktmt1kQJRKavV6DaTl1T840tP3sIs1qz.wSxVhZH5GyzbJnPBAUMdzQ.6uiVYwrRBzAm7Uge9rEUjExPWGbgerF177A7tMuQurJAqBhgk3_5vee5DRH84kHSapFOxcEuDdMBEQLI7V2E0F57-d01TFStHzwtgtSmeZRQ6JSIL5XlgJouwHfSxn9Z_TGl5xxq4TksORHED1vnRA.5jPyPWanJVqlOohApEbHmxi3JHp1MXbmvQe2_dVd8FI'
+ >>> ET = jwt.JWT(key=key, jwt=e)
+ >>> ST = jwt.JWT(key=key, jwt=ET.claims)
+ >>> ST.claims
+ u'{"info":"I\'m a signed token"}'
diff --git a/jwcrypto/__init__.py b/jwcrypto/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/jwcrypto/__init__.py
diff --git a/jwcrypto/common.py b/jwcrypto/common.py
new file mode 100644
index 0000000..1bfa3eb
--- /dev/null
+++ b/jwcrypto/common.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
+
+import json
+from base64 import urlsafe_b64decode, urlsafe_b64encode
+
+
+# Padding stripping versions as described in
+# RFC 7515 Appendix C
+
+
+def base64url_encode(payload):
+ if not isinstance(payload, bytes):
+ payload = payload.encode('utf-8')
+ encode = urlsafe_b64encode(payload)
+ return encode.decode('utf-8').rstrip('=')
+
+
+def base64url_decode(payload):
+ l = len(payload) % 4
+ if l == 2:
+ payload += '=='
+ elif l == 3:
+ payload += '='
+ elif l != 0:
+ raise ValueError('Invalid base64 string')
+ return urlsafe_b64decode(payload.encode('utf-8'))
+
+
+# JSON encoding/decoding helpers with good defaults
+
+def json_encode(string):
+ if isinstance(string, bytes):
+ string = string.decode('utf-8')
+ return json.dumps(string, separators=(',', ':'), sort_keys=True)
+
+
+def json_decode(string):
+ if isinstance(string, bytes):
+ string = string.decode('utf-8')
+ return json.loads(string)
+
+
+class JWException(Exception):
+ pass
+
+
+class InvalidJWAAlgorithm(JWException):
+ def __init__(self, message=None):
+ msg = 'Invalid JWA Algorithm name'
+ if message:
+ msg += ' (%s)' % message
+ super(InvalidJWAAlgorithm, self).__init__(msg)
+
+
+class InvalidCEKeyLength(JWException):
+ """Invalid CEK Key Length.
+
+ This exception is raised when a Content Encryption Key does not match
+ the required lenght.
+ """
+
+ def __init__(self, expected, obtained):
+ msg = 'Expected key of length %d bits, got %d' % (expected, obtained)
+ super(InvalidCEKeyLength, self).__init__(msg)
+
+
+class InvalidJWEOperation(JWException):
+ """Invalid JWS Object.
+
+ This exception is raised when a requested operation cannot
+ be execute due to unsatisfied conditions.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = message
+ else:
+ msg = 'Unknown Operation Failure'
+ if exception:
+ msg += ' {%s}' % repr(exception)
+ super(InvalidJWEOperation, self).__init__(msg)
+
+
+class InvalidJWEKeyType(JWException):
+ """Invalid JWE Key Type.
+
+ This exception is raised when the provided JWK Key does not match
+ the type required by the sepcified algorithm.
+ """
+
+ def __init__(self, expected, obtained):
+ msg = 'Expected key type %s, got %s' % (expected, obtained)
+ super(InvalidJWEKeyType, self).__init__(msg)
+
+
+class InvalidJWEKeyLength(JWException):
+ """Invalid JWE Key Length.
+
+ This exception is raised when the provided JWK Key does not match
+ the lenght required by the sepcified algorithm.
+ """
+
+ def __init__(self, expected, obtained):
+ msg = 'Expected key of lenght %d, got %d' % (expected, obtained)
+ super(InvalidJWEKeyLength, self).__init__(msg)
diff --git a/jwcrypto/jwa.py b/jwcrypto/jwa.py
new file mode 100644
index 0000000..45064ee
--- /dev/null
+++ b/jwcrypto/jwa.py
@@ -0,0 +1,1104 @@
+# Copyright (C) 2016 JWCrypto Project Contributors - see LICENSE file
+
+import abc
+import os
+import struct
+from binascii import hexlify, unhexlify
+
+from cryptography.exceptions import InvalidSignature
+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.asymmetric import utils as ec_utils
+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
+
+import six
+
+from jwcrypto.common import InvalidCEKeyLength
+from jwcrypto.common import InvalidJWAAlgorithm
+from jwcrypto.common import InvalidJWEKeyLength
+from jwcrypto.common import InvalidJWEKeyType
+from jwcrypto.common import InvalidJWEOperation
+from jwcrypto.common import base64url_decode, base64url_encode
+from jwcrypto.common import json_decode
+from jwcrypto.jwk import JWK
+
+# Implements RFC 7518 - JSON Web Algorithms (JWA)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class JWAAlgorithm(object):
+
+ @abc.abstractproperty
+ def name(self):
+ """The algorithm Name"""
+ pass
+
+ @abc.abstractproperty
+ def description(self):
+ """A short description"""
+ pass
+
+ @abc.abstractproperty
+ def keysize(self):
+ """The actual/recommended/minimum key size"""
+ pass
+
+ @abc.abstractproperty
+ def algorithm_usage_location(self):
+ """One of 'alg', 'enc' or 'JWK'"""
+ pass
+
+ @abc.abstractproperty
+ def algorithm_use(self):
+ """One of 'sig', 'kex', 'enc'"""
+ pass
+
+
+def _bitsize(x):
+ return len(x) * 8
+
+
+def _inbytes(x):
+ return x // 8
+
+
+def _randombits(x):
+ if x % 8 != 0:
+ raise ValueError("lenght must be a multiple of 8")
+ return os.urandom(_inbytes(x))
+
+
+# Note: the number of bits should be a multiple of 16
+def _encode_int(n, bits):
+ e = '{:x}'.format(n)
+ ilen = ((bits + 7) // 8) * 2 # number of bytes rounded up times 2 bytes
+ return unhexlify(e.rjust(ilen, '0')[:ilen])
+
+
+def _decode_int(n):
+ return int(hexlify(n), 16)
+
+
+class _RawJWS(object):
+
+ def sign(self, key, payload):
+ raise NotImplementedError
+
+ def verify(self, key, payload, signature):
+ raise NotImplementedError
+
+
+class _RawHMAC(_RawJWS):
+
+ def __init__(self, hashfn):
+ self.backend = default_backend()
+ self.hashfn = hashfn
+
+ def _hmac_setup(self, key, payload):
+ h = hmac.HMAC(key, self.hashfn, backend=self.backend)
+ h.update(payload)
+ return h
+
+ def sign(self, key, payload):
+ skey = base64url_decode(key.get_op_key('sign'))
+ h = self._hmac_setup(skey, payload)
+ return h.finalize()
+
+ def verify(self, key, payload, signature):
+ vkey = base64url_decode(key.get_op_key('verify'))
+ h = self._hmac_setup(vkey, payload)
+ h.verify(signature)
+
+
+class _RawRSA(_RawJWS):
+ def __init__(self, padfn, hashfn):
+ self.padfn = padfn
+ self.hashfn = hashfn
+
+ def sign(self, key, payload):
+ skey = key.get_op_key('sign')
+ return skey.sign(payload, self.padfn, self.hashfn)
+
+ def verify(self, key, payload, signature):
+ pkey = key.get_op_key('verify')
+ pkey.verify(signature, payload, self.padfn, self.hashfn)
+
+
+class _RawEC(_RawJWS):
+ def __init__(self, curve, hashfn):
+ self._curve = curve
+ self.hashfn = hashfn
+
+ @property
+ def curve(self):
+ return self._curve
+
+ def sign(self, key, payload):
+ skey = key.get_op_key('sign', self._curve)
+ signature = skey.sign(payload, ec.ECDSA(self.hashfn))
+ r, s = ec_utils.decode_rfc6979_signature(signature)
+ l = key.get_curve(self._curve).key_size
+ return _encode_int(r, l) + _encode_int(s, l)
+
+ def verify(self, key, payload, signature):
+ pkey = key.get_op_key('verify', self._curve)
+ r = signature[:len(signature) // 2]
+ s = signature[len(signature) // 2:]
+ enc_signature = ec_utils.encode_rfc6979_signature(
+ int(hexlify(r), 16), int(hexlify(s), 16))
+ pkey.verify(enc_signature, payload, ec.ECDSA(self.hashfn))
+
+
+class _RawNone(_RawJWS):
+
+ def sign(self, key, payload):
+ return ''
+
+ def verify(self, key, payload, signature):
+ raise InvalidSignature('The "none" signature cannot be verified')
+
+
+class _HS256(_RawHMAC, JWAAlgorithm):
+
+ name = "HS256"
+ description = "HMAC using SHA-256"
+ keysize = 256
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_HS256, self).__init__(hashes.SHA256())
+
+
+class _HS384(_RawHMAC, JWAAlgorithm):
+
+ name = "HS384"
+ description = "HMAC using SHA-384"
+ keysize = 384
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_HS384, self).__init__(hashes.SHA384())
+
+
+class _HS512(_RawHMAC, JWAAlgorithm):
+
+ name = "HS512"
+ description = "HMAC using SHA-512"
+ keysize = 512
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_HS512, self).__init__(hashes.SHA512())
+
+
+class _RS256(_RawRSA, JWAAlgorithm):
+
+ name = "RS256"
+ description = "RSASSA-PKCS1-v1_5 using SHA-256"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_RS256, self).__init__(padding.PKCS1v15(), hashes.SHA256())
+
+
+class _RS384(_RawRSA, JWAAlgorithm):
+
+ name = "RS384"
+ description = "RSASSA-PKCS1-v1_5 using SHA-384"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_RS384, self).__init__(padding.PKCS1v15(), hashes.SHA384())
+
+
+class _RS512(_RawRSA, JWAAlgorithm):
+
+ name = "RS512"
+ description = "RSASSA-PKCS1-v1_5 using SHA-512"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_RS512, self).__init__(padding.PKCS1v15(), hashes.SHA512())
+
+
+class _ES256(_RawEC, JWAAlgorithm):
+
+ name = "ES256"
+ description = "ECDSA using P-256 and SHA-256"
+ keysize = 256
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_ES256, self).__init__('P-256', hashes.SHA256())
+
+
+class _ES384(_RawEC, JWAAlgorithm):
+
+ name = "ES384"
+ description = "ECDSA using P-384 and SHA-384"
+ keysize = 384
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_ES384, self).__init__('P-384', hashes.SHA384())
+
+
+class _ES512(_RawEC, JWAAlgorithm):
+
+ name = "ES512"
+ description = "ECDSA using P-521 and SHA-512"
+ keysize = 512
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ super(_ES512, self).__init__('P-521', hashes.SHA512())
+
+
+class _PS256(_RawRSA, JWAAlgorithm):
+
+ name = "PS256"
+ description = "RSASSA-PSS using SHA-256 and MGF1 with SHA-256"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ padfn = padding.PSS(padding.MGF1(hashes.SHA256()),
+ hashes.SHA256.digest_size)
+ super(_PS256, self).__init__(padfn, hashes.SHA256())
+
+
+class _PS384(_RawRSA, JWAAlgorithm):
+
+ name = "PS384"
+ description = "RSASSA-PSS using SHA-384 and MGF1 with SHA-384"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ padfn = padding.PSS(padding.MGF1(hashes.SHA384()),
+ hashes.SHA384.digest_size)
+ super(_PS384, self).__init__(padfn, hashes.SHA384())
+
+
+class _PS512(_RawRSA, JWAAlgorithm):
+
+ name = "PS512"
+ description = "RSASSA-PSS using SHA-512 and MGF1 with SHA-512"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+ def __init__(self):
+ padfn = padding.PSS(padding.MGF1(hashes.SHA512()),
+ hashes.SHA512.digest_size)
+ super(_PS512, self).__init__(padfn, hashes.SHA512())
+
+
+class _None(_RawNone, JWAAlgorithm):
+
+ name = "none"
+ description = "No digital signature or MAC performed"
+ keysize = 0
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'sig'
+
+
+class _RawKeyMgmt(object):
+
+ def wrap(self, key, bitsize, cek, headers):
+ raise NotImplementedError
+
+ def unwrap(self, key, bitsize, ek, headers):
+ raise NotImplementedError
+
+
+class _RSA(_RawKeyMgmt):
+
+ def __init__(self, padfn):
+ 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)
+
+ # FIXME: get key size and insure > 2048 bits
+ def wrap(self, key, bitsize, cek, headers):
+ self._check_key(key)
+ if not cek:
+ cek = _randombits(bitsize)
+ rk = key.get_op_key('wrapKey')
+ ek = rk.encrypt(cek, self.padfn)
+ return {'cek': cek, 'ek': ek}
+
+ def unwrap(self, key, bitsize, ek, headers):
+ self._check_key(key)
+ rk = key.get_op_key('decrypt')
+ cek = rk.decrypt(ek, self.padfn)
+ if _bitsize(cek) != bitsize:
+ raise InvalidJWEKeyLength(bitsize, _bitsize(cek))
+ return cek
+
+
+class _Rsa15(_RSA, JWAAlgorithm):
+
+ name = 'RSA1_5'
+ description = "RSAES-PKCS1-v1_5"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+ def __init__(self):
+ super(_Rsa15, self).__init__(padding.PKCS1v15())
+
+ def unwrap(self, key, bitsize, 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 = _randombits(bitsize)
+ try:
+ cek = super(_Rsa15, self).unwrap(key, bitsize, 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, JWAAlgorithm):
+
+ name = 'RSA-OAEP'
+ description = "RSAES OAEP using default parameters"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+ def __init__(self):
+ super(_RsaOaep, self).__init__(
+ padding.OAEP(padding.MGF1(hashes.SHA1()),
+ hashes.SHA1(), None))
+
+
+class _RsaOaep256(_RSA, JWAAlgorithm): # noqa: ignore=N801
+
+ name = 'RSA-OAEP-256'
+ description = "RSAES OAEP using SHA-256 and MGF1 with SHA-256"
+ keysize = 2048
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+ def __init__(self):
+ super(_RsaOaep256, self).__init__(
+ padding.OAEP(padding.MGF1(hashes.SHA256()),
+ hashes.SHA256(), None))
+
+
+class _AesKw(_RawKeyMgmt):
+
+ keysize = None
+
+ def __init__(self):
+ self.backend = default_backend()
+
+ 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))
+ if _bitsize(rk) != self.keysize:
+ raise InvalidJWEKeyLength(self.keysize, _bitsize(rk))
+ return rk
+
+ def wrap(self, key, bitsize, cek, headers):
+ rk = self._get_key(key, 'encrypt')
+ if not cek:
+ cek = _randombits(bitsize)
+
+ # Implement RFC 3394 Key Unwrap - 2.2.2
+ # TODO: Use cryptography once issue #1733 is resolved
+ iv = 'a6a6a6a6a6a6a6a6'
+ a = unhexlify(iv)
+ r = [cek[i:i + 8] for i in range(0, len(cek), 8)]
+ n = len(r)
+ for j in range(0, 6):
+ for i in range(0, n):
+ e = Cipher(algorithms.AES(rk), modes.ECB(),
+ backend=self.backend).encryptor()
+ b = e.update(a + r[i]) + e.finalize()
+ a = _encode_int(_decode_int(b[:8]) ^ ((n * j) + i + 1), 64)
+ r[i] = b[-8:]
+ ek = a
+ for i in range(0, n):
+ ek += r[i]
+ return {'cek': cek, 'ek': ek}
+
+ def unwrap(self, key, bitsize, ek, headers):
+ rk = self._get_key(key, 'decrypt')
+
+ # Implement RFC 3394 Key Unwrap - 2.2.3
+ # TODO: Use cryptography once issue #1733 is resolved
+ iv = 'a6a6a6a6a6a6a6a6'
+ aiv = unhexlify(iv)
+
+ r = [ek[i:i + 8] for i in range(0, len(ek), 8)]
+ a = r.pop(0)
+ n = len(r)
+ for j in range(5, -1, -1):
+ for i in range(n - 1, -1, -1):
+ da = _decode_int(a)
+ atr = _encode_int((da ^ ((n * j) + i + 1)), 64) + r[i]
+ d = Cipher(algorithms.AES(rk), modes.ECB(),
+ backend=self.backend).decryptor()
+ b = d.update(atr) + d.finalize()
+ a = b[:8]
+ r[i] = b[-8:]
+
+ if a != aiv:
+ raise RuntimeError('Decryption Failed')
+
+ cek = b''.join(r)
+ if _bitsize(cek) != bitsize:
+ raise InvalidJWEKeyLength(bitsize, _bitsize(cek))
+ return cek
+
+
+class _A128KW(_AesKw, JWAAlgorithm):
+
+ name = 'A128KW'
+ description = "AES Key Wrap using 128-bit key"
+ keysize = 128
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _A192KW(_AesKw, JWAAlgorithm):
+
+ name = 'A192KW'
+ description = "AES Key Wrap using 192-bit key"
+ keysize = 192
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _A256KW(_AesKw, JWAAlgorithm):
+
+ name = 'A256KW'
+ description = "AES Key Wrap using 256-bit key"
+ keysize = 256
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _AesGcmKw(_RawKeyMgmt):
+
+ keysize = None
+
+ def __init__(self):
+ self.backend = default_backend()
+
+ 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))
+ if _bitsize(rk) != self.keysize:
+ raise InvalidJWEKeyLength(self.keysize, _bitsize(rk))
+ return rk
+
+ def wrap(self, key, bitsize, cek, headers):
+ rk = self._get_key(key, 'encrypt')
+ if not cek:
+ cek = _randombits(bitsize)
+
+ iv = _randombits(96)
+ cipher = Cipher(algorithms.AES(rk), modes.GCM(iv),
+ backend=self.backend)
+ encryptor = cipher.encryptor()
+ ek = encryptor.update(cek) + encryptor.finalize()
+
+ tag = encryptor.tag
+ return {'cek': cek, 'ek': ek,
+ 'header': {'iv': base64url_encode(iv),
+ 'tag': base64url_encode(tag)}}
+
+ def unwrap(self, key, bitsize, ek, headers):
+ rk = self._get_key(key, 'decrypt')
+
+ if 'iv' not in headers:
+ raise ValueError('Invalid Header, missing "iv" parameter')
+ iv = base64url_decode(headers['iv'])
+ if 'tag' not in headers:
+ raise ValueError('Invalid Header, missing "tag" parameter')
+ tag = base64url_decode(headers['tag'])
+
+ cipher = Cipher(algorithms.AES(rk), modes.GCM(iv, tag),
+ backend=self.backend)
+ decryptor = cipher.decryptor()
+ cek = decryptor.update(ek) + decryptor.finalize()
+ if _bitsize(cek) != bitsize:
+ raise InvalidJWEKeyLength(bitsize, _bitsize(cek))
+ return cek
+
+
+class _A128GcmKw(_AesGcmKw, JWAAlgorithm):
+
+ name = 'A128GCMKW'
+ description = "Key wrapping with AES GCM using 128-bit key"
+ keysize = 128
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _A192GcmKw(_AesGcmKw, JWAAlgorithm):
+
+ name = 'A192GCMKW'
+ description = "Key wrapping with AES GCM using 192-bit key"
+ keysize = 192
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _A256GcmKw(_AesGcmKw, JWAAlgorithm):
+
+ name = 'A256GCMKW'
+ description = "Key wrapping with AES GCM using 256-bit key"
+ keysize = 256
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _Pbes2HsAesKw(_RawKeyMgmt):
+
+ name = None
+ keysize = None
+ hashsize = None
+
+ def __init__(self):
+ self.backend = default_backend()
+ self.aeskwmap = {128: _A128KW, 192: _A192KW, 256: _A256KW}
+
+ def _get_key(self, alg, key, p2s, p2c):
+ if isinstance(key, bytes):
+ plain = key
+ else:
+ plain = key.encode('utf8')
+ salt = bytes(self.name.encode('utf8')) + b'\x00' + p2s
+
+ if self.hashsize == 256:
+ hashalg = hashes.SHA256()
+ elif self.hashsize == 384:
+ hashalg = hashes.SHA384()
+ elif self.hashsize == 512:
+ hashalg = hashes.SHA512()
+ else:
+ raise ValueError('Unknown Hash Size')
+
+ kdf = PBKDF2HMAC(algorithm=hashalg, length=_inbytes(self.keysize),
+ salt=salt, iterations=p2c, backend=self.backend)
+ rk = kdf.derive(plain)
+ if _bitsize(rk) != self.keysize:
+ raise InvalidJWEKeyLength(self.keysize, len(rk))
+ return JWK(kty="oct", use="enc", k=base64url_encode(rk))
+
+ def wrap(self, key, bitsize, cek, headers):
+ p2s = _randombits(128)
+ p2c = 8192
+ kek = self._get_key(headers['alg'], key, p2s, p2c)
+
+ aeskw = self.aeskwmap[self.keysize]()
+ ret = aeskw.wrap(kek, bitsize, cek, headers)
+ ret['header'] = {'p2s': base64url_encode(p2s), 'p2c': p2c}
+ return ret
+
+ def unwrap(self, key, bitsize, ek, headers):
+ if 'p2s' not in headers:
+ raise ValueError('Invalid Header, missing "p2s" parameter')
+ if 'p2c' not in headers:
+ raise ValueError('Invalid Header, missing "p2c" parameter')
+ p2s = base64url_decode(headers['p2s'])
+ p2c = headers['p2c']
+ kek = self._get_key(headers['alg'], key, p2s, p2c)
+
+ aeskw = self.aeskwmap[self.keysize]()
+ return aeskw.unwrap(kek, bitsize, ek, headers)
+
+
+class _Pbes2Hs256A128Kw(_Pbes2HsAesKw, JWAAlgorithm):
+
+ name = 'PBES2-HS256+A128KW'
+ description = 'PBES2 with HMAC SHA-256 and "A128KW" wrapping'
+ keysize = 128
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+ hashsize = 256
+
+
+class _Pbes2Hs384A192Kw(_Pbes2HsAesKw, JWAAlgorithm):
+
+ name = 'PBES2-HS384+A192KW'
+ description = 'PBES2 with HMAC SHA-384 and "A192KW" wrapping'
+ keysize = 192
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+ hashsize = 384
+
+
+class _Pbes2Hs512A256Kw(_Pbes2HsAesKw, JWAAlgorithm):
+
+ name = 'PBES2-HS512+A256KW'
+ description = 'PBES2 with HMAC SHA-512 and "A256KW" wrapping'
+ keysize = 256
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+ hashsize = 512
+
+
+class _Direct(_RawKeyMgmt, JWAAlgorithm):
+
+ name = 'dir'
+ description = "Direct use of a shared symmetric key"
+ keysize = 128
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+ 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)
+
+ def wrap(self, key, bitsize, cek, headers):
+ self._check_key(key)
+ if cek:
+ return (cek, None)
+ k = base64url_decode(key.get_op_key('encrypt'))
+ if _bitsize(k) != bitsize:
+ raise InvalidCEKeyLength(bitsize, _bitsize(k))
+ return {'cek': k}
+
+ def unwrap(self, key, bitsize, ek, headers):
+ self._check_key(key)
+ if ek != b'':
+ raise ValueError('Invalid Encryption Key.')
+ cek = base64url_decode(key.get_op_key('decrypt'))
+ if _bitsize(cek) != bitsize:
+ raise InvalidJWEKeyLength(bitsize, _bitsize(cek))
+ return cek
+
+
+class _EcdhEs(_RawKeyMgmt, JWAAlgorithm):
+
+ name = 'ECDH-ES'
+ description = "ECDH-ES using Concat KDF"
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+ keysize = None
+
+ def __init__(self):
+ self.backend = default_backend()
+ self.aeskwmap = {128: _A128KW, 192: _A192KW, 256: _A256KW}
+
+ 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)
+
+ def _derive(self, privkey, pubkey, alg, bitsize, 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', bitsize)
+
+ # no SuppPrivInfo
+
+ shared_key = privkey.exchange(ec.ECDH(), pubkey)
+ ckdf = ConcatKDFHash(algorithm=hashes.SHA256(),
+ length=_inbytes(bitsize),
+ otherinfo=otherinfo,
+ backend=self.backend)
+ return ckdf.derive(shared_key)
+
+ def wrap(self, key, bitsize, cek, headers):
+ self._check_key(key)
+ if self.keysize is None:
+ if cek is not None:
+ raise InvalidJWEOperation('ECDH-ES cannot use an existing CEK')
+ alg = headers['enc']
+ else:
+ bitsize = self.keysize
+ 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, bitsize, headers)
+
+ if self.keysize is None:
+ ret = {'cek': dk}
+ else:
+ aeskw = self.aeskwmap[bitsize]()
+ kek = JWK(kty="oct", use="enc", k=base64url_encode(dk))
+ ret = aeskw.wrap(kek, bitsize, cek, headers)
+
+ ret['header'] = {'epk': json_decode(epk.export_public())}
+ return ret
+
+ def unwrap(self, key, bitsize, ek, headers):
+ if 'epk' not in headers:
+ raise ValueError('Invalid Header, missing "epk" parameter')
+ self._check_key(key)
+ if self.keysize is None:
+ alg = headers['enc']
+ else:
+ bitsize = self.keysize
+ alg = headers['alg']
+
+ epk = JWK(**headers['epk'])
+ dk = self._derive(key.get_op_key('unwrapKey'),
+ epk.get_op_key('wrapKey'),
+ alg, bitsize, headers)
+ if self.keysize is None:
+ return dk
+ else:
+ aeskw = self.aeskwmap[bitsize]()
+ kek = JWK(kty="oct", use="enc", k=base64url_encode(dk))
+ cek = aeskw.unwrap(kek, bitsize, ek, headers)
+ return cek
+
+
+class _EcdhEsAes128Kw(_EcdhEs):
+
+ name = 'ECDH-ES+A128KW'
+ description = 'ECDH-ES using Concat KDF and "A128KW" wrapping'
+ keysize = 128
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _EcdhEsAes192Kw(_EcdhEs):
+
+ name = 'ECDH-ES+A192KW'
+ description = 'ECDH-ES using Concat KDF and "A192KW" wrapping'
+ keysize = 192
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _EcdhEsAes256Kw(_EcdhEs):
+
+ name = 'ECDH-ES+A256KW'
+ description = 'ECDH-ES using Concat KDF and "A128KW" wrapping'
+ keysize = 256
+ algorithm_usage_location = 'alg'
+ algorithm_use = 'kex'
+
+
+class _RawJWE(object):
+
+ def encrypt(self, k, a, m):
+ raise NotImplementedError
+
+ def decrypt(self, k, a, iv, e, t):
+ raise NotImplementedError
+
+
+class _AesCbcHmacSha2(_RawJWE):
+
+ keysize = None
+
+ def __init__(self, hashfn):
+ self.backend = default_backend()
+ self.hashfn = hashfn
+ self.blocksize = algorithms.AES.block_size
+ self.wrap_key_size = self.keysize * 2
+
+ def _mac(self, k, a, iv, e):
+ al = _encode_int(_bitsize(a), 64)
+ h = hmac.HMAC(k, self.hashfn, backend=self.backend)
+ h.update(a)
+ h.update(iv)
+ h.update(e)
+ h.update(al)
+ m = h.finalize()
+ return m[:_inbytes(self.keysize)]
+
+ # RFC 7518 - 5.2.2
+ def encrypt(self, k, a, m):
+ """ Encrypt according to the selected encryption and hashing
+ functions.
+
+ :param k: Encryption key (optional)
+ :param a: Additional Authentication Data
+ :param m: Plaintext
+
+ Returns a dictionary with the computed data.
+ """
+ hkey = k[:_inbytes(self.keysize)]
+ ekey = k[_inbytes(self.keysize):]
+
+ # encrypt
+ iv = _randombits(self.blocksize)
+ cipher = Cipher(algorithms.AES(ekey), modes.CBC(iv),
+ backend=self.backend)
+ encryptor = cipher.encryptor()
+ padder = PKCS7(self.blocksize).padder()
+ padded_data = padder.update(m) + padder.finalize()
+ e = encryptor.update(padded_data) + encryptor.finalize()
+
+ # mac
+ t = self._mac(hkey, a, iv, e)
+
+ return (iv, e, t)
+
+ def decrypt(self, k, a, iv, e, t):
+ """ Decrypt according to the selected encryption and hashing
+ functions.
+ :param k: Encryption key (optional)
+ :param a: Additional Authenticated Data
+ :param iv: Initialization Vector
+ :param e: Ciphertext
+ :param t: Authentication Tag
+
+ Returns plaintext or raises an error
+ """
+ hkey = k[:_inbytes(self.keysize)]
+ dkey = k[_inbytes(self.keysize):]
+
+ # verify mac
+ if not constant_time.bytes_eq(t, self._mac(hkey, a, iv, e)):
+ raise InvalidSignature('Failed to verify MAC')
+
+ # decrypt
+ cipher = Cipher(algorithms.AES(dkey), modes.CBC(iv),
+ backend=self.backend)
+ decryptor = cipher.decryptor()
+ d = decryptor.update(e) + decryptor.finalize()
+ unpadder = PKCS7(self.blocksize).unpadder()
+ return unpadder.update(d) + unpadder.finalize()
+
+
+class _A128CbcHs256(_AesCbcHmacSha2, JWAAlgorithm):
+
+ name = 'A128CBC-HS256'
+ description = "AES_128_CBC_HMAC_SHA_256 authenticated"
+ keysize = 128
+ algorithm_usage_location = 'enc'
+ algorithm_use = 'enc'
+
+ def __init__(self):
+ super(_A128CbcHs256, self).__init__(hashes.SHA256())
+
+
+class _A192CbcHs384(_AesCbcHmacSha2, JWAAlgorithm):
+
+ name = 'A192CBC-HS384'
+ description = "AES_192_CBC_HMAC_SHA_384 authenticated"
+ keysize = 192
+ algorithm_usage_location = 'enc'
+ algorithm_use = 'enc'
+
+ def __init__(self):
+ super(_A192CbcHs384, self).__init__(hashes.SHA384())
+
+
+class _A256CbcHs512(_AesCbcHmacSha2, JWAAlgorithm):
+
+ name = 'A256CBC-HS512'
+ description = "AES_256_CBC_HMAC_SHA_512 authenticated"
+ keysize = 256
+ algorithm_usage_location = 'enc'
+ algorithm_use = 'enc'
+
+ def __init__(self):
+ super(_A256CbcHs512, self).__init__(hashes.SHA512())
+
+
+class _AesGcm(_RawJWE):
+
+ keysize = None
+
+ def __init__(self):
+ self.backend = default_backend()
+ self.wrap_key_size = self.keysize
+
+ # RFC 7518 - 5.3
+ def encrypt(self, k, a, m):
+ """ Encrypt accoriding to the selected encryption and hashing
+ functions.
+
+ :param k: Encryption key (optional)
+ :param a: Additional Authentication Data
+ :param m: Plaintext
+
+ Returns a dictionary with the computed data.
+ """
+ iv = _randombits(96)
+ cipher = Cipher(algorithms.AES(k), modes.GCM(iv),
+ backend=self.backend)
+ encryptor = cipher.encryptor()
+ encryptor.authenticate_additional_data(a)
+ e = encryptor.update(m) + encryptor.finalize()
+
+ return (iv, e, encryptor.tag)
+
+ def decrypt(self, k, a, iv, e, t):
+ """ Decrypt accoriding to the selected encryption and hashing
+ functions.
+ :param k: Encryption key (optional)
+ :param a: Additional Authenticated Data
+ :param iv: Initialization Vector
+ :param e: Ciphertext
+ :param t: Authentication Tag
+
+ Returns plaintext or raises an error
+ """
+ cipher = Cipher(algorithms.AES(k), modes.GCM(iv, t),
+ backend=self.backend)
+ decryptor = cipher.decryptor()
+ decryptor.authenticate_additional_data(a)
+ return decryptor.update(e) + decryptor.finalize()
+
+
+class _A128Gcm(_AesGcm, JWAAlgorithm):
+
+ name = 'A128GCM'
+ description = "AES GCM using 128-bit key"
+ keysize = 128
+ algorithm_usage_location = 'enc'
+ algorithm_use = 'enc'
+
+
+class _A192Gcm(_AesGcm, JWAAlgorithm):
+
+ name = 'A192GCM'
+ description = "AES GCM using 192-bit key"
+ keysize = 192
+ algorithm_usage_location = 'enc'
+ algorithm_use = 'enc'
+
+
+class _A256Gcm(_AesGcm, JWAAlgorithm):
+
+ name = 'A256GCM'
+ description = "AES GCM using 256-bit key"
+ keysize = 256
+ algorithm_usage_location = 'enc'
+ algorithm_use = 'enc'
+
+
+class JWA(object):
+ """JWA Signing Algorithms.
+
+ This class provides access to all JWA algorithms.
+ """
+
+ algorithms_registry = {
+ 'HS256': _HS256,
+ 'HS384': _HS384,
+ 'HS512': _HS512,
+ 'RS256': _RS256,
+ 'RS384': _RS384,
+ 'RS512': _RS512,
+ 'ES256': _ES256,
+ 'ES384': _ES384,
+ 'ES512': _ES512,
+ 'PS256': _PS256,
+ 'PS384': _PS384,
+ 'PS512': _PS512,
+ 'none': _None,
+ 'RSA1_5': _Rsa15,
+ 'RSA-OAEP': _RsaOaep,
+ 'RSA-OAEP-256': _RsaOaep256,
+ 'A128KW': _A128KW,
+ '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,
+ 'PBES2-HS256+A128KW': _Pbes2Hs256A128Kw,
+ 'PBES2-HS384+A192KW': _Pbes2Hs384A192Kw,
+ 'PBES2-HS512+A256KW': _Pbes2Hs512A256Kw,
+ 'A128CBC-HS256': _A128CbcHs256,
+ 'A192CBC-HS384': _A192CbcHs384,
+ 'A256CBC-HS512': _A256CbcHs512,
+ 'A128GCM': _A128Gcm,
+ 'A192GCM': _A192Gcm,
+ 'A256GCM': _A256Gcm
+ }
+
+ @classmethod
+ def instantiate_alg(cls, name, use=None):
+ alg = cls.algorithms_registry[name]
+ if use is not None and alg.algorithm_use != use:
+ raise KeyError
+ return alg()
+
+ @classmethod
+ def signing_alg(cls, name):
+ try:
+ return cls.instantiate_alg(name, use='sig')
+ except KeyError:
+ raise InvalidJWAAlgorithm(
+ '%s is not a valid Signign algorithm name' % name)
+
+ @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)
+
+ @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)
diff --git a/jwcrypto/jwe.py b/jwcrypto/jwe.py
new file mode 100644
index 0000000..353820a
--- /dev/null
+++ b/jwcrypto/jwe.py
@@ -0,0 +1,485 @@
+# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
+
+import zlib
+
+from jwcrypto import common
+from jwcrypto.common import base64url_decode, base64url_encode
+from jwcrypto.common import json_decode, json_encode
+from jwcrypto.jwa import JWA
+
+
+# RFC 7516 - 4.1
+# name: (description, supported?)
+JWEHeaderRegistry = {'alg': ('Algorithm', True),
+ 'enc': ('Encryption Algorithm', True),
+ 'zip': ('Compression Algorithm', True),
+ 'jku': ('JWK Set URL', False),
+ 'jwk': ('JSON Web Key', False),
+ 'kid': ('Key ID', True),
+ 'x5u': ('X.509 URL', False),
+ 'x5c': ('X.509 Certificate Chain', False),
+ 'x5t': ('X.509 Certificate SHA-1 Thumbprint', False),
+ 'x5t#S256': ('X.509 Certificate SHA-256 Thumbprint',
+ False),
+ 'typ': ('Type', True),
+ 'cty': ('Content Type', True),
+ 'crit': ('Critical', True)}
+"""Registry of valid header parameters"""
+
+default_allowed_algs = [
+ # Key Management Algorithms
+ 'RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256',
+ 'A128KW', 'A192KW', 'A256KW',
+ 'dir',
+ 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
+ 'A128GCMKW', 'A192GCMKW', 'A256GCMKW',
+ 'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW',
+ # Content Encryption Algoritms
+ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512',
+ 'A128GCM', 'A192GCM', 'A256GCM']
+"""Default allowed algorithms"""
+
+
+class InvalidJWEData(Exception):
+ """Invalid JWE Object.
+
+ This exception is raised when the JWE Object is invalid and/or
+ improperly formatted.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = message
+ else:
+ msg = 'Unknown Data Verification Failure'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(InvalidJWEData, self).__init__(msg)
+
+
+# These have been moved to jwcrypto.common, maintain here for bacwards compat
+InvalidCEKeyLength = common.InvalidCEKeyLength
+InvalidJWEKeyLength = common.InvalidJWEKeyLength
+InvalidJWEKeyType = common.InvalidJWEKeyType
+InvalidJWEOperation = common.InvalidJWEOperation
+
+
+class JWE(object):
+ """JSON Web Encryption object
+
+ This object represent a JWE token.
+ """
+
+ def __init__(self, plaintext=None, protected=None, unprotected=None,
+ aad=None, algs=None, recipient=None, header=None):
+ """Creates a JWE token.
+
+ :param plaintext(bytes): An arbitrary plaintext to be encrypted.
+ :param protected: A JSON string with the protected header.
+ :param unprotected: A JSON string with the shared unprotected header.
+ :param aad(bytes): Arbitrary additional authenticated data
+ :param algs: An optional list of allowed algorithms
+ :param recipient: An optional, default recipient key
+ :param header: An optional header for the default recipient
+ """
+ self._allowed_algs = None
+ self.objects = dict()
+ self.plaintext = None
+ if plaintext is not None:
+ if isinstance(plaintext, bytes):
+ self.plaintext = plaintext
+ else:
+ self.plaintext = plaintext.encode('utf-8')
+ self.cek = None
+ self.decryptlog = None
+ if aad:
+ self.objects['aad'] = aad
+ if protected:
+ if isinstance(protected, dict):
+ protected = json_encode(protected)
+ else:
+ json_decode(protected) # check header encoding
+ self.objects['protected'] = protected
+ if unprotected:
+ if isinstance(unprotected, dict):
+ unprotected = json_encode(unprotected)
+ else:
+ json_decode(unprotected) # check header encoding
+ self.objects['unprotected'] = unprotected
+ if algs:
+ self.allowed_algs = algs
+
+ if recipient:
+ self.add_recipient(recipient, header=header)
+ elif header:
+ raise ValueError('Header is allowed only with default recipient')
+
+ def _jwa_keymgmt(self, name):
+ allowed = self._allowed_algs or default_allowed_algs
+ if name not in allowed:
+ raise InvalidJWEOperation('Algorithm not allowed')
+ return JWA.keymgmt_alg(name)
+
+ def _jwa_enc(self, name):
+ allowed = self._allowed_algs or default_allowed_algs
+ if name not in allowed:
+ raise InvalidJWEOperation('Algorithm not allowed')
+ return JWA.encryption_alg(name)
+
+ @property
+ def allowed_algs(self):
+ """Allowed algorithms.
+
+ The list of allowed algorithms.
+ Can be changed by setting a list of algorithm names.
+ """
+
+ if self._allowed_algs:
+ return self._allowed_algs
+ else:
+ return default_allowed_algs
+
+ @allowed_algs.setter
+ def allowed_algs(self, algs):
+ if not isinstance(algs, list):
+ raise TypeError('Allowed Algs must be a list')
+ self._allowed_algs = algs
+
+ def _merge_headers(self, h1, h2):
+ for k in list(h1.keys()):
+ if k in h2:
+ raise InvalidJWEData('Duplicate header: "%s"' % k)
+ h1.update(h2)
+ return h1
+
+ def _get_jose_header(self, header=None):
+ jh = dict()
+ if 'protected' in self.objects:
+ ph = json_decode(self.objects['protected'])
+ jh = self._merge_headers(jh, ph)
+ if 'unprotected' in self.objects:
+ uh = json_decode(self.objects['unprotected'])
+ jh = self._merge_headers(jh, uh)
+ if header:
+ rh = json_decode(header)
+ jh = self._merge_headers(jh, rh)
+ return jh
+
+ def _get_alg_enc_from_headers(self, jh):
+ algname = jh.get('alg', None)
+ if algname is None:
+ raise InvalidJWEData('Missing "alg" from headers')
+ alg = self._jwa_keymgmt(algname)
+ encname = jh.get('enc', None)
+ if encname is None:
+ raise InvalidJWEData('Missing "enc" from headers')
+ enc = self._jwa_enc(encname)
+ return alg, enc
+
+ def _encrypt(self, alg, enc, jh):
+ aad = base64url_encode(self.objects.get('protected', ''))
+ if 'aad' in self.objects:
+ aad += '.' + base64url_encode(self.objects['aad'])
+ aad = aad.encode('utf-8')
+
+ compress = jh.get('zip', None)
+ if compress == 'DEF':
+ data = zlib.compress(self.plaintext)[2:-4]
+ elif compress is None:
+ data = self.plaintext
+ else:
+ raise ValueError('Unknown compression')
+
+ iv, ciphertext, tag = enc.encrypt(self.cek, aad, data)
+ self.objects['iv'] = iv
+ self.objects['ciphertext'] = ciphertext
+ self.objects['tag'] = tag
+
+ def add_recipient(self, key, header=None):
+ """Encrypt the plaintext with the given key.
+
+ :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 compression type is unknown.
+ :raises InvalidJWAAlgorithm: if the 'alg' provided in the JOSE
+ headers is missing or unknown, or otherwise not implemented.
+ """
+ if self.plaintext is None:
+ raise ValueError('Missing plaintext')
+ if not isinstance(self.plaintext, bytes):
+ raise ValueError("Plaintext must be 'bytes'")
+
+ if isinstance(header, dict):
+ header = json_encode(header)
+
+ jh = self._get_jose_header(header)
+ alg, enc = self._get_alg_enc_from_headers(jh)
+
+ rec = dict()
+ if header:
+ rec['header'] = header
+
+ wrapped = alg.wrap(key, enc.wrap_key_size, self.cek, jh)
+ self.cek = wrapped['cek']
+
+ if 'ek' in wrapped:
+ rec['encrypted_key'] = wrapped['ek']
+
+ if 'header' in wrapped:
+ h = json_decode(rec.get('header', '{}'))
+ nh = self._merge_headers(h, wrapped['header'])
+ rec['header'] = json_encode(nh)
+
+ if 'ciphertext' not in self.objects:
+ self._encrypt(alg, enc, jh)
+
+ if 'recipients' in self.objects:
+ self.objects['recipients'].append(rec)
+ elif 'encrypted_key' in self.objects or 'header' in self.objects:
+ self.objects['recipients'] = list()
+ n = dict()
+ if 'encrypted_key' in self.objects:
+ n['encrypted_key'] = self.objects.pop('encrypted_key')
+ if 'header' in self.objects:
+ n['header'] = self.objects.pop('header')
+ self.objects['recipients'].append(n)
+ self.objects['recipients'].append(rec)
+ else:
+ self.objects.update(rec)
+
+ def serialize(self, compact=False):
+ """Serializes the object into a JWE token.
+
+ :param compact(boolean): if True generates the compact
+ representation, otherwise generates a standard JSON format.
+
+ :raises InvalidJWEOperation: if the object cannot serialized
+ with the compact representation and `compact` is True.
+ :raises InvalidJWEOperation: if no recipients have been added
+ to the object.
+ """
+
+ if 'ciphertext' not in self.objects:
+ raise InvalidJWEOperation("No available ciphertext")
+
+ if compact:
+ for invalid in 'aad', 'unprotected':
+ if invalid in self.objects:
+ raise InvalidJWEOperation("Can't use compact encoding")
+ if 'recipients' in self.objects:
+ if len(self.objects['recipients']) != 1:
+ raise InvalidJWEOperation("Invalid number of recipients")
+ rec = self.objects['recipients'][0]
+ else:
+ rec = self.objects
+ if 'header' in rec:
+ # The AESGCMKW algorithm generates data (iv, tag) we put in the
+ # per-recipient unpotected header by default. Move it to the
+ # protected header and re-encrypt the payload, as the protected
+ # header is used as additional authenticated data.
+ h = json_decode(rec['header'])
+ ph = json_decode(self.objects['protected'])
+ nph = self._merge_headers(h, ph)
+ self.objects['protected'] = json_encode(nph)
+ jh = self._get_jose_header()
+ alg, enc = self._get_alg_enc_from_headers(jh)
+ self._encrypt(alg, enc, jh)
+ del rec['header']
+
+ return '.'.join([base64url_encode(self.objects['protected']),
+ base64url_encode(rec.get('encrypted_key', '')),
+ base64url_encode(self.objects['iv']),
+ base64url_encode(self.objects['ciphertext']),
+ base64url_encode(self.objects['tag'])])
+ else:
+ obj = self.objects
+ enc = {'ciphertext': base64url_encode(obj['ciphertext']),
+ 'iv': base64url_encode(obj['iv']),
+ 'tag': base64url_encode(self.objects['tag'])}
+ if 'protected' in obj:
+ enc['protected'] = base64url_encode(obj['protected'])
+ if 'unprotected' in obj:
+ enc['unprotected'] = json_decode(obj['unprotected'])
+ if 'aad' in obj:
+ enc['aad'] = base64url_encode(obj['aad'])
+ if 'recipients' in obj:
+ enc['recipients'] = list()
+ for rec in obj['recipients']:
+ e = dict()
+ if 'encrypted_key' in rec:
+ e['encrypted_key'] = \
+ base64url_encode(rec['encrypted_key'])
+ if 'header' in rec:
+ e['header'] = json_decode(rec['header'])
+ enc['recipients'].append(e)
+ else:
+ if 'encrypted_key' in obj:
+ enc['encrypted_key'] = \
+ base64url_encode(obj['encrypted_key'])
+ if 'header' in obj:
+ enc['header'] = json_decode(obj['header'])
+ return json_encode(enc)
+
+ def _check_crit(self, crit):
+ for k in crit:
+ if k not in JWEHeaderRegistry:
+ raise InvalidJWEData('Unknown critical header: "%s"' % k)
+ else:
+ if not JWEHeaderRegistry[k][1]:
+ raise InvalidJWEData('Unsupported critical header: '
+ '"%s"' % k)
+
+ # FIXME: allow to specify which algorithms to accept as valid
+ def _decrypt(self, key, ppe):
+
+ jh = self._get_jose_header(ppe.get('header', None))
+
+ # TODO: allow caller to specify list of headers it understands
+ self._check_crit(jh.get('crit', dict()))
+
+ alg = self._jwa_keymgmt(jh.get('alg', None))
+ enc = self._jwa_enc(jh.get('enc', None))
+
+ aad = base64url_encode(self.objects.get('protected', ''))
+ if 'aad' in self.objects:
+ aad += '.' + base64url_encode(self.objects['aad'])
+
+ cek = alg.unwrap(key, enc.wrap_key_size,
+ ppe.get('encrypted_key', b''), jh)
+ data = enc.decrypt(cek, aad.encode('utf-8'),
+ self.objects['iv'],
+ self.objects['ciphertext'],
+ self.objects['tag'])
+
+ self.decryptlog.append('Success')
+ self.cek = cek
+
+ compress = jh.get('zip', None)
+ if compress == 'DEF':
+ self.plaintext = zlib.decompress(data, -zlib.MAX_WBITS)
+ elif compress is None:
+ self.plaintext = data
+ else:
+ raise ValueError('Unknown compression')
+
+ def decrypt(self, key):
+ """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 'ciphertext' not in self.objects:
+ raise InvalidJWEOperation("No available ciphertext")
+ self.decryptlog = list()
+
+ if 'recipients' in self.objects:
+ for rec in self.objects['recipients']:
+ try:
+ self._decrypt(key, rec)
+ except Exception as e: # pylint: disable=broad-except
+ self.decryptlog.append('Failed: [%s]' % repr(e))
+ else:
+ try:
+ self._decrypt(key, self.objects)
+ except Exception as e: # pylint: disable=broad-except
+ self.decryptlog.append('Failed: [%s]' % repr(e))
+
+ if not self.plaintext:
+ raise InvalidJWEData('No recipient matched the provided '
+ 'key' + repr(self.decryptlog))
+
+ def deserialize(self, raw_jwe, key=None):
+ """Deserialize a JWE token.
+
+ NOTE: Destroys any current status and tries to import the raw
+ JWE provided.
+
+ :param raw_jwe: a 'raw' JWE token (JSON Encoded or Compact
+ notation) string.
+ :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.
+
+ :raises InvalidJWEData: if the raw object is an invaid JWE token.
+ :raises InvalidJWEOperation: if the decryption fails.
+ """
+
+ self.objects = dict()
+ self.plaintext = None
+ self.cek = None
+
+ o = dict()
+ try:
+ try:
+ djwe = json_decode(raw_jwe)
+ o['iv'] = base64url_decode(djwe['iv'])
+ o['ciphertext'] = base64url_decode(djwe['ciphertext'])
+ o['tag'] = base64url_decode(djwe['tag'])
+ if 'protected' in djwe:
+ p = base64url_decode(djwe['protected'])
+ o['protected'] = p.decode('utf-8')
+ if 'unprotected' in djwe:
+ o['unprotected'] = json_encode(djwe['unprotected'])
+ if 'aad' in djwe:
+ o['aad'] = base64url_decode(djwe['aad'])
+ if 'recipients' in djwe:
+ o['recipients'] = list()
+ for rec in djwe['recipients']:
+ e = dict()
+ if 'encrypted_key' in rec:
+ e['encrypted_key'] = \
+ base64url_decode(rec['encrypted_key'])
+ if 'header' in rec:
+ e['header'] = json_encode(rec['header'])
+ o['recipients'].append(e)
+ else:
+ if 'encrypted_key' in djwe:
+ o['encrypted_key'] = \
+ base64url_decode(djwe['encrypted_key'])
+ if 'header' in djwe:
+ o['header'] = json_encode(djwe['header'])
+
+ except ValueError:
+ c = raw_jwe.split('.')
+ if len(c) != 5:
+ raise InvalidJWEData()
+ p = base64url_decode(c[0])
+ o['protected'] = p.decode('utf-8')
+ ekey = base64url_decode(c[1])
+ if ekey != b'':
+ o['encrypted_key'] = base64url_decode(c[1])
+ o['iv'] = base64url_decode(c[2])
+ o['ciphertext'] = base64url_decode(c[3])
+ o['tag'] = base64url_decode(c[4])
+
+ self.objects = o
+
+ except Exception as e: # pylint: disable=broad-except
+ raise InvalidJWEData('Invalid format', repr(e))
+
+ if key:
+ self.decrypt(key)
+
+ @property
+ def payload(self):
+ if not self.plaintext:
+ raise InvalidJWEOperation("Plaintext not available")
+ return self.plaintext
+
+ @property
+ def jose_header(self):
+ jh = self._get_jose_header()
+ if len(jh) == 0:
+ raise InvalidJWEOperation("JOSE Header not available")
+ return jh
diff --git a/jwcrypto/jwk.py b/jwcrypto/jwk.py
new file mode 100644
index 0000000..b64a0b5
--- /dev/null
+++ b/jwcrypto/jwk.py
@@ -0,0 +1,803 @@
+# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
+
+import os
+
+from binascii import hexlify, unhexlify
+
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
+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 jwcrypto.common import base64url_decode, base64url_encode
+from jwcrypto.common import json_decode, json_encode
+
+
+# RFC 7518 - 7.4
+JWKTypesRegistry = {'EC': 'Elliptic Curve',
+ 'RSA': 'RSA',
+ 'oct': 'Octet sequence'}
+"""Registry of valid Key Types"""
+
+# 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', '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', ),
+ 'use': ('Public Key Use', 'Public'),
+ 'key_ops': ('Key Operations', 'Public'),
+ 'alg': ('Algorithm', 'Public'),
+ 'kid': ('Key ID', 'Public'),
+ 'x5u': ('X.509 URL', 'Public'),
+ 'x5c': ('X.509 Certificate Chain', 'Public'),
+ 'x5t': ('X.509 Certificate SHA-1 Thumbprint', 'Public'),
+ 'x5t#S256': ('X.509 Certificate SHA-256 Thumbprint',
+ 'Public')}
+"""Regstry of valid key parameters"""
+
+# RFC 7518 - 7.6
+JWKEllipticCurveRegistry = {'P-256': 'P-256 curve',
+ 'P-384': 'P-384 curve',
+ 'P-521': 'P-521 curve'}
+"""Registry of allowed Elliptic Curves"""
+
+# RFC 7517 - 8.2
+JWKUseRegistry = {'sig': 'Digital Signature or MAC',
+ 'enc': 'Encryption'}
+"""Registry of allowed uses"""
+
+# RFC 7517 - 8.3
+JWKOperationsRegistry = {'sign': 'Compute digital Signature or MAC',
+ 'verify': 'Verify digital signature or MAC',
+ 'encrypt': 'Encrypt content',
+ 'decrypt': 'Decrypt content and validate'
+ ' decryption, if applicable',
+ 'wrapKey': 'Encrypt key',
+ 'unwrapKey': 'Decrypt key and validate'
+ ' decryption, if applicable',
+ 'deriveKey': 'Derive key',
+ 'deriveBits': 'Derive bits not to be used as a key'}
+"""Registry of allowed operations"""
+
+JWKpycaCurveMap = {'secp256r1': 'P-256',
+ 'secp384r1': 'P-384',
+ 'secp521r1': 'P-521'}
+
+
+class InvalidJWKType(Exception):
+ """Invalid JWK Type Exception.
+
+ This exception is raised when an invalid parameter type is used.
+ """
+
+ def __init__(self, value=None):
+ super(InvalidJWKType, self).__init__()
+ self.value = value
+
+ def __str__(self):
+ return 'Unknown type "%s", valid types are: %s' % (
+ self.value, list(JWKTypesRegistry.keys()))
+
+
+class InvalidJWKUsage(Exception):
+ """Invalid JWK usage Exception.
+
+ This exception is raised when an invalid key usage is requested,
+ based on the key type and declared usage constraints.
+ """
+
+ def __init__(self, use, value):
+ super(InvalidJWKUsage, self).__init__()
+ self.value = value
+ self.use = use
+
+ def __str__(self):
+ if self.use in list(JWKUseRegistry.keys()):
+ usage = JWKUseRegistry[self.use]
+ else:
+ usage = 'Unknown(%s)' % self.use
+ if self.value in list(JWKUseRegistry.keys()):
+ valid = JWKUseRegistry[self.value]
+ else:
+ valid = 'Unknown(%s)' % self.value
+ return 'Invalid usage requested: "%s". Valid for: "%s"' % (usage,
+ valid)
+
+
+class InvalidJWKOperation(Exception):
+ """Invalid JWK Operation Exception.
+
+ This exception is raised when an invalid key operation is requested,
+ based on the key type and declared usage constraints.
+ """
+
+ def __init__(self, operation, values):
+ super(InvalidJWKOperation, self).__init__()
+ self.op = operation
+ self.values = values
+
+ def __str__(self):
+ if self.op in list(JWKOperationsRegistry.keys()):
+ op = JWKOperationsRegistry[self.op]
+ else:
+ op = 'Unknown(%s)' % self.op
+ valid = list()
+ for v in self.values:
+ if v in list(JWKOperationsRegistry.keys()):
+ valid.append(JWKOperationsRegistry[v])
+ else:
+ valid.append('Unknown(%s)' % v)
+ return 'Invalid operation requested: "%s". Valid for: "%s"' % (op,
+ valid)
+
+
+class InvalidJWKValue(Exception):
+ """Invalid JWK Value Exception.
+
+ This exception is raised when an invalid/unknown value is used in the
+ context of an operation that requires specific values to be used based
+ on the key type or other constraints.
+ """
+
+ pass
+
+
+class JWK(object):
+ """JSON Web Key object
+
+ This object represent a Key.
+ It must be instantiated by using the standard defined key/value pairs
+ as arguments of the initialization function.
+ """
+
+ def __init__(self, **kwargs):
+ """Creates a new JWK object.
+
+ The function arguments must be valid parameters as defined in the
+ 'IANA JSON Web Key Set Parameters registry' and specified in
+ the :data:`JWKParamsRegistry` variable. The 'kty' parameter must
+ always be provided and its value must be a valid one as defined
+ by the 'IANA JSON Web Key Types registry' and specified in the
+ :data:`JWKTypesRegistry` variable. The valid key parameters per
+ key type are defined in the :data:`JWKValuesregistry` variable.
+
+ To generate a new random key call the class method generate() with
+ the appropriate 'kty' parameter, and other parameters as needed (key
+ size, public exponents, curve types, etc..)
+
+ Valid options per type, when generating new keys:
+ * oct: size(int)
+ * RSA: public_exponent(int), size(int)
+ * EC: curve(str) (one of P-256, P-384, P-521)
+
+ Deprecated:
+ Alternatively if the 'generate' parameter is provided, with a
+ valid key type as value then a new key will be generated according
+ to the defaults or provided key strenght options (type specific).
+
+ :raises InvalidJWKType: if the key type is invalid
+ :raises InvalidJWKValue: if incorrect or inconsistent parameters
+ are provided.
+ """
+ self._params = dict()
+ self._key = dict()
+ self._unknown = dict()
+
+ if 'generate' in kwargs:
+ self.generate_key(**kwargs)
+ elif kwargs:
+ self.import_key(**kwargs)
+
+ @classmethod
+ def generate(cls, **kwargs):
+ obj = cls()
+ try:
+ kty = kwargs['kty']
+ gen = getattr(obj, '_generate_%s' % kty)
+ except (KeyError, AttributeError):
+ raise InvalidJWKType(kty)
+ gen(kwargs)
+ return obj
+
+ def generate_key(self, **params):
+ try:
+ kty = params.pop('generate')
+ gen = getattr(self, '_generate_%s' % kty)
+ except (KeyError, AttributeError):
+ raise InvalidJWKType(kty)
+
+ gen(params)
+
+ def _get_gen_size(self, params, default_size=None):
+ size = default_size
+ if 'size' in params:
+ size = params.pop('size')
+ elif 'alg' in params:
+ try:
+ from jwcrypto.jwa import JWA
+ alg = JWA.instantiate_alg(params['alg'])
+ except KeyError:
+ raise ValueError("Invalid 'alg' parameter")
+ size = alg.keysize
+ return size
+
+ def _generate_oct(self, params):
+ size = self._get_gen_size(params, 128)
+ key = os.urandom(size // 8)
+ params['kty'] = 'oct'
+ params['k'] = base64url_encode(key)
+ self.import_key(**params)
+
+ def _encode_int(self, i):
+ intg = hex(i).rstrip("L").lstrip("0x")
+ return base64url_encode(unhexlify((len(intg) % 2) * '0' + intg))
+
+ def _generate_RSA(self, params):
+ pubexp = 65537
+ size = self._get_gen_size(params, 2048)
+ if 'public_exponent' in params:
+ pubexp = params.pop('public_exponent')
+ key = rsa.generate_private_key(pubexp, size, default_backend())
+ self._import_pyca_pri_rsa(key, **params)
+
+ def _import_pyca_pri_rsa(self, key, **params):
+ pn = key.private_numbers()
+ 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, **params):
+ pn = key.public_numbers()
+ 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):
+ if name == 'P-256':
+ return ec.SECP256R1()
+ elif name == 'P-384':
+ return ec.SECP384R1()
+ elif name == 'P-521':
+ return ec.SECP521R1()
+ else:
+ raise InvalidJWKValue('Unknown Elliptic Curve Type')
+
+ def _generate_EC(self, params):
+ curve = 'P-256'
+ if 'curve' in params:
+ curve = params.pop('curve')
+ # 'curve' is for backwards compat, if 'crv' is defined it takes
+ # precedence
+ if 'crv' in params:
+ curve = params.pop('crv')
+ curve_name = self._get_curve_by_name(curve)
+ key = ec.generate_private_key(curve_name, default_backend())
+ self._import_pyca_pri_ec(key, **params)
+
+ def _import_pyca_pri_ec(self, key, **params):
+ pn = key.private_numbers()
+ 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, **params):
+ pn = key.public_numbers()
+ 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):
+ names = list(kwargs.keys())
+
+ for name in list(JWKParamsRegistry.keys()):
+ if name in kwargs:
+ self._params[name] = kwargs[name]
+ while name in names:
+ names.remove(name)
+
+ kty = self._params.get('kty', None)
+ if kty not in JWKTypesRegistry:
+ raise InvalidJWKType(kty)
+
+ for name in list(JWKValuesRegistry[kty].keys()):
+ if name in kwargs:
+ self._key[name] = kwargs[name]
+ 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:
+ self._unknown[name] = kwargs[name]
+
+ if len(self._key) == 0:
+ raise InvalidJWKValue('No Key Values found')
+
+ # check key_ops
+ if 'key_ops' in self._params:
+ for ko in self._params['key_ops']:
+ c = 0
+ for cko in self._params['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:
+ sigl = ['sign', 'verify']
+ encl = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey',
+ 'deriveKey', 'deriveBits']
+ if self._params['use'] == 'sig':
+ for op in encl:
+ if op in self._params['key_ops']:
+ raise InvalidJWKValue('Incompatible "use" and'
+ ' "key_ops" values specified at'
+ ' the same time')
+ elif self._params['use'] == 'enc':
+ for op in sigl:
+ if op in self._params['key_ops']:
+ raise InvalidJWKValue('Incompatible "use" and'
+ ' "key_ops" values specified at'
+ ' the same time')
+
+ def export(self, private_key=True):
+ """Exports the key in the standard JSON format.
+ Exports the key regardless of type, if private_key is False
+ and the key is_symmetric an exceptionis raised.
+
+ :param private_key(bool): Whether to export the private key.
+ Defaults to True.
+ """
+ if private_key is True:
+ # Use _export_all for backwards compatibility, as this
+ # function allows to export symmetrict keys too
+ return self._export_all()
+ else:
+ return self.export_public()
+
+ def export_public(self):
+ """Exports the public key in the standard JSON format.
+ It fails if one is not available like when this function
+ is called on a symmetric key.
+ """
+ if not self.has_public:
+ raise InvalidJWKType("No public key available")
+ pub = {}
+ preg = JWKParamsRegistry
+ for name in preg:
+ if preg[name][1] == 'Public':
+ if name in self._params:
+ pub[name] = self._params[name]
+ reg = JWKValuesRegistry[self._params['kty']]
+ for param in reg:
+ if reg[param][1] == 'Public':
+ pub[param] = self._key[param]
+ return json_encode(pub)
+
+ def _export_all(self):
+ d = dict()
+ d.update(self._params)
+ d.update(self._key)
+ d.update(self._unknown)
+ return json_encode(d)
+
+ def export_private(self):
+ """Export the private key in the standard JSON format.
+ It fails for a JWK that has only a public key or is symmetric.
+ """
+ if self.has_private:
+ return self._export_all()
+ raise InvalidJWKType("No private key available")
+
+ def export_symmetric(self):
+ if self.is_symmetric:
+ return self._export_all()
+ raise InvalidJWKType("Not a symmetric key")
+
+ @property
+ def has_public(self):
+ """Whether this JWK has an asymmetric Public key."""
+ if self.is_symmetric:
+ return False
+ reg = JWKValuesRegistry[self._params['kty']]
+ for value in reg:
+ if reg[value][1] == 'Public' and value in self._key:
+ return True
+
+ @property
+ def has_private(self):
+ """Whether this JWK has an asymmetric key Private key."""
+ if self.is_symmetric:
+ return False
+ reg = JWKValuesRegistry[self._params['kty']]
+ for value in reg:
+ if reg[value][1] == 'Private' and value in self._key:
+ return True
+ return False
+
+ @property
+ def is_symmetric(self):
+ """Whether this JWK is a symmetric key."""
+ return self.key_type == 'oct'
+
+ @property
+ def key_type(self):
+ """The Key type"""
+ return self._params.get('kty', None)
+
+ @property
+ def key_id(self):
+ """The Key ID.
+ Provided by the kid parameter if present, otherwise returns None.
+ """
+ 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.
+
+ :param arg: an optional curve name
+
+ :raises InvalidJWKType: the key is not an EC key.
+ :raises InvalidJWKValue: if the curve names is invalid.
+ """
+ k = self._key
+ if self._params['kty'] != 'EC':
+ raise InvalidJWKType('Not an EC key')
+ if arg and k['crv'] != arg:
+ raise InvalidJWKValue('Curve requested is "%s", but '
+ 'key curve is "%s"' % (arg, k['crv']))
+
+ return self._get_curve_by_name(k['crv'])
+
+ def _check_constraints(self, usage, operation):
+ use = self._params.get('use', None)
+ if use and use != usage:
+ raise InvalidJWKUsage(usage, use)
+ ops = self._params.get('key_ops', None)
+ if ops:
+ if not isinstance(ops, list):
+ ops = [ops]
+ if operation not in ops:
+ raise InvalidJWKOperation(operation, ops)
+ # TODO: check alg ?
+
+ 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 _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())
+ 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())
+ else:
+ raise NotImplementedError
+
+ def get_op_key(self, operation=None, arg=None):
+ """Get the key object associated to the requested opration.
+ For example the public RSA key for the 'verify' operation or
+ the private EC key for the 'decrypt' operation.
+
+ :param operation: The requested operation.
+ The valid set of operations is availble in the
+ :data:`JWKOperationsRegistry` registry.
+ :param arg: an optional, context specific, argument
+ For example a curve name.
+
+ :raises InvalidJWKOperation: if the operation is unknown or
+ not permitted with this key.
+ :raises InvalidJWKUsage: if the use constraints do not permit
+ the operation.
+ """
+ validops = self._params.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']
+ raise InvalidJWKOperation(operation, validops)
+ elif operation == 'sign':
+ self._check_constraints('sig', operation)
+ return self._get_private_key(arg)
+ elif operation == 'verify':
+ self._check_constraints('sig', operation)
+ return self._get_public_key(arg)
+ elif operation == 'encrypt' or operation == 'wrapKey':
+ self._check_constraints('enc', operation)
+ return self._get_public_key(arg)
+ elif operation == 'decrypt' or operation == 'unwrapKey':
+ self._check_constraints('enc', operation)
+ return self._get_private_key(arg)
+ else:
+ raise NotImplementedError
+
+ def import_from_pyca(self, key):
+ if isinstance(key, rsa.RSAPrivateKey):
+ self._import_pyca_pri_rsa(key)
+ elif isinstance(key, rsa.RSAPublicKey):
+ self._import_pyca_pub_rsa(key)
+ elif isinstance(key, ec.EllipticCurvePrivateKey):
+ self._import_pyca_pri_ec(key)
+ elif isinstance(key, ec.EllipticCurvePublicKey):
+ self._import_pyca_pub_ec(key)
+ else:
+ raise InvalidJWKValue('Unknown key object %r' % key)
+
+ def import_from_pem(self, data, password=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
+ public keys can be imported with this interface.
+
+ :param data(bytes): The data contained in a PEM file.
+ :param password(bytes): An optional password to unwrap the key.
+ """
+
+ try:
+ key = serialization.load_pem_private_key(
+ data, password=password, backend=default_backend())
+ except ValueError as e:
+ if password is not None:
+ raise e
+ try:
+ key = serialization.load_pem_public_key(
+ data, backend=default_backend())
+ except ValueError:
+ try:
+ cert = x509.load_pem_x509_certificate(
+ data, backend=default_backend())
+ key = cert.public_key()
+ except ValueError:
+ raise e
+
+ self.import_from_pyca(key)
+ self._params['kid'] = self.thumbprint()
+
+ def export_to_pem(self, private_key=False, password=False):
+ """Exports keys to a data buffer suitable to be stored as a PEM file.
+ Either the public or the private key can be exported to a PEM file.
+ For private keys the PKCS#8 format is used. If a password is provided
+ the best encryption method available as determined by the cryptography
+ module is used to wrap the key.
+
+ :param private_key: Whether the private key should be exported.
+ Defaults to `False` which means the public key is exported by default.
+ :param password(bytes): A password for wrapping the private key.
+ Defaults to False which will cause the operation to fail. To avoid
+ encryption the user must explicitly pass None, otherwise the user
+ needs to provide a password in a bytes buffer.
+ """
+ e = serialization.Encoding.PEM
+ if private_key:
+ if not self.has_private:
+ raise InvalidJWKType("No private key available")
+ f = serialization.PrivateFormat.PKCS8
+ if password is None:
+ a = serialization.NoEncryption()
+ elif isinstance(password, bytes):
+ a = serialization.BestAvailableEncryption(password)
+ elif password is False:
+ raise ValueError("The password must be None or a bytes string")
+ else:
+ raise TypeError("The password string must be bytes")
+ return self._get_private_key().private_bytes(
+ encoding=e, format=f, encryption_algorithm=a)
+ else:
+ if not self.has_public:
+ raise InvalidJWKType("No public key available")
+ f = serialization.PublicFormat.SubjectPublicKeyInfo
+ return self._get_public_key().public_bytes(encoding=e, format=f)
+
+ @classmethod
+ def from_pyca(cls, key):
+ obj = cls()
+ obj.import_from_pyca(key)
+ return obj
+
+ @classmethod
+ def from_pem(cls, data, password=None):
+ """Creates a key from PKCS#8 formatted data loaded from a PEM file.
+ See the function `import_from_pem` for details.
+
+ :param data(bytes): The data contained in a PEM file.
+ :param password(bytes): An optional password to unwrap the key.
+ """
+ obj = cls()
+ obj.import_from_pem(data, password)
+ 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):
+
+ def add(self, elem):
+ """Adds a JWK object to the set
+
+ :param elem: the JWK object to add.
+
+ :raises TypeError: if the object is not a JWK.
+ """
+ if not isinstance(elem, JWK):
+ raise TypeError('Only JWK objects are valid elements')
+ set.add(self, elem)
+
+
+class JWKSet(dict):
+ """A set of JWK objects.
+
+ Inherits from the standard 'dict' bultin type.
+ Creates a special key 'keys' that is of a type derived from 'set'
+ The 'keys' attribute accepts only :class:`jwcrypto.jwk.JWK` elements.
+ """
+ def __init__(self, *args, **kwargs):
+ super(JWKSet, self).__init__()
+ super(JWKSet, self).__setitem__('keys', _JWKkeys())
+ self.update(*args, **kwargs)
+
+ def __setitem__(self, key, val):
+ if key == 'keys':
+ self['keys'].add(val)
+ else:
+ super(JWKSet, self).__setitem__(key, val)
+
+ def update(self, *args, **kwargs):
+ for k, v in iteritems(dict(*args, **kwargs)):
+ self.__setitem__(k, v)
+
+ def add(self, elem):
+ self['keys'].add(elem)
+
+ def export(self, private_keys=True):
+ """Exports a RFC 7517 keyset using the standard JSON format
+
+ :param private_key(bool): Whether to export private keys.
+ Defaults to True.
+ """
+ exp_dict = dict()
+ for k, v in iteritems(self):
+ if k == 'keys':
+ keys = list()
+ for jwk in v:
+ keys.append(json_decode(jwk.export(private_keys)))
+ v = keys
+ exp_dict[k] = v
+ return json_encode(exp_dict)
+
+ def import_keyset(self, keyset):
+ """Imports a RFC 7517 keyset using the standard JSON format.
+
+ :param keyset: The RFC 7517 representation of a JOSE Keyset.
+ """
+ try:
+ jwkset = json_decode(keyset)
+ except:
+ raise InvalidJWKValue()
+
+ if 'keys' not in jwkset:
+ raise InvalidJWKValue()
+
+ for k, v in iteritems(jwkset):
+ if k == 'keys':
+ for jwk in v:
+ self['keys'].add(JWK(**jwk))
+ else:
+ self[k] = v
+
+ return self
+
+ @classmethod
+ def from_json(cls, keyset):
+ """Creates a RFC 7517 keyset from the standard JSON format.
+
+ :param keyset: The RFC 7517 representation of a JOSE Keyset.
+ """
+ obj = cls()
+ return obj.import_keyset(keyset)
+
+ def get_key(self, kid):
+ """Gets a key from the set.
+ :param kid: the 'kid' key identifier.
+ """
+ for jwk in self['keys']:
+ if jwk.key_id == kid:
+ return jwk
+ return None
diff --git a/jwcrypto/jws.py b/jwcrypto/jws.py
new file mode 100644
index 0000000..c3158a6
--- /dev/null
+++ b/jwcrypto/jws.py
@@ -0,0 +1,505 @@
+# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
+
+from jwcrypto.common import base64url_decode, base64url_encode
+from jwcrypto.common import json_decode, json_encode
+from jwcrypto.jwa import JWA
+from jwcrypto.jwk import JWK
+
+
+# RFC 7515 - 9.1
+# name: (description, supported?)
+JWSHeaderRegistry = {'alg': ('Algorithm', True),
+ 'jku': ('JWK Set URL', False),
+ 'jwk': ('JSON Web Key', False),
+ 'kid': ('Key ID', True),
+ 'x5u': ('X.509 URL', False),
+ 'x5c': ('X.509 Certificate Chain', False),
+ 'x5t': ('X.509 Certificate SHA-1 Thumbprint', False),
+ 'x5t#S256': ('X.509 Certificate SHA-256 Thumbprint',
+ False),
+ 'typ': ('Type', True),
+ 'cty': ('Content Type', True),
+ 'crit': ('Critical', True)}
+"""Registry of valid header parameters"""
+
+default_allowed_algs = [
+ 'HS256', 'HS384', 'HS512',
+ 'RS256', 'RS384', 'RS512',
+ 'ES256', 'ES384', 'ES512',
+ 'PS256', 'PS384', 'PS512']
+"""Default allowed algorithms"""
+
+
+class InvalidJWSSignature(Exception):
+ """Invalid JWS Signature.
+
+ This exception is raised when a signature cannot be validated.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Unknown Signature Verification Failure'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(InvalidJWSSignature, self).__init__(msg)
+
+
+class InvalidJWSObject(Exception):
+ """Invalid JWS Object.
+
+ This exception is raised when the JWS Object is invalid and/or
+ improperly formatted.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = 'Invalid JWS Object'
+ if message:
+ msg += ' [%s]' % message
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(InvalidJWSObject, self).__init__(msg)
+
+
+class InvalidJWSOperation(Exception):
+ """Invalid JWS Object.
+
+ This exception is raised when a requested operation cannot
+ be execute due to unsatisfied conditions.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = message
+ else:
+ msg = 'Unknown Operation Failure'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(InvalidJWSOperation, self).__init__(msg)
+
+
+class JWSCore(object):
+ """The inner JWS Core object.
+
+ This object SHOULD NOT be used directly, the JWS object should be
+ used instead as JWS perform necessary checks on the validity of
+ the object and requested operations.
+
+ """
+
+ def __init__(self, alg, key, header, payload, algs=None):
+ """Core JWS token handling.
+
+ :param alg: The algorithm used to produce the signature.
+ See RFC 7518
+ :param key: A (:class:`jwcrypto.jwk.JWK`) key of appropriate
+ type for the "alg" provided in the 'protected' json string.
+ :param header: A JSON string representing the protected header.
+ :param payload(bytes): An arbitrary value
+ :param algs: An optional list of allowed algorithms
+
+ :raises ValueError: if the key is not a :class:`JWK` object
+ :raises InvalidJWAAlgorithm: if the algorithm is not valid, is
+ unknown or otherwise not yet implemented.
+ """
+ self.alg = alg
+ self.engine = self._jwa(alg, algs)
+ if not isinstance(key, JWK):
+ raise ValueError('key is not a JWK object')
+ self.key = key
+
+ if header is not None:
+ if isinstance(header, dict):
+ header = json_encode(header)
+ self.protected = base64url_encode(header.encode('utf-8'))
+ else:
+ self.protected = ''
+ self.payload = base64url_encode(payload)
+
+ def _jwa(self, name, allowed):
+ if allowed is None:
+ allowed = default_allowed_algs
+ if name not in allowed:
+ raise InvalidJWSOperation('Algorithm not allowed')
+ return JWA.signing_alg(name)
+
+ def sign(self):
+ """Generates a signature"""
+ sigin = ('.'.join([self.protected, self.payload])).encode('utf-8')
+ signature = self.engine.sign(self.key, sigin)
+ return {'protected': self.protected,
+ 'payload': self.payload,
+ 'signature': base64url_encode(signature)}
+
+ def verify(self, signature):
+ """Verifies a signature
+
+ :raises InvalidJWSSignature: if the verification fails.
+ """
+ try:
+ sigin = ('.'.join([self.protected, self.payload])).encode('utf-8')
+ self.engine.verify(self.key, sigin, signature)
+ except Exception as e: # pylint: disable=broad-except
+ raise InvalidJWSSignature('Verification failed', repr(e))
+ return True
+
+
+class JWS(object):
+ """JSON Web Signature object
+
+ This object represent a JWS token.
+ """
+
+ def __init__(self, payload=None):
+ """Creates a JWS object.
+
+ :param payload(bytes): An arbitrary value (optional).
+ """
+ self.objects = dict()
+ if payload:
+ self.objects['payload'] = payload
+ self.verifylog = None
+ self._allowed_algs = None
+
+ def _check_crit(self, crit):
+ for k in crit:
+ if k not in JWSHeaderRegistry:
+ raise InvalidJWSSignature('Unknown critical header: '
+ '"%s"' % k)
+ else:
+ if not JWSHeaderRegistry[k][1]:
+ raise InvalidJWSSignature('Unsupported critical '
+ 'header: "%s"' % k)
+
+ @property
+ def allowed_algs(self):
+ """Allowed algorithms.
+
+ The list of allowed algorithms.
+ Can be changed by setting a list of algorithm names.
+ """
+
+ if self._allowed_algs:
+ return self._allowed_algs
+ else:
+ return default_allowed_algs
+
+ @allowed_algs.setter
+ def allowed_algs(self, algs):
+ if not isinstance(algs, list):
+ raise TypeError('Allowed Algs must be a list')
+ self._allowed_algs = algs
+
+ @property
+ def is_valid(self):
+ return self.objects.get('valid', False)
+
+ def _merge_headers(self, h1, h2):
+ for k in list(h1.keys()):
+ if k in h2:
+ raise InvalidJWSObject('Duplicate header: "%s"' % k)
+ h1.update(h2)
+ return h1
+
+ # TODO: support selecting key with 'kid' and passing in multiple keys
+ def _verify(self, alg, key, payload, signature, protected, header=None):
+ # verify it is a valid JSON object and keep a decode copy
+ if protected is not None:
+ p = json_decode(protected)
+ else:
+ p = dict()
+ if not isinstance(p, dict):
+ raise InvalidJWSSignature('Invalid Protected header')
+ # merge heders, and verify there are no duplicates
+ if header:
+ if not isinstance(header, dict):
+ raise InvalidJWSSignature('Invalid Unprotected header')
+ p = self._merge_headers(p, header)
+ # verify critical headers
+ # TODO: allow caller to specify list of headers it understands
+ if 'crit' in p:
+ self._check_crit(p['crit'])
+ # check 'alg' is present
+ if alg is None and 'alg' not in p:
+ raise InvalidJWSSignature('No "alg" in headers')
+ if alg:
+ if 'alg' in p and alg != p['alg']:
+ raise InvalidJWSSignature('"alg" mismatch, requested '
+ '"%s", found "%s"' % (alg,
+ p['alg']))
+ a = alg
+ else:
+ a = p['alg']
+
+ # the following will verify the "alg" is supported and the signature
+ # verifies
+ c = JWSCore(a, key, protected, payload, self._allowed_algs)
+ c.verify(signature)
+
+ def verify(self, key, alg=None):
+ """Verifies a JWS token.
+
+ :param key: The (:class:`jwcrypto.jwk.JWK`) verification key.
+ :param alg: The signing algorithm (optional). usually the algorithm
+ is known as it is provided with the JOSE Headers of the token.
+
+ :raises InvalidJWSSignature: if the verification fails.
+ """
+
+ self.verifylog = list()
+ self.objects['valid'] = False
+ obj = self.objects
+ if 'signature' in obj:
+ try:
+ self._verify(alg, key,
+ obj['payload'],
+ obj['signature'],
+ obj.get('protected', None),
+ obj.get('header', None))
+ obj['valid'] = True
+ except Exception as e: # pylint: disable=broad-except
+ self.verifylog.append('Failed: [%s]' % repr(e))
+
+ elif 'signatures' in obj:
+ for o in obj['signatures']:
+ try:
+ self._verify(alg, key,
+ obj['payload'],
+ o['signature'],
+ o.get('protected', None),
+ o.get('header', None))
+ # Ok if at least one verifies
+ obj['valid'] = True
+ except Exception as e: # pylint: disable=broad-except
+ self.verifylog.append('Failed: [%s]' % repr(e))
+ else:
+ raise InvalidJWSSignature('No signatures availble')
+
+ if not self.is_valid:
+ raise InvalidJWSSignature('Verification failed for all '
+ 'signatures' + repr(self.verifylog))
+
+ def deserialize(self, raw_jws, key=None, alg=None):
+ """Deserialize a JWS token.
+
+ NOTE: Destroys any current status and tries to import the raw
+ JWS provided.
+
+ :param raw_jws: a 'raw' JWS token (JSON Encoded or Compact
+ notation) string.
+ :param key: A (:class:`jwcrypto.jwk.JWK`) verification key (optional).
+ If a key is provided a verification step will be attempted after
+ the object is successfully deserialized.
+ :param alg: The signing algorithm (optional). usually the algorithm
+ is known as it is provided with the JOSE Headers of the token.
+
+ :raises InvalidJWSObject: if the raw object is an invaid JWS token.
+ :raises InvalidJWSSignature: if the verification fails.
+ """
+ self.objects = dict()
+ o = dict()
+ try:
+ try:
+ djws = json_decode(raw_jws)
+ o['payload'] = base64url_decode(str(djws['payload']))
+ if 'signatures' in djws:
+ o['signatures'] = list()
+ for s in djws['signatures']:
+ os = dict()
+ os['signature'] = base64url_decode(str(s['signature']))
+ if 'protected' in s:
+ p = base64url_decode(str(s['protected']))
+ os['protected'] = p.decode('utf-8')
+ if 'header' in s:
+ os['header'] = s['header']
+ o['signatures'].append(os)
+ else:
+ o['signature'] = base64url_decode(str(djws['signature']))
+ if 'protected' in djws:
+ p = base64url_decode(str(djws['protected']))
+ o['protected'] = p.decode('utf-8')
+ if 'header' in djws:
+ o['header'] = djws['header']
+
+ except ValueError:
+ c = raw_jws.split('.')
+ if len(c) != 3:
+ raise InvalidJWSObject('Unrecognized representation')
+ p = base64url_decode(str(c[0]))
+ if len(p) > 0:
+ o['protected'] = p.decode('utf-8')
+ o['payload'] = base64url_decode(str(c[1]))
+ o['signature'] = base64url_decode(str(c[2]))
+
+ self.objects = o
+
+ except Exception as e: # pylint: disable=broad-except
+ raise InvalidJWSObject('Invalid format', repr(e))
+
+ if key:
+ self.verify(key, alg)
+
+ def add_signature(self, key, alg=None, protected=None, header=None):
+ """Adds a new signature to the object.
+
+ :param key: A (:class:`jwcrypto.jwk.JWK`) key of appropriate for
+ the "alg" provided.
+ :param alg: An optional algorithm name. If already provided as an
+ element of the protected or unprotected header it can be safely
+ omitted.
+ :param potected: The Protected Header (optional)
+ :param header: The Unprotected Header (optional)
+
+ :raises InvalidJWSObject: if no payload has been set on the object.
+ :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.
+ :raises InvalidJWAAlgorithm: if the algorithm is not valid, is
+ unknown or otherwise not yet implemented.
+ """
+
+ if not self.objects.get('payload', None):
+ raise InvalidJWSObject('Missing Payload')
+
+ p = dict()
+ if protected:
+ if isinstance(protected, dict):
+ protected = json_encode(protected)
+ p = json_decode(protected)
+ # TODO: allow caller to specify list of headers it understands
+ if 'crit' in p:
+ self._check_crit(p['crit'])
+
+ if header:
+ if isinstance(header, dict):
+ header = json_encode(header)
+ h = json_decode(header)
+ p = self._merge_headers(p, h)
+
+ if 'alg' in p:
+ if alg is None:
+ alg = p['alg']
+ elif alg != p['alg']:
+ raise ValueError('"alg" value mismatch, specified "alg" '
+ 'does not match JOSE header value')
+
+ if alg is None:
+ raise ValueError('"alg" not specified')
+
+ c = JWSCore(alg, key, protected, self.objects['payload'])
+ sig = c.sign()
+
+ o = dict()
+ o['signature'] = base64url_decode(sig['signature'])
+ if protected:
+ o['protected'] = protected
+ if header:
+ o['header'] = h
+ o['valid'] = True
+
+ if 'signatures' in self.objects:
+ self.objects['signatures'].append(o)
+ elif 'signature' in self.objects:
+ self.objects['signatures'] = list()
+ n = dict()
+ n['signature'] = self.objects.pop('signature')
+ if 'protected' in self.objects:
+ n['protected'] = self.objects.pop('protected')
+ if 'header' in self.objects:
+ n['header'] = self.objects.pop('header')
+ if 'valid' in self.objects:
+ n['valid'] = self.objects.pop('valid')
+ self.objects['signatures'].append(n)
+ self.objects['signatures'].append(o)
+ else:
+ self.objects.update(o)
+
+ def serialize(self, compact=False):
+ """Serializes the object into a JWS token.
+
+ :param compact(boolean): if True generates the compact
+ representation, otherwise generates a standard JSON format.
+
+ :raises InvalidJWSOperation: if the object cannot serialized
+ with the compact representation and `compat` is True.
+ :raises InvalidJWSSignature: if no signature has been added
+ to the object, or no valid signature can be found.
+ """
+
+ if compact:
+ if 'signatures' in self.objects:
+ raise InvalidJWSOperation("Can't use compact encoding with "
+ "multiple signatures")
+ if 'signature' not in self.objects:
+ raise InvalidJWSSignature("No available signature")
+ if not self.objects.get('valid', False):
+ raise InvalidJWSSignature("No valid signature found")
+ if 'protected' in self.objects:
+ protected = base64url_encode(self.objects['protected'])
+ else:
+ protected = ''
+ return '.'.join([protected,
+ base64url_encode(self.objects['payload']),
+ base64url_encode(self.objects['signature'])])
+ else:
+ obj = self.objects
+ if 'signature' in obj:
+ if not obj.get('valid', False):
+ raise InvalidJWSSignature("No valid signature found")
+ sig = {'payload': base64url_encode(obj['payload']),
+ 'signature': base64url_encode(obj['signature'])}
+ if 'protected' in obj:
+ sig['protected'] = base64url_encode(obj['protected'])
+ if 'header' in obj:
+ sig['header'] = obj['header']
+ elif 'signatures' in obj:
+ sig = {'payload': base64url_encode(obj['payload']),
+ 'signatures': list()}
+ for o in obj['signatures']:
+ if not o.get('valid', False):
+ continue
+ s = {'signature': base64url_encode(o['signature'])}
+ if 'protected' in o:
+ s['protected'] = base64url_encode(o['protected'])
+ if 'header' in o:
+ s['header'] = o['header']
+ sig['signatures'].append(s)
+ if len(sig['signatures']) == 0:
+ raise InvalidJWSSignature("No valid signature found")
+ else:
+ raise InvalidJWSSignature("No available signature")
+ return json_encode(sig)
+
+ @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']
+
+ @property
+ def jose_header(self):
+ obj = self.objects
+ if 'signature' in obj:
+ jh = dict()
+ if 'protected' in obj:
+ p = json_decode(obj['protected'])
+ jh = self._merge_headers(jh, p)
+ jh = self._merge_headers(jh, obj.get('header', dict()))
+ return jh
+ elif 'signatures' in self.objects:
+ jhl = list()
+ for o in obj['signatures']:
+ jh = dict()
+ if 'protected' in obj:
+ p = json_decode(o['protected'])
+ jh = self._merge_headers(jh, p)
+ jh = self._merge_headers(jh, o.get('header', dict()))
+ jhl.append(jh)
+ return jhl
+ else:
+ raise InvalidJWSOperation("JOSE Header(s) not available")
diff --git a/jwcrypto/jwt.py b/jwcrypto/jwt.py
new file mode 100644
index 0000000..3df7da6
--- /dev/null
+++ b/jwcrypto/jwt.py
@@ -0,0 +1,496 @@
+# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
+
+import time
+import uuid
+
+from six import string_types
+
+from jwcrypto.common import json_decode, json_encode
+from jwcrypto.jwe import JWE
+from jwcrypto.jwk import JWK, JWKSet
+from jwcrypto.jws import JWS
+
+
+# RFC 7519 - 4.1
+# name: description
+JWTClaimsRegistry = {'iss': 'Issuer',
+ 'sub': 'Subject',
+ 'aud': 'Audience',
+ 'exp': 'Expiration Time',
+ 'nbf': 'Not Before',
+ 'iat': 'Issued At',
+ 'jti': 'JWT ID'}
+
+
+class JWTExpired(Exception):
+ """Json Web Token is expired.
+
+ This exception is raised when a token is expired accoring to its claims.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Token expired'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(JWTExpired, self).__init__(msg)
+
+
+class JWTNotYetValid(Exception):
+ """Json Web Token is not yet valid.
+
+ This exception is raised when a token is not valid yet according to its
+ claims.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Token not yet valid'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(JWTNotYetValid, self).__init__(msg)
+
+
+class JWTMissingClaim(Exception):
+ """Json Web Token claim is invalid.
+
+ This exception is raised when a claim does not match the expected value.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Invalid Claim Value'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(JWTMissingClaim, self).__init__(msg)
+
+
+class JWTInvalidClaimValue(Exception):
+ """Json Web Token claim is invalid.
+
+ This exception is raised when a claim does not match the expected value.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Invalid Claim Value'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(JWTInvalidClaimValue, self).__init__(msg)
+
+
+class JWTInvalidClaimFormat(Exception):
+ """Json Web Token claim format is invalid.
+
+ This exception is raised when a claim is not in a valid format.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Invalid Claim Format'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(JWTInvalidClaimFormat, self).__init__(msg)
+
+
+class JWTMissingKeyID(Exception):
+ """Json Web Token is missing key id.
+
+ This exception is raised when trying to decode a JWT with a key set
+ that does not have a kid value in its header.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Missing Key ID'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(JWTMissingKeyID, self).__init__(msg)
+
+
+class JWTMissingKey(Exception):
+ """Json Web Token is using a key not in the key set.
+
+ This exception is raised if the key that was used is not available
+ in the passed key set.
+ """
+
+ def __init__(self, message=None, exception=None):
+ msg = None
+ if message:
+ msg = str(message)
+ else:
+ msg = 'Missing Key'
+ if exception:
+ msg += ' {%s}' % str(exception)
+ super(JWTMissingKey, self).__init__(msg)
+
+
+class JWT(object):
+ """JSON Web token object
+
+ This object represent a generic token.
+ """
+
+ def __init__(self, header=None, claims=None, jwt=None, key=None,
+ algs=None, default_claims=None, check_claims=None):
+ """Creates a JWT object.
+
+ :param header: A dict or a JSON string with the JWT Header data.
+ :param claims: A dict or a string withthe JWT Claims data.
+ :param jwt: a 'raw' JWT token
+ :param key: A (:class:`jwcrypto.jwk.JWK`) key to deserialize
+ the token. A (:class:`jwcrypt.jwk.JWKSet`) can also be used.
+ :param algs: An optional list of allowed algorithms
+ :param default_claims: An optional dict with default values for
+ registred claims. A None value for NumericDate type claims
+ will cause generation according to system time. Only the values
+ fro RFC 7519 - 4.1 are evaluated.
+ :param check_claims: An optional dict of claims that must be
+ present in the token, if the value is not None the claim must
+ match exactly.
+
+ Note: either the header,claims or jwt,key parameters should be
+ provided as a deserialization operation (which occurs if the jwt
+ is provided will wipe any header os claim provided by setting
+ those obtained from the deserialization of the jwt token.
+
+ Note: if check_claims is not provided the 'exp' and 'nbf' claims
+ are checked if they are set on the token but not enforced if not
+ set. Any other RFC 7519 registered claims are checked only for
+ format conformance.
+ """
+
+ self._header = None
+ self._claims = None
+ self._token = None
+ self._algs = algs
+ self._reg_claims = None
+ self._check_claims = None
+ self._leeway = 60 # 1 minute clock skew allowed
+ self._validity = 600 # 10 minutes validity (up to 11 with leeway)
+
+ if header:
+ self.header = header
+
+ if default_claims is not None:
+ self._reg_claims = default_claims
+
+ if check_claims is not None:
+ self._check_claims = check_claims
+
+ if claims:
+ self.claims = claims
+
+ if jwt is not None:
+ self.deserialize(jwt, key)
+
+ @property
+ def header(self):
+ if self._header is None:
+ raise KeyError("'header' not set")
+ return self._header
+
+ @header.setter
+ def header(self, h):
+ if isinstance(h, dict):
+ self._header = json_encode(h)
+ else:
+ self._header = h
+
+ @property
+ def claims(self):
+ if self._claims is None:
+ raise KeyError("'claims' not set")
+ return self._claims
+
+ @claims.setter
+ def claims(self, c):
+ if isinstance(c, dict):
+ self._add_default_claims(c)
+ self._claims = json_encode(c)
+ else:
+ self._claims = c
+
+ @property
+ def token(self):
+ return self._token
+
+ @token.setter
+ def token(self, t):
+ if isinstance(t, JWS) or isinstance(t, JWE) or isinstance(t, JWT):
+ self._token = t
+ else:
+ raise TypeError("Invalid token type, must be one of JWS,JWE,JWT")
+
+ @property
+ def leeway(self):
+ return self._leeway
+
+ @leeway.setter
+ def leeway(self, l):
+ self._leeway = int(l)
+
+ @property
+ def validity(self):
+ return self._validity
+
+ @validity.setter
+ def validity(self, v):
+ self._validity = int(v)
+
+ def _add_optional_claim(self, name, claims):
+ if name in claims:
+ return
+ val = self._reg_claims.get(name, None)
+ if val is not None:
+ claims[name] = val
+
+ def _add_time_claim(self, name, claims, defval):
+ if name in claims:
+ return
+ if name in self._reg_claims:
+ if self._reg_claims[name] is None:
+ claims[name] = defval
+ else:
+ claims[name] = self._reg_claims[name]
+
+ def _add_jti_claim(self, claims):
+ if 'jti' in claims or 'jti' not in self._reg_claims:
+ return
+ claims['jti'] = uuid.uuid4()
+
+ def _add_default_claims(self, claims):
+ if self._reg_claims is None:
+ return
+
+ now = int(time.time())
+ self._add_optional_claim('iss', claims)
+ self._add_optional_claim('sub', claims)
+ self._add_optional_claim('aud', claims)
+ self._add_time_claim('exp', claims, now + self.validity)
+ self._add_time_claim('nbf', claims, now)
+ self._add_time_claim('iat', claims, now)
+ self._add_jti_claim(claims)
+
+ def _check_string_claim(self, name, claims):
+ if name not in claims:
+ return
+ if not isinstance(claims[name], string_types):
+ 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):
+ raise JWTInvalidClaimFormat(
+ "Claim %s contains non StringOrURI types" % (name, ))
+ elif not isinstance(claims[name], string_types):
+ raise JWTInvalidClaimFormat(
+ "Claim %s is not a StringOrURI type" % (name, ))
+
+ def _check_integer_claim(self, name, claims):
+ if name not in claims:
+ return
+ try:
+ int(claims[name])
+ except ValueError:
+ raise JWTInvalidClaimFormat(
+ "Claim %s is not an integer" % (name, ))
+
+ def _check_exp(self, claim, limit, leeway):
+ if claim < limit - leeway:
+ raise JWTExpired('Expired at %d, time: %d(leeway: %d)' % (
+ claim, limit, leeway))
+
+ def _check_nbf(self, claim, limit, leeway):
+ if claim > limit + leeway:
+ raise JWTNotYetValid('Valid from %d, time: %d(leeway: %d)' % (
+ claim, limit, leeway))
+
+ def _check_default_claims(self, claims):
+ self._check_string_claim('iss', claims)
+ self._check_string_claim('sub', claims)
+ self._check_array_or_string_claim('aud', claims)
+ self._check_integer_claim('exp', claims)
+ self._check_integer_claim('nbf', claims)
+ self._check_integer_claim('iat', claims)
+ self._check_string_claim('jti', claims)
+
+ if self._check_claims is None:
+ if 'exp' in claims:
+ self._check_exp(claims['exp'], time.time(), self._leeway)
+ if 'nbf' in claims:
+ self._check_nbf(claims['nbf'], time.time(), self._leeway)
+
+ def _check_provided_claims(self):
+ # check_claims can be set to False to skip any check
+ if self._check_claims is False:
+ return
+
+ try:
+ claims = json_decode(self.claims)
+ if not isinstance(claims, dict):
+ raise ValueError()
+ except ValueError:
+ if self._check_claims is not None:
+ raise JWTInvalidClaimFormat(
+ "Claims check requested but claims is not a json dict")
+ return
+
+ self._check_default_claims(claims)
+
+ if self._check_claims is None:
+ return
+
+ for name, value in self._check_claims.items():
+ if name not in claims:
+ raise JWTMissingClaim("Claim %s is missing" % (name, ))
+
+ if name in ['iss', 'sub', 'jti']:
+ if value is not None and value != claims[name]:
+ raise JWTInvalidClaimValue(
+ "Invalid '%s' value. Expected '%s' got '%s'" % (
+ name, value, claims[name]))
+
+ elif name == 'aud':
+ if value is not None:
+ if value == claims[name]:
+ continue
+ if isinstance(claims[name], list):
+ if value in claims[name]:
+ continue
+ raise JWTInvalidClaimValue(
+ "Invalid '%s' value. Expected '%s' in '%s'" % (
+ name, value, claims[name]))
+
+ elif name == 'exp':
+ if value is not None:
+ self._check_exp(claims[name], value, 0)
+ else:
+ self._check_exp(claims[name], time.time(), self._leeway)
+
+ elif name == 'nbf':
+ if value is not None:
+ self._check_nbf(claims[name], value, 0)
+ else:
+ self._check_nbf(claims[name], time.time(), self._leeway)
+
+ else:
+ if value is not None and value != claims[name]:
+ raise JWTInvalidClaimValue(
+ "Invalid '%s' value. Expected '%d' got '%d'" % (
+ name, value, claims[name]))
+
+ def make_signed_token(self, key):
+ """Signs the payload.
+
+ Creates a JWS token with the header as the JWS protected header and
+ the claims as the payload. See (:class:`jwcrypto.jws.JWS`) for
+ details on the exceptions that may be reaised.
+
+ :param key: A (:class:`jwcrypto.jwk.JWK`) key.
+ """
+
+ t = JWS(self.claims)
+ t.add_signature(key, protected=self.header)
+ self.token = t
+
+ def make_encrypted_token(self, key):
+ """Encrypts the payload.
+
+ Creates a JWE token with the header as the JWE protected header and
+ the claims as the plaintext. See (:class:`jwcrypto.jwe.JWE`) for
+ details on the exceptions that may be reaised.
+
+ :param key: A (:class:`jwcrypto.jwk.JWK`) key.
+ """
+
+ t = JWE(self.claims, self.header)
+ t.add_recipient(key)
+ self.token = t
+
+ def deserialize(self, jwt, key=None):
+ """Deserialize a JWT token.
+
+ NOTE: Destroys any current status and tries to import the raw
+ token provided.
+
+ :param jwt: a 'raw' JWT token.
+ :param key: A (:class:`jwcrypto.jwk.JWK`) verification or
+ decryption key, or a (:class:`jwcrypt.jwk.JWKSet`) that
+ contains a key indexed by the 'kid' header.
+ """
+ c = jwt.count('.')
+ if c == 2:
+ self.token = JWS()
+ elif c == 4:
+ self.token = JWE()
+ else:
+ raise ValueError("Token format unrecognized")
+
+ # Apply algs restrictions if any, before performing any operation
+ if self._algs:
+ self.token.allowed_algs = self._algs
+
+ # now deserialize and also decrypt/verify (or raise) if we
+ # have a key
+ if key is None:
+ self.token.deserialize(jwt, None)
+ elif isinstance(key, JWK):
+ self.token.deserialize(jwt, key)
+ elif isinstance(key, JWKSet):
+ self.token.deserialize(jwt, None)
+ if 'kid' not in self.token.jose_header:
+ raise JWTMissingKeyID('No key ID in JWT header')
+
+ token_key = key.get_key(self.token.jose_header['kid'])
+ if not token_key:
+ raise JWTMissingKey('Key ID %s not in key set'
+ % self.token.jose_header['kid'])
+
+ if isinstance(self.token, JWE):
+ self.token.decrypt(token_key)
+ elif isinstance(self.token, JWS):
+ self.token.verify(token_key)
+ else:
+ raise RuntimeError("Unknown Token Type")
+ else:
+ raise ValueError("Unrecognized Key Type")
+
+ if key is not None:
+ self.header = self.token.jose_header
+ self.claims = self.token.payload.decode('utf-8')
+ self._check_provided_claims()
+
+ def serialize(self, compact=True):
+ """Serializes the object into a JWS token.
+
+ :param compact(boolean): must be True.
+
+ Note: the compact parameter is provided for general compatibility
+ with the serialize() functions of :class:`jwcrypto.jws.JWS` and
+ :class:`jwcrypto.jwe.JWE` so that these objects can all be used
+ interchangeably. However the only valid JWT representtion is the
+ compact representation.
+ """
+ return self.token.serialize(compact)
diff --git a/jwcrypto/tests-cookbook.py b/jwcrypto/tests-cookbook.py
new file mode 100644
index 0000000..40b8d36
--- /dev/null
+++ b/jwcrypto/tests-cookbook.py
@@ -0,0 +1,1293 @@
+# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
+
+import unittest
+
+from jwcrypto import jwe
+from jwcrypto import jwk
+from jwcrypto import jws
+from jwcrypto.common import base64url_decode, base64url_encode
+from jwcrypto.common import json_decode, json_encode
+
+# Based on: RFC 7520
+
+EC_Public_Key_3_1 = {
+ "kty": "EC",
+ "kid": "bilbo.baggins@hobbiton.example",
+ "use": "sig",
+ "crv": "P-521",
+ "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9"
+ "A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
+ "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVy"
+ "SsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1"}
+
+EC_Private_Key_3_2 = {
+ "kty": "EC",
+ "kid": "bilbo.baggins@hobbiton.example",
+ "use": "sig",
+ "crv": "P-521",
+ "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9"
+ "A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
+ "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVy"
+ "SsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1",
+ "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zb"
+ "KipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"}
+
+RSA_Public_Key_3_3 = {
+ "kty": "RSA",
+ "kid": "bilbo.baggins@hobbiton.example",
+ "use": "sig",
+ "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT"
+ "-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV"
+ "wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-"
+ "oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde"
+ "3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC"
+ "LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g"
+ "HdrNP5zw",
+ "e": "AQAB"}
+
+RSA_Private_Key_3_4 = {
+ "kty": "RSA",
+ "kid": "bilbo.baggins@hobbiton.example",
+ "use": "sig",
+ "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT"
+ "-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV"
+ "wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-"
+ "oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde"
+ "3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC"
+ "LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g"
+ "HdrNP5zw",
+ "e": "AQAB",
+ "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78e"
+ "iZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRld"
+ "Y7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-b"
+ "MwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU"
+ "6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDj"
+ "d18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOc"
+ "OpBrQzwQ",
+ "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nR"
+ "aO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmG"
+ "peNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8"
+ "bUq0k",
+ "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT"
+ "8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7an"
+ "V5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0"
+ "s7pFc",
+ "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q"
+ "1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn"
+ "-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX"
+ "59ehik",
+ "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pEr"
+ "AMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJK"
+ "bi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdK"
+ "T1cYF8",
+ "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-N"
+ "ZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDh"
+ "jJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpP"
+ "z8aaI4"}
+
+Symmetric_Key_MAC_3_5 = {
+ "kty": "oct",
+ "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037",
+ "use": "sig",
+ "alg": "HS256",
+ "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"}
+
+Symmetric_Key_Enc_3_6 = {
+ "kty": "oct",
+ "kid": "1e571774-2e08-40da-8308-e8d68773842d",
+ "use": "enc",
+ "alg": "A256GCM",
+ "k": "AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8"}
+
+Payload_plaintext_b64_4 = \
+ "SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH" + \
+ "lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk" + \
+ "b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm" + \
+ "UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"
+
+# 4.1
+JWS_Protected_Header_4_1_2 = \
+ "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX" + \
+ "hhbXBsZSJ9"
+
+JWS_Signature_4_1_2 = \
+ "MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK" + \
+ "ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J" + \
+ "IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w" + \
+ "W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP" + \
+ "xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f" + \
+ "cIe8u9ipH84ogoree7vjbU5y18kDquDg"
+
+JWS_compact_4_1_3 = \
+ "%s.%s.%s" % (JWS_Protected_Header_4_1_2,
+ Payload_plaintext_b64_4,
+ JWS_Signature_4_1_2)
+
+JWS_general_4_1_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "signatures": [{
+ "protected": JWS_Protected_Header_4_1_2,
+ "signature": JWS_Signature_4_1_2}]}
+
+JWS_flattened_4_1_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "protected": JWS_Protected_Header_4_1_2,
+ "signature": JWS_Signature_4_1_2}
+
+# 4.2
+JWS_Protected_Header_4_2_2 = \
+ "eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX" + \
+ "hhbXBsZSJ9"
+
+JWS_Signature_4_2_2 = \
+ "cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2I" + \
+ "pN6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXU" + \
+ "vdvWXzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRX" + \
+ "e8P_ijQ7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT" + \
+ "0qI0n6uiP1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a" + \
+ "6GYmJUAfmWjwZ6oD4ifKo8DYM-X72Eaw"
+
+JWS_compact_4_2_3 = \
+ "%s.%s.%s" % (JWS_Protected_Header_4_2_2,
+ Payload_plaintext_b64_4,
+ JWS_Signature_4_2_2)
+
+JWS_general_4_2_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "signatures": [{
+ "protected": JWS_Protected_Header_4_2_2,
+ "signature": JWS_Signature_4_2_2}]}
+
+JWS_flattened_4_2_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "protected": JWS_Protected_Header_4_2_2,
+ "signature": JWS_Signature_4_2_2}
+
+# 4.3
+JWS_Protected_Header_4_3_2 = \
+ "eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX" + \
+ "hhbXBsZSJ9"
+
+JWS_Signature_4_3_2 = \
+ "AE_R_YZCChjn4791jSQCrdPZCNYqHXCTZH0-JZGYNlaAjP2kqaluUIIUnC9qvb" + \
+ "u9Plon7KRTzoNEuT4Va2cmL1eJAQy3mtPBu_u_sDDyYjnAMDxXPn7XrT0lw-kv" + \
+ "AD890jl8e2puQens_IEKBpHABlsbEPX6sFY8OcGDqoRuBomu9xQ2"
+
+JWS_compact_4_3_3 = \
+ "%s.%s.%s" % (JWS_Protected_Header_4_3_2,
+ Payload_plaintext_b64_4,
+ JWS_Signature_4_3_2)
+
+JWS_general_4_3_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "signatures": [{
+ "protected": JWS_Protected_Header_4_3_2,
+ "signature": JWS_Signature_4_3_2}]}
+
+JWS_flattened_4_3_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "protected": JWS_Protected_Header_4_3_2,
+ "signature": JWS_Signature_4_3_2}
+
+# 4.4
+JWS_Protected_Header_4_4_2 = \
+ "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW" + \
+ "VlZjMxNGJjNzAzNyJ9"
+
+JWS_Signature_4_4_2 = "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"
+
+JWS_compact_4_4_3 = \
+ "%s.%s.%s" % (JWS_Protected_Header_4_4_2,
+ Payload_plaintext_b64_4,
+ JWS_Signature_4_4_2)
+
+JWS_general_4_4_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "signatures": [{
+ "protected": JWS_Protected_Header_4_4_2,
+ "signature": JWS_Signature_4_4_2}]}
+
+JWS_flattened_4_4_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "protected": JWS_Protected_Header_4_4_2,
+ "signature": JWS_Signature_4_4_2}
+
+# 4.5 - TBD, see Issue #4
+
+# 4.6
+JWS_Protected_Header_4_6_2 = "eyJhbGciOiJIUzI1NiJ9"
+
+JWS_Unprotected_Header_4_6_2 = {"kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037"}
+
+JWS_Signature_4_6_2 = "bWUSVaxorn7bEF1djytBd0kHv70Ly5pvbomzMWSOr20"
+
+JWS_general_4_6_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "signatures": [{
+ "protected": JWS_Protected_Header_4_6_2,
+ "header": JWS_Unprotected_Header_4_6_2,
+ "signature": JWS_Signature_4_6_2}]}
+
+JWS_flattened_4_6_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "protected": JWS_Protected_Header_4_6_2,
+ "header": JWS_Unprotected_Header_4_6_2,
+ "signature": JWS_Signature_4_6_2}
+
+# 4.7
+JWS_Unprotected_Header_4_7_2 = {"alg": "HS256",
+ "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037"}
+
+JWS_Signature_4_7_2 = "xuLifqLGiblpv9zBpuZczWhNj1gARaLV3UxvxhJxZuk"
+
+JWS_general_4_7_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "signatures": [{
+ "header": JWS_Unprotected_Header_4_7_2,
+ "signature": JWS_Signature_4_7_2}]}
+
+JWS_flattened_4_7_3 = {
+ "payload": Payload_plaintext_b64_4,
+ "header": JWS_Unprotected_Header_4_7_2,
+ "signature": JWS_Signature_4_7_2}
+
+# 4.8
+JWS_Protected_Header_4_8_2 = "eyJhbGciOiJSUzI1NiJ9"
+
+JWS_Unprotected_Header_4_8_2 = {"kid": "bilbo.baggins@hobbiton.example"}
+
+JWS_Signature_4_8_2 = \
+ "MIsjqtVlOpa71KE-Mss8_Nq2YH4FGhiocsqrgi5NvyG53uoimic1tcMdSg-qpt" + \
+ "rzZc7CG6Svw2Y13TDIqHzTUrL_lR2ZFcryNFiHkSw129EghGpwkpxaTn_THJTC" + \
+ "glNbADko1MZBCdwzJxwqZc-1RlpO2HibUYyXSwO97BSe0_evZKdjvvKSgsIqjy" + \
+ "tKSeAMbhMBdMma622_BG5t4sdbuCHtFjp9iJmkio47AIwqkZV1aIZsv33uPUqB" + \
+ "BCXbYoQJwt7mxPftHmNlGoOSMxR_3thmXTCm4US-xiNOyhbm8afKK64jU6_TPt" + \
+ "QHiJeQJxz9G3Tx-083B745_AfYOnlC9w"
+
+JWS_Unprotected_Header_4_8_3 = {"alg": "ES512",
+ "kid": "bilbo.baggins@hobbiton.example"}
+
+JWS_Signature_4_8_3 = \
+ "ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHjwDO1muhcdCoFZFFjfISu0Cdkn9Yb" + \
+ "dlmi54ho0x924DUz8sK7ZXkhc7AFM8ObLfTvNCrqcI3Jkl2U5IX3utNhODH6v7" + \
+ "xgy1Qahsn0fyb4zSAkje8bAWz4vIfj5pCMYxxm4fgV3q7ZYhm5eD"
+
+JWS_Protected_Header_4_8_4 = \
+ "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW" + \
+ "VlZjMxNGJjNzAzNyJ9"
+
+JWS_Signature_4_8_4 = "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"
+
+JWS_general_4_8_5 = {
+ "payload": Payload_plaintext_b64_4,
+ "signatures": [
+ {"protected": JWS_Protected_Header_4_8_2,
+ "header": JWS_Unprotected_Header_4_8_2,
+ "signature": JWS_Signature_4_8_2},
+ {"header": JWS_Unprotected_Header_4_8_3,
+ "signature": JWS_Signature_4_8_3},
+ {"protected": JWS_Protected_Header_4_8_4,
+ "signature": JWS_Signature_4_8_4}]}
+
+
+class Cookbook08JWSTests(unittest.TestCase):
+
+ def test_4_1_signing(self):
+ plaintext = base64url_decode(Payload_plaintext_b64_4)
+ protected = \
+ base64url_decode(JWS_Protected_Header_4_1_2).decode('utf-8')
+ pub_key = jwk.JWK(**RSA_Public_Key_3_3)
+ pri_key = jwk.JWK(**RSA_Private_Key_3_4)
+ s = jws.JWS(payload=plaintext)
+ s.add_signature(pri_key, None, protected)
+ self.assertEqual(JWS_compact_4_1_3, s.serialize(compact=True))
+ s.deserialize(json_encode(JWS_general_4_1_3), pub_key)
+ s.deserialize(json_encode(JWS_flattened_4_1_3), pub_key)
+
+ def test_4_2_signing(self):
+ plaintext = base64url_decode(Payload_plaintext_b64_4)
+ protected = \
+ base64url_decode(JWS_Protected_Header_4_2_2).decode('utf-8')
+ pub_key = jwk.JWK(**RSA_Public_Key_3_3)
+ pri_key = jwk.JWK(**RSA_Private_Key_3_4)
+ s = jws.JWS(payload=plaintext)
+ s.add_signature(pri_key, None, protected)
+ # Can't compare signature with reference because RSASSA-PSS uses
+ # random nonces every time a signature is generated.
+ sig = s.serialize()
+ s.deserialize(sig, pub_key)
+ # Just deserialize each example form
+ s.deserialize(JWS_compact_4_2_3, pub_key)
+ s.deserialize(json_encode(JWS_general_4_2_3), pub_key)
+ s.deserialize(json_encode(JWS_flattened_4_2_3), pub_key)
+
+ def test_4_3_signing(self):
+ plaintext = base64url_decode(Payload_plaintext_b64_4)
+ protected = \
+ base64url_decode(JWS_Protected_Header_4_3_2).decode('utf-8')
+ pub_key = jwk.JWK(**EC_Public_Key_3_1)
+ pri_key = jwk.JWK(**EC_Private_Key_3_2)
+ s = jws.JWS(payload=plaintext)
+ s.add_signature(pri_key, None, protected)
+ # Can't compare signature with reference because ECDSA uses
+ # random nonces every time a signature is generated.
+ sig = s.serialize()
+ s.deserialize(sig, pub_key)
+ # Just deserialize each example form
+ s.deserialize(JWS_compact_4_3_3, pub_key)
+ s.deserialize(json_encode(JWS_general_4_3_3), pub_key)
+ s.deserialize(json_encode(JWS_flattened_4_3_3), pub_key)
+
+ def test_4_4_signing(self):
+ plaintext = base64url_decode(Payload_plaintext_b64_4)
+ protected = \
+ base64url_decode(JWS_Protected_Header_4_4_2).decode('utf-8')
+ key = jwk.JWK(**Symmetric_Key_MAC_3_5)
+ s = jws.JWS(payload=plaintext)
+ s.add_signature(key, None, protected)
+ sig = s.serialize(compact=True)
+ s.deserialize(sig, key)
+ self.assertEqual(sig, JWS_compact_4_4_3)
+ # Just deserialize each example form
+ s.deserialize(JWS_compact_4_4_3, key)
+ s.deserialize(json_encode(JWS_general_4_4_3), key)
+ s.deserialize(json_encode(JWS_flattened_4_4_3), key)
+
+ def test_4_6_signing(self):
+ plaintext = base64url_decode(Payload_plaintext_b64_4)
+ protected = \
+ base64url_decode(JWS_Protected_Header_4_6_2).decode('utf-8')
+ header = json_encode(JWS_Unprotected_Header_4_6_2)
+ key = jwk.JWK(**Symmetric_Key_MAC_3_5)
+ s = jws.JWS(payload=plaintext)
+ s.add_signature(key, None, protected, header)
+ sig = s.serialize()
+ s.deserialize(sig, key)
+ self.assertEqual(json_decode(sig), JWS_flattened_4_6_3)
+ # Just deserialize each example form
+ s.deserialize(json_encode(JWS_general_4_6_3), key)
+ s.deserialize(json_encode(JWS_flattened_4_6_3), key)
+
+ def test_4_7_signing(self):
+ plaintext = base64url_decode(Payload_plaintext_b64_4)
+ header = json_encode(JWS_Unprotected_Header_4_7_2)
+ key = jwk.JWK(**Symmetric_Key_MAC_3_5)
+ s = jws.JWS(payload=plaintext)
+ s.add_signature(key, None, None, header)
+ sig = s.serialize()
+ s.deserialize(sig, key)
+ self.assertEqual(json_decode(sig), JWS_flattened_4_7_3)
+ # Just deserialize each example form
+ s.deserialize(json_encode(JWS_general_4_7_3), key)
+ s.deserialize(json_encode(JWS_flattened_4_7_3), key)
+
+ def test_4_8_signing(self):
+ plaintext = base64url_decode(Payload_plaintext_b64_4)
+ s = jws.JWS(payload=plaintext)
+ # 4_8_2
+ protected = \
+ base64url_decode(JWS_Protected_Header_4_8_2).decode('utf-8')
+ header = json_encode(JWS_Unprotected_Header_4_8_2)
+ pri_key = jwk.JWK(**RSA_Private_Key_3_4)
+ s.add_signature(pri_key, None, protected, header)
+ # 4_8_3
+ header = json_encode(JWS_Unprotected_Header_4_8_3)
+ pri_key = jwk.JWK(**EC_Private_Key_3_2)
+ s.add_signature(pri_key, None, None, header)
+ # 4_8_4
+ protected = \
+ base64url_decode(JWS_Protected_Header_4_8_4).decode('utf-8')
+ sym_key = jwk.JWK(**Symmetric_Key_MAC_3_5)
+ s.add_signature(sym_key, None, protected)
+ sig = s.serialize()
+ # Can't compare signature with reference because ECDSA uses
+ # random nonces every time a signature is generated.
+ rsa_key = jwk.JWK(**RSA_Public_Key_3_3)
+ ec_key = jwk.JWK(**EC_Public_Key_3_1)
+ s.deserialize(sig, rsa_key)
+ s.deserialize(sig, ec_key)
+ s.deserialize(sig, sym_key)
+ # Just deserialize each example form
+ s.deserialize(json_encode(JWS_general_4_8_5), rsa_key)
+ s.deserialize(json_encode(JWS_general_4_8_5), ec_key)
+ s.deserialize(json_encode(JWS_general_4_8_5), sym_key)
+
+
+# 5.0
+Payload_plaintext_5 = \
+ b"You can trust us to stick with you through thick and " + \
+ b"thin\xe2\x80\x93to the bitter end. And you can trust us to " + \
+ b"keep any secret of yours\xe2\x80\x93closer than you keep it " + \
+ b"yourself. But you cannot trust us to let you face trouble " + \
+ b"alone, and go off without a word. We are your friends, Frodo."
+
+# 5.1
+RSA_key_5_1_1 = {
+ "kty": "RSA",
+ "kid": "frodo.baggins@hobbiton.example",
+ "use": "enc",
+ "n": "maxhbsmBtdQ3CNrKvprUE6n9lYcregDMLYNeTAWcLj8NnPU9XIYegT"
+ "HVHQjxKDSHP2l-F5jS7sppG1wgdAqZyhnWvXhYNvcM7RfgKxqNx_xAHx"
+ "6f3yy7s-M9PSNCwPC2lh6UAkR4I00EhV9lrypM9Pi4lBUop9t5fS9W5U"
+ "NwaAllhrd-osQGPjIeI1deHTwx-ZTHu3C60Pu_LJIl6hKn9wbwaUmA4c"
+ "R5Bd2pgbaY7ASgsjCUbtYJaNIHSoHXprUdJZKUMAzV0WOKPfA6OPI4oy"
+ "pBadjvMZ4ZAj3BnXaSYsEZhaueTXvZB4eZOAjIyh2e_VOIKVMsnDrJYA"
+ "VotGlvMQ",
+ "e": "AQAB",
+ "d": "Kn9tgoHfiTVi8uPu5b9TnwyHwG5dK6RE0uFdlpCGnJN7ZEi963R7wy"
+ "bQ1PLAHmpIbNTztfrheoAniRV1NCIqXaW_qS461xiDTp4ntEPnqcKsyO"
+ "5jMAji7-CL8vhpYYowNFvIesgMoVaPRYMYT9TW63hNM0aWs7USZ_hLg6"
+ "Oe1mY0vHTI3FucjSM86Nff4oIENt43r2fspgEPGRrdE6fpLc9Oaq-qeP"
+ "1GFULimrRdndm-P8q8kvN3KHlNAtEgrQAgTTgz80S-3VD0FgWfgnb1PN"
+ "miuPUxO8OpI9KDIfu_acc6fg14nsNaJqXe6RESvhGPH2afjHqSy_Fd2v"
+ "pzj85bQQ",
+ "p": "2DwQmZ43FoTnQ8IkUj3BmKRf5Eh2mizZA5xEJ2MinUE3sdTYKSLtaE"
+ "oekX9vbBZuWxHdVhM6UnKCJ_2iNk8Z0ayLYHL0_G21aXf9-unynEpUsH"
+ "7HHTklLpYAzOOx1ZgVljoxAdWNn3hiEFrjZLZGS7lOH-a3QQlDDQoJOJ"
+ "2VFmU",
+ "q": "te8LY4-W7IyaqH1ExujjMqkTAlTeRbv0VLQnfLY2xINnrWdwiQ93_V"
+ "F099aP1ESeLja2nw-6iKIe-qT7mtCPozKfVtUYfz5HrJ_XY2kfexJINb"
+ "9lhZHMv5p1skZpeIS-GPHCC6gRlKo1q-idn_qxyusfWv7WAxlSVfQfk8"
+ "d6Et0",
+ "dp": "UfYKcL_or492vVc0PzwLSplbg4L3-Z5wL48mwiswbpzOyIgd2xHTH"
+ "QmjJpFAIZ8q-zf9RmgJXkDrFs9rkdxPtAsL1WYdeCT5c125Fkdg317JV"
+ "RDo1inX7x2Kdh8ERCreW8_4zXItuTl_KiXZNU5lvMQjWbIw2eTx1lpsf"
+ "lo0rYU",
+ "dq": "iEgcO-QfpepdH8FWd7mUFyrXdnOkXJBCogChY6YKuIHGc_p8Le9Mb"
+ "pFKESzEaLlN1Ehf3B6oGBl5Iz_ayUlZj2IoQZ82znoUrpa9fVYNot87A"
+ "CfzIG7q9Mv7RiPAderZi03tkVXAdaBau_9vs5rS-7HMtxkVrxSUvJY14"
+ "TkXlHE",
+ "qi": "kC-lzZOqoFaZCr5l0tOVtREKoVqaAYhQiqIRGL-MzS4sCmRkxm5vZ"
+ "lXYx6RtE1n_AagjqajlkjieGlxTTThHD8Iga6foGBMaAr5uR1hGQpSc7"
+ "Gl7CF1DZkBJMTQN6EshYzZfxW08mIO8M6Rzuh0beL6fG9mkDcIyPrBXx"
+ "2bQ_mM"}
+
+JWE_IV_5_1_2 = "bbd5sTkYwhAIqfHsx8DayA"
+
+JWE_Encrypted_Key_5_1_3 = \
+ "laLxI0j-nLH-_BgLOXMozKxmy9gffy2gTdvqzfTihJBuuzxg0V7yk1WClnQePF" + \
+ "vG2K-pvSlWc9BRIazDrn50RcRai__3TDON395H3c62tIouJJ4XaRvYHFjZTZ2G" + \
+ "Xfz8YAImcc91Tfk0WXC2F5Xbb71ClQ1DDH151tlpH77f2ff7xiSxh9oSewYrcG" + \
+ "TSLUeeCt36r1Kt3OSj7EyBQXoZlN7IxbyhMAfgIe7Mv1rOTOI5I8NQqeXXW8Vl" + \
+ "zNmoxaGMny3YnGir5Wf6Qt2nBq4qDaPdnaAuuGUGEecelIO1wx1BpyIfgvfjOh" + \
+ "MBs9M8XL223Fg47xlGsMXdfuY-4jaqVw"
+
+JWE_Protected_Header_5_1_4 = \
+ "eyJhbGciOiJSU0ExXzUiLCJraWQiOiJmcm9kby5iYWdnaW5zQGhvYmJpdG9uLm" + \
+ "V4YW1wbGUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0"
+
+JWE_Ciphertext_5_1_4 = \
+ "0fys_TY_na7f8dwSfXLiYdHaA2DxUjD67ieF7fcVbIR62JhJvGZ4_FNVSiGc_r" + \
+ "aa0HnLQ6s1P2sv3Xzl1p1l_o5wR_RsSzrS8Z-wnI3Jvo0mkpEEnlDmZvDu_k8O" + \
+ "WzJv7eZVEqiWKdyVzFhPpiyQU28GLOpRc2VbVbK4dQKPdNTjPPEmRqcaGeTWZV" + \
+ "yeSUvf5k59yJZxRuSvWFf6KrNtmRdZ8R4mDOjHSrM_s8uwIFcqt4r5GX8TKaI0" + \
+ "zT5CbL5Qlw3sRc7u_hg0yKVOiRytEAEs3vZkcfLkP6nbXdC_PkMdNS-ohP78T2" + \
+ "O6_7uInMGhFeX4ctHG7VelHGiT93JfWDEQi5_V9UN1rhXNrYu-0fVMkZAKX3VW" + \
+ "i7lzA6BP430m"
+
+JWE_Authentication_Tag_5_1_4 = "kvKuFBXHe5mQr4lqgobAUg"
+
+JWE_compact_5_1_5 = \
+ "%s.%s.%s.%s.%s" % (JWE_Protected_Header_5_1_4,
+ JWE_Encrypted_Key_5_1_3,
+ JWE_IV_5_1_2,
+ JWE_Ciphertext_5_1_4,
+ JWE_Authentication_Tag_5_1_4)
+
+JWE_general_5_1_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_1_3}],
+ "protected": JWE_Protected_Header_5_1_4,
+ "iv": JWE_IV_5_1_2,
+ "ciphertext": JWE_Ciphertext_5_1_4,
+ "tag": JWE_Authentication_Tag_5_1_4}
+
+JWE_flattened_5_1_5 = {
+ "protected": JWE_Protected_Header_5_1_4,
+ "encrypted_key": JWE_Encrypted_Key_5_1_3,
+ "iv": JWE_IV_5_1_2,
+ "ciphertext": JWE_Ciphertext_5_1_4,
+ "tag": JWE_Authentication_Tag_5_1_4}
+
+# 5.2
+RSA_key_5_2_1 = {
+ "kty": "RSA",
+ "kid": "samwise.gamgee@hobbiton.example",
+ "use": "enc",
+ "n": "wbdxI55VaanZXPY29Lg5hdmv2XhvqAhoxUkanfzf2-5zVUxa6prHRr"
+ "I4pP1AhoqJRlZfYtWWd5mmHRG2pAHIlh0ySJ9wi0BioZBl1XP2e-C-Fy"
+ "XJGcTy0HdKQWlrfhTm42EW7Vv04r4gfao6uxjLGwfpGrZLarohiWCPnk"
+ "Nrg71S2CuNZSQBIPGjXfkmIy2tl_VWgGnL22GplyXj5YlBLdxXp3XeSt"
+ "sqo571utNfoUTU8E4qdzJ3U1DItoVkPGsMwlmmnJiwA7sXRItBCivR4M"
+ "5qnZtdw-7v4WuR4779ubDuJ5nalMv2S66-RPcnFAzWSKxtBDnFJJDGIU"
+ "e7Tzizjg1nms0Xq_yPub_UOlWn0ec85FCft1hACpWG8schrOBeNqHBOD"
+ "FskYpUc2LC5JA2TaPF2dA67dg1TTsC_FupfQ2kNGcE1LgprxKHcVWYQb"
+ "86B-HozjHZcqtauBzFNV5tbTuB-TpkcvJfNcFLlH3b8mb-H_ox35FjqB"
+ "SAjLKyoeqfKTpVjvXhd09knwgJf6VKq6UC418_TOljMVfFTWXUxlnfhO"
+ "OnzW6HSSzD1c9WrCuVzsUMv54szidQ9wf1cYWf3g5qFDxDQKis99gcDa"
+ "iCAwM3yEBIzuNeeCa5dartHDb1xEB_HcHSeYbghbMjGfasvKn0aZRsnT"
+ "yC0xhWBlsolZE",
+ "e": "AQAB",
+ "alg": "RSA-OAEP",
+ "d": "n7fzJc3_WG59VEOBTkayzuSMM780OJQuZjN_KbH8lOZG25ZoA7T4Bx"
+ "cc0xQn5oZE5uSCIwg91oCt0JvxPcpmqzaJZg1nirjcWZ-oBtVk7gCAWq"
+ "-B3qhfF3izlbkosrzjHajIcY33HBhsy4_WerrXg4MDNE4HYojy68TcxT"
+ "2LYQRxUOCf5TtJXvM8olexlSGtVnQnDRutxEUCwiewfmmrfveEogLx9E"
+ "A-KMgAjTiISXxqIXQhWUQX1G7v_mV_Hr2YuImYcNcHkRvp9E7ook0876"
+ "DhkO8v4UOZLwA1OlUX98mkoqwc58A_Y2lBYbVx1_s5lpPsEqbbH-nqIj"
+ "h1fL0gdNfihLxnclWtW7pCztLnImZAyeCWAG7ZIfv-Rn9fLIv9jZ6r7r"
+ "-MSH9sqbuziHN2grGjD_jfRluMHa0l84fFKl6bcqN1JWxPVhzNZo01yD"
+ "F-1LiQnqUYSepPf6X3a2SOdkqBRiquE6EvLuSYIDpJq3jDIsgoL8Mo1L"
+ "oomgiJxUwL_GWEOGu28gplyzm-9Q0U0nyhEf1uhSR8aJAQWAiFImWH5W"
+ "_IQT9I7-yrindr_2fWQ_i1UgMsGzA7aOGzZfPljRy6z-tY_KuBG00-28"
+ "S_aWvjyUc-Alp8AUyKjBZ-7CWH32fGWK48j1t-zomrwjL_mnhsPbGs0c"
+ "9WsWgRzI-K8gE",
+ "p": "7_2v3OQZzlPFcHyYfLABQ3XP85Es4hCdwCkbDeltaUXgVy9l9etKgh"
+ "vM4hRkOvbb01kYVuLFmxIkCDtpi-zLCYAdXKrAK3PtSbtzld_XZ9nlsY"
+ "a_QZWpXB_IrtFjVfdKUdMz94pHUhFGFj7nr6NNxfpiHSHWFE1zD_AC3m"
+ "Y46J961Y2LRnreVwAGNw53p07Db8yD_92pDa97vqcZOdgtybH9q6uma-"
+ "RFNhO1AoiJhYZj69hjmMRXx-x56HO9cnXNbmzNSCFCKnQmn4GQLmRj9s"
+ "fbZRqL94bbtE4_e0Zrpo8RNo8vxRLqQNwIy85fc6BRgBJomt8QdQvIgP"
+ "gWCv5HoQ",
+ "q": "zqOHk1P6WN_rHuM7ZF1cXH0x6RuOHq67WuHiSknqQeefGBA9PWs6Zy"
+ "KQCO-O6mKXtcgE8_Q_hA2kMRcKOcvHil1hqMCNSXlflM7WPRPZu2qCDc"
+ "qssd_uMbP-DqYthH_EzwL9KnYoH7JQFxxmcv5An8oXUtTwk4knKjkIYG"
+ "RuUwfQTus0w1NfjFAyxOOiAQ37ussIcE6C6ZSsM3n41UlbJ7TCqewzVJ"
+ "aPJN5cxjySPZPD3Vp01a9YgAD6a3IIaKJdIxJS1ImnfPevSJQBE79-EX"
+ "e2kSwVgOzvt-gsmM29QQ8veHy4uAqca5dZzMs7hkkHtw1z0jHV90epQJ"
+ "JlXXnH8Q",
+ "dp": "19oDkBh1AXelMIxQFm2zZTqUhAzCIr4xNIGEPNoDt1jK83_FJA-xn"
+ "x5kA7-1erdHdms_Ef67HsONNv5A60JaR7w8LHnDiBGnjdaUmmuO8XAxQ"
+ "J_ia5mxjxNjS6E2yD44USo2JmHvzeeNczq25elqbTPLhUpGo1IZuG72F"
+ "ZQ5gTjXoTXC2-xtCDEUZfaUNh4IeAipfLugbpe0JAFlFfrTDAMUFpC3i"
+ "XjxqzbEanflwPvj6V9iDSgjj8SozSM0dLtxvu0LIeIQAeEgT_yXcrKGm"
+ "pKdSO08kLBx8VUjkbv_3Pn20Gyu2YEuwpFlM_H1NikuxJNKFGmnAq9Lc"
+ "nwwT0jvoQ",
+ "dq": "S6p59KrlmzGzaQYQM3o0XfHCGvfqHLYjCO557HYQf72O9kLMCfd_1"
+ "VBEqeD-1jjwELKDjck8kOBl5UvohK1oDfSP1DleAy-cnmL29DqWmhgwM"
+ "1ip0CCNmkmsmDSlqkUXDi6sAaZuntyukyflI-qSQ3C_BafPyFaKrt1fg"
+ "dyEwYa08pESKwwWisy7KnmoUvaJ3SaHmohFS78TJ25cfc10wZ9hQNOrI"
+ "ChZlkiOdFCtxDqdmCqNacnhgE3bZQjGp3n83ODSz9zwJcSUvODlXBPc2"
+ "AycH6Ci5yjbxt4Ppox_5pjm6xnQkiPgj01GpsUssMmBN7iHVsrE7N2iz"
+ "nBNCeOUIQ",
+ "qi": "FZhClBMywVVjnuUud-05qd5CYU0dK79akAgy9oX6RX6I3IIIPckCc"
+ "iRrokxglZn-omAY5CnCe4KdrnjFOT5YUZE7G_Pg44XgCXaarLQf4hl80"
+ "oPEf6-jJ5Iy6wPRx7G2e8qLxnh9cOdf-kRqgOS3F48Ucvw3ma5V6KGMw"
+ "QqWFeV31XtZ8l5cVI-I3NzBS7qltpUVgz2Ju021eyc7IlqgzR98qKONl"
+ "27DuEES0aK0WE97jnsyO27Yp88Wa2RiBrEocM89QZI1seJiGDizHRUP4"
+ "UZxw9zsXww46wy0P6f9grnYp7t8LkyDDk8eoI4KX6SNMNVcyVS9IWjlq"
+ "8EzqZEKIA"}
+
+JWE_IV_5_2_2 = "-nBoKLH0YkLZPSI9"
+
+JWE_Encrypted_Key_5_2_3 = \
+ "rT99rwrBTbTI7IJM8fU3Eli7226HEB7IchCxNuh7lCiud48LxeolRdtFF4nzQi" + \
+ "beYOl5S_PJsAXZwSXtDePz9hk-BbtsTBqC2UsPOdwjC9NhNupNNu9uHIVftDyu" + \
+ "cvI6hvALeZ6OGnhNV4v1zx2k7O1D89mAzfw-_kT3tkuorpDU-CpBENfIHX1Q58" + \
+ "-Aad3FzMuo3Fn9buEP2yXakLXYa15BUXQsupM4A1GD4_H4Bd7V3u9h8Gkg8Bpx" + \
+ "KdUV9ScfJQTcYm6eJEBz3aSwIaK4T3-dwWpuBOhROQXBosJzS1asnuHtVMt2pK" + \
+ "IIfux5BC6huIvmY7kzV7W7aIUrpYm_3H4zYvyMeq5pGqFmW2k8zpO878TRlZx7" + \
+ "pZfPYDSXZyS0CfKKkMozT_qiCwZTSz4duYnt8hS4Z9sGthXn9uDqd6wycMagnQ" + \
+ "fOTs_lycTWmY-aqWVDKhjYNRf03NiwRtb5BE-tOdFwCASQj3uuAgPGrO2AWBe3" + \
+ "8UjQb0lvXn1SpyvYZ3WFc7WOJYaTa7A8DRn6MC6T-xDmMuxC0G7S2rscw5lQQU" + \
+ "06MvZTlFOt0UvfuKBa03cxA_nIBIhLMjY2kOTxQMmpDPTr6Cbo8aKaOnx6ASE5" + \
+ "Jx9paBpnNmOOKH35j_QlrQhDWUN6A2Gg8iFayJ69xDEdHAVCGRzN3woEI2ozDR" + \
+ "s"
+
+JWE_Protected_Header_5_2_4 = \
+ "eyJhbGciOiJSU0EtT0FFUCIsImtpZCI6InNhbXdpc2UuZ2FtZ2VlQGhvYmJpdG" + \
+ "9uLmV4YW1wbGUiLCJlbmMiOiJBMjU2R0NNIn0"
+
+JWE_Ciphertext_5_2_4 = \
+ "o4k2cnGN8rSSw3IDo1YuySkqeS_t2m1GXklSgqBdpACm6UJuJowOHC5ytjqYgR" + \
+ "L-I-soPlwqMUf4UgRWWeaOGNw6vGW-xyM01lTYxrXfVzIIaRdhYtEMRBvBWbEw" + \
+ "P7ua1DRfvaOjgZv6Ifa3brcAM64d8p5lhhNcizPersuhw5f-pGYzseva-TUaL8" + \
+ "iWnctc-sSwy7SQmRkfhDjwbz0fz6kFovEgj64X1I5s7E6GLp5fnbYGLa1QUiML" + \
+ "7Cc2GxgvI7zqWo0YIEc7aCflLG1-8BboVWFdZKLK9vNoycrYHumwzKluLWEbSV" + \
+ "maPpOslY2n525DxDfWaVFUfKQxMF56vn4B9QMpWAbnypNimbM8zVOw"
+
+JWE_Authentication_Tag_5_2_4 = "UCGiqJxhBI3IFVdPalHHvA"
+
+JWE_compact_5_2_5 = \
+ "%s.%s.%s.%s.%s" % (JWE_Protected_Header_5_2_4,
+ JWE_Encrypted_Key_5_2_3,
+ JWE_IV_5_2_2,
+ JWE_Ciphertext_5_2_4,
+ JWE_Authentication_Tag_5_2_4)
+
+JWE_general_5_2_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_2_3}],
+ "protected": JWE_Protected_Header_5_2_4,
+ "iv": JWE_IV_5_2_2,
+ "ciphertext": JWE_Ciphertext_5_2_4,
+ "tag": JWE_Authentication_Tag_5_2_4}
+
+JWE_flattened_5_2_5 = {
+ "protected": JWE_Protected_Header_5_2_4,
+ "encrypted_key": JWE_Encrypted_Key_5_2_3,
+ "iv": JWE_IV_5_2_2,
+ "ciphertext": JWE_Ciphertext_5_2_4,
+ "tag": JWE_Authentication_Tag_5_2_4}
+
+# 5.3
+Payload_plaintext_5_3_1 = \
+ b'{"keys":[{"kty":"oct","kid":"77c7e2b8-6e13-45cf-8672-617b5b45' + \
+ b'243a","use":"enc","alg":"A128GCM","k":"XctOhJAkA-pD9Lh7ZgW_2A' + \
+ b'"},{"kty":"oct","kid":"81b20965-8332-43d9-a468-82160ad91ac8",' + \
+ b'"use":"enc","alg":"A128KW","k":"GZy6sIZ6wl9NJOKB-jnmVQ"},{"kt' + \
+ b'y":"oct","kid":"18ec08e1-bfa9-4d95-b205-2b4dd1d4321d","use":"' + \
+ b'enc","alg":"A256GCMKW","k":"qC57l_uxcm7Nm3K-ct4GFjx8tM1U8CZ0N' + \
+ b'LBvdQstiS8"}]}'
+
+Password_5_3_1 = b'entrap_o\xe2\x80\x93peter_long\xe2\x80\x93credit_tun'
+
+JWE_IV_5_3_2 = "VBiCzVHNoLiR3F4V82uoTQ"
+
+JWE_Encrypted_Key_5_3_3 = \
+ "d3qNhUWfqheyPp4H8sjOWsDYajoej4c5Je6rlUtFPWdgtURtmeDV1g"
+
+JWE_Protected_Header_no_p2x = {
+ "alg": "PBES2-HS512+A256KW",
+ "cty": "jwk-set+json",
+ "enc": "A128CBC-HS256"}
+
+JWE_Protected_Header_5_3_4 = \
+ "eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJwMnMiOiI4UTFTemluYXNSM3" + \
+ "hjaFl6NlpaY0hBIiwicDJjIjo4MTkyLCJjdHkiOiJqd2stc2V0K2pzb24iLCJl" + \
+ "bmMiOiJBMTI4Q0JDLUhTMjU2In0"
+
+JWE_Ciphertext_5_3_4 = \
+ "23i-Tb1AV4n0WKVSSgcQrdg6GRqsUKxjruHXYsTHAJLZ2nsnGIX86vMXqIi6IR" + \
+ "sfywCRFzLxEcZBRnTvG3nhzPk0GDD7FMyXhUHpDjEYCNA_XOmzg8yZR9oyjo6l" + \
+ "TF6si4q9FZ2EhzgFQCLO_6h5EVg3vR75_hkBsnuoqoM3dwejXBtIodN84PeqMb" + \
+ "6asmas_dpSsz7H10fC5ni9xIz424givB1YLldF6exVmL93R3fOoOJbmk2GBQZL" + \
+ "_SEGllv2cQsBgeprARsaQ7Bq99tT80coH8ItBjgV08AtzXFFsx9qKvC982KLKd" + \
+ "PQMTlVJKkqtV4Ru5LEVpBZXBnZrtViSOgyg6AiuwaS-rCrcD_ePOGSuxvgtrok" + \
+ "AKYPqmXUeRdjFJwafkYEkiuDCV9vWGAi1DH2xTafhJwcmywIyzi4BqRpmdn_N-" + \
+ "zl5tuJYyuvKhjKv6ihbsV_k1hJGPGAxJ6wUpmwC4PTQ2izEm0TuSE8oMKdTw8V" + \
+ "3kobXZ77ulMwDs4p"
+
+JWE_Authentication_Tag_5_3_4 = "0HlwodAhOCILG5SQ2LQ9dg"
+
+JWE_compact_5_3_5 = \
+ "%s.%s.%s.%s.%s" % (JWE_Protected_Header_5_3_4,
+ JWE_Encrypted_Key_5_3_3,
+ JWE_IV_5_3_2,
+ JWE_Ciphertext_5_3_4,
+ JWE_Authentication_Tag_5_3_4)
+
+JWE_general_5_3_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_3_3}],
+ "protected": JWE_Protected_Header_5_3_4,
+ "iv": JWE_IV_5_3_2,
+ "ciphertext": JWE_Ciphertext_5_3_4,
+ "tag": JWE_Authentication_Tag_5_3_4}
+
+JWE_flattened_5_3_5 = {
+ "protected": JWE_Protected_Header_5_3_4,
+ "encrypted_key": JWE_Encrypted_Key_5_3_3,
+ "iv": JWE_IV_5_3_2,
+ "ciphertext": JWE_Ciphertext_5_3_4,
+ "tag": JWE_Authentication_Tag_5_3_4}
+
+# 5.4
+EC_key_5_4_1 = {
+ "kty": "EC",
+ "kid": "peregrin.took@tuckborough.example",
+ "use": "enc",
+ "crv": "P-384",
+ "x": "YU4rRUzdmVqmRtWOs2OpDE_T5fsNIodcG8G5FWPrTPMyxpzsSOGaQLpe2FpxBmu2",
+ "y": "A8-yxCHxkfBz3hKZfI1jUYMjUhsEveZ9THuwFjH2sCNdtksRJU7D5-SkgaFL1ETP",
+ "d": "iTx2pk7wW-GqJkHcEkFQb2EFyYcO7RugmaW3mRrQVAOUiPommT0IdnYK2xDlZh-j"}
+
+JWE_IV_5_4_2 = "mH-G2zVqgztUtnW_"
+
+JWE_Encrypted_Key_5_4_3 = \
+ "0DJjBXri_kBcC46IkU5_Jk9BqaQeHdv2"
+
+JWE_Protected_Header_no_epk_5_4_4 = {
+ "alg": "ECDH-ES+A128KW",
+ "kid": "peregrin.took@tuckborough.example",
+ "enc": "A128GCM"}
+
+JWE_Protected_Header_5_4_4 = \
+ "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImtpZCI6InBlcmVncmluLnRvb2tAdH" + \
+ "Vja2Jvcm91Z2guZXhhbXBsZSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAt" + \
+ "Mzg0IiwieCI6InVCbzRrSFB3Nmtiang1bDB4b3dyZF9vWXpCbWF6LUdLRlp1NH" + \
+ "hBRkZrYllpV2d1dEVLNml1RURzUTZ3TmROZzMiLCJ5Ijoic3AzcDVTR2haVkMy" + \
+ "ZmFYdW1JLWU5SlUyTW84S3BvWXJGRHI1eVBOVnRXNFBnRXdaT3lRVEEtSmRhWT" + \
+ "h0YjdFMCJ9LCJlbmMiOiJBMTI4R0NNIn0"
+
+JWE_Ciphertext_5_4_4 = \
+ "tkZuOO9h95OgHJmkkrfLBisku8rGf6nzVxhRM3sVOhXgz5NJ76oID7lpnAi_cP" + \
+ "WJRCjSpAaUZ5dOR3Spy7QuEkmKx8-3RCMhSYMzsXaEwDdXta9Mn5B7cCBoJKB0" + \
+ "IgEnj_qfo1hIi-uEkUpOZ8aLTZGHfpl05jMwbKkTe2yK3mjF6SBAsgicQDVCkc" + \
+ "Y9BLluzx1RmC3ORXaM0JaHPB93YcdSDGgpgBWMVrNU1ErkjcMqMoT_wtCex3w0" + \
+ "3XdLkjXIuEr2hWgeP-nkUZTPU9EoGSPj6fAS-bSz87RCPrxZdj_iVyC6QWcqAu" + \
+ "07WNhjzJEPc4jVntRJ6K53NgPQ5p99l3Z408OUqj4ioYezbS6vTPlQ"
+
+JWE_Authentication_Tag_5_4_4 = "WuGzxmcreYjpHGJoa17EBg"
+
+JWE_compact_5_4_5 = \
+ "%s.%s.%s.%s.%s" % (JWE_Protected_Header_5_4_4,
+ JWE_Encrypted_Key_5_4_3,
+ JWE_IV_5_4_2,
+ JWE_Ciphertext_5_4_4,
+ JWE_Authentication_Tag_5_4_4)
+
+JWE_general_5_4_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_4_3}],
+ "protected": JWE_Protected_Header_5_4_4,
+ "iv": JWE_IV_5_4_2,
+ "ciphertext": JWE_Ciphertext_5_4_4,
+ "tag": JWE_Authentication_Tag_5_4_4}
+
+JWE_flattened_5_4_5 = {
+ "protected": JWE_Protected_Header_5_4_4,
+ "encrypted_key": JWE_Encrypted_Key_5_4_3,
+ "iv": JWE_IV_5_4_2,
+ "ciphertext": JWE_Ciphertext_5_4_4,
+ "tag": JWE_Authentication_Tag_5_4_4}
+
+# 5.5
+EC_key_5_5_1 = {
+ "kty": "EC",
+ "kid": "meriadoc.brandybuck@buckland.example",
+ "use": "enc",
+ "crv": "P-256",
+ "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0",
+ "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw",
+ "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8"}
+
+JWE_IV_5_5_2 = "yc9N8v5sYyv3iGQT926IUg"
+
+JWE_Protected_Header_no_epk_5_5_4 = {
+ "alg": "ECDH-ES",
+ "kid": "meriadoc.brandybuck@buckland.example",
+ "enc": "A128CBC-HS256"
+}
+
+JWE_Protected_Header_5_5_4 = \
+ "eyJhbGciOiJFQ0RILUVTIiwia2lkIjoibWVyaWFkb2MuYnJhbmR5YnVja0BidW" + \
+ "NrbGFuZC5leGFtcGxlIiwiZXBrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYi" + \
+ "LCJ4IjoibVBVS1RfYkFXR0hJaGcwVHBqanFWc1AxclhXUXVfdndWT0hIdE5rZF" + \
+ "lvQSIsInkiOiI4QlFBc0ltR2VBUzQ2ZnlXdzVNaFlmR1RUMElqQnBGdzJTUzM0" + \
+ "RHY0SXJzIn0sImVuYyI6IkExMjhDQkMtSFMyNTYifQ"
+
+JWE_Ciphertext_5_5_4 = \
+ "BoDlwPnTypYq-ivjmQvAYJLb5Q6l-F3LIgQomlz87yW4OPKbWE1zSTEFjDfhU9" + \
+ "IPIOSA9Bml4m7iDFwA-1ZXvHteLDtw4R1XRGMEsDIqAYtskTTmzmzNa-_q4F_e" + \
+ "vAPUmwlO-ZG45Mnq4uhM1fm_D9rBtWolqZSF3xGNNkpOMQKF1Cl8i8wjzRli7-" + \
+ "IXgyirlKQsbhhqRzkv8IcY6aHl24j03C-AR2le1r7URUhArM79BY8soZU0lzwI" + \
+ "-sD5PZ3l4NDCCei9XkoIAfsXJWmySPoeRb2Ni5UZL4mYpvKDiwmyzGd65KqVw7" + \
+ "MsFfI_K767G9C9Azp73gKZD0DyUn1mn0WW5LmyX_yJ-3AROq8p1WZBfG-ZyJ61" + \
+ "95_JGG2m9Csg"
+
+JWE_Authentication_Tag_5_5_4 = "WCCkNa-x4BeB9hIDIfFuhg"
+
+JWE_compact_5_5_5 = \
+ "%s..%s.%s.%s" % (JWE_Protected_Header_5_5_4,
+ JWE_IV_5_5_2,
+ JWE_Ciphertext_5_5_4,
+ JWE_Authentication_Tag_5_5_4)
+
+JWE_general_5_5_5 = {
+ "protected": JWE_Protected_Header_5_5_4,
+ "iv": JWE_IV_5_5_2,
+ "ciphertext": JWE_Ciphertext_5_5_4,
+ "tag": JWE_Authentication_Tag_5_5_4}
+
+# 5.6
+AES_key_5_6_1 = {
+ "kty": "oct",
+ "kid": "77c7e2b8-6e13-45cf-8672-617b5b45243a",
+ "use": "enc",
+ "alg": "A128GCM",
+ "k": "XctOhJAkA-pD9Lh7ZgW_2A"}
+
+JWE_IV_5_6_2 = "refa467QzzKx6QAB"
+
+JWE_Protected_Header_5_6_3 = \
+ "eyJhbGciOiJkaXIiLCJraWQiOiI3N2M3ZTJiOC02ZTEzLTQ1Y2YtODY3Mi02MT" + \
+ "diNWI0NTI0M2EiLCJlbmMiOiJBMTI4R0NNIn0"
+
+JWE_Ciphertext_5_6_3 = \
+ "JW_i_f52hww_ELQPGaYyeAB6HYGcR559l9TYnSovc23XJoBcW29rHP8yZOZG7Y" + \
+ "hLpT1bjFuvZPjQS-m0IFtVcXkZXdH_lr_FrdYt9HRUYkshtrMmIUAyGmUnd9zM" + \
+ "DB2n0cRDIHAzFVeJUDxkUwVAE7_YGRPdcqMyiBoCO-FBdE-Nceb4h3-FtBP-c_" + \
+ "BIwCPTjb9o0SbdcdREEMJMyZBH8ySWMVi1gPD9yxi-aQpGbSv_F9N4IZAxscj5" + \
+ "g-NJsUPbjk29-s7LJAGb15wEBtXphVCgyy53CoIKLHHeJHXex45Uz9aKZSRSIn" + \
+ "ZI-wjsY0yu3cT4_aQ3i1o-tiE-F8Ios61EKgyIQ4CWao8PFMj8TTnp"
+
+JWE_Authentication_Tag_5_6_3 = "vbb32Xvllea2OtmHAdccRQ"
+
+JWE_compact_5_6_4 = \
+ "%s..%s.%s.%s" % (JWE_Protected_Header_5_6_3,
+ JWE_IV_5_6_2,
+ JWE_Ciphertext_5_6_3,
+ JWE_Authentication_Tag_5_6_3)
+
+JWE_general_5_6_4 = {
+ "protected": JWE_Protected_Header_5_6_3,
+ "iv": JWE_IV_5_6_2,
+ "ciphertext": JWE_Ciphertext_5_6_3,
+ "tag": JWE_Authentication_Tag_5_6_3}
+
+# 5.7 - A256GCMKW not implemented yet
+AES_key_5_7_1 = {
+ "kty": "oct",
+ "kid": "18ec08e1-bfa9-4d95-b205-2b4dd1d4321d",
+ "use": "enc",
+ "alg": "A256GCMKW",
+ "k": "qC57l_uxcm7Nm3K-ct4GFjx8tM1U8CZ0NLBvdQstiS8"}
+
+JWE_IV_5_7_2 = "gz6NjyEFNm_vm8Gj6FwoFQ"
+
+JWE_Encrypted_Key_5_7_3 = "lJf3HbOApxMEBkCMOoTnnABxs_CvTWUmZQ2ElLvYNok"
+
+JWE_Protected_Header_no_ivtag = {
+ "alg": "A256GCMKW",
+ "kid": "18ec08e1-bfa9-4d95-b205-2b4dd1d4321d",
+ "enc": "A128CBC-HS256"}
+
+JWE_Protected_Header_5_7_4 = \
+ "eyJhbGciOiJBMjU2R0NNS1ciLCJraWQiOiIxOGVjMDhlMS1iZmE5LTRkOTUtYj" + \
+ "IwNS0yYjRkZDFkNDMyMWQiLCJ0YWciOiJrZlBkdVZRM1QzSDZ2bmV3dC0ta3N3" + \
+ "IiwiaXYiOiJLa1lUMEdYXzJqSGxmcU5fIiwiZW5jIjoiQTEyOENCQy1IUzI1Ni" + \
+ "J9"
+
+JWE_Ciphertext_5_7_4 = \
+ "Jf5p9-ZhJlJy_IQ_byKFmI0Ro7w7G1QiaZpI8OaiVgD8EqoDZHyFKFBupS8iaE" + \
+ "eVIgMqWmsuJKuoVgzR3YfzoMd3GxEm3VxNhzWyWtZKX0gxKdy6HgLvqoGNbZCz" + \
+ "LjqcpDiF8q2_62EVAbr2uSc2oaxFmFuIQHLcqAHxy51449xkjZ7ewzZaGV3eFq" + \
+ "hpco8o4DijXaG5_7kp3h2cajRfDgymuxUbWgLqaeNQaJtvJmSMFuEOSAzw9Hde" + \
+ "b6yhdTynCRmu-kqtO5Dec4lT2OMZKpnxc_F1_4yDJFcqb5CiDSmA-psB2k0Jtj" + \
+ "xAj4UPI61oONK7zzFIu4gBfjJCndsZfdvG7h8wGjV98QhrKEnR7xKZ3KCr0_qR" + \
+ "1B-gxpNk3xWU"
+
+JWE_Authentication_Tag_5_7_4 = "DKW7jrb4WaRSNfbXVPlT5g"
+
+JWE_compact_5_7_5 = \
+ "%s.%s.%s.%s.%s" % (JWE_Protected_Header_5_7_4,
+ JWE_Encrypted_Key_5_7_3,
+ JWE_IV_5_7_2,
+ JWE_Ciphertext_5_7_4,
+ JWE_Authentication_Tag_5_7_4)
+
+JWE_general_5_7_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_7_3}],
+ "protected": JWE_Protected_Header_5_7_4,
+ "iv": JWE_IV_5_7_2,
+ "ciphertext": JWE_Ciphertext_5_7_4,
+ "tag": JWE_Authentication_Tag_5_7_4}
+
+JWE_flattened_5_7_5 = {
+ "protected": JWE_Protected_Header_5_7_4,
+ "encrypted_key": JWE_Encrypted_Key_5_7_3,
+ "iv": JWE_IV_5_7_2,
+ "ciphertext": JWE_Ciphertext_5_7_4,
+ "tag": JWE_Authentication_Tag_5_7_4}
+
+# 5.8
+AES_key_5_8_1 = {
+ "kty": "oct",
+ "kid": "81b20965-8332-43d9-a468-82160ad91ac8",
+ "use": "enc",
+ "alg": "A128KW",
+ "k": "GZy6sIZ6wl9NJOKB-jnmVQ"}
+
+JWE_IV_5_8_2 = "Qx0pmsDa8KnJc9Jo"
+
+JWE_Encrypted_Key_5_8_3 = "CBI6oDw8MydIx1IBntf_lQcw2MmJKIQx"
+
+JWE_Protected_Header_5_8_4 = \
+ "eyJhbGciOiJBMTI4S1ciLCJraWQiOiI4MWIyMDk2NS04MzMyLTQzZDktYTQ2OC" + \
+ "04MjE2MGFkOTFhYzgiLCJlbmMiOiJBMTI4R0NNIn0"
+
+JWE_Ciphertext_5_8_4 = \
+ "AwliP-KmWgsZ37BvzCefNen6VTbRK3QMA4TkvRkH0tP1bTdhtFJgJxeVmJkLD6" + \
+ "1A1hnWGetdg11c9ADsnWgL56NyxwSYjU1ZEHcGkd3EkU0vjHi9gTlb90qSYFfe" + \
+ "F0LwkcTtjbYKCsiNJQkcIp1yeM03OmuiYSoYJVSpf7ej6zaYcMv3WwdxDFl8RE" + \
+ "wOhNImk2Xld2JXq6BR53TSFkyT7PwVLuq-1GwtGHlQeg7gDT6xW0JqHDPn_H-p" + \
+ "uQsmthc9Zg0ojmJfqqFvETUxLAF-KjcBTS5dNy6egwkYtOt8EIHK-oEsKYtZRa" + \
+ "a8Z7MOZ7UGxGIMvEmxrGCPeJa14slv2-gaqK0kEThkaSqdYw0FkQZF"
+
+JWE_Authentication_Tag_5_8_4 = "ER7MWJZ1FBI_NKvn7Zb1Lw"
+
+JWE_compact_5_8_5 = \
+ "%s.%s.%s.%s.%s" % (JWE_Protected_Header_5_8_4,
+ JWE_Encrypted_Key_5_8_3,
+ JWE_IV_5_8_2,
+ JWE_Ciphertext_5_8_4,
+ JWE_Authentication_Tag_5_8_4)
+
+JWE_general_5_8_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_8_3}],
+ "protected": JWE_Protected_Header_5_8_4,
+ "iv": JWE_IV_5_8_2,
+ "ciphertext": JWE_Ciphertext_5_8_4,
+ "tag": JWE_Authentication_Tag_5_8_4}
+
+JWE_flattened_5_8_5 = {
+ "protected": JWE_Protected_Header_5_8_4,
+ "encrypted_key": JWE_Encrypted_Key_5_8_3,
+ "iv": JWE_IV_5_8_2,
+ "ciphertext": JWE_Ciphertext_5_8_4,
+ "tag": JWE_Authentication_Tag_5_8_4}
+
+# 5.9
+JWE_IV_5_9_2 = "p9pUq6XHY0jfEZIl"
+
+JWE_Encrypted_Key_5_9_3 = "5vUT2WOtQxKWcekM_IzVQwkGgzlFDwPi"
+
+JWE_Protected_Header_5_9_4 = \
+ "eyJhbGciOiJBMTI4S1ciLCJraWQiOiI4MWIyMDk2NS04MzMyLTQzZDktYTQ2OC" + \
+ "04MjE2MGFkOTFhYzgiLCJlbmMiOiJBMTI4R0NNIiwiemlwIjoiREVGIn0"
+
+JWE_Ciphertext_5_9_4 = \
+ "HbDtOsdai1oYziSx25KEeTxmwnh8L8jKMFNc1k3zmMI6VB8hry57tDZ61jXyez" + \
+ "SPt0fdLVfe6Jf5y5-JaCap_JQBcb5opbmT60uWGml8blyiMQmOn9J--XhhlYg0" + \
+ "m-BHaqfDO5iTOWxPxFMUedx7WCy8mxgDHj0aBMG6152PsM-w5E_o2B3jDbrYBK" + \
+ "hpYA7qi3AyijnCJ7BP9rr3U8kxExCpG3mK420TjOw"
+
+JWE_Authentication_Tag_5_9_4 = "VILuUwuIxaLVmh5X-T7kmA"
+
+JWE_compact_5_9_5 = \
+ "%s.%s.%s.%s.%s" % (JWE_Protected_Header_5_9_4,
+ JWE_Encrypted_Key_5_9_3,
+ JWE_IV_5_9_2,
+ JWE_Ciphertext_5_9_4,
+ JWE_Authentication_Tag_5_9_4)
+
+JWE_general_5_9_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_9_3}],
+ "protected": JWE_Protected_Header_5_9_4,
+ "iv": JWE_IV_5_9_2,
+ "ciphertext": JWE_Ciphertext_5_9_4,
+ "tag": JWE_Authentication_Tag_5_9_4}
+
+JWE_flattened_5_9_5 = {
+ "protected": JWE_Protected_Header_5_9_4,
+ "encrypted_key": JWE_Encrypted_Key_5_9_3,
+ "iv": JWE_IV_5_9_2,
+ "ciphertext": JWE_Ciphertext_5_9_4,
+ "tag": JWE_Authentication_Tag_5_9_4}
+
+# 5.10
+AAD_5_10_1 = base64url_encode(json_encode(
+ ["vcard",
+ [["version", {}, "text", "4.0"],
+ ["fn", {}, "text", "Meriadoc Brandybuck"],
+ ["n", {}, "text", ["Brandybuck", "Meriadoc", "Mr.", ""]],
+ ["bday", {}, "text", "TA 2982"],
+ ["gender", {}, "text", "M"]]]))
+
+JWE_IV_5_10_2 = "veCx9ece2orS7c_N"
+
+JWE_Encrypted_Key_5_10_3 = "4YiiQ_ZzH76TaIkJmYfRFgOV9MIpnx4X"
+
+JWE_Protected_Header_5_10_4 = \
+ "eyJhbGciOiJBMTI4S1ciLCJraWQiOiI4MWIyMDk2NS04MzMyLTQzZDktYTQ2OC" + \
+ "04MjE2MGFkOTFhYzgiLCJlbmMiOiJBMTI4R0NNIn0"
+
+JWE_Ciphertext_5_10_4 = \
+ "Z_3cbr0k3bVM6N3oSNmHz7Lyf3iPppGf3Pj17wNZqteJ0Ui8p74SchQP8xygM1" + \
+ "oFRWCNzeIa6s6BcEtp8qEFiqTUEyiNkOWDNoF14T_4NFqF-p2Mx8zkbKxI7oPK" + \
+ "8KNarFbyxIDvICNqBLba-v3uzXBdB89fzOI-Lv4PjOFAQGHrgv1rjXAmKbgkft" + \
+ "9cB4WeyZw8MldbBhc-V_KWZslrsLNygon_JJWd_ek6LQn5NRehvApqf9ZrxB4a" + \
+ "q3FXBxOxCys35PhCdaggy2kfUfl2OkwKnWUbgXVD1C6HxLIlqHhCwXDG59weHr" + \
+ "RDQeHyMRoBljoV3X_bUTJDnKBFOod7nLz-cj48JMx3SnCZTpbQAkFV"
+
+JWE_Authentication_Tag_5_10_4 = "vOaH_Rajnpy_3hOtqvZHRA"
+
+JWE_general_5_10_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_10_3}],
+ "protected": JWE_Protected_Header_5_10_4,
+ "iv": JWE_IV_5_10_2,
+ "aad": AAD_5_10_1,
+ "ciphertext": JWE_Ciphertext_5_10_4,
+ "tag": JWE_Authentication_Tag_5_10_4}
+
+JWE_flattened_5_10_5 = {
+ "protected": JWE_Protected_Header_5_10_4,
+ "encrypted_key": JWE_Encrypted_Key_5_10_3,
+ "iv": JWE_IV_5_10_2,
+ "aad": AAD_5_10_1,
+ "ciphertext": JWE_Ciphertext_5_10_4,
+ "tag": JWE_Authentication_Tag_5_10_4}
+
+# 5.11
+JWE_IV_5_11_2 = "WgEJsDS9bkoXQ3nR"
+
+JWE_Encrypted_Key_5_11_3 = "jJIcM9J-hbx3wnqhf5FlkEYos0sHsF0H"
+
+JWE_Protected_Header_5_11_4 = "eyJlbmMiOiJBMTI4R0NNIn0"
+
+JWE_Ciphertext_5_11_4 = \
+ "lIbCyRmRJxnB2yLQOTqjCDKV3H30ossOw3uD9DPsqLL2DM3swKkjOwQyZtWsFL" + \
+ "YMj5YeLht_StAn21tHmQJuuNt64T8D4t6C7kC9OCCJ1IHAolUv4MyOt80MoPb8" + \
+ "fZYbNKqplzYJgIL58g8N2v46OgyG637d6uuKPwhAnTGm_zWhqc_srOvgiLkzyF" + \
+ "XPq1hBAURbc3-8BqeRb48iR1-_5g5UjWVD3lgiLCN_P7AW8mIiFvUNXBPJK3nO" + \
+ "WL4teUPS8yHLbWeL83olU4UAgL48x-8dDkH23JykibVSQju-f7e-1xreHWXzWL" + \
+ "Hs1NqBbre0dEwK3HX_xM0LjUz77Krppgegoutpf5qaKg3l-_xMINmf"
+
+JWE_Authentication_Tag_5_11_4 = "fNYLqpUe84KD45lvDiaBAQ"
+
+JWE_Unprotected_Header_5_11_5 = {
+ "alg": "A128KW",
+ "kid": "81b20965-8332-43d9-a468-82160ad91ac8"}
+
+JWE_general_5_11_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_11_3}],
+ "unprotected": JWE_Unprotected_Header_5_11_5,
+ "protected": JWE_Protected_Header_5_11_4,
+ "iv": JWE_IV_5_11_2,
+ "ciphertext": JWE_Ciphertext_5_11_4,
+ "tag": JWE_Authentication_Tag_5_11_4}
+
+JWE_flattened_5_11_5 = {
+ "protected": JWE_Protected_Header_5_11_4,
+ "unprotected": JWE_Unprotected_Header_5_11_5,
+ "encrypted_key": JWE_Encrypted_Key_5_11_3,
+ "iv": JWE_IV_5_11_2,
+ "ciphertext": JWE_Ciphertext_5_11_4,
+ "tag": JWE_Authentication_Tag_5_11_4}
+
+# 5.11
+JWE_IV_5_12_2 = "YihBoVOGsR1l7jCD"
+
+JWE_Encrypted_Key_5_12_3 = "244YHfO_W7RMpQW81UjQrZcq5LSyqiPv"
+
+JWE_Ciphertext_5_12_4 = \
+ "qtPIMMaOBRgASL10dNQhOa7Gqrk7Eal1vwht7R4TT1uq-arsVCPaIeFwQfzrSS" + \
+ "6oEUWbBtxEasE0vC6r7sphyVziMCVJEuRJyoAHFSP3eqQPb4Ic1SDSqyXjw_L3" + \
+ "svybhHYUGyQuTmUQEDjgjJfBOifwHIsDsRPeBz1NomqeifVPq5GTCWFo5k_MNI" + \
+ "QURR2Wj0AHC2k7JZfu2iWjUHLF8ExFZLZ4nlmsvJu_mvifMYiikfNfsZAudISO" + \
+ "a6O73yPZtL04k_1FI7WDfrb2w7OqKLWDXzlpcxohPVOLQwpA3mFNRKdY-bQz4Z" + \
+ "4KX9lfz1cne31N4-8BKmojpw-OdQjKdLOGkC445Fb_K1tlDQXw2sBF"
+
+JWE_Authentication_Tag_5_12_4 = "e2m0Vm7JvjK2VpCKXS-kyg"
+
+JWE_Unprotected_Header_5_12_5 = {
+ "alg": "A128KW",
+ "kid": "81b20965-8332-43d9-a468-82160ad91ac8",
+ "enc": "A128GCM"}
+
+JWE_general_5_12_5 = {
+ "recipients": [{
+ "encrypted_key": JWE_Encrypted_Key_5_12_3}],
+ "unprotected": JWE_Unprotected_Header_5_12_5,
+ "iv": JWE_IV_5_12_2,
+ "ciphertext": JWE_Ciphertext_5_12_4,
+ "tag": JWE_Authentication_Tag_5_12_4}
+
+JWE_flattened_5_12_5 = {
+ "unprotected": JWE_Unprotected_Header_5_12_5,
+ "encrypted_key": JWE_Encrypted_Key_5_12_3,
+ "iv": JWE_IV_5_12_2,
+ "ciphertext": JWE_Ciphertext_5_12_4,
+ "tag": JWE_Authentication_Tag_5_12_4}
+
+# 5.13 - A256GCMKW not implemented yet
+
+
+# In general we can't compare ciphertexts with the reference because
+# either the algorithms use random nonces to authenticate the ciphertext
+# or we randomly genrate the nonce when we create the JWe.
+# To double check implementation we encrypt/decrypt our own input and then
+# decrypt the reference and check it against the given plaintext
+class Cookbook08JWETests(unittest.TestCase):
+
+ def test_5_1_encryption(self):
+ 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.add_recipient(rsa_key)
+ enc = e.serialize()
+ e.deserialize(enc, rsa_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_1_5, rsa_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_1_5), rsa_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_1_5), rsa_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_2_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = base64url_decode(JWE_Protected_Header_5_2_4)
+ rsa_key = jwk.JWK(**RSA_key_5_2_1)
+ e = jwe.JWE(plaintext, protected)
+ e.add_recipient(rsa_key)
+ enc = e.serialize()
+ e.deserialize(enc, rsa_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_2_5, rsa_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_2_5), rsa_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_2_5), rsa_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_3_encryption(self):
+ plaintext = Payload_plaintext_5_3_1
+ 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, 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), unicodepwd)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_3_5), password)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_4_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = json_encode(JWE_Protected_Header_no_epk_5_4_4)
+ ec_key = jwk.JWK(**EC_key_5_4_1)
+ e = jwe.JWE(plaintext, protected)
+ e.add_recipient(ec_key)
+ enc = e.serialize(compact=True)
+ e.deserialize(enc, ec_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_4_5, ec_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_4_5), ec_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_5_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = json_encode(JWE_Protected_Header_no_epk_5_5_4)
+ ec_key = jwk.JWK(**EC_key_5_5_1)
+ e = jwe.JWE(plaintext, protected)
+ e.add_recipient(ec_key)
+ enc = e.serialize(compact=True)
+ e.deserialize(enc, ec_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_5_5, ec_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_5_5), ec_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_6_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = base64url_decode(JWE_Protected_Header_5_6_3)
+ aes_key = jwk.JWK(**AES_key_5_6_1)
+ e = jwe.JWE(plaintext, protected)
+ e.add_recipient(aes_key)
+ e.serialize(compact=True)
+ enc = e.serialize()
+ e.deserialize(enc, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_6_4, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_6_4), aes_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_7_encryption(self):
+ plaintext = Payload_plaintext_5
+ aes_key = jwk.JWK(**AES_key_5_7_1)
+ e = jwe.JWE(plaintext, json_encode(JWE_Protected_Header_no_ivtag))
+ e.add_recipient(aes_key)
+ enc = e.serialize(compact=True)
+ e.deserialize(enc, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_7_5, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_7_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_7_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_8_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = base64url_decode(JWE_Protected_Header_5_8_4)
+ aes_key = jwk.JWK(**AES_key_5_8_1)
+ e = jwe.JWE(plaintext, protected)
+ e.add_recipient(aes_key)
+ enc = e.serialize()
+ e.deserialize(enc, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_8_5, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_8_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_8_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_9_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = base64url_decode(JWE_Protected_Header_5_9_4)
+ aes_key = jwk.JWK(**AES_key_5_8_1)
+ e = jwe.JWE(plaintext, protected)
+ e.add_recipient(aes_key)
+ enc = e.serialize()
+ e.deserialize(enc, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(JWE_compact_5_9_5, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_9_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_9_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_10_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = base64url_decode(JWE_Protected_Header_5_10_4)
+ aad = base64url_decode(AAD_5_10_1)
+ aes_key = jwk.JWK(**AES_key_5_8_1)
+ e = jwe.JWE(plaintext, protected, aad=aad)
+ e.add_recipient(aes_key)
+ enc = e.serialize()
+ e.deserialize(enc, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_10_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_10_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_11_encryption(self):
+ plaintext = Payload_plaintext_5
+ protected = base64url_decode(JWE_Protected_Header_5_11_4)
+ unprotected = json_encode(JWE_Unprotected_Header_5_11_5)
+ aes_key = jwk.JWK(**AES_key_5_8_1)
+ e = jwe.JWE(plaintext, protected, unprotected)
+ e.add_recipient(aes_key)
+ enc = e.serialize()
+ e.deserialize(enc, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_11_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_11_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+
+ def test_5_12_encryption(self):
+ plaintext = Payload_plaintext_5
+ unprotected = json_encode(JWE_Unprotected_Header_5_12_5)
+ aes_key = jwk.JWK(**AES_key_5_8_1)
+ e = jwe.JWE(plaintext, None, unprotected)
+ e.add_recipient(aes_key)
+ enc = e.serialize()
+ e.deserialize(enc, aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_general_5_12_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+ e.deserialize(json_encode(JWE_flattened_5_12_5), aes_key)
+ self.assertEqual(e.payload, plaintext)
+
+# 5.13 - AES-GCM key wrapping not implemented yet
+# def test_5_13_encryption(self):
diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py
new file mode 100644
index 0000000..fc592b0
--- /dev/null
+++ b/jwcrypto/tests.py
@@ -0,0 +1,1150 @@
+# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
+
+from __future__ import unicode_literals
+
+import copy
+
+import unittest
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+from jwcrypto import jwa
+from jwcrypto import jwe
+from jwcrypto import jwk
+from jwcrypto import jws
+from jwcrypto import jwt
+from jwcrypto.common import base64url_decode, base64url_encode
+from jwcrypto.common import json_decode, json_encode
+
+# RFC 7517 - A.1
+PublicKeys = {"keys": [
+ {"kty": "EC",
+ "crv": "P-256",
+ "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
+ "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
+ "use": "enc",
+ "kid": "1"},
+ {"kty": "RSA",
+ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbf"
+ "AAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknj"
+ "hMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65"
+ "YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQ"
+ "vRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lF"
+ "d2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzK"
+ "nqDKgw",
+ "e": "AQAB",
+ "alg": "RS256",
+ "kid": "2011-04-29"}],
+ "thumbprints": ["cn-I_WNMClehiVp51i_0VpOENW1upEerA8sEam5hn-s",
+ "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"]}
+
+# RFC 7517 - A.2
+PrivateKeys = {"keys": [
+ {"kty": "EC",
+ "crv": "P-256",
+ "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
+ "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
+ "d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",
+ "use": "enc",
+ "kid": "1"},
+ {"kty": "RSA",
+ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbb"
+ "fAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3ok"
+ "njhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v"
+ "-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu"
+ "6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0"
+ "fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8a"
+ "wapJzKnqDKgw",
+ "e": "AQAB",
+ "d": "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7d"
+ "x5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_"
+ "YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywb"
+ "ReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5Zi"
+ "G7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z"
+ "4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc"
+ "0X4jfcKoAC8Q",
+ "p": "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVn"
+ "wD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XO"
+ "uVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O"
+ "0nVbfs",
+ "q": "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumq"
+ "jVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VV"
+ "S78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb"
+ "6yelxk",
+ "dp": "G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimY"
+ "wxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA"
+ "77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8Y"
+ "eiKkTiBj0",
+ "dq": "s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUv"
+ "MfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqU"
+ "fLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txX"
+ "w494Q_cgk",
+ "qi": "GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgU"
+ "IZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_"
+ "mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia"
+ "6zTKhAVRU",
+ "alg": "RS256",
+ "kid": "2011-04-29"}]}
+
+# RFC 7517 - A.3
+SymmetricKeys = {"keys": [
+ {"kty": "oct",
+ "alg": "A128KW",
+ "k": "GawgguFyGrWKav7AX4VKUg"},
+ {"kty": "oct",
+ "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH7"
+ "5aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow",
+ "kid": "HMAC key used in JWS A.1 example"}]}
+
+# RFC 7517 - B
+Useofx5c = {"kty": "RSA",
+ "use": "sig",
+ "kid": "1b94c",
+ "n": "vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08PLbK"
+ "_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Qu2j"
+ "8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4aYW"
+ "Achc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwHM"
+ "TplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMv"
+ "VfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ",
+ "e": "AQAB",
+ "x5c": ["MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJ"
+ "BgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRww"
+ "GgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5Ccmlh"
+ "biBDYW1wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVa"
+ "MGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVu"
+ "dmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQD"
+ "Ew5CcmlhbiBDYW1wYmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC"
+ "AQoCggEBAL64zn8/QnHYMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9"
+ "if6amFtPDy2yvz3YlRij66s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u"
+ "3WG7K+IiZhtELto/A7Fck9Ws6SQvzRvOE8uSirYbgmj6He4iO8NCyvaK"
+ "0jIQRMMGQwsU1quGmFgHIXPLfnpnfajr1rVTAwtgV5LEZ4Iel+W1GC8u"
+ "gMhyr4/p1MtcIM42EA8BzE6ZQqC7VPqPvEjZ2dbZkaBhPbiZAS3YeYBR"
+ "DWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVkaZdklLQp2Btgt9qr21m42f"
+ "4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAh8zGlfSl"
+ "cI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL+9gGlqCz5iWLOgWs"
+ "nrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1zFo+Owb1zxtp3"
+ "PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL2Bo3UPGrps"
+ "HzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo4tpzd5r"
+ "FXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTqgawR"
+ "+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA=="
+ ]}
+
+# RFC 7517 - C.1
+RSAPrivateKey = {"kty": "RSA",
+ "kid": "juliet@capulet.lit",
+ "use": "enc",
+ "n": "t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNq"
+ "FMSQRyO125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR"
+ "0-Iqom-QFcNP8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQ"
+ "lO8Yns5jCtLCRwLHL0Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-"
+ "AqWS9zIQ2ZilgT-GqUmipg0XOC0Cc20rgLe2ymLHjpHciCKVAbY5-L"
+ "32-lSeZO-Os6U15_aXrk9Gw8cPUaX1_I8sLGuSiVdt3C_Fn2PZ3Z8i"
+ "744FPFGGcG1qs2Wz-Q",
+ "e": "AQAB",
+ "d": "GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTea"
+ "STyWfSNkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWa"
+ "Cl3hdlPKXy9UvqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo"
+ "4_PMaenNnPiQgO0xnuToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDms"
+ "XOfUENOyMqADC6p1M3h33tsurY15k9qMSpG9OX_IJAXmxzAh_tWiZO"
+ "wk2K4yxH9tS3Lq1yX8C1EWmeRDkK2ahecG85-oLKQt5VEpWHKmjOi_"
+ "gJSdSgqcN96X52esAQ",
+ "p": "2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9u"
+ "w-PIHfQP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPP"
+ "SYB9yk31s0Q8UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3r"
+ "CT5T3yJws",
+ "q": "1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjs"
+ "Zu0c6Iedis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjV"
+ "tG6TlV8CLCYKrYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5"
+ "B0f808I4s",
+ "dp": "KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwK"
+ "qvVDq3tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_l"
+ "hqigI4y_kqS1wY52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttW"
+ "txVqLCRViD6c",
+ "dq": "AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1"
+ "xDkbN9GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCz"
+ "kOkmxIe3KRbBymXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRF"
+ "COJ3xDea-ots",
+ "qi": "lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEo"
+ "PwmUqqabu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDM"
+ "eAvmj4sm-Fp0oYu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu"
+ "9HCJ-UsfSOI8"}
+
+# From
+# vectors/cryptography_vectors/asymmetric/PEM_Serialization/rsa_private_key.pem
+RSAPrivatePEM = b"""-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,B4B3C8C536E57CBE
+
+B8Lq1K/wcOr4JMspWrX3zCX14WAp3xgHsKAB4XfuCuju/HQZoWXtok1xoi5e2Ovw
+ENA99Jvb2yvBdDUfOlp1L1L+By3q+SwcdeNuEKjwGFG6MY2uZaVtLSiAFXf1N8PL
+id7FMRGPIxpTtXKMhfAq4luRb0BgKh7+ZvM7LkxkRxF7M1XVQPGhrU0OfxX9VODe
+YFH1q47os5JzHRcrRaFx6sn30e79ij2gRjzMVFuAX07n+yw3qeyNQNYdmDNP7iCZ
+x//0iN0NboTI81coNlxx7TL4bYwgESt1c2i/TCfLITjKgEny7MKqU1/jTrOJWu85
+PiK/ojaD1EMx9xxVgBCioQJVG/Jm9y+XhtGFAJUShzzsabX7KuANKRn3fgUN+yZS
+yp8hmD+R5gQHJk/8+zZ6/Imv8W/G+7fPZuSMgWeWtDReCkfzgnyIdjaIp3Pdp5yN
+WLLWADI4tHmNUqIzY7T25gVfg0P2tgQNzn3WzHxq4SfZN9Aw57woi8eSRpLBEn+C
+JjqwTxtFQ14ynG6GPsBaDcAduchmJPL7e9PuAfFyLJuM8sU8QyB2oir1M/qYFhTC
+ClXw2yylYjAy8TFw1L3UZA4hfAflINjYUY8pgAtTAjxeD/9PhiKSoMEX8Q/8Npti
+1Db5RpAClIEdB6nPywj6BzC+6El3dSGaCV0sTQ42LD+S3QH8VCwTB2AuKq7zyuD6
+wEQopcbIOGHSir875vYLmWLmqR9MCWZtKj/dWfTIQpBsPsI2ssZn/MptNqyEN9TW
+GfnWoTuzoziCS5YmEq7Mh98fwP9Krb0abo3fFvu6CY3dhvvoxPaXahyAxBfpKArB
+9nOf3gzHGReWNiFUtNZlvueYrC5CnblFzKaKB+81Imjw6RXM3QtuzbZ71zp+reL8
+JeiwE/mriwuGbxTJx5gwQX48zA5PJ342CCrl7jMeIos5KXmYkWoU5hEuGM3tK4Lx
+VAoGqcd/a4cWHuLWub8fbhFkIDcxFaMF8yQi0r2LOmvMOsv3RVpyfgJ07z5b9X1B
+w76CYkjGqgr0EdU40VTPtNhtHq7rrJSzGbapRsFUpvqgnkEwUSdbY6bRknLETmfo
+H3dPf2XQwXXPDMZTW54QsmQ9WjundqOFI2YsH6dCX/kmZK0IJVBpikL8SuM/ZJLK
+LcYJcrNGetENEKKl6hDwTTIsG1y3gx6y3wPzBkyJ2DtMx9dPoCqYhPHsIGc/td0/
+r4Ix9TWVLIl3MKq3z+/Hszd7jOnrkflfmKeA0DgJlqVJsuxP75pbdiKS/hCKRf8D
+AFJLvt6JSGBnz9ZZCB4KrjpHK/k+X7p8Y65uc/aX5BLu8vyRqFduhg98GVXJmD7k
+0ggXnqqFnies6SpnQ45cjfKSGDx/NjY0AwoGPH8n8CL6ZagU6K1utfHIMrqKkJME
+F6KcPHWrQkECojLdMoDInnRirdRb/FcAadWBSPrf+6Nln4ilbBJIi8W/yzeM/WFj
+UKKNjk4W26PGnNO6+TO5h1EpocDI4fx6UYIMmFjnyaLdLrSn1/SzuLL6I7pYZ0Um
+8qI4aWjP9RiUvGYJirfAUjL5Vp9w4+osf1sGiioe0GH/1WVuHeQ93A==
+-----END RSA PRIVATE KEY-----
+"""
+
+RSAPrivatePassword = b"123456"
+
+# From
+# vectors/cryptography_vectors/asymmetric/PEM_Serialization/rsa_public_key.pem
+RSAPublicPEM = b"""-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnR4AZ+tgWYql+S3MaTQ6
+zeIO1fKzFIoau9Q0zGuv/1oCAewXwxeDSSxw+/Z3GL1NpuuS9CpbR5EQ3d71bD0v
+0G+Sf+mShSl0oljG7YqnNSPzKl+EQ3/KE+eEButcwas6KGof2BA4bFNCw/fPbuhk
+u/d8sIIEgdzBMiGRMdW33uci3rsdOenMZQA7uWsM/q/pu85YLAVOxq6wlUCzP4FM
+Tw/RKzayrPkn3Jfbqcy1aM2HDlFVx24vaN+RRbPSnVoQbo5EQYkUMXE8WmadSyHl
+pXGRnWsJSV9AdGyDrbU+6tcFwcIwnW22jb/OJy8swHdqKGkuR1kQ0XqokK1yGKFZ
+8wIDAQAB
+-----END PUBLIC KEY-----
+"""
+
+# From cryptography/vectors/cryptography_vectors/x509/v1_cert.pem
+PublicCert = b"""-----BEGIN CERTIFICATE-----
+MIIBWzCCAQYCARgwDQYJKoZIhvcNAQEEBQAwODELMAkGA1UEBhMCQVUxDDAKBgNV
+BAgTA1FMRDEbMBkGA1UEAxMSU1NMZWF5L3JzYSB0ZXN0IENBMB4XDTk1MDYxOTIz
+MzMxMloXDTk1MDcxNzIzMzMxMlowOjELMAkGA1UEBhMCQVUxDDAKBgNVBAgTA1FM
+RDEdMBsGA1UEAxMUU1NMZWF5L3JzYSB0ZXN0IGNlcnQwXDANBgkqhkiG9w0BAQEF
+AANLADBIAkEAqtt6qS5GTxVxGZYWa0/4u+IwHf7p2LNZbcPBp9/OfIcYAXBQn8hO
+/Re1uwLKXdCjIoaGs4DLdG88rkzfyK5dPQIDAQABMAwGCCqGSIb3DQIFBQADQQAE
+Wc7EcF8po2/ZO6kNCwK/ICH6DobgLekA5lSLr5EvuioZniZp5lFzAw4+YzPQ7XKJ
+zl9HYIMxATFyqSiD9jsx
+-----END CERTIFICATE-----
+"""
+
+PublicCertThumbprint = u'7KITkGJF74IZ9NKVvHfuJILbuIZny6j-roaNjB1vgiA'
+
+
+class TestJWK(unittest.TestCase):
+ def test_create_pubKeys(self):
+ keylist = PublicKeys['keys']
+ for key in keylist:
+ jwk.JWK(**key)
+
+ def test_create_priKeys(self):
+ keylist = PrivateKeys['keys']
+ for key in keylist:
+ jwk.JWK(**key)
+
+ def test_create_symKeys(self):
+ keylist = SymmetricKeys['keys']
+ for key in keylist:
+ jwkey = jwk.JWK(**key)
+ jwkey.get_op_key('sign')
+ jwkey.get_op_key('verify')
+ e = jwkey.export()
+ self.assertEqual(json_decode(e), key)
+
+ jwk.JWK(**Useofx5c)
+ jwk.JWK(**RSAPrivateKey)
+
+ def test_generate_keys(self):
+ 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', alg='A192KW', kid='MySymmetricKey')
+ self.assertEqual(k.key_id, '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')
+ self.assertEqual(k.get_op_key('encrypt').key_size, 4096)
+
+ def test_export_public_keys(self):
+ k = jwk.JWK(**RSAPrivateKey)
+ 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)
+
+ def test_generate_oct_key(self):
+ key = jwk.JWK.generate(kty='oct', size=128)
+ e = jwe.JWE('test', '{"alg":"A128KW","enc":"A128GCM"}')
+ e.add_recipient(key)
+ enc = e.serialize()
+ e.deserialize(enc, key)
+ self.assertEqual(e.payload.decode('utf-8'), 'test')
+
+ def test_generate_EC_key(self):
+ # Backwards compat curve
+ key = jwk.JWK.generate(kty='EC', curve='P-256')
+ key.get_curve('P-256')
+ # New param
+ key = jwk.JWK.generate(kty='EC', crv='P-521')
+ key.get_curve('P-521')
+ # New param prevails
+ key = jwk.JWK.generate(kty='EC', curve='P-256', crv='P-521')
+ key.get_curve('P-521')
+
+ 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')
+ 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')
+ 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)
+ self.assertRaises(jwk.InvalidJWKValue,
+ jwk.JWK.from_pyca, dict())
+
+ def test_jwkset(self):
+ k = jwk.JWK(**RSAPrivateKey)
+ ks = jwk.JWKSet()
+ ks.add(k)
+ ks2 = jwk.JWKSet().import_keyset(ks.export())
+ self.assertEqual(len(ks), len(ks2))
+ 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'])
+ # test class method import too
+ 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])
+
+ def test_import_from_pem(self):
+ pubk = jwk.JWK.from_pem(RSAPublicPEM)
+ self.assertEqual(pubk.export_to_pem(), RSAPublicPEM)
+ rsapub = pubk.get_op_key('verify')
+
+ prik = jwk.JWK.from_pem(RSAPrivatePEM, password=RSAPrivatePassword)
+ rsapri = prik.get_op_key('sign')
+ self.assertEqual(rsapri.public_key().public_numbers().n,
+ rsapub.public_numbers().n)
+
+ pubc = jwk.JWK.from_pem(PublicCert)
+ self.assertEqual(pubc.key_id, PublicCertThumbprint)
+
+ def test_export_symmetric(self):
+ key = jwk.JWK(**SymmetricKeys['keys'][0])
+ self.assertTrue(key.is_symmetric)
+ self.assertFalse(key.has_public)
+ self.assertFalse(key.has_private)
+ self.assertEqual(json_encode(SymmetricKeys['keys'][0]),
+ key.export_symmetric())
+
+ def test_export_public(self):
+ key = jwk.JWK.from_pem(PublicCert)
+ self.assertFalse(key.is_symmetric)
+ self.assertTrue(key.has_public)
+ self.assertFalse(key.has_private)
+ pubc = key.export_public()
+ self.assertEqual(json_decode(pubc)["kid"], PublicCertThumbprint)
+
+ def test_export_private(self):
+ key = jwk.JWK.from_pem(RSAPrivatePEM, password=RSAPrivatePassword)
+ self.assertFalse(key.is_symmetric)
+ self.assertTrue(key.has_public)
+ self.assertTrue(key.has_private)
+ pri = key.export_private()
+ prikey = jwk.JWK(**json_decode(pri))
+ self.assertTrue(prikey.has_private)
+ pub = key.export_public()
+ pubkey = jwk.JWK(**json_decode(pub))
+ self.assertFalse(pubkey.has_private)
+ self.assertEqual(prikey.key_id, pubkey.key_id)
+
+
+# RFC 7515 - A.1
+A1_protected = \
+ [123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32,
+ 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125]
+A1_payload = \
+ [123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10,
+ 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56,
+ 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97,
+ 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111,
+ 111, 116, 34, 58, 116, 114, 117, 101, 125]
+A1_signature = \
+ [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
+ 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
+ 132, 141, 121]
+A1_example = {'key': SymmetricKeys['keys'][1],
+ 'alg': 'HS256',
+ 'protected': bytes(bytearray(A1_protected)).decode('utf-8'),
+ 'payload': bytes(bytearray(A1_payload)),
+ 'signature': bytes(bytearray(A1_signature))}
+
+# RFC 7515 - A.2
+A2_protected = \
+ [123, 34, 97, 108, 103, 34, 58, 34, 82, 83, 50, 53, 54, 34, 125]
+A2_payload = A1_payload
+A2_key = \
+ {"kty": "RSA",
+ "n": "ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx"
+ "HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs"
+ "D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH"
+ "SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV"
+ "MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8"
+ "NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
+ "e": "AQAB",
+ "d": "Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I"
+ "jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0"
+ "BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn"
+ "439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT"
+ "CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh"
+ "BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ",
+ "p": "4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi"
+ "YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG"
+ "BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc",
+ "q": "uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa"
+ "ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA"
+ "-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc",
+ "dp": "BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q"
+ "CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb"
+ "34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0",
+ "dq": "h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa"
+ "7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky"
+ "NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU",
+ "qi": "IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o"
+ "y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU"
+ "W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U"}
+A2_signature = \
+ [112, 46, 33, 137, 67, 232, 143, 209, 30, 181, 216, 45, 191, 120, 69,
+ 243, 65, 6, 174, 27, 129, 255, 247, 115, 17, 22, 173, 209, 113, 125,
+ 131, 101, 109, 66, 10, 253, 60, 150, 238, 221, 115, 162, 102, 62, 81,
+ 102, 104, 123, 0, 11, 135, 34, 110, 1, 135, 237, 16, 115, 249, 69,
+ 229, 130, 173, 252, 239, 22, 216, 90, 121, 142, 232, 198, 109, 219,
+ 61, 184, 151, 91, 23, 208, 148, 2, 190, 237, 213, 217, 217, 112, 7,
+ 16, 141, 178, 129, 96, 213, 248, 4, 12, 167, 68, 87, 98, 184, 31,
+ 190, 127, 249, 217, 46, 10, 231, 111, 36, 242, 91, 51, 187, 230, 244,
+ 74, 230, 30, 177, 4, 10, 203, 32, 4, 77, 62, 249, 18, 142, 212, 1,
+ 48, 121, 91, 212, 189, 59, 65, 238, 202, 208, 102, 171, 101, 25, 129,
+ 253, 228, 141, 247, 127, 55, 45, 195, 139, 159, 175, 221, 59, 239,
+ 177, 139, 93, 163, 204, 60, 46, 176, 47, 158, 58, 65, 214, 18, 202,
+ 173, 21, 145, 18, 115, 160, 95, 35, 185, 232, 56, 250, 175, 132, 157,
+ 105, 132, 41, 239, 90, 30, 136, 121, 130, 54, 195, 212, 14, 96, 69,
+ 34, 165, 68, 200, 242, 122, 122, 45, 184, 6, 99, 209, 108, 247, 202,
+ 234, 86, 222, 64, 92, 178, 33, 90, 69, 178, 194, 85, 102, 181, 90,
+ 193, 167, 72, 160, 112, 223, 200, 163, 42, 70, 149, 67, 208, 25, 238,
+ 251, 71]
+A2_example = {'key': A2_key,
+ 'alg': 'RS256',
+ 'protected': bytes(bytearray(A2_protected)).decode('utf-8'),
+ 'payload': bytes(bytearray(A2_payload)),
+ 'signature': bytes(bytearray(A2_signature))}
+
+# RFC 7515 - A.3
+A3_protected = \
+ [123, 34, 97, 108, 103, 34, 58, 34, 69, 83, 50, 53, 54, 34, 125]
+A3_payload = A2_payload
+A3_key = \
+ {"kty": "EC",
+ "crv": "P-256",
+ "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
+ "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
+ "d": "jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI"}
+A3_signature = \
+ [14, 209, 33, 83, 121, 99, 108, 72, 60, 47, 127, 21, 88,
+ 7, 212, 2, 163, 178, 40, 3, 58, 249, 124, 126, 23, 129,
+ 154, 195, 22, 158, 166, 101] + \
+ [197, 10, 7, 211, 140, 60, 112, 229, 216, 241, 45, 175,
+ 8, 74, 84, 128, 166, 101, 144, 197, 242, 147, 80, 154,
+ 143, 63, 127, 138, 131, 163, 84, 213]
+A3_example = {'key': A3_key,
+ 'alg': 'ES256',
+ 'protected': bytes(bytearray(A3_protected)).decode('utf-8'),
+ 'payload': bytes(bytearray(A3_payload)),
+ 'signature': bytes(bytearray(A3_signature))}
+
+
+# RFC 7515 - A.4
+A4_protected = \
+ [123, 34, 97, 108, 103, 34, 58, 34, 69, 83, 53, 49, 50, 34, 125]
+A4_payload = [80, 97, 121, 108, 111, 97, 100]
+A4_key = \
+ {"kty": "EC",
+ "crv": "P-521",
+ "x": "AekpBQ8ST8a8VcfVOTNl353vSrDCLLJXmPk06wTjxrrjcBpXp5EOnYG_"
+ "NjFZ6OvLFV1jSfS9tsz4qUxcWceqwQGk",
+ "y": "ADSmRA43Z1DSNx_RvcLI87cdL07l6jQyyBXMoxVg_l2Th-x3S1WDhjDl"
+ "y79ajL4Kkd0AZMaZmh9ubmf63e3kyMj2",
+ "d": "AY5pb7A0UFiB3RELSD64fTLOSV_jazdF7fLYyuTw8lOfRhWg6Y6rUrPA"
+ "xerEzgdRhajnu0ferB0d53vM9mE15j2C"}
+A4_signature = \
+ [1, 220, 12, 129, 231, 171, 194, 209, 232, 135, 233, 117, 247, 105,
+ 122, 210, 26, 125, 192, 1, 217, 21, 82, 91, 45, 240, 255, 83, 19,
+ 34, 239, 71, 48, 157, 147, 152, 105, 18, 53, 108, 163, 214, 68,
+ 231, 62, 153, 150, 106, 194, 164, 246, 72, 143, 138, 24, 50, 129,
+ 223, 133, 206, 209, 172, 63, 237, 119, 109] + \
+ [0, 111, 6, 105, 44, 5, 41, 208, 128, 61, 152, 40, 92, 61, 152, 4,
+ 150, 66, 60, 69, 247, 196, 170, 81, 193, 199, 78, 59, 194, 169,
+ 16, 124, 9, 143, 42, 142, 131, 48, 206, 238, 34, 175, 83, 203,
+ 220, 159, 3, 107, 155, 22, 27, 73, 111, 68, 68, 21, 238, 144, 229,
+ 232, 148, 188, 222, 59, 242, 103]
+A4_example = {'key': A4_key,
+ 'alg': 'ES512',
+ 'protected': bytes(bytearray(A4_protected)).decode('utf-8'),
+ 'payload': bytes(bytearray(A4_payload)),
+ 'signature': bytes(bytearray(A4_signature))}
+
+
+# RFC 7515 - A.4
+A5_protected = 'eyJhbGciOiJub25lIn0'
+A5_payload = A2_payload
+A5_key = \
+ {"kty": "oct", "k": ""}
+A5_signature = b''
+A5_example = {'key': A5_key,
+ 'alg': 'none',
+ 'protected': base64url_decode(A5_protected).decode('utf-8'),
+ 'payload': bytes(bytearray(A5_payload)),
+ 'signature': A5_signature}
+
+A6_serialized = \
+ '{' + \
+ '"payload":' + \
+ '"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF' + \
+ 'tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",' + \
+ '"signatures":[' + \
+ '{"protected":"eyJhbGciOiJSUzI1NiJ9",' + \
+ '"header":' + \
+ '{"kid":"2010-12-29"},' + \
+ '"signature":' + \
+ '"cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZ' + \
+ 'mh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjb' + \
+ 'KBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHl' + \
+ 'b1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZES' + \
+ 'c6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AX' + \
+ 'LIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"},' + \
+ '{"protected":"eyJhbGciOiJFUzI1NiJ9",' + \
+ '"header":' + \
+ '{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"},' + \
+ '"signature":' + \
+ '"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS' + \
+ 'lSApmWQxfKTUJqPP3-Kg6NU1Q"}]' + \
+ '}'
+A6_example = {
+ 'payload': bytes(bytearray(A2_payload)),
+ 'key1': jwk.JWK(**A2_key),
+ 'protected1': bytes(bytearray(A2_protected)).decode('utf-8'),
+ 'header1': json_encode({"kid": "2010-12-29"}),
+ 'key2': jwk.JWK(**A3_key),
+ 'protected2': bytes(bytearray(A3_protected)).decode('utf-8'),
+ 'header2': json_encode({"kid": "e9bc097a-ce51-4036-9562-d2ade882db0d"}),
+ 'serialized': A6_serialized}
+
+A7_example = \
+ '{' + \
+ '"payload":' + \
+ '"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF' + \
+ 'tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",' + \
+ '"protected":"eyJhbGciOiJFUzI1NiJ9",' + \
+ '"header":' + \
+ '{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"},' + \
+ '"signature":' + \
+ '"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS' + \
+ 'lSApmWQxfKTUJqPP3-Kg6NU1Q"' + \
+ '}'
+
+E_negative = \
+ 'eyJhbGciOiJub25lIiwNCiAiY3JpdCI6WyJodHRwOi8vZXhhbXBsZS5jb20vVU5ERU' + \
+ 'ZJTkVEIl0sDQogImh0dHA6Ly9leGFtcGxlLmNvbS9VTkRFRklORUQiOnRydWUNCn0.' + \
+ 'RkFJTA.'
+
+
+class TestJWS(unittest.TestCase):
+ def check_sign(self, test):
+ s = jws.JWSCore(test['alg'],
+ jwk.JWK(**test['key']),
+ test['protected'],
+ test['payload'],
+ test.get('allowed_algs', None))
+ sig = s.sign()
+ decsig = base64url_decode(sig['signature'])
+ s.verify(decsig)
+ # ECDSA signatures are always different every time
+ # they are generated unlike RSA or symmetric ones
+ if test['key']['kty'] != 'EC':
+ self.assertEqual(decsig, test['signature'])
+ else:
+ # Check we can verify the test signature independently
+ # this is so taht we can test the ECDSA agaist a known
+ # good signature
+ s.verify(test['signature'])
+
+ def test_A1(self):
+ self.check_sign(A1_example)
+
+ def test_A2(self):
+ self.check_sign(A2_example)
+
+ def test_A3(self):
+ self.check_sign(A3_example)
+
+ def test_A4(self):
+ self.check_sign(A4_example)
+
+ def test_A5(self):
+ self.assertRaises(jws.InvalidJWSOperation,
+ self.check_sign, A5_example)
+ a5_bis = {'allowed_algs': ['none']}
+ a5_bis.update(A5_example)
+ with self.assertRaises(jws.InvalidJWSSignature):
+ self.check_sign(a5_bis)
+
+ def test_A6(self):
+ s = jws.JWS(A6_example['payload'])
+ s.add_signature(A6_example['key1'], None,
+ A6_example['protected1'],
+ A6_example['header1'])
+ s.add_signature(A6_example['key2'], None,
+ A6_example['protected2'],
+ A6_example['header2'])
+ s.verify(A6_example['key1'])
+ s.verify(A6_example['key2'])
+ sig = s.serialize()
+ s.deserialize(sig, A6_example['key1'])
+ s.deserialize(A6_serialized, A6_example['key2'])
+
+ def test_A7(self):
+ s = jws.JWS(A6_example['payload'])
+ s.deserialize(A7_example, A6_example['key2'])
+
+ def test_E(self):
+ s = jws.JWS(A6_example['payload'])
+ with self.assertRaises(jws.InvalidJWSSignature):
+ jws.InvalidJWSSignature(s.deserialize, E_negative)
+ s.verify(None)
+
+
+E_A1_plaintext = \
+ [84, 104, 101, 32, 116, 114, 117, 101, 32, 115, 105, 103, 110, 32,
+ 111, 102, 32, 105, 110, 116, 101, 108, 108, 105, 103, 101, 110, 99,
+ 101, 32, 105, 115, 32, 110, 111, 116, 32, 107, 110, 111, 119, 108,
+ 101, 100, 103, 101, 32, 98, 117, 116, 32, 105, 109, 97, 103, 105,
+ 110, 97, 116, 105, 111, 110, 46]
+E_A1_protected = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ"
+E_A1_key = \
+ {"kty": "RSA",
+ "n": "oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW"
+ "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S"
+ "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a"
+ "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS"
+ "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj"
+ "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw",
+ "e": "AQAB",
+ "d": "kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N"
+ "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9"
+ "3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk"
+ "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl"
+ "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd"
+ "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ",
+ "p": "1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-"
+ "SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf"
+ "fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0",
+ "q": "wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm"
+ "UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX"
+ "IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc",
+ "dp": "ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL"
+ "hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827"
+ "rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE",
+ "dq": "Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj"
+ "ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB"
+ "UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis",
+ "qi": "VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7"
+ "AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3"
+ "eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY"}
+E_A1_vector = \
+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ." \
+ "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe" \
+ "ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb" \
+ "Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV" \
+ "mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8" \
+ "1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi" \
+ "6UklfCpIMfIjf7iGdXKHzg." \
+ "48V1_ALb6US04U3b." \
+ "5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji" \
+ "SdiwkIr3ajwQzaBtQD_A." \
+ "XFBoMYUZodetZdvTiFvSkQ"
+
+E_A1_ex = {'key': jwk.JWK(**E_A1_key),
+ 'protected': base64url_decode(E_A1_protected),
+ 'plaintext': bytes(bytearray(E_A1_plaintext)),
+ 'vector': E_A1_vector}
+
+E_A2_plaintext = "Live long and prosper."
+E_A2_protected = "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0"
+E_A2_key = \
+ {"kty": "RSA",
+ "n": "sXchDaQebHnPiGvyDOAT4saGEUetSyo9MKLOoWFsueri23bOdgWp4Dy1Wl"
+ "UzewbgBHod5pcM9H95GQRV3JDXboIRROSBigeC5yjU1hGzHHyXss8UDpre"
+ "cbAYxknTcQkhslANGRUZmdTOQ5qTRsLAt6BTYuyvVRdhS8exSZEy_c4gs_"
+ "7svlJJQ4H9_NxsiIoLwAEk7-Q3UXERGYw_75IDrGA84-lA_-Ct4eTlXHBI"
+ "Y2EaV7t7LjJaynVJCpkv4LKjTTAumiGUIuQhrNhZLuF_RJLqHpM2kgWFLU"
+ "7-VTdL1VbC2tejvcI2BlMkEpk1BzBZI0KQB0GaDWFLN-aEAw3vRw",
+ "e": "AQAB",
+ "d": "VFCWOqXr8nvZNyaaJLXdnNPXZKRaWCjkU5Q2egQQpTBMwhprMzWzpR8Sxq"
+ "1OPThh_J6MUD8Z35wky9b8eEO0pwNS8xlh1lOFRRBoNqDIKVOku0aZb-ry"
+ "nq8cxjDTLZQ6Fz7jSjR1Klop-YKaUHc9GsEofQqYruPhzSA-QgajZGPbE_"
+ "0ZaVDJHfyd7UUBUKunFMScbflYAAOYJqVIVwaYR5zWEEceUjNnTNo_CVSj"
+ "-VvXLO5VZfCUAVLgW4dpf1SrtZjSt34YLsRarSb127reG_DUwg9Ch-Kyvj"
+ "T1SkHgUWRVGcyly7uvVGRSDwsXypdrNinPA4jlhoNdizK2zF2CWQ",
+ "p": "9gY2w6I6S6L0juEKsbeDAwpd9WMfgqFoeA9vEyEUuk4kLwBKcoe1x4HG68"
+ "ik918hdDSE9vDQSccA3xXHOAFOPJ8R9EeIAbTi1VwBYnbTp87X-xcPWlEP"
+ "krdoUKW60tgs1aNd_Nnc9LEVVPMS390zbFxt8TN_biaBgelNgbC95sM",
+ "q": "uKlCKvKv_ZJMVcdIs5vVSU_6cPtYI1ljWytExV_skstvRSNi9r66jdd9-y"
+ "BhVfuG4shsp2j7rGnIio901RBeHo6TPKWVVykPu1iYhQXw1jIABfw-MVsN"
+ "-3bQ76WLdt2SDxsHs7q7zPyUyHXmps7ycZ5c72wGkUwNOjYelmkiNS0",
+ "dp": "w0kZbV63cVRvVX6yk3C8cMxo2qCM4Y8nsq1lmMSYhG4EcL6FWbX5h9yuv"
+ "ngs4iLEFk6eALoUS4vIWEwcL4txw9LsWH_zKI-hwoReoP77cOdSL4AVcra"
+ "Hawlkpyd2TWjE5evgbhWtOxnZee3cXJBkAi64Ik6jZxbvk-RR3pEhnCs",
+ "dq": "o_8V14SezckO6CNLKs_btPdFiO9_kC1DsuUTd2LAfIIVeMZ7jn1Gus_Ff"
+ "7B7IVx3p5KuBGOVF8L-qifLb6nQnLysgHDh132NDioZkhH7mI7hPG-PYE_"
+ "odApKdnqECHWw0J-F0JWnUd6D2B_1TvF9mXA2Qx-iGYn8OVV1Bsmp6qU",
+ "qi": "eNho5yRBEBxhGBtQRww9QirZsB66TrfFReG_CcteI1aCneT0ELGhYlRlC"
+ "tUkTRclIfuEPmNsNDPbLoLqqCVznFbvdB7x-Tl-m0l_eFTj2KiqwGqE9PZ"
+ "B9nNTwMVvH3VRRSLWACvPnSiwP8N5Usy-WRXS-V7TbpxIhvepTfE0NNo"}
+E_A2_vector = \
+ "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." \
+ "UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm" \
+ "1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7Pc" \
+ "HALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIF" \
+ "NPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8" \
+ "rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv" \
+ "-B3oWh2TbqmScqXMR4gp_A." \
+ "AxY8DCtDaGlsbGljb3RoZQ." \
+ "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY." \
+ "9hH0vgRfYgPnAHOd8stkvw"
+
+E_A2_ex = {'key': jwk.JWK(**E_A2_key),
+ 'protected': base64url_decode(E_A2_protected),
+ 'plaintext': E_A2_plaintext,
+ 'vector': E_A2_vector}
+
+E_A3_plaintext = "Live long and prosper."
+E_A3_protected = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0"
+E_A3_key = {"kty": "oct", "k": "GawgguFyGrWKav7AX4VKUg"}
+E_A3_vector = \
+ "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." \
+ "6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ." \
+ "AxY8DCtDaGlsbGljb3RoZQ." \
+ "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY." \
+ "U0m_YmjN04DJvceFICbCVQ"
+
+E_A3_ex = {'key': jwk.JWK(**E_A3_key),
+ 'protected': base64url_decode(E_A3_protected).decode('utf-8'),
+ 'plaintext': E_A3_plaintext,
+ 'vector': E_A3_vector}
+
+E_A4_protected = "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0"
+E_A4_unprotected = {"jku": "https://server.example.com/keys.jwks"}
+E_A4_vector = \
+ '{"protected":"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",' \
+ '"unprotected":{"jku":"https://server.example.com/keys.jwks"},' \
+ '"recipients":[' \
+ '{"header":{"alg":"RSA1_5","kid":"2011-04-29"},' \
+ '"encrypted_key":'\
+ '"UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-' \
+ 'kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx' \
+ 'GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3' \
+ 'YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh' \
+ 'cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg' \
+ 'wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A"},' \
+ '{"header":{"alg":"A128KW","kid":"7"},' \
+ '"encrypted_key":' \
+ '"6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ"}],' \
+ '"iv":"AxY8DCtDaGlsbGljb3RoZQ",' \
+ '"ciphertext":"KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",' \
+ '"tag":"Mz-VPPyU4RlcuYv1IwIvzw"}'
+
+E_A4_ex = {'key1': jwk.JWK(**E_A2_key),
+ 'header1': '{"alg":"RSA1_5","kid":"2011-04-29"}',
+ 'key2': jwk.JWK(**E_A3_key),
+ 'header2': '{"alg":"A128KW","kid":"7"}',
+ 'protected': base64url_decode(E_A4_protected),
+ 'unprotected': json_encode(E_A4_unprotected),
+ 'plaintext': E_A3_plaintext,
+ 'vector': E_A4_vector}
+
+E_A5_ex = \
+ '{"protected":"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",' \
+ '"unprotected":{"jku":"https://server.example.com/keys.jwks"},' \
+ '"header":{"alg":"A128KW","kid":"7"},' \
+ '"encrypted_key":' \
+ '"6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ",' \
+ '"iv":"AxY8DCtDaGlsbGljb3RoZQ",' \
+ '"ciphertext":"KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",' \
+ '"tag":"Mz-VPPyU4RlcuYv1IwIvzw"}'
+
+
+class TestJWE(unittest.TestCase):
+ def check_enc(self, plaintext, protected, key, vector):
+ e = jwe.JWE(plaintext, protected)
+ e.add_recipient(key)
+ # Encrypt and serialize using compact
+ enc = e.serialize()
+ # And test that we can decrypt our own
+ e.deserialize(enc, key)
+ # Now test the Spec Test Vector
+ e.deserialize(vector, key)
+
+ def test_A1(self):
+ self.check_enc(E_A1_ex['plaintext'], E_A1_ex['protected'],
+ E_A1_ex['key'], E_A1_ex['vector'])
+
+ def test_A2(self):
+ self.check_enc(E_A2_ex['plaintext'], E_A2_ex['protected'],
+ E_A2_ex['key'], E_A2_ex['vector'])
+
+ def test_A3(self):
+ self.check_enc(E_A3_ex['plaintext'], E_A3_ex['protected'],
+ E_A3_ex['key'], E_A3_ex['vector'])
+
+ def test_A4(self):
+ e = jwe.JWE(E_A4_ex['plaintext'], E_A4_ex['protected'])
+ 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()
+ e.deserialize(enc, E_A4_ex['key1'])
+ e.deserialize(enc, E_A4_ex['key2'])
+ # Now test the Spec Test Vector
+ e.deserialize(E_A4_ex['vector'], E_A4_ex['key1'])
+ e.deserialize(E_A4_ex['vector'], E_A4_ex['key2'])
+
+ def test_A5(self):
+ e = jwe.JWE()
+ e.deserialize(E_A5_ex, E_A4_ex['key2'])
+ with self.assertRaises(jwe.InvalidJWEData):
+ e = jwe.JWE(algs=['A256KW'])
+ e.deserialize(E_A5_ex, E_A4_ex['key2'])
+
+
+MMA_vector_key = jwk.JWK(**E_A2_key)
+MMA_vector_ok_cek = \
+ '{"protected":"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",' \
+ '"unprotected":{"jku":"https://server.example.com/keys.jwks"},' \
+ '"recipients":[' \
+ '{"header":{"alg":"RSA1_5","kid":"2011-04-29"},' \
+ '"encrypted_key":'\
+ '"UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-' \
+ 'kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx' \
+ 'GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3' \
+ 'YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh' \
+ 'cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg' \
+ 'wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A"}],' \
+ '"iv":"AxY8DCtDaGlsbGljb3RoZQ",' \
+ '"ciphertext":"PURPOSEFULLYBROKENYGS4HffxPSUrfmqCHXaI9wOGY",' \
+ '"tag":"Mz-VPPyU4RlcuYv1IwIvzw"}'
+MMA_vector_ko_cek = \
+ '{"protected":"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",' \
+ '"unprotected":{"jku":"https://server.example.com/keys.jwks"},' \
+ '"recipients":[' \
+ '{"header":{"alg":"RSA1_5","kid":"2011-04-29"},' \
+ '"encrypted_key":'\
+ '"UGhIOguC7IuEvf_NPVaYsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-' \
+ 'kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx' \
+ 'GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3' \
+ 'YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh' \
+ 'cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg' \
+ 'wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A"}],' \
+ '"iv":"AxY8DCtDaGlsbGljb3RoZQ",' \
+ '"ciphertext":"PURPOSEFULLYBROKENYGS4HffxPSUrfmqCHXaI9wOGY",' \
+ '"tag":"Mz-VPPyU4RlcuYv1IwIvzw"}'
+
+
+class TestMMA(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ import os
+ cls.enableMMA = os.environ.get('JWCRYPTO_TESTS_ENABLE_MMA', False)
+ cls.iterations = 500
+ cls.sub_iterations = 100
+
+ def test_MMA(self):
+ if self.enableMMA:
+
+ print('Testing MMA timing attacks')
+
+ ok_cek = 0
+ ok_e = jwe.JWE()
+ ok_e.deserialize(MMA_vector_ok_cek)
+ ko_cek = 0
+ ko_e = jwe.JWE()
+ ko_e.deserialize(MMA_vector_ko_cek)
+
+ import time
+ counter = getattr(time, 'perf_counter', time.time)
+
+ for _ in range(self.iterations):
+ start = counter()
+ for _ in range(self.sub_iterations):
+ with self.assertRaises(jwe.InvalidJWEData):
+ ok_e.decrypt(MMA_vector_key)
+ stop = counter()
+ ok_cek += (stop - start) / self.sub_iterations
+
+ start = counter()
+ for _ in range(self.sub_iterations):
+ with self.assertRaises(jwe.InvalidJWEData):
+ ko_e.decrypt(MMA_vector_key)
+ stop = counter()
+ ko_cek += (stop - start) / self.sub_iterations
+
+ ok_cek /= self.iterations
+ ko_cek /= self.iterations
+
+ deviation = ((ok_cek - ko_cek) / ok_cek) * 100
+ print('MMA ok cek: {}'.format(ok_cek))
+ print('MMA ko cek: {}'.format(ko_cek))
+ print('MMA deviation: {}% ({})'.format(int(deviation), deviation))
+ self.assertLess(deviation, 2)
+
+
+# RFC 7519
+A1_header = {
+ "alg": "RSA1_5",
+ "enc": "A128CBC-HS256"}
+
+A1_claims = {
+ "iss": "joe",
+ "exp": 1300819380,
+ "http://example.com/is_root": True}
+
+A1_token = \
+ "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." + \
+ "QR1Owv2ug2WyPBnbQrRARTeEk9kDO2w8qDcjiHnSJflSdv1iNqhWXaKH4MqAkQtM" + \
+ "oNfABIPJaZm0HaA415sv3aeuBWnD8J-Ui7Ah6cWafs3ZwwFKDFUUsWHSK-IPKxLG" + \
+ "TkND09XyjORj_CHAgOPJ-Sd8ONQRnJvWn_hXV1BNMHzUjPyYwEsRhDhzjAD26ima" + \
+ "sOTsgruobpYGoQcXUwFDn7moXPRfDE8-NoQX7N7ZYMmpUDkR-Cx9obNGwJQ3nM52" + \
+ "YCitxoQVPzjbl7WBuB7AohdBoZOdZ24WlN1lVIeh8v1K4krB8xgKvRU8kgFrEn_a" + \
+ "1rZgN5TiysnmzTROF869lQ." + \
+ "AxY8DCtDaGlsbGljb3RoZQ." + \
+ "MKOle7UQrG6nSxTLX6Mqwt0orbHvAKeWnDYvpIAeZ72deHxz3roJDXQyhxx0wKaM" + \
+ "HDjUEOKIwrtkHthpqEanSBNYHZgmNOV7sln1Eu9g3J8." + \
+ "fiK51VwhsxJ-siBMR-YFiA"
+
+A2_token = \
+ "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiSldU" + \
+ "In0." + \
+ "g_hEwksO1Ax8Qn7HoN-BVeBoa8FXe0kpyk_XdcSmxvcM5_P296JXXtoHISr_DD_M" + \
+ "qewaQSH4dZOQHoUgKLeFly-9RI11TG-_Ge1bZFazBPwKC5lJ6OLANLMd0QSL4fYE" + \
+ "b9ERe-epKYE3xb2jfY1AltHqBO-PM6j23Guj2yDKnFv6WO72tteVzm_2n17SBFvh" + \
+ "DuR9a2nHTE67pe0XGBUS_TK7ecA-iVq5COeVdJR4U4VZGGlxRGPLRHvolVLEHx6D" + \
+ "YyLpw30Ay9R6d68YCLi9FYTq3hIXPK_-dmPlOUlKvPr1GgJzRoeC9G5qCvdcHWsq" + \
+ "JGTO_z3Wfo5zsqwkxruxwA." + \
+ "UmVkbW9uZCBXQSA5ODA1Mg." + \
+ "VwHERHPvCNcHHpTjkoigx3_ExK0Qc71RMEParpatm0X_qpg-w8kozSjfNIPPXiTB" + \
+ "BLXR65CIPkFqz4l1Ae9w_uowKiwyi9acgVztAi-pSL8GQSXnaamh9kX1mdh3M_TT" + \
+ "-FZGQFQsFhu0Z72gJKGdfGE-OE7hS1zuBD5oEUfk0Dmb0VzWEzpxxiSSBbBAzP10" + \
+ "l56pPfAtrjEYw-7ygeMkwBl6Z_mLS6w6xUgKlvW6ULmkV-uLC4FUiyKECK4e3WZY" + \
+ "Kw1bpgIqGYsw2v_grHjszJZ-_I5uM-9RA8ycX9KqPRp9gc6pXmoU_-27ATs9XCvr" + \
+ "ZXUtK2902AUzqpeEUJYjWWxSNsS-r1TJ1I-FMJ4XyAiGrfmo9hQPcNBYxPz3GQb2" + \
+ "8Y5CLSQfNgKSGt0A4isp1hBUXBHAndgtcslt7ZoQJaKe_nNJgNliWtWpJ_ebuOpE" + \
+ "l8jdhehdccnRMIwAmU1n7SPkmhIl1HlSOpvcvDfhUN5wuqU955vOBvfkBOh5A11U" + \
+ "zBuo2WlgZ6hYi9-e3w29bR0C2-pp3jbqxEDw3iWaf2dc5b-LnR0FEYXvI_tYk5rd" + \
+ "_J9N0mg0tQ6RbpxNEMNoA9QWk5lgdPvbh9BaO195abQ." + \
+ "AVO9iT5AV4CzvDJCdhSFlQ"
+
+
+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.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)
+ # then try the test vector with explicit expiration date
+ t = jwt.JWT(jwt=A1_token, key=key, check_claims={'exp': 1300819380})
+ # Finally check it raises for expired tokens
+ self.assertRaises(jwt.JWTExpired, jwt.JWT, jwt=A1_token, key=key)
+
+ def test_A2(self):
+ sigkey = jwk.JWK(**A2_example['key'])
+ touter = jwt.JWT(jwt=A2_token, key=E_A2_ex['key'])
+ 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'])
+
+ def test_decrypt_keyset(self):
+ key = jwk.JWK(kid='testkey', **E_A2_key)
+ keyset = jwk.JWKSet()
+ # decrypt without keyid
+ t = jwt.JWT(A1_header, A1_claims)
+ t.make_encrypted_token(key)
+ token = t.serialize()
+ self.assertRaises(jwt.JWTMissingKeyID, jwt.JWT, jwt=token,
+ key=keyset)
+ # encrypt a new JWT
+ header = copy.copy(A1_header)
+ header['kid'] = 'testkey'
+ t = jwt.JWT(header, A1_claims)
+ t.make_encrypted_token(key)
+ token = t.serialize()
+ # try to decrypt without 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})
+
+
+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'])
+
+ def test_key_ops_values(self):
+ self.assertRaises(jwk.InvalidJWKValue, jwk.JWK,
+ kty='RSA', n=1, key_ops=['sign'], use='enc')
+ self.assertRaises(jwk.InvalidJWKValue, jwk.JWK,
+ kty='RSA', n=1, key_ops=['sign', 'sign'])
+
+ def test_jwe_no_protected_header(self):
+ enc = jwe.JWE(plaintext='plain')
+ enc.add_recipient(jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16)),
+ '{"alg":"A128KW","enc":"A128GCM"}')
+
+ def test_jwe_no_alg_in_jose_headers(self):
+ enc = jwe.JWE(plaintext='plain')
+ self.assertRaises(jwe.InvalidJWEData, enc.add_recipient,
+ jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16)),
+ '{"enc":"A128GCM"}')
+
+ def test_jwe_no_enc_in_jose_headers(self):
+ enc = jwe.JWE(plaintext='plain')
+ self.assertRaises(jwe.InvalidJWEData, enc.add_recipient,
+ jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16)),
+ '{"alg":"A128KW"}')
+
+ def test_aes_128(self):
+ enc = jwe.JWE(plaintext='plain')
+ key128 = jwk.JWK(kty='oct', k=base64url_encode(b'A' * (128 // 8)))
+ enc.add_recipient(key128, '{"alg":"A128KW","enc":"A128CBC-HS256"}')
+ enc.add_recipient(key128, '{"alg":"A128KW","enc":"A128GCM"}')
+
+ def test_aes_192(self):
+ enc = jwe.JWE(plaintext='plain')
+ key192 = jwk.JWK(kty='oct', k=base64url_encode(b'B' * (192 // 8)))
+ enc.add_recipient(key192, '{"alg":"A192KW","enc":"A192CBC-HS384"}')
+ enc.add_recipient(key192, '{"alg":"A192KW","enc":"A192GCM"}')
+
+ def test_aes_256(self):
+ enc = jwe.JWE(plaintext='plain')
+ key256 = jwk.JWK(kty='oct', k=base64url_encode(b'C' * (256 // 8)))
+ enc.add_recipient(key256, '{"alg":"A256KW","enc":"A256CBC-HS512"}')
+ enc.add_recipient(key256, '{"alg":"A256KW","enc":"A256GCM"}')
+
+ def test_jws_loopback(self):
+ sign = jws.JWS(payload='message')
+ sign.add_signature(jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16)),
+ alg="HS512")
+ o = sign.serialize()
+ check = jws.JWS()
+ check.deserialize(o, jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16)),
+ alg="HS512")
+ self.assertTrue(check.objects['valid'])
+
+ def test_jws_headers_as_dicts(self):
+ sign = jws.JWS(payload='message')
+ key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16))
+ sign.add_signature(key, protected={'alg': 'HS512'},
+ header={'kid': key.thumbprint()})
+ o = sign.serialize()
+ check = jws.JWS()
+ check.deserialize(o, key, alg="HS512")
+ self.assertTrue(check.objects['valid'])
+ self.assertEqual(check.jose_header['kid'], key.thumbprint())
+
+ def test_jwe_headers_as_dicts(self):
+ enc = jwe.JWE(plaintext='message',
+ protected={"alg": "A256KW", "enc": "A256CBC-HS512"})
+ key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * 32))
+ enc.add_recipient(key, {'kid': key.thumbprint()})
+ o = enc.serialize()
+ check = jwe.JWE()
+ check.deserialize(o)
+ check.decrypt(key)
+ self.assertEqual(check.payload, b'message')
+ self.assertEqual(
+ json_decode(check.objects['header'])['kid'], key.thumbprint())
+
+ def test_jwe_default_recipient(self):
+ key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * (128 // 8)))
+ enc = jwe.JWE(plaintext='plain',
+ protected='{"alg":"A128KW","enc":"A128GCM"}',
+ recipient=key).serialize()
+ check = jwe.JWE()
+ check.deserialize(enc, key)
+ self.assertEqual(b'plain', check.payload)
+
+
+class JWATests(unittest.TestCase):
+ def test_jwa_create(self):
+ for name, cls in jwa.JWA.algorithms_registry.items():
+ self.assertEqual(cls.name, name)
+ self.assertIn(cls.algorithm_usage_location, {'alg', 'enc'})
+ if name == 'ECDH-ES':
+ self.assertIs(cls.keysize, None)
+ else:
+ self.assertIsInstance(cls.keysize, int)
+ self.assertGreaterEqual(cls.keysize, 0)
+
+ if cls.algorithm_use == 'sig':
+ with self.assertRaises(jwa.InvalidJWAAlgorithm):
+ jwa.JWA.encryption_alg(name)
+ with self.assertRaises(jwa.InvalidJWAAlgorithm):
+ jwa.JWA.keymgmt_alg(name)
+ inst = jwa.JWA.signing_alg(name)
+ self.assertIsInstance(inst, jwa.JWAAlgorithm)
+ self.assertEqual(inst.name, name)
+ elif cls.algorithm_use == 'kex':
+ with self.assertRaises(jwa.InvalidJWAAlgorithm):
+ jwa.JWA.encryption_alg(name)
+ with self.assertRaises(jwa.InvalidJWAAlgorithm):
+ jwa.JWA.signing_alg(name)
+ inst = jwa.JWA.keymgmt_alg(name)
+ self.assertIsInstance(inst, jwa.JWAAlgorithm)
+ self.assertEqual(inst.name, name)
+ elif cls.algorithm_use == 'enc':
+ with self.assertRaises(jwa.InvalidJWAAlgorithm):
+ jwa.JWA.signing_alg(name)
+ with self.assertRaises(jwa.InvalidJWAAlgorithm):
+ jwa.JWA.keymgmt_alg(name)
+ inst = jwa.JWA.encryption_alg(name)
+ self.assertIsInstance(inst, jwa.JWAAlgorithm)
+ self.assertEqual(inst.name, name)
+ else:
+ self.fail((name, cls))
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2d857e9
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,6 @@
+[bdist_wheel]
+universal = 1
+
+[aliases]
+packages = clean --all egg_info bdist_wheel sdist --format=zip sdist --format=gztar
+release = packages register upload
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..5888c1e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2015 JWCrypto Project Contributors, see LICENSE file
+
+from setuptools import setup
+
+setup(
+ name = 'jwcrypto',
+ version = '0.4.2',
+ license = 'LGPLv3+',
+ maintainer = 'JWCrypto Project Contributors',
+ maintainer_email = 'simo@redhat.com',
+ url='https://github.com/latchset/jwcrypto',
+ 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',
+ 'Intended Audience :: Developers',
+ 'Topic :: Security',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ],
+ data_files = [('share/doc/jwcrypto', ['LICENSE', 'README.md'])],
+ install_requires = [
+ 'cryptography >= 1.5',
+ ],
+)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..488678d
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,68 @@
+[tox]
+envlist = lint,py27,py34,py35,py36,pep8py2,pep8py3,doc,sphinx
+skip_missing_interpreters = true
+
+[testenv]
+setenv =
+ PYTHONPATH = {envsitepackagesdir}
+deps =
+ pytest
+ coverage
+sitepackages = True
+commands =
+ {envpython} -bb -m coverage run -m pytest --capture=no --strict {posargs}
+ {envpython} -m coverage report -m
+
+[testenv:lint]
+basepython = python2.7
+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]
+basepython = python3
+deps =
+ flake8
+ flake8-import-order
+ pep8-naming
+commands =
+ {envpython} -m flake8 {posargs} jwcrypto
+
+[testenv:doc]
+deps =
+ doc8
+ docutils
+ markdown
+basepython = python2.7
+commands =
+ doc8 --allow-long-titles README.md
+ markdown_py README.md -f {toxworkdir}/README.md.html
+
+[testenv:sphinx]
+basepython = python2.7
+changedir = docs/source
+deps =
+ sphinx < 1.3.0
+commands =
+ sphinx-build -v -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
+
+[pytest]
+python_files = jwcrypto/test*.py
+
+[flake8]
+exclude = .tox,*.egg,dist,build,docs/source
+show-source = true
+max-line-length = 79
+ignore = N802
+application-import-names = jwcrypto