commit a004b940311eef4d324179a3041bd8593716ecb7 Author: Timo Aaltonen Date: Sat Dec 23 09:00:03 2017 +0100 Import python-jwcrypto_0.4.2.orig.tar.gz [dgit import orig python-jwcrypto_0.4.2.orig.tar.gz] 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. + 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 ' where 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 +# " v 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 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 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