././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8059587 josepy-1.14.0/CHANGELOG.rst0000644000000000000000000000701614520452152012061 0ustar00Changelog ========= 1.14.0 (2023-11-01) ------------------- * Added support for Python 3.11 and 3.12. * Support for Python 3.7 has been deprecated and will be removed in the next scheduled release. * Dropped support for Python 3.6. * Added a new valid PGP key for signing our PyPI packages with the fingerprint F2871B4152AE13C49519111F447BF683AA3B26C3 1.13.0 (2022-03-10) ------------------- * Support for Python 3.6 has been deprecated and will be removed in the next scheduled release. * Corrected some type annotations. 1.12.0 (2022-01-11) ------------------- * Corrected some type annotations. * Dropped support for cryptography<1.5. * Added the top level attributes josepy.JWKEC, josepy.JWKOct, and josepy.ComparableECKey for convenience and consistency. 1.11.0 (2021-11-17) ------------------- * Added support for Python 3.10. * We changed the PGP key used to sign the packages we upload to PyPI. Going forward, releases will be signed with one of three different keys. All of these keys are available on major key servers and signed by our previous PGP key. The fingerprints of these new keys are: - BF6BCFC89E90747B9A680FD7B6029E8500F7DB16 - 86379B4F0AF371B50CD9E5FF3402831161D1D280 - 20F201346BF8F3F455A73F9A780CC99432A28621 1.10.0 (2021-09-27) ------------------- * josepy is now compliant with PEP-561: type checkers will fetch types from the inline types annotations when josepy is installed as a dependency in a Python project. * Added a `field` function to assist in adding type annotations for Fields in classes. If the field function is used to define a `Field` in a `JSONObjectWithFields` based class without a type annotation, an error will be raised. * josepy's tests can no longer be imported under the name josepy, however, they are still included in the package and you can run them by installing josepy with "tests" extras and running `python -m pytest`. 1.9.0 (2021-09-09) ------------------ * Removed pytest-cache testing dependency. * Fixed a bug that sometimes caused incorrect padding to be used when serializing Elliptic Curve keys as JSON Web Keys. 1.8.0 (2021-03-15) ------------------ * Removed external mock dependency. * Removed dependency on six. * Deprecated the module josepy.magic_typing. * Fix JWS/JWK generation with EC keys when keys or signatures have leading zeros. 1.7.0 (2021-02-11) ------------------ * Dropped support for Python 2.7. * Added support for EC keys. 1.6.0 (2021-01-26) ------------------ * Deprecated support for Python 2.7. 1.5.0 (2020-11-03) ------------------ * Added support for Python 3.9. * Dropped support for Python 3.5. * Stopped supporting running tests with ``python setup.py test`` which is deprecated in favor of ``python -m pytest``. 1.4.0 (2020-08-17) ------------------ * Deprecated support for Python 3.5. 1.3.0 (2020-01-28) ------------------ * Deprecated support for Python 3.4. * Officially add support for Python 3.8. 1.2.0 (2019-06-28) ------------------ * Support for Python 2.6 and 3.3 has been removed. * Known incompatibilities with Python 3.8 have been resolved. 1.1.0 (2018-04-13) ------------------ * Deprecated support for Python 2.6 and 3.3. * Use the ``sign`` and ``verify`` methods when they are available in ``cryptography`` instead of the deprecated methods ``signer`` and ``verifier``. 1.0.1 (2017-10-25) ------------------ Stop installing mock as part of the default but only as part of the testing dependencies. 1.0.0 (2017-10-13) ------------------- First release after moving the josepy package into a standalone library. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.806066 josepy-1.14.0/CONTRIBUTING.md0000644000000000000000000000431114520452152012264 0ustar00 # Certbot Contributing Guide Hi! Welcome to the Certbot project. We look forward to collaborating with you. If you're reporting a bug in Certbot, please make sure to include: - The version of Certbot you're running. - The operating system you're running it on. - The commands you ran. - What you expected to happen, and - What actually happened. If you're a developer, we have some helpful information in our [Developer's Guide](https://certbot.eff.org/docs/contributing.html) to get you started. In particular, we recommend you read these sections - [Finding issues to work on](https://certbot.eff.org/docs/contributing.html#find-issues-to-work-on) - [Coding style](https://certbot.eff.org/docs/contributing.html#coding-style) - [Submitting a pull request](https://certbot.eff.org/docs/contributing.html#submitting-a-pull-request) # Specific instructions for Josepy ## Configure a development environment 1) Install Poetry: https://python-poetry.org/docs/#installation 2) Setup a Python virtual environment ```bash $ poetry install -E docs ``` 3) Activate the Python virtual environment ```bash # (On Linux) $ source .venv/bin/activate # (On Windows Powershell) $ .\.venv\Script\activate ``` 4) Optionally set up [pre-commit](https://pre-commit.com/) which will cause simple tests to be automatically run on your changes when you commit them ```bash $ pre-commit install ``` ## Run the tests and quality checks 1) Configure a development environment ([see above](#configure-a-development-environment)) 2) Run the tests ```bash $ tox ``` 3) You can also run specific tests ```bash $ tox -e py ``` You can get a listing of the available tests by running ```bash $ tox -l ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8061626 josepy-1.14.0/LICENSE.txt0000644000000000000000000002504214520452152011662 0ustar00 Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.806271 josepy-1.14.0/README.rst0000644000000000000000000000125414520452152011525 0ustar00JOSE protocol implementation in Python using cryptography .. image:: https://github.com/certbot/josepy/actions/workflows/check.yaml/badge.svg :target: https://github.com/certbot/josepy/actions/workflows/check.yaml .. image:: https://readthedocs.org/projects/josepy/badge/?version=latest :target: http://josepy.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black Originally developed as part of the ACME_ protocol implementation. .. _ACME: https://pypi.python.org/pypi/acme To learn how to contribute to this project, see CONTRIBUTING.md_. .. _CONTRIBUTING.md: CONTRIBUTING.md ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.806369 josepy-1.14.0/docs/.gitignore0000644000000000000000000000001114520452152012744 0ustar00/_build/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8064413 josepy-1.14.0/docs/Makefile0000644000000000000000000000113714520452152012426 0ustar00# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = josepy SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8065174 josepy-1.14.0/docs/_static/.gitignore0000644000000000000000000000000014520452152014370 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8065925 josepy-1.14.0/docs/_templates/.gitignore0000644000000000000000000000000014520452152015077 0ustar00././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.806688 josepy-1.14.0/docs/api/base64.rst0000644000000000000000000000010114520452152013343 0ustar00JOSE Base64 ----------- .. automodule:: josepy.b64 :members: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.806741 josepy-1.14.0/docs/api/errors.rst0000644000000000000000000000007214520452152013602 0ustar00Errors ------ .. automodule:: josepy.errors :members: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.806792 josepy-1.14.0/docs/api/interfaces.rst0000644000000000000000000000010614520452152014407 0ustar00Interfaces ---------- .. automodule:: josepy.interfaces :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8068552 josepy-1.14.0/docs/api/json_util.rst0000644000000000000000000000011514520452152014272 0ustar00JSON utilities -------------- .. automodule:: josepy.json_util :members: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.806915 josepy-1.14.0/docs/api/jwa.rst0000644000000000000000000000012114520452152013042 0ustar00JSON Web Algorithms ------------------- .. automodule:: josepy.jwa :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8069696 josepy-1.14.0/docs/api/jwk.rst0000644000000000000000000000010314520452152013054 0ustar00JSON Web Key ------------ .. automodule:: josepy.jwk :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8070254 josepy-1.14.0/docs/api/jws.rst0000644000000000000000000000011714520452152013071 0ustar00JSON Web Signature ------------------ .. automodule:: josepy.jws :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8070726 josepy-1.14.0/docs/api/util.rst0000644000000000000000000000007614520452152013247 0ustar00Utilities --------- .. automodule:: josepy.util :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8071172 josepy-1.14.0/docs/changelog.rst0000644000000000000000000000003614520452152013444 0ustar00.. include:: ../CHANGELOG.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8338335 josepy-1.14.0/docs/conf.py0000644000000000000000000001324714520452152012272 0ustar00# -*- coding: utf-8 -*- # # josepy documentation build configuration file, created by # sphinx-quickstart on Wed Oct 11 17:05:53 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.viewcode", ] autodoc_member_order = "bysource" autodoc_default_flags = ["show-inheritance", "private-members"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "josepy" copyright = "2015-2017, Let's Encrypt Project" author = "Let's Encrypt Project" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'1.14' # The full version, including alpha/beta/rc tags. release = u'1.14.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # The reST default role (used for this markup: `text`) to use for all # documents. default_role = "py:obj" # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { "**": [ "about.html", "navigation.html", "relations.html", # needs 'show_related': True theme option to display "searchbox.html", "donate.html", ] } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = "josepydoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "josepy.tex", "josepy Documentation", "Let's Encrypt Project", "manual"), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "josepy", "josepy Documentation", [author], 1), ("man/jws", "jws", "jws script documentation", [project], 1), ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "josepy", "josepy Documentation", author, "josepy", "One line description of project.", "Miscellaneous", ), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "https://docs.python.org/": None, "https://cryptography.io/en/latest/": None, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8073084 josepy-1.14.0/docs/index.rst0000644000000000000000000000034614520452152012630 0ustar00josepy ====== .. automodule:: josepy :members: .. toctree:: :maxdepth: 2 :caption: Contents: :glob: api/* changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.807372 josepy-1.14.0/docs/jws-help.txt0000644000000000000000000000024714520452152013261 0ustar00usage: jws [-h] [--compact] {sign,verify} ... positional arguments: {sign,verify} optional arguments: -h, --help show this help message and exit --compact ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8074582 josepy-1.14.0/docs/man/jws.rst0000644000000000000000000000004414520452152013072 0ustar00.. literalinclude:: ../jws-help.txt ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8075662 josepy-1.14.0/docs/requirements.txt0000644000000000000000000000015514520452152014251 0ustar00# We pin our dependencies with a constraints file for increased stability. -c ../constraints.txt -e .[docs] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845802.032093 josepy-1.14.0/pyproject.toml0000644000000000000000000000656114520452152012760 0ustar00# PEP-517 build [build-system] requires = ["poetry_core>=1.0.8"] build-backend = "poetry.core.masonry.api" # Poetry tooling configuration [tool.poetry] name = "josepy" version = "1.14.0" description = "JOSE protocol implementation in Python" license = "Apache License 2.0" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Internet :: WWW/HTTP", "Topic :: Security", ] homepage = "https://github.com/certbot/josepy" authors = ["Certbot Project "] readme = "README.rst" include = [ "CHANGELOG.rst", "CONTRIBUTING.md", "docs", "tests", ] [tool.poetry.dependencies] # This should be kept in sync with the value of target-version in our # configuration for black below. python = "^3.7" # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) # add sign() and verify() to asymetric keys (RSA >=1.4, ECDSA >=1.5) cryptography = ">=1.5" # Connection.set_tlsext_host_name (>=0.13) pyopenssl = ">=0.13" # >=4.3.0 is needed for Python 3.10 support sphinx = {version = ">=4.3.0", optional = true} sphinx-rtd-theme = {version = ">=1.0", optional = true} [tool.poetry.dev-dependencies] # coverage[toml] extra is required to read the coverage config from pyproject.toml coverage = {version = ">=4.0", extras = ["toml"]} # importlib_resources 1.3 was the version included in Python 3.9 which # introduced the functionality we are using. See # https://github.com/python/importlib_resources/tree/7f4fbb5ee026d7610636d5ece18b09c64aa0c893#compatibility. importlib_resources = {version = ">=1.3", python = "<3.9"} mypy = "*" types-pyOpenSSL = "*" types-pyRFC3339 = "*" types-requests = "*" types-setuptools = "*" typing-extensions = "*" pre-commit = "*" pytest = ">=2.8.0" pytest-cov = "*" tox = "*" twine = "*" [tool.poetry.extras] docs = [ "sphinx", "sphinx-rtd-theme", ] [tool.poetry.scripts] jws = "josepy.jws:CLI.run" # Black tooling configuration [tool.black] line-length = 100 # This should be kept in sync with the version of Python specified in poetry's # dependencies above. target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312'] # Mypy tooling configuration [tool.mypy] ignore_missing_imports = true warn_unused_ignores = true show_error_codes = true disallow_untyped_defs = true # Pytest tooling configuration [tool.pytest.ini_options] # We also ignore our own deprecation warning about dropping Python 3.7 support. filterwarnings = ["error", "ignore:Python 3.7 support will be dropped:DeprecationWarning"] norecursedirs = "*.egg .eggs dist build docs .tox" # Isort tooling configuration [tool.isort] combine_as_imports = false default_section = "THIRDPARTY" known_first_party = "josepy" line_length = 79 profile = "black" # Coverage tooling configuration [tool.coverage.run] branch = true source = ["josepy"] [tool.coverage.paths] source = [ ".tox/*/lib/python*/site-packages/josepy", ".tox/pypy*/site-packages/josepy", ] [tool.coverage.report] show_missing = true ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.808747 josepy-1.14.0/src/josepy/__init__.py0000644000000000000000000000364014520452152014250 0ustar00"""Javascript Object Signing and Encryption (JOSE). This package is a Python implementation of the standards developed by IETF `Javascript Object Signing and Encryption (Active WG)`_, in particular the following RFCs: - `JSON Web Algorithms (JWA)`_ - `JSON Web Key (JWK)`_ - `JSON Web Signature (JWS)`_ Originally developed as part of the ACME_ protocol implementation. .. _`Javascript Object Signing and Encryption (Active WG)`: https://tools.ietf.org/wg/jose/ .. _`JSON Web Algorithms (JWA)`: https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-algorithms/ .. _`JSON Web Key (JWK)`: https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-key/ .. _`JSON Web Signature (JWS)`: https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-signature/ .. _ACME: https://pypi.python.org/pypi/acme """ import sys import warnings # flake8: noqa from josepy.b64 import b64decode, b64encode from josepy.errors import ( DeserializationError, Error, SerializationError, UnrecognizedTypeError, ) from josepy.interfaces import JSONDeSerializable from josepy.json_util import ( Field, JSONObjectWithFields, TypedJSONObjectWithFields, decode_b64jose, decode_cert, decode_csr, decode_hex16, encode_b64jose, encode_cert, encode_csr, encode_hex16, field, ) from josepy.jwa import ( ES256, ES384, ES512, HS256, HS384, HS512, PS256, PS384, PS512, RS256, RS384, RS512, JWASignature, ) from josepy.jwk import JWK, JWKEC, JWKRSA, JWKOct from josepy.jws import JWS, Header, Signature from josepy.util import ( ComparableECKey, ComparableKey, ComparableRSAKey, ComparableX509, ImmutableMap, ) if sys.version_info[:2] == (3, 7): warnings.warn( "Python 3.7 support will be dropped in the next scheduled release of " "josepy. Please upgrade your Python version.", DeprecationWarning, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8088791 josepy-1.14.0/src/josepy/b64.py0000644000000000000000000000272114520452152013103 0ustar00"""`JOSE Base64`_ is defined as: - URL-safe Base64 - padding stripped .. _`JOSE Base64`: https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-37#appendix-C .. Do NOT try to call this module "base64", as it will "shadow" the standard library. """ import base64 from typing import Union def b64encode(data: bytes) -> bytes: """JOSE Base64 encode. :param data: Data to be encoded. :type data: bytes :returns: JOSE Base64 string. :rtype: bytes :raises TypeError: if ``data`` is of incorrect type """ if not isinstance(data, bytes): raise TypeError("argument should be bytes") return base64.urlsafe_b64encode(data).rstrip(b"=") def b64decode(data: Union[bytes, str]) -> bytes: """JOSE Base64 decode. :param data: Base64 string to be decoded. If it's unicode, then only ASCII characters are allowed. :type data: bytes or unicode :returns: Decoded data. :rtype: bytes :raises TypeError: if input is of incorrect type :raises ValueError: if input is unicode with non-ASCII characters """ if isinstance(data, str): try: data = data.encode("ascii") except UnicodeEncodeError: raise ValueError("unicode argument should contain only ASCII characters") elif not isinstance(data, bytes): raise TypeError("argument should be a str or unicode") return base64.urlsafe_b64decode(data + b"=" * (4 - (len(data) % 4))) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8089967 josepy-1.14.0/src/josepy/errors.py0000644000000000000000000000142714520452152014026 0ustar00"""JOSE errors.""" from typing import Any class Error(Exception): """Generic JOSE Error.""" class DeserializationError(Error): """JSON deserialization error.""" def __str__(self) -> str: return "Deserialization error: {0}".format(super().__str__()) class SerializationError(Error): """JSON serialization error.""" class UnrecognizedTypeError(DeserializationError): """Unrecognized type error. :ivar str typ: The unrecognized type of the JSON object. :ivar jobj: Full JSON object. """ def __init__(self, typ: str, jobj: Any) -> None: self.typ = typ self.jobj = jobj super().__init__(str(self)) def __str__(self) -> str: return "{0} was not recognized, full message: {1}".format(self.typ, self.jobj) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8091414 josepy-1.14.0/src/josepy/interfaces.py0000644000000000000000000001715614520452152014643 0ustar00"""JOSE interfaces.""" import abc import json from collections.abc import Mapping, Sequence from typing import Any, Type, TypeVar, Union from josepy import errors GenericJSONDeSerializable = TypeVar("GenericJSONDeSerializable", bound="JSONDeSerializable") class JSONDeSerializable(metaclass=abc.ABCMeta): """Interface for (de)serializable JSON objects. Please recall, that standard Python library implements :class:`json.JSONEncoder` and :class:`json.JSONDecoder` that perform translations based on respective :ref:`conversion tables ` that look pretty much like the one below (for complete tables see relevant Python documentation): .. _conversion-table: ====== ====== JSON Python ====== ====== object dict ... ... ====== ====== While the above **conversion table** is about translation of JSON documents to/from the basic Python types only, :class:`JSONDeSerializable` introduces the following two concepts: serialization Turning an arbitrary Python object into Python object that can be encoded into a JSON document. **Full serialization** produces a Python object composed of only basic types as required by the :ref:`conversion table `. **Partial serialization** (accomplished by :meth:`to_partial_json`) produces a Python object that might also be built from other :class:`JSONDeSerializable` objects. deserialization Turning a decoded Python object (necessarily one of the basic types as required by the :ref:`conversion table `) into an arbitrary Python object. Serialization produces **serialized object** ("partially serialized object" or "fully serialized object" for partial and full serialization respectively) and deserialization produces **deserialized object**, both usually denoted in the source code as ``jobj``. Wording in the official Python documentation might be confusing after reading the above, but in the light of those definitions, one can view :meth:`json.JSONDecoder.decode` as decoder and deserializer of basic types, :meth:`json.JSONEncoder.default` as serializer of basic types, :meth:`json.JSONEncoder.encode` as serializer and encoder of basic types. One could extend :mod:`json` to support arbitrary object (de)serialization either by: - overriding :meth:`json.JSONDecoder.decode` and :meth:`json.JSONEncoder.default` in subclasses - or passing ``object_hook`` argument (or ``object_hook_pairs``) to :func:`json.load`/:func:`json.loads` or ``default`` argument for :func:`json.dump`/:func:`json.dumps`. Interestingly, ``default`` is required to perform only partial serialization, as :func:`json.dumps` applies ``default`` recursively. This is the idea behind making :meth:`to_partial_json` produce only partial serialization, while providing custom :meth:`json_dumps` that dumps with ``default`` set to :meth:`json_dump_default`. To make further documentation a bit more concrete, please, consider the following imaginatory implementation example:: class Foo(JSONDeSerializable): def to_partial_json(self): return 'foo' @classmethod def from_json(cls, jobj): return Foo() class Bar(JSONDeSerializable): def to_partial_json(self): return [Foo(), Foo()] @classmethod def from_json(cls, jobj): return Bar() """ @abc.abstractmethod def to_partial_json(self) -> Any: # pragma: no cover """Partially serialize. Following the example, **partial serialization** means the following:: assert isinstance(Bar().to_partial_json()[0], Foo) assert isinstance(Bar().to_partial_json()[1], Foo) # in particular... assert Bar().to_partial_json() != ['foo', 'foo'] :raises josepy.errors.SerializationError: in case of any serialization error. :returns: Partially serializable object. """ raise NotImplementedError() def to_json(self) -> Any: """Fully serialize. Again, following the example from before, **full serialization** means the following:: assert Bar().to_json() == ['foo', 'foo'] :raises josepy.errors.SerializationError: in case of any serialization error. :returns: Fully serialized object. """ def _serialize(obj: Any) -> Any: if isinstance(obj, JSONDeSerializable): return _serialize(obj.to_partial_json()) if isinstance(obj, str): # strings are Sequence return obj elif isinstance(obj, list): return [_serialize(subobj) for subobj in obj] elif isinstance(obj, Sequence): # default to tuple, otherwise Mapping could get # unhashable list return tuple(_serialize(subobj) for subobj in obj) elif isinstance(obj, Mapping): return {_serialize(key): _serialize(value) for key, value in obj.items()} else: return obj return _serialize(self) @classmethod @abc.abstractmethod def from_json(cls: Type[GenericJSONDeSerializable], jobj: Any) -> GenericJSONDeSerializable: """Deserialize a decoded JSON document. :param jobj: Python object, composed of only other basic data types, as decoded from JSON document. Not necessarily :class:`dict` (as decoded from "JSON object" document). :raises josepy.errors.DeserializationError: if decoding was unsuccessful, e.g. in case of unparseable X509 certificate, or wrong padding in JOSE base64 encoded string, etc. """ # TypeError: Can't instantiate abstract class with # abstract methods from_json, to_partial_json return cls() @classmethod def json_loads( cls: Type[GenericJSONDeSerializable], json_string: Union[str, bytes] ) -> GenericJSONDeSerializable: """Deserialize from JSON document string.""" try: loads = json.loads(json_string) except ValueError as error: raise errors.DeserializationError(error) return cls.from_json(loads) def json_dumps(self, **kwargs: Any) -> str: """Dump to JSON string using proper serializer. :returns: JSON document string. :rtype: str """ return json.dumps(self, default=self.json_dump_default, **kwargs) def json_dumps_pretty(self) -> str: """Dump the object to pretty JSON document string. :rtype: str """ return self.json_dumps(sort_keys=True, indent=4, separators=(",", ": ")) @classmethod def json_dump_default(cls, python_object: "JSONDeSerializable") -> Any: """Serialize Python object. This function is meant to be passed as ``default`` to :func:`json.dump` or :func:`json.dumps`. They call ``default(python_object)`` only for non-basic Python types, so this function necessarily raises :class:`TypeError` if ``python_object`` is not an instance of :class:`IJSONSerializable`. Please read the class docstring for more information. """ if isinstance(python_object, JSONDeSerializable): return python_object.to_partial_json() else: # this branch is necessary, cannot just "return" raise TypeError(repr(python_object) + " is not JSON serializable") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8093278 josepy-1.14.0/src/josepy/json_util.py0000644000000000000000000004416414520452152014525 0ustar00"""JSON (de)serialization framework. The framework presented here is somewhat based on `Go's "json" package`_ (especially the ``omitempty`` functionality). .. _`Go's "json" package`: http://golang.org/pkg/encoding/json/ """ import abc import binascii import logging from typing import ( Any, Callable, Dict, Iterable, List, Mapping, Optional, Type, TypeVar, ) from OpenSSL import crypto from josepy import b64, errors, interfaces, util logger = logging.getLogger(__name__) def field( json_name: str, default: Any = None, omitempty: bool = False, decoder: Optional[Callable[[Any], Any]] = None, encoder: Optional[Callable[[Any], Any]] = None, ) -> Any: """Convenient function to declare a :class:`Field` with proper type annotations. This function allows to write the following code: import josepy class JSON(josepy.JSONObjectWithFields): typ: str = josepy.field('type') def other_type(self) -> str: return self.typ """ return _TypedField( json_name=json_name, default=default, omitempty=omitempty, decoder=decoder, encoder=encoder ) class Field: """JSON object field. :class:`Field` is meant to be used together with :class:`JSONObjectWithFields`. ``encoder`` (``decoder``) is a callable that accepts a single parameter, i.e. a value to be encoded (decoded), and returns the serialized (deserialized) value. In case of errors it should raise :class:`~josepy.errors.SerializationError` (:class:`~josepy.errors.DeserializationError`). Note, that ``decoder`` should perform partial serialization only. :ivar str json_name: Name of the field when encoded to JSON. :ivar default: Default value (used when not present in JSON object). :ivar bool omitempty: If ``True`` and the field value is empty, then it will not be included in the serialized JSON object, and ``default`` will be used for deserialization. Otherwise, if ``False``, field is considered as required, value will always be included in the serialized JSON objected, and it must also be present when deserializing. """ __slots__ = ("json_name", "default", "omitempty", "fdec", "fenc") def __init__( self, json_name: str, default: Any = None, omitempty: bool = False, decoder: Optional[Callable[[Any], Any]] = None, encoder: Optional[Callable[[Any], Any]] = None, ) -> None: self.json_name = json_name self.default = default self.omitempty = omitempty self.fdec = self.default_decoder if decoder is None else decoder self.fenc = self.default_encoder if encoder is None else encoder @classmethod def _empty(cls, value: Any) -> bool: """Is the provided value considered "empty" for this field? This is useful for subclasses that might want to override the definition of being empty, e.g. for some more exotic data types. """ return not isinstance(value, bool) and not value def omit(self, value: Any) -> bool: """Omit the value in output?""" return self._empty(value) and self.omitempty def _update_params(self, **kwargs: Any) -> "Field": current = { "json_name": self.json_name, "default": self.default, "omitempty": self.omitempty, "decoder": self.fdec, "encoder": self.fenc, **kwargs, } return type(self)(**current) def decoder(self, fdec: Callable[[Any], Any]) -> "Field": """Descriptor to change the decoder on JSON object field.""" return self._update_params(decoder=fdec) def encoder(self, fenc: Callable[[Any], Any]) -> "Field": """Descriptor to change the encoder on JSON object field.""" return self._update_params(encoder=fenc) def decode(self, value: Any) -> Any: """Decode a value, optionally with context JSON object.""" return self.fdec(value) def encode(self, value: Any) -> Any: """Encode a value, optionally with context JSON object.""" return self.fenc(value) @classmethod def default_decoder(cls, value: Any) -> Any: """Default decoder. Recursively deserialize into immutable types ( :class:`josepy.util.frozendict` instead of :func:`dict`, :func:`tuple` instead of :func:`list`). """ # bases cases for different types returned by json.loads if isinstance(value, list): return tuple(cls.default_decoder(subvalue) for subvalue in value) elif isinstance(value, dict): return util.frozendict( { cls.default_decoder(key): cls.default_decoder(value) for key, value in value.items() } ) else: # integer or string return value @classmethod def default_encoder(cls, value: Any) -> Any: """Default (passthrough) encoder.""" # field.to_partial_json() is no good as encoder has to do partial # serialization only return value class _TypedField(Field): """Specialized class to mark a JSON object field with typed annotations. This class is kept private because fields are supposed to be declared using the :function:`field` in this situation. In the future the :class:`Field` may be removed in favor of this one.""" class JSONObjectWithFieldsMeta(abc.ABCMeta): """Metaclass for :class:`JSONObjectWithFields` and its subclasses. It makes sure that, for any class ``cls`` with ``__metaclass__`` set to ``JSONObjectWithFieldsMeta``: 1. All fields (attributes of type :class:`Field`) in the class definition are moved to the ``cls._fields`` dictionary, where keys are field attribute names and values are fields themselves. 2. ``cls.__slots__`` is extended by all field attribute names (i.e. not :attr:`Field.json_name`). Original ``cls.__slots__`` are stored in ``cls._orig_slots``. In a consequence, for a field attribute name ``some_field``, ``cls.some_field`` will be a slot descriptor and not an instance of :class:`Field`. For example:: some_field = Field('someField', default=()) class Foo: __metaclass__ = JSONObjectWithFieldsMeta __slots__ = ('baz',) some_field = some_field assert Foo.__slots__ == ('some_field', 'baz') assert Foo._orig_slots == () assert Foo.some_field is not Field assert Foo._fields.keys() == ['some_field'] assert Foo._fields['some_field'] is some_field As an implementation note, this metaclass inherits from :class:`abc.ABCMeta` (and not the usual :class:`type`) to mitigate the metaclass conflict (:class:`ImmutableMap` and :class:`JSONDeSerializable`, parents of :class:`JSONObjectWithFields`, use :class:`abc.ABCMeta` as its metaclass). """ _fields: Dict[str, Field] = {} _orig_slots: Iterable[str] def __new__( mcs, name: str, bases: List[str], namespace: Dict[str, Any] ) -> "JSONObjectWithFieldsMeta": fields: Dict[str, Field] = {} for base in bases: fields.update(getattr(base, "_fields", {})) # Do not reorder, this class might override fields from base classes! # We use a copy of namespace in the loop because the loop modifies it. for key, value in namespace.copy().items(): if isinstance(value, Field): if isinstance(value, _TypedField): # Ensure the type annotation has been set for the field. # Error out if it is not the case. if key not in namespace.get("__annotations__", {}): raise ValueError( f"Field `{key}` in JSONObject `{name}` has no type annotation." ) fields[key] = namespace.pop(key) namespace["_orig_slots"] = namespace.get("__slots__", ()) namespace["__slots__"] = tuple(list(namespace["_orig_slots"]) + list(fields.keys())) namespace["_fields"] = fields return abc.ABCMeta.__new__(mcs, name, bases, namespace) # type: ignore[arg-type] GenericJSONObjectWithFields = TypeVar("GenericJSONObjectWithFields", bound="JSONObjectWithFields") class JSONObjectWithFields( util.ImmutableMap, interfaces.JSONDeSerializable, metaclass=JSONObjectWithFieldsMeta ): """JSON object with fields. Example:: class Foo(JSONObjectWithFields): bar = Field('Bar') empty = Field('Empty', omitempty=True) @bar.encoder def bar(value): return value + 'bar' @bar.decoder def bar(value): if not value.endswith('bar'): raise errors.DeserializationError('No bar suffix!') return value[:-3] assert Foo(bar='baz').to_partial_json() == {'Bar': 'bazbar'} assert Foo.from_json({'Bar': 'bazbar'}) == Foo(bar='baz') assert (Foo.from_json({'Bar': 'bazbar', 'Empty': '!'}) == Foo(bar='baz', empty='!')) assert Foo(bar='baz').bar == 'baz' """ @classmethod def _defaults(cls) -> Dict[str, Any]: """Get default fields values.""" return {slot: field.default for slot, field in cls._fields.items()} def __init__(self, **kwargs: Any) -> None: super().__init__(**{**self._defaults(), **kwargs}) def encode(self, name: str) -> Any: """Encode a single field. :param str name: Name of the field to be encoded. :raises errors.SerializationError: if field cannot be serialized :raises errors.Error: if field could not be found """ try: field = self._fields[name] # type: ignore[attr-defined] except KeyError: raise errors.Error("Field not found: {0}".format(name)) return field.encode(getattr(self, name)) def fields_to_partial_json(self) -> Dict[str, Any]: """Serialize fields to JSON.""" jobj = {} omitted = set() for slot, field in self._fields.items(): # type: ignore[attr-defined] value = getattr(self, slot) if field.omit(value): omitted.add((slot, value)) else: try: jobj[field.json_name] = field.encode(value) except errors.SerializationError as error: raise errors.SerializationError( "Could not encode {0} ({1}): {2}".format(slot, value, error) ) return jobj def to_partial_json(self) -> Dict[str, Any]: return self.fields_to_partial_json() @classmethod def _check_required(cls, jobj: Mapping[str, Any]) -> None: missing = set() for _, field in cls._fields.items(): if not field.omitempty and field.json_name not in jobj: missing.add(field.json_name) if missing: raise errors.DeserializationError( "The following fields are required: {0}".format(",".join(missing)) ) @classmethod def fields_from_json(cls, jobj: Mapping[str, Any]) -> Any: """Deserialize fields from JSON.""" cls._check_required(jobj) fields = {} for slot, field in cls._fields.items(): if field.json_name not in jobj and field.omitempty: fields[slot] = field.default else: value = jobj[field.json_name] try: fields[slot] = field.decode(value) except errors.DeserializationError as error: raise errors.DeserializationError( "Could not decode {0!r} ({1!r}): {2}".format(slot, value, error) ) return fields @classmethod def from_json( cls: Type[GenericJSONObjectWithFields], jobj: Mapping[str, Any] ) -> GenericJSONObjectWithFields: return cls(**cls.fields_from_json(jobj)) def encode_b64jose(data: bytes) -> str: """Encode JOSE Base-64 field. :param bytes data: :rtype: `str` """ # b64encode produces ASCII characters only return b64.b64encode(data).decode("ascii") def decode_b64jose(data: str, size: Optional[int] = None, minimum: bool = False) -> bytes: """Decode JOSE Base-64 field. :param unicode data: :param int size: Required length (after decoding). :param bool minimum: If ``True``, then `size` will be treated as minimum required length, as opposed to exact equality. :rtype: bytes """ try: decoded = b64.b64decode(data.encode()) except binascii.Error as error: raise errors.DeserializationError(error) if size is not None and ( (not minimum and len(decoded) != size) or (minimum and len(decoded) < size) ): raise errors.DeserializationError("Expected at least or exactly {0} bytes".format(size)) return decoded def encode_hex16(value: bytes) -> str: """Hexlify. :param bytes value: :rtype: unicode """ return binascii.hexlify(value).decode() def decode_hex16(value: str, size: Optional[int] = None, minimum: bool = False) -> bytes: """Decode hexlified field. :param unicode value: :param int size: Required length (after decoding). :param bool minimum: If ``True``, then `size` will be treated as minimum required length, as opposed to exact equality. :rtype: bytes """ value_b = value.encode() if size is not None and ( (not minimum and len(value_b) != size * 2) or (minimum and len(value_b) < size * 2) ): raise errors.DeserializationError() try: return binascii.unhexlify(value_b) except binascii.Error as error: raise errors.DeserializationError(error) def encode_cert(cert: util.ComparableX509) -> str: """Encode certificate as JOSE Base-64 DER. :type cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :rtype: unicode """ if isinstance(cert.wrapped, crypto.X509Req): raise ValueError("Error input is actually a certificate request.") return encode_b64jose(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped)) def decode_cert(b64der: str) -> util.ComparableX509: """Decode JOSE Base-64 DER-encoded certificate. :param unicode b64der: :rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ try: return util.ComparableX509( crypto.load_certificate(crypto.FILETYPE_ASN1, decode_b64jose(b64der)) ) except crypto.Error as error: raise errors.DeserializationError(error) def encode_csr(csr: util.ComparableX509) -> str: """Encode CSR as JOSE Base-64 DER. :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` :rtype: unicode """ if isinstance(csr.wrapped, crypto.X509): raise ValueError("Error input is actually a certificate.") return encode_b64jose(crypto.dump_certificate_request(crypto.FILETYPE_ASN1, csr.wrapped)) def decode_csr(b64der: str) -> util.ComparableX509: """Decode JOSE Base-64 DER-encoded CSR. :param unicode b64der: :rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` """ try: return util.ComparableX509( crypto.load_certificate_request(crypto.FILETYPE_ASN1, decode_b64jose(b64der)) ) except crypto.Error as error: raise errors.DeserializationError(error) GenericTypedJSONObjectWithFields = TypeVar( "GenericTypedJSONObjectWithFields", bound="TypedJSONObjectWithFields" ) class TypedJSONObjectWithFields(JSONObjectWithFields): """JSON object with type.""" typ: str = NotImplemented """Type of the object. Subclasses must override.""" type_field_name: str = "type" """Field name used to distinguish different object types. Subclasses will probably have to override this. """ TYPES: Dict[str, Type] = NotImplemented """Types registered for JSON deserialization""" @classmethod def register( cls, type_cls: Type[GenericTypedJSONObjectWithFields], typ: Optional[str] = None ) -> Type[GenericTypedJSONObjectWithFields]: """Register class for JSON deserialization.""" typ = type_cls.typ if typ is None else typ cls.TYPES[typ] = type_cls return type_cls @classmethod def get_type_cls(cls, jobj: Mapping[str, Any]) -> Type["TypedJSONObjectWithFields"]: """Get the registered class for ``jobj``.""" if cls in cls.TYPES.values(): if cls.type_field_name not in jobj: raise errors.DeserializationError( "Missing type field ({0})".format(cls.type_field_name) ) # cls is already registered type_cls, force to use it # so that, e.g Revocation.from_json(jobj) fails if # jobj["type"] != "revocation". return cls if not isinstance(jobj, dict): raise errors.DeserializationError("{0} is not a dictionary object".format(jobj)) try: typ = jobj[cls.type_field_name] except KeyError: raise errors.DeserializationError("missing type field") try: return cls.TYPES[typ] except KeyError: raise errors.UnrecognizedTypeError(typ, jobj) def to_partial_json(self) -> Dict[str, Any]: """Get JSON serializable object. :returns: Serializable JSON object representing ACME typed object. :meth:`validate` will almost certainly not work, due to reasons explained in :class:`josepy.interfaces.IJSONSerializable`. :rtype: dict """ jobj = self.fields_to_partial_json() jobj[self.type_field_name] = self.typ return jobj @classmethod def from_json(cls, jobj: Mapping[str, Any]) -> "TypedJSONObjectWithFields": """Deserialize ACME object from valid JSON object. :raises josepy.errors.UnrecognizedTypeError: if type of the ACME object has not been registered. """ # make sure subclasses don't cause infinite recursive from_json calls type_cls = cls.get_type_cls(jobj) return type_cls(**type_cls.fields_from_json(jobj)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8094788 josepy-1.14.0/src/josepy/jwa.py0000644000000000000000000001647414520452152013303 0ustar00"""JSON Web Algorithms. https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 """ import abc import logging from collections.abc import Hashable from typing import Any, Callable, Dict import cryptography.exceptions from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, hmac from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa from cryptography.hazmat.primitives.asymmetric.utils import ( decode_dss_signature, encode_dss_signature, ) from cryptography.hazmat.primitives.hashes import HashAlgorithm from josepy import errors, interfaces, jwk logger = logging.getLogger(__name__) class JWA(interfaces.JSONDeSerializable): # for some reason disable=abstract-method has to be on the line # above... """JSON Web Algorithm.""" class JWASignature(JWA, Hashable): """Base class for JSON Web Signature Algorithms.""" SIGNATURES: Dict[str, "JWASignature"] = {} kty: Any def __init__(self, name: str) -> None: self.name = name def __eq__(self, other: Any) -> bool: if not isinstance(other, JWASignature): return NotImplemented return self.name == other.name def __hash__(self) -> int: return hash((self.__class__, self.name)) @classmethod def register(cls, signature_cls: "JWASignature") -> "JWASignature": """Register class for JSON deserialization.""" cls.SIGNATURES[signature_cls.name] = signature_cls return signature_cls def to_partial_json(self) -> Any: return self.name @classmethod def from_json(cls, jobj: Any) -> "JWASignature": return cls.SIGNATURES[jobj] @abc.abstractmethod def sign(self, key: Any, msg: bytes) -> bytes: # pragma: no cover """Sign the ``msg`` using ``key``.""" raise NotImplementedError() @abc.abstractmethod def verify(self, key: Any, msg: bytes, sig: bytes) -> bool: # pragma: no cover """Verify the ``msg`` and ``sig`` using ``key``.""" raise NotImplementedError() def __repr__(self) -> str: return self.name class _JWAHS(JWASignature): kty = jwk.JWKOct def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]): super().__init__(name) self.hash = hash_() def sign(self, key: bytes, msg: bytes) -> bytes: signer = hmac.HMAC(key, self.hash, backend=default_backend()) signer.update(msg) return signer.finalize() def verify(self, key: bytes, msg: bytes, sig: bytes) -> bool: verifier = hmac.HMAC(key, self.hash, backend=default_backend()) verifier.update(msg) try: verifier.verify(sig) except cryptography.exceptions.InvalidSignature as error: logger.debug(error, exc_info=True) return False else: return True class _JWARSA: kty = jwk.JWKRSA padding: Any = NotImplemented hash: HashAlgorithm = NotImplemented def sign(self, key: rsa.RSAPrivateKey, msg: bytes) -> bytes: """Sign the ``msg`` using ``key``.""" try: return key.sign(msg, self.padding, self.hash) except AttributeError as error: logger.debug(error, exc_info=True) raise errors.Error("Public key cannot be used for signing") except ValueError as error: # digest too large logger.debug(error, exc_info=True) raise errors.Error(str(error)) def verify(self, key: rsa.RSAPublicKey, msg: bytes, sig: bytes) -> bool: """Verify the ``msg` and ``sig`` using ``key``.""" try: key.verify(sig, msg, self.padding, self.hash) except cryptography.exceptions.InvalidSignature as error: logger.debug(error, exc_info=True) return False else: return True class _JWARS(_JWARSA, JWASignature): def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]) -> None: super().__init__(name) self.padding = padding.PKCS1v15() self.hash = hash_() class _JWAPS(_JWARSA, JWASignature): def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]) -> None: super().__init__(name) self.padding = padding.PSS(mgf=padding.MGF1(hash_()), salt_length=padding.PSS.MAX_LENGTH) self.hash = hash_() class _JWAEC(JWASignature): kty = jwk.JWKEC def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]): super().__init__(name) self.hash = hash_() def sign(self, key: ec.EllipticCurvePrivateKey, msg: bytes) -> bytes: """Sign the ``msg`` using ``key``.""" sig = self._sign(key, msg) dr, ds = decode_dss_signature(sig) length = jwk.JWKEC.expected_length_for_curve(key.curve) return dr.to_bytes(length=length, byteorder="big") + ds.to_bytes( length=length, byteorder="big" ) def _sign(self, key: ec.EllipticCurvePrivateKey, msg: bytes) -> bytes: try: return key.sign(msg, ec.ECDSA(self.hash)) except AttributeError as error: logger.debug(error, exc_info=True) raise errors.Error("Public key cannot be used for signing") except ValueError as error: # digest too large logger.debug(error, exc_info=True) raise errors.Error(str(error)) def verify(self, key: ec.EllipticCurvePublicKey, msg: bytes, sig: bytes) -> bool: """Verify the ``msg` and ``sig`` using ``key``.""" rlen = jwk.JWKEC.expected_length_for_curve(key.curve) if len(sig) != 2 * rlen: # Format error - rfc7518 - 3.4 … MUST NOT be shortened to omit any leading zero octets return False asn1sig = encode_dss_signature( int.from_bytes(sig[0:rlen], byteorder="big"), int.from_bytes(sig[rlen:], byteorder="big"), ) return self._verify(key, msg, asn1sig) def _verify(self, key: ec.EllipticCurvePublicKey, msg: bytes, asn1sig: bytes) -> bool: try: key.verify(asn1sig, msg, ec.ECDSA(self.hash)) except cryptography.exceptions.InvalidSignature as error: logger.debug(error, exc_info=True) return False else: return True #: HMAC using SHA-256 HS256 = JWASignature.register(_JWAHS("HS256", hashes.SHA256)) #: HMAC using SHA-384 HS384 = JWASignature.register(_JWAHS("HS384", hashes.SHA384)) #: HMAC using SHA-512 HS512 = JWASignature.register(_JWAHS("HS512", hashes.SHA512)) #: RSASSA-PKCS-v1_5 using SHA-256 RS256 = JWASignature.register(_JWARS("RS256", hashes.SHA256)) #: RSASSA-PKCS-v1_5 using SHA-384 RS384 = JWASignature.register(_JWARS("RS384", hashes.SHA384)) #: RSASSA-PKCS-v1_5 using SHA-512 RS512 = JWASignature.register(_JWARS("RS512", hashes.SHA512)) #: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 PS256 = JWASignature.register(_JWAPS("PS256", hashes.SHA256)) #: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 PS384 = JWASignature.register(_JWAPS("PS384", hashes.SHA384)) #: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 PS512 = JWASignature.register(_JWAPS("PS512", hashes.SHA512)) #: ECDSA using P-256 and SHA-256 ES256 = JWASignature.register(_JWAEC("ES256", hashes.SHA256)) #: ECDSA using P-384 and SHA-384 ES384 = JWASignature.register(_JWAEC("ES384", hashes.SHA384)) #: ECDSA using P-521 and SHA-512 ES512 = JWASignature.register(_JWAEC("ES512", hashes.SHA512)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8096414 josepy-1.14.0/src/josepy/jwk.py0000644000000000000000000003324114520452152013304 0ustar00"""JSON Web Key.""" import abc import json import logging import math from typing import ( Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, Union, ) import cryptography.exceptions from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa import josepy.util from josepy import errors, json_util, util logger = logging.getLogger(__name__) class JWK(json_util.TypedJSONObjectWithFields, metaclass=abc.ABCMeta): """JSON Web Key.""" type_field_name = "kty" TYPES: Dict[str, Type["JWK"]] = {} cryptography_key_types: Tuple[Type[Any], ...] = () """Subclasses should override.""" required: Sequence[str] = NotImplemented """Required members of public key's representation as defined by JWK/JWA.""" _thumbprint_json_dumps_params: Dict[str, Union[Optional[int], Sequence[str], bool]] = { # "no whitespace or line breaks before or after any syntactic # elements" "indent": None, "separators": (",", ":"), # "members ordered lexicographically by the Unicode [UNICODE] # code points of the member names" "sort_keys": True, } key: Any def thumbprint( self, hash_function: Callable[[], hashes.HashAlgorithm] = hashes.SHA256 ) -> bytes: """Compute JWK Thumbprint. https://tools.ietf.org/html/rfc7638 :returns: bytes """ digest = hashes.Hash(hash_function(), backend=default_backend()) digest.update( json.dumps( {k: v for k, v in self.to_json().items() if k in self.required}, **self._thumbprint_json_dumps_params, # type: ignore[arg-type] ).encode() ) return digest.finalize() @abc.abstractmethod def public_key(self) -> "JWK": # pragma: no cover """Generate JWK with public key. For symmetric cryptosystems, this would return ``self``. """ raise NotImplementedError() @classmethod def _load_cryptography_key( cls, data: bytes, password: Optional[bytes] = None, backend: Optional[Any] = None ) -> Any: backend = default_backend() if backend is None else backend exceptions = {} # private key? loader_private: Any for loader_private in ( serialization.load_pem_private_key, serialization.load_der_private_key, ): try: return loader_private(data, password, backend) except (ValueError, TypeError, cryptography.exceptions.UnsupportedAlgorithm) as error: exceptions[str(loader_private)] = error # public key? loader_public: Any for loader_public in (serialization.load_pem_public_key, serialization.load_der_public_key): try: return loader_public(data, backend) except (ValueError, cryptography.exceptions.UnsupportedAlgorithm) as error: exceptions[str(loader_public)] = error # no luck raise errors.Error("Unable to deserialize key: {0}".format(exceptions)) @classmethod def load( cls, data: bytes, password: Optional[bytes] = None, backend: Optional[Any] = None ) -> "JWK": """Load serialized key as JWK. :param str data: Public or private key serialized as PEM or DER. :param str password: Optional password. :param backend: A `.PEMSerializationBackend` and `.DERSerializationBackend` provider. :raises errors.Error: if unable to deserialize, or unsupported JWK algorithm :returns: JWK of an appropriate type. :rtype: `JWK` """ try: key = cls._load_cryptography_key(data, password, backend) except errors.Error as error: logger.debug("Loading symmetric key, asymmetric failed: %s", error) return JWKOct(key=data) if cls.typ is not NotImplemented and not isinstance(key, cls.cryptography_key_types): raise errors.Error( "Unable to deserialize {0} into {1}".format(key.__class__, cls.__class__) ) for jwk_cls in cls.TYPES.values(): if isinstance(key, jwk_cls.cryptography_key_types): return jwk_cls(key=key) raise errors.Error("Unsupported algorithm: {0}".format(key.__class__)) @JWK.register class JWKOct(JWK): """Symmetric JWK.""" typ = "oct" __slots__ = ("key",) required = ("k", JWK.type_field_name) key: bytes def fields_to_partial_json(self) -> Dict[str, str]: # TODO: An "alg" member SHOULD also be present to identify the # algorithm intended to be used with the key, unless the # application uses another means or convention to determine # the algorithm used. return {"k": json_util.encode_b64jose(self.key)} @classmethod def fields_from_json(cls, jobj: Mapping[str, Any]) -> "JWKOct": return cls(key=json_util.decode_b64jose(jobj["k"])) def public_key(self) -> "JWKOct": return self @JWK.register class JWKRSA(JWK): """RSA JWK. :ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` or :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` wrapped in :class:`~josepy.util.ComparableRSAKey` """ typ = "RSA" cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) __slots__ = ("key",) required = ("e", JWK.type_field_name, "n") key: josepy.util.ComparableRSAKey def __init__(self, *args: Any, **kwargs: Any) -> None: if "key" in kwargs and not isinstance(kwargs["key"], util.ComparableRSAKey): kwargs["key"] = util.ComparableRSAKey(kwargs["key"]) super().__init__(*args, **kwargs) @classmethod def _encode_param(cls, data: int) -> str: """Encode Base64urlUInt. :type data: long :rtype: unicode """ length = max(data.bit_length(), 8) # decoding 0 length = math.ceil(length / 8) return json_util.encode_b64jose(data.to_bytes(byteorder="big", length=length)) @classmethod def _decode_param(cls, data: str) -> int: """Decode Base64urlUInt.""" try: binary = json_util.decode_b64jose(data) if not binary: raise errors.DeserializationError() return int.from_bytes(binary, byteorder="big") except ValueError: # invalid literal for long() with base 16 raise errors.DeserializationError() def public_key(self) -> "JWKRSA": return type(self)(key=self.key.public_key()) @classmethod def fields_from_json(cls, jobj: Mapping[str, Any]) -> "JWKRSA": n, e = (cls._decode_param(jobj[x]) for x in ("n", "e")) public_numbers = rsa.RSAPublicNumbers(e=e, n=n) # public key if "d" not in jobj: return cls(key=public_numbers.public_key(default_backend())) # private key d = cls._decode_param(jobj["d"]) if ( "p" in jobj or "q" in jobj or "dp" in jobj or "dq" in jobj or "qi" in jobj or "oth" in jobj ): # "If the producer includes any of the other private # key parameters, then all of the others MUST be # present, with the exception of "oth", which MUST # only be present when more than two prime factors # were used." ( p, q, dp, dq, qi, ) = all_params = tuple(jobj.get(x) for x in ("p", "q", "dp", "dq", "qi")) if tuple(param for param in all_params if param is None): raise errors.Error("Some private parameters are missing: {0}".format(all_params)) p, q, dp, dq, qi = tuple(cls._decode_param(str(x)) for x in all_params) # TODO: check for oth else: # cryptography>=0.8 p, q = rsa.rsa_recover_prime_factors(n, e, d) dp = rsa.rsa_crt_dmp1(d, p) dq = rsa.rsa_crt_dmq1(d, q) qi = rsa.rsa_crt_iqmp(p, q) key = rsa.RSAPrivateNumbers(p, q, d, dp, dq, qi, public_numbers).private_key( default_backend() ) return cls(key=key) def fields_to_partial_json(self) -> Dict[str, Any]: if isinstance(self.key._wrapped, rsa.RSAPublicKey): numbers = self.key.public_numbers() params = { "n": numbers.n, "e": numbers.e, } else: # rsa.RSAPrivateKey private = self.key.private_numbers() public = self.key.public_key().public_numbers() params = { "n": public.n, "e": public.e, "d": private.d, "p": private.p, "q": private.q, "dp": private.dmp1, "dq": private.dmq1, "qi": private.iqmp, } return {key: self._encode_param(value) for key, value in params.items()} @JWK.register class JWKEC(JWK): """EC JWK. :ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` or :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` wrapped in :class:`~josepy.util.ComparableECKey` """ typ = "EC" __slots__ = ("key",) cryptography_key_types = (ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) required = ("crv", JWK.type_field_name, "x", "y") key: josepy.util.ComparableECKey def __init__(self, *args: Any, **kwargs: Any) -> None: if "key" in kwargs and not isinstance(kwargs["key"], util.ComparableECKey): kwargs["key"] = util.ComparableECKey(kwargs["key"]) super().__init__(*args, **kwargs) @classmethod def _encode_param(cls, data: int, length: int) -> str: """Encode Base64urlUInt. :type data: long :type key_size: long :rtype: unicode """ return json_util.encode_b64jose(data.to_bytes(byteorder="big", length=length)) @classmethod def _decode_param(cls, data: str, name: str, valid_length: int) -> int: """Decode Base64urlUInt.""" try: binary = json_util.decode_b64jose(data) if len(binary) != valid_length: raise errors.DeserializationError( f'Expected parameter "{name}" to be {valid_length} bytes ' f"after base64-decoding; got {len(binary)} bytes instead" ) return int.from_bytes(binary, byteorder="big") except ValueError: # invalid literal for long() with base 16 raise errors.DeserializationError() @classmethod def _curve_name_to_crv(cls, curve_name: str) -> str: if curve_name == "secp256r1": return "P-256" if curve_name == "secp384r1": return "P-384" if curve_name == "secp521r1": return "P-521" raise errors.SerializationError() @classmethod def _crv_to_curve(cls, crv: str) -> ec.EllipticCurve: # crv is case-sensitive if crv == "P-256": return ec.SECP256R1() if crv == "P-384": return ec.SECP384R1() if crv == "P-521": return ec.SECP521R1() raise errors.DeserializationError() @classmethod def expected_length_for_curve(cls, curve: ec.EllipticCurve) -> int: if isinstance(curve, ec.SECP256R1): return 32 elif isinstance(curve, ec.SECP384R1): return 48 elif isinstance(curve, ec.SECP521R1): return 66 raise ValueError(f"Unexpected curve: {curve}") def fields_to_partial_json(self) -> Dict[str, Any]: params = {} if isinstance(self.key._wrapped, ec.EllipticCurvePublicKey): public = self.key.public_numbers() elif isinstance(self.key._wrapped, ec.EllipticCurvePrivateKey): private = self.key.private_numbers() public = self.key.public_key().public_numbers() params["d"] = private.private_value else: raise errors.SerializationError( "Supplied key is neither of type EllipticCurvePublicKey " "nor EllipticCurvePrivateKey" ) params["x"] = public.x params["y"] = public.y params = { key: self._encode_param(value, self.expected_length_for_curve(public.curve)) for key, value in params.items() } params["crv"] = self._curve_name_to_crv(public.curve.name) return params @classmethod def fields_from_json(cls, jobj: Mapping[str, Any]) -> "JWKEC": curve = cls._crv_to_curve(jobj["crv"]) expected_length = cls.expected_length_for_curve(curve) x, y = (cls._decode_param(jobj[n], n, expected_length) for n in ("x", "y")) public_numbers = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=curve) # private key if "d" not in jobj: return cls(key=public_numbers.public_key(default_backend())) # private key d = cls._decode_param(jobj["d"], "d", expected_length) key = ec.EllipticCurvePrivateNumbers(d, public_numbers).private_key(default_backend()) return cls(key=key) def public_key(self) -> "JWKEC": # Unlike RSAPrivateKey, EllipticCurvePrivateKey does not contain public_key() if hasattr(self.key, "public_key"): key = self.key.public_key() else: key = self.key.public_numbers().public_key(default_backend()) return type(self)(key=key) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.809802 josepy-1.14.0/src/josepy/jws.py0000644000000000000000000003620114520452152013313 0ustar00"""JSON Web Signature.""" import argparse import base64 import sys from typing import ( Any, Dict, FrozenSet, List, Mapping, Optional, Tuple, Type, cast, ) from OpenSSL import crypto import josepy from josepy import b64, errors, json_util, jwa from josepy import jwk as jwk_mod from josepy import util class MediaType: """MediaType field encoder/decoder.""" PREFIX = "application/" """MIME Media Type and Content Type prefix.""" @classmethod def decode(cls, value: str) -> str: """Decoder.""" # 4.1.10 if "/" not in value: if ";" in value: raise errors.DeserializationError("Unexpected semi-colon") return cls.PREFIX + value return value @classmethod def encode(cls, value: str) -> str: """Encoder.""" # 4.1.10 if ";" not in value: assert value.startswith(cls.PREFIX) return value[len(cls.PREFIX) :] return value class Header(json_util.JSONObjectWithFields): """JOSE Header. .. warning:: This class supports **only** Registered Header Parameter Names (as defined in section 4.1 of the protocol). If you need Public Header Parameter Names (4.2) or Private Header Parameter Names (4.3), you must subclass and override :meth:`from_json` and :meth:`to_partial_json` appropriately. .. warning:: This class does not support any extensions through the "crit" (Critical) Header Parameter (4.1.11) and as a conforming implementation, :meth:`from_json` treats its occurrence as an error. Please subclass if you seek for a different behaviour. :ivar x5tS256: "x5t#S256" :ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`. :ivar str cty: Content-Type, inc. :const:`MediaType.PREFIX`. """ alg: Optional[jwa.JWASignature] = json_util.field( "alg", decoder=jwa.JWASignature.from_json, omitempty=True ) jku: Optional[bytes] = json_util.field("jku", omitempty=True) jwk: Optional[jwk_mod.JWK] = json_util.field( "jwk", decoder=jwk_mod.JWK.from_json, omitempty=True ) kid: Optional[str] = json_util.field("kid", omitempty=True) x5u: Optional[bytes] = json_util.field("x5u", omitempty=True) x5c: Tuple[util.ComparableX509, ...] = json_util.field("x5c", omitempty=True, default=()) x5t: Optional[bytes] = json_util.field("x5t", decoder=json_util.decode_b64jose, omitempty=True) x5tS256: Optional[bytes] = json_util.field( "x5t#S256", decoder=json_util.decode_b64jose, omitempty=True ) typ: Optional[MediaType] = json_util.field( "typ", encoder=MediaType.encode, decoder=MediaType.decode, omitempty=True ) cty: Optional[MediaType] = json_util.field( "cty", encoder=MediaType.encode, decoder=MediaType.decode, omitempty=True ) crit: Tuple[Any, ...] = json_util.field("crit", omitempty=True, default=()) _fields: Dict[str, json_util.Field] def not_omitted(self) -> Dict[str, json_util.Field]: """Fields that would not be omitted in the JSON object.""" return { name: getattr(self, name) for name, field in self._fields.items() if not field.omit(getattr(self, name)) } def __add__(self, other: Any) -> "Header": if not isinstance(other, type(self)): raise TypeError("Header cannot be added to: {0}".format(type(other))) not_omitted_self = self.not_omitted() not_omitted_other = other.not_omitted() if set(not_omitted_self).intersection(not_omitted_other): raise TypeError("Addition of overlapping headers not defined") not_omitted_self.update(not_omitted_other) return type(self)(**not_omitted_self) def find_key(self) -> josepy.JWK: """Find key based on header. .. todo:: Supports only "jwk" header parameter lookup. :returns: (Public) key found in the header. :rtype: .JWK :raises josepy.errors.Error: if key could not be found """ if self.jwk is None: raise errors.Error("No key found") return self.jwk @crit.decoder # type: ignore def crit(unused_value: Any) -> Any: raise errors.DeserializationError('"crit" is not supported, please subclass') # x5c does NOT use JOSE Base64 (4.1.6) @x5c.encoder # type: ignore def x5c(value): return [ base64.b64encode(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped)) for cert in value ] @x5c.decoder # type: ignore def x5c(value): try: return tuple( util.ComparableX509( crypto.load_certificate(crypto.FILETYPE_ASN1, base64.b64decode(cert)) ) for cert in value ) except crypto.Error as error: raise errors.DeserializationError(error) class Signature(json_util.JSONObjectWithFields): """JWS Signature. :ivar combined: Combined Header (protected and unprotected, :class:`Header`). :ivar unicode protected: JWS protected header (Jose Base-64 decoded). :ivar header: JWS Unprotected Header (:class:`Header`). :ivar str signature: The signature. """ header_cls = Header combined: Header __slots__ = ("combined",) protected: str = json_util.field("protected", omitempty=True, default="") header: Header = json_util.field( "header", omitempty=True, default=header_cls(), decoder=header_cls.from_json ) signature: bytes = json_util.field( "signature", decoder=json_util.decode_b64jose, encoder=json_util.encode_b64jose ) @protected.encoder # type: ignore def protected(value: str) -> str: # wrong type guess (Signature, not bytes) | pylint: disable=no-member return json_util.encode_b64jose(value.encode("utf-8")) @protected.decoder # type: ignore def protected(value: str) -> str: return json_util.decode_b64jose(value).decode("utf-8") def __init__(self, **kwargs: Any) -> None: if "combined" not in kwargs: kwargs = self._with_combined(kwargs) super().__init__(**kwargs) assert self.combined.alg is not None @classmethod def _with_combined(cls, kwargs: Any) -> Dict[str, Any]: assert "combined" not in kwargs header = kwargs.get("header", cls._fields["header"].default) protected = kwargs.get("protected", cls._fields["protected"].default) if protected: combined = header + cls.header_cls.json_loads(protected) else: combined = header kwargs["combined"] = combined return kwargs @classmethod def _msg(cls, protected: str, payload: bytes) -> bytes: return b64.b64encode(protected.encode("utf-8")) + b"." + b64.b64encode(payload) def verify(self, payload: bytes, key: Optional[josepy.JWK] = None) -> bool: """Verify. :param bytes payload: Payload to verify. :param JWK key: Key used for verification. """ actual_key: josepy.JWK = self.combined.find_key() if key is None else key if not self.combined.alg: raise josepy.Error("Not signature algorithm defined.") return self.combined.alg.verify( key=actual_key.key, sig=self.signature, msg=self._msg(self.protected, payload) ) @classmethod def sign( cls, payload: bytes, key: josepy.JWK, alg: josepy.JWASignature, include_jwk: bool = True, protect: FrozenSet = frozenset(), **kwargs: Any, ) -> "Signature": """Sign. :param bytes payload: Payload to sign. :param JWK key: Key for signature. :param JWASignature alg: Signature algorithm to use to sign. :param bool include_jwk: If True, insert the JWK inside the signature headers. :param FrozenSet protect: List of headers to protect. """ assert isinstance(key, alg.kty) header_params = kwargs header_params["alg"] = alg if include_jwk: header_params["jwk"] = key.public_key() assert set(header_params).issubset(cls.header_cls._fields) assert protect.issubset(cls.header_cls._fields) protected_params = {} for header in protect: if header in header_params: protected_params[header] = header_params.pop(header) if protected_params: protected = cls.header_cls(**protected_params).json_dumps() else: protected = "" header = cls.header_cls(**header_params) signature = alg.sign(key.key, cls._msg(protected, payload)) return cls(protected=protected, header=header, signature=signature) def fields_to_partial_json(self) -> Dict[str, Any]: fields = super().fields_to_partial_json() if not fields["header"].not_omitted(): del fields["header"] return fields @classmethod def fields_from_json(cls, jobj: Mapping[str, Any]) -> Dict[str, Any]: fields = super().fields_from_json(jobj) fields_with_combined = cls._with_combined(fields) if "alg" not in fields_with_combined["combined"].not_omitted(): raise errors.DeserializationError("alg not present") return fields_with_combined class JWS(json_util.JSONObjectWithFields): """JSON Web Signature. :ivar str payload: JWS Payload. :ivar str signature: JWS Signatures. """ __slots__ = ("payload", "signatures") payload: bytes signatures: List[Signature] signature_cls = Signature def verify(self, key: Optional[josepy.JWK] = None) -> bool: """Verify.""" return all(sig.verify(self.payload, key) for sig in self.signatures) @classmethod def sign(cls, payload: bytes, **kwargs: Any) -> "JWS": """Sign.""" return cls(payload=payload, signatures=(cls.signature_cls.sign(payload=payload, **kwargs),)) @property def signature(self) -> Signature: """Get a singleton signature. :rtype: :class:`JWS.signature_cls` """ assert len(self.signatures) == 1 return self.signatures[0] def to_compact(self) -> bytes: """Compact serialization. :rtype: bytes """ assert len(self.signatures) == 1 assert "alg" not in self.signature.header.not_omitted() # ... it must be in protected return ( b64.b64encode(self.signature.protected.encode("utf-8")) + b"." + b64.b64encode(self.payload) + b"." + b64.b64encode(self.signature.signature) ) @classmethod def from_compact(cls, compact: bytes) -> "JWS": """Compact deserialization. :param bytes compact: """ try: protected, payload, signature = compact.split(b".") except ValueError: raise errors.DeserializationError( "Compact JWS serialization should comprise of exactly" " 3 dot-separated components" ) sig = cls.signature_cls( protected=b64.b64decode(protected).decode("utf-8"), signature=b64.b64decode(signature) ) return cls(payload=b64.b64decode(payload), signatures=(sig,)) def to_partial_json(self, flat: bool = True) -> Dict[str, Any]: assert self.signatures payload = json_util.encode_b64jose(self.payload) if flat and len(self.signatures) == 1: ret = self.signatures[0].to_partial_json() ret["payload"] = payload return ret else: return { "payload": payload, "signatures": self.signatures, } @classmethod def from_json(cls, jobj: Mapping[str, Any]) -> "JWS": if "signature" in jobj and "signatures" in jobj: raise errors.DeserializationError("Flat mixed with non-flat") elif "signature" in jobj: # flat filtered = {key: value for key, value in jobj.items() if key != "payload"} return cls( payload=json_util.decode_b64jose(jobj["payload"]), signatures=(cls.signature_cls.from_json(filtered),), ) else: return cls( payload=json_util.decode_b64jose(jobj["payload"]), signatures=tuple(cls.signature_cls.from_json(sig) for sig in jobj["signatures"]), ) class CLI: """JWS CLI.""" @classmethod def sign(cls, args: argparse.Namespace) -> None: """Sign.""" key = args.alg.kty.load(args.key.read()) args.key.close() if args.protect is None: args.protect = [] if args.compact: args.protect.append("alg") sig = JWS.sign( payload=sys.stdin.read().encode(), key=key, alg=args.alg, protect=set(args.protect) ) if args.compact: print(sig.to_compact().decode("utf-8")) else: # JSON print(sig.json_dumps_pretty()) @classmethod def verify(cls, args: argparse.Namespace) -> bool: """Verify.""" if args.compact: sig = JWS.from_compact(sys.stdin.read().encode()) else: # JSON try: sig = cast(JWS, JWS.json_loads(sys.stdin.read())) except errors.Error as error: print(error) return False if args.key is not None: assert args.kty is not None key = args.kty.load(args.key.read()).public_key() args.key.close() else: key = None sys.stdout.write(sig.payload.decode()) return not sig.verify(key=key) @classmethod def _alg_type(cls, arg: Any) -> jwa.JWASignature: return jwa.JWASignature.from_json(arg) @classmethod def _header_type(cls, arg: Any) -> Any: assert arg in Signature.header_cls._fields return arg @classmethod def _kty_type(cls, arg: Any) -> Type[jwk_mod.JWK]: assert arg in jwk_mod.JWK.TYPES return jwk_mod.JWK.TYPES[arg] @classmethod def run(cls, args: Optional[List[str]] = None) -> Optional[bool]: """Parse arguments and sign/verify.""" if args is None: args = sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument("--compact", action="store_true") subparsers = parser.add_subparsers() parser_sign = subparsers.add_parser("sign") parser_sign.set_defaults(func=cls.sign) parser_sign.add_argument("-k", "--key", type=argparse.FileType("rb"), required=True) parser_sign.add_argument("-a", "--alg", type=cls._alg_type, default=jwa.RS256) parser_sign.add_argument("-p", "--protect", action="append", type=cls._header_type) parser_verify = subparsers.add_parser("verify") parser_verify.set_defaults(func=cls.verify) parser_verify.add_argument("-k", "--key", type=argparse.FileType("rb"), required=False) parser_verify.add_argument("--kty", type=cls._kty_type, required=False) parsed = parser.parse_args(args) return parsed.func(parsed) if __name__ == "__main__": exit(CLI.run()) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8099189 josepy-1.14.0/src/josepy/magic_typing.py0000644000000000000000000000105414520452152015160 0ustar00"""Shim class to not have to depend on typing module in prod.""" # mypy: ignore-errors import sys import warnings from typing import Any warnings.warn( "josepy.magic_typing is deprecated and will be removed in a future release.", DeprecationWarning ) class TypingClass: """Ignore import errors by getting anything""" def __getattr__(self, name: str) -> Any: return None try: # mypy doesn't respect modifying sys.modules from typing import * # noqa: F401,F403 except ImportError: sys.modules[__name__] = TypingClass() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.809946 josepy-1.14.0/src/josepy/py.typed0000644000000000000000000000000014520452152013621 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8101056 josepy-1.14.0/src/josepy/util.py0000644000000000000000000002350114520452152013464 0ustar00"""JOSE utilities.""" import abc import sys import warnings from collections.abc import Hashable, Mapping from types import ModuleType from typing import Any, Callable, Iterator, List, Tuple, TypeVar, Union, cast from cryptography.hazmat.primitives.asymmetric import ec, rsa from OpenSSL import crypto # Deprecated. Please use built-in decorators @classmethod and abc.abstractmethod together instead. def abstractclassmethod(func: Callable) -> classmethod: return classmethod(abc.abstractmethod(func)) class ComparableX509: """Wrapper for OpenSSL.crypto.X509** objects that supports __eq__. :ivar wrapped: Wrapped certificate or certificate request. :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. """ def __init__(self, wrapped: Union[crypto.X509, crypto.X509Req]) -> None: assert isinstance(wrapped, crypto.X509) or isinstance(wrapped, crypto.X509Req) self.wrapped = wrapped def __getattr__(self, name: str) -> Any: return getattr(self.wrapped, name) def _dump(self, filetype: int = crypto.FILETYPE_ASN1) -> bytes: """Dumps the object into a buffer with the specified encoding. :param int filetype: The desired encoding. Should be one of `OpenSSL.crypto.FILETYPE_ASN1`, `OpenSSL.crypto.FILETYPE_PEM`, or `OpenSSL.crypto.FILETYPE_TEXT`. :returns: Encoded X509 object. :rtype: bytes """ if isinstance(self.wrapped, crypto.X509): return crypto.dump_certificate(filetype, self.wrapped) # assert in __init__ makes sure this is X509Req return crypto.dump_certificate_request(filetype, self.wrapped) def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): return NotImplemented return self._dump() == other._dump() def __hash__(self) -> int: return hash((self.__class__, self._dump())) def __repr__(self) -> str: return "<{0}({1!r})>".format(self.__class__.__name__, self.wrapped) class ComparableKey: """Comparable wrapper for ``cryptography`` keys. See https://github.com/pyca/cryptography/issues/2122. """ __hash__: Callable[[], int] = NotImplemented def __init__( self, wrapped: Union[ rsa.RSAPrivateKeyWithSerialization, rsa.RSAPublicKeyWithSerialization, ec.EllipticCurvePrivateKeyWithSerialization, ec.EllipticCurvePublicKeyWithSerialization, ], ): self._wrapped = wrapped def __getattr__(self, name: str) -> Any: return getattr(self._wrapped, name) def __eq__(self, other: Any) -> bool: if ( not isinstance(other, self.__class__) or self._wrapped.__class__ is not other._wrapped.__class__ ): return NotImplemented elif hasattr(self._wrapped, "private_numbers"): return self.private_numbers() == other.private_numbers() elif hasattr(self._wrapped, "public_numbers"): return self.public_numbers() == other.public_numbers() else: return NotImplemented def __repr__(self) -> str: return "<{0}({1!r})>".format(self.__class__.__name__, self._wrapped) def public_key(self) -> "ComparableKey": """Get wrapped public key.""" if isinstance( self._wrapped, (rsa.RSAPublicKeyWithSerialization, ec.EllipticCurvePublicKeyWithSerialization), ): return self return self.__class__(self._wrapped.public_key()) class ComparableRSAKey(ComparableKey): """Wrapper for ``cryptography`` RSA keys. Wraps around: - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` """ def __hash__(self) -> int: # public_numbers() hasn't got stable hash! # https://github.com/pyca/cryptography/issues/2143 if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization): priv = self.private_numbers() pub = priv.public_numbers return hash( (self.__class__, priv.p, priv.q, priv.dmp1, priv.dmq1, priv.iqmp, pub.n, pub.e) ) elif isinstance(self._wrapped, rsa.RSAPublicKeyWithSerialization): pub = self.public_numbers() return hash((self.__class__, pub.n, pub.e)) raise NotImplementedError() class ComparableECKey(ComparableKey): """Wrapper for ``cryptography`` EC keys. Wraps around: - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` """ def __hash__(self) -> int: # public_numbers() hasn't got stable hash! # https://github.com/pyca/cryptography/issues/2143 if isinstance(self._wrapped, ec.EllipticCurvePrivateKeyWithSerialization): priv = self.private_numbers() pub = priv.public_numbers return hash((self.__class__, pub.curve.name, pub.x, pub.y, priv.private_value)) elif isinstance(self._wrapped, ec.EllipticCurvePublicKeyWithSerialization): pub = self.public_numbers() return hash((self.__class__, pub.curve.name, pub.x, pub.y)) raise NotImplementedError() GenericImmutableMap = TypeVar("GenericImmutableMap", bound="ImmutableMap") class ImmutableMap(Mapping, Hashable): """Immutable key to value mapping with attribute access.""" __slots__: Tuple[str, ...] = () """Must be overridden in subclasses.""" def __init__(self, **kwargs: Any) -> None: if set(kwargs) != set(self.__slots__): raise TypeError( "__init__() takes exactly the following arguments: {0} " "({1} given)".format( ", ".join(self.__slots__), ", ".join(kwargs) if kwargs else "none" ) ) for slot in self.__slots__: object.__setattr__(self, slot, kwargs.pop(slot)) def update(self: GenericImmutableMap, **kwargs: Any) -> GenericImmutableMap: """Return updated map.""" items: Mapping[str, Any] = {**self, **kwargs} return type(self)(**items) def __getitem__(self, key: str) -> Any: try: return getattr(self, key) except AttributeError: raise KeyError(key) def __iter__(self) -> Iterator[str]: return iter(self.__slots__) def __len__(self) -> int: return len(self.__slots__) def __hash__(self) -> int: return hash(tuple(getattr(self, slot) for slot in self.__slots__)) def __setattr__(self, name: str, value: Any) -> None: raise AttributeError("can't set attribute") def __repr__(self) -> str: return "{0}({1})".format( self.__class__.__name__, ", ".join("{0}={1!r}".format(key, value) for key, value in self.items()), ) class frozendict(Mapping, Hashable): """Frozen dictionary.""" __slots__ = ("_items", "_keys") def __init__(self, *args: Any, **kwargs: Any) -> None: items: Mapping if kwargs and not args: items = dict(kwargs) elif len(args) == 1 and isinstance(args[0], Mapping): items = args[0] else: raise TypeError() # TODO: support generators/iterators object.__setattr__(self, "_items", items) object.__setattr__(self, "_keys", tuple(sorted(items.keys()))) def __getitem__(self, key: str) -> Any: return self._items[key] def __iter__(self) -> Iterator[str]: return iter(self._keys) def __len__(self) -> int: return len(self._items) def _sorted_items(self) -> Tuple[Tuple[str, Any], ...]: return tuple((key, self[key]) for key in self._keys) def __hash__(self) -> int: return hash(self._sorted_items()) def __getattr__(self, name: str) -> Any: try: return self._items[name] except KeyError: raise AttributeError(name) def __setattr__(self, name: str, value: Any) -> None: raise AttributeError("can't set attribute") def __repr__(self) -> str: return "frozendict({0})".format( ", ".join("{0}={1!r}".format(key, value) for key, value in self._sorted_items()) ) # This class takes a similar approach to the cryptography project to deprecate attributes # in public modules. See the _ModuleWithDeprecation class here: # https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 class _UtilDeprecationModule: """ Internal class delegating to a module, and displaying warnings when attributes related to the deprecated "abstractclassmethod" attributes in the josepy.util module. """ def __init__(self, module: ModuleType) -> None: self.__dict__["_module"] = module def __getattr__(self, attr: str) -> Any: if attr == "abstractclassmethod": warnings.warn( "The abstractclassmethod attribute in josepy.util is deprecated and will " "be removed soon. Please use the built-in decorators @classmethod and " "@abc.abstractmethod together instead.", DeprecationWarning, stacklevel=2, ) return getattr(self._module, attr) def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover setattr(self._module, attr, value) def __delattr__(self, attr: str) -> None: # pragma: no cover delattr(self._module, attr) def __dir__(self) -> List[str]: # pragma: no cover return ["_module"] + dir(self._module) # Patching ourselves to warn about deprecation and planned removal of some elements in the module. sys.modules[__name__] = cast(ModuleType, _UtilDeprecationModule(sys.modules[__name__])) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.810268 josepy-1.14.0/tests/b64_test.py0000644000000000000000000000477614520452152013220 0ustar00"""Tests for josepy.b64.""" import sys from typing import Union import pytest # https://en.wikipedia.org/wiki/Base64#Examples B64_PADDING_EXAMPLES = { b"any carnal pleasure.": (b"YW55IGNhcm5hbCBwbGVhc3VyZS4", b"="), b"any carnal pleasure": (b"YW55IGNhcm5hbCBwbGVhc3VyZQ", b"=="), b"any carnal pleasur": (b"YW55IGNhcm5hbCBwbGVhc3Vy", b""), b"any carnal pleasu": (b"YW55IGNhcm5hbCBwbGVhc3U", b"="), b"any carnal pleas": (b"YW55IGNhcm5hbCBwbGVhcw", b"=="), } B64_URL_UNSAFE_EXAMPLES = { bytes((251, 239)): b"--8", bytes((255,)) * 2: b"__8", } class B64EncodeTest: """Tests for josepy.b64.b64encode.""" @classmethod def _call(cls, data: bytes) -> bytes: from josepy.b64 import b64encode return b64encode(data) def test_empty(self) -> None: assert self._call(b"") == b"" def test_unsafe_url(self) -> None: for text, b64 in B64_URL_UNSAFE_EXAMPLES.items(): assert self._call(text) == b64 def test_different_paddings(self) -> None: for text, (b64, _) in B64_PADDING_EXAMPLES.items(): assert self._call(text) == b64 def test_unicode_fails_with_type_error(self) -> None: with pytest.raises(TypeError): # We're purposefully testing with the incorrect type here. self._call("some unicode") # type: ignore class B64DecodeTest: """Tests for josepy.b64.b64decode.""" @classmethod def _call(cls, data: Union[bytes, str]) -> bytes: from josepy.b64 import b64decode return b64decode(data) def test_unsafe_url(self) -> None: for text, b64 in B64_URL_UNSAFE_EXAMPLES.items(): assert self._call(b64) == text def test_input_without_padding(self) -> None: for text, (b64, _) in B64_PADDING_EXAMPLES.items(): assert self._call(b64) == text def test_input_with_padding(self) -> None: for text, (b64, pad) in B64_PADDING_EXAMPLES.items(): assert self._call(b64 + pad) == text def test_unicode_with_ascii(self) -> None: assert self._call("YQ") == b"a" def test_non_ascii_unicode_fails(self) -> None: with pytest.raises(ValueError): self._call("\u0105") def test_type_error_no_unicode_or_bytes(self) -> None: with pytest.raises(TypeError): # We're purposefully testing with the incorrect type here. self._call(object()) # type: ignore if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8103957 josepy-1.14.0/tests/errors_test.py0000644000000000000000000000077114520452152014130 0ustar00"""Tests for josepy.errors.""" import sys import unittest import pytest class UnrecognizedTypeErrorTest(unittest.TestCase): def setUp(self) -> None: from josepy.errors import UnrecognizedTypeError self.error = UnrecognizedTypeError("foo", {"type": "foo"}) def test_str(self) -> None: assert "foo was not recognized, full message: {'type': 'foo'}" == str(self.error) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.810521 josepy-1.14.0/tests/init_test.py0000644000000000000000000000120114520452152013544 0ustar00import importlib import re import sys import warnings import pytest import josepy @pytest.mark.skipif(sys.version_info[:2] != (3, 7), reason="requires Python 3.7") def test_warns() -> None: with pytest.warns(DeprecationWarning, match=re.escape(r"Python 3.7 support")): importlib.reload(josepy) @pytest.mark.skipif(sys.version_info[:2] == (3, 7), reason="requires Python != 3.7") def test_does_not_warn() -> None: with warnings.catch_warnings(): warnings.simplefilter("error") importlib.reload(josepy) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.810647 josepy-1.14.0/tests/interfaces_test.py0000644000000000000000000000723214520452152014736 0ustar00"""Tests for josepy.interfaces.""" import sys import unittest from typing import Any, Dict, List import pytest class JSONDeSerializableTest(unittest.TestCase): def setUp(self) -> None: from josepy.interfaces import JSONDeSerializable class Basic(JSONDeSerializable): def __init__(self, v: Any) -> None: self.v = v def to_partial_json(self) -> Any: return self.v @classmethod def from_json(cls, jobj: Any) -> "Basic": return cls(jobj) class Sequence(JSONDeSerializable): def __init__(self, x: Basic, y: Basic) -> None: self.x = x self.y = y def to_partial_json(self) -> List[Basic]: return [self.x, self.y] @classmethod def from_json(cls, jobj: List[Any]) -> "Sequence": return cls(Basic.from_json(jobj[0]), Basic.from_json(jobj[1])) class Mapping(JSONDeSerializable): def __init__(self, x: Any, y: Any) -> None: self.x = x self.y = y def to_partial_json(self) -> Dict[Basic, Basic]: return {self.x: self.y} @classmethod def from_json(cls, jobj: Any) -> "Mapping": return cls("dummy", "values") # pragma: no cover self.basic1 = Basic("foo1") self.basic2 = Basic("foo2") self.seq = Sequence(self.basic1, self.basic2) self.mapping = Mapping(self.basic1, self.basic2) self.nested = Basic([[self.basic1]]) self.tuple = Basic(("foo",)) self.Basic = Basic self.Sequence = Sequence self.Mapping = Mapping def test_to_json_sequence(self) -> None: assert self.seq.to_json() == ["foo1", "foo2"] def test_to_json_mapping(self) -> None: assert self.mapping.to_json() == {"foo1": "foo2"} def test_to_json_other(self) -> None: mock_value = object() assert self.Basic(mock_value).to_json() is mock_value def test_to_json_nested(self) -> None: assert self.nested.to_json() == [["foo1"]] def test_to_json(self) -> None: assert self.tuple.to_json() == (("foo",)) def test_from_json_not_implemented(self) -> None: from josepy.interfaces import JSONDeSerializable with pytest.raises(TypeError): JSONDeSerializable.from_json("xxx") def test_json_loads(self) -> None: seq = self.Sequence.json_loads('["foo1", "foo2"]') assert isinstance(seq, self.Sequence) assert isinstance(seq.x, self.Basic) assert isinstance(seq.y, self.Basic) assert seq.x.v == "foo1" assert seq.y.v == "foo2" def test_json_dumps(self) -> None: assert '["foo1", "foo2"]' == self.seq.json_dumps() def test_json_dumps_pretty(self) -> None: assert self.seq.json_dumps_pretty() == '[\n "foo1",\n "foo2"\n]' def test_json_dump_default(self) -> None: from josepy.interfaces import JSONDeSerializable assert "foo1" == JSONDeSerializable.json_dump_default(self.basic1) jobj = JSONDeSerializable.json_dump_default(self.seq) assert len(jobj) == 2 assert jobj[0] is self.basic1 assert jobj[1] is self.basic2 def test_json_dump_default_type_error(self) -> None: from josepy.interfaces import JSONDeSerializable with pytest.raises(TypeError): # We're purposefully testing with the incorrect type here. JSONDeSerializable.json_dump_default(object()) # type: ignore if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8108008 josepy-1.14.0/tests/json_util_test.py0000644000000000000000000003372414520452152014626 0ustar00"""Tests for josepy.json_util.""" import itertools import sys import unittest from typing import Any, Dict, Mapping from unittest import mock import pytest import test_util from josepy import errors, interfaces, util CERT = test_util.load_comparable_cert("cert.pem") CSR = test_util.load_comparable_csr("csr.pem") class FieldTest(unittest.TestCase): """Tests for josepy.json_util.field and josepy.json_util.Field.""" def test_field_function(self) -> None: from josepy.json_util import Field, field test = field("foo", default="bar") assert isinstance(test, Field) assert test.json_name == "foo" assert test.default == "bar" def test_type_field_control(self) -> None: from josepy.json_util import JSONObjectWithFields, field class DummyProperlyTyped(JSONObjectWithFields): type: str = field("type") index: int = field("index") with pytest.raises(ValueError): class DummyImproperlyTyped(JSONObjectWithFields): type = field("type") index: int = field("index") def test_no_omit_boolean(self) -> None: from josepy.json_util import Field for default, omitempty, value in itertools.product( [True, False], [True, False], [True, False] ): assert Field("foo", default=default, omitempty=omitempty).omit(value) is False def test_descriptors(self) -> None: mock_value = mock.MagicMock() def decoder(unused_value: Any) -> str: return "d" def encoder(unused_value: Any) -> str: return "e" from josepy.json_util import Field field = Field("foo") field = field.encoder(encoder) assert "e" == field.encode(mock_value) field = field.decoder(decoder) assert "e" == field.encode(mock_value) assert "d" == field.decode(mock_value) def test_default_encoder_is_partial(self) -> None: class MockField(interfaces.JSONDeSerializable): def to_partial_json(self) -> Dict[str, Any]: return {"foo": "bar"} # pragma: no cover @classmethod def from_json(cls, jobj: Mapping[str, Any]) -> "MockField": return cls() # pragma: no cover mock_field = MockField() from josepy.json_util import Field assert Field.default_encoder(mock_field) is mock_field # in particular... assert "foo" != Field.default_encoder(mock_field) def test_default_encoder_passthrough(self) -> None: mock_value = mock.MagicMock() from josepy.json_util import Field assert Field.default_encoder(mock_value) is mock_value def test_default_decoder_list_to_tuple(self) -> None: from josepy.json_util import Field assert (1, 2, 3) == Field.default_decoder([1, 2, 3]) def test_default_decoder_dict_to_frozendict(self) -> None: from josepy.json_util import Field obj = Field.default_decoder({"x": 2}) assert isinstance(obj, util.frozendict) assert obj == util.frozendict(x=2) def test_default_decoder_passthrough(self) -> None: mock_value = mock.MagicMock() from josepy.json_util import Field assert Field.default_decoder(mock_value) is mock_value class JSONObjectWithFieldsMetaTest(unittest.TestCase): """Tests for josepy.json_util.JSONObjectWithFieldsMeta.""" def setUp(self) -> None: from josepy.json_util import Field, JSONObjectWithFieldsMeta self.field = Field("Baz") self.field2 = Field("Baz2") class A(metaclass=JSONObjectWithFieldsMeta): __slots__ = ("bar",) baz = self.field class B(A): pass class C(A): baz = self.field2 self.a_cls = A self.b_cls = B self.c_cls = C def test_fields(self) -> None: assert {"baz": self.field} == self.a_cls._fields assert {"baz": self.field} == self.b_cls._fields def test_fields_inheritance(self) -> None: assert {"baz": self.field2} == self.c_cls._fields def test_slots(self) -> None: assert ("bar", "baz") == self.a_cls.__slots__ assert ("baz",) == self.b_cls.__slots__ def test_orig_slots(self) -> None: assert ("bar",) == self.a_cls._orig_slots assert () == self.b_cls._orig_slots class JSONObjectWithFieldsTest(unittest.TestCase): """Tests for josepy.json_util.JSONObjectWithFields.""" def setUp(self) -> None: from josepy.json_util import Field, JSONObjectWithFields class MockJSONObjectWithFields(JSONObjectWithFields): x = Field("x", omitempty=True, encoder=(lambda x: x * 2), decoder=(lambda x: x / 2)) y = Field("y") z = Field("Z") # on purpose uppercase @y.encoder # type: ignore def y(value): if value == 500: raise errors.SerializationError() return value @y.decoder # type: ignore def y(value): if value == 500: raise errors.DeserializationError() return value self.MockJSONObjectWithFields = MockJSONObjectWithFields self.mock = MockJSONObjectWithFields(x=None, y=2, z=3) def test_init_defaults(self) -> None: assert self.mock == self.MockJSONObjectWithFields(y=2, z=3) def test_encode(self) -> None: assert 10 == self.MockJSONObjectWithFields(x=5, y=0, z=0).encode("x") def test_encode_wrong_field(self) -> None: with pytest.raises(errors.Error): self.mock.encode("foo") def test_encode_serialization_error_passthrough(self) -> None: with pytest.raises(errors.SerializationError): self.MockJSONObjectWithFields(y=500, z=None).encode("y") def test_fields_to_partial_json_omits_empty(self) -> None: assert self.mock.fields_to_partial_json() == {"y": 2, "Z": 3} def test_fields_from_json_fills_default_for_empty(self) -> None: assert {"x": None, "y": 2, "z": 3} == self.MockJSONObjectWithFields.fields_from_json( {"y": 2, "Z": 3} ) def test_fields_from_json_fails_on_missing(self) -> None: with pytest.raises(errors.DeserializationError): self.MockJSONObjectWithFields.fields_from_json({"y": 0}) with pytest.raises(errors.DeserializationError): self.MockJSONObjectWithFields.fields_from_json({"Z": 0}) with pytest.raises(errors.DeserializationError): self.MockJSONObjectWithFields.fields_from_json({"x": 0, "y": 0}) with pytest.raises(errors.DeserializationError): self.MockJSONObjectWithFields.fields_from_json({"x": 0, "Z": 0}) def test_fields_to_partial_json_encoder(self) -> None: assert self.MockJSONObjectWithFields(x=1, y=2, z=3).to_partial_json() == { "x": 2, "y": 2, "Z": 3, } def test_fields_from_json_decoder(self) -> None: assert {"x": 2, "y": 2, "z": 3} == self.MockJSONObjectWithFields.fields_from_json( {"x": 4, "y": 2, "Z": 3} ) def test_fields_to_partial_json_error_passthrough(self) -> None: with pytest.raises(errors.SerializationError): self.MockJSONObjectWithFields(x=1, y=500, z=3).to_partial_json() def test_fields_from_json_error_passthrough(self) -> None: with pytest.raises(errors.DeserializationError): self.MockJSONObjectWithFields.from_json({"x": 4, "y": 500, "Z": 3}) class DeEncodersTest(unittest.TestCase): def setUp(self) -> None: self.b64_cert = ( "MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhM" "CVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKz" "ApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxF" "DASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIx" "ODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRI" "wEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTW" "ljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwD" "QYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1" "AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwE" "AATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMnd" "fk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o" ) self.b64_csr = ( "MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2F" "uMRIwEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECw" "wWVW5pdmVyc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb" "20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD_N_HW9As" "dRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3" "C5QIDAQABoCkwJwYJKoZIhvcNAQkOMRowGDAWBgNVHREEDzANggtleGFtcG" "xlLmNvbTANBgkqhkiG9w0BAQsFAANBAHJH_O6BtC9aGzEVCMGOZ7z9iIRHW" "Szr9x_bOzn7hLwsbXPAgO1QxEwL-X-4g20Gn9XBE1N9W6HCIEut2d8wACg" ) def test_encode_b64jose(self) -> None: from josepy.json_util import encode_b64jose encoded = encode_b64jose(b"x") assert isinstance(encoded, str) assert "eA" == encoded def test_decode_b64jose(self) -> None: from josepy.json_util import decode_b64jose decoded = decode_b64jose("eA") assert isinstance(decoded, bytes) assert b"x" == decoded def test_decode_b64jose_padding_error(self) -> None: from josepy.json_util import decode_b64jose with pytest.raises(errors.DeserializationError): decode_b64jose("x") def test_decode_b64jose_size(self) -> None: from josepy.json_util import decode_b64jose assert b"foo" == decode_b64jose("Zm9v", size=3) with pytest.raises(errors.DeserializationError): decode_b64jose("Zm9v", size=2) with pytest.raises(errors.DeserializationError): decode_b64jose("Zm9v", size=4) def test_decode_b64jose_minimum_size(self) -> None: from josepy.json_util import decode_b64jose assert b"foo" == decode_b64jose("Zm9v", size=3, minimum=True) assert b"foo" == decode_b64jose("Zm9v", size=2, minimum=True) with pytest.raises(errors.DeserializationError): decode_b64jose("Zm9v", size=4, minimum=True) def test_encode_hex16(self) -> None: from josepy.json_util import encode_hex16 encoded = encode_hex16(b"foo") assert "666f6f" == encoded assert isinstance(encoded, str) def test_decode_hex16(self) -> None: from josepy.json_util import decode_hex16 decoded = decode_hex16("666f6f") assert b"foo" == decoded assert isinstance(decoded, bytes) def test_decode_hex16_minimum_size(self) -> None: from josepy.json_util import decode_hex16 assert b"foo" == decode_hex16("666f6f", size=3, minimum=True) assert b"foo" == decode_hex16("666f6f", size=2, minimum=True) with pytest.raises(errors.DeserializationError): decode_hex16("666f6f", size=4, minimum=True) def test_decode_hex16_odd_length(self) -> None: from josepy.json_util import decode_hex16 with pytest.raises(errors.DeserializationError): decode_hex16("x") def test_encode_cert(self) -> None: from josepy.json_util import encode_cert assert self.b64_cert == encode_cert(CERT) def test_decode_cert(self) -> None: from josepy.json_util import decode_cert cert = decode_cert(self.b64_cert) assert isinstance(cert, util.ComparableX509) assert cert == CERT with pytest.raises(errors.DeserializationError): decode_cert("") def test_encode_csr(self) -> None: from josepy.json_util import encode_csr assert self.b64_csr == encode_csr(CSR) def test_decode_csr(self) -> None: from josepy.json_util import decode_csr csr = decode_csr(self.b64_csr) assert isinstance(csr, util.ComparableX509) assert csr == CSR with pytest.raises(errors.DeserializationError): decode_csr("") class TypedJSONObjectWithFieldsTest(unittest.TestCase): def setUp(self) -> None: from josepy.json_util import TypedJSONObjectWithFields class MockParentTypedJSONObjectWithFields(TypedJSONObjectWithFields): TYPES = {} type_field_name = "type" @MockParentTypedJSONObjectWithFields.register class MockTypedJSONObjectWithFields(MockParentTypedJSONObjectWithFields): foo: str typ = "test" __slots__ = ("foo",) @classmethod def fields_from_json(cls, jobj: Mapping[str, Any]) -> Dict[str, Any]: return {"foo": jobj["foo"]} def fields_to_partial_json(self) -> Any: return {"foo": self.foo} self.parent_cls = MockParentTypedJSONObjectWithFields self.msg = MockTypedJSONObjectWithFields(foo="bar") def test_to_partial_json(self) -> None: assert self.msg.to_partial_json() == { "type": "test", "foo": "bar", } def test_from_json_non_dict_fails(self) -> None: for value in [[], (), 5, "asd"]: # all possible input types with pytest.raises(errors.DeserializationError): # We're purposefully testing with the incorrect type here. self.parent_cls.from_json(value) # type: ignore def test_from_json_dict_no_type_fails(self) -> None: with pytest.raises(errors.DeserializationError): self.parent_cls.from_json({}) def test_from_json_unknown_type_fails(self) -> None: with pytest.raises(errors.UnrecognizedTypeError): self.parent_cls.from_json({"type": "bar"}) def test_from_json_returns_obj(self) -> None: assert {"foo": "bar"} == self.parent_cls.from_json({"type": "test", "foo": "bar"}) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8109448 josepy-1.14.0/tests/jwa_test.py0000644000000000000000000001604514520452152013376 0ustar00"""Tests for josepy.jwa.""" import sys import unittest from typing import Any from unittest import mock import pytest import test_util from josepy import errors RSA256_KEY = test_util.load_rsa_private_key("rsa256_key.pem") RSA512_KEY = test_util.load_rsa_private_key("rsa512_key.pem") RSA1024_KEY = test_util.load_rsa_private_key("rsa1024_key.pem") EC_P256_KEY = test_util.load_ec_private_key("ec_p256_key.pem") EC_P384_KEY = test_util.load_ec_private_key("ec_p384_key.pem") EC_P521_KEY = test_util.load_ec_private_key("ec_p521_key.pem") class JWASignatureTest(unittest.TestCase): """Tests for josepy.jwa.JWASignature.""" def setUp(self) -> None: from josepy.jwa import JWASignature class MockSig(JWASignature): def sign(self, key: Any, msg: bytes) -> bytes: raise NotImplementedError() # pragma: no cover def verify(self, key: Any, msg: bytes, sig: bytes) -> bool: raise NotImplementedError() # pragma: no cover self.Sig1 = MockSig("Sig1") self.Sig2 = MockSig("Sig2") def test_eq(self) -> None: assert self.Sig1 == self.Sig1 def test_ne(self) -> None: assert self.Sig1 != self.Sig2 def test_ne_other_type(self) -> None: assert self.Sig1 != 5 def test_repr(self) -> None: assert "Sig1" == repr(self.Sig1) assert "Sig2" == repr(self.Sig2) def test_to_partial_json(self) -> None: assert self.Sig1.to_partial_json() == "Sig1" assert self.Sig2.to_partial_json() == "Sig2" def test_from_json(self) -> None: from josepy.jwa import RS256, JWASignature assert JWASignature.from_json("RS256") is RS256 class JWAHSTest(unittest.TestCase): def test_it(self) -> None: from josepy.jwa import HS256 sig = ( b"\xceR\xea\xcd\x94\xab\xcf\xfb\xe0\xacA.:\x1a'\x08i\xe2\xc4" b"\r\x85+\x0e\x85\xaeUZ\xd4\xb3\x97zO" ) assert HS256.sign(b"some key", b"foo") == sig assert HS256.verify(b"some key", b"foo", sig) is True assert HS256.verify(b"some key", b"foo", sig + b"!") is False class JWARSTest(unittest.TestCase): def test_sign_no_private_part(self) -> None: from josepy.jwa import RS256 with pytest.raises(errors.Error): RS256.sign(RSA512_KEY.public_key(), b"foo") def test_sign_key_too_small(self) -> None: from josepy.jwa import PS256, RS256 with pytest.raises(errors.Error): RS256.sign(RSA256_KEY, b"foo") with pytest.raises(errors.Error): PS256.sign(RSA256_KEY, b"foo") def test_rs(self) -> None: from josepy.jwa import RS256 sig = ( b"|\xc6\xb2\xa4\xab(\x87\x99\xfa*:\xea\xf8\xa0N&}\x9f\x0f\xc0O" b'\xc6t\xa3\xe6\xfa\xbb"\x15Y\x80Y\xe0\x81\xb8\x88)\xba\x0c\x9c' b"\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99" b"\xd2\xb9.>}\xfd" ) assert RS256.sign(RSA512_KEY, b"foo") == sig assert RS256.verify(RSA512_KEY.public_key(), b"foo", sig) is True assert RS256.verify(RSA512_KEY.public_key(), b"foo", sig + b"!") is False def test_ps(self) -> None: from josepy.jwa import PS256 sig = PS256.sign(RSA1024_KEY, b"foo") assert PS256.verify(RSA1024_KEY.public_key(), b"foo", sig) is True assert PS256.verify(RSA1024_KEY.public_key(), b"foo", sig + b"!") is False def test_sign_new_api(self) -> None: from josepy.jwa import RS256 key = mock.MagicMock() RS256.sign(key, b"message") assert key.sign.called is True def test_verify_new_api(self) -> None: from josepy.jwa import RS256 key = mock.MagicMock() RS256.verify(key, b"message", b"signature") assert key.verify.called is True class JWAECTest(unittest.TestCase): def test_sign_no_private_part(self) -> None: from josepy.jwa import ES256 with pytest.raises(errors.Error): ES256.sign(EC_P256_KEY.public_key(), b"foo") def test_es256_sign_and_verify(self) -> None: from josepy.jwa import ES256 message = b"foo" signature = ES256.sign(EC_P256_KEY, message) assert ES256.verify(EC_P256_KEY.public_key(), message, signature) is True def test_es384_sign_and_verify(self) -> None: from josepy.jwa import ES384 message = b"foo" signature = ES384.sign(EC_P384_KEY, message) assert ES384.verify(EC_P384_KEY.public_key(), message, signature) is True def test_verify_with_wrong_jwa(self) -> None: from josepy.jwa import ES256, ES384 message = b"foo" signature = ES256.sign(EC_P256_KEY, message) assert ES384.verify(EC_P384_KEY.public_key(), message, signature) is False def test_verify_with_different_key(self) -> None: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec from josepy.jwa import ES256 message = b"foo" signature = ES256.sign(EC_P256_KEY, message) different_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) assert ES256.verify(different_key.public_key(), message, signature) is False def test_sign_new_api(self) -> None: from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 from josepy.jwa import ES256 key = mock.MagicMock(curve=SECP256R1()) with mock.patch("josepy.jwa.decode_dss_signature") as decode_patch: decode_patch.return_value = (0, 0) ES256.sign(key, b"message") assert key.sign.called is True def test_verify_new_api(self) -> None: import math from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 from josepy.jwa import ES256 key = mock.MagicMock(key_size=256, curve=SECP256R1()) ES256.verify(key, b"message", b"\x00" * math.ceil(key.key_size / 8) * 2) assert key.verify.called is True def test_signature_size(self) -> None: from josepy.jwa import ES512 from josepy.jwk import JWK key = JWK.from_json( { "d": ( "Af9KP6DqLRbtit6NS_LRIaCP_-NdC5l5R2ugbILdfpv6dS9R4wUPNxiGw" "-vVWumA56Yo1oBnEm8ZdR4W-u1lPHw5" ), "x": ( "AD4i4STyJ07iZJkHkpKEOuICpn6IHknzwAlrf-1w1a5dqOsRe30EECSN4vFxae" "AmtdBSCKBwCq7h1q4bPgMrMUvF" ), "y": ( "AHAlXxrabjcx_yBxGObnm_DkEQMJK1E69OHY3x3VxF5VXoKc93CG4GLoaPvphZQv" "Znt5EfExQoPktwOMIVhBHaFR" ), "crv": "P-521", "kty": "EC", } ) with mock.patch("josepy.jwa.decode_dss_signature") as decode_patch: decode_patch.return_value = (0, 0) assert isinstance(key, JWK) sig = ES512.sign(key.key, b"test") assert len(sig) == 2 * 66 if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8110952 josepy-1.14.0/tests/jwk_test.py0000644000000000000000000003216114520452152013405 0ustar00"""Tests for josepy.jwk.""" import binascii import sys import unittest from typing import TYPE_CHECKING import pytest import test_util from josepy import errors, json_util, util # The approach used here and below is based on # https://github.com/certbot/certbot/pull/8748. if TYPE_CHECKING: from typing_extensions import Protocol else: Protocol = object DSA_PEM = test_util.load_vector("dsa512_key.pem") RSA256_KEY = test_util.load_rsa_private_key("rsa256_key.pem") RSA512_KEY = test_util.load_rsa_private_key("rsa512_key.pem") EC_P256_KEY = test_util.load_ec_private_key("ec_p256_key.pem") EC_P384_KEY = test_util.load_ec_private_key("ec_p384_key.pem") EC_P521_KEY = test_util.load_ec_private_key("ec_p521_key.pem") class JWKTest(unittest.TestCase): """Tests for josepy.jwk.JWK.""" def test_load(self) -> None: from josepy.jwk import JWK with pytest.raises(errors.Error): JWK.load(DSA_PEM) def test_load_subclass_wrong_type(self) -> None: from josepy.jwk import JWKRSA with pytest.raises(errors.Error): JWKRSA.load(DSA_PEM) class JWKSubclassTest(Protocol): from josepy.jwk import JWK jwk: JWK thumbprint: bytes class JWKTestBaseMixin: """Mixin test for JWK subclass tests.""" thumbprint: bytes = NotImplemented def test_thumbprint_private(self: JWKSubclassTest) -> None: assert self.thumbprint == self.jwk.thumbprint() def test_thumbprint_public(self: JWKSubclassTest) -> None: assert self.thumbprint == self.jwk.public_key().thumbprint() class JWKOctTest(unittest.TestCase, JWKTestBaseMixin): """Tests for josepy.jwk.JWKOct.""" thumbprint = ( b"\xf3\xe7\xbe\xa8`\xd2\xdap\xe9}\x9c\xce>" b"\xd0\xfcI\xbe\xcd\x92'\xd4o\x0e\xf41\xea" b"\x8e(\x8a\xb2i\x1c" ) def setUp(self) -> None: from josepy.jwk import JWKOct self.jwk = JWKOct(key=b"foo") self.jobj = {"kty": "oct", "k": json_util.encode_b64jose(b"foo")} def test_to_partial_json(self) -> None: assert self.jwk.to_partial_json() == self.jobj def test_from_json(self) -> None: from josepy.jwk import JWKOct assert self.jwk == JWKOct.from_json(self.jobj) def test_from_json_hashable(self) -> None: from josepy.jwk import JWKOct hash(JWKOct.from_json(self.jobj)) def test_load(self) -> None: from josepy.jwk import JWKOct assert self.jwk == JWKOct.load(b"foo") def test_public_key(self) -> None: assert self.jwk.public_key() is self.jwk class JWKRSATest(unittest.TestCase, JWKTestBaseMixin): """Tests for josepy.jwk.JWKRSA.""" thumbprint = ( b"\x83K\xdc#3\x98\xca\x98\xed\xcb\x80\x80<\x0c" b"\xf0\x95\xb9H\xb2*l\xbd$\xe5&|O\x91\xd4 \xb0Y" ) def setUp(self) -> None: from josepy.jwk import JWKRSA self.jwk256 = JWKRSA(key=RSA256_KEY.public_key()) self.jwk256json = { "kty": "RSA", "e": "AQAB", "n": "m2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk", } self.jwk256_not_comparable = JWKRSA(key=RSA256_KEY.public_key()._wrapped) self.jwk512 = JWKRSA(key=RSA512_KEY.public_key()) self.jwk512json = { "kty": "RSA", "e": "AQAB", "n": "rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5" "80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q", } self.private = JWKRSA(key=RSA256_KEY) self.private_json_small = self.jwk256json.copy() self.private_json_small["d"] = "lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE" self.private_json = self.jwk256json.copy() self.private_json.update( { "d": "lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE", "p": "zUVNZn4lLLBD1R6NE8TKNQ", "q": "wcfKfc7kl5jfqXArCRSURQ", "dp": "CWJFq43QvT5Bm5iN8n1okQ", "dq": "bHh2u7etM8LKKCF2pY2UdQ", "qi": "oi45cEkbVoJjAbnQpFY87Q", } ) self.jwk = self.private def test_init_auto_comparable(self) -> None: assert isinstance(self.jwk256_not_comparable.key, util.ComparableRSAKey) assert self.jwk256 == self.jwk256_not_comparable def test_encode_param_zero(self) -> None: from josepy.jwk import JWKRSA # TODO: move encode/decode _param to separate class assert "AA" == JWKRSA._encode_param(0) def test_equals(self) -> None: assert self.jwk256 == self.jwk256 assert self.jwk512 == self.jwk512 def test_not_equals(self) -> None: assert self.jwk256 != self.jwk512 assert self.jwk512 != self.jwk256 def test_load(self) -> None: from josepy.jwk import JWKRSA assert self.private == JWKRSA.load(test_util.load_vector("rsa256_key.pem")) def test_public_key(self) -> None: assert self.jwk256 == self.private.public_key() def test_to_partial_json(self) -> None: assert self.jwk256.to_partial_json() == self.jwk256json assert self.jwk512.to_partial_json() == self.jwk512json assert self.private.to_partial_json() == self.private_json def test_from_json(self) -> None: from josepy.jwk import JWK assert self.jwk256 == JWK.from_json(self.jwk256json) assert self.jwk512 == JWK.from_json(self.jwk512json) assert self.private == JWK.from_json(self.private_json) def test_from_json_private_small(self) -> None: from josepy.jwk import JWK assert self.private == JWK.from_json(self.private_json_small) def test_from_json_missing_one_additional(self) -> None: from josepy.jwk import JWK del self.private_json["q"] with pytest.raises(errors.Error): JWK.from_json(self.private_json) def test_from_json_hashable(self) -> None: from josepy.jwk import JWK hash(JWK.from_json(self.jwk256json)) def test_from_json_non_schema_errors(self) -> None: # valid against schema, but still failing from josepy.jwk import JWK with pytest.raises(errors.DeserializationError): JWK.from_json({"kty": "RSA", "e": "AQAB", "n": ""}) with pytest.raises(errors.DeserializationError): JWK.from_json({"kty": "RSA", "e": "AQAB", "n": "1"}) def test_thumbprint_go_jose(self) -> None: # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk.go#L155 # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L331-L344 # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L384 from josepy.jwk import JWKRSA key = JWKRSA.json_loads( """{ "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB" }""" # noqa ) assert ( binascii.hexlify(key.thumbprint()) == b"f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932" ) class JWKECTest(unittest.TestCase, JWKTestBaseMixin): """Tests for josepy.jwk.JWKEC.""" # pylint: disable=too-many-instance-attributes thumbprint = ( b"\x06\xceL\x1b\xa8\x8d\x86\x1flF\x99J\x8b\xe0$\t\xbbj" b"\xd8\xf6O\x1ed\xdeR\x8f\x97\xff\xf6\xa2\x86\xd3" ) def setUp(self) -> None: from josepy.jwk import JWKEC self.jwk256 = JWKEC(key=EC_P256_KEY.public_key()) self.jwk384 = JWKEC(key=EC_P384_KEY.public_key()) self.jwk521 = JWKEC(key=EC_P521_KEY.public_key()) self.jwk256_not_comparable = JWKEC(key=EC_P256_KEY.public_key()._wrapped) self.jwk256json = { "kty": "EC", "crv": "P-256", "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", "y": "EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc", } self.jwk384json = { "kty": "EC", "crv": "P-384", "x": "tIhpNtEXkadUbrY84rYGgApFM1X_3l3EWQRuOP1IWtxlTftrZQwneJZF0k0eRn00", "y": "KW2Gp-TThDXmZ-9MJPnD8hv-X130SVvfZRl1a04HPVwIbvLe87mvA_iuOa-myUyv", } self.jwk521json = { "kty": "EC", "crv": "P-521", "x": "AFkdl6cKzBmP18U8fffpP4IZN2eED45hDcwRPl5ZeClwHcLtnMBMuWYFFO_Nzm6DL2MhpN0zI2bcMLJd95aY2tPs", # noqa "y": "AYvZq3wByjt7nQd8nYMqhFNCL3j_-U6GPWZet1hYBY_XZHrC4yIV0R4JnssRAY9eqc1EElpCc4hziis1jiV1iR4W", # noqa } self.private = JWKEC(key=EC_P256_KEY) self.private_json = { "d": "xReNQBKqqTthG8oTmBdhp4EQYImSK1dVqfa2yyMn2rc", "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", "y": "EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc", "crv": "P-256", "kty": "EC", } self.jwk = self.private def test_init_auto_comparable(self) -> None: assert isinstance(self.jwk256_not_comparable.key, util.ComparableECKey) assert self.jwk256 == self.jwk256_not_comparable def test_encode_param_zero(self) -> None: from josepy.jwk import JWKEC # pylint: disable=protected-access # TODO: move encode/decode _param to separate class assert "AA" == JWKEC._encode_param(0, 1) def test_equals(self) -> None: assert self.jwk256 == self.jwk256 assert self.jwk384 == self.jwk384 assert self.jwk521 == self.jwk521 def test_not_equals(self) -> None: assert self.jwk256 != self.jwk384 assert self.jwk256 != self.jwk521 assert self.jwk384 != self.jwk256 assert self.jwk384 != self.jwk521 assert self.jwk521 != self.jwk256 assert self.jwk521 != self.jwk384 def test_load(self) -> None: from josepy.jwk import JWKEC assert self.private == JWKEC.load(test_util.load_vector("ec_p256_key.pem")) def test_public_key(self) -> None: assert self.jwk256 == self.private.public_key() def test_to_partial_json(self) -> None: assert self.jwk256.to_partial_json() == self.jwk256json assert self.jwk384.to_partial_json() == self.jwk384json assert self.jwk521.to_partial_json() == self.jwk521json assert self.private.to_partial_json() == self.private_json def test_from_json(self) -> None: from josepy.jwk import JWK assert self.jwk256 == JWK.from_json(self.jwk256json) assert self.jwk384 == JWK.from_json(self.jwk384json) assert self.jwk521 == JWK.from_json(self.jwk521json) assert self.private == JWK.from_json(self.private_json) def test_from_json_missing_x_coordinate(self) -> None: from josepy.jwk import JWK del self.private_json["x"] with pytest.raises(KeyError): JWK.from_json(self.private_json) def test_from_json_missing_y_coordinate(self) -> None: from josepy.jwk import JWK del self.private_json["y"] with pytest.raises(KeyError): JWK.from_json(self.private_json) def test_from_json_hashable(self) -> None: from josepy.jwk import JWK hash(JWK.from_json(self.jwk256json)) def test_from_json_non_schema_errors(self) -> None: # valid against schema, but still failing from josepy.jwk import JWK with pytest.raises(errors.DeserializationError): JWK.from_json( { "kty": "EC", "crv": "P-256", "x": "AQAB", "y": "m2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk", } ) with pytest.raises(errors.DeserializationError): JWK.from_json( { "kty": "EC", "crv": "P-256", "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", "y": "1", } ) def test_unknown_crv_name(self) -> None: from josepy.jwk import JWK with pytest.raises(errors.DeserializationError): JWK.from_json( { "kty": "EC", "crv": "P-255", "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", "y": "EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc", } ) def test_encode_y_leading_zero_p256(self) -> None: import josepy from josepy.jwk import JWK, JWKEC data = b"""-----BEGIN EC PRIVATE KEY----- MHcCAQEEICZ7LCI99Na2KZ/Fq8JmJROakGJ5+J7rHiGSPoO36kOAoAoGCCqGSM49 AwEHoUQDQgAEGS5RvStca15z2FEanCM3juoX7tE/LB7iD44GWawGE40APAl/iZuH 31wQfst4glTZpxkpEI/MzNZHjiYnqrGeSw== -----END EC PRIVATE KEY-----""" key = JWKEC.load(data) json = key.to_partial_json() y = josepy.json_util.decode_b64jose(json["y"]) assert y[0] == 0 assert len(y) == 32 JWK.from_json(json) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8112416 josepy-1.14.0/tests/jws_test.py0000644000000000000000000002067114520452152013420 0ustar00"""Tests for josepy.jws.""" import base64 import sys import unittest from unittest import mock import OpenSSL import pytest import test_util from josepy import errors, json_util, jwa, jwk CERT = test_util.load_comparable_cert("cert.pem") KEY = jwk.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class MediaTypeTest(unittest.TestCase): """Tests for josepy.jws.MediaType.""" def test_decode(self) -> None: from josepy.jws import MediaType assert "application/app" == MediaType.decode("application/app") assert "application/app" == MediaType.decode("app") with pytest.raises(errors.DeserializationError): MediaType.decode("app;foo") def test_encode(self) -> None: from josepy.jws import MediaType assert "app" == MediaType.encode("application/app") assert "application/app;foo" == MediaType.encode("application/app;foo") class HeaderTest(unittest.TestCase): """Tests for josepy.jws.Header.""" def setUp(self) -> None: from josepy.jws import Header self.header1 = Header(jwk="foo") self.header2 = Header(jwk="bar") self.crit = Header(crit=("a", "b")) self.empty = Header() def test_add_non_empty(self) -> None: from josepy.jws import Header assert Header(jwk="foo", crit=("a", "b")) == self.header1 + self.crit def test_add_empty(self) -> None: assert self.header1 == self.header1 + self.empty assert self.header1 == self.empty + self.header1 def test_add_overlapping_error(self) -> None: with pytest.raises(TypeError): self.header1.__add__(self.header2) def test_add_wrong_type_error(self) -> None: with pytest.raises(TypeError): self.header1.__add__("xxx") def test_crit_decode_always_errors(self) -> None: from josepy.jws import Header with pytest.raises(errors.DeserializationError): Header.from_json({"crit": ["a", "b"]}) def test_x5c_decoding(self) -> None: from josepy.jws import Header header = Header(x5c=(CERT, CERT)) jobj = header.to_partial_json() assert isinstance(CERT.wrapped, OpenSSL.crypto.X509) cert_asn1 = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped) cert_b64 = base64.b64encode(cert_asn1) assert jobj == {"x5c": [cert_b64, cert_b64]} assert header == Header.from_json(jobj) jobj["x5c"][0] = base64.b64encode(b"xxx" + cert_asn1) with pytest.raises(errors.DeserializationError): Header.from_json(jobj) def test_find_key(self) -> None: assert "foo" == self.header1.find_key() assert "bar" == self.header2.find_key() with pytest.raises(errors.Error): self.crit.find_key() class SignatureTest(unittest.TestCase): """Tests for josepy.jws.Signature.""" def test_from_json(self) -> None: from josepy.jws import Header, Signature assert Signature(signature=b"foo", header=Header(alg=jwa.RS256)) == Signature.from_json( {"signature": "Zm9v", "header": {"alg": "RS256"}} ) def test_from_json_no_alg_error(self) -> None: from josepy.jws import Signature with pytest.raises(errors.DeserializationError): Signature.from_json({"signature": "foo"}) class JWSTest(unittest.TestCase): """Tests for josepy.jws.JWS.""" def setUp(self) -> None: self.privkey = KEY self.pubkey = self.privkey.public_key() from josepy.jws import JWS self.unprotected = JWS.sign(payload=b"foo", key=self.privkey, alg=jwa.RS256) self.protected = JWS.sign( payload=b"foo", key=self.privkey, alg=jwa.RS256, protect=frozenset(["jwk", "alg"]) ) self.mixed = JWS.sign( payload=b"foo", key=self.privkey, alg=jwa.RS256, protect=frozenset(["alg"]) ) def test_pubkey_jwk(self) -> None: assert self.unprotected.signature.combined.jwk == self.pubkey assert self.protected.signature.combined.jwk == self.pubkey assert self.mixed.signature.combined.jwk == self.pubkey def test_sign_unprotected(self) -> None: assert self.unprotected.verify() is True def test_sign_protected(self) -> None: assert self.protected.verify() is True def test_sign_mixed(self) -> None: assert self.mixed.verify() is True def test_compact_lost_unprotected(self) -> None: compact = self.mixed.to_compact() assert ( b"eyJhbGciOiAiUlMyNTYifQ.Zm9v.OHdxFVj73l5LpxbFp1AmYX4yJM0Pyb" b"_893n1zQjpim_eLS5J1F61lkvrCrCDErTEJnBGOGesJ72M7b6Ve1cAJA" == compact ) from josepy.jws import JWS mixed = JWS.from_compact(compact) assert self.mixed != mixed assert {"alg"} == set(mixed.signature.combined.not_omitted()) def test_from_compact_missing_components(self) -> None: from josepy.jws import JWS with pytest.raises(errors.DeserializationError): JWS.from_compact(b".") def test_json_omitempty(self) -> None: protected_jobj = self.protected.to_partial_json(flat=True) unprotected_jobj = self.unprotected.to_partial_json(flat=True) assert "protected" not in unprotected_jobj assert "header" not in protected_jobj unprotected_jobj["header"] = unprotected_jobj["header"].to_json() from josepy.jws import JWS assert JWS.from_json(protected_jobj) == self.protected assert JWS.from_json(unprotected_jobj) == self.unprotected def test_json_flat(self) -> None: jobj_to = { "signature": json_util.encode_b64jose(self.mixed.signature.signature), "payload": json_util.encode_b64jose(b"foo"), "header": self.mixed.signature.header, "protected": json_util.encode_b64jose(self.mixed.signature.protected.encode("utf-8")), } from josepy.jws import Header jobj_from = jobj_to.copy() header = jobj_from["header"] assert isinstance(header, Header) jobj_from["header"] = header.to_json() assert self.mixed.to_partial_json(flat=True) == jobj_to from josepy.jws import JWS assert self.mixed == JWS.from_json(jobj_from) def test_json_not_flat(self) -> None: jobj_to = { "signatures": (self.mixed.signature,), "payload": json_util.encode_b64jose(b"foo"), } jobj_from = jobj_to.copy() signature = jobj_to["signatures"][0] from josepy.jws import Signature assert isinstance(signature, Signature) jobj_from["signatures"] = [signature.to_json()] assert self.mixed.to_partial_json(flat=False) == jobj_to from josepy.jws import JWS assert self.mixed == JWS.from_json(jobj_from) def test_from_json_mixed_flat(self) -> None: from josepy.jws import JWS with pytest.raises(errors.DeserializationError): JWS.from_json({"signatures": (), "signature": "foo"}) def test_from_json_hashable(self) -> None: from josepy.jws import JWS hash(JWS.from_json(self.mixed.to_json())) class CLITest(unittest.TestCase): def setUp(self) -> None: self.key_path = test_util.vector_path("rsa512_key.pem") def test_unverified(self) -> None: from josepy.jws import CLI with mock.patch("sys.stdin") as sin: sin.read.return_value = '{"payload": "foo", "signature": "xxx"}' with mock.patch("sys.stdout"): assert CLI.run(["verify"]) is False def test_json(self) -> None: from josepy.jws import CLI with mock.patch("sys.stdin") as sin: sin.read.return_value = "foo" with mock.patch("sys.stdout") as sout: CLI.run(["sign", "-k", self.key_path, "-a", "RS256", "-p", "jwk"]) sin.read.return_value = sout.write.mock_calls[0][1][0] assert 0 == CLI.run(["verify"]) def test_compact(self) -> None: from josepy.jws import CLI with mock.patch("sys.stdin") as sin: sin.read.return_value = "foo" with mock.patch("sys.stdout") as sout: CLI.run(["--compact", "sign", "-k", self.key_path]) sin.read.return_value = sout.write.mock_calls[0][1][0] assert 0 == CLI.run(["--compact", "verify", "--kty", "RSA", "-k", self.key_path]) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8113623 josepy-1.14.0/tests/magic_typing_test.py0000644000000000000000000000145514520452152015266 0ustar00"""Tests for josepy.magic_typing.""" import sys import warnings from unittest import mock import pytest def test_import_success() -> None: import typing as temp_typing typing_class_mock = mock.MagicMock() text_mock = mock.MagicMock() typing_class_mock.Text = text_mock sys.modules["typing"] = typing_class_mock if "josepy.magic_typing" in sys.modules: del sys.modules["josepy.magic_typing"] # pragma: no cover with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) from josepy.magic_typing import Text assert Text == text_mock del sys.modules["josepy.magic_typing"] sys.modules["typing"] = temp_typing if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8114953 josepy-1.14.0/tests/test_util.py0000644000000000000000000000644414520452152013574 0ustar00"""Test utilities.""" import atexit import contextlib import os import sys from typing import Any from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from OpenSSL import crypto import josepy.util from josepy import ComparableRSAKey, ComparableX509 from josepy.util import ComparableECKey # This approach is based on the recommendation at # https://github.com/python/mypy/issues/1153#issuecomment-1207333806. if sys.version_info >= (3, 9): import importlib.resources as importlib_resources else: import importlib_resources TESTDATA = importlib_resources.files("testdata") def vector_path(*names: str) -> str: """Path to a test vector.""" # This code is based on the recommendation at # https://web.archive.org/web/20230131043552/https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-filename. file_manager = contextlib.ExitStack() atexit.register(file_manager.close) ref = TESTDATA.joinpath(*names) # We convert the value to str here because some of the calling code doesn't # work with pathlib objects. return str(file_manager.enter_context(importlib_resources.as_file(ref))) def load_vector(*names: str) -> bytes: """Load contents of a test vector.""" return TESTDATA.joinpath(*names).read_bytes() def _guess_loader(filename: str, loader_pem: Any, loader_der: Any) -> Any: _, ext = os.path.splitext(filename) if ext.lower() == ".pem": return loader_pem elif ext.lower() == ".der": return loader_der else: # pragma: no cover raise ValueError("Loader could not be recognized based on extension") def load_cert(*names: str) -> crypto.X509: """Load certificate.""" loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate(loader, load_vector(*names)) def load_comparable_cert(*names: str) -> josepy.util.ComparableX509: """Load ComparableX509 cert.""" return ComparableX509(load_cert(*names)) def load_csr(*names: str) -> crypto.X509Req: """Load certificate request.""" loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate_request(loader, load_vector(*names)) def load_comparable_csr(*names: str) -> josepy.util.ComparableX509: """Load ComparableX509 certificate request.""" return ComparableX509(load_csr(*names)) def load_rsa_private_key(*names: str) -> josepy.util.ComparableRSAKey: """Load RSA private key.""" loader = _guess_loader( names[-1], serialization.load_pem_private_key, serialization.load_der_private_key ) return ComparableRSAKey(loader(load_vector(*names), password=None, backend=default_backend())) def load_ec_private_key(*names: str) -> josepy.util.ComparableECKey: """Load EC private key.""" loader = _guess_loader( names[-1], serialization.load_pem_private_key, serialization.load_der_private_key ) return ComparableECKey(loader(load_vector(*names), password=None, backend=default_backend())) def load_pyopenssl_private_key(*names: str) -> crypto.PKey: """Load pyOpenSSL private key.""" loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_privatekey(loader, load_vector(*names)) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.811604 josepy-1.14.0/tests/testdata/README0000644000000000000000000000130714520452152013670 0ustar00In order for josepy.test_util._guess_loader to work properly, make sure to use appropriate extension for vector filenames: .pem for PEM and .der for DER. The following command has been used to generate test keys: for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done openssl ecparam -name prime256v1 -genkey -out ec_p256_key.pem openssl ecparam -name secp384r1 -genkey -out ec_p384_key.pem openssl ecparam -name secp521r1 -genkey -out ec_p521_key.pem and for the CSR: openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der and for the certificate: openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8116364 josepy-1.14.0/tests/testdata/__init__.py0000644000000000000000000000000014520452152015106 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8117142 josepy-1.14.0/tests/testdata/cert-100sans.pem0000644000000000000000000000530014520452152015630 0ustar00-----BEGIN CERTIFICATE----- MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3 LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5 LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1 LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8117888 josepy-1.14.0/tests/testdata/cert-idnsans.pem0000644000000000000000000000351214520452152016105 0ustar00-----BEGIN CERTIFICATE----- MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM 2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf 2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3 2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi 2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi 2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN 247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4 27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt 4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm 4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2 LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8118541 josepy-1.14.0/tests/testdata/cert-san.pem0000644000000000000000000000142214520452152015225 0ustar00-----BEGIN CERTIFICATE----- MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 RDjyGMKy5ZgM2w== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8119147 josepy-1.14.0/tests/testdata/cert.der0000644000000000000000000000140314520452152014436 0ustar0000 2WZd0  *H  010U example.com0 151028072441Z 151127072441Z010U example.com0"0  *H 0 |0w3$Gav1.k>=ae\u[DnXߋ":ājP]teMp*8(if3-/e\vui DKlK˷ nal3g3$aLOGL) 3S񪓪̀,˞j s|H"0yc_M=.ۢh5JIh^+WɯFWs;?Gs9_̆eƞT4)kP0N0U6  )+eX70U#06  )+eX70 U00  *H  T2O]J7 ԣN^ x1[IK,=^2k뮫mHG[2-mlhfll@'DpH4C'{[C c60u;d FAzk&eP Ǫa3EuPjW3u ,S~C6Q8YOfs UpM!ȑ)+m,(hE {b KRVhŗ ;lv1././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8119688 josepy-1.14.0/tests/testdata/cert.pem0000644000000000000000000000130514520452152014446 0ustar00-----BEGIN CERTIFICATE----- MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn B/o= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8120356 josepy-1.14.0/tests/testdata/critical-san.pem0000644000000000000000000000322314520452152016063 0ustar00-----BEGIN CERTIFICATE----- MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5 MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M 0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU 0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0 dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv KA== -----END CERTIFICATE-----././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1698845801.8121 josepy-1.14.0/tests/testdata/csr-100sans.pem0000644000000000000000000000502114520452152015462 0ustar00-----BEGIN CERTIFICATE REQUEST----- MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4 YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4 YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4 YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1 My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4 YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2 OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4 YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4 NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4 YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3 DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8121645 josepy-1.14.0/tests/testdata/csr-6sans.pem0000644000000000000000000000124414520452152015332 0ustar00-----BEGIN CERTIFICATE REQUEST----- MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv IvzVBz/nD11drfz/RNuX -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8122313 josepy-1.14.0/tests/testdata/csr-idnsans.pem0000644000000000000000000000323314520452152015737 0ustar00-----BEGIN CERTIFICATE REQUEST----- MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5 4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ 4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ 4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.812292 josepy-1.14.0/tests/testdata/csr-nosans.pem0000644000000000000000000000070414520452152015601 0ustar00-----BEGIN CERTIFICATE REQUEST----- MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8123577 josepy-1.14.0/tests/testdata/csr-san.pem0000644000000000000000000000107614520452152015064 0ustar00-----BEGIN CERTIFICATE REQUEST----- MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8125281 josepy-1.14.0/tests/testdata/csr.der0000644000000000000000000000113714520452152014274 0ustar000[0C010U example.com0"0  *H 0 |0w3$Gav1.k>=ae\u[DnXߋ":ājP]teMp*8(if3-/e\vui DKlK˷ nal3g3$aLOGL) 3S񪓪̀,˞j s|H"0yc_M=.ۢh5JIh^+WɯFWs;?Gs9_̆eƞT4)k0  *H  r0J0*;s k[-I%I!6gu%i8O}D6>:}_QMKYB4a861p @TEXUBï Pr>h6(.Μhw ٖqaQ_PkN=EКS?E %OdѻK=.gh첶Be+_)uUusZQÍV8s0Qr;`z=FI././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8125849 josepy-1.14.0/tests/testdata/csr.pem0000644000000000000000000000104614520452152014302 0ustar00-----BEGIN CERTIFICATE REQUEST----- MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G n9XBE1N9W6HCIEut2d8wACg= -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.812649 josepy-1.14.0/tests/testdata/dsa512_key.pem0000644000000000000000000000125414520452152015363 0ustar00-----BEGIN DSA PARAMETERS----- MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl 41pgNJpgu99YOYqPpS0g7A== -----END DSA PARAMETERS----- -----BEGIN DSA PRIVATE KEY----- MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE mNv063So6E+eYaIN -----END DSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8127682 josepy-1.14.0/tests/testdata/ec_p256_key.pem0000644000000000000000000000045514520452152015531 0ustar00-----BEGIN EC PARAMETERS----- BggqhkjOPQMBBw== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MHcCAQEEIMUXjUASqqk7YRvKE5gXYaeBEGCJkitXVan2tssjJ9q3oAoGCCqGSM49 AwEHoUQDQgAEjjQtV+fA7J/tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5UQ8DDz/zPs 9gqwcfqGUZKWxbEWgWXv7S8zRBEZuae8Jw== -----END EC PRIVATE KEY-----././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.812879 josepy-1.14.0/tests/testdata/ec_p384_key.pem0000644000000000000000000000054614520452152015534 0ustar00-----BEGIN EC PARAMETERS----- BgUrgQQAIg== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MIGkAgEBBDBHGWgnk8VgEwuXadVIOaCXw+MJ7qVrSHSiAOZri9LExJGSioGpGtZa fzqLGitkNAygBwYFK4EEACKhZANiAAS0iGk20ReRp1RutjzitgaACkUzVf/eXcRZ BG44/Uha3GVN+2tlDCd4lkXSTR5GfTQpbYan5NOENeZn70wk+cPyG/5fXfRJW99l GXVrTgc9XAhu8t7zua8D+K45r6bJTK8= -----END EC PRIVATE KEY-----././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1698845801.812992 josepy-1.14.0/tests/testdata/ec_p521_key.pem0000644000000000000000000000066314520452152015525 0ustar00-----BEGIN EC PARAMETERS----- BgUrgQQAIw== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIB9I82YbB0mhnHdWOnwP4Ag0Gl2x+tKYnfTWNEaqW0+cztSrI0OD/W WtjJo+8fC+2QWGxV4l1sHpXebBnTG8NWS2OgBwYFK4EEACOhgYkDgYYABABZHZen CswZj9fFPH336T+CGTdnhA+OYQ3MET5eWXgpcB3C7ZzATLlmBRTvzc5ugy9jIaTd MyNm3DCyXfeWmNrT7AGL2at8Aco7e50HfJ2DKoRTQi94//lOhj1mXrdYWAWP12R6 wuMiFdEeCZ7LEQGPXqnNRBJaQnOIc4orNY4ldYkeFg== -----END EC PRIVATE KEY-----././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8130527 josepy-1.14.0/tests/testdata/rsa1024_key.pem0000644000000000000000000000156714520452152015467 0ustar00-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi 4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/ w4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB AoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB Ia+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc TmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB CZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X UTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak Co9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt 73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa HgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU 6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ c2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc= -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8131106 josepy-1.14.0/tests/testdata/rsa2048_cert.pem0000644000000000000000000000241614520452152015635 0ustar00-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm 7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1 RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8 kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+ aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf Esg= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8131728 josepy-1.14.0/tests/testdata/rsa2048_key.pem0000644000000000000000000000325014520452152015465 0ustar00-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3 3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/ IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/ 9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj 9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3 YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3 xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6 kmf1UgGFcKrJuXgwEtTVxw== -----END PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8132286 josepy-1.14.0/tests/testdata/rsa256_key.pem0000644000000000000000000000045214520452152015405 0ustar00-----BEGIN RSA PRIVATE KEY----- MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8132896 josepy-1.14.0/tests/testdata/rsa512_key.pem0000644000000000000000000000075514520452152015406 0ustar00-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1698845801.8134267 josepy-1.14.0/tests/util_test.py0000644000000000000000000002000614520452152013562 0ustar00"""Tests for josepy.util.""" import functools import sys import unittest import pytest import test_util class ComparableX509Test(unittest.TestCase): """Tests for josepy.util.ComparableX509.""" def setUp(self) -> None: # test_util.load_comparable_{csr,cert} return ComparableX509 self.req1 = test_util.load_comparable_csr("csr.pem") self.req2 = test_util.load_comparable_csr("csr.pem") self.req_other = test_util.load_comparable_csr("csr-san.pem") self.cert1 = test_util.load_comparable_cert("cert.pem") self.cert2 = test_util.load_comparable_cert("cert.pem") self.cert_other = test_util.load_comparable_cert("cert-san.pem") def test_getattr_proxy(self) -> None: assert self.cert1.has_expired() is True def test_eq(self) -> None: assert self.req1 == self.req2 assert self.cert1 == self.cert2 def test_ne(self) -> None: assert self.req1 != self.req_other assert self.cert1 != self.cert_other def test_ne_wrong_types(self) -> None: assert self.req1 != 5 assert self.cert1 != 5 def test_hash(self) -> None: assert hash(self.req1) == hash(self.req2) assert hash(self.req1) != hash(self.req_other) assert hash(self.cert1) == hash(self.cert2) assert hash(self.cert1) != hash(self.cert_other) def test_repr(self) -> None: for x509 in self.req1, self.cert1: assert repr(x509) == "".format(x509.wrapped) class ComparableRSAKeyTest(unittest.TestCase): """Tests for josepy.util.ComparableRSAKey.""" def setUp(self) -> None: # test_utl.load_rsa_private_key return ComparableRSAKey self.key = test_util.load_rsa_private_key("rsa256_key.pem") self.key_same = test_util.load_rsa_private_key("rsa256_key.pem") self.key2 = test_util.load_rsa_private_key("rsa512_key.pem") def test_getattr_proxy(self) -> None: assert 256 == self.key.key_size def test_eq(self) -> None: assert self.key == self.key_same def test_ne(self) -> None: assert self.key != self.key2 def test_ne_different_types(self) -> None: assert self.key != 5 def test_ne_not_wrapped(self) -> None: assert self.key != self.key_same._wrapped def test_ne_no_serialization(self) -> None: from josepy.util import ComparableRSAKey assert ComparableRSAKey(5) != ComparableRSAKey(5) # type: ignore def test_hash(self) -> None: assert isinstance(hash(self.key), int) assert hash(self.key) == hash(self.key_same) assert hash(self.key) != hash(self.key2) def test_repr(self) -> None: assert repr(self.key).startswith(" None: from josepy.util import ComparableRSAKey assert isinstance(self.key.public_key(), ComparableRSAKey) class ComparableECKeyTest(unittest.TestCase): """Tests for josepy.util.ComparableECKey.""" def setUp(self) -> None: # test_utl.load_ec_private_key return ComparableECKey self.p256_key = test_util.load_ec_private_key("ec_p256_key.pem") self.p256_key_same = test_util.load_ec_private_key("ec_p256_key.pem") self.p384_key = test_util.load_ec_private_key("ec_p384_key.pem") self.p521_key = test_util.load_ec_private_key("ec_p521_key.pem") def test_getattr_proxy(self) -> None: assert 256 == self.p256_key.key_size def test_eq(self) -> None: assert self.p256_key == self.p256_key_same def test_ne(self) -> None: assert self.p256_key != self.p384_key assert self.p256_key != self.p521_key def test_ne_different_types(self) -> None: assert self.p256_key != 5 def test_ne_not_wrapped(self) -> None: assert self.p256_key != self.p256_key_same._wrapped def test_ne_no_serialization(self) -> None: from josepy.util import ComparableECKey assert ComparableECKey(5) != ComparableECKey(5) # type: ignore def test_hash(self) -> None: assert isinstance(hash(self.p256_key), int) assert hash(self.p256_key) == hash(self.p256_key_same) assert hash(self.p256_key) != hash(self.p384_key) assert hash(self.p256_key) != hash(self.p521_key) def test_repr(self) -> None: assert repr(self.p256_key).startswith(" None: from josepy.util import ComparableECKey assert isinstance(self.p256_key.public_key(), ComparableECKey) class ImmutableMapTest(unittest.TestCase): """Tests for josepy.util.ImmutableMap.""" def setUp(self) -> None: from josepy.util import ImmutableMap class A(ImmutableMap): x: int y: int __slots__ = ("x", "y") class B(ImmutableMap): x: int y: int __slots__ = ("x", "y") self.A = A self.B = B self.a1 = self.A(x=1, y=2) self.a1_swap = self.A(y=2, x=1) self.a2 = self.A(x=3, y=4) self.b = self.B(x=1, y=2) def test_update(self) -> None: assert self.A(x=2, y=2) == self.a1.update(x=2) assert self.a2 == self.a1.update(x=3, y=4) def test_get_missing_item_raises_key_error(self) -> None: with pytest.raises(KeyError): self.a1.__getitem__("z") def test_order_of_args_does_not_matter(self) -> None: assert self.a1 == self.a1_swap def test_type_error_on_missing(self) -> None: with pytest.raises(TypeError): self.A(x=1) with pytest.raises(TypeError): self.A(y=2) def test_type_error_on_unrecognized(self) -> None: with pytest.raises(TypeError): self.A(x=1, z=2) with pytest.raises(TypeError): self.A(x=1, y=2, z=3) def test_get_attr(self) -> None: assert 1 == self.a1.x assert 2 == self.a1.y assert 1 == self.a1_swap.x assert 2 == self.a1_swap.y def test_set_attr_raises_attribute_error(self) -> None: with pytest.raises(AttributeError): functools.partial(self.a1.__setattr__, "x")(10) def test_equal(self) -> None: assert self.a1 == self.a1 assert self.a2 == self.a2 assert self.a1 != self.a2 def test_hash(self) -> None: assert hash((1, 2)) == hash(self.a1) def test_unhashable(self) -> None: with pytest.raises(TypeError): self.A(x=1, y={}).__hash__() def test_repr(self) -> None: assert "A(x=1, y=2)" == repr(self.a1) assert "A(x=1, y=2)" == repr(self.a1_swap) assert "B(x=1, y=2)" == repr(self.b) assert "B(x='foo', y='bar')" == repr(self.B(x="foo", y="bar")) class frozendictTest(unittest.TestCase): """Tests for josepy.util.frozendict.""" def setUp(self) -> None: from josepy.util import frozendict self.fdict = frozendict(x=1, y="2") def test_init_dict(self) -> None: from josepy.util import frozendict assert self.fdict == frozendict({"x": 1, "y": "2"}) def test_init_other_raises_type_error(self) -> None: from josepy.util import frozendict # specifically fail for generators... with pytest.raises(TypeError): frozendict({"a": "b"}.items()) def test_len(self) -> None: assert 2 == len(self.fdict) def test_hash(self) -> None: assert isinstance(hash(self.fdict), int) def test_getattr_proxy(self) -> None: assert 1 == self.fdict.x assert "2" == self.fdict.y def test_getattr_raises_attribute_error(self) -> None: with pytest.raises(AttributeError): self.fdict.__getattr__("z") def test_setattr_immutable(self) -> None: with pytest.raises(AttributeError): self.fdict.__setattr__("z", 3) def test_repr(self) -> None: assert "frozendict(x=1, y='2')" == repr(self.fdict) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover josepy-1.14.0/PKG-INFO0000644000000000000000000000341100000000000011071 0ustar00Metadata-Version: 2.1 Name: josepy Version: 1.14.0 Summary: JOSE protocol implementation in Python Home-page: https://github.com/certbot/josepy License: Apache-2.0 Author: Certbot Project Author-email: certbot-dev@eff.org Requires-Python: >=3.7,<4.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Provides-Extra: docs Requires-Dist: cryptography (>=1.5) Requires-Dist: pyopenssl (>=0.13) Requires-Dist: sphinx (>=4.3.0) ; extra == "docs" Requires-Dist: sphinx-rtd-theme (>=1.0) ; extra == "docs" Description-Content-Type: text/x-rst JOSE protocol implementation in Python using cryptography .. image:: https://github.com/certbot/josepy/actions/workflows/check.yaml/badge.svg :target: https://github.com/certbot/josepy/actions/workflows/check.yaml .. image:: https://readthedocs.org/projects/josepy/badge/?version=latest :target: http://josepy.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black Originally developed as part of the ACME_ protocol implementation. .. _ACME: https://pypi.python.org/pypi/acme To learn how to contribute to this project, see CONTRIBUTING.md_. .. _CONTRIBUTING.md: CONTRIBUTING.md