├── .appveyor.yml ├── .github ├── CODEOWNERS └── workflows │ ├── codeql-analysis.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── doc ├── Makefile ├── conf.py ├── index.rst ├── jwe.rst ├── jws.rst ├── keyhandling.rst ├── make.bat └── source │ ├── index.rst │ ├── jwe.rst │ ├── jwk.rst │ ├── jws.rst │ ├── jwt.rst │ ├── jwx.rst │ ├── keybundle.rst │ ├── keyjar.rst │ ├── simple_jwt.rst │ └── utils.rst ├── mypy.ini ├── pylama.ini ├── pyproject.toml ├── pytest.ini ├── src └── cryptojwt │ ├── __init__.py │ ├── exception.py │ ├── jwe │ ├── __init__.py │ ├── aes.py │ ├── exception.py │ ├── fernet.py │ ├── jwe.py │ ├── jwe_ec.py │ ├── jwe_hmac.py │ ├── jwe_rsa.py │ ├── jwekey.py │ ├── jwenc.py │ ├── rsa.py │ └── utils.py │ ├── jwk │ ├── __init__.py │ ├── asym.py │ ├── ec.py │ ├── hmac.py │ ├── jwk.py │ ├── okp.py │ ├── rsa.py │ ├── utils.py │ ├── wrap.py │ └── x509.py │ ├── jws │ ├── __init__.py │ ├── dsa.py │ ├── eddsa.py │ ├── exception.py │ ├── hmac.py │ ├── jws.py │ ├── pss.py │ ├── rsa.py │ └── utils.py │ ├── jwt.py │ ├── jwx.py │ ├── key_bundle.py │ ├── key_issuer.py │ ├── key_jar.py │ ├── serialize │ ├── __init__.py │ └── item.py │ ├── simple_jwt.py │ ├── tools │ ├── __init__.py │ ├── jwtpeek.py │ ├── keyconv.py │ └── keygen.py │ └── utils.py └── tests ├── 570-ec-sect571r1-keypair.pem ├── cert.der ├── cert.pem ├── ec-public.pem ├── ec.pem ├── ed25519.pem ├── ed448.pem ├── invalid_ecdh.py ├── jwk_private_ec_key.json ├── jwk_private_key.json ├── rsa-public.pem ├── rsa.key ├── rsa.pub ├── server.key ├── size2048.key ├── test_01_simplejwt.py ├── test_02_jwk.py ├── test_03_key_bundle.py ├── test_04_key_issuer.py ├── test_04_key_jar.py ├── test_05_jwx.py ├── test_06_jws.py ├── test_07_jwe.py ├── test_09_jwt.py ├── test_10_jwk_wrap.py ├── test_20_jws.py ├── test_30_tools.py ├── test_31_utils.py ├── test_40_serialize.py ├── test_50_argument_alias.py ├── test_keys ├── cert.key ├── ec-p256-private.pem ├── ec-p256-public.pem ├── ec-p256.json ├── ec-p384-private.pem ├── ec-p384-public.pem ├── ec-p384.json ├── ec.key ├── jwk.json ├── rsa-1024-private.pem ├── rsa-1024-public.pem ├── rsa-1024.json ├── rsa-1280-private.pem ├── rsa-1280-public.pem ├── rsa-1280.json ├── rsa-2048-private.pem ├── rsa-2048-public.pem ├── rsa-2048.json ├── rsa-3072-private.pem ├── rsa-3072-public.pem ├── rsa-3072.json ├── rsa-4096-private.pem ├── rsa-4096-public.pem ├── rsa-4096.json └── rsa.key └── test_vector.py /.appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\Python35" 4 | PYTHON_ARCH: "32" 5 | - PYTHON: "C:\\Python35-x64" 6 | PYTHON_ARCH: "64" 7 | - PYTHON: "C:\\Python36" 8 | PYTHON_ARCH: "32" 9 | - PYTHON: "C:\\Python36-x64" 10 | PYTHON_ARCH: "64" 11 | 12 | build: off 13 | 14 | install: 15 | - "%PYTHON%\\python.exe -m pip install .[testing]" 16 | 17 | test_script: 18 | - "%PYTHON%\\python.exe -m pytest tests/ -m \"not network\"" 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @IdentityPython/oidc 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '22 14 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | pypi_release: 9 | name: Build with Poetry and Publish to PyPI 10 | runs-on: ubuntu-latest 11 | environment: 12 | name: pypi 13 | url: https://pypi.org/p/cryptojwt 14 | permissions: 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | - name: Install and configure Poetry 20 | run: pip install poetry 21 | - name: Publish package 22 | run: poetry build 23 | - name: Publish package distributions to PyPI 24 | uses: pypa/gh-action-pypi-publish@release/v1 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | pre_job: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | should_skip: ${{ steps.skip_check.outputs.should_skip }} 12 | steps: 13 | - id: skip_check 14 | uses: fkirc/skip-duplicate-actions@master 15 | with: 16 | do_not_skip: '["pull_request"]' 17 | cancel_others: 'true' 18 | concurrent_skipping: same_content 19 | ruff: 20 | needs: pre_job 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: astral-sh/ruff-action@v3 25 | with: 26 | version: latest 27 | - run: ruff check 28 | - run: ruff format --check 29 | test: 30 | needs: ruff 31 | if: ${{ needs.pre_job.outputs.should_skip != 'true' }} 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | python-version: 36 | - "3.9" 37 | - "3.10" 38 | - "3.11" 39 | - "3.12" 40 | - "3.13" 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Set up Python ${{ matrix.python-version }} 44 | uses: actions/setup-python@v5 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | - name: Install and configure Poetry 48 | run: | 49 | pip install poetry 50 | poetry config virtualenvs.in-project true 51 | - name: Install dependencies 52 | run: poetry install 53 | - name: Run pytest 54 | run: | 55 | poetry run pytest -vvv -ra --cov=cryptojwt --cov-report=xml 56 | - name: Upload coverage to Codecov 57 | uses: codecov/codecov-action@v4 58 | with: 59 | token: ${{ secrets.CODECOV_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | /.project 91 | /.pydevproject 92 | .isort.cfg~ 93 | draft/eval.py 94 | draft/idfed.xml 95 | draft/modifications 96 | draft/out 97 | draft/parse_ms.py 98 | draft/pyoidc 99 | draft/pyoidc.pub 100 | draft/seq.out 101 | draft/sequence.py 102 | draft/sequence_0.1.py 103 | fed_op/client_db.db 104 | fed_op/cp_error 105 | fed_op/jwks/ 106 | fed_op/keys/ 107 | fed_op/modules/ 108 | fed_op/static/jwks.json 109 | fed_operator/certs/ 110 | fed_operator/jwks/ 111 | fed_operator/keys/ 112 | fed_operator/statement.json 113 | fed_rp/certs/ 114 | fed_rp/keys/ 115 | fed_rp/static/ 116 | tests/data/ 117 | tests/test_weed.py 118 | .idea/ 119 | doc/_build/ 120 | doc/_static/ 121 | doc/conf.py~ 122 | fed_conf/ 123 | fo_jwks/ 124 | mds/ 125 | ms_dir/ 126 | scripts/iss.jwks 127 | scripts/request.json 128 | test_dir/ 129 | tests/mds_10/ 130 | tests/mds_6/ 131 | tests/mds_dir_6/ 132 | tests/ms_dir_10/ 133 | tests/ms_dir_6/ 134 | tests/ms_path/ 135 | tests/ms_path_6/ 136 | tests/pyoidc 137 | tests/pyoidc.pub 138 | tests/xtest_usage.py 139 | 140 | # Poetry 141 | poetry.lock 142 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-merge-conflict 6 | - id: debug-statements 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-json 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | rev: v0.9.9 13 | hooks: 14 | - id: ruff 15 | - id: ruff-format 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to JwtConnect-Python 2 | 3 | All contributions to the Python JwtConnect packages are welcome! 4 | 5 | Note that as this library is planned to be used in high-profile production code, 6 | we insist on a very high standards for the code and design, but don't feel shy: 7 | discuss your plans over 8 | [GitHub Issues](https://github.com/openid/JWTConnect-Python-OidcMsg/issues) and the 9 | [mailing list](http://lists.openid.net/mailman/listinfo/openid-specs-ab), and 10 | send in those pull requests! 11 | 12 | # Signing the Agreements 13 | 14 | In order to contribute to this project, you need to execute two legal agreements 15 | that cover your contributions. Pull requests from users who have not signed 16 | these agreements will not be merged. 17 | 18 | ## Execute the Contributor License Agreement (CLA) 19 | 20 | 1. Visit http://openid.net/contribution-license-agreement/ 21 | 2. Tap *Execute OpenID Foundation Contribution License Agreement* for the 22 | version relevant to you (Individual or Corporate). 23 | 3. Follow the instructions to sign the agreement. 24 | 25 | ## Execute the Working Group Contribution Agreement 26 | 27 | In addition to the Code License Agreement, the OpenID Foundation also requires 28 | a working group contribution agreement to cover any contributions you may make 29 | towards the OpenID Connect spec itself (e.g. in comments, bug reports, feature 30 | requests). 31 | 32 | 1. Visit http://openid.net/intellectual-property/ 33 | 2. Tap *Execute Contributor Agreement By Electronic Signature* in the box 34 | marked *Resources*. 35 | 3. Follow the instructions to sign the document, state `OpenID AB/Connect` as 36 | the Initial Working Group. 37 | 38 | # Making a Pull Request 39 | 40 | ## Before you Start 41 | 42 | Before you work on a big new feature, get in touch to make sure that your work 43 | is inline with the direction of the project and get input on your architecture. 44 | You can file an [Issue](https://github.com/openid/JWTConnect-Python-OidcMsg/issues) 45 | discussing your proposal, or email the 46 | [list](http://lists.openid.net/mailman/listinfo/openid-specs-ab). 47 | 48 | ## Coding Standards 49 | 50 | The JWTCOnnect-Python-OidcMsg library follows the 51 | [PEP8](https://www.python.org/dev/peps/pep-0008/) 52 | coding style for Python implementations. Please review your own code 53 | for adherence to the standard. 54 | 55 | ## Pull Request Reviews 56 | 57 | All pull requests, even by members who have repository write access need to be 58 | reviewed and marked as "LGTM" before they will be merged. 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | test: 4 | poetry run pytest --ruff --ruff-format --cov 5 | 6 | reformat: 7 | poetry run ruff check --select I --fix src tests 8 | poetry run ruff format src tests 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cryptojwt 2 | 3 | ![License](https://img.shields.io/badge/license-Apache%202-blue.svg) 4 | ![Python version](https://img.shields.io/badge/python-3.7%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%203.12%203.13-blue.svg) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | 7 | An implementation of the JSON cryptographic specs JWS, JWE, JWK, and JWA [RFC 7515-7518] and JSON Web Token (JWT) [RFC 7519] 8 | 9 | Please read the [Official Documentation](https://cryptojwt.readthedocs.io/en/latest/) for getting usage examples and further informations. 10 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: main 3 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = CryptoJWT 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # ruff: noqa 4 | # -*- coding: utf-8 -*- 5 | # 6 | # CryptoJWT documentation build configuration file, created by 7 | # sphinx-quickstart on Wed Oct 3 09:24:06 2018. 8 | # 9 | # This file is execfile()d with the current directory set to its 10 | # containing dir. 11 | # 12 | # Note that not all possible configuration values are present in this 13 | # autogenerated file. 14 | # 15 | # All configuration values have a default; values that are commented out 16 | # serve to show the default. 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | # 22 | # import os 23 | # import sys 24 | # sys.path.insert(0, os.path.abspath('.')) 25 | 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.intersphinx", 39 | "sphinx.ext.coverage", 40 | "sphinx.ext.viewcode", 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ["_templates"] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = ".rst" 51 | 52 | # The master toctree document. 53 | master_doc = "index" 54 | 55 | # General information about the project. 56 | project = "CryptoJWT" 57 | copyright = "2018, Roland Hedberg" 58 | author = "Roland Hedberg" 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = "0.4" 66 | # The full version, including alpha/beta/rc tags. 67 | release = "0.4" 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This patterns also effect to html_static_path and html_extra_path 79 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = "sphinx" 83 | 84 | # If true, `todo` and `todoList` produce output, else they produce nothing. 85 | todo_include_todos = False 86 | 87 | 88 | # -- Options for HTML output ---------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = "default" 94 | 95 | # Theme options are theme-specific and customize the look and feel of a theme 96 | # further. For a list of options available for each theme, see the 97 | # documentation. 98 | # 99 | # html_theme_options = {} 100 | 101 | # Add any paths that contain custom static files (such as style sheets) here, 102 | # relative to this directory. They are copied after the builtin static files, 103 | # so a file named "default.css" will overwrite the builtin "default.css". 104 | html_static_path = ["_static"] 105 | 106 | # Custom sidebar templates, must be a dictionary that maps document names 107 | # to template names. 108 | # 109 | # This is required for the alabaster theme 110 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 111 | html_sidebars = { 112 | "**": [ 113 | "relations.html", # needs 'show_related': True theme option to display 114 | "searchbox.html", 115 | ] 116 | } 117 | 118 | 119 | # -- Options for HTMLHelp output ------------------------------------------ 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = "CryptoJWTdoc" 123 | 124 | 125 | # -- Options for LaTeX output --------------------------------------------- 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | # The font size ('10pt', '11pt' or '12pt'). 132 | # 133 | # 'pointsize': '10pt', 134 | # Additional stuff for the LaTeX preamble. 135 | # 136 | # 'preamble': '', 137 | # Latex figure (float) alignment 138 | # 139 | # 'figure_align': 'htbp', 140 | } 141 | 142 | # Grouping the document tree into LaTeX files. List of tuples 143 | # (source start file, target name, title, 144 | # author, documentclass [howto, manual, or own class]). 145 | latex_documents = [ 146 | (master_doc, "CryptoJWT.tex", "CryptoJWT Documentation", "Roland Hedberg", "manual"), 147 | ] 148 | 149 | 150 | # -- Options for manual page output --------------------------------------- 151 | 152 | # One entry per manual page. List of tuples 153 | # (source start file, name, description, authors, manual section). 154 | man_pages = [(master_doc, "cryptojwt", "CryptoJWT Documentation", [author], 1)] 155 | 156 | 157 | # -- Options for Texinfo output ------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | ( 164 | master_doc, 165 | "CryptoJWT", 166 | "CryptoJWT Documentation", 167 | author, 168 | "CryptoJWT", 169 | "One line description of project.", 170 | "Miscellaneous", 171 | ), 172 | ] 173 | 174 | 175 | # Example configuration for intersphinx: refer to the Python standard library. 176 | intersphinx_mapping = {"https://docs.python.org/": None} 177 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. CryptoJWT documentation master file, created by 2 | sphinx-quickstart on Wed Oct 3 09:24:06 2018. 3 | 4 | Welcome to CryptoJWT's documentation! 5 | ===================================== 6 | 7 | CryptoJWT is supposed to provide you (a Python programmer) with all you need, 8 | to deal with what is described in RFC 7515-7519. 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | keyhandling 14 | jws 15 | jwe 16 | source/index.rst 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /doc/jwe.rst: -------------------------------------------------------------------------------- 1 | .. _jwe: 2 | 3 | JSON Web Encryption (JWE) 4 | ========================= 5 | 6 | JSON Web Encryption (JWE) represents encrypted content using JSON-based data 7 | structures. 8 | 9 | It is assumed that you know all you need to know about key handling if not 10 | please spend some time reading keyhandling_ . 11 | 12 | When it comes to JWE there are basically 2 things you want to be able to do: 13 | encrypt some data and decrypt some encrypted data. I'll deal with 14 | them in that order. 15 | 16 | Encrypting a document 17 | --------------------- 18 | 19 | This is the high level way of doing things. 20 | There are a few steps you have to go through. Let us start with an example and then break it into its parts:: 21 | 22 | >>> from cryptojwt.jwk.rsa import RSAKey 23 | >>> from cryptojwt.jwe.jwe import JWE 24 | 25 | >>> priv_key = import_private_rsa_key_from_file(KEY) 26 | >>> pub_key = priv_key.public_key() 27 | >>> encryption_key = RSAKey(use="enc", pub_key=pub_key, kid="some-key-id") 28 | >>> plain = b'Now is the time for all good men to come to the aid of ...' 29 | >>> encryptor = JWE(plain, alg="RSA-OAEP", enc="A256CBC-HS512") 30 | >>> jwe = encryptor.encrypt(keys=[encryption_key], kid="some-key-id") 31 | 32 | The steps: 33 | 34 | 1. You need an encryption key. The key *MUST* be an instance of 35 | :py:class:`cryptojwt.jwk.JWK`. 36 | 2. You need the information that is to be signed. It must be in the form of a string. 37 | 3. You initiate the encryptor, provide it with the message and other 38 | needed information. 39 | 4. And then you encrypt as described in RFC7516_ . 40 | 41 | There is a lower level way of doing the same, it will look like this:: 42 | 43 | >>> from cryptojwt.jwk.rsa import import_private_rsa_key_from_file 44 | >>> from cryptojwt.jwe.jwe_rsa import JWE_RSA 45 | 46 | >>> priv_key = import_private_rsa_key_from_file('certs/key.pem') 47 | >>> pub_key = priv_key.public_key() 48 | >>> plain = b'Now is the time for all good men to come to the aid of ...' 49 | >>> _rsa = JWE_RSA(plain, alg="RSA-OAEP", enc="A128CBC-HS256") 50 | >>> jwe = _rsa.encrypt(pub_key) 51 | 52 | Here the key is an cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey 53 | instance and the encryptor is a :py:class:`cryptojwt.jwe.jew_rsa.JWE_RSA` 54 | instance. 55 | 56 | Decrypting something encrypted 57 | ------------------------------ 58 | 59 | Decrypting using the encrypted message above. 60 | 61 | >>> from cryptojwt.jwe.jwe import factory 62 | >>> from cryptojwt.jwk.rsa import RSAKey 63 | 64 | >>> _decryptor = factory(jwe, alg="RSA-OAEP", enc="A128CBC-HS256") 65 | >>> _dkey = RSAKey(priv_key=priv_key) 66 | >>> msg = _decryptor.decrypt(jwe, [_dkey]) 67 | 68 | or if you know what you're doing:: 69 | 70 | >>> _decryptor = JWE_RSA() 71 | >>> msg = _decryptor.decrypt(jwe, priv_key) 72 | 73 | 74 | 75 | 76 | 77 | .. _RFC7516: https://tools.ietf.org/html/rfc7516 78 | -------------------------------------------------------------------------------- /doc/jws.rst: -------------------------------------------------------------------------------- 1 | .. _jws: 2 | 3 | JSON Web Signature (JWS) 4 | ======================== 5 | 6 | JSON Web Signature (JWS) represents content secured with digital signatures 7 | or Message Authentication Codes (MACs) using JSON-based data structures. 8 | 9 | It is assumed that you know all you need to know about key handling if not 10 | please spend some time reading keyhandling_ . 11 | 12 | When it comes to JWS there are basically 2 things you want to be able to do: sign some data and verify that a 13 | signature over some data is correct. I'll deal with them in that order. 14 | 15 | Signing a document 16 | ------------------ 17 | 18 | There are few steps you have to go through. Let us start with an example and then break it into its parts:: 19 | 20 | >>> from cryptojwt.jwk.hmac import SYMKey 21 | >>> from cryptojwt.jws.jws import JWS 22 | 23 | >>> key = SYMKey(key=b'My hollow echo chamber', alg="HS512") 24 | >>> payload = "Please take a moment to register today" 25 | >>> _signer = JWS(payload, alg="HS512") 26 | >>> _jws = _signer.sign_compact([key]) 27 | 28 | The steps: 29 | 30 | 1. You need keys, one or more. If you provide more than one the software will pick one that has all the necessary 31 | qualifications. The keys *MUST* be an instance of :py:class:`cryptojwt.jwk.JWK` or of a sub class of that class. 32 | 2. You need the information that is to be signed. It must be in the form of a string. 33 | 3. You initiate the signer, providing it with the message and other needed information. 34 | 4. You sign using the compact or the JSON method as described in section 7 of RFC7515_ . 35 | 36 | 37 | Verifying a signature 38 | --------------------- 39 | 40 | Verifying a signature works like this (_jws comes from the first signing example):: 41 | 42 | >>> from cryptojwt.jwk.hmac import SYMKey 43 | >>> from cryptojwt.jws.jws import JWS 44 | 45 | >>> key = SYMKey(key=b'My hollow echo chamber', alg="HS512") 46 | >>> _verifier = JWS(alg="HS512") 47 | >>> _msg = _verifier.verify_compact(_jws, [key]) 48 | >>> print(_msg) 49 | "Please take a moment to register today" 50 | 51 | The steps: 52 | 53 | 1. As with signing, you need a set of keys that can be used to verify the signature. If you provide more than 54 | one key, the default is to use them one by one until one works or the list is empty. 55 | 2. Initiate the verifier. If you have a reason to expect that a particular signing algorithm is to be used you 56 | should give that information to the verifier as shown here. If you don't know, you can leave it out. 57 | 3. Verify, using the compact or JSON method. 58 | 59 | Or slightly different:: 60 | 61 | >>> from cryptojwt.jws.jws import factory 62 | >>> from cryptojwt.jwk.hmac import SYMKey 63 | 64 | >>> key = SYMKey(key=b'My hollow echo chamber', alg="HS512") 65 | >>> _verifier = factory(_jwt) 66 | >>> _verifier.verify_alg('HS512') 67 | True 68 | >>> print(_verifier.verify_compact(_jwt, [key])) 69 | "Please take a moment to register today" 70 | 71 | Or 72 | 73 | >>> from cryptojwt.jws.jws import factory 74 | >>> from cryptojwt.jwk.hmac import SYMKey 75 | 76 | >>> key = SYMKey(key=b'My hollow echo chamber', alg="HS512") 77 | >>> _verifier = factory(_jwt, alg="HS512") 78 | >>> print(_verifier.verify_compact(_jwt, [key])) 79 | "Please take a moment to register today" 80 | 81 | In which case the check of the signing algorithm is done by default. 82 | 83 | If you have Key Jar instead of a simple set of keys you can do (not showing how the key jar was initiated here):: 84 | 85 | >>> _verifier = factory(_jws, alg=RS256") 86 | >>> keys = key_jar.get_jwt_verify_keys(_verifier.jwt) 87 | >>> msg = _verifier.verify_compact(token, keys) 88 | 89 | This is a trick that is used in :py:class:`cryptojwt.jwt.JWT` 90 | 91 | 92 | .. _RFC7515: https://tools.ietf.org/html/rfc7515 93 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=CryptoJWT 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | CryptoJWT classes and functions documentation! 2 | ============================================== 3 | 4 | Description of CryptoJWT classes and functions. 5 | 6 | Contents: 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | keybundle 12 | keyjar 13 | jwk 14 | simple_jwt 15 | jwx 16 | jws 17 | jwe 18 | jwt 19 | utils 20 | -------------------------------------------------------------------------------- /doc/source/jwe.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.jwe package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | cryptojwt\.jwe\.aes module 8 | -------------------------- 9 | 10 | .. automodule:: cryptojwt.jwe.aes 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | cryptojwt\.jwe\.exception module 16 | -------------------------------- 17 | 18 | .. automodule:: cryptojwt.jwe.exception 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | cryptojwt\.jwe\.jwe module 24 | -------------------------- 25 | 26 | .. automodule:: cryptojwt.jwe.jwe 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | cryptojwt\.jwe\.jwe_ec module 32 | ----------------------------- 33 | 34 | .. automodule:: cryptojwt.jwe.jwe_ec 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | cryptojwt\.jwe\.jwe_hmac module 40 | ------------------------------- 41 | 42 | .. automodule:: cryptojwt.jwe.jwe_hmac 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | cryptojwt\.jwe\.jwe_rsa module 48 | ------------------------------ 49 | 50 | .. automodule:: cryptojwt.jwe.jwe_rsa 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | cryptojwt\.jwe\.jwekey module 56 | ----------------------------- 57 | 58 | .. automodule:: cryptojwt.jwe.jwekey 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | cryptojwt\.jwe\.jwenc module 64 | ---------------------------- 65 | 66 | .. automodule:: cryptojwt.jwe.jwenc 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | cryptojwt\.jwe\.rsa module 72 | -------------------------- 73 | 74 | .. automodule:: cryptojwt.jwe.rsa 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | cryptojwt\.jwe\.utils module 80 | ---------------------------- 81 | 82 | .. automodule:: cryptojwt.jwe.utils 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | -------------------------------------------------------------------------------- /doc/source/jwk.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.jwk package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | cryptojwt\.jwk\.asym module 8 | --------------------------- 9 | 10 | .. automodule:: cryptojwt.jwk.asym 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | cryptojwt\.jwk\.ec module 16 | ------------------------- 17 | 18 | .. automodule:: cryptojwt.jwk.ec 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | cryptojwt\.jwk\.hmac module 24 | --------------------------- 25 | 26 | .. automodule:: cryptojwt.jwk.hmac 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | cryptojwt\.jwk\.jwk module 32 | -------------------------- 33 | 34 | .. automodule:: cryptojwt.jwk.jwk 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | cryptojwt\.jwk\.rsa module 40 | -------------------------- 41 | 42 | .. automodule:: cryptojwt.jwk.rsa 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | cryptojwt\.jwk\.utils module 48 | ---------------------------- 49 | 50 | .. automodule:: cryptojwt.jwk.utils 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /doc/source/jws.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.jws package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | cryptojwt\.jws\.dsa module 8 | -------------------------- 9 | 10 | .. automodule:: cryptojwt.jws.dsa 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | cryptojwt\.jws\.exception module 16 | -------------------------------- 17 | 18 | .. automodule:: cryptojwt.jws.exception 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | cryptojwt\.jws\.hmac module 24 | --------------------------- 25 | 26 | .. automodule:: cryptojwt.jws.hmac 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | cryptojwt\.jws\.jws module 32 | -------------------------- 33 | 34 | .. automodule:: cryptojwt.jws.jws 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | cryptojwt\.jws\.pss module 40 | -------------------------- 41 | 42 | .. automodule:: cryptojwt.jws.pss 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | cryptojwt\.jws\.rsa module 48 | -------------------------- 49 | 50 | .. automodule:: cryptojwt.jws.rsa 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | cryptojwt\.jws\.utils module 56 | ---------------------------- 57 | 58 | .. automodule:: cryptojwt.jws.utils 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | -------------------------------------------------------------------------------- /doc/source/jwt.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.jwt package 2 | ========================== 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: cryptojwt.jwt 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /doc/source/jwx.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.jwx package 2 | ========================== 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: cryptojwt.jwx 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /doc/source/keybundle.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.key_bundle package 2 | ============================= 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: cryptojwt.key_bundle 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /doc/source/keyjar.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.key_jar package 2 | ========================== 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: cryptojwt.key_jar 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /doc/source/simple_jwt.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.simple_jwt package 2 | ============================= 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: cryptojwt.simple_jwt 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /doc/source/utils.rst: -------------------------------------------------------------------------------- 1 | cryptojwt\.utils package 2 | ========================== 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: cryptojwt.utils 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = True 3 | 4 | [mypy-jwkest.*] 5 | ignore_missing_imports = True 6 | 7 | [mypy-saml2.*] 8 | ignore_missing_imports = True 9 | 10 | [mypy-ldap.*] 11 | ignore_missing_imports = True 12 | 13 | [mypy-cryptography.*] 14 | ignore_missing_imports = True 15 | 16 | [mypy-defusedxml.*] 17 | ignore_missing_imports = True 18 | 19 | [mypy-pytest.*] 20 | ignore_missing_imports = True 21 | 22 | [mypy-responses.*] 23 | ignore_missing_imports = True 24 | 25 | [mypy-freezegun.*] 26 | ignore_missing_imports = True 27 | 28 | [mypy-testfixtures.*] 29 | ignore_missing_imports = True 30 | 31 | [mypy-mako.*] 32 | ignore_missing_imports = True 33 | -------------------------------------------------------------------------------- /pylama.ini: -------------------------------------------------------------------------------- 1 | [pylama:pycodestyle] 2 | max_line_length = 120 3 | 4 | [pylama:mccabe] 5 | complexity = 40 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # PEP 518: https://www.python.org/dev/peps/pep-0518/ 2 | 3 | [tool.poetry] 4 | name = "cryptojwt" 5 | version = "1.9.4" 6 | description = "Python implementation of JWT, JWE, JWS and JWK" 7 | authors = ["Roland Hedberg "] 8 | license = "Apache-2.0" 9 | repository = "https://github.com/IdentityPython/JWTConnect-Python-CryptoJWT" 10 | readme = "README.md" 11 | packages = [ 12 | { include = "cryptojwt", from = "src" } 13 | ] 14 | 15 | [tool.poetry.scripts] 16 | jwkgen = "cryptojwt.tools.keygen:main" 17 | jwkconv = "cryptojwt.tools.keyconv:main" 18 | jwtpeek = "cryptojwt.tools.jwtpeek:main" 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.9" 22 | cryptography = ">=3.4.6" 23 | requests = "^2.25.1" 24 | 25 | [tool.poetry.group.dev.dependencies] 26 | alabaster = "^0.7.12" 27 | pytest = "^8.2.1" 28 | pytest-cov = "^4.0.0" 29 | responses = "^0.13.0" 30 | sphinx = "^3.5.2" 31 | sphinx-autobuild = "^2021.3.14" 32 | coverage = "^7" 33 | ruff = ">=0.9.9" 34 | pytest-ruff = "^0.3.2" 35 | 36 | [build-system] 37 | requires = ["poetry-core>=1.0.0"] 38 | build-backend = "poetry.core.masonry.api" 39 | 40 | [tool.coverage.run] 41 | branch = true 42 | 43 | [tool.coverage.report] 44 | exclude_lines = [ 45 | "pragma: no cover", 46 | "raise NotImplementedError", 47 | ] 48 | 49 | [tool.ruff] 50 | line-length = 100 51 | 52 | [tool.ruff.lint] 53 | select = [ 54 | # pycodestyle 55 | "E", 56 | # Pyflakes 57 | "F", 58 | # pyupgrade 59 | "UP", 60 | # flake8-bugbear 61 | "B", 62 | # flake8-simplify 63 | "SIM", 64 | # isort 65 | "I", 66 | ] 67 | ignore = ["E501", "I001", "SIM102", "UP006", "UP035"] 68 | exclude = ["examples/*"] 69 | 70 | [tool.ruff.lint.isort] 71 | force-sort-within-sections = false 72 | combine-as-imports = true 73 | split-on-trailing-comma = false 74 | known-first-party = ["cryptojwt"] 75 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | network: mark a test as a network. 4 | -------------------------------------------------------------------------------- /src/cryptojwt/__init__.py: -------------------------------------------------------------------------------- 1 | """JSON Web Token""" 2 | 3 | import logging 4 | from importlib.metadata import version 5 | 6 | from cryptojwt.jwe.jwe import JWE 7 | from cryptojwt.jwk import JWK 8 | from cryptojwt.jws.jws import JWS 9 | from cryptojwt.jwt import JWT 10 | from cryptojwt.key_bundle import KeyBundle 11 | from cryptojwt.key_jar import KeyJar 12 | 13 | from .exception import BadSyntax 14 | from .utils import as_unicode, b64d, b64encode_item, split_token 15 | 16 | __version__ = version("cryptojwt") 17 | 18 | __all__ = [ 19 | "JWE", 20 | "JWE", 21 | "JWK", 22 | "JWS", 23 | "JWT", 24 | "KeyBundle", 25 | "KeyJar", 26 | "BadSyntax", 27 | "as_unicode", 28 | "b64d", 29 | "b64encode_item", 30 | "split_token", 31 | ] 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | JWT_TYPES = ("JWT", "application/jws", "JWS", "JWE") 36 | 37 | JWT_CLAIMS = { 38 | "iss": str, 39 | "sub": str, 40 | "aud": str, 41 | "exp": int, 42 | "nbf": int, 43 | "iat": int, 44 | "jti": str, 45 | "typ": str, 46 | } 47 | 48 | JWT_HEADERS = ["typ", "cty"] 49 | -------------------------------------------------------------------------------- /src/cryptojwt/exception.py: -------------------------------------------------------------------------------- 1 | class JWKESTException(Exception): 2 | pass 3 | 4 | 5 | # XXX Should this be a subclass of ValueError? 6 | class Invalid(JWKESTException): 7 | """The JWT is invalid.""" 8 | 9 | 10 | class WrongNumberOfParts(Invalid): 11 | pass 12 | 13 | 14 | class BadSyntax(Invalid): 15 | """The JWT could not be parsed because the syntax is invalid.""" 16 | 17 | def __init__(self, value, msg): 18 | Invalid.__init__(self) 19 | self.value = value 20 | self.msg = msg 21 | 22 | def __str__(self): 23 | return f"{self.msg}: {self.value!r}" 24 | 25 | 26 | class BadSignature(Invalid): 27 | """The signature of the JWT is invalid.""" 28 | 29 | 30 | class Expired(Invalid): 31 | """The JWT claim has expired or is not yet valid.""" 32 | 33 | 34 | class UnknownAlgorithm(Invalid): 35 | """The JWT uses an unknown signing algorithm""" 36 | 37 | 38 | class BadType(Invalid): 39 | """The JWT has an unexpected "typ" value.""" 40 | 41 | 42 | class MissingKey(JWKESTException): 43 | """No usable key""" 44 | 45 | 46 | class KeyNotFound(KeyError): 47 | """Key not found""" 48 | 49 | 50 | class IssuerNotFound(KeyError): 51 | """Issuer not found""" 52 | 53 | 54 | class KeyIOError(Exception): 55 | pass 56 | 57 | 58 | class UnknownKeyType(KeyIOError): 59 | pass 60 | 61 | 62 | class UpdateFailed(KeyIOError): 63 | pass 64 | 65 | 66 | class JWKException(JWKESTException): 67 | pass 68 | 69 | 70 | class FormatError(JWKException): 71 | pass 72 | 73 | 74 | class SerializationNotPossible(JWKException): 75 | pass 76 | 77 | 78 | class DeSerializationNotPossible(JWKException): 79 | pass 80 | 81 | 82 | class HeaderError(JWKESTException): 83 | pass 84 | 85 | 86 | class Unsupported(JWKESTException): 87 | pass 88 | 89 | 90 | class MissingValue(JWKESTException): 91 | pass 92 | 93 | 94 | class VerificationError(JWKESTException): 95 | pass 96 | 97 | 98 | class UnsupportedAlgorithm(JWKESTException): 99 | pass 100 | 101 | 102 | class WrongKeyType(JWKESTException): 103 | pass 104 | 105 | 106 | class UnsupportedKeyType(JWKESTException): 107 | pass 108 | 109 | 110 | class WrongUsage(JWKESTException): 111 | pass 112 | 113 | 114 | class HTTPException(JWKESTException): 115 | pass 116 | 117 | 118 | class UnsupportedECurve(Unsupported): 119 | pass 120 | 121 | 122 | class UnsupportedOKPCurve(Unsupported): 123 | pass 124 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/__init__.py: -------------------------------------------------------------------------------- 1 | KEY_LEN = { 2 | "A128GCM": 128, 3 | "A192GCM": 192, 4 | "A256GCM": 256, 5 | "A128CBC-HS256": 256, 6 | "A192CBC-HS384": 384, 7 | "A256CBC-HS512": 512, 8 | } 9 | 10 | KEY_LEN_BYTES = dict([(s, int(n / 8)) for s, n in KEY_LEN.items()]) 11 | 12 | SUPPORTED = { 13 | "alg": [ 14 | "RSA1_5", 15 | "RSA-OAEP", 16 | "RSA-OAEP-256", 17 | "A128KW", 18 | "A192KW", 19 | "A256KW", 20 | "ECDH-ES", 21 | "ECDH-ES+A128KW", 22 | "ECDH-ES+A192KW", 23 | "ECDH-ES+A256KW", 24 | ], 25 | "enc": [ 26 | "A128CBC-HS256", 27 | "A192CBC-HS384", 28 | "A256CBC-HS512", 29 | "A128GCM", 30 | "A192GCM", 31 | "A256GCM", 32 | ], 33 | } 34 | 35 | DEPRECATED = { 36 | "alg": ["RSA1_5"], 37 | "enc": [], 38 | } 39 | 40 | 41 | class Encrypter: 42 | """Abstract base class for encryption algorithms.""" 43 | 44 | def __init__(self, with_digest=False): 45 | self.with_digest = with_digest 46 | 47 | def encrypt(self, msg, key, **kwargs): 48 | """Encrypt ``msg`` with ``key`` and return the encrypted message.""" 49 | raise NotImplementedError 50 | 51 | def decrypt(self, msg, key, **kwargs): 52 | """Return decrypted message.""" 53 | raise NotImplementedError 54 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/aes.py: -------------------------------------------------------------------------------- 1 | import os 2 | from struct import pack 3 | 4 | from cryptography.hazmat.primitives import hmac 5 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 6 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM 7 | from cryptography.hazmat.primitives.padding import PKCS7 8 | 9 | from ..exception import MissingKey, Unsupported, VerificationError 10 | from . import Encrypter 11 | from .exception import UnsupportedBitLength 12 | from .utils import get_keys_seclen_dgst 13 | 14 | 15 | class AES_CBCEncrypter(Encrypter): 16 | """""" 17 | 18 | def __init__(self, key_len=32, key=None, msg_padding="PKCS7"): 19 | Encrypter.__init__(self) 20 | if key: 21 | self.key = key 22 | else: 23 | self.key = os.urandom(key_len) 24 | 25 | if msg_padding == "PKCS7": 26 | self.padder = PKCS7(128).padder() 27 | self.unpadder = PKCS7(128).unpadder() 28 | else: 29 | raise Unsupported(f"Message padding: {msg_padding}") 30 | 31 | self.iv = None 32 | 33 | def _mac(self, hash_key, hash_func, auth_data, iv, enc_msg, key_len): 34 | al = pack("!Q", 8 * len(auth_data)) 35 | h = hmac.HMAC(hash_key, hash_func()) 36 | h.update(auth_data) 37 | h.update(iv) 38 | h.update(enc_msg) 39 | h.update(al) 40 | m = h.finalize() 41 | return m[:key_len] 42 | 43 | def encrypt(self, msg, iv="", auth_data=b""): 44 | if not iv: 45 | iv = os.urandom(16) 46 | self.iv = iv 47 | else: 48 | self.iv = iv 49 | 50 | hash_key, enc_key, key_len, hash_func = get_keys_seclen_dgst(self.key, iv) 51 | 52 | cipher = Cipher(algorithms.AES(enc_key), modes.CBC(iv)) 53 | encryptor = cipher.encryptor() 54 | 55 | pmsg = self.padder.update(msg) 56 | pmsg += self.padder.finalize() 57 | ct = encryptor.update(pmsg) 58 | ct += encryptor.finalize() 59 | tag = self._mac(hash_key, hash_func, auth_data, iv, ct, key_len) 60 | return ct, tag 61 | 62 | def decrypt(self, msg, iv="", auth_data=b"", tag=b"", key=None): 63 | if key is None: 64 | if self.key: 65 | key = self.key 66 | else: 67 | raise MissingKey("No available key") 68 | 69 | hash_key, enc_key, key_len, hash_func = get_keys_seclen_dgst(key, iv) 70 | 71 | comp_tag = self._mac(hash_key, hash_func, auth_data, iv, msg, key_len) 72 | if comp_tag != tag: 73 | raise VerificationError("AES-CBC HMAC") 74 | 75 | cipher = Cipher(algorithms.AES(enc_key), modes.CBC(iv)) 76 | decryptor = cipher.decryptor() 77 | 78 | ctext = decryptor.update(msg) 79 | ctext += decryptor.finalize() 80 | unpad = self.unpadder.update(ctext) 81 | unpad += self.unpadder.finalize() 82 | return unpad 83 | 84 | 85 | class AES_GCMEncrypter(Encrypter): 86 | def __init__(self, bit_length=0, key=None): 87 | Encrypter.__init__(self) 88 | if key: 89 | self.key = AESGCM(key) 90 | elif bit_length: 91 | if bit_length not in [128, 192, 256]: 92 | raise UnsupportedBitLength(bit_length) 93 | 94 | self.key = AESGCM(AESGCM.generate_key(bit_length=bit_length)) 95 | else: 96 | raise ValueError("Need key or key bit length") 97 | 98 | def encrypt(self, msg, iv="", auth_data=None): 99 | """ 100 | Encrypts and authenticates the data provided as well as authenticating 101 | the associated_data. 102 | 103 | :param msg: The message to be encrypted 104 | :param iv: MUST be present, at least 96-bit long 105 | :param auth_data: Associated data 106 | :return: The cipher text bytes with the 16 byte tag appended. 107 | """ 108 | if not iv: 109 | raise ValueError("Missing Nonce") 110 | 111 | return self.key.encrypt(iv, msg, auth_data) 112 | 113 | def decrypt(self, cipher_text, iv="", auth_data=None, tag=b""): 114 | """ 115 | Decrypts the data and authenticates the associated_data (if provided). 116 | 117 | :param cipher_text: The data to decrypt including tag 118 | :param iv: Initialization Vector 119 | :param auth_data: Associated data 120 | :param tag: Authentication tag 121 | :return: The original plaintext 122 | """ 123 | if not iv: 124 | raise ValueError("Missing Nonce") 125 | 126 | return self.key.decrypt(iv, cipher_text + tag, auth_data) 127 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/exception.py: -------------------------------------------------------------------------------- 1 | from ..exception import JWKESTException 2 | 3 | 4 | class JWEException(JWKESTException): 5 | pass 6 | 7 | 8 | class CannotDecode(JWEException): 9 | pass 10 | 11 | 12 | class NotSupportedAlgorithm(JWEException): 13 | pass 14 | 15 | 16 | class MethodNotSupported(JWEException): 17 | pass 18 | 19 | 20 | class ParameterError(JWEException): 21 | pass 22 | 23 | 24 | class NoSuitableEncryptionKey(JWEException): 25 | pass 26 | 27 | 28 | class NoSuitableDecryptionKey(JWEException): 29 | pass 30 | 31 | 32 | class NoSuitableECDHKey(JWEException): 33 | pass 34 | 35 | 36 | class DecryptionFailed(JWEException): 37 | pass 38 | 39 | 40 | class WrongEncryptionAlgorithm(JWEException): 41 | pass 42 | 43 | 44 | class UnsupportedBitLength(JWEException): 45 | pass 46 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/fernet.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | from typing import Optional, Union 4 | 5 | from cryptography.fernet import Fernet 6 | from cryptography.hazmat.primitives import hashes 7 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 8 | 9 | from cryptojwt.jwe import Encrypter 10 | from cryptojwt.utils import as_bytes 11 | 12 | DEFAULT_ITERATIONS = 390000 13 | 14 | 15 | class FernetEncrypter(Encrypter): 16 | def __init__( 17 | self, 18 | password: Optional[str] = None, 19 | salt: Optional[bytes] = "", 20 | key: Optional[bytes] = None, 21 | hash_alg: Optional[str] = "SHA256", 22 | digest_size: Optional[int] = 0, 23 | iterations: Optional[int] = DEFAULT_ITERATIONS, 24 | ): 25 | Encrypter.__init__(self) 26 | 27 | if key is not None: 28 | if not isinstance(key, bytes): 29 | raise TypeError("Raw key must be bytes") 30 | if len(key) != 32: 31 | raise ValueError("Raw key must be 32 bytes") 32 | self.key = base64.urlsafe_b64encode(key) 33 | elif password is not None: 34 | _alg = getattr(hashes, hash_alg) 35 | # A bit special for SHAKE* and BLAKE* hashes 36 | if hash_alg.startswith("SHAKE") or hash_alg.startswith("BLAKE"): 37 | _algorithm = _alg(digest_size) 38 | else: 39 | _algorithm = _alg() 40 | salt = as_bytes(salt) if salt else os.urandom(16) 41 | kdf = PBKDF2HMAC(algorithm=_algorithm, length=32, salt=salt, iterations=iterations) 42 | self.key = base64.urlsafe_b64encode(kdf.derive(as_bytes(password))) 43 | else: 44 | self.key = Fernet.generate_key() 45 | 46 | self.core = Fernet(self.key) 47 | 48 | def encrypt(self, msg: Union[str, bytes], **kwargs) -> bytes: 49 | text = as_bytes(msg) 50 | # Padding to block size of AES 51 | if len(text) % 16: 52 | text += b" " * (16 - len(text) % 16) 53 | return self.core.encrypt(as_bytes(text)) 54 | 55 | def decrypt(self, msg: Union[str, bytes], **kwargs) -> bytes: 56 | dec_text = self.core.decrypt(as_bytes(msg)) 57 | dec_text = dec_text.rstrip(b" ") 58 | return dec_text 59 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/jwe.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import logging 3 | 4 | from ..jwk.asym import AsymmetricKey 5 | from ..jwk.ec import ECKey 6 | from ..jwk.hmac import SYMKey 7 | from ..jwk.jwk import key_from_jwk_dict 8 | from ..jwx import JWx 9 | from .exception import ( 10 | DecryptionFailed, 11 | NoSuitableDecryptionKey, 12 | NoSuitableEncryptionKey, 13 | NotSupportedAlgorithm, 14 | WrongEncryptionAlgorithm, 15 | ) 16 | from .jwe_ec import JWE_EC 17 | from .jwe_hmac import JWE_SYM 18 | from .jwe_rsa import JWE_RSA 19 | from .jwenc import JWEnc 20 | from .utils import alg2keytype 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | __author__ = "Roland Hedberg" 25 | 26 | 27 | KEY_ERR = "Could not find any suitable encryption key for alg='{}'" 28 | 29 | 30 | class JWE(JWx): 31 | args = [ 32 | "alg", 33 | "enc", 34 | "epk", 35 | "zip", 36 | "jku", 37 | "jwk", 38 | "x5u", 39 | "x5t", 40 | "x5c", 41 | "kid", 42 | "typ", 43 | "cty", 44 | "apu", 45 | "crit", 46 | ] 47 | 48 | """ 49 | :param msg: The message 50 | :param alg: Algorithm 51 | :param enc: Encryption Method 52 | :param epk: Ephemeral Public Key 53 | :param zip: Compression Algorithm 54 | :param jku: a URI that refers to a resource for a set of JSON-encoded 55 | public keys, one of which corresponds to the key used to digitally 56 | sign the JWS 57 | :param jwk: A JSON Web Key that corresponds to the key used to 58 | digitally sign the JWS 59 | :param x5u: a URI that refers to a resource for the X.509 public key 60 | certificate or certificate chain [RFC5280] corresponding to the key 61 | used to digitally sign the JWS. 62 | :param x5t: a base64url encoded SHA-1 thumbprint (a.k.a. digest) of the 63 | DER encoding of the X.509 certificate [RFC5280] corresponding to 64 | the key used to digitally sign the JWS. 65 | :param x5c: the X.509 public key certificate or certificate chain 66 | corresponding to the key used to digitally sign the JWS. 67 | :param kid: Key ID a hint indicating which key was used to secure the 68 | JWS. 69 | :param typ: the type of this object. 'JWS' == JWS Compact Serialization 70 | 'JWS+JSON' == JWS JSON Serialization 71 | :param cty: Content Type 72 | :param apu: Agreement PartyUInfo 73 | :param crit: indicates which extensions that are being used and MUST 74 | be understood and processed. 75 | :return: A class instance 76 | """ 77 | 78 | def encrypt(self, keys=None, cek="", iv="", **kwargs): 79 | """ 80 | Encrypt a payload. 81 | 82 | :param keys: A set of possibly usable keys 83 | :param cek: Content master key 84 | :param iv: Initialization vector 85 | :param kwargs: Extra key word arguments 86 | :return: Encrypted message 87 | """ 88 | 89 | _alg = self["alg"] 90 | 91 | # Find Usable Keys 92 | if keys: 93 | keys = self.pick_keys(keys, use="enc") 94 | else: 95 | keys = self.pick_keys(self._get_keys(), use="enc") 96 | 97 | if not keys: 98 | logger.error(KEY_ERR.format(_alg)) 99 | raise NoSuitableEncryptionKey(_alg) 100 | 101 | # Determine Encryption Class by Algorithm 102 | if _alg in ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"]: 103 | encrypter = JWE_RSA(self.msg, **self._dict) 104 | elif _alg.startswith("A") and _alg.endswith("KW"): 105 | encrypter = JWE_SYM(self.msg, **self._dict) 106 | else: # _alg.startswith("ECDH-ES"): 107 | encrypter = JWE_EC(**self._dict) 108 | cek, encrypted_key, iv, params, eprivk = encrypter.enc_setup( 109 | self.msg, key=keys[0], **self._dict 110 | ) 111 | kwargs["encrypted_key"] = encrypted_key 112 | kwargs["params"] = params 113 | 114 | if cek: 115 | kwargs["cek"] = cek 116 | 117 | if iv: 118 | kwargs["iv"] = iv 119 | 120 | for key in keys: 121 | if isinstance(key, SYMKey): 122 | _key = key.key 123 | elif isinstance(key, ECKey): 124 | _key = key.public_key() 125 | else: # isinstance(key, RSAKey): 126 | _key = key.public_key() 127 | 128 | if key.kid: 129 | encrypter["kid"] = key.kid 130 | 131 | try: 132 | token = encrypter.encrypt(key=_key, **kwargs) 133 | self["cek"] = encrypter.cek if "cek" in encrypter else None 134 | except TypeError as err: 135 | raise err 136 | else: 137 | logger.debug(f"Encrypted message using key with kid={key.kid}") 138 | return token 139 | 140 | # logger.error("Could not find any suitable encryption key") 141 | # raise NoSuitableEncryptionKey() 142 | 143 | def decrypt(self, token=None, keys=None, alg=None, cek=None): 144 | if token: 145 | _jwe = JWEnc().unpack(token) 146 | # header, ek, eiv, ctxt, tag = token.split(b".") 147 | # self.parse_header(header) 148 | elif self.jwt: 149 | _jwe = self.jwt 150 | else: 151 | raise ValueError("Nothing to decrypt") 152 | 153 | _alg = _jwe.headers["alg"] 154 | if alg and alg != _alg: 155 | raise WrongEncryptionAlgorithm() 156 | 157 | # Find appropriate keys 158 | if keys: 159 | keys = self.pick_keys(keys, use="enc", alg=_alg) 160 | else: 161 | keys = self.pick_keys(self._get_keys(), use="enc", alg=_alg) 162 | 163 | with contextlib.suppress(KeyError): 164 | keys.append(key_from_jwk_dict(_jwe.headers["jwk"])) 165 | 166 | if not keys and not cek: 167 | raise NoSuitableDecryptionKey(_alg) 168 | 169 | if _alg in ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"]: 170 | decrypter = JWE_RSA(**self._dict) 171 | elif _alg.startswith("A") and _alg.endswith("KW"): 172 | decrypter = JWE_SYM(self.msg, **self._dict) 173 | elif _alg.startswith("ECDH-ES"): 174 | decrypter = JWE_EC(**self._dict) 175 | 176 | _key = keys[0].private_key() if isinstance(keys[0], AsymmetricKey) else keys[0].key 177 | 178 | cek = decrypter.dec_setup(_jwe, key=_key) 179 | else: 180 | raise NotSupportedAlgorithm 181 | 182 | if cek: 183 | try: 184 | msg = decrypter.decrypt(_jwe, cek=cek) 185 | self["cek"] = decrypter.cek if "cek" in decrypter else None 186 | except (KeyError, DecryptionFailed): 187 | pass 188 | else: 189 | logger.debug("Decrypted message using exiting CEK") 190 | return msg 191 | 192 | for key in keys: 193 | _key = key.private_key() if isinstance(key, AsymmetricKey) else key.key 194 | 195 | try: 196 | msg = decrypter.decrypt(_jwe, _key) 197 | self["cek"] = decrypter.cek if "cek" in decrypter else None 198 | except (KeyError, DecryptionFailed): 199 | pass 200 | else: 201 | logger.debug(f"Decrypted message using key with kid={key.kid}") 202 | return msg 203 | 204 | raise DecryptionFailed("No available key that could decrypt the message") 205 | 206 | def alg2keytype(self, alg): 207 | return alg2keytype(alg) 208 | 209 | 210 | def factory(token, alg="", enc=""): 211 | try: 212 | _jwt = JWEnc().unpack(token, alg=alg, enc=enc) 213 | except KeyError: 214 | return None 215 | 216 | if _jwt.is_jwe(): 217 | _jwe = JWE() 218 | _jwe.jwt = _jwt 219 | return _jwe 220 | else: 221 | return None 222 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/jwe_ec.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import struct 3 | 4 | from cryptography.hazmat.primitives.asymmetric import ec 5 | from cryptography.hazmat.primitives.keywrap import aes_key_unwrap, aes_key_wrap 6 | 7 | from ..jwk.ec import NIST2SEC, ECKey 8 | from ..utils import as_bytes, as_unicode, b64d, b64e 9 | from . import KEY_LEN 10 | from .jwekey import JWEKey 11 | from .jwenc import JWEnc 12 | from .utils import concat_sha256, get_random_bytes 13 | 14 | 15 | def ecdh_derive_key(key, epk, apu, apv, alg, dk_len): 16 | """ 17 | ECDH key derivation, as defined by JWA 18 | 19 | :param key : Elliptic curve private key 20 | :param epk : Elliptic curve public key 21 | :param apu : PartyUInfo 22 | :param apv : PartyVInfo 23 | :param alg : Algorithm identifier 24 | :param dk_len: Length of key to be derived, in bits 25 | :return: The derived key 26 | """ 27 | # Compute shared secret 28 | shared_key = key.exchange(ec.ECDH(), epk) 29 | # Derive the key 30 | # AlgorithmID || PartyUInfo || PartyVInfo || SuppPubInfo 31 | otherInfo = ( 32 | struct.pack("!I", len(alg)) 33 | + bytes(alg) 34 | + struct.pack("!I", len(apu)) 35 | + apu 36 | + struct.pack("!I", len(apv)) 37 | + apv 38 | + struct.pack("!I", dk_len) 39 | ) 40 | return concat_sha256(shared_key, dk_len, otherInfo) 41 | 42 | 43 | class JWE_EC(JWEKey): 44 | args = JWEKey.args[:] 45 | args.append("enc") 46 | 47 | def __init__(self, msg=None, with_digest=False, **kwargs): 48 | JWEKey.__init__(self, msg, with_digest, **kwargs) 49 | self.msg_valid = False 50 | self.auth_data = b"" 51 | 52 | def enc_setup(self, msg, key=None, auth_data=b"", **kwargs): 53 | """ 54 | 55 | :param msg: Message to be encrypted 56 | :param auth_data: 57 | :param key: An EC key 58 | :param kwargs: 59 | :return: 60 | """ 61 | encrypted_key = "" 62 | self.msg = msg 63 | self.auth_data = auth_data 64 | 65 | # Generate the input parameters 66 | try: 67 | apu = b64d(kwargs["apu"]) 68 | except KeyError: 69 | apu = get_random_bytes(16) 70 | try: 71 | apv = b64d(kwargs["apv"]) 72 | except KeyError: 73 | apv = get_random_bytes(16) 74 | 75 | # Handle Local Key and Ephemeral Public Key 76 | if not key or not isinstance(key, ECKey): 77 | raise ValueError("EC Key Required for ECDH-ES JWE Encryption Setup") 78 | 79 | # epk is either an Elliptic curve key instance or a JWK description of 80 | # one. This key belongs to the entity on the other side. 81 | try: 82 | _epk = kwargs["epk"] 83 | except KeyError: 84 | _epk = ec.generate_private_key(curve=NIST2SEC[as_unicode(key.crv)]()) 85 | epk = ECKey().load_key(_epk.public_key()) 86 | else: 87 | if isinstance(_epk, ec.EllipticCurvePrivateKey): 88 | epk = ECKey().load_key(_epk) 89 | elif isinstance(_epk, ECKey): 90 | epk = _epk 91 | _epk = epk.private_key() 92 | else: 93 | raise ValueError("epk of a type I can't handle") 94 | 95 | params = {"apu": b64e(apu), "apv": b64e(apv), "epk": epk.serialize(False)} 96 | 97 | cek = iv = None 98 | if "cek" in kwargs and kwargs["cek"]: 99 | cek = kwargs["cek"] 100 | if "iv" in kwargs and kwargs["iv"]: 101 | iv = kwargs["iv"] 102 | 103 | iv = self._generate_iv(self.enc, iv=iv) 104 | 105 | if self.alg == "ECDH-ES": 106 | try: 107 | dk_len = KEY_LEN[self.enc] 108 | except KeyError as exc: 109 | raise ValueError(f"Unknown key length for algorithm {self.enc}") from exc 110 | 111 | cek = ecdh_derive_key(_epk, key.pub_key, apu, apv, str(self.enc).encode(), dk_len) 112 | elif self.alg in ["ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"]: 113 | _pre, _post = self.alg.split("+") 114 | klen = int(_post[1:4]) 115 | kek = ecdh_derive_key(_epk, key.pub_key, apu, apv, str(_post).encode(), klen) 116 | cek = self._generate_key(self.enc, cek=cek) 117 | encrypted_key = aes_key_wrap(kek, cek) 118 | else: 119 | raise Exception(f"Unsupported algorithm {self.alg}") 120 | 121 | return cek, encrypted_key, iv, params, epk 122 | 123 | def dec_setup(self, token, key=None, **kwargs): 124 | """ 125 | 126 | :param token: signed JSON Web token 127 | :param key: Private Elliptic Curve Key 128 | :param kwargs: 129 | :return: 130 | """ 131 | self.headers = token.headers 132 | self.iv = token.initialization_vector() 133 | self.ctxt = token.ciphertext() 134 | self.tag = token.authentication_tag() 135 | 136 | # Handle EPK / Curve 137 | if "epk" not in self.headers or "crv" not in self.headers["epk"]: 138 | raise Exception("Ephemeral Public Key Missing in ECDH-ES Computation") 139 | 140 | epubkey = ECKey(**self.headers["epk"]) 141 | apu = apv = b"" 142 | if "apu" in self.headers: 143 | apu = b64d(self.headers["apu"].encode()) 144 | if "apv" in self.headers: 145 | apv = b64d(self.headers["apv"].encode()) 146 | 147 | if self.headers["alg"] == "ECDH-ES": 148 | try: 149 | dk_len = KEY_LEN[self.headers["enc"]] 150 | except KeyError as exc: 151 | raise Exception("Unknown key length for algorithm") from exc 152 | 153 | self.cek = ecdh_derive_key( 154 | key, 155 | epubkey.pub_key, 156 | apu, 157 | apv, 158 | str(self.headers["enc"]).encode(), 159 | dk_len, 160 | ) 161 | elif self.headers["alg"] in [ 162 | "ECDH-ES+A128KW", 163 | "ECDH-ES+A192KW", 164 | "ECDH-ES+A256KW", 165 | ]: 166 | _pre, _post = self.headers["alg"].split("+") 167 | klen = int(_post[1:4]) 168 | kek = ecdh_derive_key(key, epubkey.pub_key, apu, apv, str(_post).encode(), klen) 169 | self.cek = aes_key_unwrap(kek, token.encrypted_key()) 170 | else: 171 | raise Exception("Unsupported algorithm {}".format(self.headers["alg"])) 172 | 173 | return self.cek 174 | 175 | def encrypt(self, key=None, iv="", cek="", **kwargs): 176 | """ 177 | Produces a JWE as defined in RFC7516 using an Elliptic curve key 178 | 179 | :param key: *Not used*, only there to present the same API as JWE_RSA and JWE_SYM 180 | :param iv: Initialization vector 181 | :param cek: Content master key 182 | :param kwargs: Extra keyword arguments 183 | :return: An encrypted JWT 184 | """ 185 | _msg = as_bytes(self.msg) 186 | 187 | _args = self._dict 188 | with contextlib.suppress(KeyError): 189 | _args["kid"] = kwargs["kid"] 190 | 191 | if "params" in kwargs: 192 | if "apu" in kwargs["params"]: 193 | _args["apu"] = kwargs["params"]["apu"] 194 | if "apv" in kwargs["params"]: 195 | _args["apv"] = kwargs["params"]["apv"] 196 | if "epk" in kwargs["params"]: 197 | _args["epk"] = kwargs["params"]["epk"] 198 | 199 | jwe = JWEnc(**_args) 200 | ctxt, tag, cek = super().enc_setup( 201 | self["enc"], _msg, auth_data=jwe.b64_encode_header(), key=cek, iv=iv 202 | ) 203 | if "encrypted_key" in kwargs: 204 | return jwe.pack(parts=[kwargs["encrypted_key"], iv, ctxt, tag]) 205 | return jwe.pack(parts=[iv, ctxt, tag]) 206 | 207 | def decrypt(self, token=None, **kwargs): 208 | jwe = token if isinstance(token, JWEnc) else JWEnc().unpack(token) 209 | 210 | if not self.cek: 211 | raise Exception("Content Encryption Key is Not Yet Set") 212 | 213 | msg = super()._decrypt( 214 | self.headers["enc"], 215 | self.cek, 216 | self.ctxt, 217 | auth_data=jwe.b64part[0], 218 | iv=self.iv, 219 | tag=self.tag, 220 | ) 221 | self.msg = msg 222 | self.msg_valid = True 223 | return msg 224 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/jwe_hmac.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import logging 3 | import zlib 4 | 5 | from cryptography.hazmat.primitives.keywrap import aes_key_unwrap, aes_key_wrap 6 | 7 | from ..exception import MissingKey, WrongNumberOfParts 8 | from ..jwk.hmac import SYMKey 9 | from ..utils import as_bytes, intarr2str 10 | from .jwekey import JWEKey 11 | from .jwenc import JWEnc 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | __author__ = "Roland Hedberg" 16 | 17 | 18 | class JWE_SYM(JWEKey): 19 | args = JWEKey.args[:] 20 | args.append("enc") 21 | 22 | def encrypt(self, key, iv="", cek="", **kwargs): 23 | """ 24 | Produces a JWE as defined in RFC7516 using symmetric keys 25 | 26 | :param key: Shared symmetric key 27 | :param iv: Initialization vector 28 | :param cek: Content master key 29 | :param kwargs: Extra keyword arguments, just ignore for now. 30 | :return: 31 | """ 32 | _msg = as_bytes(self.msg) 33 | 34 | _args = self._dict 35 | with contextlib.suppress(KeyError): 36 | _args["kid"] = kwargs["kid"] 37 | 38 | jwe = JWEnc(**_args) 39 | 40 | # If no iv and cek are given generate them 41 | iv = self._generate_iv(self["enc"], iv) 42 | cek = self._generate_key(self["enc"], cek) 43 | if isinstance(key, SYMKey): 44 | try: 45 | kek = key.key.encode("utf8") 46 | except AttributeError: 47 | kek = key.key 48 | elif isinstance(key, bytes): 49 | kek = key 50 | else: 51 | kek = intarr2str(key) 52 | 53 | # The iv for this function must be 64 bit 54 | # Which is certainly different from the one above 55 | jek = aes_key_wrap(kek, cek) 56 | 57 | _enc = self["enc"] 58 | _auth_data = jwe.b64_encode_header() 59 | ctxt, tag, cek = self.enc_setup(_enc, _msg, auth_data=_auth_data, key=cek, iv=iv) 60 | return jwe.pack(parts=[jek, iv, ctxt, tag]) 61 | 62 | def decrypt(self, token, key=None, cek=None): 63 | logger.debug("SYM decrypt") 64 | if not key and not cek: 65 | raise MissingKey("On of key or cek must be specified") 66 | 67 | jwe = token if isinstance(token, JWEnc) else JWEnc().unpack(token) 68 | 69 | if len(jwe) != 5: 70 | raise WrongNumberOfParts(len(jwe)) 71 | 72 | if not cek: 73 | jek = jwe.encrypted_key() 74 | if isinstance(key, SYMKey): 75 | try: 76 | key = key.key.encode("utf8") 77 | except AttributeError: 78 | key = key.key 79 | # The iv for this function must be 64 bit 80 | cek = aes_key_unwrap(key, jek) 81 | 82 | auth_data = jwe.b64_protected_header() 83 | msg = self._decrypt( 84 | jwe.headers["enc"], 85 | cek, 86 | jwe.ciphertext(), 87 | auth_data=auth_data, 88 | iv=jwe.initialization_vector(), 89 | tag=jwe.authentication_tag(), 90 | ) 91 | 92 | if "zip" in self and self["zip"] == "DEF": 93 | msg = zlib.decompress(msg) 94 | 95 | return msg 96 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/jwe_rsa.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import zlib 3 | 4 | from ..utils import as_bytes 5 | from . import SUPPORTED 6 | from .exception import NotSupportedAlgorithm, ParameterError 7 | from .jwekey import JWEKey 8 | from .jwenc import JWEnc 9 | from .rsa import RSAEncrypter 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | __author__ = "Roland Hedberg" 14 | 15 | 16 | class JWE_RSA(JWEKey): 17 | args = [ 18 | "msg", 19 | "alg", 20 | "enc", 21 | "epk", 22 | "zip", 23 | "jku", 24 | "jwk", 25 | "x5u", 26 | "x5t", 27 | "x5c", 28 | "kid", 29 | "typ", 30 | "cty", 31 | "apu", 32 | "crit", 33 | ] 34 | 35 | def encrypt(self, key, iv="", cek="", **kwargs): 36 | """ 37 | Produces a JWE as defined in RFC7516 using RSA algorithms 38 | 39 | :param key: RSA key 40 | :param iv: Initialization vector 41 | :param cek: Content master key 42 | :param kwargs: Extra keyword arguments 43 | :return: A signed payload 44 | """ 45 | 46 | _msg = as_bytes(self.msg) 47 | if "zip" in self: 48 | if self["zip"] == "DEF": 49 | _msg = zlib.compress(_msg) 50 | else: 51 | raise ParameterError("Zip has unknown value: {}".format(self["zip"])) 52 | 53 | kwarg_cek = cek or None 54 | 55 | _enc = self["enc"] 56 | iv = self._generate_iv(_enc, iv) 57 | cek = self._generate_key(_enc, cek) 58 | self["cek"] = cek 59 | 60 | logger.debug(f"cek: {[c for c in cek]}, iv: {[c for c in iv]}") 61 | 62 | _encrypt = RSAEncrypter(self.with_digest).encrypt 63 | 64 | _alg = self["alg"] 65 | if kwarg_cek: 66 | jwe_enc_key = "" 67 | elif _alg == "RSA-OAEP": 68 | jwe_enc_key = _encrypt(cek, key, "pkcs1_oaep_padding") 69 | elif _alg == "RSA-OAEP-256": 70 | jwe_enc_key = _encrypt(cek, key, "pkcs1_oaep_256_padding") 71 | elif _alg == "RSA1_5": 72 | jwe_enc_key = _encrypt(cek, key) 73 | else: 74 | raise NotSupportedAlgorithm(_alg) 75 | 76 | jwe = JWEnc(**self.headers()) 77 | 78 | try: 79 | _auth_data = kwargs["auth_data"] 80 | except KeyError: 81 | _auth_data = jwe.b64_encode_header() 82 | 83 | ctxt, tag, key = self.enc_setup(_enc, _msg, key=cek, iv=iv, auth_data=_auth_data) 84 | return jwe.pack(parts=[jwe_enc_key, iv, ctxt, tag]) 85 | 86 | def decrypt(self, token, key, cek=None): 87 | """Decrypts a JWT 88 | 89 | :param token: The JWT 90 | :param key: A key to use for decrypting 91 | :param cek: Ephemeral cipher key 92 | :return: The decrypted message 93 | """ 94 | jwe = JWEnc().unpack(token) if not isinstance(token, JWEnc) else token 95 | 96 | self.jwt = jwe.encrypted_key() 97 | jek = jwe.encrypted_key() 98 | 99 | _decrypt = RSAEncrypter(self.with_digest).decrypt 100 | 101 | _alg = jwe.headers["alg"] 102 | if cek: 103 | pass 104 | elif _alg == "RSA-OAEP": 105 | cek = _decrypt(jek, key, "pkcs1_oaep_padding") 106 | elif _alg == "RSA-OAEP-256": 107 | cek = _decrypt(jek, key, "pkcs1_oaep_256_padding") 108 | elif _alg == "RSA1_5": 109 | cek = _decrypt(jek, key) 110 | else: 111 | raise NotSupportedAlgorithm(_alg) 112 | 113 | self["cek"] = cek 114 | enc = jwe.headers["enc"] 115 | if enc not in SUPPORTED["enc"]: 116 | raise NotSupportedAlgorithm(enc) 117 | 118 | auth_data = jwe.b64_protected_header() 119 | 120 | msg = self._decrypt( 121 | enc, 122 | cek, 123 | jwe.ciphertext(), 124 | auth_data=auth_data, 125 | iv=jwe.initialization_vector(), 126 | tag=jwe.authentication_tag(), 127 | ) 128 | 129 | if "zip" in jwe.headers and jwe.headers["zip"] == "DEF": 130 | msg = zlib.decompress(msg) 131 | 132 | return msg 133 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/jwekey.py: -------------------------------------------------------------------------------- 1 | from ..jwx import JWx 2 | from . import KEY_LEN_BYTES 3 | from .aes import AES_CBCEncrypter, AES_GCMEncrypter 4 | from .exception import DecryptionFailed, NotSupportedAlgorithm 5 | from .utils import alg2keytype, get_random_bytes, split_ctx_and_tag 6 | 7 | 8 | class JWEKey(JWx): 9 | @staticmethod 10 | def _generate_iv(encalg, iv=""): 11 | if iv: 12 | return iv 13 | else: 14 | _iv = get_random_bytes(16) 15 | 16 | return _iv 17 | 18 | @staticmethod 19 | def _generate_key(encalg, cek=""): 20 | if cek: 21 | return cek 22 | 23 | try: 24 | _key = get_random_bytes(KEY_LEN_BYTES[encalg]) 25 | except KeyError: 26 | try: 27 | _key = get_random_bytes(KEY_LEN_BYTES[encalg]) 28 | except KeyError as exc: 29 | raise ValueError(f"Unsupported encryption algorithm {encalg}") from exc 30 | 31 | return _key 32 | 33 | def alg2keytype(self, alg): 34 | return alg2keytype(alg) 35 | 36 | def enc_setup(self, enc_alg, msg, auth_data=b"", key=None, iv=""): 37 | """Encrypt JWE content. 38 | 39 | :param enc_alg: The JWE "enc" value specifying the encryption algorithm 40 | :param msg: The plain text message 41 | :param auth_data: Additional authenticated data 42 | :param key: Key (CEK) 43 | :return: Tuple (ciphertext, tag), both as bytes 44 | """ 45 | 46 | iv = self._generate_iv(enc_alg, iv) 47 | 48 | if enc_alg in ["A192GCM", "A128GCM", "A256GCM"]: 49 | aes = AES_GCMEncrypter(key=key) 50 | ctx, tag = split_ctx_and_tag(aes.encrypt(msg, iv, auth_data)) 51 | elif enc_alg in ["A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"]: 52 | aes = AES_CBCEncrypter(key=key) 53 | ctx, tag = aes.encrypt(msg, iv, auth_data) 54 | else: 55 | raise NotSupportedAlgorithm(enc_alg) 56 | 57 | return ctx, tag, aes.key 58 | 59 | @staticmethod 60 | def _decrypt(enc, key, ctxt, iv, tag, auth_data=b""): 61 | """Decrypt JWE content. 62 | 63 | :param enc: The JWE "enc" value specifying the encryption algorithm 64 | :param key: Key (CEK) 65 | :param iv : Initialization vector 66 | :param auth_data: Additional authenticated data (AAD) 67 | :param ctxt : Ciphertext 68 | :param tag: Authentication tag 69 | :return: plain text message or None if decryption failed 70 | """ 71 | if enc in ["A128GCM", "A192GCM", "A256GCM"]: 72 | aes = AES_GCMEncrypter(key=key) 73 | elif enc in ["A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"]: 74 | aes = AES_CBCEncrypter(key=key) 75 | else: 76 | raise Exception(f"Unsupported encryption algorithm {enc}") 77 | 78 | try: 79 | return aes.decrypt(ctxt, iv=iv, auth_data=auth_data, tag=tag) 80 | except DecryptionFailed: 81 | raise 82 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/jwenc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ..simple_jwt import SimpleJWT 4 | from ..utils import b64encode_item 5 | from . import SUPPORTED 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class JWEnc(SimpleJWT): 11 | def b64_protected_header(self): 12 | return self.b64part[0] 13 | 14 | def b64_encrypted_key(self): 15 | return self.b64part[1] 16 | 17 | def b64_initialization_vector(self): 18 | return self.b64part[2] 19 | 20 | def b64_ciphertext(self): 21 | return self.b64part[3] 22 | 23 | def b64_authentication_tag(self): 24 | return self.b64part[4] 25 | 26 | def protected_header(self): 27 | return self.part[0] 28 | 29 | def encrypted_key(self): 30 | return self.part[1] 31 | 32 | def initialization_vector(self): 33 | return self.part[2] 34 | 35 | def ciphertext(self): 36 | return self.part[3] 37 | 38 | def authentication_tag(self): 39 | return self.part[4] 40 | 41 | def b64_encode_header(self): 42 | return b64encode_item(self.headers) 43 | 44 | def is_jwe(self): 45 | if "typ" in self.headers and self.headers["typ"].lower() == "jwe": 46 | return True 47 | 48 | if "alg" in self.headers and "enc" in self.headers: 49 | for typ in ["alg", "enc"]: 50 | if self.headers[typ] not in SUPPORTED[typ]: 51 | logger.debug(f"Not supported {typ} algorithm: {self.headers[typ]}") 52 | return False 53 | else: 54 | return False 55 | return True 56 | 57 | def __len__(self): 58 | return len(self.part) 59 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/rsa.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.primitives import hashes 2 | from cryptography.hazmat.primitives.asymmetric import padding 3 | 4 | from . import Encrypter 5 | 6 | 7 | class RSAEncrypter(Encrypter): 8 | def encrypt(self, msg, key, sign_padding="pkcs1_padding"): 9 | _chosen_hash = hashes.SHA1 10 | if sign_padding == "pkcs1_padding": 11 | _padding = padding.PKCS1v15 12 | return key.encrypt(msg, _padding()) 13 | elif sign_padding == "pkcs1_oaep_padding": 14 | _padding = padding.OAEP 15 | elif sign_padding == "pkcs1_oaep_256_padding": 16 | _padding = padding.OAEP 17 | _chosen_hash = hashes.SHA256 18 | else: 19 | raise Exception("Unsupported padding") 20 | return key.encrypt( 21 | msg, 22 | _padding( 23 | mgf=padding.MGF1(algorithm=_chosen_hash()), 24 | algorithm=_chosen_hash(), 25 | label=None, 26 | ), 27 | ) 28 | 29 | def decrypt(self, ciphertext, key, sign_padding="pkcs1_padding"): 30 | _chosen_hash = hashes.SHA1 31 | if sign_padding == "pkcs1_padding": 32 | _padding = padding.PKCS1v15 33 | return key.decrypt(ciphertext, _padding()) 34 | elif sign_padding == "pkcs1_oaep_padding": 35 | _padding = padding.OAEP 36 | elif sign_padding == "pkcs1_oaep_256_padding": 37 | _padding = padding.OAEP 38 | _chosen_hash = hashes.SHA256 39 | else: 40 | raise Exception("Unsupported padding") 41 | 42 | try: 43 | text = key.decrypt( 44 | ciphertext, 45 | _padding( 46 | mgf=padding.MGF1(algorithm=_chosen_hash()), 47 | algorithm=_chosen_hash(), 48 | label=None, 49 | ), 50 | ) 51 | except Exception: 52 | raise 53 | 54 | return text 55 | -------------------------------------------------------------------------------- /src/cryptojwt/jwe/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import struct 3 | from math import ceil 4 | 5 | from cryptography.hazmat.primitives import hashes 6 | from cryptography.hazmat.primitives.hashes import SHA256, SHA384, SHA512 7 | 8 | LENMET = {32: (16, SHA256), 48: (24, SHA384), 64: (32, SHA512)} 9 | 10 | 11 | def get_keys_seclen_dgst(key, iv): 12 | # Validate input 13 | if len(iv) != 16: 14 | raise Exception("IV for AES-CBC must be 16 octets long") 15 | 16 | # Select the digest to use based on key length 17 | try: 18 | seclen, hash_method = LENMET[len(key)] 19 | except KeyError as exc: 20 | raise Exception(f"Invalid CBC+HMAC key length: {len(key)} bytes") from exc 21 | 22 | # Split the key 23 | ka = key[:seclen] 24 | ke = key[seclen:] 25 | 26 | return ka, ke, seclen, hash_method 27 | 28 | 29 | # def int2big_endian(n): 30 | # return [ord(c) for c in struct.pack('>I', n)] 31 | 32 | 33 | # def party_value(pv): 34 | # if pv: 35 | # s = b64e(pv) 36 | # r = int2big_endian(len(s)) 37 | # r.extend(s) 38 | # return r 39 | # else: 40 | # return [0, 0, 0, 0] 41 | 42 | 43 | # def _hash_input(cmk, enc, label, rond=1, length=128, hashsize=256, 44 | # epu="", epv=""): 45 | # r = [0, 0, 0, rond] 46 | # r.extend(cmk) 47 | # r.extend([0, 0, 0, length]) 48 | # r.extend([ord(c) for c in enc]) 49 | # r.extend(party_value(epu)) 50 | # r.extend(party_value(epv)) 51 | # r.extend(label) 52 | # return r 53 | # 54 | # 55 | # def keysize(spec): 56 | # if spec.startswith("HS"): 57 | # return int(spec[2:]) 58 | # elif spec.startswith("CS"): 59 | # return int(spec[2:]) 60 | # elif spec.startswith("A"): 61 | # return int(spec[1:4]) 62 | # return 0 63 | 64 | 65 | def alg2keytype(alg): 66 | if alg.startswith("RSA"): 67 | return "RSA" 68 | elif alg.startswith("A"): 69 | return "oct" 70 | elif alg.startswith("ECDH"): 71 | return "EC" 72 | else: 73 | return None 74 | 75 | 76 | def split_ctx_and_tag(ctext): 77 | tag_length = 16 78 | tag = ctext[-tag_length:] 79 | ciphertext = ctext[:-tag_length] 80 | return ciphertext, tag 81 | 82 | 83 | def get_random_bytes(len): 84 | return os.urandom(len) 85 | 86 | 87 | def concat_sha256(secret, dk_len, other_info): 88 | """ 89 | The Concat KDF, using SHA256 as the hash function. 90 | 91 | Note: Does not validate that otherInfo meets the requirements of 92 | SP800-56A. 93 | 94 | :param secret: The shared secret value 95 | :param dk_len: Length of key to be derived, in bits 96 | :param other_info: Other info to be incorporated (see SP800-56A) 97 | :return: The derived key 98 | """ 99 | dkm = b"" 100 | dk_bytes = int(ceil(dk_len / 8.0)) 101 | counter = 0 102 | while len(dkm) < dk_bytes: 103 | counter += 1 104 | counter_bytes = struct.pack("!I", counter) 105 | digest = hashes.Hash(hashes.SHA256()) 106 | digest.update(counter_bytes) 107 | digest.update(secret) 108 | digest.update(other_info) 109 | dkm += digest.finalize() 110 | return dkm[:dk_bytes] 111 | -------------------------------------------------------------------------------- /src/cryptojwt/jwk/asym.py: -------------------------------------------------------------------------------- 1 | from . import JWK, USE 2 | 3 | 4 | class AsymmetricKey(JWK): 5 | """ 6 | JSON Web key representation of an Asymmetric key 7 | """ 8 | 9 | def __init__( 10 | self, 11 | kty="oct", 12 | alg="", 13 | use="", 14 | kid="", 15 | x5c=None, 16 | x5t="", 17 | x5u="", 18 | k="", 19 | pub_key=None, 20 | priv_key=None, 21 | **kwargs, 22 | ): 23 | JWK.__init__(self, kty, alg, use, kid, x5c, x5t, x5u, **kwargs) 24 | self.k = k 25 | self.pub_key = pub_key 26 | self.priv_key = priv_key 27 | 28 | def appropriate_for(self, usage, **kwargs): 29 | """ 30 | Make sure there is a key instance present that can be used for 31 | the specified usage. 32 | 33 | :param usage: Usage specification, one of [sign, verify, decrypt, 34 | encrypt] 35 | :param kwargs: Extra keyword arguments 36 | :return: Suitable key or None 37 | """ 38 | try: 39 | _use = USE[usage] 40 | except KeyError as exc: 41 | raise ValueError("Unknown key usage") from exc 42 | else: 43 | if usage in ["sign", "decrypt"]: 44 | if not self.use or _use == self.use: 45 | if self.priv_key: 46 | return self.priv_key 47 | return None 48 | else: # has to be one of ['encrypt', 'verify'] 49 | if not self.use or _use == self.use: 50 | if self.pub_key: 51 | return self.pub_key 52 | return None 53 | 54 | def has_private_key(self): 55 | """ 56 | Checks whether there is a private key avaliable. 57 | 58 | :return: True/False 59 | """ 60 | return bool(self.priv_key) 61 | 62 | def public_key(self): 63 | """ 64 | Return a public key instance. 65 | """ 66 | return self.pub_key 67 | 68 | def private_key(self): 69 | """ 70 | Return a private key instance. 71 | """ 72 | return self.priv_key 73 | -------------------------------------------------------------------------------- /src/cryptojwt/jwk/hmac.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from cryptojwt.exception import KeyNotFound 5 | 6 | from ..exception import JWKException, UnsupportedAlgorithm, WrongUsage 7 | from ..utils import as_bytes, as_unicode, b64d, b64e 8 | from . import JWK, USE 9 | from .utils import sha256_digest, sha384_digest, sha512_digest 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | ALG2KEYLEN = { 14 | "A128KW": 16, 15 | "A192KW": 24, 16 | "A256KW": 32, 17 | "HS256": 32, 18 | "HS384": 48, 19 | "HS512": 64, 20 | } 21 | 22 | 23 | class SYMKey(JWK): 24 | """ 25 | JSON Web key representation of a Symmetric key. 26 | According to RFC 7517 a JWK representation of a symmetric key can look like 27 | this:: 28 | 29 | { 30 | "kty":"oct", 31 | "alg":"A128KW", 32 | "k":"GawgguFyGrWKav7AX4VKUg" 33 | } 34 | 35 | """ 36 | 37 | members = JWK.members[:] 38 | members.extend(["kty", "alg", "use", "kid", "k"]) 39 | public_members = JWK.public_members[:] 40 | required = ["k", "kty"] 41 | 42 | def __init__( 43 | self, kty="oct", alg="", use="", kid="", x5c=None, x5t="", x5u="", k="", key="", **kwargs 44 | ): 45 | JWK.__init__(self, kty, alg, use, kid, x5c, x5t, x5u, **kwargs) 46 | self.k = k 47 | self.key = as_bytes(key) 48 | if not self.key and self.k: 49 | if isinstance(self.k, str): 50 | self.k = self.k.encode("utf-8") 51 | self.key = b64d(bytes(self.k)) 52 | 53 | if len(self.key) < 16: 54 | raise UnsupportedAlgorithm("client_secret too short, it should be at least 16 digits") 55 | 56 | def deserialize(self): 57 | self.key = b64d(bytes(self.k)) 58 | 59 | def serialize(self, private=True): 60 | res = self.common() 61 | res["k"] = as_unicode(b64e(bytes(self.key))) 62 | return res 63 | 64 | def get_key(self, **kwargs): 65 | if not self.key: 66 | self.deserialize() 67 | return self.key 68 | 69 | def appropriate_for(self, usage, alg="HS256"): 70 | """ 71 | Make sure there is a key instance present that can be used for 72 | the specified usage. 73 | """ 74 | try: 75 | _use = USE[usage] 76 | except Exception as exc: 77 | raise ValueError("Unknown key usage") from exc 78 | else: 79 | if not self.use or self.use == _use: 80 | if _use == "sig": 81 | return self.get_key() 82 | else: 83 | return self.encryption_key(alg) 84 | 85 | raise WrongUsage(f"This key can't be used for {usage}") 86 | 87 | def encryption_key(self, alg, **kwargs): 88 | """ 89 | Return an encryption key as per 90 | http://openid.net/specs/openid-connect-core-1_0.html#Encryption 91 | 92 | :param alg: encryption algorithm 93 | :param kwargs: 94 | :return: encryption key as byte string 95 | """ 96 | if not self.key: 97 | self.deserialize() 98 | 99 | try: 100 | tsize = ALG2KEYLEN[alg] 101 | except KeyError as exc: 102 | raise UnsupportedAlgorithm(alg) from exc 103 | 104 | if tsize <= 32: 105 | # SHA256 106 | _enc_key = sha256_digest(self.key)[:tsize] 107 | elif tsize <= 48: 108 | # SHA384 109 | _enc_key = sha384_digest(self.key)[:tsize] 110 | elif tsize <= 64: 111 | # SHA512 112 | _enc_key = sha512_digest(self.key)[:tsize] 113 | else: 114 | raise JWKException("No support for symmetric keys > 512 bits") 115 | 116 | logger.debug(f"Symmetric encryption key: {as_unicode(b64e(_enc_key))}") 117 | 118 | return _enc_key 119 | 120 | def __eq__(self, other): 121 | """ 122 | Compare 2 JWK instances to find out if they represent the same key 123 | 124 | :param other: The other JWK instance 125 | :return: True if they are the same otherwise False. 126 | """ 127 | if self.__class__ != other.__class__: 128 | return False 129 | 130 | if set(self.__dict__.keys()) != set(other.__dict__.keys()): 131 | return False 132 | 133 | if self.key != other.key: 134 | return False 135 | 136 | for key in self.public_members: 137 | if getattr(other, key) != getattr(self, key): 138 | if key == "kid": 139 | # if one has a value and the other not then assume they are the same 140 | if getattr(self, key) == "" or getattr(other, key) == "": 141 | return True 142 | return False 143 | 144 | return True 145 | 146 | def key_len(self): 147 | if self.key: 148 | return len(self.key) 149 | else: 150 | raise KeyNotFound 151 | 152 | 153 | def new_sym_key(use="", bytes=24, kid=""): 154 | _key = SYMKey(use=use, kid=kid, key=as_unicode(os.urandom(bytes))) 155 | if not _key.kid: 156 | _key.add_kid() 157 | return _key 158 | -------------------------------------------------------------------------------- /src/cryptojwt/jwk/jwk.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | 5 | from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa 6 | from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp 7 | 8 | from ..exception import MissingValue, UnknownKeyType, UnsupportedAlgorithm, WrongKeyType 9 | from ..utils import base64url_to_long 10 | from .ec import NIST2SEC, ECKey 11 | from .hmac import SYMKey 12 | from .okp import OKPKey 13 | from .rsa import RSAKey 14 | 15 | EC_PUBLIC_REQUIRED = frozenset(["crv", "x", "y"]) 16 | EC_PUBLIC = EC_PUBLIC_REQUIRED 17 | EC_PRIVATE_REQUIRED = frozenset(["d"]) 18 | EC_PRIVATE_OPTIONAL = frozenset() 19 | EC_PRIVATE = EC_PRIVATE_REQUIRED | EC_PRIVATE_OPTIONAL 20 | 21 | OKP_PUBLIC_REQUIRED = frozenset(["crv", "x"]) 22 | OKP_PUBLIC = OKP_PUBLIC_REQUIRED 23 | OKP_PRIVATE_REQUIRED = frozenset(["d"]) 24 | OKP_PRIVATE_OPTIONAL = frozenset() 25 | OKP_PRIVATE = OKP_PRIVATE_REQUIRED | OKP_PRIVATE_OPTIONAL 26 | 27 | RSA_PUBLIC_REQUIRED = frozenset(["e", "n"]) 28 | RSA_PUBLIC = RSA_PUBLIC_REQUIRED 29 | RSA_PRIVATE_REQUIRED = frozenset(["p", "q", "d"]) 30 | RSA_PRIVATE_OPTIONAL = frozenset(["qi", "dp", "dq"]) 31 | RSA_PRIVATE = RSA_PRIVATE_REQUIRED | RSA_PRIVATE_OPTIONAL 32 | 33 | 34 | def ensure_ec_params(jwk_dict, private): 35 | """Ensure all required EC parameters are present in dictionary""" 36 | provided = frozenset(jwk_dict.keys()) 37 | if private is not None and private: 38 | required = EC_PUBLIC_REQUIRED | EC_PRIVATE_REQUIRED 39 | else: 40 | required = EC_PUBLIC_REQUIRED 41 | return ensure_params("EC", provided, required) 42 | 43 | 44 | def ensure_okp_params(jwk_dict, private): 45 | """Ensure all required OKP parameters are present in dictionary""" 46 | provided = frozenset(jwk_dict.keys()) 47 | if private is not None and private: 48 | required = OKP_PUBLIC_REQUIRED | OKP_PRIVATE_REQUIRED 49 | else: 50 | required = OKP_PUBLIC_REQUIRED 51 | return ensure_params("OKP", provided, required) 52 | 53 | 54 | def ensure_rsa_params(jwk_dict, private): 55 | """Ensure all required RSA parameters are present in dictionary""" 56 | provided = frozenset(jwk_dict.keys()) 57 | if private is not None and private: 58 | required = RSA_PUBLIC_REQUIRED | RSA_PRIVATE_REQUIRED 59 | else: 60 | required = RSA_PUBLIC_REQUIRED 61 | return ensure_params("RSA", provided, required) 62 | 63 | 64 | def ensure_params(kty, provided, required): 65 | """Ensure all required parameters are present in dictionary""" 66 | if not required <= provided: 67 | missing = required - provided 68 | raise MissingValue(f"Missing properties for kty={kty}, {str(list(missing))}") 69 | 70 | 71 | def key_from_jwk_dict(jwk_dict, private=None): 72 | """Load JWK from dictionary 73 | 74 | :param jwk_dict: Dictionary representing a JWK 75 | """ 76 | 77 | # uncouple from the original item 78 | _jwk_dict = copy.deepcopy(jwk_dict) 79 | 80 | if "kty" not in _jwk_dict: 81 | raise MissingValue("kty missing") 82 | 83 | if _jwk_dict["kty"] == "EC": 84 | ensure_ec_params(_jwk_dict, private) 85 | 86 | if private is not None and not private: 87 | # remove private components 88 | for v in EC_PRIVATE: 89 | _jwk_dict.pop(v, None) 90 | 91 | if _jwk_dict["crv"] in NIST2SEC: 92 | curve = NIST2SEC[_jwk_dict["crv"]]() 93 | else: 94 | raise UnsupportedAlgorithm("Unknown curve: {}".format(_jwk_dict["crv"])) 95 | 96 | if _jwk_dict.get("d", None) is not None: 97 | # Ecdsa private key. 98 | _jwk_dict["priv_key"] = ec.derive_private_key(base64url_to_long(_jwk_dict["d"]), curve) 99 | _jwk_dict["pub_key"] = _jwk_dict["priv_key"].public_key() 100 | else: 101 | # Ecdsa public key. 102 | ec_pub_numbers = ec.EllipticCurvePublicNumbers( 103 | base64url_to_long(_jwk_dict["x"]), 104 | base64url_to_long(_jwk_dict["y"]), 105 | curve, 106 | ) 107 | _jwk_dict["pub_key"] = ec_pub_numbers.public_key() 108 | return ECKey(**_jwk_dict) 109 | elif _jwk_dict["kty"] == "RSA": 110 | ensure_rsa_params(_jwk_dict, private) 111 | 112 | if private is not None and not private: 113 | # remove private components 114 | for v in RSA_PRIVATE: 115 | _jwk_dict.pop(v, None) 116 | 117 | rsa_pub_numbers = rsa.RSAPublicNumbers( 118 | base64url_to_long(_jwk_dict["e"]), base64url_to_long(_jwk_dict["n"]) 119 | ) 120 | if _jwk_dict.get("p", None) is not None: 121 | # Rsa private key. These MUST be present 122 | p_long = base64url_to_long(_jwk_dict["p"]) 123 | q_long = base64url_to_long(_jwk_dict["q"]) 124 | d_long = base64url_to_long(_jwk_dict["d"]) 125 | # If not present these can be calculated from the others 126 | if "dp" not in _jwk_dict: 127 | dp_long = rsa_crt_dmp1(d_long, p_long) 128 | else: 129 | dp_long = base64url_to_long(_jwk_dict["dp"]) 130 | if "dq" not in _jwk_dict: 131 | dq_long = rsa_crt_dmq1(d_long, q_long) 132 | else: 133 | dq_long = base64url_to_long(_jwk_dict["dq"]) 134 | if "qi" not in _jwk_dict: 135 | qi_long = rsa_crt_iqmp(p_long, q_long) 136 | else: 137 | qi_long = base64url_to_long(_jwk_dict["qi"]) 138 | 139 | rsa_priv_numbers = rsa.RSAPrivateNumbers( 140 | p_long, q_long, d_long, dp_long, dq_long, qi_long, rsa_pub_numbers 141 | ) 142 | _jwk_dict["priv_key"] = rsa_priv_numbers.private_key() 143 | _jwk_dict["pub_key"] = _jwk_dict["priv_key"].public_key() 144 | else: 145 | _jwk_dict["pub_key"] = rsa_pub_numbers.public_key() 146 | 147 | if _jwk_dict["kty"] != "RSA": 148 | raise WrongKeyType('"{}" should have been "RSA"'.format(_jwk_dict["kty"])) 149 | return RSAKey(**_jwk_dict) 150 | elif _jwk_dict["kty"] == "OKP": 151 | ensure_okp_params(_jwk_dict, private) 152 | 153 | if private is not None and not private: 154 | # remove private components 155 | for v in OKP_PRIVATE: 156 | _jwk_dict.pop(v, None) 157 | 158 | return OKPKey(**_jwk_dict) 159 | elif _jwk_dict["kty"] == "oct": 160 | if "key" not in _jwk_dict and "k" not in _jwk_dict: 161 | raise MissingValue('There has to be one of "k" or "key" in a symmetric key') 162 | 163 | return SYMKey(**_jwk_dict) 164 | else: 165 | raise UnknownKeyType 166 | 167 | 168 | def jwk_wrap(key, use="", kid=""): 169 | """ 170 | Instantiate a Key instance with the given key 171 | 172 | :param key: The keys to wrap 173 | :param use: What the key are expected to be use for 174 | :param kid: A key id 175 | :return: The Key instance 176 | """ 177 | if isinstance(key, (rsa.RSAPublicKey, rsa.RSAPrivateKey)): 178 | kspec = RSAKey(use=use, kid=kid).load_key(key) 179 | elif isinstance(key, str): 180 | kspec = SYMKey(key=key, use=use, kid=kid) 181 | elif isinstance(key, ec.EllipticCurvePublicKey): 182 | kspec = ECKey(use=use, kid=kid).load_key(key) 183 | elif isinstance(key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)): 184 | kspec = OKPKey(use=use, kid=kid).load_key(key) 185 | else: 186 | raise Exception("Unknown key type:key=" + str(type(key))) 187 | 188 | if not kspec.kid: 189 | kspec.add_kid() 190 | 191 | kspec.serialize() 192 | return kspec 193 | 194 | 195 | def dump_jwk(filename, key): 196 | """Writes a RSAKey, ECKey or SYMKey instance as a JWK to a file.""" 197 | head, tail = os.path.split(filename) 198 | if head and not os.path.isdir(head): 199 | os.makedirs(head) 200 | 201 | with open(filename, "w") as fp: 202 | fp.write(json.dumps(key.to_dict())) 203 | 204 | 205 | def import_jwk(filename): 206 | """Reads a JWK from a file and converts it into the appropriate key class instance.""" 207 | if os.path.isfile(filename): 208 | with open(filename) as jwk_file: 209 | jwk_dict = json.loads(jwk_file.read()) 210 | return key_from_jwk_dict(jwk_dict) 211 | return None 212 | -------------------------------------------------------------------------------- /src/cryptojwt/jwk/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from ..utils import as_bytes 4 | 5 | 6 | def sha256_digest(msg): 7 | """ 8 | Produce a SHA256 digest of a message 9 | 10 | :param msg: The message 11 | :return: A SHA256 digest 12 | """ 13 | return hashlib.sha256(as_bytes(msg)).digest() 14 | 15 | 16 | def sha384_digest(msg): 17 | """ 18 | Produce a SHA384 digest of a message 19 | 20 | :param msg: The message 21 | :return: A SHA384 digest 22 | """ 23 | return hashlib.sha384(as_bytes(msg)).digest() 24 | 25 | 26 | def sha512_digest(msg): 27 | """ 28 | Produce a SHA512 digest of a message 29 | 30 | :param msg: The message 31 | :return: A SHA512 digest 32 | """ 33 | return hashlib.sha512(as_bytes(msg)).digest() 34 | 35 | 36 | DIGEST_HASH = { 37 | "SHA-256": sha256_digest, 38 | "SHA-384": sha384_digest, 39 | "SHA-512": sha512_digest, 40 | } 41 | -------------------------------------------------------------------------------- /src/cryptojwt/jwk/wrap.py: -------------------------------------------------------------------------------- 1 | """JWK wrapping""" 2 | 3 | import json 4 | 5 | from .. import JWE 6 | from . import JWK 7 | from .jwk import key_from_jwk_dict 8 | 9 | __author__ = "jschlyter" 10 | 11 | DEFAULT_WRAP_PARAMS = { 12 | "EC": {"alg": "ECDH-ES+A128KW", "enc": "A128GCM"}, 13 | "RSA": {"alg": "RSA1_5", "enc": "A128CBC-HS256"}, 14 | "oct": {"alg": "A128KW", "enc": "A128CBC-HS256"}, 15 | } 16 | 17 | 18 | def wrap_key(key: JWK, wrapping_key: JWK, wrap_params: dict = DEFAULT_WRAP_PARAMS) -> str: 19 | message = json.dumps(key.serialize(private=True)).encode() 20 | try: 21 | enc_params = wrap_params[wrapping_key.kty] 22 | except KeyError as exc: 23 | raise ValueError("Unsupported wrapping key type") from exc 24 | _jwe = JWE(msg=message, **enc_params) 25 | return _jwe.encrypt(keys=[wrapping_key], kid=wrapping_key.kid) 26 | 27 | 28 | def unwrap_key(jwe: str, wrapping_keys: JWK) -> JWK: 29 | _jwe = JWE() 30 | message = _jwe.decrypt(token=jwe, keys=wrapping_keys) 31 | return key_from_jwk_dict(json.loads(message)) 32 | -------------------------------------------------------------------------------- /src/cryptojwt/jwk/x509.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import logging 4 | 5 | from cryptography import x509 6 | from cryptography.hazmat.primitives import serialization 7 | from cryptography.hazmat.primitives.asymmetric import ec, rsa 8 | 9 | from cryptojwt.utils import b64e 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def import_public_key_from_pem_file(filename): 15 | """ 16 | Read a public RSA key from a PEM file. 17 | 18 | :param filename: The name of the file 19 | :param passphrase: A pass phrase to use to unpack the PEM file. 20 | :return: A public key instance 21 | """ 22 | with open(filename, "rb") as key_file: 23 | public_key = serialization.load_pem_public_key(key_file.read()) 24 | return public_key 25 | 26 | 27 | def import_private_key_from_pem_file(filename, passphrase=None): 28 | """ 29 | Read a private RSA key from a PEM file. 30 | 31 | :param filename: The name of the file 32 | :param passphrase: A pass phrase to use to unpack the PEM file. 33 | :return: A private key instance 34 | """ 35 | with open(filename, "rb") as key_file: 36 | private_key = serialization.load_pem_private_key(key_file.read(), password=passphrase) 37 | return private_key 38 | 39 | 40 | PREFIX = "-----BEGIN CERTIFICATE-----" 41 | POSTFIX = "-----END CERTIFICATE-----" 42 | 43 | 44 | def import_public_key_from_pem_data(pem_data): 45 | """ 46 | Extract an RSA key from a PEM-encoded X.509 certificate 47 | 48 | :param pem_data: RSA key encoded in standard form 49 | :return: rsa.RSAPublicKey instance 50 | """ 51 | if not pem_data.startswith(PREFIX): 52 | pem_data = bytes(f"{PREFIX}\n{pem_data}\n{POSTFIX}", "utf-8") 53 | else: 54 | pem_data = bytes(pem_data, "utf-8") 55 | cert = x509.load_pem_x509_certificate(pem_data) 56 | return cert.public_key() 57 | 58 | 59 | def import_public_key_from_cert_file(filename): 60 | """ 61 | Read a public key from a certificate file. 62 | 63 | :param filename: The name of the file 64 | :return: A public key instance 65 | """ 66 | with open(filename, "rb") as key_file: 67 | cert = x509.load_pem_x509_certificate(key_file.read()) 68 | return cert.public_key() 69 | 70 | 71 | def der_cert(der_data): 72 | """ 73 | Load a DER encoded certificate 74 | 75 | :param der_data: DER-encoded certificate 76 | :return: A cryptography.x509.certificate instance 77 | """ 78 | if isinstance(der_data, str): 79 | der_data = bytes(der_data, "utf-8") 80 | return x509.load_der_x509_certificate(der_data) 81 | 82 | 83 | def load_x509_cert(url, httpc, spec2key, **get_args): 84 | """ 85 | Get and transform a X509 cert into a key. 86 | 87 | :param url: Where the X509 cert can be found 88 | :param httpc: HTTP client to use for fetching 89 | :param spec2key: A dictionary over keys already seen 90 | :param get_args: Extra key word arguments to the HTTP GET request 91 | :return: List of 2-tuples (keytype, key) 92 | """ 93 | try: 94 | r = httpc("GET", url, allow_redirects=True, **get_args) 95 | if r.status_code == 200: 96 | cert = str(r.text) 97 | try: 98 | public_key = spec2key[cert] # If I've already seen it 99 | except KeyError: 100 | public_key = import_public_key_from_pem_data(cert) 101 | spec2key[cert] = public_key 102 | 103 | if isinstance(public_key, rsa.RSAPublicKey): 104 | return {"rsa": public_key} 105 | elif isinstance(public_key, ec.EllipticCurvePublicKey): 106 | return {"ec": public_key} 107 | else: 108 | raise Exception(f"HTTP Get error: {r.status_code}") 109 | except Exception as err: # not a RSA key 110 | logger.warning(f"Can't load key: {err}") 111 | return [] 112 | 113 | 114 | def x5t_calculation(cert): 115 | """ 116 | base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER 117 | encoding of an X.509 certificate. 118 | 119 | :param cert: DER encoded X.509 certificate 120 | :return: x5t value 121 | """ 122 | if isinstance(cert, str): 123 | der_cert = base64.b64decode(cert.encode("ascii")) 124 | else: 125 | der_cert = base64.b64decode(cert) 126 | 127 | return b64e(hashlib.sha1(der_cert).digest()) 128 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/__init__.py: -------------------------------------------------------------------------------- 1 | class Signer: 2 | """Abstract base class for signing algorithms.""" 3 | 4 | def sign(self, msg, key): 5 | """Sign ``msg`` with ``key`` and return the signature.""" 6 | raise NotImplementedError() 7 | 8 | def verify(self, msg, sig, key): 9 | """Return True if ``sig`` is a valid signature for ``msg``.""" 10 | raise NotImplementedError() 11 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/dsa.py: -------------------------------------------------------------------------------- 1 | from cryptography.exceptions import InvalidSignature 2 | from cryptography.hazmat.primitives import hashes 3 | from cryptography.hazmat.primitives.asymmetric import ec 4 | from cryptography.hazmat.primitives.asymmetric.utils import ( 5 | decode_dss_signature, 6 | encode_dss_signature, 7 | ) 8 | 9 | from ..exception import BadSignature, Unsupported 10 | from . import Signer 11 | 12 | 13 | class ECDSASigner(Signer): 14 | def __init__(self, algorithm="ES256"): 15 | if algorithm == "ES256": 16 | self.hash_algorithm = hashes.SHA256 17 | self.curve_name = "secp256r1" 18 | elif algorithm == "ES256K": 19 | self.hash_algorithm = hashes.SHA256 20 | self.curve_name = "secp256k1" 21 | elif algorithm == "ES384": 22 | self.hash_algorithm = hashes.SHA384 23 | self.curve_name = "secp384r1" 24 | elif algorithm == "ES512": 25 | self.hash_algorithm = hashes.SHA512 26 | self.curve_name = "secp521r1" 27 | else: 28 | raise Unsupported(f"algorithm: {algorithm}") 29 | 30 | self.algorithm = algorithm 31 | 32 | def sign(self, msg, key): 33 | """ 34 | Create a signature over a message as defined in RFC7515 using an 35 | Elliptic curve key 36 | 37 | :param msg: The message 38 | :param key: An ec.EllipticCurvePrivateKey instance 39 | :return: 40 | """ 41 | 42 | if not isinstance(key, ec.EllipticCurvePrivateKey): 43 | raise TypeError("The private key must be an instance of ec.EllipticCurvePrivateKey") 44 | 45 | self._cross_check(key.public_key()) 46 | num_bits = key.curve.key_size 47 | num_bytes = (num_bits + 7) // 8 48 | asn1sig = key.sign(msg, ec.ECDSA(self.hash_algorithm())) 49 | # Cryptography returns ASN.1-encoded signature data; decode as JWS 50 | # uses raw signatures (r||s) 51 | (r, s) = decode_dss_signature(asn1sig) 52 | return int.to_bytes(r, num_bytes, "big") + int.to_bytes(s, num_bytes, "big") 53 | 54 | def verify(self, msg, sig, key): 55 | """ 56 | Verify a message signature 57 | 58 | :param msg: The message 59 | :param sig: A signature 60 | :param key: A ec.EllipticCurvePublicKey to use for the verification. 61 | :raises: BadSignature if the signature can't be verified. 62 | :return: True 63 | """ 64 | if not isinstance(key, ec.EllipticCurvePublicKey): 65 | raise TypeError("The public key must be an instance of ec.EllipticCurvePublicKey") 66 | self._cross_check(key) 67 | 68 | num_bits = key.curve.key_size 69 | num_bytes = (num_bits + 7) // 8 70 | if len(sig) != 2 * num_bytes: 71 | raise ValueError("Invalid signature") 72 | 73 | try: 74 | # cryptography uses ASN.1-encoded signature data; split JWS 75 | # signature (r||s) and encode before verification 76 | (r, s) = self._split_raw_signature(sig) 77 | asn1sig = encode_dss_signature(r, s) 78 | key.verify(asn1sig, msg, ec.ECDSA(self.hash_algorithm())) 79 | except InvalidSignature as exc: 80 | raise BadSignature(exc) from exc 81 | else: 82 | return True 83 | 84 | def _cross_check(self, pub_key): 85 | """ 86 | In Ecdsa, both the key and the algorithm define the curve. 87 | Therefore, we must crosscheck them to make sure they're the same. 88 | 89 | :param key: 90 | :raises: ValueError is the curves are not the same 91 | """ 92 | if self.curve_name != pub_key.curve.name: 93 | raise ValueError( 94 | f"The curve in private key {pub_key.curve.name} and in algorithm {self.curve_name} don't " 95 | "match" 96 | ) 97 | 98 | @staticmethod 99 | def _split_raw_signature(sig): 100 | """ 101 | Split raw signature into components 102 | 103 | :param sig: The signature 104 | :return: A 2-tuple 105 | """ 106 | c_length = len(sig) // 2 107 | r = int.from_bytes(sig[:c_length], byteorder="big") 108 | s = int.from_bytes(sig[c_length:], byteorder="big") 109 | return r, s 110 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/eddsa.py: -------------------------------------------------------------------------------- 1 | from cryptography.exceptions import InvalidSignature 2 | from cryptography.hazmat.primitives.asymmetric import ed448, ed25519 3 | 4 | from ..exception import BadSignature 5 | from . import Signer 6 | 7 | 8 | class EDDSASigner(Signer): 9 | def __init__(self, algorithm=None): 10 | self.algorithm = algorithm 11 | 12 | def sign(self, msg, key): 13 | """ 14 | Create a signature over a message as defined in RFC7515 using an 15 | Octet Key Pair key 16 | 17 | :param msg: The message 18 | :param key: An Ed25519PrivateKey or Ed448PrivateKey instance 19 | :return: 20 | """ 21 | 22 | if self.algorithm: 23 | if self.algorithm == "Ed25519" and not isinstance(key, ed25519.Ed25519PrivateKey): 24 | raise TypeError("The private key must be an instance of Ed25519PrivateKey") 25 | if self.algorithm == "Ed448" and not isinstance(key, ed448.Ed448PrivateKey): 26 | raise TypeError("The private key must be an instance of Ed448PrivateKey") 27 | 28 | if not isinstance(key, (ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey)): 29 | raise TypeError( 30 | "The private key must be an instance of Ed25519PrivateKey or Ed448PrivateKey" 31 | ) 32 | 33 | if not isinstance(key, (ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey)): 34 | raise TypeError( 35 | "The private key must be an instance of Ed25519PrivateKey or Ed448PrivateKey" 36 | ) 37 | 38 | return key.sign(msg) 39 | 40 | def verify(self, msg, sig, key): 41 | """ 42 | Verify a message signature 43 | 44 | :param msg: The message 45 | :param sig: A signature 46 | :param key: A Ed25519PublicKey or Ed448PublicKey to use for the verification. 47 | :raises: BadSignature if the signature can't be verified. 48 | :return: True 49 | """ 50 | 51 | if self.algorithm: 52 | if self.algorithm == "Ed25519" and not isinstance(key, ed25519.Ed25519PublicKey): 53 | raise TypeError("The public key must be an instance of Ed25519PublicKey") 54 | if self.algorithm == "Ed448" and not isinstance(key, ed448.Ed448PublicKey): 55 | raise TypeError("The public key must be an instance of Ed448PublicKey") 56 | 57 | if not isinstance(key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)): 58 | raise TypeError( 59 | "The public key must be an instance of Ed25519PublicKey or Ed448PublicKey" 60 | ) 61 | 62 | try: 63 | key.verify(sig, msg) 64 | except InvalidSignature as exc: 65 | raise BadSignature(exc) from exc 66 | else: 67 | return True 68 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/exception.py: -------------------------------------------------------------------------------- 1 | from ..exception import JWKESTException 2 | 3 | 4 | class JWSException(JWKESTException): 5 | pass 6 | 7 | 8 | class NoSuitableSigningKeys(JWSException): 9 | pass 10 | 11 | 12 | class FormatError(JWSException): 13 | pass 14 | 15 | 16 | class WrongTypeOfKey(JWSException): 17 | pass 18 | 19 | 20 | class UnknownSignerAlg(JWSException): 21 | pass 22 | 23 | 24 | class SignerAlgError(JWSException): 25 | pass 26 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/hmac.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.primitives import hashes, hmac 2 | 3 | from ..exception import Unsupported 4 | from . import Signer 5 | 6 | 7 | class HMACSigner(Signer): 8 | def __init__(self, algorithm="SHA256"): 9 | if algorithm == "SHA256": 10 | self.algorithm = hashes.SHA256 11 | elif algorithm == "SHA384": 12 | self.algorithm = hashes.SHA384 13 | elif algorithm == "SHA512": 14 | self.algorithm = hashes.SHA512 15 | else: 16 | raise Unsupported(f"algorithm: {algorithm}") 17 | 18 | def sign(self, msg, key): 19 | """ 20 | Create a signature over a message as defined in RFC7515 using a 21 | symmetric key 22 | 23 | :param msg: The message 24 | :param key: The key 25 | :return: A signature 26 | """ 27 | h = hmac.HMAC(key, self.algorithm()) 28 | h.update(msg) 29 | return h.finalize() 30 | 31 | def verify(self, msg, sig, key): 32 | """ 33 | Verifies whether sig is the correct message authentication code of data. 34 | 35 | :param msg: The data 36 | :param sig: The message authentication code to verify against data. 37 | :param key: The key to use 38 | :return: Returns true if the mac was valid otherwise it will raise an 39 | Exception. 40 | """ 41 | try: 42 | h = hmac.HMAC(key, self.algorithm()) 43 | h.update(msg) 44 | h.verify(sig) 45 | return True 46 | except Exception: 47 | return False 48 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/pss.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from cryptography.exceptions import InvalidSignature 4 | from cryptography.hazmat.primitives import hashes 5 | from cryptography.hazmat.primitives.asymmetric import padding, utils 6 | 7 | from ..exception import BadSignature, Unsupported 8 | from . import Signer 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class PSSSigner(Signer): 14 | def __init__(self, algorithm="SHA256"): 15 | if algorithm == "SHA256": 16 | self.hash_algorithm = hashes.SHA256 17 | elif algorithm == "SHA384": 18 | self.hash_algorithm = hashes.SHA384 19 | elif algorithm == "SHA512": 20 | self.hash_algorithm = hashes.SHA512 21 | else: 22 | raise Unsupported(f"algorithm: {algorithm}") 23 | 24 | def sign(self, msg, key): 25 | """ 26 | Create a signature over a message 27 | 28 | :param msg: The message 29 | :param key: The key 30 | :return: A signature 31 | """ 32 | hasher = hashes.Hash(self.hash_algorithm()) 33 | hasher.update(msg) 34 | digest = hasher.finalize() 35 | sig = key.sign( 36 | digest, 37 | padding.PSS( 38 | mgf=padding.MGF1(self.hash_algorithm()), 39 | salt_length=padding.PSS.MAX_LENGTH, 40 | ), 41 | utils.Prehashed(self.hash_algorithm()), 42 | ) 43 | return sig 44 | 45 | def verify(self, msg, signature, key): 46 | """ 47 | Verify a message signature 48 | 49 | :param msg: The message 50 | :param sig: A signature 51 | :param key: A ec.EllipticCurvePublicKey to use for the verification. 52 | :raises: BadSignature if the signature can't be verified. 53 | :return: True 54 | """ 55 | try: 56 | key.verify( 57 | signature, 58 | msg, 59 | padding.PSS( 60 | mgf=padding.MGF1(self.hash_algorithm()), 61 | salt_length=padding.PSS.MAX_LENGTH, 62 | ), 63 | self.hash_algorithm(), 64 | ) 65 | except InvalidSignature as exc: 66 | raise BadSignature(exc) from exc 67 | else: 68 | return True 69 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/rsa.py: -------------------------------------------------------------------------------- 1 | from cryptography.exceptions import InvalidSignature 2 | from cryptography.hazmat.primitives.asymmetric import rsa 3 | 4 | from ..exception import BadSignature 5 | from . import Signer 6 | from .utils import parse_rsa_algorithm 7 | 8 | 9 | class RSASigner(Signer): 10 | def __init__(self, algorithm="RS256"): 11 | (self.hash, self.padding) = parse_rsa_algorithm(algorithm) 12 | 13 | def sign(self, msg, key): 14 | """ 15 | Create a signature over a message as defined in RFC7515 using an 16 | RSA key 17 | 18 | :param msg: the message. 19 | :type msg: bytes 20 | :returns: bytes, the signature of data. 21 | :rtype: bytes 22 | """ 23 | 24 | if not isinstance(key, rsa.RSAPrivateKey): 25 | raise TypeError("The key must be an instance of rsa.RSAPrivateKey") 26 | sig = key.sign(msg, self.padding, self.hash) 27 | return sig 28 | 29 | def verify(self, msg, signature, key): 30 | """ 31 | Verifies whether signature is a valid signature for message 32 | 33 | :param msg: the message 34 | :type msg: bytes 35 | :param signature: The signature to be verified 36 | :type signature: bytes 37 | :param key: The key 38 | :return: True is the signature is valid otherwise False 39 | """ 40 | 41 | if not isinstance(key, rsa.RSAPublicKey): 42 | raise TypeError("The public key must be an instance of RSAPublicKey") 43 | try: 44 | key.verify(signature, msg, self.padding, self.hash) 45 | except InvalidSignature as exc: 46 | raise BadSignature(str(exc)) from exc 47 | except AttributeError: 48 | return False 49 | else: 50 | return True 51 | -------------------------------------------------------------------------------- /src/cryptojwt/jws/utils.py: -------------------------------------------------------------------------------- 1 | # import struct 2 | 3 | from cryptography.hazmat.primitives import hashes 4 | from cryptography.hazmat.primitives.asymmetric import padding 5 | 6 | from ..exception import UnsupportedAlgorithm 7 | from ..jwk.hmac import sha256_digest, sha384_digest, sha512_digest 8 | from ..utils import as_unicode, b64e 9 | 10 | 11 | def left_hash(msg, func="HS256"): 12 | """Calculate left hash as described in 13 | https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken 14 | for at_hash and in 15 | for c_hash 16 | 17 | :param msg: The message over which the hash should be calculated 18 | :param func: Which hash function that was used for the ID token 19 | """ 20 | if func == "HS256": 21 | return as_unicode(b64e(sha256_digest(msg)[:16])) 22 | elif func == "HS384": 23 | return as_unicode(b64e(sha384_digest(msg)[:24])) 24 | elif func == "HS512": 25 | return as_unicode(b64e(sha512_digest(msg)[:32])) 26 | 27 | 28 | # def mpint(b): 29 | # b += b"\x00" 30 | # return struct.pack(">L", len(b)) + b 31 | # 32 | 33 | 34 | def alg2keytype(alg): 35 | """ 36 | Go from algorithm name to key type. 37 | 38 | :param alg: The algorithm name 39 | :return: The key type 40 | """ 41 | if not alg or alg.lower() == "none": 42 | return "none" 43 | elif alg.startswith("RS") or alg.startswith("PS"): 44 | return "RSA" 45 | elif alg.startswith("HS") or alg.startswith("A"): 46 | return "oct" 47 | elif alg == "Ed25519" or alg == "Ed448": 48 | return "OKP" 49 | elif alg.startswith("ES") or alg.startswith("ECDH-ES"): 50 | return "EC" 51 | elif alg == "EdDSA": 52 | return "OKP" 53 | else: 54 | return None 55 | 56 | 57 | def parse_rsa_algorithm(algorithm): 58 | """ 59 | Parses a RSA algorithm and returns tuple (hash, padding). 60 | 61 | :param algorithm: string, RSA algorithm as defined at 62 | https://tools.ietf.org/html/rfc7518#section-3.1. 63 | :raises: UnsupportedAlgorithm: if the algorithm is not supported. 64 | :returns: (hash, padding) tuple. 65 | """ 66 | 67 | if algorithm == "RS256": 68 | return hashes.SHA256(), padding.PKCS1v15() 69 | elif algorithm == "RS384": 70 | return hashes.SHA384(), padding.PKCS1v15() 71 | elif algorithm == "RS512": 72 | return hashes.SHA512(), padding.PKCS1v15() 73 | elif algorithm == "PS256": 74 | return ( 75 | hashes.SHA256(), 76 | padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), 77 | ) 78 | elif algorithm == "PS384": 79 | return ( 80 | hashes.SHA384(), 81 | padding.PSS(mgf=padding.MGF1(hashes.SHA384()), salt_length=padding.PSS.MAX_LENGTH), 82 | ) 83 | elif algorithm == "PS512": 84 | return ( 85 | hashes.SHA512(), 86 | padding.PSS(mgf=padding.MGF1(hashes.SHA512()), salt_length=padding.PSS.MAX_LENGTH), 87 | ) 88 | else: 89 | raise UnsupportedAlgorithm(f"Unknown algorithm: {algorithm}") 90 | -------------------------------------------------------------------------------- /src/cryptojwt/jwx.py: -------------------------------------------------------------------------------- 1 | """A basic class on which to build the JWS and JWE classes.""" 2 | 3 | import json 4 | import logging 5 | import warnings 6 | 7 | import requests 8 | 9 | from cryptojwt.jwk import JWK 10 | from cryptojwt.key_bundle import KeyBundle 11 | 12 | from .exception import HeaderError 13 | from .jwe import DEPRECATED 14 | from .jwk.jwk import key_from_jwk_dict 15 | from .jwk.rsa import RSAKey, import_rsa_key 16 | from .jwk.x509 import load_x509_cert 17 | from .utils import as_bytes, as_unicode, b64d 18 | 19 | LOGGER = logging.getLogger(__name__) 20 | 21 | __author__ = "Roland Hedberg" 22 | 23 | 24 | class JWx: 25 | """A basic class with the commonalities between the JWS and JWE classes. 26 | 27 | :param alg: The signing algorithm 28 | :param jku: a URI that refers to a resource for a set of JSON-encoded 29 | public keys, one of which corresponds to the key used to digitally 30 | sign the JWS 31 | :param jwk: A JSON Web Key that corresponds to the key used to 32 | digitally sign the JWS 33 | :param x5u: a URI that refers to a resource for the X.509 public key 34 | certificate or certificate chain [RFC5280] corresponding to the key 35 | used to digitally sign the JWS. 36 | :param x5t: a base64url encoded SHA-1 thumbprint (a.k.a. digest) of the 37 | DER encoding of the X.509 certificate [RFC5280] corresponding to 38 | the key used to digitally sign the JWS. 39 | :param x5c: the X.509 public key certificate or certificate chain 40 | corresponding to the key used to digitally sign the JWS. 41 | :param kid: a hint indicating which key was used to secure the JWS. 42 | :param typ: the type of this object. 'JWS' == JWS Compact Serialization 43 | 'JWS+JSON' == JWS JSON Serialization 44 | :param cty: the type of the secured content 45 | :param crit: indicates which extensions that are being used and MUST 46 | be understood and processed. 47 | :param kwargs: Extra header parameters 48 | :return: A class instance 49 | """ 50 | 51 | args = ["alg", "jku", "jwk", "x5u", "x5t", "x5c", "kid", "typ", "cty", "crit", "trust_chain"] 52 | 53 | def __init__(self, msg=None, with_digest=False, httpc=None, **kwargs): 54 | self.msg = msg 55 | 56 | self._dict = {} 57 | self.with_digest = with_digest 58 | if httpc: 59 | self.httpc = httpc 60 | else: 61 | self.httpc = requests.request 62 | 63 | self.jwt = None 64 | self._jwk = None 65 | self._jwks = None 66 | self._header = {} 67 | 68 | if kwargs: 69 | for key in self.args: 70 | try: 71 | _val = kwargs[key] 72 | except KeyError: 73 | continue 74 | 75 | if key == "jwk": 76 | self._set_jwk(_val) 77 | self._jwk = self._dict["jwk"] 78 | elif key == "x5c": 79 | self._dict["x5c"] = _val 80 | _pub_key = import_rsa_key(_val) 81 | self._jwk = RSAKey(pub_key=_pub_key).to_dict() 82 | elif key == "jku": 83 | self._jwks = KeyBundle(source=_val, httpc=self.httpc) 84 | self._dict["jku"] = _val 85 | elif "x5u" in self: 86 | try: 87 | _spec = load_x509_cert(self["x5u"], self.httpc, {}) 88 | self._jwk = RSAKey(pub_key=_spec["rsa"]).to_dict() 89 | except Exception as exc: 90 | # ca_chain = load_x509_cert_chain(self["x5u"]) 91 | raise ValueError("x5u") from exc 92 | else: 93 | self._dict[key] = _val 94 | if key in DEPRECATED and _val in DEPRECATED[key]: 95 | warnings.warn(f"{key}={_val} deprecated", stacklevel=1) 96 | 97 | def _set_jwk(self, val): 98 | if isinstance(val, dict): 99 | _k = key_from_jwk_dict(val) 100 | self._dict["jwk"] = val 101 | elif isinstance(val, str): 102 | # verify that it's a real JWK 103 | _val = json.loads(val) 104 | _j = key_from_jwk_dict(_val) 105 | self._dict["jwk"] = _val 106 | elif isinstance(val, JWK): 107 | self._dict["jwk"] = val.to_dict() 108 | else: 109 | raise ValueError("JWK must be a string a JSON object or a JWK instance") 110 | 111 | def __contains__(self, item): 112 | return item in self._dict 113 | 114 | def __getitem__(self, item): 115 | return self._dict[item] 116 | 117 | def __setitem__(self, key, value): 118 | self._dict[key] = value 119 | 120 | def __getattr__(self, item): 121 | try: 122 | return self._dict[item] 123 | except KeyError as exc: 124 | raise AttributeError(item) from exc 125 | 126 | def keys(self): 127 | """Return all keys.""" 128 | return list(self._dict.keys()) 129 | 130 | def _set_header_jwk(self, header, **kwargs): 131 | if "jwk" in self: 132 | header["jwk"] = self["jwk"] 133 | else: 134 | try: 135 | _jwk = kwargs["jwk"] 136 | except KeyError: 137 | pass 138 | else: 139 | try: 140 | header["jwk"] = _jwk.serialize() # JWK instance 141 | except AttributeError: 142 | if isinstance(_jwk, dict): 143 | header["jwk"] = _jwk # dictionary 144 | else: 145 | _d = json.loads(_jwk) # JSON 146 | # Verify that it's a valid JWK 147 | _k = key_from_jwk_dict(_d) 148 | header["jwk"] = _d 149 | 150 | def headers(self, **kwargs): 151 | """Return the JWE/JWS header.""" 152 | _header = self._header.copy() 153 | for param in self.args: 154 | try: 155 | _header[param] = kwargs[param] 156 | except KeyError: 157 | try: 158 | if self._dict[param]: 159 | _header[param] = self._dict[param] 160 | except KeyError: 161 | pass 162 | 163 | self._set_header_jwk(_header, **kwargs) 164 | 165 | if "kid" in self: 166 | if not isinstance(self["kid"], str): 167 | raise HeaderError("kid of wrong value type") 168 | 169 | return _header 170 | 171 | def _get_keys(self): 172 | _keys = [] 173 | if self._jwk: 174 | _keys.append(key_from_jwk_dict(self._jwk)) 175 | if self._jwks is not None: 176 | _keys.extend(self._jwks.keys()) 177 | return _keys 178 | 179 | def alg2keytype(self, alg): 180 | """Convert an algorithm identifier to a key type identifier.""" 181 | raise NotImplementedError() 182 | 183 | def pick_keys(self, keys, use="", alg=""): 184 | """ 185 | The assumption is that upper layer has made certain you only get 186 | keys you can use. 187 | 188 | :param alg: The crypto algorithm 189 | :param use: What the key should be used for 190 | :param keys: A list of JWK instances 191 | :return: A list of JWK instances that fulfill the requirements 192 | """ 193 | if not alg: 194 | alg = self["alg"] 195 | 196 | if alg == "none": 197 | return [] 198 | 199 | _k = self.alg2keytype(alg) 200 | if _k is None: 201 | LOGGER.error("Unknown algorithm '%s'", alg) 202 | raise ValueError("Unknown cryptography algorithm") 203 | 204 | LOGGER.debug("Picking key by key type=%s", _k) 205 | _kty = [ 206 | _k.lower(), 207 | _k.upper(), 208 | _k.lower().encode("utf-8"), 209 | _k.upper().encode("utf-8"), 210 | ] 211 | _keys = [k for k in keys if k.kty in _kty] 212 | try: 213 | _kid = self["kid"] 214 | except KeyError: 215 | try: 216 | _kid = self.jwt.headers["kid"] 217 | except (AttributeError, KeyError): 218 | _kid = None 219 | 220 | LOGGER.debug("Picking key based on alg=%s, kid=%s and use=%s", alg, _kid, use) 221 | 222 | pkey = [] 223 | for _key in _keys: 224 | LOGGER.debug("Picked: kid:%s, use:%s, kty:%s", _key.kid, _key.use, _key.kty) 225 | if _kid: 226 | if _kid != _key.kid: 227 | continue 228 | 229 | if use and _key.use and _key.use != use: 230 | continue 231 | 232 | if alg and _key.alg and _key.alg != alg: 233 | continue 234 | 235 | pkey.append(_key) 236 | 237 | return pkey 238 | 239 | def _pick_alg(self, keys): 240 | alg = None 241 | try: 242 | alg = self["alg"] 243 | except KeyError: 244 | # try to get alg from key if there is only one 245 | if keys is not None and len(keys) == 1: 246 | key = next(iter(keys)) # first element from either list or dict 247 | if key.alg: 248 | self["alg"] = alg = key.alg 249 | 250 | if not alg: 251 | self["alg"] = alg = "none" 252 | 253 | return alg 254 | 255 | def _decode(self, payload): 256 | _msg = b64d(as_bytes(payload)) 257 | if "cty" in self: 258 | if self["cty"] == "JWT": 259 | _msg = json.loads(as_unicode(_msg)) 260 | return _msg 261 | 262 | def dump_header(self): 263 | """Return all attributes with values.""" 264 | return {x: self._dict[x] for x in self.args if x in self._dict} 265 | -------------------------------------------------------------------------------- /src/cryptojwt/serialize/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/JWTConnect-Python-CryptoJWT/2b735fefe72b66a54ee7d6233afe9e895e865d5b/src/cryptojwt/serialize/__init__.py -------------------------------------------------------------------------------- /src/cryptojwt/serialize/item.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from cryptojwt import key_issuer 4 | 5 | 6 | class KeyIssuer: 7 | @staticmethod 8 | def serialize(item: key_issuer.KeyIssuer) -> str: 9 | """Convert from KeyIssuer to JSON""" 10 | return json.dumps(item.dump(exclude_attributes=["keybundle_cls"])) 11 | 12 | def deserialize(self, spec: str) -> key_issuer.KeyIssuer: 13 | """Convert from JSON to KeyIssuer""" 14 | _dict = json.loads(spec) 15 | issuer = key_issuer.KeyIssuer().load(_dict) 16 | return issuer 17 | -------------------------------------------------------------------------------- /src/cryptojwt/simple_jwt.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | import logging 4 | 5 | from cryptojwt.exception import HeaderError 6 | 7 | from .utils import as_unicode, b64d, b64encode_item, split_token 8 | 9 | __author__ = "Roland Hedberg" 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class SimpleJWT: 15 | """ 16 | Basic JSON Web Token class that doesn't make any assumptions as to what 17 | can or should be in the payload 18 | """ 19 | 20 | def __init__(self, **headers): 21 | if not headers.get("alg"): 22 | headers["alg"] = None 23 | self.headers = headers 24 | self.b64part = [b64encode_item(headers)] 25 | self.part = [b64d(self.b64part[0])] 26 | 27 | def unpack(self, token, **kwargs): 28 | """ 29 | Unpacks a JWT into its parts and base64 decodes the parts 30 | individually 31 | 32 | :param token: The JWT 33 | :param kwargs: A possible empty set of claims to verify the header 34 | against. 35 | """ 36 | if isinstance(token, str): 37 | with contextlib.suppress(UnicodeDecodeError): 38 | token = token.encode("utf-8") 39 | 40 | part = split_token(token) 41 | self.b64part = part 42 | self.part = [b64d(p) for p in part] 43 | self.headers = json.loads(as_unicode(self.part[0])) 44 | for key, val in kwargs.items(): 45 | if not val and key in self.headers: 46 | continue 47 | 48 | try: 49 | _ok = self.verify_header(key, val) 50 | except KeyError: 51 | raise 52 | else: 53 | if not _ok: 54 | raise HeaderError(f'Expected "{key}" to be "{val}", was "{self.headers[key]}"') 55 | 56 | return self 57 | 58 | def pack(self, parts=None, headers=None): 59 | """ 60 | Packs components into a JWT 61 | 62 | :param parts: List of parts to pack 63 | :param headers: The JWT headers 64 | :return: 65 | """ 66 | if not headers: 67 | headers = self.headers if self.headers else {"alg": "none"} 68 | 69 | logging.debug(f"(pack) JWT header: {headers}") 70 | 71 | if not parts: 72 | return ".".join([a.decode() for a in self.b64part]) 73 | 74 | self.part = [headers] + parts 75 | _all = self.b64part = [b64encode_item(headers)] 76 | _all.extend([b64encode_item(p) for p in parts]) 77 | 78 | return ".".join([a.decode() for a in _all]) 79 | 80 | def payload(self): 81 | """ 82 | Picks out the payload from the different parts of the signed/encrypted 83 | JSON Web Token. If the content type is said to be 'jwt' deserialize the 84 | payload into a Python object otherwise return as-is. 85 | 86 | :return: The payload 87 | """ 88 | _msg = as_unicode(self.part[1]) 89 | 90 | # If not JSON web token assume JSON 91 | if "cty" in self.headers and self.headers["cty"].lower() != "jwt": 92 | pass 93 | else: 94 | with contextlib.suppress(ValueError): 95 | _msg = json.loads(_msg) 96 | 97 | return _msg 98 | 99 | def verify_header(self, key, val): 100 | """ 101 | Check that a particular header claim is present and has a specific value 102 | 103 | :param key: The claim 104 | :param val: The value of the claim 105 | :raises: KeyError if the claim is not present in the header 106 | :return: True if the claim exists in the header and has the prescribed 107 | value 108 | """ 109 | 110 | if isinstance(val, list): 111 | return self.headers[key] in val 112 | else: 113 | return self.headers[key] == val 114 | 115 | def verify_headers(self, check_presence=True, **kwargs): 116 | """ 117 | Check that a set of particular header claim are present and has 118 | specific values 119 | 120 | :param kwargs: The claim/value sets as a dictionary 121 | :return: True if the claim that appears in the header has the 122 | prescribed values. If a claim is not present in the header and 123 | check_presence is True then False is returned. 124 | """ 125 | for key, val in kwargs.items(): 126 | try: 127 | _ok = self.verify_header(key, val) 128 | except KeyError: 129 | if check_presence: 130 | return False 131 | else: 132 | pass 133 | else: 134 | if not _ok: 135 | return False 136 | return True 137 | -------------------------------------------------------------------------------- /src/cryptojwt/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/JWTConnect-Python-CryptoJWT/2b735fefe72b66a54ee7d6233afe9e895e865d5b/src/cryptojwt/tools/__init__.py -------------------------------------------------------------------------------- /src/cryptojwt/tools/jwtpeek.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Thanks to @rohe Roland Hedberg for most of the lines in this script :). 4 | import argparse 5 | import json 6 | import os 7 | import sys 8 | 9 | from pygments import highlight 10 | from pygments.formatters.terminal import TerminalFormatter 11 | from pygments.lexers.data import JsonLexer 12 | 13 | from cryptojwt.jwe import jwe 14 | from cryptojwt.jwk.hmac import SYMKey 15 | from cryptojwt.jwk.jwk import key_from_jwk_dict 16 | from cryptojwt.jwk.rsa import RSAKey, import_rsa_key 17 | from cryptojwt.jws import jws 18 | from cryptojwt.key_bundle import KeyBundle 19 | from cryptojwt.key_issuer import KeyIssuer 20 | 21 | __author__ = "roland" 22 | 23 | """ 24 | Tool to view, verify signature on and/or decrypt JSON Web Token. 25 | 26 | Usage examples: 27 | 28 | (1) read JWT from stdin, no keys 29 | 30 | cat idtoken | ./jwtpeek.py -f - 31 | 32 | or 33 | 34 | cat idtoken | ./jwtpeek.py 35 | 36 | (2) read JWT from file, use keys from file with a JWKS to verify/decrypt 37 | 38 | ./jwtpeek.py -f idtoken -J keys.jwks 39 | 40 | or 41 | 42 | (3) JWT from stdin, no keys 43 | 44 | echo json.web.token | ./jwtpeek.py 45 | 46 | """ 47 | 48 | 49 | def process(jwt, keys, quiet): 50 | _jw = jwe.factory(jwt) 51 | if _jw: 52 | if not quiet: 53 | print("Encrypted JSON Web Token") 54 | print(f"Headers: {_jw.jwt.headers}") 55 | if keys: 56 | res = _jw.decrypt(keys=keys) 57 | json_object = json.loads(res) 58 | json_str = json.dumps(json_object, indent=2) 59 | print(highlight(json_str, JsonLexer(), TerminalFormatter())) 60 | else: 61 | print("No keys can't decrypt") 62 | sys.exit(1) 63 | else: 64 | _jw = jws.factory(jwt) 65 | if _jw: 66 | if quiet: 67 | json_object = json.loads(_jw.jwt.part[1].decode("utf-8")) 68 | json_str = json.dumps(json_object, indent=2) 69 | print(highlight(json_str, JsonLexer(), TerminalFormatter())) 70 | else: 71 | print("Signed JSON Web Token") 72 | print(f"Headers: {_jw.jwt.headers}") 73 | if keys: 74 | res = _jw.verify_compact(keys=keys) 75 | print(f"Verified message: {res}") 76 | else: 77 | json_object = json.loads(_jw.jwt.part[1].decode("utf-8")) 78 | json_str = json.dumps(json_object, indent=2) 79 | print( 80 | f"Unverified message: {highlight(json_str, JsonLexer(), TerminalFormatter())}" 81 | ) 82 | 83 | 84 | def main(): 85 | parser = argparse.ArgumentParser() 86 | parser.add_argument("-r", dest="rsa_file", help="File containing a RSA key") 87 | parser.add_argument("-k", dest="hmac_key", help="If using a HMAC algorithm this is the key") 88 | parser.add_argument("-i", dest="kid", help="key id") 89 | parser.add_argument("-j", dest="jwk", help="JSON Web Key") 90 | parser.add_argument("-J", dest="jwks", help="JSON Web Keys") 91 | parser.add_argument("-u", dest="jwks_url", help="JSON Web Keys URL") 92 | parser.add_argument("-f", dest="msg", help="The message") 93 | parser.add_argument( 94 | "-q", 95 | dest="quiet", 96 | help="Quiet mode -- only show the RAW but prettified JSON", 97 | action="store_true", 98 | ) 99 | 100 | args = parser.parse_args() 101 | 102 | _kid = args.kid if args.kid else "" 103 | 104 | keys = [] 105 | if args.rsa_file: 106 | keys.append(RSAKey(key=import_rsa_key(args.rsa_file), kid=_kid)) 107 | if args.hmac_key: 108 | keys.append(SYMKey(key=args.hmac_key, kid=_kid)) 109 | 110 | if args.jwk: 111 | with open(args.jwk) as fp: 112 | _key = key_from_jwk_dict(fp.read()) 113 | keys.append(_key) 114 | 115 | if args.jwks: 116 | _iss = KeyIssuer() 117 | with open(args.jwks) as fp: 118 | _iss.import_jwks(fp.read()) 119 | keys.extend(_iss.all_keys()) 120 | 121 | if args.jwks_url: 122 | _kb = KeyBundle(source=args.jwks_url) 123 | keys.extend(_kb.get()) 124 | 125 | if not args.msg or args.msg == "-": # If nothing specified assume stdin 126 | message = sys.stdin.read() 127 | else: 128 | if os.path.isfile(args.msg): 129 | with open(args.msg) as fp: 130 | message = fp.read().strip("\n") 131 | else: 132 | message = args.msg 133 | 134 | message = message.strip() 135 | message = message.strip('"') 136 | process(message, keys, args.quiet) 137 | -------------------------------------------------------------------------------- /src/cryptojwt/tools/keyconv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Convert JWK from/to PEM and other formats""" 4 | 5 | import argparse 6 | import json 7 | from binascii import hexlify 8 | from getpass import getpass 9 | from typing import Optional 10 | 11 | from cryptography.hazmat.primitives import serialization 12 | 13 | from cryptojwt.jwk import JWK 14 | from cryptojwt.jwk.ec import ECKey, import_private_ec_key_from_file, import_public_ec_key_from_file 15 | from cryptojwt.jwk.hmac import SYMKey 16 | from cryptojwt.jwk.okp import ( 17 | OKPKey, 18 | import_private_okp_key_from_file, 19 | import_public_okp_key_from_file, 20 | ) 21 | from cryptojwt.jwk.rsa import ( 22 | RSAKey, 23 | import_private_rsa_key_from_file, 24 | import_public_rsa_key_from_file, 25 | ) 26 | from cryptojwt.jwx import key_from_jwk_dict 27 | 28 | 29 | def jwk_from_file(filename: str, private: bool = True) -> JWK: 30 | """Read JWK from file""" 31 | with open(filename) as input_file: 32 | jwk_dict = json.loads(input_file.read()) 33 | return key_from_jwk_dict(jwk_dict, private=private) 34 | 35 | 36 | def pem2rsa( 37 | filename: str, 38 | kid: Optional[str] = None, 39 | private: bool = False, 40 | passphrase: Optional[str] = None, 41 | ) -> JWK: 42 | """Convert RSA key from PEM to JWK""" 43 | if private: 44 | key = import_private_rsa_key_from_file(filename, passphrase) 45 | else: 46 | key = import_public_rsa_key_from_file(filename) 47 | jwk = RSAKey(kid=kid) 48 | jwk.load_key(key) 49 | return jwk 50 | 51 | 52 | def pem2ec( 53 | filename: str, 54 | kid: Optional[str] = None, 55 | private: bool = False, 56 | passphrase: Optional[str] = None, 57 | ) -> JWK: 58 | """Convert EC key from PEM to JWK""" 59 | if private: 60 | key = import_private_ec_key_from_file(filename, passphrase) 61 | else: 62 | key = import_public_ec_key_from_file(filename) 63 | jwk = ECKey(kid=kid) 64 | jwk.load_key(key) 65 | return jwk 66 | 67 | 68 | def pem2okp( 69 | filename: str, 70 | kid: Optional[str] = None, 71 | private: bool = False, 72 | passphrase: Optional[str] = None, 73 | ) -> JWK: 74 | """Convert OKP key from PEM to JWK""" 75 | if private: 76 | key = import_private_okp_key_from_file(filename, passphrase) 77 | else: 78 | key = import_public_okp_key_from_file(filename) 79 | jwk = OKPKey(kid=kid) 80 | jwk.load_key(key) 81 | return jwk 82 | 83 | 84 | def bin2jwk(filename: str, kid: str) -> JWK: 85 | """Read raw key from filename and return JWK""" 86 | with open(filename, "rb") as file: 87 | content = file.read() 88 | return SYMKey(kid=kid, key=content) 89 | 90 | 91 | def pem2jwk( 92 | filename: str, 93 | kid: Optional[str] = None, 94 | kty: Optional[str] = None, 95 | private: bool = False, 96 | passphrase: Optional[str] = None, 97 | ) -> JWK: 98 | """Read PEM from filename and return JWK""" 99 | with open(filename) as file: 100 | content = file.readlines() 101 | header = content[0] 102 | 103 | if private: 104 | if passphrase is None: 105 | passphrase = getpass("Private key passphrase: ") 106 | if len(passphrase) == 0: 107 | passphrase = None 108 | else: 109 | passphrase = None 110 | 111 | if "BEGIN PUBLIC KEY" in header: 112 | if kty is not None and kty == "EC": 113 | jwk = pem2ec(filename, kid, private=False) 114 | elif kty is not None and kty == "RSA": 115 | jwk = pem2rsa(filename, kid, private=False) 116 | elif kty is not None and kty == "OKP": 117 | jwk = pem2okp(filename, kid, private=False) 118 | else: 119 | raise ValueError("Unknown key type") 120 | elif "BEGIN PRIVATE KEY" in header: 121 | if kty is not None and kty == "EC": 122 | jwk = pem2ec(filename, kid, private=True, passphrase=passphrase) 123 | elif kty is not None and kty == "RSA": 124 | jwk = pem2rsa(filename, kid, private=True, passphrase=passphrase) 125 | elif kty is not None and kty == "OKP": 126 | jwk = pem2okp(filename, kid, private=True, passphrase=passphrase) 127 | else: 128 | raise ValueError("Unknown key type") 129 | elif "BEGIN EC PRIVATE KEY" in header: 130 | jwk = pem2ec(filename, kid, private=True, passphrase=passphrase) 131 | elif "BEGIN EC PUBLIC KEY" in header: 132 | jwk = pem2ec(filename, kid, private=False) 133 | elif "BEGIN RSA PRIVATE KEY" in header: 134 | jwk = pem2rsa(filename, kid, private=True, passphrase=passphrase) 135 | elif "BEGIN RSA PUBLIC KEY" in header: 136 | jwk = pem2rsa(filename, kid, private=False) 137 | else: 138 | raise ValueError("Unknown PEM format") 139 | 140 | return jwk 141 | 142 | 143 | def export_jwk( 144 | jwk: JWK, 145 | private: bool = False, 146 | encrypt: bool = False, 147 | passphrase: Optional[str] = None, 148 | ) -> bytes: 149 | """Export JWK as PEM/bin""" 150 | 151 | if jwk.kty == "oct": # jwk is in fact a SYMKey 152 | return jwk.key 153 | 154 | # All other key types have private and public keys 155 | 156 | if private: 157 | if encrypt: 158 | if passphrase is None: 159 | passphrase = getpass("Private key passphrase: ") 160 | else: 161 | passphrase = None 162 | if passphrase: 163 | enc = serialization.BestAvailableEncryption(passphrase.encode()) 164 | else: 165 | enc = serialization.NoEncryption() 166 | serialized = jwk.priv_key.private_bytes( 167 | encoding=serialization.Encoding.PEM, 168 | format=serialization.PrivateFormat.PKCS8, 169 | encryption_algorithm=enc, 170 | ) 171 | else: 172 | serialized = jwk.pub_key.public_bytes( 173 | encoding=serialization.Encoding.PEM, 174 | format=serialization.PublicFormat.SubjectPublicKeyInfo, 175 | ) 176 | 177 | return serialized 178 | 179 | 180 | def output_jwk(jwk: JWK, private: bool = False, filename: Optional[str] = None) -> None: 181 | """Output JWK to file""" 182 | serialized = jwk.serialize(private=private) 183 | if filename is not None: 184 | with open(filename, mode="w") as file: 185 | file.write(json.dumps(serialized)) 186 | else: 187 | print(json.dumps(serialized, indent=4)) 188 | 189 | 190 | def output_bytes(data: bytes, binary: bool = False, filename: Optional[str] = None) -> None: 191 | """Output data to file""" 192 | if filename is not None: 193 | with open(filename, mode="wb") as file: 194 | file.write(data) 195 | else: 196 | if binary: 197 | print(hexlify(data).decode()) 198 | else: 199 | print(data.decode()) 200 | 201 | 202 | def main(): 203 | """Main function""" 204 | parser = argparse.ArgumentParser(description="JWK Conversion Utility") 205 | 206 | parser.add_argument("--kid", dest="kid", metavar="key_id", help="Key ID") 207 | parser.add_argument("--kty", dest="kty", metavar="type", help="Key type") 208 | parser.add_argument("--private", dest="private", action="store_true", help="Output private key") 209 | parser.add_argument( 210 | "--encrypt", dest="encrypt", action="store_true", help="Encrypt private key" 211 | ) 212 | parser.add_argument("--output", dest="output", metavar="filename", help="Output file name") 213 | parser.add_argument("filename", metavar="filename", nargs=1, help="filename") 214 | args = parser.parse_args() 215 | 216 | f = args.filename[0] 217 | 218 | if f.endswith(".json"): 219 | jwk = jwk_from_file(f, args.private) 220 | serialized = export_jwk(jwk, private=args.private, encrypt=args.encrypt) 221 | output_bytes(data=serialized, binary=(jwk.kty == "oct"), filename=args.output) 222 | elif f.endswith(".bin"): 223 | jwk = bin2jwk(filename=f, kid=args.kid) 224 | output_jwk(jwk=jwk, private=True, filename=args.output) 225 | elif f.endswith(".pem"): 226 | jwk = pem2jwk(filename=f, kid=args.kid, private=args.private, kty=args.kty) 227 | output_jwk(jwk=jwk, private=args.private, filename=args.output) 228 | else: 229 | exit(-1) 230 | 231 | 232 | if __name__ == "__main__": 233 | main() 234 | -------------------------------------------------------------------------------- /src/cryptojwt/tools/keygen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """JSON Web Key (JWK) Generator""" 4 | 5 | import argparse 6 | import json 7 | import sys 8 | 9 | from cryptojwt.jwk.ec import NIST2SEC, new_ec_key 10 | from cryptojwt.jwk.hmac import new_sym_key 11 | from cryptojwt.jwk.okp import OKP_CRV2PUBLIC, new_okp_key 12 | from cryptojwt.jwk.rsa import new_rsa_key 13 | 14 | DEFAULT_SYM_KEYSIZE = 32 15 | DEFAULT_RSA_KEYSIZE = 2048 16 | DEFAULT_RSA_EXP = 65537 17 | DEFAULT_EC_CURVE = "P-256" 18 | 19 | 20 | def main(): 21 | """Main function""" 22 | parser = argparse.ArgumentParser(description="JSON Web Key (JWK) Generator") 23 | 24 | parser.add_argument("--kty", dest="kty", metavar="type", help="Key type", required=True) 25 | parser.add_argument("--size", dest="keysize", type=int, metavar="size", help="Key size") 26 | parser.add_argument( 27 | "--crv", 28 | dest="crv", 29 | metavar="curve", 30 | help="EC curve", 31 | choices=list(NIST2SEC.keys()) + list(OKP_CRV2PUBLIC.keys()), 32 | default=DEFAULT_EC_CURVE, 33 | ) 34 | parser.add_argument( 35 | "--exp", 36 | dest="rsa_exp", 37 | type=int, 38 | metavar="exponent", 39 | help=f"RSA public key exponent (default {DEFAULT_RSA_EXP})", 40 | default=DEFAULT_RSA_EXP, 41 | ) 42 | parser.add_argument("--kid", dest="kid", metavar="id", help="Key ID") 43 | args = parser.parse_args() 44 | 45 | if args.kty.upper() == "RSA": 46 | if args.keysize is None: 47 | args.keysize = DEFAULT_RSA_KEYSIZE 48 | jwk = new_rsa_key(public_exponent=args.rsa_exp, key_size=args.keysize, kid=args.kid) 49 | elif args.kty.upper() == "EC": 50 | if args.crv not in NIST2SEC: 51 | print(f"Unknown curve: {args.crv}", file=sys.stderr) 52 | exit(1) 53 | jwk = new_ec_key(crv=args.crv, kid=args.kid) 54 | elif args.kty.upper() == "OKP": 55 | if args.crv not in OKP_CRV2PUBLIC: 56 | print(f"Unknown curve: {args.crv}", file=sys.stderr) 57 | exit(1) 58 | jwk = new_okp_key(crv=args.crv, kid=args.kid) 59 | elif args.kty.upper() == "SYM" or args.kty.upper() == "OCT": 60 | if args.keysize is None: 61 | args.keysize = DEFAULT_SYM_KEYSIZE 62 | jwk = new_sym_key(bytes=args.keysize, kid=args.kid) 63 | else: 64 | print(f"Unknown key type: {args.kty}", file=sys.stderr) 65 | exit(1) 66 | 67 | jwk_dict = jwk.serialize(private=True) 68 | print(json.dumps(jwk_dict, sort_keys=True, indent=4)) 69 | print("SHA-256: " + jwk.thumbprint("SHA-256").decode(), file=sys.stderr) 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /tests/570-ec-sect571r1-keypair.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIHuAgEBBEgBH3PwaVkfMDZwbwQB0FlKTYFTObjkNxRo++SRg8XOIpNCJk118Z26 3 | pjVLS9UadaisLopUMsZ+f/pQITUhbPl//0rRBfM2KtigBwYFK4EEACehgZUDgZIA 4 | BAIZ0Rc0Y3jsqPqqptRz3tiSAuvTHA9vUigM2gUjM6YkTKofP7RRls4dqt6aM7/1 5 | eLbFg4Jdh9DXS4zU1EFeiZQZ+drSQYAmAgAtTzpmtmUoy+miwtiSBomu3CSUe6Yr 6 | VvWb+Oirmvw2x3BCTJW2Xjhy5y6tDPVRRyhg0nh5wm/UxZv4jo7AZuJV8ztZKwCE 7 | AA== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /tests/cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/JWTConnect-Python-CryptoJWT/2b735fefe72b66a54ee7d6233afe9e895e865d5b/tests/cert.der -------------------------------------------------------------------------------- /tests/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB2jCCAUOgAwIBAgIBATANBgkqhkiG9w0BAQUFADA0MRgwFgYDVQQDEw9UaGUg 3 | Y29kZSB0ZXN0ZXIxGDAWBgNVBAoTD1VtZWEgVW5pdmVyc2l0eTAeFw0xMjEwMDQw 4 | MDIzMDNaFw0xMzEwMDQwMDIzMDNaMDIxCzAJBgNVBAYTAlNFMSMwIQYDVQQDExpP 5 | cGVuSUQgQ29ubmVjdCBUZXN0IFNlcnZlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw 6 | gYkCgYEAwf+wiusGhA+gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVze 7 | q7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB/87ds3dy3Rfym/GUSc5B0l1TgEobcyae 8 | p8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8CAwEAATANBgkq 9 | hkiG9w0BAQUFAAOBgQCsTntG4dfW5kO/Qle6uBhIhZU+3IreIPmbwzpXoCbcgjRa 10 | 01z6WiBLwDC1RLAL7ucaF/EVlUq4e0cNXKt4ESGNc1xHISOMLetwvS1SN5tKWA9H 11 | Nua/SaqRtiShxLUjPjmrtpUgotLNDRvUYnTdTT1vhZar7TSPr1yObirjvz/qLw== 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /tests/ec-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBNVkO23bySJFBEg/YXNxlL9fkBrf 3 | gHCr7s8X4z0aqsy7b5VOZ1CjjNq4CwjFT9IYruMLYERClGC8mx9VD8BLrw== 4 | -----END PUBLIC KEY----- 5 | 6 | -------------------------------------------------------------------------------- /tests/ec.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEAb97f3Sw5anaTuSuP/QrP27wtN4tkewd 3 | \nbFJelq3pCBB0xpe1SjU5waxfnyrlMKG1oj0gm3sIhDGfyFFM2n65KIPMPl+q9V 4 | VS\nWvb9mSCrRZ7FDRrTTYAC5PsrrtYoIATf 5 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /tests/ed25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEIFp/m1fdvi+8lyL11WjusBF566clBk556Rpx/ZLtOyE3 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/ed448.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MEcCAQAwBQYDK2VxBDsEOXswwlU/yneCGw8vZZLRGYxk71AFyv8W4+rZcXpVV9i2 3 | 8w6Cvd8wk1S9itC4VSqrnuEFpfHVaY47wA== 4 | -----END PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /tests/invalid_ecdh.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cryptojwt.jwe import JWE_EC, factory 4 | from cryptojwt.jwk import ECKey 5 | 6 | JWK = { 7 | "kty": "EC", 8 | "kid": "3f7b122d-e9d2-4ff7-bdeb-a1487063d799", 9 | "crv": "P-256", 10 | "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", 11 | "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", 12 | "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw", 13 | } 14 | 15 | ALG = "ECDH-ES+A128KW" 16 | ENC = "A128CBC-HS256" 17 | PLAINTEXT = "Gambling is illegal at Bushwood sir, and I never slice." 18 | 19 | maliciousJWE = ".".join( 20 | [ 21 | "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhDQkMtSFMyNTYiLCJlcGsiOnsia3R5IjoiRUMiLCJ4IjoiZ1RsaTY1ZVRRN3otQmgxNDdmZjhLM203azJVaURpRzJMcFlrV0FhRkpDYyIsInkiOiJjTEFuakthNGJ6akQ3REpWUHdhOUVQclJ6TUc3ck9OZ3NpVUQta2YzMEZzIiwiY3J2IjoiUC0yNTYifX0", 22 | "qGAdxtEnrV_3zbIxU2ZKrMWcejNltjA_dtefBFnRh9A2z9cNIqYRWg", 23 | "pEA5kX304PMCOmFSKX_cEg", 24 | "a9fwUrx2JXi1OnWEMOmZhXd94-bEGCH9xxRwqcGuG2AMo-AwHoljdsH5C_kcTqlXS5p51OB1tvgQcMwB5rpTxg", 25 | "72CHiYFecyDvuUa43KKT6w", 26 | ] 27 | ) 28 | 29 | 30 | def test(): 31 | key = ECKey(**JWK) 32 | 33 | ret_jwe = factory(maliciousJWE) 34 | jwdec = JWE_EC() 35 | with pytest.raises(ValueError): 36 | jwdec.dec_setup(ret_jwe.jwt, key=key.keys()) 37 | -------------------------------------------------------------------------------- /tests/jwk_private_ec_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "crv": "P-256", 4 | "alg": "ES256", 5 | "x": "Ijdv9ZAD7QaEYnEqY5als5NFbNP_LsyZgJMTcQhNsYo", 6 | "y": "Hpc9vdz77lQG0NJf5FZAaeOJTR-bMjIw9kkCE1duxKE", 7 | "d": "omnOYCv3UxQRIogRcd2VIHhaj46FTZU8GfdYS4qX2_w" 8 | } -------------------------------------------------------------------------------- /tests/jwk_private_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "alg": "RS256", 3 | "d": "vT9bnSZ63uIdaVsmZjrbmcvrDZG-_qzVQ1KmrSSC398sLJiyaQKRPkmBRvV-MGxW1MVPeCkhnSULCRgtqHq-zQxMeCviSScHTKOuDYJfwMB5qdOE3FkuqPMsEVf6EXYaSd90-O6GOA88LBCPNR4iKxsrQ6LNkawwiJoPw7muK3TbQk9HzuznF8WDkt72CQFxd4eT6wJ97xpaIgxZce0oRmFcLYkQ4A0pgVhF42zxJjJDIBj_ZrSl5_qZIgiE76PV4hjHt9Nv4ZveabObnNbyz9YOiWHiOLdYZGmixHuauM98NK8udMxI6IuOkRypFhJzaQZFwMroa7ZNZF-mm78VYQ", 4 | "dp": "wLqivLfMc0FBhGFFRTb6WWzDpVZukcgOEQGb8wW3knmNEpgch699WQ4ZY_ws1xSbvQZtbx7MaIBXpn3qT1LYZosoP5oHVTAvdg6G8I7zgWyqj-nG4evciuoeAa1Ff52h4-J1moZ6FF2GelLdjXHoCbjIBjz_VljelSqOk5Sh5HU", 5 | "dq": "KXIUYNfDxwxv3A_w1t9Ohm92gOs-UJdI3_IVpe4FauCDrJ4mqgsnTisA15KY-9fCEvKfqG571WK6EKpBcxaRrqSU0ekpBvgJx8o3MGlqXWj-Lw0co8N9_-fo1rYx_8g-wCRrm5zeA5pYJdwdhOBnmKOqw_GsXJEcYeUod1xkcfU", 6 | "e": "AQAB", 7 | "ext": "true", 8 | "key_ops": "sign", 9 | "kty": "RSA", 10 | "n": "wl0DPln-EFLqr_Ftn6A87wEQAUVbpZsUTN2OCEsJV0nhlvmX3GUzyZx5UXdlM3Dz68PfUWCgfx67Il6sURqWVCnjnU-_gr3GeDyzedj-lZejnBx-lEy_3j6B98SbcDfkJF6saXnPd7_kgilJT1_g-EVI9ifFB1cxZXHCd2WBeRABSCprAlCglF-YmnUeeDs5K32z2ckVjadF9BG27CO5UfNq0K8jI9Yj_coOhM9dRNrQ9UVZNdQVG-bAIDhB2y2o3ASGwqchHouIxv5YZNGS0SMJL5t0edh483q1tSWPqBw-ZeryLztOedBBzSuJk7QDmL1B6B7KKUIrlUYJmVsYzw", 11 | "p": "6MEg5Di_IFiPGKvMFRjyx2t7YAOQ4KfdIkU_Khny1t1eCG5O07omPe_jLU8I5fPaD5F5HhWExLNureHD4K6LB18JPE3VE8chQROiRSNPZo1-faUvHu-Dy0pr7I-TS8pl_P3vop1KelIbGwXhzPIRKQMqCEKi3tLJt4R_MQ18Dx0", 12 | "q": "1cZVPpUbf4p5n4cMv_kERCPh3cieMs4aVojgh3feAiJiLwWWL9Pc43oJUekK44aWMnbs68Y4kqXtc52PMtBDzVp0Gjt0lCY3M7MYRVI4JhtknqvQynMKQ2nKs3VldvVfY2SxyUmnRyEolQUGRA7rRMUyPb4AXhSR7oroRrJD59s", 13 | "qi": "50PhyaqbLSczhipWiYy149sLsGlx9cX0tnGMswy1JLam7nBvH4-MWB2oGwD2hmG-YN66q-xXBS9CVDLZZrj1sonRTQPtWE-zuZqds6_NVlk2Ge4_IAA3TZ9tvIfM5FZVTOQsExu3_LX8FGCspWC1R-zDqT45Y9bpaCwxekluO7Q" 14 | } -------------------------------------------------------------------------------- /tests/rsa-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfDFz3zYHVj1aHxDr0W4bpsoIv 3 | rbVBGf2Pk7PwiKAep3nko0CWds4MUIRlioCiigE6KSj2eExStNAFDf3jcjOfYpEV 4 | kaUsyGmwclKXxAPNRMr6GAJeKlbJqTs5eYd5tpBjdeW2rjkD/W6R77zp+EtuU4jY 5 | /zR2YWeqB6GdTnduKwIDAQAB 6 | -----END PUBLIC KEY----- 7 | 8 | -------------------------------------------------------------------------------- /tests/rsa.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQD6vqn19W/VB215DBADRakfPmCtFBf8/+YyhGqixWIwDiEl/L6L 3 | w5HKZCUPVgrC0ADhJfvAbn4fte5MWBCTkqgepKL3BySMA0LMaBF12pbHlPSUbmQG 4 | BJmTX4NNXuUel6TbPYJAU2Nh5Nan0Mb7Bmb8QpFvS0Hw7qZRW8y2eIttfwIDAQAB 5 | AoGBAJVf9FxkRKUB8cOE3h006JWGUY2KROghgn9hxy0ErYO3RyQcN1+HuFh75GAI 6 | gAyiYYO/XwS6TkSR2057wBRJ8ABzcL3+v5g+16Vbh0BjXVE+cv1WGdNGujyzl6ji 7 | jlyF4cb6tXDyqWTLkMAtV20NfO/CGsfii6YEkZb2P90usthRAkEA/oG7a9EvQ7eR 8 | gSEqppzW7KCwidPjnZTr/ROIZQU33nwkIJ0ElTjMNYKP8DerSuixR9skw2ZY8Q8I 9 | 1PTBnocHwwJBAPw3SAQYwxZwQMu1trVPMNOGIbSY4rQlMZGXrCZSu/TnozczFLA8 10 | qNM84g5veyJOzHKmYkIsMG1gwg5VNniG45UCQF6SlLOW0upl70K9sVyiUVcyywcc 11 | Xqty6FJtjLSFQOKC3OXlkwtkRLXpo1UPSq6WUzIxY7LceFZzUMPZg41F/gMCQHNr 12 | POqbBlPzZMOUUZthNP/nhu8lc8Fqr+dnmGElRVxK0JdHKfWInN2mI/DlNV064Dar 13 | S5XqsPKs78EtX7MCT40CQFQZiry8m7ROubOU4+HDG9o1w9zcKXCkmbD9hBCGvTAj 14 | BQNuGE0DtC6FEWTs8bXybLM5yBRq1XiKLdmi5N+3n4g= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /tests/rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD6vqn19W/VB215DBADRakfPmCt 3 | FBf8/+YyhGqixWIwDiEl/L6Lw5HKZCUPVgrC0ADhJfvAbn4fte5MWBCTkqgepKL3 4 | BySMA0LMaBF12pbHlPSUbmQGBJmTX4NNXuUel6TbPYJAU2Nh5Nan0Mb7Bmb8QpFv 5 | S0Hw7qZRW8y2eIttfwIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /tests/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDnNs1schgiQYYndEZ1EqRiYXiA6iu/l4O5QpNW267dW/Guk5a/ 3 | CcVX41jQuCqRBIwzJXOh0kTdJS7BBNQc9ddjnvzi+eQWrM9HHpseA/ErPtd/BCMQ 4 | FNp9BMyY55gjzAUg4gdjmLRFZH6DENuH8PlNe5SpJw46pHju9KkhtQf6dwIDAQAB 5 | AoGAKuSxy1KHQ6OgPaWGhKWGtXGbp17J6usy1qWRK+XpVMt/1IEw0BQB9kII8f+Y 6 | dfq//6UNBJI7kEMbn1dD+nNpF4ncO9QWHE5oqacHgaZOl6+MF3ePy8aXkADhwiel 7 | L7CtZjhwbcjGt5PI6AIcpFfmBAbu5Pf4gidr6bR+MoJGlhECQQDzfMaRqruJkqsz 8 | Z5b9boIr08orx1xPoHTmE5g0ET9+UJy/BBgx7DNv+AQhJ2UC1ZaKcgqwetOwJhQs 9 | u8Cbrct9AkEA8xiQSwqlM7ltpNl6L2VvSxzTd897it+FJElXbD6u80RvzMuo3Xw3 10 | +M+F0kDobM4vsyBuZRw418/yOpnOv8x4AwJATj5WgRDgWwEqysYLGz2bzwGsAg16 11 | eIwThKvfSTwRr0GwXSGvtLs2fFCy4wSJzTNdwPeMv9F4nS5fZVCgQGbE8QJAMZBG 12 | iyZGfH9H/Z5hrRwvTs83xmvFMpFUIgvaCTXWkb7YVJcJfO8AsngNPssBGH4Jd6ob 13 | F/5jEI1TQ+NsJerYZQJBAJdqDlnPQyqek4kdBvwh2hYo9EwOrgOchmruMOeP5lE6 14 | 2TLIyjYC3uVMPJj+ESayVcAMrgj4Enk4qh/WKVeMJ7c= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /tests/size2048.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAnN08w4Vq6+yXa115JzPI3bVuLTtZfP4bZ+TGJ35JEVN6bLwK 3 | TTUu6PIEHVm1kA6MJIIUKfvxlXIvMUe1dWtrpMo1+HAm4jrNj4SyreAlgBa6vtjZ 4 | nESwDZS02mCJ/KAz2xuRbzzr95rqGiN6QmVsxsDAyRkte/fxbMwsvO+50ZwClKwL 5 | TV1LkoHaqYvW5SjpIX/RLpfvfobW6oc67Pwp1uA7+jvG7QhLbGdmCLZFNcqefvi+ 6 | hBmWybIP4y7fk7GhWCAPAuPm01nBpvZAUvfom/35gZbUAGiy5uy+urMJYtfbqXUN 7 | TetKVVwLMiRaLPJ4kUd+EIRnb8Vo6YRLe821twIDAQABAoIBAB0PS0t5cvZj7SVB 8 | uskNad/Q1alhflGOjaswkZkNZyHjkiGEsG/fM2KKO0LotJ8MYt/8jRm+B+JEtgNu 9 | ImqvTNDJeTgeJsXwWNaGocdeZ/QTweLLL30oqGjLrLlr+wQm9ZRYxheSdLB5LXdM 10 | LUERoxYq7UwT8v4tT7d1F7CkxIrVDtEdhMgjYd2Qq3ClZkpMtst+DGmUWyLTJ5Sq 11 | XH5S+IJNwajQN4h3XXpKF1T4ZYrRfJXW35BJbV5baAykitwZHL4Rf3m75t+eoRiT 12 | 7DDHRgXeF4uVmRp8YRQWqUxMr/LZAECpxCQqPJvl2a8hDJg8mmRxHErnVanW2Yfe 13 | QifSo1kCgYEAxrr5+dtCbiZhf7wVmxHuvZkZwfefSsJdZP+D5EK8fvM3rMHLPdZE 14 | TNacG9em+v9o5UrNtXMy94akl83Wd0qGUaWwmA3WZzJYh1pboGE+NjM7g4yvhywL 15 | etH0NHflSrW5ODKR5c37nRzIGpMnUttRxYFjrakEld/G2mNyP1YJ0GMCgYEAyhGm 16 | Tffz5hGNc6zmtJwROEkEyy3uMOh6R07P8xsGkkFrn3cmlMGEzVuLMph4GHIoouNr 17 | +HJ8MHub31wIZeklY77e7sK2p8POdav+qN6ne7BtVF/IhZGubcbq47pWkFxtHe+/ 18 | 3HUnTh9JXnozHnGW3D3C8n/XBD+gXBJmQ0N1Q50CgYEAnc5RvBdhG+i8WVs4aOgH 19 | hWTysDT6t7m5wf6oc/SOi9yEpFlLE7J2j/GHCxDm1r9EQ3dv/BsbKLUxNB5OpHF4 20 | UZNZtJSgYVf4NpsI0UUGyrFKG/72T0cpYoEgAx4F5JqDFt4JPsufdrYpK/kYK9Je 21 | j5BcX8UsD/sfg8GeMi8XIfMCgYEAojrZUe+oGSYucph2ALrr+ExnVNhjS1DaGzXj 22 | LkKcW/+58CUxDGzxug2tC8ntYXMFvQDpRmZj3mjfE8xH0coFArfTM82P59EEiOS1 23 | Z86amGtk20CrNNFpQSlam8qWhQAvesFpQA2uqMHlnbxuHhsEC35qbVbFwdtzW3hK 24 | MeaO1uECgYBJgvilqYno7Stxu0nbK5GoBYqMSnJiLuJ83+6aTcFw7IryZFdaff24 25 | iKfRJ0mvbJoklJuBGDjoRYE6AT20nGZTc+Wbx2cMR3HNGqlPKRbUjViwo8xkR7fx 26 | mBmyncZCL2HldD8gPo5SvLEKcIwYg8foXCM/tNLcmYgdf6amY+MyXQ== 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /tests/test_01_simplejwt.py: -------------------------------------------------------------------------------- 1 | from cryptojwt.simple_jwt import SimpleJWT 2 | 3 | __author__ = "roland" 4 | 5 | 6 | def _eq(l1, l2): 7 | return set(l1) == set(l2) 8 | 9 | 10 | def test_pack_jwt(): 11 | _jwt = SimpleJWT(**{"alg": "none", "cty": "jwt"}) 12 | jwt = _jwt.pack( 13 | parts=[ 14 | {"iss": "joe", "exp": 1300819380, "http://example.com/is_root": True}, 15 | "", 16 | ] 17 | ) 18 | 19 | p = jwt.split(".") 20 | assert len(p) == 3 21 | 22 | 23 | def test_unpack_pack(): 24 | _jwt = SimpleJWT(**{"alg": "none"}) 25 | payload = {"iss": "joe", "exp": 1300819380, "http://example.com/is_root": True} 26 | jwt = _jwt.pack(parts=[payload, ""]) 27 | repacked = SimpleJWT().unpack(jwt).pack() 28 | 29 | assert jwt == repacked 30 | 31 | 32 | def test_pack_unpack(): 33 | _jwt = SimpleJWT(**{"alg": "none"}) 34 | payload = {"iss": "joe", "exp": 1300819380, "http://example.com/is_root": True} 35 | jwt = _jwt.pack(parts=[payload, ""]) 36 | 37 | _jwt2 = SimpleJWT().unpack(jwt) 38 | 39 | assert _jwt2 40 | out_payload = _jwt2.payload() 41 | assert _eq(out_payload.keys(), ["iss", "exp", "http://example.com/is_root"]) 42 | assert out_payload["iss"] == payload["iss"] 43 | assert out_payload["exp"] == payload["exp"] 44 | assert out_payload["http://example.com/is_root"] == payload["http://example.com/is_root"] 45 | 46 | 47 | def test_pack_with_headers(): 48 | _jwt = SimpleJWT() 49 | jwt = _jwt.pack(parts=["", ""], headers={"foo": "bar"}) 50 | assert SimpleJWT().unpack(jwt).headers["foo"] == "bar" 51 | 52 | 53 | def test_unpack_str(): 54 | _jwt = SimpleJWT(**{"alg": "none"}) 55 | payload = {"iss": "joe", "exp": 1300819380, "http://example.com/is_root": True} 56 | jwt = _jwt.pack(parts=[payload, ""]) 57 | 58 | _jwt2 = SimpleJWT().unpack(jwt) 59 | assert _jwt2 60 | _ = _jwt2.payload() 61 | -------------------------------------------------------------------------------- /tests/test_05_jwx.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import pytest 5 | 6 | from cryptojwt.jwk.rsa import RSAKey 7 | from cryptojwt.jwx import JWx 8 | 9 | BASEDIR = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | 12 | def full_path(local_file): 13 | return os.path.join(BASEDIR, local_file) 14 | 15 | 16 | JSON_RSA_PUB_KEY = r""" 17 | { 18 | "kty":"RSA", 19 | "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", 20 | "e":"AQAB", 21 | "alg":"RS256" 22 | }""" 23 | 24 | 25 | def test_jwx_set_jwk(): 26 | jwx = JWx(jwk=JSON_RSA_PUB_KEY) 27 | keys = jwx._get_keys() 28 | assert len(keys) 29 | assert isinstance(keys[0], RSAKey) 30 | 31 | 32 | def test_jwx_set_json_jwk(): 33 | jwx = JWx(jwk=json.loads(JSON_RSA_PUB_KEY)) 34 | keys = jwx._get_keys() 35 | assert len(keys) 36 | assert isinstance(keys[0], RSAKey) 37 | 38 | 39 | def test_jwx_set_jwk_error(): 40 | with pytest.raises(ValueError): 41 | JWx(jwk=[RSAKey()]) 42 | 43 | 44 | @pytest.mark.network 45 | def test_jws_set_jku(): 46 | jwx = JWx(jku="https://login.salesforce.com/id/keys") 47 | keys = jwx._get_keys() 48 | # I know there will be keys, how many and what type may change 49 | assert len(keys) 50 | 51 | 52 | def test_jwx_set_x5c(): 53 | with open(full_path("cert.pem")) as fp: 54 | jwx = JWx(x5c=fp.read()) 55 | keys = jwx._get_keys() 56 | assert len(keys) 57 | assert isinstance(keys[0], RSAKey) 58 | 59 | 60 | def test_jwx_get_set(): 61 | jwx = JWx() 62 | if "alg" not in jwx: 63 | jwx["alg"] = "RS256" 64 | 65 | assert jwx["alg"] == "RS256" 66 | assert jwx.alg == "RS256" 67 | assert list(jwx.keys()) == ["alg"] 68 | 69 | 70 | def test_jwx_get_non_existent_attribute(): 71 | jwx = JWx() 72 | with pytest.raises(AttributeError): 73 | _ = jwx.alg 74 | 75 | 76 | def test_get_headers(): 77 | jwx = JWx(jwk=JSON_RSA_PUB_KEY, alg="RS256") 78 | _headers = jwx.headers() 79 | assert set(_headers.keys()) == {"jwk", "alg"} 80 | 81 | _headers = jwx.headers(kid="123") 82 | assert set(_headers.keys()) == {"jwk", "alg", "kid"} 83 | 84 | 85 | @pytest.mark.network 86 | def test_headers_jku(): 87 | jwx = JWx(jku="https://login.salesforce.com/id/keys") 88 | _headers = jwx.headers() 89 | assert set(_headers.keys()) == {"jku"} 90 | 91 | 92 | def test_decode(): 93 | jwx = JWx(cty="JWT") 94 | _msg = jwx._decode("eyJmb28iOiJiYXIifQ") 95 | assert _msg == {"foo": "bar"} 96 | 97 | 98 | def test_extra_headers(): 99 | jwx = JWx() 100 | headers = jwx.headers(jwk=JSON_RSA_PUB_KEY, alg="RS256") 101 | assert set(headers.keys()) == {"jwk", "alg"} 102 | -------------------------------------------------------------------------------- /tests/test_10_jwk_wrap.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cryptojwt.jwk.ec import new_ec_key 4 | from cryptojwt.jwk.hmac import SYMKey 5 | from cryptojwt.jwk.rsa import new_rsa_key 6 | from cryptojwt.jwk.wrap import unwrap_key, wrap_key 7 | 8 | __author__ = "jschlyter" 9 | 10 | WRAPPING_KEYS = [ 11 | SYMKey(use="enc", key=os.urandom(32)), 12 | new_ec_key(crv="P-256"), 13 | new_ec_key(crv="P-384"), 14 | new_rsa_key(size=2048), 15 | new_rsa_key(size=4096), 16 | ] 17 | 18 | SECRET_KEYS = [ 19 | SYMKey(use="enc", key=os.urandom(32)), 20 | new_ec_key(crv="P-256"), 21 | new_rsa_key(size=2048), 22 | ] 23 | 24 | 25 | def test_wrap_default(): 26 | for wrapping_key in WRAPPING_KEYS: 27 | for key in SECRET_KEYS: 28 | wrapped_key = wrap_key(key, wrapping_key) 29 | unwrapped_key = unwrap_key(wrapped_key, [wrapping_key]) 30 | assert key == unwrapped_key 31 | 32 | 33 | def test_wrap_params(): 34 | wrap_params = { 35 | "EC": {"alg": "ECDH-ES+A256KW", "enc": "A256GCM"}, 36 | "RSA": {"alg": "RSA1_5", "enc": "A256CBC-HS512"}, 37 | "oct": {"alg": "A256KW", "enc": "A256CBC-HS512"}, 38 | } 39 | for wrapping_key in WRAPPING_KEYS: 40 | for key in SECRET_KEYS: 41 | wrapped_key = wrap_key(key, wrapping_key, wrap_params) 42 | unwrapped_key = unwrap_key(wrapped_key, [wrapping_key]) 43 | assert key == unwrapped_key 44 | -------------------------------------------------------------------------------- /tests/test_30_tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from cryptojwt.jwk import JWK 5 | from cryptojwt.jwx import key_from_jwk_dict 6 | from cryptojwt.tools import keyconv as keyconv 7 | 8 | BASEDIR = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | 11 | def jwk_from_file(filename: str, private: bool = True) -> JWK: 12 | """Read JWK from file""" 13 | with open(filename) as input_file: 14 | jwk_dict = json.loads(input_file.read()) 15 | return key_from_jwk_dict(jwk_dict, private=private) 16 | 17 | 18 | def convert_rsa_pem2jwt(filename_jwk: str, filename_pem: str, private: bool): 19 | k1 = jwk_from_file(filename=filename_jwk, private=private) 20 | k2 = keyconv.pem2jwk( 21 | filename=filename_pem, kid=k1.kid, kty="RSA", private=private, passphrase="" 22 | ) 23 | if k1 != k2: 24 | raise Exception("Keys differ") 25 | 26 | 27 | def convert_ec_pem2jwt(filename_jwk: str, filename_pem: str, private: bool): 28 | k1 = jwk_from_file(filename=filename_jwk, private=private) 29 | k2 = keyconv.pem2jwk( 30 | filename=filename_pem, kid=k1.kid, kty="EC", private=private, passphrase="" 31 | ) 32 | if k1 != k2: 33 | raise Exception("Keys differ") 34 | 35 | 36 | def test_pem2rsa_public(): 37 | convert_rsa_pem2jwt( 38 | BASEDIR + "/test_keys/rsa-1024.json", 39 | BASEDIR + "/test_keys/rsa-1024-public.pem", 40 | private=False, 41 | ) 42 | convert_rsa_pem2jwt( 43 | BASEDIR + "/test_keys/rsa-1280.json", 44 | BASEDIR + "/test_keys/rsa-1280-public.pem", 45 | private=False, 46 | ) 47 | convert_rsa_pem2jwt( 48 | BASEDIR + "/test_keys/rsa-2048.json", 49 | BASEDIR + "/test_keys/rsa-2048-public.pem", 50 | private=False, 51 | ) 52 | convert_rsa_pem2jwt( 53 | BASEDIR + "/test_keys/rsa-3072.json", 54 | BASEDIR + "/test_keys/rsa-3072-public.pem", 55 | private=False, 56 | ) 57 | convert_rsa_pem2jwt( 58 | BASEDIR + "/test_keys/rsa-4096.json", 59 | BASEDIR + "/test_keys/rsa-4096-public.pem", 60 | private=False, 61 | ) 62 | 63 | 64 | def test_pem2rsa_private(): 65 | convert_rsa_pem2jwt( 66 | BASEDIR + "/test_keys/rsa-1024.json", 67 | BASEDIR + "/test_keys/rsa-1024-private.pem", 68 | private=True, 69 | ) 70 | convert_rsa_pem2jwt( 71 | BASEDIR + "/test_keys/rsa-1280.json", 72 | BASEDIR + "/test_keys/rsa-1280-private.pem", 73 | private=True, 74 | ) 75 | convert_rsa_pem2jwt( 76 | BASEDIR + "/test_keys/rsa-2048.json", 77 | BASEDIR + "/test_keys/rsa-2048-private.pem", 78 | private=True, 79 | ) 80 | convert_rsa_pem2jwt( 81 | BASEDIR + "/test_keys/rsa-3072.json", 82 | BASEDIR + "/test_keys/rsa-3072-private.pem", 83 | private=True, 84 | ) 85 | convert_rsa_pem2jwt( 86 | BASEDIR + "/test_keys/rsa-4096.json", 87 | BASEDIR + "/test_keys/rsa-4096-private.pem", 88 | private=True, 89 | ) 90 | 91 | 92 | def test_pem2ec_public(): 93 | convert_ec_pem2jwt( 94 | BASEDIR + "/test_keys/ec-p256.json", 95 | BASEDIR + "/test_keys/ec-p256-public.pem", 96 | private=False, 97 | ) 98 | convert_ec_pem2jwt( 99 | BASEDIR + "/test_keys/ec-p384.json", 100 | BASEDIR + "/test_keys/ec-p384-public.pem", 101 | private=False, 102 | ) 103 | 104 | 105 | def test_pem2ec_private(): 106 | convert_ec_pem2jwt( 107 | BASEDIR + "/test_keys/ec-p256.json", 108 | BASEDIR + "/test_keys/ec-p256-private.pem", 109 | private=True, 110 | ) 111 | convert_ec_pem2jwt( 112 | BASEDIR + "/test_keys/ec-p384.json", 113 | BASEDIR + "/test_keys/ec-p384-private.pem", 114 | private=True, 115 | ) 116 | -------------------------------------------------------------------------------- /tests/test_31_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cryptojwt.utils import check_content_type 4 | 5 | 6 | def test_check_content_type(): 7 | assert check_content_type(content_type="application/json", mime_type="application/json") is True 8 | assert ( 9 | check_content_type( 10 | content_type="application/json; charset=utf-8", mime_type="application/json" 11 | ) 12 | is True 13 | ) 14 | assert ( 15 | check_content_type( 16 | content_type="application/html; charset=utf-8", mime_type="application/json" 17 | ) 18 | is False 19 | ) 20 | assert ( 21 | check_content_type( 22 | content_type="application/jwk-set+json;charset=UTF-8", 23 | mime_type="application/application/jwk-set+json", 24 | ) 25 | is False 26 | ) 27 | assert ( 28 | check_content_type( 29 | content_type="application/jwk-set+json;charset=UTF-8", 30 | mime_type=set(["application/application/jwk-set+json", "application/json"]), 31 | ) 32 | is False 33 | ) 34 | with pytest.raises(ValueError): 35 | check_content_type(content_type="application/jwk-set+json;charset=UTF-8", mime_type=42) 36 | -------------------------------------------------------------------------------- /tests/test_40_serialize.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cryptojwt.key_bundle import keybundle_from_local_file 4 | from cryptojwt.key_issuer import KeyIssuer 5 | from cryptojwt.serialize import item 6 | 7 | 8 | def full_path(local_file): 9 | return os.path.join(BASEDIR, local_file) 10 | 11 | 12 | BASEDIR = os.path.abspath(os.path.dirname(__file__)) 13 | BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_keys")) 14 | CERT = full_path("cert.pem") 15 | 16 | 17 | def test_key_issuer(): 18 | kb = keybundle_from_local_file(f"file://{BASE_PATH}/jwk.json", "jwks", ["sig"]) 19 | assert len(kb) == 1 20 | issuer = KeyIssuer() 21 | issuer.add(kb) 22 | 23 | _item = item.KeyIssuer().serialize(issuer) 24 | _iss = item.KeyIssuer().deserialize(_item) 25 | 26 | assert len(_iss) == 1 # 1 key 27 | assert len(_iss.get("sig", "rsa")) == 1 # 1 RSA key 28 | _kb = _iss[0] 29 | assert kb.difference(_kb) == [] # no difference 30 | -------------------------------------------------------------------------------- /tests/test_50_argument_alias.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from cryptojwt.jws.jws import JWS, factory 6 | from cryptojwt.key_jar import build_keyjar, init_key_jar 7 | 8 | __author__ = "Roland Hedberg" 9 | 10 | BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_keys")) 11 | RSAKEY = os.path.join(BASE_PATH, "cert.key") 12 | RSA0 = os.path.join(BASE_PATH, "rsa.key") 13 | EC0 = os.path.join(BASE_PATH, "ec.key") 14 | BASEDIR = os.path.abspath(os.path.dirname(__file__)) 15 | 16 | 17 | def full_path(local_file): 18 | return os.path.join(BASEDIR, local_file) 19 | 20 | 21 | JWK1 = { 22 | "keys": [ 23 | { 24 | "n": "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" 25 | "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" 26 | "-NvS-aG_jN5cstVbCGWE20H0vF" 27 | "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" 28 | "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" 29 | "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", 30 | "e": "AQAB", 31 | "kty": "RSA", 32 | "kid": "rsa1", 33 | }, 34 | { 35 | "k": "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", 36 | "kty": "oct", 37 | }, 38 | ] 39 | } 40 | 41 | KEYDEFS = [ 42 | {"type": "RSA", "key": "", "use": ["sig"]}, 43 | {"type": "EC", "crv": "P-256", "use": ["sig"]}, 44 | ] 45 | 46 | 47 | class TestVerifyJWTKeys: 48 | @pytest.fixture(autouse=True) 49 | def setup(self): 50 | mkey = [ 51 | {"type": "RSA", "use": ["sig"]}, 52 | {"type": "RSA", "use": ["sig"]}, 53 | {"type": "RSA", "use": ["sig"]}, 54 | ] 55 | 56 | skey = [{"type": "RSA", "use": ["sig"]}] 57 | 58 | # Alice has multiple keys 59 | self.alice_keyjar = build_keyjar(mkey) 60 | # Bob has one single keys 61 | self.bob_keyjar = build_keyjar(skey) 62 | self.alice_keyjar["Alice"] = self.alice_keyjar[""] 63 | self.bob_keyjar["Bob"] = self.bob_keyjar[""] 64 | 65 | # To Alice's keyjar add Bob's public keys 66 | self.alice_keyjar.import_jwks(self.bob_keyjar.export_jwks(issuer_id="Bob"), "Bob") 67 | 68 | # To Bob's keyjar add Alice's public keys 69 | self.bob_keyjar.import_jwks(self.alice_keyjar.export_jwks(issuer_id="Alice"), "Alice") 70 | 71 | _jws = JWS('{"aud": "Bob", "iss": "Alice"}', alg="RS256") 72 | sig_key = self.alice_keyjar.get_signing_key("rsa", issuer_id="Alice")[0] 73 | self.sjwt_a = _jws.sign_compact([sig_key]) 74 | 75 | _jws = JWS('{"aud": "Alice", "iss": "Bob"}', alg="RS256") 76 | sig_key = self.bob_keyjar.get_signing_key("rsa", issuer_id="Bob")[0] 77 | self.sjwt_b = _jws.sign_compact([sig_key]) 78 | 79 | def test_no_kid_multiple_keys_no_kid_issuer(self): 80 | a_kids = [ 81 | k.kid for k in self.alice_keyjar.get_verify_key(issuer_id="Alice", key_type="RSA") 82 | ] 83 | no_kid_issuer = {"Alice": a_kids} 84 | _jwt = factory(self.sjwt_a) 85 | _jwt.jwt.headers["kid"] = "" 86 | keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) 87 | assert len(keys) == 3 88 | 89 | def test_aud(self): 90 | self.alice_keyjar.import_jwks(JWK1, issuer_id="D") 91 | self.bob_keyjar.import_jwks(JWK1, issuer_id="D") 92 | 93 | _jws = JWS('{"iss": "D", "aud": "A"}', alg="HS256") 94 | sig_key = self.alice_keyjar.get_signing_key("oct", issuer_id="D")[0] 95 | _sjwt = _jws.sign_compact([sig_key]) 96 | 97 | no_kid_issuer = {"D": []} 98 | 99 | _jwt = factory(_sjwt) 100 | 101 | keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) 102 | assert len(keys) == 1 103 | 104 | 105 | PUBLIC_FILE = f"{BASEDIR}/public_jwks.json" 106 | PRIVATE_FILE = f"{BASEDIR}/private_jwks.json" 107 | KEYSPEC = [ 108 | {"type": "RSA", "use": ["sig"]}, 109 | {"type": "EC", "crv": "P-256", "use": ["sig"]}, 110 | ] 111 | KEYSPEC_2 = [ 112 | {"type": "RSA", "use": ["sig"]}, 113 | {"type": "EC", "crv": "P-256", "use": ["sig"]}, 114 | {"type": "EC", "crv": "P-384", "use": ["sig"]}, 115 | ] 116 | 117 | 118 | def test_init_key_jar_dump_private(): 119 | for _file in [PRIVATE_FILE, PUBLIC_FILE]: 120 | if os.path.isfile(_file): 121 | os.unlink(_file) 122 | 123 | # New set of keys, JWKSs with keys and public written to file 124 | _keyjar = init_key_jar( 125 | private_path=PRIVATE_FILE, key_defs=KEYSPEC, issuer_id="https://example.com" 126 | ) 127 | assert list(_keyjar.owners()) == ["https://example.com"] 128 | 129 | # JWKS will be read from disc, not created new 130 | _keyjar2 = init_key_jar(private_path=PRIVATE_FILE, key_defs=KEYSPEC) 131 | assert list(_keyjar2.owners()) == [""] 132 | 133 | 134 | def test_init_key_jar_update(): 135 | for _file in [PRIVATE_FILE, PUBLIC_FILE]: 136 | if os.path.isfile(_file): 137 | os.unlink(_file) 138 | 139 | # New set of keys, JWKSs with keys and public written to file 140 | _keyjar_1 = init_key_jar( 141 | private_path=PRIVATE_FILE, 142 | key_defs=KEYSPEC, 143 | issuer_id="https://example.com", 144 | public_path=PUBLIC_FILE, 145 | read_only=False, 146 | ) 147 | assert list(_keyjar_1.owners()) == ["https://example.com"] 148 | 149 | _keyjar_2 = init_key_jar(private_path=PRIVATE_FILE, key_defs=KEYSPEC_2, public_path=PUBLIC_FILE) 150 | 151 | # Both should contain the same RSA key 152 | rsa1 = _keyjar_1.get_signing_key("RSA", "https://example.com") 153 | rsa2 = _keyjar_2.get_signing_key("RSA", "") 154 | 155 | assert len(rsa1) == 1 156 | assert len(rsa2) == 1 157 | assert rsa1[0] == rsa2[0] 158 | 159 | # keyjar1 should only contain one EC key while keyjar2 should contain 2. 160 | 161 | ec1 = _keyjar_1.get_signing_key("EC", "https://example.com") 162 | ec2 = _keyjar_2.get_signing_key("EC", "") 163 | assert len(ec1) == 1 164 | assert len(ec2) == 2 165 | 166 | # The file on disc should not have changed 167 | _keyjar_3 = init_key_jar(private_path=PRIVATE_FILE) 168 | 169 | assert len(_keyjar_3.get_signing_key("RSA")) == 1 170 | assert len(_keyjar_3.get_signing_key("EC")) == 1 171 | 172 | _keyjar_4 = init_key_jar( 173 | private_path=PRIVATE_FILE, 174 | key_defs=KEYSPEC_2, 175 | public_path=PUBLIC_FILE, 176 | read_only=False, 177 | ) 178 | 179 | # Now it should 180 | _keyjar_5 = init_key_jar(private_path=PRIVATE_FILE) 181 | 182 | assert len(_keyjar_5.get_signing_key("RSA")) == 1 183 | assert len(_keyjar_5.get_signing_key("EC")) == 2 184 | -------------------------------------------------------------------------------- /tests/test_keys/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA1ECrSVVGxHDjCm+gLdGoTPCTeIgTRvin0cE9M6s/GkD0ta3T 3 | y6DClu8GiBml+PQfQtSya5Ezv8EuUXhFhNCpR+3/vMwUjYLBzH5ybAB/NBRL8MJ1 4 | M7WUzm+WSnl1hdYMolX1xy2cy2Um5saItubMONhhVG+hFAsyy9iCDX+FV8anrOPp 5 | CBuYK1B0BaBSafMDus3SR+xq8T6LgTelwr3srJm6Q6A2AzdBsCkm+WcKW534ClRb 6 | MYfMaJQlBYpmtnQt/k8ctUWX1k/kIAJ7GR5Wh/7555wCcVJbnoc4iNgPs1KyNfmH 7 | v2RiEr2lf0BmxpkzUGnNQNaOVqB1/D1ygsNMAwIDAQABAoIBAQDUMOqMX5Jl5K01 8 | y66I3+avNHtZrkAHXaL4UYVL2FE3f+SklGj+U3L1zXPsMCf7IKL3/wd3/iuL8ibK 9 | D8EALFJvtIFMT4HkjuoL9AWT71M7z2a0BNOCpG9liazoO1DAQeNTjzgsrW7o7/Da 10 | GXSn1UgpNDjpXsfb7+4SWBp8QBYgTok/dx/nZyewZsVtkCsnwrm26Y2k8nhynLm1 11 | nGkQBbRKweQJ5O3PrsrZhRO9OXJ10WK/lYHVqRnu4dam8iP8JHKz5CQN/O7x0HI5 12 | VHHAl0Az4+YZe8wrdNhgtkkOhGxwOtIK5Q3eV8st51YBpWbMiDtQ53ghisWtyNzg 13 | EyphOksxAoGBAO1bTWHsuo6srY4cEJJlJ/C7W237iUM2p9MkeOwcewSXtgY19jAb 14 | IalYzn+yIHLqMyJ1H4cA1sBXBooNvkjVd+7niBbE8du2LyHf4avhNHN2tgKIOjpj 15 | GCsitLEqhyQmUUlYC3z8tQbh99b7fqn2kBDnhdSWPojEx84eXQ71IhMPAoGBAOTs 16 | lQ2iT3cqyore02YOieDwSg7n4flAPPG6cTGGQeOyHpNllTy9U/wyEBrHGGSCkjHI 17 | uDICHtYxyBmL6b0H5IyT4vn1Wxm19ecy8zbuK7Lmwd/iWRfKlGr+YQuez/mHVcCG 18 | YMoQvS6j8WOrQxt2wdOMNatgJeJSz66TaRy2QWfNAoGBAOmTWNJN8MSon15itdgq 19 | 3aQj6/SOfOR866h3ktvfpxu85C62eZ+bg4OwVf4J367WVB3LnovvQmYi/ddrcN8h 20 | 2xVqGV020D+DyFwQgnbvdvtNTg2t24dLryP70k8qZ7UmVAXWM+/6i3bLdmbENUCy 21 | 19Ea1XN/quhSpcFr1e37Q133AoGAD5GLXX8BWoBdf+5BgDpS5CpTTwo0EwhsXKAq 22 | XIzd5EdTzwBkktnpYUhiUf/iR8udd6dH55a/VB/UlPAv+DwWLf1MvWUTSf9W9t8/ 23 | LSgrbqJE4x34oyaSy2f7X5fwWu76RPqekH9s7kQWAYo/KRn9eo6Zg8spKGgrWZsK 24 | 1foLHq0CgYBJRjKEY79aNuKCJZw60QPpXodJ65RJufXPz9MgDdoxUOtno8eYPfep 25 | KWWyhJsQXhMJNUMZGvQXRXaaZ3ZZp1e1q18CLh1TqbInC1ODW3L/ZAWCpT9ihcdA 26 | Owj1RL042er59qut/nivipmB5fn1hTbRDLq9rng0fsNU9XlrETbUfg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/test_keys/ec-p256-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrp4E4eaFhHonpP/U 3 | 8QRaFDTvCGy8mQrSfQod+QxmViehRANCAAQE1WQ7bdvJIkUESD9hc3GUv1+QGt+A 4 | cKvuzxfjPRqqzLtvlU5nUKOM2rgLCMVP0hiu4wtgREKUYLybH1UPwEuv 5 | -----END PRIVATE KEY----- 6 | 7 | -------------------------------------------------------------------------------- /tests/test_keys/ec-p256-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBNVkO23bySJFBEg/YXNxlL9fkBrf 3 | gHCr7s8X4z0aqsy7b5VOZ1CjjNq4CwjFT9IYruMLYERClGC8mx9VD8BLrw== 4 | -----END PUBLIC KEY----- 5 | 6 | -------------------------------------------------------------------------------- /tests/test_keys/ec-p256.json: -------------------------------------------------------------------------------- 1 | { 2 | "crv": "P-256", 3 | "d": "rp4E4eaFhHonpP_U8QRaFDTvCGy8mQrSfQod-QxmVic", 4 | "kid": "dHNuMWxLYnlrSjgxMk8wdVEwWVd3Z1o1UUNsbnNTVkZxcDdDUzFUaXhWTQ", 5 | "kty": "EC", 6 | "x": "BNVkO23bySJFBEg_YXNxlL9fkBrfgHCr7s8X4z0aqsw", 7 | "y": "u2-VTmdQo4zauAsIxU_SGK7jC2BEQpRgvJsfVQ_AS68" 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_keys/ec-p384-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCUwKlpiMoGAPaoSfl5 3 | iVTFrhF1hVzR57YiMld16H7HcMzv6HNF/nHkiaITetKxCaKhZANiAATpTb3pcZ+C 4 | E9WYGiBho6391NUd5wx15qhGTWT0/2hl2DEvfxHEN/rXJUbG+mzCpvDSvz5IfDDF 5 | ryLtXb3fgSYxGA8VLPC24wT0PImF5L7u3Z4vjSmO38qkrIwBMkUKyTU= 6 | -----END PRIVATE KEY----- 7 | 8 | -------------------------------------------------------------------------------- /tests/test_keys/ec-p384-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE6U296XGfghPVmBogYaOt/dTVHecMdeao 3 | Rk1k9P9oZdgxL38RxDf61yVGxvpswqbw0r8+SHwwxa8i7V2934EmMRgPFSzwtuME 4 | 9DyJheS+7t2eL40pjt/KpKyMATJFCsk1 5 | -----END PUBLIC KEY----- 6 | 7 | -------------------------------------------------------------------------------- /tests/test_keys/ec-p384.json: -------------------------------------------------------------------------------- 1 | { 2 | "crv": "P-384", 3 | "d": "lMCpaYjKBgD2qEn5eYlUxa4RdYVc0ee2IjJXdeh-x3DM7-hzRf5x5ImiE3rSsQmi", 4 | "kid": "UDdTWUdvVFZhR0RNVXFUV1pDWEJqT1pwN1BGSnpQSC1LNUMtNE5CUjBhaw", 5 | "kty": "EC", 6 | "x": "6U296XGfghPVmBogYaOt_dTVHecMdeaoRk1k9P9oZdgxL38RxDf61yVGxvpswqbw", 7 | "y": "0r8-SHwwxa8i7V2934EmMRgPFSzwtuME9DyJheS-7t2eL40pjt_KpKyMATJFCsk1" 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_keys/ec.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIATa+jC26Vl3hw5oVDpiufCZuUOnvHdlaxW2VusNGKGqoAoGCCqGSM49 3 | AwEHoUQDQgAEoLqcipC3uBcQA7AfzSI5A9GrEpLl1fJsBPjukveTkOkL2Z5BI4Ja 4 | 5eByAQku71nVtQQhZ9tnP2BR9IrBRK/KpQ== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/test_keys/jwk.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "e": "AQAB", 6 | "kid": "abc", 7 | "kty": "RSA", 8 | "use": "sig", 9 | "n": "pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE-Yopfu3B58QlgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JBqaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9RfzT87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /tests/test_keys/rsa-1024-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAN8MXPfNgdWPVofE 3 | OvRbhumygi+ttUEZ/Y+Ts/CIoB6neeSjQJZ2zgxQhGWKgKKKATopKPZ4TFK00AUN 4 | /eNyM59ikRWRpSzIabByUpfEA81EyvoYAl4qVsmpOzl5h3m2kGN15bauOQP9bpHv 5 | vOn4S25TiNj/NHZhZ6oHoZ1Od24rAgMBAAECgYEA0wOOPH12lETL9xuFLsIcS6Eu 6 | ms66yIE/KgLxW+DVosqMfeqYYwC4hFv0NWAnvB3VdWGVOD+s7R3UIsQO6ouTGzTT 7 | Jgye2gg9YbZbB2+STV9cbeUwTCI4ILzsTp8LVHpZTmAXWvp+ui4S+mL78rT3O+pV 8 | T1BQBwPMqvGOyoBuOTECQQD7xr1Lieex4a0Wi3WeW3ck1GCtIEU0MgIeZLTwc1Y+ 9 | rhem0IQsx4axnV7His+iGikQm20i2EX9AOg2dFRRbJpjAkEA4spAGHgvd+cqUxus 10 | wQX7SfwNdrMSRko5qJIJXOL0zuUBgJ33APSLTWw2MSRND7cj7yh1b/o8eWkvnRZD 11 | AkADmQJBAMz29ZNRKPV+qtH3pkDMZSnuWuWVp8DeFSt5AHPe8Q8F2utKRM/Pfq+J 12 | VWdMccudUGDcpvP+7LsSyffKq/m9V9ECQE6xBtR2v2HHYDQ+Ig9H2A2v26wYLnsd 13 | Pixzn7QPPAqeA4txREeckslmhtc+VU7iqSFO1JDqLxmhmdfT5aReOeECQGKfe9gJ 14 | oiTy02VzeMQ/2FSdkHG7ZjZYl1+rwA1Y+2eKstg88xpk456ePp4PBJ5MQLEQKvyq 15 | wXuCdEjuK8ipOaQ= 16 | -----END PRIVATE KEY----- 17 | 18 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-1024-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfDFz3zYHVj1aHxDr0W4bpsoIv 3 | rbVBGf2Pk7PwiKAep3nko0CWds4MUIRlioCiigE6KSj2eExStNAFDf3jcjOfYpEV 4 | kaUsyGmwclKXxAPNRMr6GAJeKlbJqTs5eYd5tpBjdeW2rjkD/W6R77zp+EtuU4jY 5 | /zR2YWeqB6GdTnduKwIDAQAB 6 | -----END PUBLIC KEY----- 7 | 8 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-1024.json: -------------------------------------------------------------------------------- 1 | { 2 | "d": "0wOOPH12lETL9xuFLsIcS6Eums66yIE_KgLxW-DVosqMfeqYYwC4hFv0NWAnvB3VdWGVOD-s7R3UIsQO6ouTGzTTJgye2gg9YbZbB2-STV9cbeUwTCI4ILzsTp8LVHpZTmAXWvp-ui4S-mL78rT3O-pVT1BQBwPMqvGOyoBuOTE", 3 | "e": "AQAB", 4 | "kid": "TEM0dms2Q0dTeDYzSkUzaVludkZQVEdLb0M1UXhHSkpaRWlvWUhHRFlZUQ", 5 | "kty": "RSA", 6 | "n": "3wxc982B1Y9Wh8Q69FuG6bKCL621QRn9j5Oz8IigHqd55KNAlnbODFCEZYqAoooBOiko9nhMUrTQBQ3943Izn2KRFZGlLMhpsHJSl8QDzUTK-hgCXipWyak7OXmHebaQY3Xltq45A_1uke-86fhLblOI2P80dmFnqgehnU53bis", 7 | "p": "-8a9S4nnseGtFot1nlt3JNRgrSBFNDICHmS08HNWPq4XptCELMeGsZ1ex4rPohopEJttIthF_QDoNnRUUWyaYw", 8 | "q": "4spAGHgvd-cqUxuswQX7SfwNdrMSRko5qJIJXOL0zuUBgJ33APSLTWw2MSRND7cj7yh1b_o8eWkvnRZDAkADmQ" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-1280-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIDBgIBADANBgkqhkiG9w0BAQEFAASCAvAwggLsAgEAAoGhANYLX9BYqOdar4wj 3 | xVV0Ym01FJUxNDxUPz+RPk4XW5/z+Ds9fzsV9JInCEgKAP+NnnDmPBj1Q2eAw8so 4 | QTzXDkpGv3bzkPy5l5+M527x6vb1M6P96PnyLrMZc84A1gQ19zIfPPcn4T4G2KUg 5 | wSQFnl141/3z3lBTuyxILJJnwtUUrlZ3nAo5KzKot9nVZqHNpCbyVGJJuB/R1yov 6 | PQfvLn0CAwEAAQKBoD9QmgUNVNkq0CbKJgNDLCJVw0LmjScTXKo3EpETA71q85DC 7 | fjJCKlhZR2/X1bfCco2+7SQM2OVzB3e+7p1KmCWMi/Jp1geL9rfxfDwrP2/RSeKc 8 | BoY/+XaE34DJHBZ1PuE0KDQ6e3z8L2W8BjhHjzNtLmG3H4M6qruSTJMahqfXFGab 9 | nT5JGI295Vs51f8d5OuQgYzIoAdrN84OVLYvztkCUQDy1nnZTv4DpGj3LlHipEmS 10 | cj8V5ThnvMuIlHHYwy3SJvmuxth8reNc+1yWWn94+bEA5hjLbBwfr5LcDG9pOdNL 11 | JoB1QJkzwS6rHrE3xepOdwJRAOGlX2NVZw8btJtKjaKW+OvINS1FgbcMjZPPWbvE 12 | NA66i7VrHcuCxR7oWyuonTtujcyv419sFj8ODZqbCyxg3/Z4W4NqjfzGpzJd3aTa 13 | AaOrAlBvHMnmL+m0eu74Yv5eyLNNSe8pspdriAxNMzcgarY1mVXXre5yphIJgo4r 14 | 0b7P+NgPxDkGIzJ/IRP+kcYMjMPsd/KBNAyljLgw3jtSfMGoIwJQTZQRy5EdMbCQ 15 | 06M6NuA5DLd0sO/ovQpv0pXDgC7gxv+T8Pe28rUvGIVCcywxgrFrxyfhwHjk3SDz 16 | xGfx23wjBBY1QGVuE64o5cQn4/mf/qkCUQDpjF2GH+EqEinqTGAHlcEpKomp8cXH 17 | ZxVZ7l9KB3sk17aN58GUnuPaIda4D9dbqQjAzqRx6OcQf80oxEE4XUP6nbXkHHZO 18 | 0lakd66lHnHikQ== 19 | -----END PRIVATE KEY----- 20 | 21 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-1280-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIG/MA0GCSqGSIb3DQEBAQUAA4GtADCBqQKBoQDWC1/QWKjnWq+MI8VVdGJtNRSV 3 | MTQ8VD8/kT5OF1uf8/g7PX87FfSSJwhICgD/jZ5w5jwY9UNngMPLKEE81w5KRr92 4 | 85D8uZefjOdu8er29TOj/ej58i6zGXPOANYENfcyHzz3J+E+BtilIMEkBZ5deNf9 5 | 895QU7ssSCySZ8LVFK5Wd5wKOSsyqLfZ1WahzaQm8lRiSbgf0dcqLz0H7y59AgMB 6 | AAE= 7 | -----END PUBLIC KEY----- 8 | 9 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-1280.json: -------------------------------------------------------------------------------- 1 | { 2 | "d": "P1CaBQ1U2SrQJsomA0MsIlXDQuaNJxNcqjcSkRMDvWrzkMJ-MkIqWFlHb9fVt8Jyjb7tJAzY5XMHd77unUqYJYyL8mnWB4v2t_F8PCs_b9FJ4pwGhj_5doTfgMkcFnU-4TQoNDp7fPwvZbwGOEePM20uYbcfgzqqu5JMkxqGp9cUZpudPkkYjb3lWznV_x3k65CBjMigB2s3zg5Uti_O2Q", 3 | "e": "AQAB", 4 | "kid": "RU0wODlEMU9QRTZrSG4yTzl1ZzJKLWg4d3M4cW9yMlVCRlRLcVpiQk1fRQ", 5 | "kty": "RSA", 6 | "n": "1gtf0Fio51qvjCPFVXRibTUUlTE0PFQ_P5E-Thdbn_P4Oz1_OxX0kicISAoA_42ecOY8GPVDZ4DDyyhBPNcOSka_dvOQ_LmXn4znbvHq9vUzo_3o-fIusxlzzgDWBDX3Mh889yfhPgbYpSDBJAWeXXjX_fPeUFO7LEgskmfC1RSuVnecCjkrMqi32dVmoc2kJvJUYkm4H9HXKi89B-8ufQ", 7 | "p": "8tZ52U7-A6Ro9y5R4qRJknI_FeU4Z7zLiJRx2MMt0ib5rsbYfK3jXPtcllp_ePmxAOYYy2wcH6-S3AxvaTnTSyaAdUCZM8Euqx6xN8XqTnc", 8 | "q": "4aVfY1VnDxu0m0qNopb468g1LUWBtwyNk89Zu8Q0DrqLtWsdy4LFHuhbK6idO26NzK_jX2wWPw4NmpsLLGDf9nhbg2qN_ManMl3dpNoBo6s" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-2048-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCidoXtTB20jRA4 3 | QRhmDdAY4ZzWhxdy/zIrH4OXIG6ugYwf9zi6RkkEd9o5BvCbKESWQ1FYxLeNkRO3 4 | sRBNbsP0C3nC/IBoNi48y+5ZrHD7wgxFiO+5nyGHBIeKtta27LRsjr7JoHajdM8u 5 | EyJF+IFsrr26/5rJfLNW7mqOH+LSfPRmEaZZ9xfnpC1i4q5tytgFfeJs1LK2zaRo 6 | AoxBds4mdRF6x0zZN+yd5MeOHVcjpNm7xRiCEdKvlBE3BICvg2A3NhVTdc+Cb2fh 7 | CgB4kXLGRw23CmzN4pHiFh55iZ1XqO3eQdk39UL5b9eqoH2wmn2rbSYEUpp13Gv8 8 | WFoTLgl9AgMBAAECggEASc7q/WnNPQ+xRL1eJk80MXmeVWA+aQJDeo/wqqITZrh4 9 | PrWEUixIU/1XaP0cJoFe151xwZBti6VTfeq1a1hdMexcvxcUB0RGMVy8xoSvN9Fq 10 | 3dRehMgk5UBLi9uRJL3ZIlCfoN6Dx7LzxQzwTzWdJxEkJWamnc9HcsESK8uUsudU 11 | fpb8pKoQmDkAepG6Se6qfySOUgWtrz9XwdEtPH/GgRWGc6N0X9ifCQoM+8uEOi/8 12 | ND9TBxSTgtGHPMXcN9I1rHOPP9oVxXQa3RgQ+zvB/UC7WV+DROAb20fKljGVJXPG 13 | 6OMF6FdWFeqDaMBlEyUE0rpv3w7I0oms0k/5LpGLwQKBgQDTMg/7AmbbYTZ5eCYF 14 | v8C0ERE92DKijPO1+NClBG0tBkzeUzZ5oSjRsnqK0XZ6Q4n794Srl8RCmHTVEw07 15 | gKYbMlizhQcsqPc/OJ6ogULHzXw6oEHprN9l7C7ATIGj/h5pAAx0AdyqMRXN2qdz 16 | Oa5aADuK8WM4tgoqKnNwKVVg9QKBgQDE7dDoTjyrUV1PDDLxu3YuOPoEfwAPZh18 17 | leDdnICVi5B25cqART1sllpqoostPGDD+GUGdOzP14WAFIOmzwazGnysUpc9WB7b 18 | LbvJbsNVaXkNKOIp18dxZ3QRg+wR+6i2YyuWMQwBaSpgUM76/e3Xj3rW2jL7LgKn 19 | Gw6nPTURaQKBgEx6MHdA42ZFyagq7fne+jU6iPfQNmXOjOI0e7mF5NMNGQDZOTzK 20 | MzHcY6upNjIICwuHEi/hAVzsQEJ4Z65IY94tWRmI7pQpi38FTc7PIBOiQX4pUjUA 21 | ONV7tWJFUhzEhNaZAelwUyv+Ilss9r4vAQfuVaF2z+tVYFDslmjjTtuRAoGBALzX 22 | ySNcEzW9+pqY+ilxOO0uWiMBno8lv1T/bPlW4L+HbGt4BG5o93Qv4lirsocYhTqn 23 | 8kj0yzqsVCr0gSLvGF+cIL4nHT0ISd6oxpFtlAdN83U5JVg2wdzv9g/tz+2WxLzR 24 | 8LRAKGEZ9jcShsmNxAyYXJW3/Hd+MbEJc03QlhkxAoGAcjE1yQuFWEeXcWyH0C0F 25 | d2oejX6A2HSlnsCf1QHOzxpp1j05ZC6JrSFqxZEmNNeqYcahjdBjYXQYd0MaGqIu 26 | 42Bj0JRO2Z3K1Udlb0shIt3fCEuTCoOQLPwmHm0cqP97A21ISLfg7BPiI25nbE7V 27 | Ip9YGido3N9hQycoqkETeYI= 28 | -----END PRIVATE KEY----- 29 | 30 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-2048-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAonaF7UwdtI0QOEEYZg3Q 3 | GOGc1ocXcv8yKx+DlyBuroGMH/c4ukZJBHfaOQbwmyhElkNRWMS3jZETt7EQTW7D 4 | 9At5wvyAaDYuPMvuWaxw+8IMRYjvuZ8hhwSHirbWtuy0bI6+yaB2o3TPLhMiRfiB 5 | bK69uv+ayXyzVu5qjh/i0nz0ZhGmWfcX56QtYuKubcrYBX3ibNSyts2kaAKMQXbO 6 | JnUResdM2TfsneTHjh1XI6TZu8UYghHSr5QRNwSAr4NgNzYVU3XPgm9n4QoAeJFy 7 | xkcNtwpszeKR4hYeeYmdV6jt3kHZN/VC+W/XqqB9sJp9q20mBFKaddxr/FhaEy4J 8 | fQIDAQAB 9 | -----END PUBLIC KEY----- 10 | 11 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-2048.json: -------------------------------------------------------------------------------- 1 | { 2 | "d": "Sc7q_WnNPQ-xRL1eJk80MXmeVWA-aQJDeo_wqqITZrh4PrWEUixIU_1XaP0cJoFe151xwZBti6VTfeq1a1hdMexcvxcUB0RGMVy8xoSvN9Fq3dRehMgk5UBLi9uRJL3ZIlCfoN6Dx7LzxQzwTzWdJxEkJWamnc9HcsESK8uUsudUfpb8pKoQmDkAepG6Se6qfySOUgWtrz9XwdEtPH_GgRWGc6N0X9ifCQoM-8uEOi_8ND9TBxSTgtGHPMXcN9I1rHOPP9oVxXQa3RgQ-zvB_UC7WV-DROAb20fKljGVJXPG6OMF6FdWFeqDaMBlEyUE0rpv3w7I0oms0k_5LpGLwQ", 3 | "e": "AQAB", 4 | "kid": "cHZ5d3dhZ1NOSXpsZDNBa1AxZmRaS0VxbVE3cTBzVDlUZFB1WkpKdWV5NA", 5 | "kty": "RSA", 6 | "n": "onaF7UwdtI0QOEEYZg3QGOGc1ocXcv8yKx-DlyBuroGMH_c4ukZJBHfaOQbwmyhElkNRWMS3jZETt7EQTW7D9At5wvyAaDYuPMvuWaxw-8IMRYjvuZ8hhwSHirbWtuy0bI6-yaB2o3TPLhMiRfiBbK69uv-ayXyzVu5qjh_i0nz0ZhGmWfcX56QtYuKubcrYBX3ibNSyts2kaAKMQXbOJnUResdM2TfsneTHjh1XI6TZu8UYghHSr5QRNwSAr4NgNzYVU3XPgm9n4QoAeJFyxkcNtwpszeKR4hYeeYmdV6jt3kHZN_VC-W_XqqB9sJp9q20mBFKaddxr_FhaEy4JfQ", 7 | "p": "0zIP-wJm22E2eXgmBb_AtBERPdgyoozztfjQpQRtLQZM3lM2eaEo0bJ6itF2ekOJ-_eEq5fEQph01RMNO4CmGzJYs4UHLKj3PzieqIFCx818OqBB6azfZewuwEyBo_4eaQAMdAHcqjEVzdqnczmuWgA7ivFjOLYKKipzcClVYPU", 8 | "q": "xO3Q6E48q1FdTwwy8bt2Ljj6BH8AD2YdfJXg3ZyAlYuQduXKgEU9bJZaaqKLLTxgw_hlBnTsz9eFgBSDps8Gsxp8rFKXPVge2y27yW7DVWl5DSjiKdfHcWd0EYPsEfuotmMrljEMAWkqYFDO-v3t14961toy-y4CpxsOpz01EWk" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-3072-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIG/gIBADANBgkqhkiG9w0BAQEFAASCBugwggbkAgEAAoIBgQDFSZh8X1C+hqee 3 | A2deYBacQ9swp0ac6P0WOgp9mtTqd80nRSnn1kDfxzy9l6/czU37GYaLqm7K5Jje 4 | GJIPbYpkzt3Pi6aKvV+6sHz+yLNBFVs3AjLGAUCDEaOzfvkgHNjzVG1IZYRaAOfQ 5 | Nxw8LXLtO+GaPMSCl2bGrYS2dsEyg9ZfWJNIMUtqqEbHL3Fk1gg/1HFnMdXxxC7l 6 | FwezRtx25ldFdBGRVNjUMCJOD37WN6DOMZB2EoGupR3yxQ2xKnbk8NnMXhrCDCAX 7 | 5UJtTkITtO9zN1xYet7TZ4Ym8O2dUElMiVAJf8RiPjgMzdp+DOLSIX6fGNS8k6ax 8 | FwFcc8q0PznJUL1JJYT+S70jAJ9Dhqe9RpgXUKAMDZa4nHgjereIq2FqbX72rmzl 9 | e31I7wdVnhpHbqPp4/ad3Mgt+Pn6TDdA6QI+MhYhIC0cGvzc+c2W+qjF0SU3e18U 10 | t/HFJ/z/Goa1uyJxX9k6GftueLu+rzgml0oIP0fF4K+csjBDK/kCAwEAAQKCAYAD 11 | mOrx4Z8MSlJGHFPHMcNXyBj4EyTDebAbtqU4UxlsyRBxld5EvvBh5DUkU96+DPRO 12 | DtIYO9xXUMHE0vfe1iV+3xLGVQqNXxSex9cPokM6fZQzNZQNbjpp6WltvXP3dO8z 13 | yKdReaBnL7sKXebi5gVBdCUR9eezZeVW3BUeap3tOOAwzuGooFV6tWTM/v13VDvE 14 | RjBnLE51YRllzfyy9SjR9jNDLfGgD8cAX3xQphGvAEarW9YBgSbiM1qPca5lO5v1 15 | Lg9xXgMnKeOhpQoI2BlSPdl2uDEFm7mbDeVccIJMiflULm4uJJXz4LdvtBLXovUx 16 | sYu97/Hu9uII4FErlvT2hPpTKfh4v0ORhH8VAbP8Eg3I7DUhnQzVyTTm5pebVE/g 17 | b9XGoXuvBrBIyHArGh6qNSkdz8wJFRz4UYh0cKYXkvJ0NDq20PONQUFYaO28knpW 18 | X9S5uczx61L6tCprSLfKVoWlMKHySjKgWjGXaB3mgLBz5eL7YyIO3k/YL/fkO30C 19 | gcEA7B8QtdrdMXQ2qoDj+DLnNoVjsORjjkoKG3+KkG/o2Uxl0mPbeF0+WQRApiPx 20 | 2DNuOzwbnc6qkzvbBAOx9idNpLwsuKMtWv4jdl7VyJ9RTfV9LJdNiFJ7P3USUV0B 21 | jelv/tRtktvwBwWdhxMETNytTYhtUCORJY8VpR7yuUmihumAClmBZjmzOPFhkLJ9 22 | F/9M2wi0Z2MF0JOR0yhMmTpsZhxOmcxmm+QqpexD3viCrN6MXEpdUEcdisaKq6F+ 23 | ONHPAoHBANXlkznDLr7bb9jKMexm8PakXLjB/wuVeKYEmZ1jFUA1MFnZirk+x+sX 24 | aKL3V3b6DdXvJdjeOPZlgZsBTiJcpruMS82P3OXbDIeygXzqoA5otWN8FGXSj036 25 | sxThGFd1maQJdtk6FjfGF60WphITQi6uxxuEFxPHDMuldeqzgxrm4xjZ23OR6FNW 26 | TF24KOaQ6KpXsMS3rDVck5+TQr7bjcz7puz1IZucefnCnbmNOZRd2wIkCIJjZZI2 27 | e894ytj/twKBwBnK/vFOmjEHx9zyPXTkYptzEMPG/xURA2+jjAhhISdzj//ehp/s 28 | V54zt7guXwHGBHmWBFJFvB5fQHp8yNjhzDp9j8kPkinJbyhEDchflusUMPtI/+Av 29 | WmVBLaITrdLJuEE3BU0wl1S9CUszCZo31PqON5q0d+uJaMzjx5Hz+DwSj7kRs+/t 30 | x6UlwQmkZcPUUJnwpnxQtdbl990AHvXyCttQpkloqaDH2NzNJVDbBrNJkD2Ypf7Z 31 | 9eEt9QHwnzAnwwKBwQCEgiyq5BfZdOfdTaWP5lXw9fHbI5N1Aaw0bTUdI3zOwiSq 32 | BPLDv/jp0x6nlsNAmbhoiDbSGxj+y/N2q2BUOUGxfkCXoEAgxYsFpbhCQrfVNVjp 33 | wLyCN+c/T8gRb0E9LUV5McPG3w1UkAuq1xUarYOal/wRN8t0HSP4JRDfzUWzLMYT 34 | ZyNkfUeIGTZF+QidjbgVjy2bMI4moKtzeC8pPaqjCmNm+JVRaabG66H6iVraZyp+ 35 | MQR0gk8yPT6GUTaZKBECgcEAl3/CMQlhqsfZkDEIiq0At4/RWFjCYKRptkvc7NIy 36 | kgW+6uqke4yB603Gj3m1eqxS2bV3BcR7+ZaR5hcu46dTv/p2MZ+LnS3MMhc+43YK 37 | /w8aEPAtSSyEv/7RQrlILcL1d1SWKedZTr0vAWvCbrSD1Qawa3o32bI+FAoBa3Sg 38 | 1H60+frQ79QUAiKJm73U6o9al7NZBaTJZrAwfJGygOucprd0RmreCg4LtbSL3nt0 39 | wWHrbeVLAmn0Dlw2zyBc47B9 40 | -----END PRIVATE KEY----- 41 | 42 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-3072-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxUmYfF9QvoanngNnXmAW 3 | nEPbMKdGnOj9FjoKfZrU6nfNJ0Up59ZA38c8vZev3M1N+xmGi6puyuSY3hiSD22K 4 | ZM7dz4umir1furB8/sizQRVbNwIyxgFAgxGjs375IBzY81RtSGWEWgDn0DccPC1y 5 | 7TvhmjzEgpdmxq2EtnbBMoPWX1iTSDFLaqhGxy9xZNYIP9RxZzHV8cQu5RcHs0bc 6 | duZXRXQRkVTY1DAiTg9+1jegzjGQdhKBrqUd8sUNsSp25PDZzF4awgwgF+VCbU5C 7 | E7TvczdcWHre02eGJvDtnVBJTIlQCX/EYj44DM3afgzi0iF+nxjUvJOmsRcBXHPK 8 | tD85yVC9SSWE/ku9IwCfQ4anvUaYF1CgDA2WuJx4I3q3iKtham1+9q5s5Xt9SO8H 9 | VZ4aR26j6eP2ndzILfj5+kw3QOkCPjIWISAtHBr83PnNlvqoxdElN3tfFLfxxSf8 10 | /xqGtbsicV/ZOhn7bni7vq84JpdKCD9HxeCvnLIwQyv5AgMBAAE= 11 | -----END PUBLIC KEY----- 12 | 13 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-3072.json: -------------------------------------------------------------------------------- 1 | { 2 | "d": "A5jq8eGfDEpSRhxTxzHDV8gY-BMkw3mwG7alOFMZbMkQcZXeRL7wYeQ1JFPevgz0Tg7SGDvcV1DBxNL33tYlft8SxlUKjV8UnsfXD6JDOn2UMzWUDW46aelpbb1z93TvM8inUXmgZy-7Cl3m4uYFQXQlEfXns2XlVtwVHmqd7TjgMM7hqKBVerVkzP79d1Q7xEYwZyxOdWEZZc38svUo0fYzQy3xoA_HAF98UKYRrwBGq1vWAYEm4jNaj3GuZTub9S4PcV4DJynjoaUKCNgZUj3ZdrgxBZu5mw3lXHCCTIn5VC5uLiSV8-C3b7QS16L1MbGLve_x7vbiCOBRK5b09oT6Uyn4eL9DkYR_FQGz_BINyOw1IZ0M1ck05uaXm1RP4G_VxqF7rwawSMhwKxoeqjUpHc_MCRUc-FGIdHCmF5LydDQ6ttDzjUFBWGjtvJJ6Vl_UubnM8etS-rQqa0i3ylaFpTCh8koyoFoxl2gd5oCwc-Xi-2MiDt5P2C_35Dt9", 3 | "e": "AQAB", 4 | "kid": "YUIwTDdlWGtjNndmU29FQ09RNUJCUzB5aXRwMVBTNG9iRUxHcE1xS3lmOA", 5 | "kty": "RSA", 6 | "n": "xUmYfF9QvoanngNnXmAWnEPbMKdGnOj9FjoKfZrU6nfNJ0Up59ZA38c8vZev3M1N-xmGi6puyuSY3hiSD22KZM7dz4umir1furB8_sizQRVbNwIyxgFAgxGjs375IBzY81RtSGWEWgDn0DccPC1y7TvhmjzEgpdmxq2EtnbBMoPWX1iTSDFLaqhGxy9xZNYIP9RxZzHV8cQu5RcHs0bcduZXRXQRkVTY1DAiTg9-1jegzjGQdhKBrqUd8sUNsSp25PDZzF4awgwgF-VCbU5CE7TvczdcWHre02eGJvDtnVBJTIlQCX_EYj44DM3afgzi0iF-nxjUvJOmsRcBXHPKtD85yVC9SSWE_ku9IwCfQ4anvUaYF1CgDA2WuJx4I3q3iKtham1-9q5s5Xt9SO8HVZ4aR26j6eP2ndzILfj5-kw3QOkCPjIWISAtHBr83PnNlvqoxdElN3tfFLfxxSf8_xqGtbsicV_ZOhn7bni7vq84JpdKCD9HxeCvnLIwQyv5", 7 | "p": "7B8QtdrdMXQ2qoDj-DLnNoVjsORjjkoKG3-KkG_o2Uxl0mPbeF0-WQRApiPx2DNuOzwbnc6qkzvbBAOx9idNpLwsuKMtWv4jdl7VyJ9RTfV9LJdNiFJ7P3USUV0Bjelv_tRtktvwBwWdhxMETNytTYhtUCORJY8VpR7yuUmihumAClmBZjmzOPFhkLJ9F_9M2wi0Z2MF0JOR0yhMmTpsZhxOmcxmm-QqpexD3viCrN6MXEpdUEcdisaKq6F-ONHP", 8 | "q": "1eWTOcMuvttv2Mox7Gbw9qRcuMH_C5V4pgSZnWMVQDUwWdmKuT7H6xdoovdXdvoN1e8l2N449mWBmwFOIlymu4xLzY_c5dsMh7KBfOqgDmi1Y3wUZdKPTfqzFOEYV3WZpAl22ToWN8YXrRamEhNCLq7HG4QXE8cMy6V16rODGubjGNnbc5HoU1ZMXbgo5pDoqlewxLesNVyTn5NCvtuNzPum7PUhm5x5-cKduY05lF3bAiQIgmNlkjZ7z3jK2P-3" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-4096-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDAm70xiqv/JkuG 3 | FmYdsz0HuU91tKX625AOqLZF6oaJxfmTJN+iC/Hj5Dft2+KHIwOQRc/MWVqjnOK6 4 | du83jpQtXSUTV2E4vNuolipa1xMTOK3yWnZUJjFdw03B48V4KP/oN6y2f1IndRVQ 5 | /CioYqmTGCyqtxKPqB4PtgCIblqpmiTdmyHmDPPsSbNtb4gbM3TGc41b5/uS/VLX 6 | sNQfsUnGgIt/Ecr6w2kYzMhczmw7J6R5sqdLdmWaktSI0obaQd8FBPp07MIFDrM8 7 | 5b0pFkxHWTQphRJajueweULRlWVxxu4doU+TrJgjHMAf2e+5lpxxy3RgP8DPoKYe 8 | sUp8uvKcs7ODYv7BmDbit8Wcd7PEQxGPf6VQIjfepY5Ex09qwicvXbudILoC8DQ0 9 | 6nNDd8Y2NlzRKmZc0c0U3eo0vAH+hlN8pS2TsPIkDVNiENQ9rKutc21rtLkzO/IA 10 | UbRSAOcnp1h8nKglnrj6UWmS9OPq3QpD7zVzYiAdCi7V2qTq/SFlvXnTzE/U1VfU 11 | 8aUEaC+ZzbBPnHNSvCyVuYU+EYDbsUY863ZLW59nNk91ck8XRdmQjUge0c/JY/ig 12 | YkGAuwrMGBYP1PNbjNcaUtWeWx08N67U/wJ4VfWyRvquCIZbsnJyIoyogF3d3s0y 13 | 4Q7E8l5VctT0tlXrMjsj4SpzyDPXmQIDAQABAoICACQiU5kiwefJQGv0cc3qnW2A 14 | h3a2oFjyrhEB98ggGoNoX80KIuZcxVGStCdslA/0MqvzXOuia828FhETVQegOQ5V 15 | pin9v6CMVc1H9sQ6CzsU7BVGgWKCE8raZBEW8Sn8zx26rC1Er531aj1aQSpt8BBH 16 | fcG9PL80tAcQhwMRaPLel0H346EsFuriXMYQIYGw4pyEeyWd3OWTnuIWK4WSlhum 17 | bAU3ylPHK0VazdATbdyfME0ghiAzIxafqz3L+T6jEvr3FyGudSvLuTaYhydZJpBQ 18 | HOgZLftNSgwig7WcCH8S6gMnCy3SOGyF7UlMcpvntIvotnmEHGEXa3Lgoe/Khs8p 19 | zc6lqGacAxHpMgkZyvrvzGOP/jP1OmIhiyWtW4GqSIFaIktFN0pYu9w6BfjrAgYt 20 | mukbSsr1PnumNELyKmt7kW8q/oAUe+Kv1fgLG3wdSVJ6Pqc+61uelXPeCRHrleJ8 21 | LZiceopY3wxLQPWMhcMifMrhqLEJFMXhMXscWRXiamPvGj4QSdT7+D3rAAqyLqz6 22 | +AmfJY5nFjS7dFB+W0QAaFetXETFcorgO6qK3OqRQVb2w9OjI7S8rquzCrLqprCX 23 | nfTa/OH4nBOfpfOPVWtRYyfc8YVn53juB1vuytvkHxJjrVgGbykIDkf503gp3bto 24 | oOUicKNO9Fc1uRSdpkOBAoIBAQDlu2fIdnc7ej3fufcqpYhsJAcSLe9+ZOescd53 25 | nQzBGWDr3q3T+ffReEN8qJIf/k2JFGQp7eUrWfhYIrOkj9uvtu40eHwFj41kpqLZ 26 | gI0DLBR0B3D2S0GRFB6k6E3Bm1o3VtKVNUmm6ae8PF/0Wto1UVqn7Y+9wqJB5LL8 27 | mu/Ok/DEJVR2JbE63GQkq69rSZqfxGEXLuFnTTghQICherPHVbaY9BnWYnCrcwKV 28 | 5h7mFbF94mIAvufMFcQXEi47YEtjYrIy7fBSU0txGEWbhro/apHDO9Owany9oN4j 29 | bObJHC733MYUHeQZpGWh9eI/rUTosLlqMq5SNL4PN1YPtQEJAoIBAQDWoasjHshS 30 | 1SGvFzyyjv/WMTyhJmy1GUrNRqROoYsA8j8TXJqGZKVyw+skqaEk6k1V6gb472+i 31 | FrqQZm5yjZC2pv5TcHadn3I0z3rLkJaZL6XqfaoB0OWyQcC0VesKJJgcyAjj4ede 32 | njjcpsJTu0XT+C4VgWzhkD5BS+ZScqAD142jGswyyoVtRikrdm8H574OpbrxI8Mq 33 | 15UHVEtyZxfeBcLF8aXlx+CPQ5UEm1QRenND5IguQXXVh7I4L/f1q2ScfqLNkHcr 34 | exQ+bMNj2VS/5J/YaGsw/NsuhYRkoqKYHPjMNGtZdw9A38JlcOs/p7saAva/A7LQ 35 | /8s9fI00WBYRAoIBACEgNsTXIDY5WDascBF8MQG26DYc3Y3SKv3YpYWF5dLqP+mD 36 | AHmyb6OKveB5xihK2Q9NG2xYW1U28fJzfofaHollzU9PmEKNAoxi4tSODsdIUeKx 37 | N/6rt1aeGhUZUiz6o7WbFV5igq5IvuELq6Jmf1R02KeiP7nTGOZ7fbui87VL68m8 38 | wEMRCE8+fEo1X2QPBvbGsGFRZzsoDVHuJR4jvUye9Wd0u00IgMqYGOVYe1geBKvs 39 | Osl0vaPV96KEApqZNgWss5lYafDVBenW4CKe+LgwO7h1Jf8oO7h0O9PL3jNdlwAy 40 | m3VHIh74b5c9B1S7PVZ4NSQ1DqHX7RfwmL5HEVECggEBALjCsu+osAmeL0DqX/XV 41 | 46Ye/HI9AZdkDmBnT0nhBifqYSHptZu7SDFjNih5XZ7Jk4lBzH6+nvqJVlC33JwA 42 | AXM6jfSF+5X0i/uqH6rc7McxnUIeqxqlGRY6RAj5bTtuBQPBruV8sg9hZNGNO/uY 43 | WqP+IK2inHgQE4diLwMKaXTlOagOiXmvnOxUWVxwt2O2uoUhZavvfTKEImYMymXR 44 | GVqm2uRLAOStC0JIix9MFzJ0loT7Vb972lPWXDTCFlIFGLcezEbo9a0YZnTWa9yo 45 | WaxWwsMoIB4rjQKfLTj0R8NqLUKcXUzGkPrnpBBvBBPn78iUhJQqOuRvTiW/8k39 46 | BgECggEBAJhik2B0otmk0CA1Z4CHMa8hDZApC+ZqDOCysHFzaVr8FWiHLfJVD2OJ 47 | zJlZlioPim4SjgjbbB4AMGWGp0dZZhX6rIBHWhePf51+GJdz7PFnxup3pf7kC8Qj 48 | HXekUdh58QX9ifqe59zghE6IFOOwpPwevEQXCh/99h7p/mHB2y+L+U2c9t43JcVT 49 | dfajuukiLycB4Bl3ercPyDLDmPicUEizSO1LW+N5nk7Y2m3tGwrrncoEks19L36A 50 | H0Kua1AIg8wrLsE1eoRHaKalCYEo2J5jB8ji5gEnlvqDg4yjZ2NpTRm2HciKsQUL 51 | lyJo0JRs+DrjD7/jYUSzLyIODs3bQgk= 52 | -----END PRIVATE KEY----- 53 | 54 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-4096-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwJu9MYqr/yZLhhZmHbM9 3 | B7lPdbSl+tuQDqi2ReqGicX5kyTfogvx4+Q37dvihyMDkEXPzFlao5ziunbvN46U 4 | LV0lE1dhOLzbqJYqWtcTEzit8lp2VCYxXcNNwePFeCj/6Destn9SJ3UVUPwoqGKp 5 | kxgsqrcSj6geD7YAiG5aqZok3Zsh5gzz7EmzbW+IGzN0xnONW+f7kv1S17DUH7FJ 6 | xoCLfxHK+sNpGMzIXM5sOyekebKnS3ZlmpLUiNKG2kHfBQT6dOzCBQ6zPOW9KRZM 7 | R1k0KYUSWo7nsHlC0ZVlccbuHaFPk6yYIxzAH9nvuZaccct0YD/Az6CmHrFKfLry 8 | nLOzg2L+wZg24rfFnHezxEMRj3+lUCI33qWORMdPasInL127nSC6AvA0NOpzQ3fG 9 | NjZc0SpmXNHNFN3qNLwB/oZTfKUtk7DyJA1TYhDUPayrrXNta7S5MzvyAFG0UgDn 10 | J6dYfJyoJZ64+lFpkvTj6t0KQ+81c2IgHQou1dqk6v0hZb1508xP1NVX1PGlBGgv 11 | mc2wT5xzUrwslbmFPhGA27FGPOt2S1ufZzZPdXJPF0XZkI1IHtHPyWP4oGJBgLsK 12 | zBgWD9TzW4zXGlLVnlsdPDeu1P8CeFX1skb6rgiGW7JyciKMqIBd3d7NMuEOxPJe 13 | VXLU9LZV6zI7I+Eqc8gz15kCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | 16 | -------------------------------------------------------------------------------- /tests/test_keys/rsa-4096.json: -------------------------------------------------------------------------------- 1 | { 2 | "d": "JCJTmSLB58lAa_RxzeqdbYCHdragWPKuEQH3yCAag2hfzQoi5lzFUZK0J2yUD_Qyq_Nc66JrzbwWERNVB6A5DlWmKf2_oIxVzUf2xDoLOxTsFUaBYoITytpkERbxKfzPHbqsLUSvnfVqPVpBKm3wEEd9wb08vzS0BxCHAxFo8t6XQffjoSwW6uJcxhAhgbDinIR7JZ3c5ZOe4hYrhZKWG6ZsBTfKU8crRVrN0BNt3J8wTSCGIDMjFp-rPcv5PqMS-vcXIa51K8u5NpiHJ1kmkFAc6Bkt-01KDCKDtZwIfxLqAycLLdI4bIXtSUxym-e0i-i2eYQcYRdrcuCh78qGzynNzqWoZpwDEekyCRnK-u_MY4_-M_U6YiGLJa1bgapIgVoiS0U3Sli73DoF-OsCBi2a6RtKyvU-e6Y0QvIqa3uRbyr-gBR74q_V-AsbfB1JUno-pz7rW56Vc94JEeuV4nwtmJx6iljfDEtA9YyFwyJ8yuGosQkUxeExexxZFeJqY-8aPhBJ1Pv4PesACrIurPr4CZ8ljmcWNLt0UH5bRABoV61cRMVyiuA7qorc6pFBVvbD06MjtLyuq7MKsuqmsJed9Nr84ficE5-l849Va1FjJ9zxhWfneO4HW-7K2-QfEmOtWAZvKQgOR_nTeCndu2ig5SJwo070VzW5FJ2mQ4E", 3 | "e": "AQAB", 4 | "kid": "VkN2UlZkQVlJY0wtRG1QODBWR09DNmxlZVBJSG9yUGlwN00wNVZDNzFaUQ", 5 | "kty": "RSA", 6 | "n": "wJu9MYqr_yZLhhZmHbM9B7lPdbSl-tuQDqi2ReqGicX5kyTfogvx4-Q37dvihyMDkEXPzFlao5ziunbvN46ULV0lE1dhOLzbqJYqWtcTEzit8lp2VCYxXcNNwePFeCj_6Destn9SJ3UVUPwoqGKpkxgsqrcSj6geD7YAiG5aqZok3Zsh5gzz7EmzbW-IGzN0xnONW-f7kv1S17DUH7FJxoCLfxHK-sNpGMzIXM5sOyekebKnS3ZlmpLUiNKG2kHfBQT6dOzCBQ6zPOW9KRZMR1k0KYUSWo7nsHlC0ZVlccbuHaFPk6yYIxzAH9nvuZaccct0YD_Az6CmHrFKfLrynLOzg2L-wZg24rfFnHezxEMRj3-lUCI33qWORMdPasInL127nSC6AvA0NOpzQ3fGNjZc0SpmXNHNFN3qNLwB_oZTfKUtk7DyJA1TYhDUPayrrXNta7S5MzvyAFG0UgDnJ6dYfJyoJZ64-lFpkvTj6t0KQ-81c2IgHQou1dqk6v0hZb1508xP1NVX1PGlBGgvmc2wT5xzUrwslbmFPhGA27FGPOt2S1ufZzZPdXJPF0XZkI1IHtHPyWP4oGJBgLsKzBgWD9TzW4zXGlLVnlsdPDeu1P8CeFX1skb6rgiGW7JyciKMqIBd3d7NMuEOxPJeVXLU9LZV6zI7I-Eqc8gz15k", 7 | "p": "5btnyHZ3O3o937n3KqWIbCQHEi3vfmTnrHHed50MwRlg696t0_n30XhDfKiSH_5NiRRkKe3lK1n4WCKzpI_br7buNHh8BY-NZKai2YCNAywUdAdw9ktBkRQepOhNwZtaN1bSlTVJpumnvDxf9FraNVFap-2PvcKiQeSy_JrvzpPwxCVUdiWxOtxkJKuva0man8RhFy7hZ004IUCAoXqzx1W2mPQZ1mJwq3MCleYe5hWxfeJiAL7nzBXEFxIuO2BLY2KyMu3wUlNLcRhFm4a6P2qRwzvTsGp8vaDeI2zmyRwu99zGFB3kGaRlofXiP61E6LC5ajKuUjS-DzdWD7UBCQ", 8 | "q": "1qGrIx7IUtUhrxc8so7_1jE8oSZstRlKzUakTqGLAPI_E1yahmSlcsPrJKmhJOpNVeoG-O9voha6kGZuco2Qtqb-U3B2nZ9yNM96y5CWmS-l6n2qAdDlskHAtFXrCiSYHMgI4-HnXp443KbCU7tF0_guFYFs4ZA-QUvmUnKgA9eNoxrMMsqFbUYpK3ZvB-e-DqW68SPDKteVB1RLcmcX3gXCxfGl5cfgj0OVBJtUEXpzQ-SILkF11YeyOC_39atknH6izZB3K3sUPmzDY9lUv-Sf2GhrMPzbLoWEZKKimBz4zDRrWXcPQN_CZXDrP6e7GgL2vwOy0P_LPXyNNFgWEQ" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_keys/rsa.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQD6vqn19W/VB215DBADRakfPmCtFBf8/+YyhGqixWIwDiEl/L6L 3 | w5HKZCUPVgrC0ADhJfvAbn4fte5MWBCTkqgepKL3BySMA0LMaBF12pbHlPSUbmQG 4 | BJmTX4NNXuUel6TbPYJAU2Nh5Nan0Mb7Bmb8QpFvS0Hw7qZRW8y2eIttfwIDAQAB 5 | AoGBAJVf9FxkRKUB8cOE3h006JWGUY2KROghgn9hxy0ErYO3RyQcN1+HuFh75GAI 6 | gAyiYYO/XwS6TkSR2057wBRJ8ABzcL3+v5g+16Vbh0BjXVE+cv1WGdNGujyzl6ji 7 | jlyF4cb6tXDyqWTLkMAtV20NfO/CGsfii6YEkZb2P90usthRAkEA/oG7a9EvQ7eR 8 | gSEqppzW7KCwidPjnZTr/ROIZQU33nwkIJ0ElTjMNYKP8DerSuixR9skw2ZY8Q8I 9 | 1PTBnocHwwJBAPw3SAQYwxZwQMu1trVPMNOGIbSY4rQlMZGXrCZSu/TnozczFLA8 10 | qNM84g5veyJOzHKmYkIsMG1gwg5VNniG45UCQF6SlLOW0upl70K9sVyiUVcyywcc 11 | Xqty6FJtjLSFQOKC3OXlkwtkRLXpo1UPSq6WUzIxY7LceFZzUMPZg41F/gMCQHNr 12 | POqbBlPzZMOUUZthNP/nhu8lc8Fqr+dnmGElRVxK0JdHKfWInN2mI/DlNV064Dar 13 | S5XqsPKs78EtX7MCT40CQFQZiry8m7ROubOU4+HDG9o1w9zcKXCkmbD9hBCGvTAj 14 | BQNuGE0DtC6FEWTs8bXybLM5yBRq1XiKLdmi5N+3n4g= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /tests/test_vector.py: -------------------------------------------------------------------------------- 1 | """Copyright 2018 Google LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.""" 14 | 15 | __author__ = "quannguyen@google.com (Quan Nguyen)" 16 | 17 | import json 18 | 19 | # Test vector from https://tools.ietf.org/html/rfc7515#appendix-A.2. 20 | json_rsa_priv_key = r""" 21 | { 22 | "kty":"RSA", 23 | "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", 24 | "e":"AQAB", 25 | "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", 26 | "p":"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdiYrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPGBY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", 27 | "q":"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxaewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", 28 | "dp":"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3QCLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", 29 | "dq":"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-kyNlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", 30 | "qi":"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2oy26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLUW0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U", 31 | "alg":"RS256" 32 | }""" 33 | 34 | json_rsa_pub_key = r""" 35 | { 36 | "kty":"RSA", 37 | "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", 38 | "e":"AQAB", 39 | "alg":"RS256" 40 | }""" 41 | 42 | json_rsa_pub_key2 = r""" 43 | { 44 | "kty":"RSA", 45 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 46 | "e":"AQAB", 47 | "kid":"2011-04-29" 48 | }""" 49 | 50 | # Test vector from https://tools.ietf.org/html/rfc7515#appendix-A.2 51 | rsa_token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw" 52 | 53 | # Test vector from https://tools.ietf.org/html/rfc7515#appendix-A.3 54 | es256_ecdsa_token = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q" 55 | 56 | es256_ecdsa_priv_key = r""" 57 | { 58 | "kty":"EC", 59 | "crv":"P-256", 60 | "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", 61 | "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", 62 | "d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI", 63 | "alg":"ES256" 64 | }""" 65 | 66 | es256_ecdsa_pub_key = r""" 67 | { 68 | "kty":"EC", 69 | "crv":"P-256", 70 | "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", 71 | "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", 72 | "alg":"ES256" 73 | }""" 74 | 75 | # Test vector from https://tools.ietf.org/html/rfc7515#appendix-A.4. 76 | es512_ecdsa_token = "eyJhbGciOiJFUzUxMiJ9.UGF5bG9hZA.AdwMgeerwtHoh-l192l60hp9wAHZFVJbLfD_UxMi70cwnZOYaRI1bKPWROc-mZZqwqT2SI-KGDKB34XO0aw_7XdtAG8GaSwFKdCAPZgoXD2YBJZCPEX3xKpRwcdOO8KpEHwJjyqOgzDO7iKvU8vcnwNrmxYbSW9ERBXukOXolLzeO_Jn" 77 | 78 | es512_ecdsa_priv_key = r""" 79 | { 80 | "kty":"EC", 81 | "crv":"P-521", 82 | "x":"AekpBQ8ST8a8VcfVOTNl353vSrDCLLJXmPk06wTjxrrjcBpXp5EOnYG_NjFZ6OvLFV1jSfS9tsz4qUxcWceqwQGk", 83 | "y":"ADSmRA43Z1DSNx_RvcLI87cdL07l6jQyyBXMoxVg_l2Th-x3S1WDhjDly79ajL4Kkd0AZMaZmh9ubmf63e3kyMj2", 84 | "d":"AY5pb7A0UFiB3RELSD64fTLOSV_jazdF7fLYyuTw8lOfRhWg6Y6rUrPAxerEzgdRhajnu0ferB0d53vM9mE15j2C", 85 | "alg":"ES512" 86 | }""" 87 | 88 | es512_ecdsa_pub_key = r""" 89 | { 90 | "kty":"EC", 91 | "crv":"P-521", 92 | "x":"AekpBQ8ST8a8VcfVOTNl353vSrDCLLJXmPk06wTjxrrjcBpXp5EOnYG_NjFZ6OvLFV1jSfS9tsz4qUxcWceqwQGk", 93 | "y":"ADSmRA43Z1DSNx_RvcLI87cdL07l6jQyyBXMoxVg_l2Th-x3S1WDhjDly79ajL4Kkd0AZMaZmh9ubmf63e3kyMj2", 94 | "alg":"ES512" 95 | }""" 96 | 97 | # Test vector from https://tools.ietf.org/html/rfc7515#appendix-A.1 98 | json_hmac_key = r""" 99 | { 100 | "kty":"oct", 101 | "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", 102 | "alg":"HS256" 103 | }""" 104 | 105 | # Test vector from https://tools.ietf.org/html/rfc7515#appendix-A.1 106 | hmac_token = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 107 | 108 | # Key set containing multiple public keys. 109 | json_pub_keys = r"""{"keys":[""" + json_rsa_pub_key + "," + es256_ecdsa_pub_key + r"""]}""" 110 | 111 | # The followings are our own tests. 112 | 113 | test_header_rsa = json.dumps({"typ": "JWT", "alg": "RS256"}, separators=(",", ":")) 114 | 115 | test_header_ecdsa = json.dumps({"typ": "JWT", "alg": "ES256"}, separators=(",", ":")) 116 | 117 | test_header_hmac = json.dumps({"typ": "JWT", "alg": "HS256"}, separators=(",", ":")) 118 | 119 | test_payload = json.dumps( 120 | {"aud": "aud1", "sub": "subject1", "iss": "issuer1"}, separators=(",", ":") 121 | ) 122 | 123 | test_header_ecdsa_kid1 = json.dumps( 124 | {"typ": "JWT", "alg": "ES256", "kid": "kid1"}, separators=(",", ":") 125 | ) 126 | 127 | test_header_ecdsa_kid2 = json.dumps( 128 | {"typ": "JWT", "alg": "ES256", "kid": "kid2"}, separators=(",", ":") 129 | ) 130 | 131 | test_json_ecdsa_priv_key_kid1 = r""" 132 | { 133 | "kty":"EC", 134 | "crv":"P-256", 135 | "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", 136 | "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", 137 | "d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI", 138 | "kid":"kid1", 139 | "alg":"ES256" 140 | }""" 141 | 142 | test_json_ecdsa_pub_key_kid1 = r""" 143 | { 144 | "kty":"EC", 145 | "crv":"P-256", 146 | "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", 147 | "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", 148 | "kid":"kid1", 149 | "alg":"ES256" 150 | }""" 151 | 152 | test_json_ecdsa_priv_key_kid2 = r""" 153 | { 154 | "kty":"EC", 155 | "crv":"P-256", 156 | "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", 157 | "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", 158 | "d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI", 159 | "kid":"kid2", 160 | "alg":"ES256" 161 | }""" 162 | --------------------------------------------------------------------------------