├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── assigned.yaml │ ├── check.yaml │ ├── merged.yaml │ ├── review_requested.yaml │ └── stale.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.rst ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.rst ├── SECURITY.md ├── docs ├── .gitignore ├── Makefile ├── _static │ └── .gitignore ├── _templates │ └── .gitignore ├── api │ ├── base64.rst │ ├── errors.rst │ ├── interfaces.rst │ ├── json_util.rst │ ├── jwa.rst │ ├── jwk.rst │ ├── jws.rst │ └── util.rst ├── changelog.rst ├── conf.py ├── index.rst ├── jws-help.txt └── man │ └── jws.rst ├── examples └── pem_conversion.py ├── mattermost.json ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── src └── josepy │ ├── __init__.py │ ├── b64.py │ ├── errors.py │ ├── interfaces.py │ ├── json_util.py │ ├── jwa.py │ ├── jwk.py │ ├── jws.py │ ├── magic_typing.py │ ├── py.typed │ └── util.py ├── tests ├── b64_test.py ├── errors_test.py ├── interfaces_test.py ├── json_util_test.py ├── jwa_test.py ├── jwk_test.py ├── jws_test.py ├── magic_typing_test.py ├── test_util.py ├── testdata │ ├── README │ ├── __init__.py │ ├── cert-100sans.pem │ ├── cert-idnsans.pem │ ├── cert-san.pem │ ├── cert.der │ ├── cert.pem │ ├── critical-san.pem │ ├── csr-100sans.pem │ ├── csr-6sans.pem │ ├── csr-idnsans.pem │ ├── csr-nosans.pem │ ├── csr-san.pem │ ├── csr.der │ ├── csr.pem │ ├── dsa512_key.pem │ ├── ec_p256_key.pem │ ├── ec_p384_key.pem │ ├── ec_p521_key.pem │ ├── rsa1024_key.pem │ ├── rsa2048_cert.pem │ ├── rsa2048_key.pem │ ├── rsa256_key.pem │ └── rsa512_key.pem └── util_test.py ├── tools └── release.sh └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | 10 | [*.py] 11 | indent_style = space 12 | indent_size = 4 13 | charset = utf-8 14 | max_line_length = 100 15 | 16 | [*.yaml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Introduced the black code formatter 2 | 48729269a5a790473cb760c03577faa1b2c3245b 3 | # Ran black on docs directory 4 | 243097d2f6050f1232f90934326fce76443b84cf 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @certbot/eff-devs 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # for more information about the options in this file, see 2 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | version: 2 4 | updates: 5 | - package-ecosystem: "pip" 6 | directory: "/" 7 | schedule: 8 | interval: "monthly" 9 | groups: 10 | # one might want to only have dependabot open PRs for security updates, 11 | # but in practice, that doesn't seem to work well because for security 12 | # updates it tries to only update the vulnerable package which often 13 | # causes version conflicts unless you also update the other packages. 14 | # hopefully grouping dependabot updates together like this and coupling 15 | # it with the update interval above makes this pretty painless 16 | regular-version-updates: 17 | applies-to: version-updates 18 | patterns: 19 | - "*" 20 | # our pinnings in this repo are only used for our dev setup. 21 | # additionally, as of writing this it is not currently possible for 22 | # dependabot to group security updates and regular version updates in the 23 | # same PR 24 | security-updates-to-dev-pinnings: 25 | applies-to: security-updates 26 | patterns: 27 | - "*" 28 | -------------------------------------------------------------------------------- /.github/workflows/assigned.yaml: -------------------------------------------------------------------------------- 1 | name: Issue Assigned 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | jobs: 7 | send-mattermost-message: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: mattermost/action-mattermost-notify@master 11 | with: 12 | MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_ASSIGN_WEBHOOK }} 13 | TEXT: > 14 | ${{ github.event.assignee.login }} assigned to "${{ github.event.issue.title }}": ${{ github.event.issue.html_url }} 15 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Python Tests for Josepy 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | schedule: 9 | # Run at 4pm UTC or 9am PST 10 | - cron: "0 16 * * *" 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | persist-credentials: false 23 | - name: Setup Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: ${{ matrix.python }} 27 | allow-prereleases: true 28 | - name: Cache Dependencies 29 | uses: actions/cache@v3 30 | with: 31 | path: ~/.cache/pypoetry 32 | # Look to see if there is a cache hit for the corresponding lock file 33 | key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-poetry- 36 | ${{ runner.os }}- 37 | - name: Install Poetry & Tox 38 | run: pip install poetry>1.0.0 tox>3.3.0 39 | - name: Run tox 40 | run: tox 41 | # This job runs our tests like external parties such as packagers. 42 | external: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v3 46 | with: 47 | persist-credentials: false 48 | - name: Setup Python 49 | uses: actions/setup-python@v4 50 | with: 51 | python-version: ${{ matrix.python }} 52 | - name: Install josepy + pytest 53 | run: pip install .[docs] pytest 54 | - name: Run tests 55 | run: pytest tests 56 | notify: 57 | # Only notify about failed builds, do not notify about failed builds for 58 | # PRs, and only notify about failed pushes to main. 59 | if: ${{ failure() && github.event_name != 'pull_request' && (github.event_name != 'push' || github.ref == 'refs/heads/main') }} 60 | needs: [build, external] 61 | runs-on: ubuntu-20.04 62 | steps: 63 | - name: Write Mattermost Message 64 | run: | 65 | WORKFLOW_RUN_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" 66 | echo "{\"text\":\"** :warning: $GITHUB_REPOSITORY: Build failed :warning: ** | [(see details)]($WORKFLOW_RUN_URL) \"}" > mattermost.json 67 | - uses: mattermost/action-mattermost-notify@main 68 | env: 69 | MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }} 70 | -------------------------------------------------------------------------------- /.github/workflows/merged.yaml: -------------------------------------------------------------------------------- 1 | name: Merge Event 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | if_merged: 10 | if: github.event.pull_request.merged == true 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: mattermost/action-mattermost-notify@master 14 | with: 15 | MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_MERGE_WEBHOOK }} 16 | TEXT: > 17 | [${{ github.repository }}] | 18 | [${{ github.event.pull_request.title }} 19 | #${{ github.event.number }}](https://github.com/${{ github.repository }}/pull/${{ github.event.number }}) 20 | was merged into main by ${{ github.actor }} 21 | -------------------------------------------------------------------------------- /.github/workflows/review_requested.yaml: -------------------------------------------------------------------------------- 1 | name: Review Requested 2 | 3 | on: 4 | pull_request_target: 5 | types: [review_requested] 6 | jobs: 7 | send-mattermost-message: 8 | # Don't notify for the interim step of certbot/eff-devs being assigned 9 | if: ${{ github.event.requested_reviewer.login != ''}} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: mattermost/action-mattermost-notify@master 13 | with: 14 | MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_ASSIGN_WEBHOOK }} 15 | TEXT: > 16 | Review requested from ${{ github.event.requested_reviewer.login }} for "${{ github.event.pull_request.title }}": ${{ github.event.pull_request.html_url }} 17 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: Update Stale Issues 2 | on: 3 | schedule: 4 | # Run 1:24AM every night 5 | - cron: '24 1 * * *' 6 | workflow_dispatch: 7 | permissions: 8 | issues: write 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v6 14 | with: 15 | # Idle number of days before marking issues stale 16 | days-before-issue-stale: 365 17 | 18 | # Never mark PRs as stale 19 | days-before-pr-stale: -1 20 | 21 | # Idle number of days before closing stale issues 22 | days-before-issue-close: 30 23 | 24 | # Never close PRs 25 | days-before-pr-close: -1 26 | 27 | # Ignore issues with an assignee 28 | exempt-all-issue-assignees: true 29 | 30 | # Label to use when marking as stale 31 | stale-issue-label: stale-needs-update 32 | 33 | # Label to use when issue is automatically closed 34 | close-issue-label: auto-closed 35 | 36 | stale-issue-message: > 37 | We've made a lot of changes to Josepy since this issue was opened. If you 38 | still have this issue with an up-to-date version of Josepy, can you please 39 | add a comment letting us know? This helps us to better see what issues are 40 | still affecting our users. If there is no activity in the next 30 days, this 41 | issue will be automatically closed. 42 | 43 | close-issue-message: > 44 | This issue has been closed due to lack of activity, but if you think it 45 | should be reopened, please open a new issue with a link to this one and we'll 46 | take a look. 47 | 48 | # Limit the number of actions per run. As of writing this, GitHub's 49 | # rate limit is 1000 requests per hour so we're still a ways off. See 50 | # https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limits-for-requests-from-github-actions. 51 | operations-per-run: 180 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | venv/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | /releases/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage / mypy reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | .pytest_cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | .mypy_cache 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # Ipython Notebook 66 | .ipynb_checkpoints 67 | 68 | # Pycharm 69 | .idea 70 | 71 | # Ignore generated constraints.txt file generated by tools/release.sh 72 | constraints.txt 73 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 24.8.0 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/pycqa/flake8 7 | rev: 7.1.1 8 | hooks: 9 | - id: flake8 10 | - repo: https://github.com/pycqa/isort 11 | rev: 5.13.2 12 | hooks: 13 | - id: isort 14 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.10" 10 | jobs: 11 | # this approach was taken from 12 | # https://docs.readthedocs.io/en/stable/build-customization.html#install-dependencies-with-poetry 13 | post_create_environment: 14 | # Install poetry 15 | # https://python-poetry.org/docs/#installing-manually 16 | - pip install poetry 17 | post_install: 18 | # Install dependencies with 'docs' extras 19 | # VIRTUAL_ENV needs to be set manually for now. 20 | # See https://github.com/readthedocs/readthedocs.org/pull/11152/ 21 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --extras docs 22 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 2.1.0 (main) 5 | ------------ 6 | 7 | * Added support for Python 3.14. 8 | * Dropped support for Python 3.9.0 and 3.9.1 for compatibility with newer 9 | versions of the cryptography Python package. Python 3.9.2+ is still 10 | supported. 11 | 12 | 2.0.0 (2025-02-10) 13 | ------------------ 14 | 15 | * Breaking Change: PyOpenSSL has been fully removed. 16 | - Dropped objects: 17 | `josepy.util.ComparableX509` 18 | - Functions now expect `cryptography.x509` objects: 19 | `josepy.json_util.encode_cert` 20 | `josepy.json_util.encode_csr` 21 | `josepy.jws.Header.x5c.encoder` 22 | - Functions now return `cryptography.x509` objects: 23 | `josepy.json_util.decode_cert` 24 | `josepy.json_util.decode_csr` 25 | `josepy.jws.Header.x5c.decoder` 26 | * Dropped support for Python 3.8. 27 | 28 | 29 | 1.15.0 (2025-01-22) 30 | ------------------- 31 | 32 | * Added a deprecation warning about future backwards incompatible changes. The 33 | text of that warning is "The next major version of josepy will remove 34 | josepy.util.ComparableX509 and all uses of it as part of removing our 35 | dependency on PyOpenSSL. This includes modifying any functions with 36 | ComparableX509 parameters or return values. This will be a breaking change. 37 | To avoid breakage, we recommend pinning josepy < 2.0.0 until josepy 2.0.0 is 38 | out and you've had time to update your code." 39 | * Added support for Python 3.13. 40 | * Dropped support for Python 3.7. 41 | * Support for Python 3.8 has been deprecated and will be removed in the next 42 | scheduled release. 43 | 44 | 1.14.0 (2023-11-01) 45 | ------------------- 46 | 47 | * Added support for Python 3.11 and 3.12. 48 | * Support for Python 3.7 has been deprecated and will be removed in the next 49 | scheduled release. 50 | * Dropped support for Python 3.6. 51 | * Added a new valid PGP key for signing our PyPI packages with the fingerprint 52 | F2871B4152AE13C49519111F447BF683AA3B26C3 53 | 54 | 1.13.0 (2022-03-10) 55 | ------------------- 56 | 57 | * Support for Python 3.6 has been deprecated and will be removed in the next 58 | scheduled release. 59 | * Corrected some type annotations. 60 | 61 | 1.12.0 (2022-01-11) 62 | ------------------- 63 | 64 | * Corrected some type annotations. 65 | * Dropped support for cryptography<1.5. 66 | * Added the top level attributes josepy.JWKEC, josepy.JWKOct, and 67 | josepy.ComparableECKey for convenience and consistency. 68 | 69 | 1.11.0 (2021-11-17) 70 | ------------------- 71 | 72 | * Added support for Python 3.10. 73 | * We changed the PGP key used to sign the packages we upload to PyPI. Going 74 | forward, releases will be signed with one of three different keys. All of 75 | these keys are available on major key servers and signed by our previous PGP 76 | key. The fingerprints of these new keys are: 77 | - BF6BCFC89E90747B9A680FD7B6029E8500F7DB16 78 | - 86379B4F0AF371B50CD9E5FF3402831161D1D280 79 | - 20F201346BF8F3F455A73F9A780CC99432A28621 80 | 81 | 1.10.0 (2021-09-27) 82 | ------------------- 83 | 84 | * josepy is now compliant with PEP-561: type checkers will fetch types from the inline 85 | types annotations when josepy is installed as a dependency in a Python project. 86 | * Added a `field` function to assist in adding type annotations for Fields in classes. 87 | If the field function is used to define a `Field` in a `JSONObjectWithFields` based 88 | class without a type annotation, an error will be raised. 89 | * josepy's tests can no longer be imported under the name josepy, however, they are still 90 | included in the package and you can run them by installing josepy with "tests" extras and 91 | running `python -m pytest`. 92 | 93 | 1.9.0 (2021-09-09) 94 | ------------------ 95 | 96 | * Removed pytest-cache testing dependency. 97 | * Fixed a bug that sometimes caused incorrect padding to be used when 98 | serializing Elliptic Curve keys as JSON Web Keys. 99 | 100 | 1.8.0 (2021-03-15) 101 | ------------------ 102 | 103 | * Removed external mock dependency. 104 | * Removed dependency on six. 105 | * Deprecated the module josepy.magic_typing. 106 | * Fix JWS/JWK generation with EC keys when keys or signatures have leading zeros. 107 | 108 | 1.7.0 (2021-02-11) 109 | ------------------ 110 | 111 | * Dropped support for Python 2.7. 112 | * Added support for EC keys. 113 | 114 | 1.6.0 (2021-01-26) 115 | ------------------ 116 | 117 | * Deprecated support for Python 2.7. 118 | 119 | 1.5.0 (2020-11-03) 120 | ------------------ 121 | 122 | * Added support for Python 3.9. 123 | * Dropped support for Python 3.5. 124 | * Stopped supporting running tests with ``python setup.py test`` which is 125 | deprecated in favor of ``python -m pytest``. 126 | 127 | 1.4.0 (2020-08-17) 128 | ------------------ 129 | 130 | * Deprecated support for Python 3.5. 131 | 132 | 1.3.0 (2020-01-28) 133 | ------------------ 134 | 135 | * Deprecated support for Python 3.4. 136 | * Officially add support for Python 3.8. 137 | 138 | 1.2.0 (2019-06-28) 139 | ------------------ 140 | 141 | * Support for Python 2.6 and 3.3 has been removed. 142 | * Known incompatibilities with Python 3.8 have been resolved. 143 | 144 | 1.1.0 (2018-04-13) 145 | ------------------ 146 | 147 | * Deprecated support for Python 2.6 and 3.3. 148 | * Use the ``sign`` and ``verify`` methods when they are available in 149 | ``cryptography`` instead of the deprecated methods ``signer`` and 150 | ``verifier``. 151 | 152 | 1.0.1 (2017-10-25) 153 | ------------------ 154 | 155 | Stop installing mock as part of the default but only as part of the 156 | testing dependencies. 157 | 158 | 1.0.0 (2017-10-13) 159 | ------------------- 160 | 161 | First release after moving the josepy package into a standalone library. 162 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 17 | 18 | # Certbot Contributing Guide 19 | 20 | Hi! Welcome to the Certbot project. We look forward to collaborating with you. 21 | 22 | If you're reporting a bug in Certbot, please make sure to include: 23 | - The version of Certbot you're running. 24 | - The operating system you're running it on. 25 | - The commands you ran. 26 | - What you expected to happen, and 27 | - What actually happened. 28 | 29 | If you're a developer, we have some helpful information in our 30 | [Developer's Guide](https://certbot.eff.org/docs/contributing.html) to get you 31 | started. In particular, we recommend you read these sections 32 | 33 | - [Finding issues to work on](https://certbot.eff.org/docs/contributing.html#find-issues-to-work-on) 34 | - [Coding style](https://certbot.eff.org/docs/contributing.html#coding-style) 35 | - [Submitting a pull request](https://certbot.eff.org/docs/contributing.html#submitting-a-pull-request) 36 | 37 | # Specific instructions for Josepy 38 | 39 | ## Configure a development environment 40 | 41 | 1) Install Poetry: https://python-poetry.org/docs/#installation 42 | 2) Setup a Python virtual environment 43 | ```bash 44 | $ poetry install -E docs 45 | ``` 46 | 3) Activate the Python virtual environment 47 | ```bash 48 | # (On Linux) 49 | $ source .venv/bin/activate 50 | # (On Windows Powershell) 51 | $ .\.venv\Script\activate 52 | ``` 53 | 4) Optionally set up [pre-commit](https://pre-commit.com/) which will cause 54 | simple tests to be automatically run on your changes when you commit them 55 | ```bash 56 | $ pre-commit install 57 | ``` 58 | 59 | ## Run the tests and quality checks 60 | 61 | 1) Configure a development environment ([see above](#configure-a-development-environment)) 62 | 2) Run the tests 63 | ```bash 64 | $ tox 65 | ``` 66 | 3) You can also run specific tests 67 | ```bash 68 | $ tox -e py 69 | ``` 70 | You can get a listing of the available tests by running 71 | ```bash 72 | $ tox -l 73 | ``` 74 | 75 | ## Updating dependencies 76 | 77 | Our poetry.lock file is only used during development so security 78 | vulnerabilities in the pinned packages are rarely relevant. With that said, if 79 | you want to update package versions, you can use the [`poetry update` 80 | command](https://python-poetry.org/docs/cli/#update). 81 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Electronic Frontier Foundation and others 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Apache License 16 | Version 2.0, January 2004 17 | http://www.apache.org/licenses/ 18 | 19 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 20 | 21 | 1. Definitions. 22 | 23 | "License" shall mean the terms and conditions for use, reproduction, 24 | and distribution as defined by Sections 1 through 9 of this document. 25 | 26 | "Licensor" shall mean the copyright owner or entity authorized by 27 | the copyright owner that is granting the License. 28 | 29 | "Legal Entity" shall mean the union of the acting entity and all 30 | other entities that control, are controlled by, or are under common 31 | control with that entity. For the purposes of this definition, 32 | "control" means (i) the power, direct or indirect, to cause the 33 | direction or management of such entity, whether by contract or 34 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 35 | outstanding shares, or (iii) beneficial ownership of such entity. 36 | 37 | "You" (or "Your") shall mean an individual or Legal Entity 38 | exercising permissions granted by this License. 39 | 40 | "Source" form shall mean the preferred form for making modifications, 41 | including but not limited to software source code, documentation 42 | source, and configuration files. 43 | 44 | "Object" form shall mean any form resulting from mechanical 45 | transformation or translation of a Source form, including but 46 | not limited to compiled object code, generated documentation, 47 | and conversions to other media types. 48 | 49 | "Work" shall mean the work of authorship, whether in Source or 50 | Object form, made available under the License, as indicated by a 51 | copyright notice that is included in or attached to the work 52 | (an example is provided in the Appendix below). 53 | 54 | "Derivative Works" shall mean any work, whether in Source or Object 55 | form, that is based on (or derived from) the Work and for which the 56 | editorial revisions, annotations, elaborations, or other modifications 57 | represent, as a whole, an original work of authorship. For the purposes 58 | of this License, Derivative Works shall not include works that remain 59 | separable from, or merely link (or bind by name) to the interfaces of, 60 | the Work and Derivative Works thereof. 61 | 62 | "Contribution" shall mean any work of authorship, including 63 | the original version of the Work and any modifications or additions 64 | to that Work or Derivative Works thereof, that is intentionally 65 | submitted to Licensor for inclusion in the Work by the copyright owner 66 | or by an individual or Legal Entity authorized to submit on behalf of 67 | the copyright owner. For the purposes of this definition, "submitted" 68 | means any form of electronic, verbal, or written communication sent 69 | to the Licensor or its representatives, including but not limited to 70 | communication on electronic mailing lists, source code control systems, 71 | and issue tracking systems that are managed by, or on behalf of, the 72 | Licensor for the purpose of discussing and improving the Work, but 73 | excluding communication that is conspicuously marked or otherwise 74 | designated in writing by the copyright owner as "Not a Contribution." 75 | 76 | "Contributor" shall mean Licensor and any individual or Legal Entity 77 | on behalf of whom a Contribution has been received by Licensor and 78 | subsequently incorporated within the Work. 79 | 80 | 2. Grant of Copyright License. Subject to the terms and conditions of 81 | this License, each Contributor hereby grants to You a perpetual, 82 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 83 | copyright license to reproduce, prepare Derivative Works of, 84 | publicly display, publicly perform, sublicense, and distribute the 85 | Work and such Derivative Works in Source or Object form. 86 | 87 | 3. Grant of Patent License. Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | (except as stated in this section) patent license to make, have made, 91 | use, offer to sell, sell, import, and otherwise transfer the Work, 92 | where such license applies only to those patent claims licensable 93 | by such Contributor that are necessarily infringed by their 94 | Contribution(s) alone or by combination of their Contribution(s) 95 | with the Work to which such Contribution(s) was submitted. If You 96 | institute patent litigation against any entity (including a 97 | cross-claim or counterclaim in a lawsuit) alleging that the Work 98 | or a Contribution incorporated within the Work constitutes direct 99 | or contributory patent infringement, then any patent licenses 100 | granted to You under this License for that Work shall terminate 101 | as of the date such litigation is filed. 102 | 103 | 4. Redistribution. You may reproduce and distribute copies of the 104 | Work or Derivative Works thereof in any medium, with or without 105 | modifications, and in Source or Object form, provided that You 106 | meet the following conditions: 107 | 108 | (a) You must give any other recipients of the Work or 109 | Derivative Works a copy of this License; and 110 | 111 | (b) You must cause any modified files to carry prominent notices 112 | stating that You changed the files; and 113 | 114 | (c) You must retain, in the Source form of any Derivative Works 115 | that You distribute, all copyright, patent, trademark, and 116 | attribution notices from the Source form of the Work, 117 | excluding those notices that do not pertain to any part of 118 | the Derivative Works; and 119 | 120 | (d) If the Work includes a "NOTICE" text file as part of its 121 | distribution, then any Derivative Works that You distribute must 122 | include a readable copy of the attribution notices contained 123 | within such NOTICE file, excluding those notices that do not 124 | pertain to any part of the Derivative Works, in at least one 125 | of the following places: within a NOTICE text file distributed 126 | as part of the Derivative Works; within the Source form or 127 | documentation, if provided along with the Derivative Works; or, 128 | within a display generated by the Derivative Works, if and 129 | wherever such third-party notices normally appear. The contents 130 | of the NOTICE file are for informational purposes only and 131 | do not modify the License. You may add Your own attribution 132 | notices within Derivative Works that You distribute, alongside 133 | or as an addendum to the NOTICE text from the Work, provided 134 | that such additional attribution notices cannot be construed 135 | as modifying the License. 136 | 137 | You may add Your own copyright statement to Your modifications and 138 | may provide additional or different license terms and conditions 139 | for use, reproduction, or distribution of Your modifications, or 140 | for any such Derivative Works as a whole, provided Your use, 141 | reproduction, and distribution of the Work otherwise complies with 142 | the conditions stated in this License. 143 | 144 | 5. Submission of Contributions. Unless You explicitly state otherwise, 145 | any Contribution intentionally submitted for inclusion in the Work 146 | by You to the Licensor shall be under the terms and conditions of 147 | this License, without any additional terms or conditions. 148 | Notwithstanding the above, nothing herein shall supersede or modify 149 | the terms of any separate license agreement you may have executed 150 | with Licensor regarding such Contributions. 151 | 152 | 6. Trademarks. This License does not grant permission to use the trade 153 | names, trademarks, service marks, or product names of the Licensor, 154 | except as required for reasonable and customary use in describing the 155 | origin of the Work and reproducing the content of the NOTICE file. 156 | 157 | 7. Disclaimer of Warranty. Unless required by applicable law or 158 | agreed to in writing, Licensor provides the Work (and each 159 | Contributor provides its Contributions) on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 161 | implied, including, without limitation, any warranties or conditions 162 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 163 | PARTICULAR PURPOSE. You are solely responsible for determining the 164 | appropriateness of using or redistributing the Work and assume any 165 | risks associated with Your exercise of permissions under this License. 166 | 167 | 8. Limitation of Liability. In no event and under no legal theory, 168 | whether in tort (including negligence), contract, or otherwise, 169 | unless required by applicable law (such as deliberate and grossly 170 | negligent acts) or agreed to in writing, shall any Contributor be 171 | liable to You for damages, including any direct, indirect, special, 172 | incidental, or consequential damages of any character arising as a 173 | result of this License or out of the use or inability to use the 174 | Work (including but not limited to damages for loss of goodwill, 175 | work stoppage, computer failure or malfunction, or any and all 176 | other commercial damages or losses), even if such Contributor 177 | has been advised of the possibility of such damages. 178 | 179 | 9. Accepting Warranty or Additional Liability. While redistributing 180 | the Work or Derivative Works thereof, You may choose to offer, 181 | and charge a fee for, acceptance of support, warranty, indemnity, 182 | or other liability obligations and/or rights consistent with this 183 | License. However, in accepting such obligations, You may act only 184 | on Your own behalf and on Your sole responsibility, not on behalf 185 | of any other Contributor, and only if You agree to indemnify, 186 | defend, and hold each Contributor harmless for any liability 187 | incurred by, or claims asserted against, such Contributor by reason 188 | of your accepting any such warranty or additional liability. 189 | 190 | END OF TERMS AND CONDITIONS 191 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | JOSE protocol implementation in Python using cryptography 2 | 3 | .. image:: https://github.com/certbot/josepy/actions/workflows/check.yaml/badge.svg 4 | :target: https://github.com/certbot/josepy/actions/workflows/check.yaml 5 | 6 | .. image:: https://readthedocs.org/projects/josepy/badge/?version=latest 7 | :target: http://josepy.readthedocs.io/en/latest/?badge=latest 8 | 9 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 10 | :target: https://github.com/psf/black 11 | 12 | For more information about contributing to this project, see CONTRIBUTING.md_. 13 | 14 | .. _CONTRIBUTING.md: CONTRIBUTING.md 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Security vulnerabilities can be reported using GitHub's [private vulnerability reporting tool](https://github.com/certbot/josepy/security/advisories/new). 6 | 7 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = josepy 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certbot/josepy/8ddcaaed99a61e9277df1ec00157f0aea53378d4/docs/_static/.gitignore -------------------------------------------------------------------------------- /docs/_templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certbot/josepy/8ddcaaed99a61e9277df1ec00157f0aea53378d4/docs/_templates/.gitignore -------------------------------------------------------------------------------- /docs/api/base64.rst: -------------------------------------------------------------------------------- 1 | JOSE Base64 2 | ----------- 3 | 4 | .. automodule:: josepy.b64 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/errors.rst: -------------------------------------------------------------------------------- 1 | Errors 2 | ------ 3 | 4 | .. automodule:: josepy.errors 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/interfaces.rst: -------------------------------------------------------------------------------- 1 | Interfaces 2 | ---------- 3 | 4 | .. automodule:: josepy.interfaces 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/json_util.rst: -------------------------------------------------------------------------------- 1 | JSON utilities 2 | -------------- 3 | 4 | .. automodule:: josepy.json_util 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/jwa.rst: -------------------------------------------------------------------------------- 1 | JSON Web Algorithms 2 | ------------------- 3 | 4 | .. automodule:: josepy.jwa 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/jwk.rst: -------------------------------------------------------------------------------- 1 | JSON Web Key 2 | ------------ 3 | 4 | .. automodule:: josepy.jwk 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/jws.rst: -------------------------------------------------------------------------------- 1 | JSON Web Signature 2 | ------------------ 3 | 4 | .. automodule:: josepy.jws 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/util.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | --------- 3 | 4 | .. automodule:: josepy.util 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # josepy documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Oct 11 17:05:53 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.intersphinx", 36 | "sphinx.ext.todo", 37 | "sphinx.ext.coverage", 38 | "sphinx.ext.viewcode", 39 | ] 40 | 41 | autodoc_member_order = "bysource" 42 | autodoc_default_flags = ["show-inheritance", "private-members"] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ["_templates"] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = ".rst" 52 | 53 | # The master toctree document. 54 | master_doc = "index" 55 | 56 | # General information about the project. 57 | project = "josepy" 58 | copyright = "2015-2017, Let's Encrypt Project" 59 | author = "Let's Encrypt Project" 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = "2.1" 67 | # The full version, including alpha/beta/rc tags. 68 | release = "2.1.0.dev0" 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | pygments_style = "sphinx" 84 | 85 | # If true, `todo` and `todoList` produce output, else they produce nothing. 86 | todo_include_todos = True 87 | 88 | # The reST default role (used for this markup: `text`) to use for all 89 | # documents. 90 | default_role = "py:obj" 91 | 92 | 93 | # -- Options for HTML output ---------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | # 98 | html_theme = "sphinx_rtd_theme" 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | # 104 | # html_theme_options = {} 105 | 106 | # Add any paths that contain custom static files (such as style sheets) here, 107 | # relative to this directory. They are copied after the builtin static files, 108 | # so a file named "default.css" will overwrite the builtin "default.css". 109 | html_static_path = ["_static"] 110 | 111 | # Custom sidebar templates, must be a dictionary that maps document names 112 | # to template names. 113 | # 114 | # This is required for the alabaster theme 115 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 116 | html_sidebars = { 117 | "**": [ 118 | "about.html", 119 | "navigation.html", 120 | "relations.html", # needs 'show_related': True theme option to display 121 | "searchbox.html", 122 | "donate.html", 123 | ] 124 | } 125 | 126 | 127 | # -- Options for HTMLHelp output ------------------------------------------ 128 | 129 | # Output file base name for HTML help builder. 130 | htmlhelp_basename = "josepydoc" 131 | 132 | 133 | # -- Options for LaTeX output --------------------------------------------- 134 | 135 | latex_elements = { 136 | # The paper size ('letterpaper' or 'a4paper'). 137 | # 138 | # 'papersize': 'letterpaper', 139 | # The font size ('10pt', '11pt' or '12pt'). 140 | # 141 | # 'pointsize': '10pt', 142 | # Additional stuff for the LaTeX preamble. 143 | # 144 | # 'preamble': '', 145 | # Latex figure (float) alignment 146 | # 147 | # 'figure_align': 'htbp', 148 | } 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, 152 | # author, documentclass [howto, manual, or own class]). 153 | latex_documents = [ 154 | (master_doc, "josepy.tex", "josepy Documentation", "Let's Encrypt Project", "manual"), 155 | ] 156 | 157 | 158 | # -- Options for manual page output --------------------------------------- 159 | 160 | # One entry per manual page. List of tuples 161 | # (source start file, name, description, authors, manual section). 162 | man_pages = [ 163 | (master_doc, "josepy", "josepy Documentation", [author], 1), 164 | ("man/jws", "jws", "jws script documentation", [project], 1), 165 | ] 166 | 167 | 168 | # -- Options for Texinfo output ------------------------------------------- 169 | 170 | # Grouping the document tree into Texinfo files. List of tuples 171 | # (source start file, target name, title, author, 172 | # dir menu entry, description, category) 173 | texinfo_documents = [ 174 | ( 175 | master_doc, 176 | "josepy", 177 | "josepy Documentation", 178 | author, 179 | "josepy", 180 | "One line description of project.", 181 | "Miscellaneous", 182 | ), 183 | ] 184 | 185 | 186 | # Example configuration for intersphinx: refer to the Python standard library. 187 | intersphinx_mapping = { 188 | "python": ("https://docs.python.org/", None), 189 | "cryptography": ("https://cryptography.io/en/latest/", None), 190 | } 191 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | josepy 2 | ====== 3 | 4 | .. automodule:: josepy 5 | :members: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Contents: 10 | :glob: 11 | 12 | api/* 13 | changelog 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/jws-help.txt: -------------------------------------------------------------------------------- 1 | usage: jws [-h] [--compact] {sign,verify} ... 2 | 3 | positional arguments: 4 | {sign,verify} 5 | 6 | optional arguments: 7 | -h, --help show this help message and exit 8 | --compact 9 | -------------------------------------------------------------------------------- /docs/man/jws.rst: -------------------------------------------------------------------------------- 1 | .. literalinclude:: ../jws-help.txt 2 | -------------------------------------------------------------------------------- /examples/pem_conversion.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function # Python2 support 2 | 3 | from cryptography.hazmat.primitives import serialization 4 | 5 | import josepy 6 | 7 | 8 | def jwk_to_pem(pkey_jwk): 9 | """ 10 | LetsEncrypt uses RSA Private Keys as Account Keys. 11 | Certbot stores the Account Keys as a JWK (JSON Web Key) encoded string. 12 | Many non-certbot clients store the Account Keys using PEM encoding. 13 | 14 | Developers may need to utilize a Private Key in the PEM encoding for certain 15 | operations or to migrate existing LetsEncrypt accounts to a client. 16 | 17 | :param pkey_jwk: JSON Web Key(jwk) encoded RSA Private Key 18 | :type pkey_jwk: string 19 | 20 | :return: PEM encoded RSA Private Key 21 | :rtype: string 22 | """ 23 | pkey = josepy.JWKRSA.json_loads(pkey_jwk) 24 | as_pem = pkey.key.private_bytes( 25 | encoding=serialization.Encoding.PEM, 26 | format=serialization.PrivateFormat.TraditionalOpenSSL, 27 | encryption_algorithm=serialization.NoEncryption(), 28 | ) 29 | return as_pem 30 | 31 | 32 | def pem_to_jwk(pkey_pem, format="string"): 33 | """ 34 | LetsEncrypt uses RSA Private Keys as Account Keys. 35 | Certbot stores the Account Keys as a JWK (JSON Web Key) encoded string. 36 | Many non-certbot clients store the Account Keys using PEM encoding. 37 | 38 | Developers may need to utilize a Private Key in the JWK format for certain 39 | operations or to migrate existing LetsEncrypt accounts to a client. 40 | 41 | :param pkey_pem: PEM encoded RSA Private Key 42 | :type pkey_pem: string 43 | 44 | :param format: Should the format be the JWK as a dict or JSON?, default string 45 | :type format: string, optional 46 | 47 | :return: JSON Web Key(jwk) encoded RSA Private Key 48 | :rtype: string or dict 49 | """ 50 | if format not in ("string", "dict"): 51 | raise ValueError("`format` must be one of: string, dict") 52 | pkey = josepy.JWKRSA.load(pkey_pem) 53 | if format == "dict": 54 | # ``.fields_to_partial_json()` does not encode the `kty` Key Identifier 55 | as_jwk = pkey.to_json() 56 | else: 57 | # format == "string" 58 | as_jwk = pkey.json_dumps() 59 | return as_jwk 60 | 61 | 62 | if __name__ == "__main__": 63 | """ 64 | Certbot stores account data on a disk using this pattern: 65 | 66 | /etc/letsencrypt/accounts/##ACME_SERVER##/directory/##ACCOUNT## 67 | 68 | Each ACCOUNT folder has three files 69 | 70 | /private_key.json - JWK encoded RSA Private Key 71 | /meta.json - metadata 72 | /regr.json - registration information 73 | 74 | This example is only concerned with the `/private_key.json` file 75 | """ 76 | import json 77 | import sys 78 | 79 | _args = sys.argv 80 | if len(_args) == 2: 81 | json_data = open(_args[1]).read() 82 | as_pem = jwk_to_pem(json_data) 83 | print(as_pem) 84 | elif len(_args) == 3 and _args[2] == "roundtrip": 85 | json_data = open(_args[1]).read() 86 | as_pem = jwk_to_pem(json_data) 87 | as_jwk = pem_to_jwk(as_pem) 88 | assert json.loads(as_jwk) == json.loads(json_data) 89 | print(as_pem) 90 | print("> roundtrip >") 91 | print(as_jwk) 92 | else: 93 | indented_cmd = ( 94 | " python pem_conversion.py " 95 | "/etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/" 96 | "directory/##ACCOUNT##/private_key.json" 97 | ) 98 | print("Error.") 99 | print("Invoke this script with a single argument: the path to a certbot key.") 100 | print(indented_cmd) 101 | print("Optional: add the string 'roundtrip' after the key to perform a roundtrip") 102 | # arguments to print are printed with a space between them by default 103 | print(indented_cmd, "roundtrip") 104 | -------------------------------------------------------------------------------- /mattermost.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certbot/josepy/8ddcaaed99a61e9277df1ec00157f0aea53378d4/mattermost.json -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # PEP-517 build 2 | 3 | [build-system] 4 | requires = ["poetry_core>=1.0.8"] 5 | build-backend = "poetry.core.masonry.api" 6 | 7 | # Poetry tooling configuration 8 | 9 | [tool.poetry] 10 | name = "josepy" 11 | version = "2.1.0.dev0" 12 | description = "JOSE protocol implementation in Python" 13 | license = "Apache License 2.0" 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: Apache Software License", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "Programming Language :: Python :: 3.14", 26 | "Topic :: Internet :: WWW/HTTP", 27 | "Topic :: Security", 28 | ] 29 | homepage = "https://github.com/certbot/josepy" 30 | authors = ["Certbot Project "] 31 | readme = "README.rst" 32 | include = [ 33 | "CHANGELOG.rst", 34 | "CONTRIBUTING.md", 35 | "docs", "tests", 36 | ] 37 | 38 | [tool.poetry.dependencies] 39 | # This should be kept in sync with the value of target-version in our 40 | # configuration for black below. 41 | # 42 | # python 3.9.2 is used as a lower bound here because newer versions of 43 | # cryptography dropped support for python 3.9.0 and 3.9.1. see 44 | # https://github.com/pyca/cryptography/pull/12045. when we drop support for 45 | # python 3.9 altogether, this line can be changed to the simpler 'python = "^3.10"'. 46 | python = ">=3.9.2,<4.0" 47 | # load_pem_private/public_key (>=0.6) 48 | # rsa_recover_prime_factors (>=0.8) 49 | # add sign() and verify() to asymetric keys (RSA >=1.4, ECDSA >=1.5) 50 | cryptography = ">=1.5" 51 | # >=4.3.0 is needed for Python 3.10 support 52 | sphinx = {version = ">=4.3.0", optional = true} 53 | sphinx-rtd-theme = {version = ">=1.0", optional = true} 54 | 55 | [tool.poetry.group.dev.dependencies] 56 | # coverage[toml] extra is required to read the coverage config from pyproject.toml 57 | coverage = {version = ">=4.0", extras = ["toml"]} 58 | mypy = "*" 59 | types-pyRFC3339 = "*" 60 | types-requests = "*" 61 | types-setuptools = "*" 62 | typing-extensions = "*" 63 | pre-commit = "*" 64 | pytest = ">=2.8.0" 65 | pytest-cov = "*" 66 | tox = "*" 67 | twine = "*" 68 | 69 | [tool.poetry.extras] 70 | docs = [ 71 | "sphinx", 72 | "sphinx-rtd-theme", 73 | ] 74 | 75 | [tool.poetry.scripts] 76 | jws = "josepy.jws:CLI.run" 77 | 78 | # Black tooling configuration 79 | [tool.black] 80 | line-length = 100 81 | # This should be kept in sync with the version of Python specified in poetry's 82 | # dependencies above. 83 | # TODO add 'py314' once black supports it, see #232 for details 84 | target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] 85 | 86 | # Mypy tooling configuration 87 | 88 | [tool.mypy] 89 | ignore_missing_imports = true 90 | warn_unused_ignores = true 91 | show_error_codes = true 92 | disallow_untyped_defs = true 93 | 94 | # Pytest tooling configuration 95 | 96 | [tool.pytest.ini_options] 97 | filterwarnings = [ 98 | "error", 99 | ] 100 | norecursedirs = "*.egg .eggs dist build docs .tox" 101 | 102 | # Isort tooling configuration 103 | 104 | [tool.isort] 105 | combine_as_imports = false 106 | default_section = "THIRDPARTY" 107 | known_first_party = "josepy" 108 | line_length = 79 109 | profile = "black" 110 | 111 | # Coverage tooling configuration 112 | 113 | [tool.coverage.run] 114 | branch = true 115 | source = ["josepy"] 116 | 117 | [tool.coverage.paths] 118 | source = [ 119 | ".tox/*/lib/python*/site-packages/josepy", 120 | ".tox/pypy*/site-packages/josepy", 121 | ] 122 | 123 | [tool.coverage.report] 124 | show_missing = true 125 | -------------------------------------------------------------------------------- /src/josepy/__init__.py: -------------------------------------------------------------------------------- 1 | """Javascript Object Signing and Encryption (JOSE). 2 | 3 | This package is a Python implementation of the standards developed by 4 | IETF `Javascript Object Signing and Encryption (Active WG)`_, in 5 | particular the following RFCs: 6 | 7 | - `JSON Web Algorithms (JWA)`_ 8 | - `JSON Web Key (JWK)`_ 9 | - `JSON Web Signature (JWS)`_ 10 | 11 | Originally developed as part of the ACME_ protocol implementation. 12 | 13 | .. _`Javascript Object Signing and Encryption (Active WG)`: 14 | https://tools.ietf.org/wg/jose/ 15 | 16 | .. _`JSON Web Algorithms (JWA)`: 17 | https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-algorithms/ 18 | 19 | .. _`JSON Web Key (JWK)`: 20 | https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-key/ 21 | 22 | .. _`JSON Web Signature (JWS)`: 23 | https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-signature/ 24 | 25 | .. _ACME: https://pypi.python.org/pypi/acme 26 | 27 | """ 28 | 29 | # flake8: noqa 30 | from josepy.b64 import b64decode, b64encode 31 | from josepy.errors import ( 32 | DeserializationError, 33 | Error, 34 | SerializationError, 35 | UnrecognizedTypeError, 36 | ) 37 | from josepy.interfaces import JSONDeSerializable 38 | from josepy.json_util import ( 39 | Field, 40 | JSONObjectWithFields, 41 | TypedJSONObjectWithFields, 42 | decode_b64jose, 43 | decode_cert, 44 | decode_csr, 45 | decode_hex16, 46 | encode_b64jose, 47 | encode_cert, 48 | encode_csr, 49 | encode_hex16, 50 | field, 51 | ) 52 | from josepy.jwa import ( 53 | ES256, 54 | ES384, 55 | ES512, 56 | HS256, 57 | HS384, 58 | HS512, 59 | PS256, 60 | PS384, 61 | PS512, 62 | RS256, 63 | RS384, 64 | RS512, 65 | JWASignature, 66 | ) 67 | from josepy.jwk import JWK, JWKEC, JWKRSA, JWKOct 68 | from josepy.jws import JWS, Header, Signature 69 | from josepy.util import ( 70 | ComparableECKey, 71 | ComparableKey, 72 | ComparableRSAKey, 73 | ImmutableMap, 74 | ) 75 | -------------------------------------------------------------------------------- /src/josepy/b64.py: -------------------------------------------------------------------------------- 1 | """`JOSE Base64`_ is defined as: 2 | 3 | - URL-safe Base64 4 | - padding stripped 5 | 6 | .. _`JOSE Base64`: 7 | https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-37#appendix-C 8 | 9 | .. Do NOT try to call this module "base64", as it will "shadow" the 10 | standard library. 11 | 12 | """ 13 | 14 | import base64 15 | from typing import Union 16 | 17 | 18 | def b64encode(data: bytes) -> bytes: 19 | """JOSE Base64 encode. 20 | 21 | :param data: Data to be encoded. 22 | :type data: bytes 23 | 24 | :returns: JOSE Base64 string. 25 | :rtype: bytes 26 | 27 | :raises TypeError: if ``data`` is of incorrect type 28 | 29 | """ 30 | if not isinstance(data, bytes): 31 | raise TypeError("argument should be bytes") 32 | return base64.urlsafe_b64encode(data).rstrip(b"=") 33 | 34 | 35 | def b64decode(data: Union[bytes, str]) -> bytes: 36 | """JOSE Base64 decode. 37 | 38 | :param data: Base64 string to be decoded. If it's unicode, then 39 | only ASCII characters are allowed. 40 | :type data: bytes or unicode 41 | 42 | :returns: Decoded data. 43 | :rtype: bytes 44 | 45 | :raises TypeError: if input is of incorrect type 46 | :raises ValueError: if input is unicode with non-ASCII characters 47 | 48 | """ 49 | if isinstance(data, str): 50 | try: 51 | data = data.encode("ascii") 52 | except UnicodeEncodeError: 53 | raise ValueError("unicode argument should contain only ASCII characters") 54 | elif not isinstance(data, bytes): 55 | raise TypeError("argument should be a str or unicode") 56 | 57 | return base64.urlsafe_b64decode(data + b"=" * (4 - (len(data) % 4))) 58 | -------------------------------------------------------------------------------- /src/josepy/errors.py: -------------------------------------------------------------------------------- 1 | """JOSE errors.""" 2 | 3 | from typing import Any 4 | 5 | 6 | class Error(Exception): 7 | """Generic JOSE Error.""" 8 | 9 | 10 | class DeserializationError(Error): 11 | """JSON deserialization error.""" 12 | 13 | def __str__(self) -> str: 14 | return "Deserialization error: {0}".format(super().__str__()) 15 | 16 | 17 | class SerializationError(Error): 18 | """JSON serialization error.""" 19 | 20 | 21 | class UnrecognizedTypeError(DeserializationError): 22 | """Unrecognized type error. 23 | 24 | :ivar str typ: The unrecognized type of the JSON object. 25 | :ivar jobj: Full JSON object. 26 | 27 | """ 28 | 29 | def __init__(self, typ: str, jobj: Any) -> None: 30 | self.typ = typ 31 | self.jobj = jobj 32 | super().__init__(str(self)) 33 | 34 | def __str__(self) -> str: 35 | return "{0} was not recognized, full message: {1}".format(self.typ, self.jobj) 36 | -------------------------------------------------------------------------------- /src/josepy/interfaces.py: -------------------------------------------------------------------------------- 1 | """JOSE interfaces.""" 2 | 3 | import abc 4 | import json 5 | from collections.abc import Mapping, Sequence 6 | from typing import Any, Type, TypeVar, Union 7 | 8 | from josepy import errors 9 | 10 | GenericJSONDeSerializable = TypeVar("GenericJSONDeSerializable", bound="JSONDeSerializable") 11 | 12 | 13 | class JSONDeSerializable(metaclass=abc.ABCMeta): 14 | """Interface for (de)serializable JSON objects. 15 | 16 | Please recall, that standard Python library implements 17 | :class:`json.JSONEncoder` and :class:`json.JSONDecoder` that perform 18 | translations based on respective :ref:`conversion tables 19 | ` that look pretty much like the one below (for 20 | complete tables see relevant Python documentation): 21 | 22 | .. _conversion-table: 23 | 24 | ====== ====== 25 | JSON Python 26 | ====== ====== 27 | object dict 28 | ... ... 29 | ====== ====== 30 | 31 | While the above **conversion table** is about translation of JSON 32 | documents to/from the basic Python types only, 33 | :class:`JSONDeSerializable` introduces the following two concepts: 34 | 35 | serialization 36 | Turning an arbitrary Python object into Python object that can 37 | be encoded into a JSON document. **Full serialization** produces 38 | a Python object composed of only basic types as required by the 39 | :ref:`conversion table `. **Partial 40 | serialization** (accomplished by :meth:`to_partial_json`) 41 | produces a Python object that might also be built from other 42 | :class:`JSONDeSerializable` objects. 43 | 44 | deserialization 45 | Turning a decoded Python object (necessarily one of the basic 46 | types as required by the :ref:`conversion table 47 | `) into an arbitrary Python object. 48 | 49 | Serialization produces **serialized object** ("partially serialized 50 | object" or "fully serialized object" for partial and full 51 | serialization respectively) and deserialization produces 52 | **deserialized object**, both usually denoted in the source code as 53 | ``jobj``. 54 | 55 | Wording in the official Python documentation might be confusing 56 | after reading the above, but in the light of those definitions, one 57 | can view :meth:`json.JSONDecoder.decode` as decoder and 58 | deserializer of basic types, :meth:`json.JSONEncoder.default` as 59 | serializer of basic types, :meth:`json.JSONEncoder.encode` as 60 | serializer and encoder of basic types. 61 | 62 | One could extend :mod:`json` to support arbitrary object 63 | (de)serialization either by: 64 | 65 | - overriding :meth:`json.JSONDecoder.decode` and 66 | :meth:`json.JSONEncoder.default` in subclasses 67 | 68 | - or passing ``object_hook`` argument (or ``object_hook_pairs``) 69 | to :func:`json.load`/:func:`json.loads` or ``default`` argument 70 | for :func:`json.dump`/:func:`json.dumps`. 71 | 72 | Interestingly, ``default`` is required to perform only partial 73 | serialization, as :func:`json.dumps` applies ``default`` 74 | recursively. This is the idea behind making :meth:`to_partial_json` 75 | produce only partial serialization, while providing custom 76 | :meth:`json_dumps` that dumps with ``default`` set to 77 | :meth:`json_dump_default`. 78 | 79 | To make further documentation a bit more concrete, please, consider 80 | the following imaginatory implementation example:: 81 | 82 | class Foo(JSONDeSerializable): 83 | def to_partial_json(self): 84 | return 'foo' 85 | 86 | @classmethod 87 | def from_json(cls, jobj): 88 | return Foo() 89 | 90 | class Bar(JSONDeSerializable): 91 | def to_partial_json(self): 92 | return [Foo(), Foo()] 93 | 94 | @classmethod 95 | def from_json(cls, jobj): 96 | return Bar() 97 | 98 | """ 99 | 100 | @abc.abstractmethod 101 | def to_partial_json(self) -> Any: # pragma: no cover 102 | """Partially serialize. 103 | 104 | Following the example, **partial serialization** means the following:: 105 | 106 | assert isinstance(Bar().to_partial_json()[0], Foo) 107 | assert isinstance(Bar().to_partial_json()[1], Foo) 108 | 109 | # in particular... 110 | assert Bar().to_partial_json() != ['foo', 'foo'] 111 | 112 | :raises josepy.errors.SerializationError: 113 | in case of any serialization error. 114 | :returns: Partially serializable object. 115 | 116 | """ 117 | raise NotImplementedError() 118 | 119 | def to_json(self) -> Any: 120 | """Fully serialize. 121 | 122 | Again, following the example from before, **full serialization** 123 | means the following:: 124 | 125 | assert Bar().to_json() == ['foo', 'foo'] 126 | 127 | :raises josepy.errors.SerializationError: 128 | in case of any serialization error. 129 | :returns: Fully serialized object. 130 | 131 | """ 132 | 133 | def _serialize(obj: Any) -> Any: 134 | if isinstance(obj, JSONDeSerializable): 135 | return _serialize(obj.to_partial_json()) 136 | if isinstance(obj, str): # strings are Sequence 137 | return obj 138 | elif isinstance(obj, list): 139 | return [_serialize(subobj) for subobj in obj] 140 | elif isinstance(obj, Sequence): 141 | # default to tuple, otherwise Mapping could get 142 | # unhashable list 143 | return tuple(_serialize(subobj) for subobj in obj) 144 | elif isinstance(obj, Mapping): 145 | return {_serialize(key): _serialize(value) for key, value in obj.items()} 146 | else: 147 | return obj 148 | 149 | return _serialize(self) 150 | 151 | @classmethod 152 | @abc.abstractmethod 153 | def from_json(cls: Type[GenericJSONDeSerializable], jobj: Any) -> GenericJSONDeSerializable: 154 | """Deserialize a decoded JSON document. 155 | 156 | :param jobj: Python object, composed of only other basic data 157 | types, as decoded from JSON document. Not necessarily 158 | :class:`dict` (as decoded from "JSON object" document). 159 | 160 | :raises josepy.errors.DeserializationError: 161 | if decoding was unsuccessful, e.g. in case of unparseable 162 | X509 certificate, or wrong padding in JOSE base64 encoded 163 | string, etc. 164 | 165 | """ 166 | # TypeError: Can't instantiate abstract class with 167 | # abstract methods from_json, to_partial_json 168 | return cls() 169 | 170 | @classmethod 171 | def json_loads( 172 | cls: Type[GenericJSONDeSerializable], json_string: Union[str, bytes] 173 | ) -> GenericJSONDeSerializable: 174 | """Deserialize from JSON document string.""" 175 | try: 176 | loads = json.loads(json_string) 177 | except ValueError as error: 178 | raise errors.DeserializationError(error) 179 | return cls.from_json(loads) 180 | 181 | def json_dumps(self, **kwargs: Any) -> str: 182 | """Dump to JSON string using proper serializer. 183 | 184 | :returns: JSON document string. 185 | :rtype: str 186 | 187 | """ 188 | return json.dumps(self, default=self.json_dump_default, **kwargs) 189 | 190 | def json_dumps_pretty(self) -> str: 191 | """Dump the object to pretty JSON document string. 192 | 193 | :rtype: str 194 | 195 | """ 196 | return self.json_dumps(sort_keys=True, indent=4, separators=(",", ": ")) 197 | 198 | @classmethod 199 | def json_dump_default(cls, python_object: "JSONDeSerializable") -> Any: 200 | """Serialize Python object. 201 | 202 | This function is meant to be passed as ``default`` to 203 | :func:`json.dump` or :func:`json.dumps`. They call 204 | ``default(python_object)`` only for non-basic Python types, so 205 | this function necessarily raises :class:`TypeError` if 206 | ``python_object`` is not an instance of 207 | :class:`IJSONSerializable`. 208 | 209 | Please read the class docstring for more information. 210 | 211 | """ 212 | if isinstance(python_object, JSONDeSerializable): 213 | return python_object.to_partial_json() 214 | else: # this branch is necessary, cannot just "return" 215 | raise TypeError(repr(python_object) + " is not JSON serializable") 216 | -------------------------------------------------------------------------------- /src/josepy/jwa.py: -------------------------------------------------------------------------------- 1 | """JSON Web Algorithms. 2 | 3 | https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 4 | 5 | """ 6 | 7 | import abc 8 | import logging 9 | from collections.abc import Hashable 10 | from typing import Any, Callable, Dict 11 | 12 | import cryptography.exceptions 13 | from cryptography.hazmat.backends import default_backend 14 | from cryptography.hazmat.primitives import hashes, hmac 15 | from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa 16 | from cryptography.hazmat.primitives.asymmetric.utils import ( 17 | decode_dss_signature, 18 | encode_dss_signature, 19 | ) 20 | from cryptography.hazmat.primitives.hashes import HashAlgorithm 21 | 22 | from josepy import errors, interfaces, jwk 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | class JWA(interfaces.JSONDeSerializable): 28 | # for some reason disable=abstract-method has to be on the line 29 | # above... 30 | """JSON Web Algorithm.""" 31 | 32 | 33 | class JWASignature(JWA, Hashable): 34 | """Base class for JSON Web Signature Algorithms.""" 35 | 36 | SIGNATURES: Dict[str, "JWASignature"] = {} 37 | kty: Any 38 | 39 | def __init__(self, name: str) -> None: 40 | self.name = name 41 | 42 | def __eq__(self, other: Any) -> bool: 43 | if not isinstance(other, JWASignature): 44 | return NotImplemented 45 | return self.name == other.name 46 | 47 | def __hash__(self) -> int: 48 | return hash((self.__class__, self.name)) 49 | 50 | @classmethod 51 | def register(cls, signature_cls: "JWASignature") -> "JWASignature": 52 | """Register class for JSON deserialization.""" 53 | cls.SIGNATURES[signature_cls.name] = signature_cls 54 | return signature_cls 55 | 56 | def to_partial_json(self) -> Any: 57 | return self.name 58 | 59 | @classmethod 60 | def from_json(cls, jobj: Any) -> "JWASignature": 61 | return cls.SIGNATURES[jobj] 62 | 63 | @abc.abstractmethod 64 | def sign(self, key: Any, msg: bytes) -> bytes: # pragma: no cover 65 | """Sign the ``msg`` using ``key``.""" 66 | raise NotImplementedError() 67 | 68 | @abc.abstractmethod 69 | def verify(self, key: Any, msg: bytes, sig: bytes) -> bool: # pragma: no cover 70 | """Verify the ``msg`` and ``sig`` using ``key``.""" 71 | raise NotImplementedError() 72 | 73 | def __repr__(self) -> str: 74 | return self.name 75 | 76 | 77 | class _JWAHS(JWASignature): 78 | kty = jwk.JWKOct 79 | 80 | def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]): 81 | super().__init__(name) 82 | self.hash = hash_() 83 | 84 | def sign(self, key: bytes, msg: bytes) -> bytes: 85 | signer = hmac.HMAC(key, self.hash, backend=default_backend()) 86 | signer.update(msg) 87 | return signer.finalize() 88 | 89 | def verify(self, key: bytes, msg: bytes, sig: bytes) -> bool: 90 | verifier = hmac.HMAC(key, self.hash, backend=default_backend()) 91 | verifier.update(msg) 92 | try: 93 | verifier.verify(sig) 94 | except cryptography.exceptions.InvalidSignature as error: 95 | logger.debug(error, exc_info=True) 96 | return False 97 | else: 98 | return True 99 | 100 | 101 | class _JWARSA: 102 | kty = jwk.JWKRSA 103 | padding: Any = NotImplemented 104 | hash: HashAlgorithm = NotImplemented 105 | 106 | def sign(self, key: rsa.RSAPrivateKey, msg: bytes) -> bytes: 107 | """Sign the ``msg`` using ``key``.""" 108 | try: 109 | return key.sign(msg, self.padding, self.hash) 110 | except AttributeError as error: 111 | logger.debug(error, exc_info=True) 112 | raise errors.Error("Public key cannot be used for signing") 113 | except ValueError as error: # digest too large 114 | logger.debug(error, exc_info=True) 115 | raise errors.Error(str(error)) 116 | 117 | def verify(self, key: rsa.RSAPublicKey, msg: bytes, sig: bytes) -> bool: 118 | """Verify the ``msg` and ``sig`` using ``key``.""" 119 | try: 120 | key.verify(sig, msg, self.padding, self.hash) 121 | except cryptography.exceptions.InvalidSignature as error: 122 | logger.debug(error, exc_info=True) 123 | return False 124 | else: 125 | return True 126 | 127 | 128 | class _JWARS(_JWARSA, JWASignature): 129 | def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]) -> None: 130 | super().__init__(name) 131 | self.padding = padding.PKCS1v15() 132 | self.hash = hash_() 133 | 134 | 135 | class _JWAPS(_JWARSA, JWASignature): 136 | def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]) -> None: 137 | super().__init__(name) 138 | self.padding = padding.PSS(mgf=padding.MGF1(hash_()), salt_length=padding.PSS.MAX_LENGTH) 139 | self.hash = hash_() 140 | 141 | 142 | class _JWAEC(JWASignature): 143 | kty = jwk.JWKEC 144 | 145 | def __init__(self, name: str, hash_: Callable[[], HashAlgorithm]): 146 | super().__init__(name) 147 | self.hash = hash_() 148 | 149 | def sign(self, key: ec.EllipticCurvePrivateKey, msg: bytes) -> bytes: 150 | """Sign the ``msg`` using ``key``.""" 151 | sig = self._sign(key, msg) 152 | dr, ds = decode_dss_signature(sig) 153 | length = jwk.JWKEC.expected_length_for_curve(key.curve) 154 | return dr.to_bytes(length=length, byteorder="big") + ds.to_bytes( 155 | length=length, byteorder="big" 156 | ) 157 | 158 | def _sign(self, key: ec.EllipticCurvePrivateKey, msg: bytes) -> bytes: 159 | try: 160 | return key.sign(msg, ec.ECDSA(self.hash)) 161 | except AttributeError as error: 162 | logger.debug(error, exc_info=True) 163 | raise errors.Error("Public key cannot be used for signing") 164 | except ValueError as error: # digest too large 165 | logger.debug(error, exc_info=True) 166 | raise errors.Error(str(error)) 167 | 168 | def verify(self, key: ec.EllipticCurvePublicKey, msg: bytes, sig: bytes) -> bool: 169 | """Verify the ``msg` and ``sig`` using ``key``.""" 170 | rlen = jwk.JWKEC.expected_length_for_curve(key.curve) 171 | if len(sig) != 2 * rlen: 172 | # Format error - rfc7518 - 3.4 … MUST NOT be shortened to omit any leading zero octets 173 | return False 174 | asn1sig = encode_dss_signature( 175 | int.from_bytes(sig[0:rlen], byteorder="big"), 176 | int.from_bytes(sig[rlen:], byteorder="big"), 177 | ) 178 | return self._verify(key, msg, asn1sig) 179 | 180 | def _verify(self, key: ec.EllipticCurvePublicKey, msg: bytes, asn1sig: bytes) -> bool: 181 | try: 182 | key.verify(asn1sig, msg, ec.ECDSA(self.hash)) 183 | except cryptography.exceptions.InvalidSignature as error: 184 | logger.debug(error, exc_info=True) 185 | return False 186 | else: 187 | return True 188 | 189 | 190 | #: HMAC using SHA-256 191 | HS256 = JWASignature.register(_JWAHS("HS256", hashes.SHA256)) 192 | #: HMAC using SHA-384 193 | HS384 = JWASignature.register(_JWAHS("HS384", hashes.SHA384)) 194 | #: HMAC using SHA-512 195 | HS512 = JWASignature.register(_JWAHS("HS512", hashes.SHA512)) 196 | 197 | #: RSASSA-PKCS-v1_5 using SHA-256 198 | RS256 = JWASignature.register(_JWARS("RS256", hashes.SHA256)) 199 | #: RSASSA-PKCS-v1_5 using SHA-384 200 | RS384 = JWASignature.register(_JWARS("RS384", hashes.SHA384)) 201 | #: RSASSA-PKCS-v1_5 using SHA-512 202 | RS512 = JWASignature.register(_JWARS("RS512", hashes.SHA512)) 203 | 204 | #: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 205 | PS256 = JWASignature.register(_JWAPS("PS256", hashes.SHA256)) 206 | #: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 207 | PS384 = JWASignature.register(_JWAPS("PS384", hashes.SHA384)) 208 | #: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 209 | PS512 = JWASignature.register(_JWAPS("PS512", hashes.SHA512)) 210 | 211 | #: ECDSA using P-256 and SHA-256 212 | ES256 = JWASignature.register(_JWAEC("ES256", hashes.SHA256)) 213 | #: ECDSA using P-384 and SHA-384 214 | ES384 = JWASignature.register(_JWAEC("ES384", hashes.SHA384)) 215 | #: ECDSA using P-521 and SHA-512 216 | ES512 = JWASignature.register(_JWAEC("ES512", hashes.SHA512)) 217 | -------------------------------------------------------------------------------- /src/josepy/jwk.py: -------------------------------------------------------------------------------- 1 | """JSON Web Key.""" 2 | 3 | import abc 4 | import json 5 | import logging 6 | import math 7 | from typing import ( 8 | Any, 9 | Callable, 10 | Dict, 11 | Mapping, 12 | Optional, 13 | Sequence, 14 | Tuple, 15 | Type, 16 | Union, 17 | ) 18 | 19 | import cryptography.exceptions 20 | from cryptography.hazmat.backends import default_backend 21 | from cryptography.hazmat.primitives import hashes, serialization 22 | from cryptography.hazmat.primitives.asymmetric import ec, rsa 23 | 24 | import josepy.util 25 | from josepy import errors, json_util, util 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | class JWK(json_util.TypedJSONObjectWithFields, metaclass=abc.ABCMeta): 31 | """JSON Web Key.""" 32 | 33 | type_field_name = "kty" 34 | TYPES: Dict[str, Type["JWK"]] = {} 35 | cryptography_key_types: Tuple[Type[Any], ...] = () 36 | """Subclasses should override.""" 37 | 38 | required: Sequence[str] = NotImplemented 39 | """Required members of public key's representation as defined by JWK/JWA.""" 40 | 41 | _thumbprint_json_dumps_params: Dict[str, Union[Optional[int], Sequence[str], bool]] = { 42 | # "no whitespace or line breaks before or after any syntactic 43 | # elements" 44 | "indent": None, 45 | "separators": (",", ":"), 46 | # "members ordered lexicographically by the Unicode [UNICODE] 47 | # code points of the member names" 48 | "sort_keys": True, 49 | } 50 | key: Any 51 | 52 | def thumbprint( 53 | self, hash_function: Callable[[], hashes.HashAlgorithm] = hashes.SHA256 54 | ) -> bytes: 55 | """Compute JWK Thumbprint. 56 | 57 | https://tools.ietf.org/html/rfc7638 58 | 59 | :returns: bytes 60 | 61 | """ 62 | digest = hashes.Hash(hash_function(), backend=default_backend()) 63 | digest.update( 64 | json.dumps( 65 | {k: v for k, v in self.to_json().items() if k in self.required}, 66 | **self._thumbprint_json_dumps_params, # type: ignore[arg-type] 67 | ).encode() 68 | ) 69 | return digest.finalize() 70 | 71 | @abc.abstractmethod 72 | def public_key(self) -> "JWK": # pragma: no cover 73 | """Generate JWK with public key. 74 | 75 | For symmetric cryptosystems, this would return ``self``. 76 | 77 | """ 78 | raise NotImplementedError() 79 | 80 | @classmethod 81 | def _load_cryptography_key( 82 | cls, data: bytes, password: Optional[bytes] = None, backend: Optional[Any] = None 83 | ) -> Any: 84 | backend = default_backend() if backend is None else backend 85 | exceptions = {} 86 | 87 | # private key? 88 | loader_private: Any 89 | for loader_private in ( 90 | serialization.load_pem_private_key, 91 | serialization.load_der_private_key, 92 | ): 93 | try: 94 | return loader_private(data, password, backend) 95 | except (ValueError, TypeError, cryptography.exceptions.UnsupportedAlgorithm) as error: 96 | exceptions[str(loader_private)] = error 97 | 98 | # public key? 99 | loader_public: Any 100 | for loader_public in (serialization.load_pem_public_key, serialization.load_der_public_key): 101 | try: 102 | return loader_public(data, backend) 103 | except (ValueError, cryptography.exceptions.UnsupportedAlgorithm) as error: 104 | exceptions[str(loader_public)] = error 105 | 106 | # no luck 107 | raise errors.Error("Unable to deserialize key: {0}".format(exceptions)) 108 | 109 | @classmethod 110 | def load( 111 | cls, data: bytes, password: Optional[bytes] = None, backend: Optional[Any] = None 112 | ) -> "JWK": 113 | """Load serialized key as JWK. 114 | 115 | :param str data: Public or private key serialized as PEM or DER. 116 | :param str password: Optional password. 117 | :param backend: A `.PEMSerializationBackend` and 118 | `.DERSerializationBackend` provider. 119 | 120 | :raises errors.Error: if unable to deserialize, or unsupported 121 | JWK algorithm 122 | 123 | :returns: JWK of an appropriate type. 124 | :rtype: `JWK` 125 | 126 | """ 127 | try: 128 | key = cls._load_cryptography_key(data, password, backend) 129 | except errors.Error as error: 130 | logger.debug("Loading symmetric key, asymmetric failed: %s", error) 131 | return JWKOct(key=data) 132 | 133 | if cls.typ is not NotImplemented and not isinstance(key, cls.cryptography_key_types): 134 | raise errors.Error( 135 | "Unable to deserialize {0} into {1}".format(key.__class__, cls.__class__) 136 | ) 137 | for jwk_cls in cls.TYPES.values(): 138 | if isinstance(key, jwk_cls.cryptography_key_types): 139 | return jwk_cls(key=key) 140 | raise errors.Error("Unsupported algorithm: {0}".format(key.__class__)) 141 | 142 | 143 | @JWK.register 144 | class JWKOct(JWK): 145 | """Symmetric JWK.""" 146 | 147 | typ = "oct" 148 | __slots__ = ("key",) 149 | required = ("k", JWK.type_field_name) 150 | key: bytes 151 | 152 | def fields_to_partial_json(self) -> Dict[str, str]: 153 | # TODO: An "alg" member SHOULD also be present to identify the 154 | # algorithm intended to be used with the key, unless the 155 | # application uses another means or convention to determine 156 | # the algorithm used. 157 | return {"k": json_util.encode_b64jose(self.key)} 158 | 159 | @classmethod 160 | def fields_from_json(cls, jobj: Mapping[str, Any]) -> "JWKOct": 161 | return cls(key=json_util.decode_b64jose(jobj["k"])) 162 | 163 | def public_key(self) -> "JWKOct": 164 | return self 165 | 166 | 167 | @JWK.register 168 | class JWKRSA(JWK): 169 | """RSA JWK. 170 | 171 | :ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` 172 | or :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` wrapped 173 | in :class:`~josepy.util.ComparableRSAKey` 174 | 175 | """ 176 | 177 | typ = "RSA" 178 | cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) 179 | __slots__ = ("key",) 180 | required = ("e", JWK.type_field_name, "n") 181 | key: josepy.util.ComparableRSAKey 182 | 183 | def __init__(self, *args: Any, **kwargs: Any) -> None: 184 | if "key" in kwargs and not isinstance(kwargs["key"], util.ComparableRSAKey): 185 | kwargs["key"] = util.ComparableRSAKey(kwargs["key"]) 186 | super().__init__(*args, **kwargs) 187 | 188 | @classmethod 189 | def _encode_param(cls, data: int) -> str: 190 | """Encode Base64urlUInt. 191 | :type data: long 192 | :rtype: unicode 193 | """ 194 | length = max(data.bit_length(), 8) # decoding 0 195 | length = math.ceil(length / 8) 196 | return json_util.encode_b64jose(data.to_bytes(byteorder="big", length=length)) 197 | 198 | @classmethod 199 | def _decode_param(cls, data: str) -> int: 200 | """Decode Base64urlUInt.""" 201 | try: 202 | binary = json_util.decode_b64jose(data) 203 | if not binary: 204 | raise errors.DeserializationError() 205 | return int.from_bytes(binary, byteorder="big") 206 | except ValueError: # invalid literal for long() with base 16 207 | raise errors.DeserializationError() 208 | 209 | def public_key(self) -> "JWKRSA": 210 | return type(self)(key=self.key.public_key()) 211 | 212 | @classmethod 213 | def fields_from_json(cls, jobj: Mapping[str, Any]) -> "JWKRSA": 214 | n, e = (cls._decode_param(jobj[x]) for x in ("n", "e")) 215 | public_numbers = rsa.RSAPublicNumbers(e=e, n=n) 216 | 217 | # public key 218 | if "d" not in jobj: 219 | return cls(key=public_numbers.public_key(default_backend())) 220 | 221 | # private key 222 | d = cls._decode_param(jobj["d"]) 223 | if ( 224 | "p" in jobj 225 | or "q" in jobj 226 | or "dp" in jobj 227 | or "dq" in jobj 228 | or "qi" in jobj 229 | or "oth" in jobj 230 | ): 231 | # "If the producer includes any of the other private 232 | # key parameters, then all of the others MUST be 233 | # present, with the exception of "oth", which MUST 234 | # only be present when more than two prime factors 235 | # were used." 236 | ( 237 | p, 238 | q, 239 | dp, 240 | dq, 241 | qi, 242 | ) = all_params = tuple(jobj.get(x) for x in ("p", "q", "dp", "dq", "qi")) 243 | if tuple(param for param in all_params if param is None): 244 | raise errors.Error("Some private parameters are missing: {0}".format(all_params)) 245 | p, q, dp, dq, qi = tuple(cls._decode_param(str(x)) for x in all_params) 246 | 247 | # TODO: check for oth 248 | else: 249 | # cryptography>=0.8 250 | p, q = rsa.rsa_recover_prime_factors(n, e, d) 251 | dp = rsa.rsa_crt_dmp1(d, p) 252 | dq = rsa.rsa_crt_dmq1(d, q) 253 | qi = rsa.rsa_crt_iqmp(p, q) 254 | 255 | key = rsa.RSAPrivateNumbers(p, q, d, dp, dq, qi, public_numbers).private_key( 256 | default_backend() 257 | ) 258 | 259 | return cls(key=key) 260 | 261 | def fields_to_partial_json(self) -> Dict[str, Any]: 262 | if isinstance(self.key._wrapped, rsa.RSAPublicKey): 263 | numbers = self.key.public_numbers() 264 | params = { 265 | "n": numbers.n, 266 | "e": numbers.e, 267 | } 268 | else: # rsa.RSAPrivateKey 269 | private = self.key.private_numbers() 270 | public = self.key.public_key().public_numbers() 271 | params = { 272 | "n": public.n, 273 | "e": public.e, 274 | "d": private.d, 275 | "p": private.p, 276 | "q": private.q, 277 | "dp": private.dmp1, 278 | "dq": private.dmq1, 279 | "qi": private.iqmp, 280 | } 281 | return {key: self._encode_param(value) for key, value in params.items()} 282 | 283 | 284 | @JWK.register 285 | class JWKEC(JWK): 286 | """EC JWK. 287 | 288 | :ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` 289 | or :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` wrapped 290 | in :class:`~josepy.util.ComparableECKey` 291 | 292 | """ 293 | 294 | typ = "EC" 295 | __slots__ = ("key",) 296 | cryptography_key_types = (ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) 297 | required = ("crv", JWK.type_field_name, "x", "y") 298 | key: josepy.util.ComparableECKey 299 | 300 | def __init__(self, *args: Any, **kwargs: Any) -> None: 301 | if "key" in kwargs and not isinstance(kwargs["key"], util.ComparableECKey): 302 | kwargs["key"] = util.ComparableECKey(kwargs["key"]) 303 | super().__init__(*args, **kwargs) 304 | 305 | @classmethod 306 | def _encode_param(cls, data: int, length: int) -> str: 307 | """Encode Base64urlUInt. 308 | :type data: long 309 | :type key_size: long 310 | :rtype: unicode 311 | """ 312 | return json_util.encode_b64jose(data.to_bytes(byteorder="big", length=length)) 313 | 314 | @classmethod 315 | def _decode_param(cls, data: str, name: str, valid_length: int) -> int: 316 | """Decode Base64urlUInt.""" 317 | try: 318 | binary = json_util.decode_b64jose(data) 319 | if len(binary) != valid_length: 320 | raise errors.DeserializationError( 321 | f'Expected parameter "{name}" to be {valid_length} bytes ' 322 | f"after base64-decoding; got {len(binary)} bytes instead" 323 | ) 324 | return int.from_bytes(binary, byteorder="big") 325 | except ValueError: # invalid literal for long() with base 16 326 | raise errors.DeserializationError() 327 | 328 | @classmethod 329 | def _curve_name_to_crv(cls, curve_name: str) -> str: 330 | if curve_name == "secp256r1": 331 | return "P-256" 332 | if curve_name == "secp384r1": 333 | return "P-384" 334 | if curve_name == "secp521r1": 335 | return "P-521" 336 | raise errors.SerializationError() 337 | 338 | @classmethod 339 | def _crv_to_curve(cls, crv: str) -> ec.EllipticCurve: 340 | # crv is case-sensitive 341 | if crv == "P-256": 342 | return ec.SECP256R1() 343 | if crv == "P-384": 344 | return ec.SECP384R1() 345 | if crv == "P-521": 346 | return ec.SECP521R1() 347 | raise errors.DeserializationError() 348 | 349 | @classmethod 350 | def expected_length_for_curve(cls, curve: ec.EllipticCurve) -> int: 351 | if isinstance(curve, ec.SECP256R1): 352 | return 32 353 | elif isinstance(curve, ec.SECP384R1): 354 | return 48 355 | elif isinstance(curve, ec.SECP521R1): 356 | return 66 357 | raise ValueError(f"Unexpected curve: {curve}") 358 | 359 | def fields_to_partial_json(self) -> Dict[str, Any]: 360 | params = {} 361 | if isinstance(self.key._wrapped, ec.EllipticCurvePublicKey): 362 | public = self.key.public_numbers() 363 | elif isinstance(self.key._wrapped, ec.EllipticCurvePrivateKey): 364 | private = self.key.private_numbers() 365 | public = self.key.public_key().public_numbers() 366 | params["d"] = private.private_value 367 | else: 368 | raise errors.SerializationError( 369 | "Supplied key is neither of type EllipticCurvePublicKey " 370 | "nor EllipticCurvePrivateKey" 371 | ) 372 | params["x"] = public.x 373 | params["y"] = public.y 374 | params = { 375 | key: self._encode_param(value, self.expected_length_for_curve(public.curve)) 376 | for key, value in params.items() 377 | } 378 | params["crv"] = self._curve_name_to_crv(public.curve.name) 379 | return params 380 | 381 | @classmethod 382 | def fields_from_json(cls, jobj: Mapping[str, Any]) -> "JWKEC": 383 | curve = cls._crv_to_curve(jobj["crv"]) 384 | expected_length = cls.expected_length_for_curve(curve) 385 | x, y = (cls._decode_param(jobj[n], n, expected_length) for n in ("x", "y")) 386 | public_numbers = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=curve) 387 | 388 | # private key 389 | if "d" not in jobj: 390 | return cls(key=public_numbers.public_key(default_backend())) 391 | 392 | # private key 393 | d = cls._decode_param(jobj["d"], "d", expected_length) 394 | key = ec.EllipticCurvePrivateNumbers(d, public_numbers).private_key(default_backend()) 395 | return cls(key=key) 396 | 397 | def public_key(self) -> "JWKEC": 398 | # Unlike RSAPrivateKey, EllipticCurvePrivateKey does not contain public_key() 399 | if hasattr(self.key, "public_key"): 400 | key = self.key.public_key() 401 | else: 402 | key = self.key.public_numbers().public_key(default_backend()) 403 | return type(self)(key=key) 404 | -------------------------------------------------------------------------------- /src/josepy/jws.py: -------------------------------------------------------------------------------- 1 | """JSON Web Signature.""" 2 | 3 | import argparse 4 | import base64 5 | import sys 6 | from typing import ( 7 | Any, 8 | Dict, 9 | FrozenSet, 10 | List, 11 | Mapping, 12 | Optional, 13 | Tuple, 14 | Type, 15 | cast, 16 | ) 17 | 18 | from cryptography import x509 19 | from cryptography.hazmat.primitives.serialization import Encoding 20 | 21 | import josepy 22 | from josepy import b64, errors, json_util, jwa 23 | from josepy import jwk as jwk_mod 24 | 25 | 26 | class MediaType: 27 | """MediaType field encoder/decoder.""" 28 | 29 | PREFIX = "application/" 30 | """MIME Media Type and Content Type prefix.""" 31 | 32 | @classmethod 33 | def decode(cls, value: str) -> str: 34 | """Decoder.""" 35 | # 4.1.10 36 | if "/" not in value: 37 | if ";" in value: 38 | raise errors.DeserializationError("Unexpected semi-colon") 39 | return cls.PREFIX + value 40 | return value 41 | 42 | @classmethod 43 | def encode(cls, value: str) -> str: 44 | """Encoder.""" 45 | # 4.1.10 46 | if ";" not in value: 47 | assert value.startswith(cls.PREFIX) 48 | return value[len(cls.PREFIX) :] 49 | return value 50 | 51 | 52 | class Header(json_util.JSONObjectWithFields): 53 | """JOSE Header. 54 | 55 | .. warning:: This class supports **only** Registered Header 56 | Parameter Names (as defined in section 4.1 of the 57 | protocol). If you need Public Header Parameter Names (4.2) 58 | or Private Header Parameter Names (4.3), you must subclass 59 | and override :meth:`from_json` and :meth:`to_partial_json` 60 | appropriately. 61 | 62 | .. warning:: This class does not support any extensions through 63 | the "crit" (Critical) Header Parameter (4.1.11) and as a 64 | conforming implementation, :meth:`from_json` treats its 65 | occurrence as an error. Please subclass if you seek for 66 | a different behaviour. 67 | 68 | :ivar x5tS256: "x5t#S256" 69 | :ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`. 70 | :ivar str cty: Content-Type, inc. :const:`MediaType.PREFIX`. 71 | 72 | """ 73 | 74 | alg: Optional[jwa.JWASignature] = json_util.field( 75 | "alg", decoder=jwa.JWASignature.from_json, omitempty=True 76 | ) 77 | jku: Optional[bytes] = json_util.field("jku", omitempty=True) 78 | jwk: Optional[jwk_mod.JWK] = json_util.field( 79 | "jwk", decoder=jwk_mod.JWK.from_json, omitempty=True 80 | ) 81 | kid: Optional[str] = json_util.field("kid", omitempty=True) 82 | x5u: Optional[bytes] = json_util.field("x5u", omitempty=True) 83 | x5c: Tuple[x509.Certificate, ...] = json_util.field("x5c", omitempty=True, default=()) 84 | x5t: Optional[bytes] = json_util.field("x5t", decoder=json_util.decode_b64jose, omitempty=True) 85 | x5tS256: Optional[bytes] = json_util.field( 86 | "x5t#S256", decoder=json_util.decode_b64jose, omitempty=True 87 | ) 88 | typ: Optional[MediaType] = json_util.field( 89 | "typ", encoder=MediaType.encode, decoder=MediaType.decode, omitempty=True 90 | ) 91 | cty: Optional[MediaType] = json_util.field( 92 | "cty", encoder=MediaType.encode, decoder=MediaType.decode, omitempty=True 93 | ) 94 | crit: Tuple[Any, ...] = json_util.field("crit", omitempty=True, default=()) 95 | _fields: Dict[str, json_util.Field] 96 | 97 | def not_omitted(self) -> Dict[str, json_util.Field]: 98 | """Fields that would not be omitted in the JSON object.""" 99 | return { 100 | name: getattr(self, name) 101 | for name, field in self._fields.items() 102 | if not field.omit(getattr(self, name)) 103 | } 104 | 105 | def __add__(self, other: Any) -> "Header": 106 | if not isinstance(other, type(self)): 107 | raise TypeError("Header cannot be added to: {0}".format(type(other))) 108 | 109 | not_omitted_self = self.not_omitted() 110 | not_omitted_other = other.not_omitted() 111 | 112 | if set(not_omitted_self).intersection(not_omitted_other): 113 | raise TypeError("Addition of overlapping headers not defined") 114 | 115 | not_omitted_self.update(not_omitted_other) 116 | return type(self)(**not_omitted_self) 117 | 118 | def find_key(self) -> josepy.JWK: 119 | """Find key based on header. 120 | 121 | .. todo:: Supports only "jwk" header parameter lookup. 122 | 123 | :returns: (Public) key found in the header. 124 | :rtype: .JWK 125 | 126 | :raises josepy.errors.Error: if key could not be found 127 | 128 | """ 129 | if self.jwk is None: 130 | raise errors.Error("No key found") 131 | return self.jwk 132 | 133 | @crit.decoder # type: ignore 134 | def crit(unused_value: Any) -> Any: 135 | raise errors.DeserializationError('"crit" is not supported, please subclass') 136 | 137 | # x5c does NOT use JOSE Base64 (4.1.6) 138 | 139 | @x5c.encoder # type: ignore 140 | def x5c(value): 141 | """ 142 | .. versionchanged:: 2.0.0 143 | The values are now `cryptography.x509.Certificate` objects. 144 | Previously these were `josepy.util.ComparableX509` objects, which wrapped 145 | `OpenSSL.crypto.X509` objects. 146 | """ 147 | return [base64.b64encode(cert.public_bytes(Encoding.DER)) for cert in value] 148 | 149 | @x5c.decoder # type: ignore 150 | def x5c(value): 151 | """ 152 | .. versionchanged:: 2.0.0 153 | The values are now `cryptography.x509.Certificate` objects. 154 | Previously these were `josepy.util.ComparableX509` objects, which wrapped 155 | `OpenSSL.crypto.X509` objects. 156 | """ 157 | try: 158 | return tuple(x509.load_der_x509_certificate(base64.b64decode(cert)) for cert in value) 159 | except ValueError as error: 160 | raise errors.DeserializationError(error) 161 | 162 | 163 | class Signature(json_util.JSONObjectWithFields): 164 | """JWS Signature. 165 | 166 | :ivar combined: Combined Header (protected and unprotected, 167 | :class:`Header`). 168 | :ivar unicode protected: JWS protected header (Jose Base-64 decoded). 169 | :ivar header: JWS Unprotected Header (:class:`Header`). 170 | :ivar str signature: The signature. 171 | 172 | """ 173 | 174 | header_cls = Header 175 | combined: Header 176 | 177 | __slots__ = ("combined",) 178 | protected: str = json_util.field("protected", omitempty=True, default="") 179 | header: Header = json_util.field( 180 | "header", omitempty=True, default=header_cls(), decoder=header_cls.from_json 181 | ) 182 | signature: bytes = json_util.field( 183 | "signature", decoder=json_util.decode_b64jose, encoder=json_util.encode_b64jose 184 | ) 185 | 186 | @protected.encoder # type: ignore 187 | def protected(value: str) -> str: 188 | # wrong type guess (Signature, not bytes) | pylint: disable=no-member 189 | return json_util.encode_b64jose(value.encode("utf-8")) 190 | 191 | @protected.decoder # type: ignore 192 | def protected(value: str) -> str: 193 | return json_util.decode_b64jose(value).decode("utf-8") 194 | 195 | def __init__(self, **kwargs: Any) -> None: 196 | if "combined" not in kwargs: 197 | kwargs = self._with_combined(kwargs) 198 | super().__init__(**kwargs) 199 | assert self.combined.alg is not None 200 | 201 | @classmethod 202 | def _with_combined(cls, kwargs: Any) -> Dict[str, Any]: 203 | assert "combined" not in kwargs 204 | header = kwargs.get("header", cls._fields["header"].default) 205 | protected = kwargs.get("protected", cls._fields["protected"].default) 206 | 207 | if protected: 208 | combined = header + cls.header_cls.json_loads(protected) 209 | else: 210 | combined = header 211 | 212 | kwargs["combined"] = combined 213 | return kwargs 214 | 215 | @classmethod 216 | def _msg(cls, protected: str, payload: bytes) -> bytes: 217 | return b64.b64encode(protected.encode("utf-8")) + b"." + b64.b64encode(payload) 218 | 219 | def verify(self, payload: bytes, key: Optional[josepy.JWK] = None) -> bool: 220 | """Verify. 221 | 222 | :param bytes payload: Payload to verify. 223 | :param JWK key: Key used for verification. 224 | 225 | """ 226 | actual_key: josepy.JWK = self.combined.find_key() if key is None else key 227 | if not self.combined.alg: 228 | raise josepy.Error("Not signature algorithm defined.") 229 | return self.combined.alg.verify( 230 | key=actual_key.key, sig=self.signature, msg=self._msg(self.protected, payload) 231 | ) 232 | 233 | @classmethod 234 | def sign( 235 | cls, 236 | payload: bytes, 237 | key: josepy.JWK, 238 | alg: josepy.JWASignature, 239 | include_jwk: bool = True, 240 | protect: FrozenSet = frozenset(), 241 | **kwargs: Any, 242 | ) -> "Signature": 243 | """Sign. 244 | 245 | :param bytes payload: Payload to sign. 246 | :param JWK key: Key for signature. 247 | :param JWASignature alg: Signature algorithm to use to sign. 248 | :param bool include_jwk: If True, insert the JWK inside the signature headers. 249 | :param FrozenSet protect: List of headers to protect. 250 | 251 | """ 252 | assert isinstance(key, alg.kty) 253 | 254 | header_params = kwargs 255 | header_params["alg"] = alg 256 | if include_jwk: 257 | header_params["jwk"] = key.public_key() 258 | 259 | assert set(header_params).issubset(cls.header_cls._fields) 260 | assert protect.issubset(cls.header_cls._fields) 261 | 262 | protected_params = {} 263 | for header in protect: 264 | if header in header_params: 265 | protected_params[header] = header_params.pop(header) 266 | if protected_params: 267 | protected = cls.header_cls(**protected_params).json_dumps() 268 | else: 269 | protected = "" 270 | 271 | header = cls.header_cls(**header_params) 272 | signature = alg.sign(key.key, cls._msg(protected, payload)) 273 | 274 | return cls(protected=protected, header=header, signature=signature) 275 | 276 | def fields_to_partial_json(self) -> Dict[str, Any]: 277 | fields = super().fields_to_partial_json() 278 | if not fields["header"].not_omitted(): 279 | del fields["header"] 280 | return fields 281 | 282 | @classmethod 283 | def fields_from_json(cls, jobj: Mapping[str, Any]) -> Dict[str, Any]: 284 | fields = super().fields_from_json(jobj) 285 | fields_with_combined = cls._with_combined(fields) 286 | if "alg" not in fields_with_combined["combined"].not_omitted(): 287 | raise errors.DeserializationError("alg not present") 288 | return fields_with_combined 289 | 290 | 291 | class JWS(json_util.JSONObjectWithFields): 292 | """JSON Web Signature. 293 | 294 | :ivar str payload: JWS Payload. 295 | :ivar str signature: JWS Signatures. 296 | 297 | """ 298 | 299 | __slots__ = ("payload", "signatures") 300 | payload: bytes 301 | signatures: List[Signature] 302 | 303 | signature_cls = Signature 304 | 305 | def verify(self, key: Optional[josepy.JWK] = None) -> bool: 306 | """Verify.""" 307 | return all(sig.verify(self.payload, key) for sig in self.signatures) 308 | 309 | @classmethod 310 | def sign(cls, payload: bytes, **kwargs: Any) -> "JWS": 311 | """Sign.""" 312 | return cls(payload=payload, signatures=(cls.signature_cls.sign(payload=payload, **kwargs),)) 313 | 314 | @property 315 | def signature(self) -> Signature: 316 | """Get a singleton signature. 317 | 318 | :rtype: :class:`JWS.signature_cls` 319 | 320 | """ 321 | assert len(self.signatures) == 1 322 | return self.signatures[0] 323 | 324 | def to_compact(self) -> bytes: 325 | """Compact serialization. 326 | 327 | :rtype: bytes 328 | 329 | """ 330 | assert len(self.signatures) == 1 331 | 332 | assert "alg" not in self.signature.header.not_omitted() 333 | # ... it must be in protected 334 | 335 | return ( 336 | b64.b64encode(self.signature.protected.encode("utf-8")) 337 | + b"." 338 | + b64.b64encode(self.payload) 339 | + b"." 340 | + b64.b64encode(self.signature.signature) 341 | ) 342 | 343 | @classmethod 344 | def from_compact(cls, compact: bytes) -> "JWS": 345 | """Compact deserialization. 346 | 347 | :param bytes compact: 348 | 349 | """ 350 | try: 351 | protected, payload, signature = compact.split(b".") 352 | except ValueError: 353 | raise errors.DeserializationError( 354 | "Compact JWS serialization should comprise of exactly" " 3 dot-separated components" 355 | ) 356 | 357 | sig = cls.signature_cls( 358 | protected=b64.b64decode(protected).decode("utf-8"), signature=b64.b64decode(signature) 359 | ) 360 | return cls(payload=b64.b64decode(payload), signatures=(sig,)) 361 | 362 | def to_partial_json(self, flat: bool = True) -> Dict[str, Any]: 363 | assert self.signatures 364 | payload = json_util.encode_b64jose(self.payload) 365 | 366 | if flat and len(self.signatures) == 1: 367 | ret = self.signatures[0].to_partial_json() 368 | ret["payload"] = payload 369 | return ret 370 | else: 371 | return { 372 | "payload": payload, 373 | "signatures": self.signatures, 374 | } 375 | 376 | @classmethod 377 | def from_json(cls, jobj: Mapping[str, Any]) -> "JWS": 378 | if "signature" in jobj and "signatures" in jobj: 379 | raise errors.DeserializationError("Flat mixed with non-flat") 380 | elif "signature" in jobj: # flat 381 | filtered = {key: value for key, value in jobj.items() if key != "payload"} 382 | return cls( 383 | payload=json_util.decode_b64jose(jobj["payload"]), 384 | signatures=(cls.signature_cls.from_json(filtered),), 385 | ) 386 | else: 387 | return cls( 388 | payload=json_util.decode_b64jose(jobj["payload"]), 389 | signatures=tuple(cls.signature_cls.from_json(sig) for sig in jobj["signatures"]), 390 | ) 391 | 392 | 393 | class CLI: 394 | """JWS CLI.""" 395 | 396 | @classmethod 397 | def sign(cls, args: argparse.Namespace) -> None: 398 | """Sign.""" 399 | with open(args.key, "rb") as fp: 400 | key = args.alg.kty.load(fp.read()) 401 | if args.protect is None: 402 | args.protect = [] 403 | if args.compact: 404 | args.protect.append("alg") 405 | 406 | sig = JWS.sign( 407 | payload=sys.stdin.read().encode(), key=key, alg=args.alg, protect=set(args.protect) 408 | ) 409 | 410 | if args.compact: 411 | print(sig.to_compact().decode("utf-8")) 412 | else: # JSON 413 | print(sig.json_dumps_pretty()) 414 | 415 | @classmethod 416 | def verify(cls, args: argparse.Namespace) -> bool: 417 | """Verify.""" 418 | if args.compact: 419 | sig = JWS.from_compact(sys.stdin.read().encode()) 420 | else: # JSON 421 | try: 422 | sig = cast(JWS, JWS.json_loads(sys.stdin.read())) 423 | except errors.Error as error: 424 | print(error) 425 | return False 426 | 427 | if args.key is not None: 428 | assert args.kty is not None 429 | with open(args.key, "rb") as fp: 430 | key = args.kty.load(fp.read()).public_key() 431 | else: 432 | key = None 433 | 434 | sys.stdout.write(sig.payload.decode()) 435 | return not sig.verify(key=key) 436 | 437 | @classmethod 438 | def _alg_type(cls, arg: Any) -> jwa.JWASignature: 439 | return jwa.JWASignature.from_json(arg) 440 | 441 | @classmethod 442 | def _header_type(cls, arg: Any) -> Any: 443 | assert arg in Signature.header_cls._fields 444 | return arg 445 | 446 | @classmethod 447 | def _kty_type(cls, arg: Any) -> Type[jwk_mod.JWK]: 448 | assert arg in jwk_mod.JWK.TYPES 449 | return jwk_mod.JWK.TYPES[arg] 450 | 451 | @classmethod 452 | def run(cls, args: Optional[List[str]] = None) -> Optional[bool]: 453 | """Parse arguments and sign/verify.""" 454 | if args is None: 455 | args = sys.argv[1:] 456 | parser = argparse.ArgumentParser() 457 | parser.add_argument("--compact", action="store_true") 458 | 459 | subparsers = parser.add_subparsers() 460 | parser_sign = subparsers.add_parser("sign") 461 | parser_sign.set_defaults(func=cls.sign) 462 | parser_sign.add_argument("-k", "--key", required=True) 463 | parser_sign.add_argument("-a", "--alg", type=cls._alg_type, default=jwa.RS256) 464 | parser_sign.add_argument("-p", "--protect", action="append", type=cls._header_type) 465 | 466 | parser_verify = subparsers.add_parser("verify") 467 | parser_verify.set_defaults(func=cls.verify) 468 | parser_verify.add_argument("-k", "--key", required=False) 469 | parser_verify.add_argument("--kty", type=cls._kty_type, required=False) 470 | 471 | parsed = parser.parse_args(args) 472 | return parsed.func(parsed) 473 | 474 | 475 | if __name__ == "__main__": 476 | exit(CLI.run()) # pragma: no cover 477 | -------------------------------------------------------------------------------- /src/josepy/magic_typing.py: -------------------------------------------------------------------------------- 1 | """Shim class to not have to depend on typing module in prod.""" 2 | 3 | # mypy: ignore-errors 4 | import sys 5 | import warnings 6 | from typing import Any 7 | 8 | warnings.warn( 9 | "josepy.magic_typing is deprecated and will be removed in a future release.", DeprecationWarning 10 | ) 11 | 12 | 13 | class TypingClass: 14 | """Ignore import errors by getting anything""" 15 | 16 | def __getattr__(self, name: str) -> Any: 17 | return None 18 | 19 | 20 | try: 21 | # mypy doesn't respect modifying sys.modules 22 | from typing import * # noqa: F401,F403 23 | except ImportError: 24 | sys.modules[__name__] = TypingClass() 25 | -------------------------------------------------------------------------------- /src/josepy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certbot/josepy/8ddcaaed99a61e9277df1ec00157f0aea53378d4/src/josepy/py.typed -------------------------------------------------------------------------------- /src/josepy/util.py: -------------------------------------------------------------------------------- 1 | """JOSE utilities.""" 2 | 3 | import abc 4 | import sys 5 | import warnings 6 | from collections.abc import Hashable, Mapping 7 | from types import ModuleType 8 | from typing import Any, Callable, Iterator, List, Tuple, TypeVar, Union, cast 9 | 10 | from cryptography.hazmat.primitives.asymmetric import ec, rsa 11 | 12 | 13 | # Deprecated. Please use built-in decorators @classmethod and abc.abstractmethod together instead. 14 | def abstractclassmethod(func: Callable) -> classmethod: 15 | return classmethod(abc.abstractmethod(func)) 16 | 17 | 18 | class ComparableKey: 19 | """Comparable wrapper for ``cryptography`` keys. 20 | 21 | See https://github.com/pyca/cryptography/issues/2122. 22 | 23 | """ 24 | 25 | __hash__: Callable[[], int] = NotImplemented 26 | 27 | def __init__( 28 | self, 29 | wrapped: Union[ 30 | rsa.RSAPrivateKeyWithSerialization, 31 | rsa.RSAPublicKeyWithSerialization, 32 | ec.EllipticCurvePrivateKeyWithSerialization, 33 | ec.EllipticCurvePublicKeyWithSerialization, 34 | ], 35 | ): 36 | self._wrapped = wrapped 37 | 38 | def __getattr__(self, name: str) -> Any: 39 | return getattr(self._wrapped, name) 40 | 41 | def __eq__(self, other: Any) -> bool: 42 | if ( 43 | not isinstance(other, self.__class__) 44 | or self._wrapped.__class__ is not other._wrapped.__class__ 45 | ): 46 | return NotImplemented 47 | elif hasattr(self._wrapped, "private_numbers"): 48 | return self.private_numbers() == other.private_numbers() 49 | elif hasattr(self._wrapped, "public_numbers"): 50 | return self.public_numbers() == other.public_numbers() 51 | else: 52 | return NotImplemented 53 | 54 | def __repr__(self) -> str: 55 | return "<{0}({1!r})>".format(self.__class__.__name__, self._wrapped) 56 | 57 | def public_key(self) -> "ComparableKey": 58 | """Get wrapped public key.""" 59 | if isinstance( 60 | self._wrapped, 61 | (rsa.RSAPublicKeyWithSerialization, ec.EllipticCurvePublicKeyWithSerialization), 62 | ): 63 | return self 64 | 65 | return self.__class__(self._wrapped.public_key()) 66 | 67 | 68 | class ComparableRSAKey(ComparableKey): 69 | """Wrapper for ``cryptography`` RSA keys. 70 | 71 | Wraps around: 72 | 73 | - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` 74 | - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` 75 | 76 | """ 77 | 78 | def __hash__(self) -> int: 79 | # public_numbers() hasn't got stable hash! 80 | # https://github.com/pyca/cryptography/issues/2143 81 | if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization): 82 | priv = self.private_numbers() 83 | pub = priv.public_numbers 84 | return hash( 85 | (self.__class__, priv.p, priv.q, priv.dmp1, priv.dmq1, priv.iqmp, pub.n, pub.e) 86 | ) 87 | elif isinstance(self._wrapped, rsa.RSAPublicKeyWithSerialization): 88 | pub = self.public_numbers() 89 | return hash((self.__class__, pub.n, pub.e)) 90 | 91 | raise NotImplementedError() 92 | 93 | 94 | class ComparableECKey(ComparableKey): 95 | """Wrapper for ``cryptography`` EC keys. 96 | Wraps around: 97 | - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` 98 | - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` 99 | """ 100 | 101 | def __hash__(self) -> int: 102 | # public_numbers() hasn't got stable hash! 103 | # https://github.com/pyca/cryptography/issues/2143 104 | if isinstance(self._wrapped, ec.EllipticCurvePrivateKeyWithSerialization): 105 | priv = self.private_numbers() 106 | pub = priv.public_numbers 107 | return hash((self.__class__, pub.curve.name, pub.x, pub.y, priv.private_value)) 108 | elif isinstance(self._wrapped, ec.EllipticCurvePublicKeyWithSerialization): 109 | pub = self.public_numbers() 110 | return hash((self.__class__, pub.curve.name, pub.x, pub.y)) 111 | 112 | raise NotImplementedError() 113 | 114 | 115 | GenericImmutableMap = TypeVar("GenericImmutableMap", bound="ImmutableMap") 116 | 117 | 118 | class ImmutableMap(Mapping, Hashable): 119 | """Immutable key to value mapping with attribute access.""" 120 | 121 | __slots__: Tuple[str, ...] = () 122 | """Must be overridden in subclasses.""" 123 | 124 | def __init__(self, **kwargs: Any) -> None: 125 | if set(kwargs) != set(self.__slots__): 126 | raise TypeError( 127 | "__init__() takes exactly the following arguments: {0} " 128 | "({1} given)".format( 129 | ", ".join(self.__slots__), ", ".join(kwargs) if kwargs else "none" 130 | ) 131 | ) 132 | for slot in self.__slots__: 133 | object.__setattr__(self, slot, kwargs.pop(slot)) 134 | 135 | def update(self: GenericImmutableMap, **kwargs: Any) -> GenericImmutableMap: 136 | """Return updated map.""" 137 | items: Mapping[str, Any] = {**self, **kwargs} 138 | return type(self)(**items) 139 | 140 | def __getitem__(self, key: str) -> Any: 141 | try: 142 | return getattr(self, key) 143 | except AttributeError: 144 | raise KeyError(key) 145 | 146 | def __iter__(self) -> Iterator[str]: 147 | return iter(self.__slots__) 148 | 149 | def __len__(self) -> int: 150 | return len(self.__slots__) 151 | 152 | def __hash__(self) -> int: 153 | return hash(tuple(getattr(self, slot) for slot in self.__slots__)) 154 | 155 | def __setattr__(self, name: str, value: Any) -> None: 156 | raise AttributeError("can't set attribute") 157 | 158 | def __repr__(self) -> str: 159 | return "{0}({1})".format( 160 | self.__class__.__name__, 161 | ", ".join("{0}={1!r}".format(key, value) for key, value in self.items()), 162 | ) 163 | 164 | 165 | class frozendict(Mapping, Hashable): 166 | """Frozen dictionary.""" 167 | 168 | __slots__ = ("_items", "_keys") 169 | 170 | def __init__(self, *args: Any, **kwargs: Any) -> None: 171 | items: Mapping 172 | if kwargs and not args: 173 | items = dict(kwargs) 174 | elif len(args) == 1 and isinstance(args[0], Mapping): 175 | items = args[0] 176 | else: 177 | raise TypeError() 178 | # TODO: support generators/iterators 179 | 180 | object.__setattr__(self, "_items", items) 181 | object.__setattr__(self, "_keys", tuple(sorted(items.keys()))) 182 | 183 | def __getitem__(self, key: str) -> Any: 184 | return self._items[key] 185 | 186 | def __iter__(self) -> Iterator[str]: 187 | return iter(self._keys) 188 | 189 | def __len__(self) -> int: 190 | return len(self._items) 191 | 192 | def _sorted_items(self) -> Tuple[Tuple[str, Any], ...]: 193 | return tuple((key, self[key]) for key in self._keys) 194 | 195 | def __hash__(self) -> int: 196 | return hash(self._sorted_items()) 197 | 198 | def __getattr__(self, name: str) -> Any: 199 | try: 200 | return self._items[name] 201 | except KeyError: 202 | raise AttributeError(name) 203 | 204 | def __setattr__(self, name: str, value: Any) -> None: 205 | raise AttributeError("can't set attribute") 206 | 207 | def __repr__(self) -> str: 208 | return "frozendict({0})".format( 209 | ", ".join("{0}={1!r}".format(key, value) for key, value in self._sorted_items()) 210 | ) 211 | 212 | 213 | # This class takes a similar approach to the cryptography project to deprecate attributes 214 | # in public modules. See the _ModuleWithDeprecation class here: 215 | # https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 216 | class _UtilDeprecationModule: 217 | """ 218 | Internal class delegating to a module, and displaying warnings when attributes 219 | related to the deprecated "abstractclassmethod" attributes in the josepy.util module. 220 | """ 221 | 222 | def __init__(self, module: ModuleType) -> None: 223 | self.__dict__["_module"] = module 224 | 225 | def __getattr__(self, attr: str) -> Any: 226 | if attr == "abstractclassmethod": 227 | warnings.warn( 228 | "The abstractclassmethod attribute in josepy.util is deprecated and will " 229 | "be removed soon. Please use the built-in decorators @classmethod and " 230 | "@abc.abstractmethod together instead.", 231 | DeprecationWarning, 232 | stacklevel=2, 233 | ) 234 | return getattr(self._module, attr) 235 | 236 | def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover 237 | setattr(self._module, attr, value) 238 | 239 | def __delattr__(self, attr: str) -> None: # pragma: no cover 240 | delattr(self._module, attr) 241 | 242 | def __dir__(self) -> List[str]: # pragma: no cover 243 | return ["_module"] + dir(self._module) 244 | 245 | 246 | # Patching ourselves to warn about deprecation and planned removal of some elements in the module. 247 | sys.modules[__name__] = cast(ModuleType, _UtilDeprecationModule(sys.modules[__name__])) 248 | -------------------------------------------------------------------------------- /tests/b64_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.b64.""" 2 | 3 | import sys 4 | from typing import Union 5 | 6 | import pytest 7 | 8 | # https://en.wikipedia.org/wiki/Base64#Examples 9 | B64_PADDING_EXAMPLES = { 10 | b"any carnal pleasure.": (b"YW55IGNhcm5hbCBwbGVhc3VyZS4", b"="), 11 | b"any carnal pleasure": (b"YW55IGNhcm5hbCBwbGVhc3VyZQ", b"=="), 12 | b"any carnal pleasur": (b"YW55IGNhcm5hbCBwbGVhc3Vy", b""), 13 | b"any carnal pleasu": (b"YW55IGNhcm5hbCBwbGVhc3U", b"="), 14 | b"any carnal pleas": (b"YW55IGNhcm5hbCBwbGVhcw", b"=="), 15 | } 16 | 17 | 18 | B64_URL_UNSAFE_EXAMPLES = { 19 | bytes((251, 239)): b"--8", 20 | bytes((255,)) * 2: b"__8", 21 | } 22 | 23 | 24 | class B64EncodeTest: 25 | """Tests for josepy.b64.b64encode.""" 26 | 27 | @classmethod 28 | def _call(cls, data: bytes) -> bytes: 29 | from josepy.b64 import b64encode 30 | 31 | return b64encode(data) 32 | 33 | def test_empty(self) -> None: 34 | assert self._call(b"") == b"" 35 | 36 | def test_unsafe_url(self) -> None: 37 | for text, b64 in B64_URL_UNSAFE_EXAMPLES.items(): 38 | assert self._call(text) == b64 39 | 40 | def test_different_paddings(self) -> None: 41 | for text, (b64, _) in B64_PADDING_EXAMPLES.items(): 42 | assert self._call(text) == b64 43 | 44 | def test_unicode_fails_with_type_error(self) -> None: 45 | with pytest.raises(TypeError): 46 | # We're purposefully testing with the incorrect type here. 47 | self._call("some unicode") # type: ignore 48 | 49 | 50 | class B64DecodeTest: 51 | """Tests for josepy.b64.b64decode.""" 52 | 53 | @classmethod 54 | def _call(cls, data: Union[bytes, str]) -> bytes: 55 | from josepy.b64 import b64decode 56 | 57 | return b64decode(data) 58 | 59 | def test_unsafe_url(self) -> None: 60 | for text, b64 in B64_URL_UNSAFE_EXAMPLES.items(): 61 | assert self._call(b64) == text 62 | 63 | def test_input_without_padding(self) -> None: 64 | for text, (b64, _) in B64_PADDING_EXAMPLES.items(): 65 | assert self._call(b64) == text 66 | 67 | def test_input_with_padding(self) -> None: 68 | for text, (b64, pad) in B64_PADDING_EXAMPLES.items(): 69 | assert self._call(b64 + pad) == text 70 | 71 | def test_unicode_with_ascii(self) -> None: 72 | assert self._call("YQ") == b"a" 73 | 74 | def test_non_ascii_unicode_fails(self) -> None: 75 | with pytest.raises(ValueError): 76 | self._call("\u0105") 77 | 78 | def test_type_error_no_unicode_or_bytes(self) -> None: 79 | with pytest.raises(TypeError): 80 | # We're purposefully testing with the incorrect type here. 81 | self._call(object()) # type: ignore 82 | 83 | 84 | if __name__ == "__main__": 85 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 86 | -------------------------------------------------------------------------------- /tests/errors_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.errors.""" 2 | 3 | import sys 4 | import unittest 5 | 6 | import pytest 7 | 8 | 9 | class UnrecognizedTypeErrorTest(unittest.TestCase): 10 | def setUp(self) -> None: 11 | from josepy.errors import UnrecognizedTypeError 12 | 13 | self.error = UnrecognizedTypeError("foo", {"type": "foo"}) 14 | 15 | def test_str(self) -> None: 16 | assert "foo was not recognized, full message: {'type': 'foo'}" == str(self.error) 17 | 18 | 19 | if __name__ == "__main__": 20 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 21 | -------------------------------------------------------------------------------- /tests/interfaces_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.interfaces.""" 2 | 3 | import sys 4 | import unittest 5 | from typing import Any, Dict, List 6 | 7 | import pytest 8 | 9 | 10 | class JSONDeSerializableTest(unittest.TestCase): 11 | def setUp(self) -> None: 12 | from josepy.interfaces import JSONDeSerializable 13 | 14 | class Basic(JSONDeSerializable): 15 | def __init__(self, v: Any) -> None: 16 | self.v = v 17 | 18 | def to_partial_json(self) -> Any: 19 | return self.v 20 | 21 | @classmethod 22 | def from_json(cls, jobj: Any) -> "Basic": 23 | return cls(jobj) 24 | 25 | class Sequence(JSONDeSerializable): 26 | def __init__(self, x: Basic, y: Basic) -> None: 27 | self.x = x 28 | self.y = y 29 | 30 | def to_partial_json(self) -> List[Basic]: 31 | return [self.x, self.y] 32 | 33 | @classmethod 34 | def from_json(cls, jobj: List[Any]) -> "Sequence": 35 | return cls(Basic.from_json(jobj[0]), Basic.from_json(jobj[1])) 36 | 37 | class Mapping(JSONDeSerializable): 38 | def __init__(self, x: Any, y: Any) -> None: 39 | self.x = x 40 | self.y = y 41 | 42 | def to_partial_json(self) -> Dict[Basic, Basic]: 43 | return {self.x: self.y} 44 | 45 | @classmethod 46 | def from_json(cls, jobj: Any) -> "Mapping": 47 | return cls("dummy", "values") # pragma: no cover 48 | 49 | self.basic1 = Basic("foo1") 50 | self.basic2 = Basic("foo2") 51 | self.seq = Sequence(self.basic1, self.basic2) 52 | self.mapping = Mapping(self.basic1, self.basic2) 53 | self.nested = Basic([[self.basic1]]) 54 | self.tuple = Basic(("foo",)) 55 | 56 | self.Basic = Basic 57 | self.Sequence = Sequence 58 | self.Mapping = Mapping 59 | 60 | def test_to_json_sequence(self) -> None: 61 | assert self.seq.to_json() == ["foo1", "foo2"] 62 | 63 | def test_to_json_mapping(self) -> None: 64 | assert self.mapping.to_json() == {"foo1": "foo2"} 65 | 66 | def test_to_json_other(self) -> None: 67 | mock_value = object() 68 | assert self.Basic(mock_value).to_json() is mock_value 69 | 70 | def test_to_json_nested(self) -> None: 71 | assert self.nested.to_json() == [["foo1"]] 72 | 73 | def test_to_json(self) -> None: 74 | assert self.tuple.to_json() == (("foo",)) 75 | 76 | def test_from_json_not_implemented(self) -> None: 77 | from josepy.interfaces import JSONDeSerializable 78 | 79 | with pytest.raises(TypeError): 80 | JSONDeSerializable.from_json("xxx") 81 | 82 | def test_json_loads(self) -> None: 83 | seq = self.Sequence.json_loads('["foo1", "foo2"]') 84 | assert isinstance(seq, self.Sequence) 85 | assert isinstance(seq.x, self.Basic) 86 | assert isinstance(seq.y, self.Basic) 87 | assert seq.x.v == "foo1" 88 | assert seq.y.v == "foo2" 89 | 90 | def test_json_dumps(self) -> None: 91 | assert '["foo1", "foo2"]' == self.seq.json_dumps() 92 | 93 | def test_json_dumps_pretty(self) -> None: 94 | assert self.seq.json_dumps_pretty() == '[\n "foo1",\n "foo2"\n]' 95 | 96 | def test_json_dump_default(self) -> None: 97 | from josepy.interfaces import JSONDeSerializable 98 | 99 | assert "foo1" == JSONDeSerializable.json_dump_default(self.basic1) 100 | 101 | jobj = JSONDeSerializable.json_dump_default(self.seq) 102 | assert len(jobj) == 2 103 | assert jobj[0] is self.basic1 104 | assert jobj[1] is self.basic2 105 | 106 | def test_json_dump_default_type_error(self) -> None: 107 | from josepy.interfaces import JSONDeSerializable 108 | 109 | with pytest.raises(TypeError): 110 | # We're purposefully testing with the incorrect type here. 111 | JSONDeSerializable.json_dump_default(object()) # type: ignore 112 | 113 | 114 | if __name__ == "__main__": 115 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 116 | -------------------------------------------------------------------------------- /tests/json_util_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.json_util.""" 2 | 3 | import itertools 4 | import sys 5 | import unittest 6 | from typing import Any, Dict, Mapping 7 | from unittest import mock 8 | 9 | import pytest 10 | import test_util 11 | from cryptography import x509 12 | 13 | from josepy import errors, interfaces, util 14 | 15 | CERT = test_util.load_cert("cert.pem") 16 | CSR = test_util.load_csr("csr.pem") 17 | 18 | 19 | class FieldTest(unittest.TestCase): 20 | """Tests for josepy.json_util.field and josepy.json_util.Field.""" 21 | 22 | def test_field_function(self) -> None: 23 | from josepy.json_util import Field, field 24 | 25 | test = field("foo", default="bar") 26 | assert isinstance(test, Field) 27 | assert test.json_name == "foo" 28 | assert test.default == "bar" 29 | 30 | def test_type_field_control(self) -> None: 31 | from josepy.json_util import JSONObjectWithFields, field 32 | 33 | class DummyProperlyTyped(JSONObjectWithFields): 34 | type: str = field("type") 35 | index: int = field("index") 36 | 37 | with pytest.raises(ValueError): 38 | 39 | class DummyImproperlyTyped(JSONObjectWithFields): 40 | type = field("type") 41 | index: int = field("index") 42 | 43 | def test_no_omit_boolean(self) -> None: 44 | from josepy.json_util import Field 45 | 46 | for default, omitempty, value in itertools.product( 47 | [True, False], [True, False], [True, False] 48 | ): 49 | assert Field("foo", default=default, omitempty=omitempty).omit(value) is False 50 | 51 | def test_descriptors(self) -> None: 52 | mock_value = mock.MagicMock() 53 | 54 | def decoder(unused_value: Any) -> str: 55 | return "d" 56 | 57 | def encoder(unused_value: Any) -> str: 58 | return "e" 59 | 60 | from josepy.json_util import Field 61 | 62 | field = Field("foo") 63 | 64 | field = field.encoder(encoder) 65 | assert "e" == field.encode(mock_value) 66 | 67 | field = field.decoder(decoder) 68 | assert "e" == field.encode(mock_value) 69 | assert "d" == field.decode(mock_value) 70 | 71 | def test_default_encoder_is_partial(self) -> None: 72 | class MockField(interfaces.JSONDeSerializable): 73 | def to_partial_json(self) -> Dict[str, Any]: 74 | return {"foo": "bar"} # pragma: no cover 75 | 76 | @classmethod 77 | def from_json(cls, jobj: Mapping[str, Any]) -> "MockField": 78 | return cls() # pragma: no cover 79 | 80 | mock_field = MockField() 81 | 82 | from josepy.json_util import Field 83 | 84 | assert Field.default_encoder(mock_field) is mock_field 85 | # in particular... 86 | assert "foo" != Field.default_encoder(mock_field) 87 | 88 | def test_default_encoder_passthrough(self) -> None: 89 | mock_value = mock.MagicMock() 90 | from josepy.json_util import Field 91 | 92 | assert Field.default_encoder(mock_value) is mock_value 93 | 94 | def test_default_decoder_list_to_tuple(self) -> None: 95 | from josepy.json_util import Field 96 | 97 | assert (1, 2, 3) == Field.default_decoder([1, 2, 3]) 98 | 99 | def test_default_decoder_dict_to_frozendict(self) -> None: 100 | from josepy.json_util import Field 101 | 102 | obj = Field.default_decoder({"x": 2}) 103 | assert isinstance(obj, util.frozendict) 104 | assert obj == util.frozendict(x=2) 105 | 106 | def test_default_decoder_passthrough(self) -> None: 107 | mock_value = mock.MagicMock() 108 | from josepy.json_util import Field 109 | 110 | assert Field.default_decoder(mock_value) is mock_value 111 | 112 | 113 | class JSONObjectWithFieldsMetaTest(unittest.TestCase): 114 | """Tests for josepy.json_util.JSONObjectWithFieldsMeta.""" 115 | 116 | def setUp(self) -> None: 117 | from josepy.json_util import Field, JSONObjectWithFieldsMeta 118 | 119 | self.field = Field("Baz") 120 | self.field2 = Field("Baz2") 121 | 122 | class A(metaclass=JSONObjectWithFieldsMeta): 123 | __slots__ = ("bar",) 124 | baz = self.field 125 | 126 | class B(A): 127 | pass 128 | 129 | class C(A): 130 | baz = self.field2 131 | 132 | self.a_cls = A 133 | self.b_cls = B 134 | self.c_cls = C 135 | 136 | def test_fields(self) -> None: 137 | assert {"baz": self.field} == self.a_cls._fields 138 | assert {"baz": self.field} == self.b_cls._fields 139 | 140 | def test_fields_inheritance(self) -> None: 141 | assert {"baz": self.field2} == self.c_cls._fields 142 | 143 | def test_slots(self) -> None: 144 | assert ("bar", "baz") == self.a_cls.__slots__ 145 | assert ("baz",) == self.b_cls.__slots__ 146 | 147 | def test_orig_slots(self) -> None: 148 | assert ("bar",) == self.a_cls._orig_slots 149 | assert () == self.b_cls._orig_slots 150 | 151 | 152 | class JSONObjectWithFieldsTest(unittest.TestCase): 153 | """Tests for josepy.json_util.JSONObjectWithFields.""" 154 | 155 | def setUp(self) -> None: 156 | from josepy.json_util import Field, JSONObjectWithFields 157 | 158 | class MockJSONObjectWithFields(JSONObjectWithFields): 159 | x = Field("x", omitempty=True, encoder=(lambda x: x * 2), decoder=(lambda x: x / 2)) 160 | y = Field("y") 161 | z = Field("Z") # on purpose uppercase 162 | 163 | @y.encoder # type: ignore 164 | def y(value): 165 | if value == 500: 166 | raise errors.SerializationError() 167 | return value 168 | 169 | @y.decoder # type: ignore 170 | def y(value): 171 | if value == 500: 172 | raise errors.DeserializationError() 173 | return value 174 | 175 | self.MockJSONObjectWithFields = MockJSONObjectWithFields 176 | self.mock = MockJSONObjectWithFields(x=None, y=2, z=3) 177 | 178 | def test_init_defaults(self) -> None: 179 | assert self.mock == self.MockJSONObjectWithFields(y=2, z=3) 180 | 181 | def test_encode(self) -> None: 182 | assert 10 == self.MockJSONObjectWithFields(x=5, y=0, z=0).encode("x") 183 | 184 | def test_encode_wrong_field(self) -> None: 185 | with pytest.raises(errors.Error): 186 | self.mock.encode("foo") 187 | 188 | def test_encode_serialization_error_passthrough(self) -> None: 189 | with pytest.raises(errors.SerializationError): 190 | self.MockJSONObjectWithFields(y=500, z=None).encode("y") 191 | 192 | def test_fields_to_partial_json_omits_empty(self) -> None: 193 | assert self.mock.fields_to_partial_json() == {"y": 2, "Z": 3} 194 | 195 | def test_fields_from_json_fills_default_for_empty(self) -> None: 196 | assert {"x": None, "y": 2, "z": 3} == self.MockJSONObjectWithFields.fields_from_json( 197 | {"y": 2, "Z": 3} 198 | ) 199 | 200 | def test_fields_from_json_fails_on_missing(self) -> None: 201 | with pytest.raises(errors.DeserializationError): 202 | self.MockJSONObjectWithFields.fields_from_json({"y": 0}) 203 | with pytest.raises(errors.DeserializationError): 204 | self.MockJSONObjectWithFields.fields_from_json({"Z": 0}) 205 | with pytest.raises(errors.DeserializationError): 206 | self.MockJSONObjectWithFields.fields_from_json({"x": 0, "y": 0}) 207 | with pytest.raises(errors.DeserializationError): 208 | self.MockJSONObjectWithFields.fields_from_json({"x": 0, "Z": 0}) 209 | 210 | def test_fields_to_partial_json_encoder(self) -> None: 211 | assert self.MockJSONObjectWithFields(x=1, y=2, z=3).to_partial_json() == { 212 | "x": 2, 213 | "y": 2, 214 | "Z": 3, 215 | } 216 | 217 | def test_fields_from_json_decoder(self) -> None: 218 | assert {"x": 2, "y": 2, "z": 3} == self.MockJSONObjectWithFields.fields_from_json( 219 | {"x": 4, "y": 2, "Z": 3} 220 | ) 221 | 222 | def test_fields_to_partial_json_error_passthrough(self) -> None: 223 | with pytest.raises(errors.SerializationError): 224 | self.MockJSONObjectWithFields(x=1, y=500, z=3).to_partial_json() 225 | 226 | def test_fields_from_json_error_passthrough(self) -> None: 227 | with pytest.raises(errors.DeserializationError): 228 | self.MockJSONObjectWithFields.from_json({"x": 4, "y": 500, "Z": 3}) 229 | 230 | 231 | class DeEncodersTest(unittest.TestCase): 232 | def setUp(self) -> None: 233 | self.b64_cert = ( 234 | "MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhM" 235 | "CVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKz" 236 | "ApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxF" 237 | "DASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIx" 238 | "ODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRI" 239 | "wEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTW" 240 | "ljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwD" 241 | "QYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1" 242 | "AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwE" 243 | "AATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMnd" 244 | "fk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o" 245 | ) 246 | self.b64_csr = ( 247 | "MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2F" 248 | "uMRIwEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECw" 249 | "wWVW5pdmVyc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb" 250 | "20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD_N_HW9As" 251 | "dRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3" 252 | "C5QIDAQABoCkwJwYJKoZIhvcNAQkOMRowGDAWBgNVHREEDzANggtleGFtcG" 253 | "xlLmNvbTANBgkqhkiG9w0BAQsFAANBAHJH_O6BtC9aGzEVCMGOZ7z9iIRHW" 254 | "Szr9x_bOzn7hLwsbXPAgO1QxEwL-X-4g20Gn9XBE1N9W6HCIEut2d8wACg" 255 | ) 256 | 257 | def test_encode_b64jose(self) -> None: 258 | from josepy.json_util import encode_b64jose 259 | 260 | encoded = encode_b64jose(b"x") 261 | assert isinstance(encoded, str) 262 | assert "eA" == encoded 263 | 264 | def test_decode_b64jose(self) -> None: 265 | from josepy.json_util import decode_b64jose 266 | 267 | decoded = decode_b64jose("eA") 268 | assert isinstance(decoded, bytes) 269 | assert b"x" == decoded 270 | 271 | def test_decode_b64jose_padding_error(self) -> None: 272 | from josepy.json_util import decode_b64jose 273 | 274 | with pytest.raises(errors.DeserializationError): 275 | decode_b64jose("x") 276 | 277 | def test_decode_b64jose_size(self) -> None: 278 | from josepy.json_util import decode_b64jose 279 | 280 | assert b"foo" == decode_b64jose("Zm9v", size=3) 281 | with pytest.raises(errors.DeserializationError): 282 | decode_b64jose("Zm9v", size=2) 283 | with pytest.raises(errors.DeserializationError): 284 | decode_b64jose("Zm9v", size=4) 285 | 286 | def test_decode_b64jose_minimum_size(self) -> None: 287 | from josepy.json_util import decode_b64jose 288 | 289 | assert b"foo" == decode_b64jose("Zm9v", size=3, minimum=True) 290 | assert b"foo" == decode_b64jose("Zm9v", size=2, minimum=True) 291 | with pytest.raises(errors.DeserializationError): 292 | decode_b64jose("Zm9v", size=4, minimum=True) 293 | 294 | def test_encode_hex16(self) -> None: 295 | from josepy.json_util import encode_hex16 296 | 297 | encoded = encode_hex16(b"foo") 298 | assert "666f6f" == encoded 299 | assert isinstance(encoded, str) 300 | 301 | def test_decode_hex16(self) -> None: 302 | from josepy.json_util import decode_hex16 303 | 304 | decoded = decode_hex16("666f6f") 305 | assert b"foo" == decoded 306 | assert isinstance(decoded, bytes) 307 | 308 | def test_decode_hex16_minimum_size(self) -> None: 309 | from josepy.json_util import decode_hex16 310 | 311 | assert b"foo" == decode_hex16("666f6f", size=3, minimum=True) 312 | assert b"foo" == decode_hex16("666f6f", size=2, minimum=True) 313 | with pytest.raises(errors.DeserializationError): 314 | decode_hex16("666f6f", size=4, minimum=True) 315 | 316 | def test_decode_hex16_odd_length(self) -> None: 317 | from josepy.json_util import decode_hex16 318 | 319 | with pytest.raises(errors.DeserializationError): 320 | decode_hex16("x") 321 | 322 | def test_encode_cert(self) -> None: 323 | from josepy.json_util import encode_cert 324 | 325 | assert self.b64_cert == encode_cert(CERT) 326 | 327 | def test_decode_cert(self) -> None: 328 | from josepy.json_util import decode_cert 329 | 330 | cert = decode_cert(self.b64_cert) 331 | assert isinstance(cert, x509.Certificate) 332 | assert cert == CERT 333 | with pytest.raises(errors.DeserializationError): 334 | decode_cert("") 335 | 336 | def test_encode_csr(self) -> None: 337 | from josepy.json_util import encode_csr 338 | 339 | assert self.b64_csr == encode_csr(CSR) 340 | 341 | def test_decode_csr(self) -> None: 342 | from josepy.json_util import decode_csr 343 | 344 | csr = decode_csr(self.b64_csr) 345 | assert isinstance(csr, x509.CertificateSigningRequest) 346 | assert csr == CSR 347 | with pytest.raises(errors.DeserializationError): 348 | decode_csr("") 349 | 350 | 351 | class TypedJSONObjectWithFieldsTest(unittest.TestCase): 352 | def setUp(self) -> None: 353 | from josepy.json_util import TypedJSONObjectWithFields 354 | 355 | class MockParentTypedJSONObjectWithFields(TypedJSONObjectWithFields): 356 | TYPES = {} 357 | type_field_name = "type" 358 | 359 | @MockParentTypedJSONObjectWithFields.register 360 | class MockTypedJSONObjectWithFields(MockParentTypedJSONObjectWithFields): 361 | foo: str 362 | typ = "test" 363 | __slots__ = ("foo",) 364 | 365 | @classmethod 366 | def fields_from_json(cls, jobj: Mapping[str, Any]) -> Dict[str, Any]: 367 | return {"foo": jobj["foo"]} 368 | 369 | def fields_to_partial_json(self) -> Any: 370 | return {"foo": self.foo} 371 | 372 | self.parent_cls = MockParentTypedJSONObjectWithFields 373 | self.msg = MockTypedJSONObjectWithFields(foo="bar") 374 | 375 | def test_to_partial_json(self) -> None: 376 | assert self.msg.to_partial_json() == { 377 | "type": "test", 378 | "foo": "bar", 379 | } 380 | 381 | def test_from_json_non_dict_fails(self) -> None: 382 | for value in [[], (), 5, "asd"]: # all possible input types 383 | with pytest.raises(errors.DeserializationError): 384 | # We're purposefully testing with the incorrect type here. 385 | self.parent_cls.from_json(value) # type: ignore 386 | 387 | def test_from_json_dict_no_type_fails(self) -> None: 388 | with pytest.raises(errors.DeserializationError): 389 | self.parent_cls.from_json({}) 390 | 391 | def test_from_json_unknown_type_fails(self) -> None: 392 | with pytest.raises(errors.UnrecognizedTypeError): 393 | self.parent_cls.from_json({"type": "bar"}) 394 | 395 | def test_from_json_returns_obj(self) -> None: 396 | assert {"foo": "bar"} == self.parent_cls.from_json({"type": "test", "foo": "bar"}) 397 | 398 | 399 | if __name__ == "__main__": 400 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 401 | -------------------------------------------------------------------------------- /tests/jwa_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.jwa.""" 2 | 3 | import sys 4 | import unittest 5 | from typing import Any 6 | from unittest import mock 7 | 8 | import pytest 9 | import test_util 10 | 11 | from josepy import errors 12 | 13 | RSA256_KEY = test_util.load_rsa_private_key("rsa256_key.pem") 14 | RSA512_KEY = test_util.load_rsa_private_key("rsa512_key.pem") 15 | RSA1024_KEY = test_util.load_rsa_private_key("rsa1024_key.pem") 16 | EC_P256_KEY = test_util.load_ec_private_key("ec_p256_key.pem") 17 | EC_P384_KEY = test_util.load_ec_private_key("ec_p384_key.pem") 18 | EC_P521_KEY = test_util.load_ec_private_key("ec_p521_key.pem") 19 | 20 | 21 | class JWASignatureTest(unittest.TestCase): 22 | """Tests for josepy.jwa.JWASignature.""" 23 | 24 | def setUp(self) -> None: 25 | from josepy.jwa import JWASignature 26 | 27 | class MockSig(JWASignature): 28 | def sign(self, key: Any, msg: bytes) -> bytes: 29 | raise NotImplementedError() # pragma: no cover 30 | 31 | def verify(self, key: Any, msg: bytes, sig: bytes) -> bool: 32 | raise NotImplementedError() # pragma: no cover 33 | 34 | self.Sig1 = MockSig("Sig1") 35 | self.Sig2 = MockSig("Sig2") 36 | 37 | def test_eq(self) -> None: 38 | assert self.Sig1 == self.Sig1 39 | 40 | def test_ne(self) -> None: 41 | assert self.Sig1 != self.Sig2 42 | 43 | def test_ne_other_type(self) -> None: 44 | assert self.Sig1 != 5 45 | 46 | def test_repr(self) -> None: 47 | assert "Sig1" == repr(self.Sig1) 48 | assert "Sig2" == repr(self.Sig2) 49 | 50 | def test_to_partial_json(self) -> None: 51 | assert self.Sig1.to_partial_json() == "Sig1" 52 | assert self.Sig2.to_partial_json() == "Sig2" 53 | 54 | def test_from_json(self) -> None: 55 | from josepy.jwa import RS256, JWASignature 56 | 57 | assert JWASignature.from_json("RS256") is RS256 58 | 59 | 60 | class JWAHSTest(unittest.TestCase): 61 | def test_it(self) -> None: 62 | from josepy.jwa import HS256 63 | 64 | sig = ( 65 | b"\xceR\xea\xcd\x94\xab\xcf\xfb\xe0\xacA.:\x1a'\x08i\xe2\xc4" 66 | b"\r\x85+\x0e\x85\xaeUZ\xd4\xb3\x97zO" 67 | ) 68 | assert HS256.sign(b"some key", b"foo") == sig 69 | assert HS256.verify(b"some key", b"foo", sig) is True 70 | assert HS256.verify(b"some key", b"foo", sig + b"!") is False 71 | 72 | 73 | class JWARSTest(unittest.TestCase): 74 | def test_sign_no_private_part(self) -> None: 75 | from josepy.jwa import RS256 76 | 77 | with pytest.raises(errors.Error): 78 | RS256.sign(RSA512_KEY.public_key(), b"foo") 79 | 80 | def test_sign_key_too_small(self) -> None: 81 | from josepy.jwa import PS256, RS256 82 | 83 | with pytest.raises(errors.Error): 84 | RS256.sign(RSA256_KEY, b"foo") 85 | with pytest.raises(errors.Error): 86 | PS256.sign(RSA256_KEY, b"foo") 87 | 88 | def test_rs(self) -> None: 89 | from josepy.jwa import RS256 90 | 91 | sig = ( 92 | b"|\xc6\xb2\xa4\xab(\x87\x99\xfa*:\xea\xf8\xa0N&}\x9f\x0f\xc0O" 93 | b'\xc6t\xa3\xe6\xfa\xbb"\x15Y\x80Y\xe0\x81\xb8\x88)\xba\x0c\x9c' 94 | b"\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99" 95 | b"\xd2\xb9.>}\xfd" 96 | ) 97 | assert RS256.sign(RSA512_KEY, b"foo") == sig 98 | assert RS256.verify(RSA512_KEY.public_key(), b"foo", sig) is True 99 | assert RS256.verify(RSA512_KEY.public_key(), b"foo", sig + b"!") is False 100 | 101 | def test_ps(self) -> None: 102 | from josepy.jwa import PS256 103 | 104 | sig = PS256.sign(RSA1024_KEY, b"foo") 105 | assert PS256.verify(RSA1024_KEY.public_key(), b"foo", sig) is True 106 | assert PS256.verify(RSA1024_KEY.public_key(), b"foo", sig + b"!") is False 107 | 108 | def test_sign_new_api(self) -> None: 109 | from josepy.jwa import RS256 110 | 111 | key = mock.MagicMock() 112 | RS256.sign(key, b"message") 113 | assert key.sign.called is True 114 | 115 | def test_verify_new_api(self) -> None: 116 | from josepy.jwa import RS256 117 | 118 | key = mock.MagicMock() 119 | RS256.verify(key, b"message", b"signature") 120 | assert key.verify.called is True 121 | 122 | 123 | class JWAECTest(unittest.TestCase): 124 | def test_sign_no_private_part(self) -> None: 125 | from josepy.jwa import ES256 126 | 127 | with pytest.raises(errors.Error): 128 | ES256.sign(EC_P256_KEY.public_key(), b"foo") 129 | 130 | def test_es256_sign_and_verify(self) -> None: 131 | from josepy.jwa import ES256 132 | 133 | message = b"foo" 134 | signature = ES256.sign(EC_P256_KEY, message) 135 | assert ES256.verify(EC_P256_KEY.public_key(), message, signature) is True 136 | 137 | def test_es384_sign_and_verify(self) -> None: 138 | from josepy.jwa import ES384 139 | 140 | message = b"foo" 141 | signature = ES384.sign(EC_P384_KEY, message) 142 | assert ES384.verify(EC_P384_KEY.public_key(), message, signature) is True 143 | 144 | def test_verify_with_wrong_jwa(self) -> None: 145 | from josepy.jwa import ES256, ES384 146 | 147 | message = b"foo" 148 | signature = ES256.sign(EC_P256_KEY, message) 149 | assert ES384.verify(EC_P384_KEY.public_key(), message, signature) is False 150 | 151 | def test_verify_with_different_key(self) -> None: 152 | from cryptography.hazmat.backends import default_backend 153 | from cryptography.hazmat.primitives.asymmetric import ec 154 | 155 | from josepy.jwa import ES256 156 | 157 | message = b"foo" 158 | signature = ES256.sign(EC_P256_KEY, message) 159 | different_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) 160 | assert ES256.verify(different_key.public_key(), message, signature) is False 161 | 162 | def test_sign_new_api(self) -> None: 163 | from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 164 | 165 | from josepy.jwa import ES256 166 | 167 | key = mock.MagicMock(curve=SECP256R1()) 168 | with mock.patch("josepy.jwa.decode_dss_signature") as decode_patch: 169 | decode_patch.return_value = (0, 0) 170 | ES256.sign(key, b"message") 171 | assert key.sign.called is True 172 | 173 | def test_verify_new_api(self) -> None: 174 | import math 175 | 176 | from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 177 | 178 | from josepy.jwa import ES256 179 | 180 | key = mock.MagicMock(key_size=256, curve=SECP256R1()) 181 | ES256.verify(key, b"message", b"\x00" * math.ceil(key.key_size / 8) * 2) 182 | assert key.verify.called is True 183 | 184 | def test_signature_size(self) -> None: 185 | from josepy.jwa import ES512 186 | from josepy.jwk import JWK 187 | 188 | key = JWK.from_json( 189 | { 190 | "d": ( 191 | "Af9KP6DqLRbtit6NS_LRIaCP_-NdC5l5R2ugbILdfpv6dS9R4wUPNxiGw" 192 | "-vVWumA56Yo1oBnEm8ZdR4W-u1lPHw5" 193 | ), 194 | "x": ( 195 | "AD4i4STyJ07iZJkHkpKEOuICpn6IHknzwAlrf-1w1a5dqOsRe30EECSN4vFxae" 196 | "AmtdBSCKBwCq7h1q4bPgMrMUvF" 197 | ), 198 | "y": ( 199 | "AHAlXxrabjcx_yBxGObnm_DkEQMJK1E69OHY3x3VxF5VXoKc93CG4GLoaPvphZQv" 200 | "Znt5EfExQoPktwOMIVhBHaFR" 201 | ), 202 | "crv": "P-521", 203 | "kty": "EC", 204 | } 205 | ) 206 | with mock.patch("josepy.jwa.decode_dss_signature") as decode_patch: 207 | decode_patch.return_value = (0, 0) 208 | assert isinstance(key, JWK) 209 | sig = ES512.sign(key.key, b"test") 210 | assert len(sig) == 2 * 66 211 | 212 | 213 | if __name__ == "__main__": 214 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 215 | -------------------------------------------------------------------------------- /tests/jwk_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.jwk.""" 2 | 3 | import binascii 4 | import sys 5 | import unittest 6 | from typing import TYPE_CHECKING 7 | 8 | import pytest 9 | import test_util 10 | 11 | from josepy import errors, json_util, util 12 | 13 | # The approach used here and below is based on 14 | # https://github.com/certbot/certbot/pull/8748. 15 | if TYPE_CHECKING: 16 | from typing_extensions import Protocol 17 | else: 18 | Protocol = object 19 | 20 | DSA_PEM = test_util.load_vector("dsa512_key.pem") 21 | RSA256_KEY = test_util.load_rsa_private_key("rsa256_key.pem") 22 | RSA512_KEY = test_util.load_rsa_private_key("rsa512_key.pem") 23 | EC_P256_KEY = test_util.load_ec_private_key("ec_p256_key.pem") 24 | EC_P384_KEY = test_util.load_ec_private_key("ec_p384_key.pem") 25 | EC_P521_KEY = test_util.load_ec_private_key("ec_p521_key.pem") 26 | 27 | 28 | class JWKTest(unittest.TestCase): 29 | """Tests for josepy.jwk.JWK.""" 30 | 31 | def test_load(self) -> None: 32 | from josepy.jwk import JWK 33 | 34 | with pytest.raises(errors.Error): 35 | JWK.load(DSA_PEM) 36 | 37 | def test_load_subclass_wrong_type(self) -> None: 38 | from josepy.jwk import JWKRSA 39 | 40 | with pytest.raises(errors.Error): 41 | JWKRSA.load(DSA_PEM) 42 | 43 | 44 | class JWKSubclassTest(Protocol): 45 | from josepy.jwk import JWK 46 | 47 | jwk: JWK 48 | thumbprint: bytes 49 | 50 | 51 | class JWKTestBaseMixin: 52 | """Mixin test for JWK subclass tests.""" 53 | 54 | thumbprint: bytes = NotImplemented 55 | 56 | def test_thumbprint_private(self: JWKSubclassTest) -> None: 57 | assert self.thumbprint == self.jwk.thumbprint() 58 | 59 | def test_thumbprint_public(self: JWKSubclassTest) -> None: 60 | assert self.thumbprint == self.jwk.public_key().thumbprint() 61 | 62 | 63 | class JWKOctTest(unittest.TestCase, JWKTestBaseMixin): 64 | """Tests for josepy.jwk.JWKOct.""" 65 | 66 | thumbprint = ( 67 | b"\xf3\xe7\xbe\xa8`\xd2\xdap\xe9}\x9c\xce>" 68 | b"\xd0\xfcI\xbe\xcd\x92'\xd4o\x0e\xf41\xea" 69 | b"\x8e(\x8a\xb2i\x1c" 70 | ) 71 | 72 | def setUp(self) -> None: 73 | from josepy.jwk import JWKOct 74 | 75 | self.jwk = JWKOct(key=b"foo") 76 | self.jobj = {"kty": "oct", "k": json_util.encode_b64jose(b"foo")} 77 | 78 | def test_to_partial_json(self) -> None: 79 | assert self.jwk.to_partial_json() == self.jobj 80 | 81 | def test_from_json(self) -> None: 82 | from josepy.jwk import JWKOct 83 | 84 | assert self.jwk == JWKOct.from_json(self.jobj) 85 | 86 | def test_from_json_hashable(self) -> None: 87 | from josepy.jwk import JWKOct 88 | 89 | hash(JWKOct.from_json(self.jobj)) 90 | 91 | def test_load(self) -> None: 92 | from josepy.jwk import JWKOct 93 | 94 | assert self.jwk == JWKOct.load(b"foo") 95 | 96 | def test_public_key(self) -> None: 97 | assert self.jwk.public_key() is self.jwk 98 | 99 | 100 | class JWKRSATest(unittest.TestCase, JWKTestBaseMixin): 101 | """Tests for josepy.jwk.JWKRSA.""" 102 | 103 | thumbprint = ( 104 | b"\x83K\xdc#3\x98\xca\x98\xed\xcb\x80\x80<\x0c" 105 | b"\xf0\x95\xb9H\xb2*l\xbd$\xe5&|O\x91\xd4 \xb0Y" 106 | ) 107 | 108 | def setUp(self) -> None: 109 | from josepy.jwk import JWKRSA 110 | 111 | self.jwk256 = JWKRSA(key=RSA256_KEY.public_key()) 112 | self.jwk256json = { 113 | "kty": "RSA", 114 | "e": "AQAB", 115 | "n": "m2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk", 116 | } 117 | self.jwk256_not_comparable = JWKRSA(key=RSA256_KEY.public_key()._wrapped) 118 | self.jwk512 = JWKRSA(key=RSA512_KEY.public_key()) 119 | self.jwk512json = { 120 | "kty": "RSA", 121 | "e": "AQAB", 122 | "n": "rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5" 123 | "80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q", 124 | } 125 | self.private = JWKRSA(key=RSA256_KEY) 126 | self.private_json_small = self.jwk256json.copy() 127 | self.private_json_small["d"] = "lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE" 128 | self.private_json = self.jwk256json.copy() 129 | self.private_json.update( 130 | { 131 | "d": "lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE", 132 | "p": "zUVNZn4lLLBD1R6NE8TKNQ", 133 | "q": "wcfKfc7kl5jfqXArCRSURQ", 134 | "dp": "CWJFq43QvT5Bm5iN8n1okQ", 135 | "dq": "bHh2u7etM8LKKCF2pY2UdQ", 136 | "qi": "oi45cEkbVoJjAbnQpFY87Q", 137 | } 138 | ) 139 | self.jwk = self.private 140 | 141 | def test_init_auto_comparable(self) -> None: 142 | assert isinstance(self.jwk256_not_comparable.key, util.ComparableRSAKey) 143 | assert self.jwk256 == self.jwk256_not_comparable 144 | 145 | def test_encode_param_zero(self) -> None: 146 | from josepy.jwk import JWKRSA 147 | 148 | # TODO: move encode/decode _param to separate class 149 | assert "AA" == JWKRSA._encode_param(0) 150 | 151 | def test_equals(self) -> None: 152 | assert self.jwk256 == self.jwk256 153 | assert self.jwk512 == self.jwk512 154 | 155 | def test_not_equals(self) -> None: 156 | assert self.jwk256 != self.jwk512 157 | assert self.jwk512 != self.jwk256 158 | 159 | def test_load(self) -> None: 160 | from josepy.jwk import JWKRSA 161 | 162 | assert self.private == JWKRSA.load(test_util.load_vector("rsa256_key.pem")) 163 | 164 | def test_public_key(self) -> None: 165 | assert self.jwk256 == self.private.public_key() 166 | 167 | def test_to_partial_json(self) -> None: 168 | assert self.jwk256.to_partial_json() == self.jwk256json 169 | assert self.jwk512.to_partial_json() == self.jwk512json 170 | assert self.private.to_partial_json() == self.private_json 171 | 172 | def test_from_json(self) -> None: 173 | from josepy.jwk import JWK 174 | 175 | assert self.jwk256 == JWK.from_json(self.jwk256json) 176 | assert self.jwk512 == JWK.from_json(self.jwk512json) 177 | assert self.private == JWK.from_json(self.private_json) 178 | 179 | def test_from_json_private_small(self) -> None: 180 | from josepy.jwk import JWK 181 | 182 | assert self.private == JWK.from_json(self.private_json_small) 183 | 184 | def test_from_json_missing_one_additional(self) -> None: 185 | from josepy.jwk import JWK 186 | 187 | del self.private_json["q"] 188 | with pytest.raises(errors.Error): 189 | JWK.from_json(self.private_json) 190 | 191 | def test_from_json_hashable(self) -> None: 192 | from josepy.jwk import JWK 193 | 194 | hash(JWK.from_json(self.jwk256json)) 195 | 196 | def test_from_json_non_schema_errors(self) -> None: 197 | # valid against schema, but still failing 198 | from josepy.jwk import JWK 199 | 200 | with pytest.raises(errors.DeserializationError): 201 | JWK.from_json({"kty": "RSA", "e": "AQAB", "n": ""}) 202 | with pytest.raises(errors.DeserializationError): 203 | JWK.from_json({"kty": "RSA", "e": "AQAB", "n": "1"}) 204 | 205 | def test_thumbprint_go_jose(self) -> None: 206 | # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk.go#L155 207 | # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L331-L344 208 | # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L384 209 | from josepy.jwk import JWKRSA 210 | 211 | key = JWKRSA.json_loads( 212 | """{ 213 | "kty": "RSA", 214 | "kid": "bilbo.baggins@hobbiton.example", 215 | "use": "sig", 216 | "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", 217 | "e": "AQAB" 218 | }""" # noqa 219 | ) 220 | assert ( 221 | binascii.hexlify(key.thumbprint()) 222 | == b"f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932" 223 | ) 224 | 225 | 226 | class JWKECTest(unittest.TestCase, JWKTestBaseMixin): 227 | """Tests for josepy.jwk.JWKEC.""" 228 | 229 | # pylint: disable=too-many-instance-attributes 230 | 231 | thumbprint = ( 232 | b"\x06\xceL\x1b\xa8\x8d\x86\x1flF\x99J\x8b\xe0$\t\xbbj" 233 | b"\xd8\xf6O\x1ed\xdeR\x8f\x97\xff\xf6\xa2\x86\xd3" 234 | ) 235 | 236 | def setUp(self) -> None: 237 | from josepy.jwk import JWKEC 238 | 239 | self.jwk256 = JWKEC(key=EC_P256_KEY.public_key()) 240 | self.jwk384 = JWKEC(key=EC_P384_KEY.public_key()) 241 | self.jwk521 = JWKEC(key=EC_P521_KEY.public_key()) 242 | self.jwk256_not_comparable = JWKEC(key=EC_P256_KEY.public_key()._wrapped) 243 | self.jwk256json = { 244 | "kty": "EC", 245 | "crv": "P-256", 246 | "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", 247 | "y": "EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc", 248 | } 249 | self.jwk384json = { 250 | "kty": "EC", 251 | "crv": "P-384", 252 | "x": "tIhpNtEXkadUbrY84rYGgApFM1X_3l3EWQRuOP1IWtxlTftrZQwneJZF0k0eRn00", 253 | "y": "KW2Gp-TThDXmZ-9MJPnD8hv-X130SVvfZRl1a04HPVwIbvLe87mvA_iuOa-myUyv", 254 | } 255 | self.jwk521json = { 256 | "kty": "EC", 257 | "crv": "P-521", 258 | "x": "AFkdl6cKzBmP18U8fffpP4IZN2eED45hDcwRPl5ZeClwHcLtnMBMuWYFFO_Nzm6DL2MhpN0zI2bcMLJd95aY2tPs", # noqa 259 | "y": "AYvZq3wByjt7nQd8nYMqhFNCL3j_-U6GPWZet1hYBY_XZHrC4yIV0R4JnssRAY9eqc1EElpCc4hziis1jiV1iR4W", # noqa 260 | } 261 | self.private = JWKEC(key=EC_P256_KEY) 262 | self.private_json = { 263 | "d": "xReNQBKqqTthG8oTmBdhp4EQYImSK1dVqfa2yyMn2rc", 264 | "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", 265 | "y": "EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc", 266 | "crv": "P-256", 267 | "kty": "EC", 268 | } 269 | self.jwk = self.private 270 | 271 | def test_init_auto_comparable(self) -> None: 272 | assert isinstance(self.jwk256_not_comparable.key, util.ComparableECKey) 273 | assert self.jwk256 == self.jwk256_not_comparable 274 | 275 | def test_encode_param_zero(self) -> None: 276 | from josepy.jwk import JWKEC 277 | 278 | # pylint: disable=protected-access 279 | # TODO: move encode/decode _param to separate class 280 | assert "AA" == JWKEC._encode_param(0, 1) 281 | 282 | def test_equals(self) -> None: 283 | assert self.jwk256 == self.jwk256 284 | assert self.jwk384 == self.jwk384 285 | assert self.jwk521 == self.jwk521 286 | 287 | def test_not_equals(self) -> None: 288 | assert self.jwk256 != self.jwk384 289 | assert self.jwk256 != self.jwk521 290 | assert self.jwk384 != self.jwk256 291 | assert self.jwk384 != self.jwk521 292 | assert self.jwk521 != self.jwk256 293 | assert self.jwk521 != self.jwk384 294 | 295 | def test_load(self) -> None: 296 | from josepy.jwk import JWKEC 297 | 298 | assert self.private == JWKEC.load(test_util.load_vector("ec_p256_key.pem")) 299 | 300 | def test_public_key(self) -> None: 301 | assert self.jwk256 == self.private.public_key() 302 | 303 | def test_to_partial_json(self) -> None: 304 | assert self.jwk256.to_partial_json() == self.jwk256json 305 | assert self.jwk384.to_partial_json() == self.jwk384json 306 | assert self.jwk521.to_partial_json() == self.jwk521json 307 | assert self.private.to_partial_json() == self.private_json 308 | 309 | def test_from_json(self) -> None: 310 | from josepy.jwk import JWK 311 | 312 | assert self.jwk256 == JWK.from_json(self.jwk256json) 313 | assert self.jwk384 == JWK.from_json(self.jwk384json) 314 | assert self.jwk521 == JWK.from_json(self.jwk521json) 315 | assert self.private == JWK.from_json(self.private_json) 316 | 317 | def test_from_json_missing_x_coordinate(self) -> None: 318 | from josepy.jwk import JWK 319 | 320 | del self.private_json["x"] 321 | with pytest.raises(KeyError): 322 | JWK.from_json(self.private_json) 323 | 324 | def test_from_json_missing_y_coordinate(self) -> None: 325 | from josepy.jwk import JWK 326 | 327 | del self.private_json["y"] 328 | with pytest.raises(KeyError): 329 | JWK.from_json(self.private_json) 330 | 331 | def test_from_json_hashable(self) -> None: 332 | from josepy.jwk import JWK 333 | 334 | hash(JWK.from_json(self.jwk256json)) 335 | 336 | def test_from_json_non_schema_errors(self) -> None: 337 | # valid against schema, but still failing 338 | from josepy.jwk import JWK 339 | 340 | with pytest.raises(errors.DeserializationError): 341 | JWK.from_json( 342 | { 343 | "kty": "EC", 344 | "crv": "P-256", 345 | "x": "AQAB", 346 | "y": "m2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk", 347 | } 348 | ) 349 | with pytest.raises(errors.DeserializationError): 350 | JWK.from_json( 351 | { 352 | "kty": "EC", 353 | "crv": "P-256", 354 | "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", 355 | "y": "1", 356 | } 357 | ) 358 | 359 | def test_unknown_crv_name(self) -> None: 360 | from josepy.jwk import JWK 361 | 362 | with pytest.raises(errors.DeserializationError): 363 | JWK.from_json( 364 | { 365 | "kty": "EC", 366 | "crv": "P-255", 367 | "x": "jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U", 368 | "y": "EPAw8_8z7PYKsHH6hlGSlsWxFoFl7-0vM0QRGbmnvCc", 369 | } 370 | ) 371 | 372 | def test_encode_y_leading_zero_p256(self) -> None: 373 | import josepy 374 | from josepy.jwk import JWK, JWKEC 375 | 376 | data = b"""-----BEGIN EC PRIVATE KEY----- 377 | MHcCAQEEICZ7LCI99Na2KZ/Fq8JmJROakGJ5+J7rHiGSPoO36kOAoAoGCCqGSM49 378 | AwEHoUQDQgAEGS5RvStca15z2FEanCM3juoX7tE/LB7iD44GWawGE40APAl/iZuH 379 | 31wQfst4glTZpxkpEI/MzNZHjiYnqrGeSw== 380 | -----END EC PRIVATE KEY-----""" 381 | key = JWKEC.load(data) 382 | json = key.to_partial_json() 383 | y = josepy.json_util.decode_b64jose(json["y"]) 384 | assert y[0] == 0 385 | assert len(y) == 32 386 | JWK.from_json(json) 387 | 388 | 389 | if __name__ == "__main__": 390 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 391 | -------------------------------------------------------------------------------- /tests/jws_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.jws.""" 2 | 3 | import base64 4 | import sys 5 | import unittest 6 | from unittest import mock 7 | 8 | import pytest 9 | import test_util 10 | from cryptography import x509 11 | from cryptography.hazmat.primitives.serialization import Encoding 12 | 13 | from josepy import errors, json_util, jwa, jwk 14 | 15 | CERT = test_util.load_cert("cert.pem") 16 | KEY = jwk.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) 17 | 18 | 19 | class MediaTypeTest(unittest.TestCase): 20 | """Tests for josepy.jws.MediaType.""" 21 | 22 | def test_decode(self) -> None: 23 | from josepy.jws import MediaType 24 | 25 | assert "application/app" == MediaType.decode("application/app") 26 | assert "application/app" == MediaType.decode("app") 27 | with pytest.raises(errors.DeserializationError): 28 | MediaType.decode("app;foo") 29 | 30 | def test_encode(self) -> None: 31 | from josepy.jws import MediaType 32 | 33 | assert "app" == MediaType.encode("application/app") 34 | assert "application/app;foo" == MediaType.encode("application/app;foo") 35 | 36 | 37 | class HeaderTest(unittest.TestCase): 38 | """Tests for josepy.jws.Header.""" 39 | 40 | def setUp(self) -> None: 41 | from josepy.jws import Header 42 | 43 | self.header1 = Header(jwk="foo") 44 | self.header2 = Header(jwk="bar") 45 | self.crit = Header(crit=("a", "b")) 46 | self.empty = Header() 47 | 48 | def test_add_non_empty(self) -> None: 49 | from josepy.jws import Header 50 | 51 | assert Header(jwk="foo", crit=("a", "b")) == self.header1 + self.crit 52 | 53 | def test_add_empty(self) -> None: 54 | assert self.header1 == self.header1 + self.empty 55 | assert self.header1 == self.empty + self.header1 56 | 57 | def test_add_overlapping_error(self) -> None: 58 | with pytest.raises(TypeError): 59 | self.header1.__add__(self.header2) 60 | 61 | def test_add_wrong_type_error(self) -> None: 62 | with pytest.raises(TypeError): 63 | self.header1.__add__("xxx") 64 | 65 | def test_crit_decode_always_errors(self) -> None: 66 | from josepy.jws import Header 67 | 68 | with pytest.raises(errors.DeserializationError): 69 | Header.from_json({"crit": ["a", "b"]}) 70 | 71 | def test_x5c_decoding(self) -> None: 72 | from josepy.jws import Header 73 | 74 | header = Header(x5c=(CERT, CERT)) 75 | jobj = header.to_partial_json() 76 | assert isinstance(CERT, x509.Certificate) 77 | cert_asn1 = CERT.public_bytes(Encoding.DER) 78 | cert_b64 = base64.b64encode(cert_asn1) 79 | assert jobj == {"x5c": [cert_b64, cert_b64]} 80 | assert header == Header.from_json(jobj) 81 | jobj["x5c"][0] = base64.b64encode(b"xxx" + cert_asn1) 82 | with pytest.raises(errors.DeserializationError): 83 | Header.from_json(jobj) 84 | 85 | def test_find_key(self) -> None: 86 | assert "foo" == self.header1.find_key() 87 | assert "bar" == self.header2.find_key() 88 | with pytest.raises(errors.Error): 89 | self.crit.find_key() 90 | 91 | 92 | class SignatureTest(unittest.TestCase): 93 | """Tests for josepy.jws.Signature.""" 94 | 95 | def test_from_json(self) -> None: 96 | from josepy.jws import Header, Signature 97 | 98 | assert Signature(signature=b"foo", header=Header(alg=jwa.RS256)) == Signature.from_json( 99 | {"signature": "Zm9v", "header": {"alg": "RS256"}} 100 | ) 101 | 102 | def test_from_json_no_alg_error(self) -> None: 103 | from josepy.jws import Signature 104 | 105 | with pytest.raises(errors.DeserializationError): 106 | Signature.from_json({"signature": "foo"}) 107 | 108 | 109 | class JWSTest(unittest.TestCase): 110 | """Tests for josepy.jws.JWS.""" 111 | 112 | def setUp(self) -> None: 113 | self.privkey = KEY 114 | self.pubkey = self.privkey.public_key() 115 | 116 | from josepy.jws import JWS 117 | 118 | self.unprotected = JWS.sign(payload=b"foo", key=self.privkey, alg=jwa.RS256) 119 | self.protected = JWS.sign( 120 | payload=b"foo", key=self.privkey, alg=jwa.RS256, protect=frozenset(["jwk", "alg"]) 121 | ) 122 | self.mixed = JWS.sign( 123 | payload=b"foo", key=self.privkey, alg=jwa.RS256, protect=frozenset(["alg"]) 124 | ) 125 | 126 | def test_pubkey_jwk(self) -> None: 127 | assert self.unprotected.signature.combined.jwk == self.pubkey 128 | assert self.protected.signature.combined.jwk == self.pubkey 129 | assert self.mixed.signature.combined.jwk == self.pubkey 130 | 131 | def test_sign_unprotected(self) -> None: 132 | assert self.unprotected.verify() is True 133 | 134 | def test_sign_protected(self) -> None: 135 | assert self.protected.verify() is True 136 | 137 | def test_sign_mixed(self) -> None: 138 | assert self.mixed.verify() is True 139 | 140 | def test_compact_lost_unprotected(self) -> None: 141 | compact = self.mixed.to_compact() 142 | assert ( 143 | b"eyJhbGciOiAiUlMyNTYifQ.Zm9v.OHdxFVj73l5LpxbFp1AmYX4yJM0Pyb" 144 | b"_893n1zQjpim_eLS5J1F61lkvrCrCDErTEJnBGOGesJ72M7b6Ve1cAJA" == compact 145 | ) 146 | 147 | from josepy.jws import JWS 148 | 149 | mixed = JWS.from_compact(compact) 150 | 151 | assert self.mixed != mixed 152 | assert {"alg"} == set(mixed.signature.combined.not_omitted()) 153 | 154 | def test_from_compact_missing_components(self) -> None: 155 | from josepy.jws import JWS 156 | 157 | with pytest.raises(errors.DeserializationError): 158 | JWS.from_compact(b".") 159 | 160 | def test_json_omitempty(self) -> None: 161 | protected_jobj = self.protected.to_partial_json(flat=True) 162 | unprotected_jobj = self.unprotected.to_partial_json(flat=True) 163 | 164 | assert "protected" not in unprotected_jobj 165 | assert "header" not in protected_jobj 166 | 167 | unprotected_jobj["header"] = unprotected_jobj["header"].to_json() 168 | 169 | from josepy.jws import JWS 170 | 171 | assert JWS.from_json(protected_jobj) == self.protected 172 | assert JWS.from_json(unprotected_jobj) == self.unprotected 173 | 174 | def test_json_flat(self) -> None: 175 | jobj_to = { 176 | "signature": json_util.encode_b64jose(self.mixed.signature.signature), 177 | "payload": json_util.encode_b64jose(b"foo"), 178 | "header": self.mixed.signature.header, 179 | "protected": json_util.encode_b64jose(self.mixed.signature.protected.encode("utf-8")), 180 | } 181 | 182 | from josepy.jws import Header 183 | 184 | jobj_from = jobj_to.copy() 185 | header = jobj_from["header"] 186 | assert isinstance(header, Header) 187 | jobj_from["header"] = header.to_json() 188 | 189 | assert self.mixed.to_partial_json(flat=True) == jobj_to 190 | from josepy.jws import JWS 191 | 192 | assert self.mixed == JWS.from_json(jobj_from) 193 | 194 | def test_json_not_flat(self) -> None: 195 | jobj_to = { 196 | "signatures": (self.mixed.signature,), 197 | "payload": json_util.encode_b64jose(b"foo"), 198 | } 199 | jobj_from = jobj_to.copy() 200 | signature = jobj_to["signatures"][0] 201 | from josepy.jws import Signature 202 | 203 | assert isinstance(signature, Signature) 204 | jobj_from["signatures"] = [signature.to_json()] 205 | 206 | assert self.mixed.to_partial_json(flat=False) == jobj_to 207 | from josepy.jws import JWS 208 | 209 | assert self.mixed == JWS.from_json(jobj_from) 210 | 211 | def test_from_json_mixed_flat(self) -> None: 212 | from josepy.jws import JWS 213 | 214 | with pytest.raises(errors.DeserializationError): 215 | JWS.from_json({"signatures": (), "signature": "foo"}) 216 | 217 | def test_from_json_hashable(self) -> None: 218 | from josepy.jws import JWS 219 | 220 | hash(JWS.from_json(self.mixed.to_json())) 221 | 222 | 223 | class CLITest(unittest.TestCase): 224 | def setUp(self) -> None: 225 | self.key_path = test_util.vector_path("rsa512_key.pem") 226 | 227 | def test_unverified(self) -> None: 228 | from josepy.jws import CLI 229 | 230 | with mock.patch("sys.stdin") as sin: 231 | sin.read.return_value = '{"payload": "foo", "signature": "xxx"}' 232 | with mock.patch("sys.stdout"): 233 | assert CLI.run(["verify"]) is False 234 | 235 | def test_json(self) -> None: 236 | from josepy.jws import CLI 237 | 238 | with mock.patch("sys.stdin") as sin: 239 | sin.read.return_value = "foo" 240 | with mock.patch("sys.stdout") as sout: 241 | CLI.run(["sign", "-k", self.key_path, "-a", "RS256", "-p", "jwk"]) 242 | sin.read.return_value = sout.write.mock_calls[0][1][0] 243 | assert 0 == CLI.run(["verify"]) 244 | 245 | def test_compact(self) -> None: 246 | from josepy.jws import CLI 247 | 248 | with mock.patch("sys.stdin") as sin: 249 | sin.read.return_value = "foo" 250 | with mock.patch("sys.stdout") as sout: 251 | CLI.run(["--compact", "sign", "-k", self.key_path]) 252 | sin.read.return_value = sout.write.mock_calls[0][1][0] 253 | assert 0 == CLI.run(["--compact", "verify", "--kty", "RSA", "-k", self.key_path]) 254 | 255 | 256 | if __name__ == "__main__": 257 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 258 | -------------------------------------------------------------------------------- /tests/magic_typing_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.magic_typing.""" 2 | 3 | import sys 4 | import warnings 5 | from unittest import mock 6 | 7 | import pytest 8 | 9 | 10 | def test_import_success() -> None: 11 | import typing as temp_typing 12 | 13 | typing_class_mock = mock.MagicMock() 14 | text_mock = mock.MagicMock() 15 | typing_class_mock.Text = text_mock 16 | sys.modules["typing"] = typing_class_mock 17 | if "josepy.magic_typing" in sys.modules: 18 | del sys.modules["josepy.magic_typing"] # pragma: no cover 19 | with warnings.catch_warnings(): 20 | warnings.filterwarnings("ignore", category=DeprecationWarning) 21 | from josepy.magic_typing import Text 22 | assert Text == text_mock 23 | del sys.modules["josepy.magic_typing"] 24 | sys.modules["typing"] = temp_typing 25 | 26 | 27 | if __name__ == "__main__": 28 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 29 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | """Test utilities.""" 2 | 3 | import atexit 4 | import contextlib 5 | import importlib.resources 6 | import os 7 | from typing import Any 8 | 9 | from cryptography import x509 10 | from cryptography.hazmat.backends import default_backend 11 | from cryptography.hazmat.primitives import serialization 12 | 13 | import josepy.util 14 | from josepy import ComparableRSAKey 15 | from josepy.util import ComparableECKey 16 | 17 | TESTDATA = importlib.resources.files("testdata") 18 | 19 | 20 | def vector_path(*names: str) -> str: 21 | """Path to a test vector.""" 22 | # This code is based on the recommendation at 23 | # https://web.archive.org/web/20230131043552/https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-filename. 24 | file_manager = contextlib.ExitStack() 25 | atexit.register(file_manager.close) 26 | ref = TESTDATA.joinpath(*names) 27 | # We convert the value to str here because some of the calling code doesn't 28 | # work with pathlib objects. 29 | return str(file_manager.enter_context(importlib.resources.as_file(ref))) 30 | 31 | 32 | def load_vector(*names: str) -> bytes: 33 | """Load contents of a test vector.""" 34 | return TESTDATA.joinpath(*names).read_bytes() 35 | 36 | 37 | def _guess_loader(filename: str, loader_pem: Any, loader_der: Any) -> Any: 38 | _, ext = os.path.splitext(filename) 39 | if ext.lower() == ".pem": 40 | return loader_pem 41 | elif ext.lower() == ".der": 42 | return loader_der 43 | else: # pragma: no cover 44 | raise ValueError("Loader could not be recognized based on extension") 45 | 46 | 47 | def load_cert(*names: str) -> x509.Certificate: 48 | """Load certificate.""" 49 | loader = _guess_loader( 50 | names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate 51 | ) 52 | return loader(load_vector(*names)) 53 | 54 | 55 | def load_csr(*names: str) -> x509.CertificateSigningRequest: 56 | """Load certificate request.""" 57 | loader = _guess_loader(names[-1], x509.load_pem_x509_csr, x509.load_der_x509_csr) 58 | return loader(load_vector(*names)) 59 | 60 | 61 | def load_rsa_private_key(*names: str) -> josepy.util.ComparableRSAKey: 62 | """Load RSA private key.""" 63 | loader = _guess_loader( 64 | names[-1], serialization.load_pem_private_key, serialization.load_der_private_key 65 | ) 66 | return ComparableRSAKey(loader(load_vector(*names), password=None, backend=default_backend())) 67 | 68 | 69 | def load_ec_private_key(*names: str) -> josepy.util.ComparableECKey: 70 | """Load EC private key.""" 71 | loader = _guess_loader( 72 | names[-1], serialization.load_pem_private_key, serialization.load_der_private_key 73 | ) 74 | return ComparableECKey(loader(load_vector(*names), password=None, backend=default_backend())) 75 | -------------------------------------------------------------------------------- /tests/testdata/README: -------------------------------------------------------------------------------- 1 | In order for josepy.test_util._guess_loader to work properly, make sure 2 | to use appropriate extension for vector filenames: .pem for PEM and 3 | .der for DER. 4 | 5 | The following command has been used to generate test keys: 6 | 7 | for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done 8 | 9 | openssl ecparam -name prime256v1 -genkey -out ec_p256_key.pem 10 | openssl ecparam -name secp384r1 -genkey -out ec_p384_key.pem 11 | openssl ecparam -name secp521r1 -genkey -out ec_p521_key.pem 12 | 13 | and for the CSR: 14 | 15 | openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der 16 | 17 | and for the certificate: 18 | 19 | openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der 20 | -------------------------------------------------------------------------------- /tests/testdata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certbot/josepy/8ddcaaed99a61e9277df1ec00157f0aea53378d4/tests/testdata/__init__.py -------------------------------------------------------------------------------- /tests/testdata/cert-100sans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV 3 | BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv 4 | bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X 5 | DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU 6 | BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp 7 | ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B 8 | AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 9 | rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T 10 | BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t 11 | ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt 12 | cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j 13 | b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN 14 | ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh 15 | bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs 16 | ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx 17 | LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv 18 | bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN 19 | ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh 20 | bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs 21 | ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3 22 | LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv 23 | bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN 24 | ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh 25 | bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs 26 | ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz 27 | LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv 28 | bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN 29 | ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh 30 | bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs 31 | ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5 32 | LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv 33 | bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN 34 | ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh 35 | bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs 36 | ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1 37 | LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv 38 | bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN 39 | ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh 40 | bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs 41 | ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN 42 | AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ 43 | XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG 44 | -----END CERTIFICATE----- 45 | -------------------------------------------------------------------------------- /tests/testdata/cert-idnsans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV 3 | BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv 4 | bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X 5 | DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU 6 | BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp 7 | ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B 8 | AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 9 | rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T 10 | BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I 11 | z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g 12 | z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z 13 | z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM 14 | 2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf 15 | 2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3 16 | 2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi 17 | 2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi 18 | 2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs 19 | aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN 20 | 247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p 21 | bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4 22 | 27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt 23 | 4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh 24 | oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh 25 | oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh 26 | oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm 27 | 4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2 28 | LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT 29 | TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /tests/testdata/cert-san.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx 3 | ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM 4 | IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 5 | YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG 6 | A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix 7 | KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS 8 | BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 9 | 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c 10 | +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt 11 | cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF 12 | nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 13 | RDjyGMKy5ZgM2w== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /tests/testdata/cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certbot/josepy/8ddcaaed99a61e9277df1ec00157f0aea53378d4/tests/testdata/cert.der -------------------------------------------------------------------------------- /tests/testdata/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx 3 | ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM 4 | IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 5 | YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG 6 | A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix 7 | KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS 8 | BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 9 | 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c 10 | +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll 11 | vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn 12 | B/o= 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /tests/testdata/critical-san.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG 3 | A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5 4 | MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g 5 | U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN 6 | MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF 7 | AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M 8 | 0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV 9 | EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU 10 | 0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl 11 | XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF 12 | fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD 13 | VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl 14 | bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV 15 | HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo 16 | dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw 17 | SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0 18 | dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ 19 | KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv 20 | DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w 21 | DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L 22 | ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs 23 | OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst 24 | bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS 25 | yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be 26 | n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv 27 | KA== 28 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /tests/testdata/csr-100sans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz 3 | Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG 4 | A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt 5 | H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 6 | lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud 7 | EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv 8 | bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh 9 | bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu 10 | Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C 11 | DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4 12 | YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w 13 | bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy 14 | MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j 15 | b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C 16 | DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4 17 | YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w 18 | bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz 19 | Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j 20 | b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C 21 | DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4 22 | YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w 23 | bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1 24 | My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j 25 | b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C 26 | DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4 27 | YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w 28 | bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2 29 | OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j 30 | b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C 31 | DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4 32 | YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w 33 | bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4 34 | NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j 35 | b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C 36 | DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4 37 | YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w 38 | bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3 39 | DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo 40 | duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg== 41 | -----END CERTIFICATE REQUEST----- 42 | -------------------------------------------------------------------------------- /tests/testdata/csr-6sans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw 3 | EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy 4 | c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG 5 | 9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 6 | 9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG 7 | 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL 8 | ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t 9 | ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd 10 | k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv 11 | IvzVBz/nD11drfz/RNuX 12 | -----END CERTIFICATE REQUEST----- 13 | -------------------------------------------------------------------------------- /tests/testdata/csr-idnsans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz 3 | Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG 4 | A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt 5 | H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 6 | lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud 7 | EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP 8 | iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P 9 | oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP 10 | s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ 11 | jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z 12 | n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ 13 | t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC 14 | YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa 15 | otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh 16 | bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb 17 | jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu 18 | aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb 19 | uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg 20 | reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5 21 | 4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ 22 | 4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ 23 | 4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh 24 | puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh 25 | ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr 26 | dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng== 27 | -----END CERTIFICATE REQUEST----- 28 | -------------------------------------------------------------------------------- /tests/testdata/csr-nosans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh 3 | MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt 4 | cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn 5 | BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz 6 | AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo 7 | wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= 8 | -----END CERTIFICATE REQUEST----- 9 | -------------------------------------------------------------------------------- /tests/testdata/csr-san.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw 3 | EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy 4 | c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 5 | 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f 6 | p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN 7 | AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t 8 | MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy 9 | tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== 10 | -----END CERTIFICATE REQUEST----- 11 | -------------------------------------------------------------------------------- /tests/testdata/csr.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certbot/josepy/8ddcaaed99a61e9277df1ec00157f0aea53378d4/tests/testdata/csr.der -------------------------------------------------------------------------------- /tests/testdata/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw 3 | EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy 4 | c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 5 | 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f 6 | p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN 7 | AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB 8 | AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G 9 | n9XBE1N9W6HCIEut2d8wACg= 10 | -----END CERTIFICATE REQUEST----- 11 | -------------------------------------------------------------------------------- /tests/testdata/dsa512_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PARAMETERS----- 2 | MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC 3 | OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA 4 | qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl 5 | 41pgNJpgu99YOYqPpS0g7A== 6 | -----END DSA PARAMETERS----- 7 | -----BEGIN DSA PRIVATE KEY----- 8 | MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf 9 | n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP 10 | AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm 11 | rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth 12 | zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE 13 | mNv063So6E+eYaIN 14 | -----END DSA PRIVATE KEY----- 15 | -------------------------------------------------------------------------------- /tests/testdata/ec_p256_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEIMUXjUASqqk7YRvKE5gXYaeBEGCJkitXVan2tssjJ9q3oAoGCCqGSM49 6 | AwEHoUQDQgAEjjQtV+fA7J/tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5UQ8DDz/zPs 7 | 9gqwcfqGUZKWxbEWgWXv7S8zRBEZuae8Jw== 8 | -----END EC PRIVATE KEY----- -------------------------------------------------------------------------------- /tests/testdata/ec_p384_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQAIg== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MIGkAgEBBDBHGWgnk8VgEwuXadVIOaCXw+MJ7qVrSHSiAOZri9LExJGSioGpGtZa 6 | fzqLGitkNAygBwYFK4EEACKhZANiAAS0iGk20ReRp1RutjzitgaACkUzVf/eXcRZ 7 | BG44/Uha3GVN+2tlDCd4lkXSTR5GfTQpbYan5NOENeZn70wk+cPyG/5fXfRJW99l 8 | GXVrTgc9XAhu8t7zua8D+K45r6bJTK8= 9 | -----END EC PRIVATE KEY----- -------------------------------------------------------------------------------- /tests/testdata/ec_p521_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQAIw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MIHcAgEBBEIB9I82YbB0mhnHdWOnwP4Ag0Gl2x+tKYnfTWNEaqW0+cztSrI0OD/W 6 | WtjJo+8fC+2QWGxV4l1sHpXebBnTG8NWS2OgBwYFK4EEACOhgYkDgYYABABZHZen 7 | CswZj9fFPH336T+CGTdnhA+OYQ3MET5eWXgpcB3C7ZzATLlmBRTvzc5ugy9jIaTd 8 | MyNm3DCyXfeWmNrT7AGL2at8Aco7e50HfJ2DKoRTQi94//lOhj1mXrdYWAWP12R6 9 | wuMiFdEeCZ7LEQGPXqnNRBJaQnOIc4orNY4ldYkeFg== 10 | -----END EC PRIVATE KEY----- -------------------------------------------------------------------------------- /tests/testdata/rsa1024_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi 3 | 4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/ 4 | w4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB 5 | AoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB 6 | Ia+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc 7 | TmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB 8 | CZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X 9 | UTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak 10 | Co9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt 11 | 73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa 12 | HgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU 13 | 6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ 14 | c2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /tests/testdata/rsa2048_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV 3 | BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do 4 | ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx 5 | MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC 6 | V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy 7 | dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 8 | ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN 9 | PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm 10 | 7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn 11 | xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD 12 | g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1 13 | RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU 14 | uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G 15 | A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T 16 | RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx 17 | nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8 18 | kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+ 19 | aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P 20 | AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf 21 | Esg= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /tests/testdata/rsa2048_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq 3 | r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo 4 | VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW 5 | AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ 6 | MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W 7 | Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp 8 | v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3 9 | 3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO 10 | z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB 11 | o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK 12 | lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH 13 | Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a 14 | hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/ 15 | IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/ 16 | 9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP 17 | n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj 18 | 9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy 19 | ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb 20 | f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3 21 | YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3 22 | xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG 23 | wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL 24 | Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK 25 | jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh 26 | QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6 27 | kmf1UgGFcKrJuXgwEtTVxw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/testdata/rsa256_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh 3 | AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N 4 | E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 5 | rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt 6 | -----END RSA PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /tests/testdata/rsa512_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 3 | vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn 4 | elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc 5 | mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp 6 | Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 7 | 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 8 | 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ 9 | -----END RSA PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /tests/util_test.py: -------------------------------------------------------------------------------- 1 | """Tests for josepy.util.""" 2 | 3 | import functools 4 | import sys 5 | import unittest 6 | 7 | import pytest 8 | import test_util 9 | 10 | 11 | class ComparableRSAKeyTest(unittest.TestCase): 12 | """Tests for josepy.util.ComparableRSAKey.""" 13 | 14 | def setUp(self) -> None: 15 | # test_utl.load_rsa_private_key return ComparableRSAKey 16 | self.key = test_util.load_rsa_private_key("rsa256_key.pem") 17 | self.key_same = test_util.load_rsa_private_key("rsa256_key.pem") 18 | self.key2 = test_util.load_rsa_private_key("rsa512_key.pem") 19 | 20 | def test_getattr_proxy(self) -> None: 21 | assert 256 == self.key.key_size 22 | 23 | def test_eq(self) -> None: 24 | assert self.key == self.key_same 25 | 26 | def test_ne(self) -> None: 27 | assert self.key != self.key2 28 | 29 | def test_ne_different_types(self) -> None: 30 | assert self.key != 5 31 | 32 | def test_ne_not_wrapped(self) -> None: 33 | assert self.key != self.key_same._wrapped 34 | 35 | def test_ne_no_serialization(self) -> None: 36 | from josepy.util import ComparableRSAKey 37 | 38 | assert ComparableRSAKey(5) != ComparableRSAKey(5) # type: ignore 39 | 40 | def test_hash(self) -> None: 41 | assert isinstance(hash(self.key), int) 42 | assert hash(self.key) == hash(self.key_same) 43 | assert hash(self.key) != hash(self.key2) 44 | 45 | def test_repr(self) -> None: 46 | assert repr(self.key).startswith(" None: 49 | from josepy.util import ComparableRSAKey 50 | 51 | assert isinstance(self.key.public_key(), ComparableRSAKey) 52 | 53 | 54 | class ComparableECKeyTest(unittest.TestCase): 55 | """Tests for josepy.util.ComparableECKey.""" 56 | 57 | def setUp(self) -> None: 58 | # test_utl.load_ec_private_key return ComparableECKey 59 | self.p256_key = test_util.load_ec_private_key("ec_p256_key.pem") 60 | self.p256_key_same = test_util.load_ec_private_key("ec_p256_key.pem") 61 | self.p384_key = test_util.load_ec_private_key("ec_p384_key.pem") 62 | self.p521_key = test_util.load_ec_private_key("ec_p521_key.pem") 63 | 64 | def test_getattr_proxy(self) -> None: 65 | assert 256 == self.p256_key.key_size 66 | 67 | def test_eq(self) -> None: 68 | assert self.p256_key == self.p256_key_same 69 | 70 | def test_ne(self) -> None: 71 | assert self.p256_key != self.p384_key 72 | assert self.p256_key != self.p521_key 73 | 74 | def test_ne_different_types(self) -> None: 75 | assert self.p256_key != 5 76 | 77 | def test_ne_not_wrapped(self) -> None: 78 | assert self.p256_key != self.p256_key_same._wrapped 79 | 80 | def test_ne_no_serialization(self) -> None: 81 | from josepy.util import ComparableECKey 82 | 83 | assert ComparableECKey(5) != ComparableECKey(5) # type: ignore 84 | 85 | def test_hash(self) -> None: 86 | assert isinstance(hash(self.p256_key), int) 87 | assert hash(self.p256_key) == hash(self.p256_key_same) 88 | assert hash(self.p256_key) != hash(self.p384_key) 89 | assert hash(self.p256_key) != hash(self.p521_key) 90 | 91 | def test_repr(self) -> None: 92 | assert repr(self.p256_key).startswith(" None: 95 | from josepy.util import ComparableECKey 96 | 97 | assert isinstance(self.p256_key.public_key(), ComparableECKey) 98 | 99 | 100 | class ImmutableMapTest(unittest.TestCase): 101 | """Tests for josepy.util.ImmutableMap.""" 102 | 103 | def setUp(self) -> None: 104 | from josepy.util import ImmutableMap 105 | 106 | class A(ImmutableMap): 107 | x: int 108 | y: int 109 | __slots__ = ("x", "y") 110 | 111 | class B(ImmutableMap): 112 | x: int 113 | y: int 114 | __slots__ = ("x", "y") 115 | 116 | self.A = A 117 | self.B = B 118 | 119 | self.a1 = self.A(x=1, y=2) 120 | self.a1_swap = self.A(y=2, x=1) 121 | self.a2 = self.A(x=3, y=4) 122 | self.b = self.B(x=1, y=2) 123 | 124 | def test_update(self) -> None: 125 | assert self.A(x=2, y=2) == self.a1.update(x=2) 126 | assert self.a2 == self.a1.update(x=3, y=4) 127 | 128 | def test_get_missing_item_raises_key_error(self) -> None: 129 | with pytest.raises(KeyError): 130 | self.a1.__getitem__("z") 131 | 132 | def test_order_of_args_does_not_matter(self) -> None: 133 | assert self.a1 == self.a1_swap 134 | 135 | def test_type_error_on_missing(self) -> None: 136 | with pytest.raises(TypeError): 137 | self.A(x=1) 138 | with pytest.raises(TypeError): 139 | self.A(y=2) 140 | 141 | def test_type_error_on_unrecognized(self) -> None: 142 | with pytest.raises(TypeError): 143 | self.A(x=1, z=2) 144 | with pytest.raises(TypeError): 145 | self.A(x=1, y=2, z=3) 146 | 147 | def test_get_attr(self) -> None: 148 | assert 1 == self.a1.x 149 | assert 2 == self.a1.y 150 | assert 1 == self.a1_swap.x 151 | assert 2 == self.a1_swap.y 152 | 153 | def test_set_attr_raises_attribute_error(self) -> None: 154 | with pytest.raises(AttributeError): 155 | functools.partial(self.a1.__setattr__, "x")(10) 156 | 157 | def test_equal(self) -> None: 158 | assert self.a1 == self.a1 159 | assert self.a2 == self.a2 160 | assert self.a1 != self.a2 161 | 162 | def test_hash(self) -> None: 163 | assert hash((1, 2)) == hash(self.a1) 164 | 165 | def test_unhashable(self) -> None: 166 | with pytest.raises(TypeError): 167 | self.A(x=1, y={}).__hash__() 168 | 169 | def test_repr(self) -> None: 170 | assert "A(x=1, y=2)" == repr(self.a1) 171 | assert "A(x=1, y=2)" == repr(self.a1_swap) 172 | assert "B(x=1, y=2)" == repr(self.b) 173 | assert "B(x='foo', y='bar')" == repr(self.B(x="foo", y="bar")) 174 | 175 | 176 | class frozendictTest(unittest.TestCase): 177 | """Tests for josepy.util.frozendict.""" 178 | 179 | def setUp(self) -> None: 180 | from josepy.util import frozendict 181 | 182 | self.fdict = frozendict(x=1, y="2") 183 | 184 | def test_init_dict(self) -> None: 185 | from josepy.util import frozendict 186 | 187 | assert self.fdict == frozendict({"x": 1, "y": "2"}) 188 | 189 | def test_init_other_raises_type_error(self) -> None: 190 | from josepy.util import frozendict 191 | 192 | # specifically fail for generators... 193 | with pytest.raises(TypeError): 194 | frozendict({"a": "b"}.items()) 195 | 196 | def test_len(self) -> None: 197 | assert 2 == len(self.fdict) 198 | 199 | def test_hash(self) -> None: 200 | assert isinstance(hash(self.fdict), int) 201 | 202 | def test_getattr_proxy(self) -> None: 203 | assert 1 == self.fdict.x 204 | assert "2" == self.fdict.y 205 | 206 | def test_getattr_raises_attribute_error(self) -> None: 207 | with pytest.raises(AttributeError): 208 | self.fdict.__getattr__("z") 209 | 210 | def test_setattr_immutable(self) -> None: 211 | with pytest.raises(AttributeError): 212 | self.fdict.__setattr__("z", 3) 213 | 214 | def test_repr(self) -> None: 215 | assert "frozendict(x=1, y='2')" == repr(self.fdict) 216 | 217 | 218 | if __name__ == "__main__": 219 | sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover 220 | -------------------------------------------------------------------------------- /tools/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | # Release dev packages to PyPI 3 | 4 | PrintUsageAndExit() { 5 | echo Usage: 6 | echo "$0 --changelog-ok " 7 | exit 1 8 | } 9 | 10 | if [ "`dirname $0`" != "tools" ] ; then 11 | echo Please run this script from the repo root 12 | exit 1 13 | fi 14 | 15 | if ! poetry export --help 2>&1 | grep -q constraints.txt ; then 16 | # turn off set -x for saner output 17 | set +x 18 | echo 'Please install poetry with poetry-plugin-export>=1.1.0' 19 | echo 'before running this script.' 20 | exit 1 21 | fi 22 | 23 | if [ "$1" != "--changelog-ok" ]; then 24 | # turn off set -x for saner output 25 | set +x 26 | echo "Make sure the changelog includes the exact text you want for the" 27 | echo "release and run the script again with --changelog-ok." 28 | echo 29 | PrintUsageAndExit 30 | fi 31 | 32 | CheckVersion() { 33 | # Args: 34 | if ! echo "$2" | grep -q -e '[0-9]\+.[0-9]\+.[0-9]\+' ; then 35 | echo "$1 doesn't look like 1.2.3" 36 | exit 1 37 | fi 38 | } 39 | 40 | version="$2" 41 | CheckVersion Version "$version" 42 | echo Releasing production version "$version"... 43 | nextversion="$3" 44 | CheckVersion "Next version" "$nextversion" 45 | RELEASE_BRANCH="candidate-$version" 46 | 47 | # If RELEASE_GPG_KEY isn't set, determine the key to use. 48 | if [ "$RELEASE_GPG_KEY" = "" ]; then 49 | TRUSTED_KEYS=" 50 | BF6BCFC89E90747B9A680FD7B6029E8500F7DB16 51 | 86379B4F0AF371B50CD9E5FF3402831161D1D280 52 | 20F201346BF8F3F455A73F9A780CC99432A28621 53 | F2871B4152AE13C49519111F447BF683AA3B26C3 54 | " 55 | for key in $TRUSTED_KEYS; do 56 | if gpg --with-colons --card-status | grep -q "$key"; then 57 | RELEASE_GPG_KEY="$key" 58 | break 59 | fi 60 | done 61 | if [ "$RELEASE_GPG_KEY" = "" ]; then 62 | echo A trusted PGP key was not found on your PGP card. 63 | exit 1 64 | fi 65 | fi 66 | 67 | # Needed to fix problems with git signatures and pinentry 68 | export GPG_TTY=$(tty) 69 | 70 | # port for a local Python Package Index (used in testing) 71 | PORT=${PORT:-1234} 72 | 73 | tag="v$version" 74 | mv "dist.$version" "dist.$version.$(date +%s).bak" || true 75 | git tag --delete "$tag" || true 76 | 77 | root_without_jose="$version.$$" 78 | root="./releases/jose.$root_without_jose" 79 | 80 | echo "Cloning into fresh copy at $root" # clean repo = no artifacts 81 | git clone . $root 82 | git rev-parse HEAD 83 | cd $root 84 | if [ "$RELEASE_BRANCH" != "candidate-$version" ] ; then 85 | git branch -f "$RELEASE_BRANCH" 86 | fi 87 | git checkout "$RELEASE_BRANCH" 88 | 89 | SetVersion() { 90 | ver="$1" 91 | short_ver=$(echo "$ver" | cut -d. -f1,2) 92 | sed -i "s/^release.*/release = \"$ver\"/" docs/conf.py 93 | sed -i "s/^version.*/version = \"$short_ver\"/" docs/conf.py 94 | poetry version "$ver" 95 | 96 | # interactive user input 97 | git add -p . 98 | 99 | } 100 | 101 | SetVersion "$version" 102 | 103 | echo "Preparing sdists and wheels" 104 | poetry build 105 | 106 | echo "Signing josepy" 107 | for x in dist/*.tar.gz dist/*.whl 108 | do 109 | gpg -u "$RELEASE_GPG_KEY" --detach-sign --armor --sign --digest-algo sha256 $x 110 | done 111 | 112 | mkdir "dist.$version" 113 | mv dist "dist.$version/josepy" 114 | poetry export -f constraints.txt --with dev --without-hashes > constraints.txt 115 | 116 | echo "Testing packages" 117 | cd "dist.$version" 118 | # start local PyPI 119 | python3 -m http.server "$PORT" & 120 | # cd .. is NOT done on purpose: we make sure that all subpackages are 121 | # installed from local PyPI rather than current directory (repo root) 122 | python3 -m venv ../venv 123 | . ../venv/bin/activate 124 | pip install -U setuptools 125 | pip install -U pip 126 | # Now, use our local PyPI. Disable cache so we get the correct KGS even if we 127 | # (or our dependencies) have conditional dependencies implemented with if 128 | # statements in setup.py and we have cached wheels lying around that would 129 | # cause those ifs to not be evaluated. 130 | pip install \ 131 | --no-cache-dir \ 132 | --extra-index-url http://localhost:$PORT \ 133 | --constraint ../constraints.txt \ 134 | josepy pytest 135 | # stop local PyPI 136 | kill $! 137 | cd ~- 138 | 139 | cd .. 140 | # freeze before installing anything else, so that we know end-user KGS 141 | # make sure "twine upload" doesn't catch "kgs" 142 | if [ -d kgs ] ; then 143 | echo Deleting old kgs... 144 | rm -rf kgs 145 | fi 146 | mkdir kgs 147 | kgs="kgs/$version" 148 | pip freeze | tee $kgs 149 | cd ~- 150 | echo testing josepy 151 | pytest 152 | deactivate 153 | 154 | git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version" 155 | git tag --local-user "$RELEASE_GPG_KEY" --sign --message "Release $version" "$tag" 156 | 157 | echo Now run twine upload "$root/dist.$version/*/*" 158 | 159 | if [ "$RELEASE_BRANCH" = candidate-"$version" ] ; then 160 | SetVersion "$nextversion".dev0 161 | git commit -m "Bump version to $nextversion" 162 | fi 163 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,.tox 3 | # E203 and W503 are ignored here for compatibility with black 4 | ignore = E203,W503,W504 5 | max-line-length = 100 6 | 7 | [tox] 8 | isolated_build = true 9 | envlist = mypy,pre-commit,py 10 | 11 | [testenv] 12 | allowlist_externals = 13 | echo 14 | false 15 | poetry 16 | commands_pre = poetry install -v 17 | # This and the next few testenvs are a workaround for 18 | # https://github.com/tox-dev/tox/issues/2858. 19 | commands = 20 | echo "Unrecognized environment name {envname}" 21 | false 22 | 23 | [testenv:py] 24 | commands = poetry run pytest -v --cov-report xml --cov-report=term-missing --cov=josepy {posargs} 25 | 26 | [testenv:py3{,8,9,10,11,12,13,14}] 27 | commands = {[testenv:py]commands} 28 | 29 | [testenv:py3.{8,9,10,11,12,13,14}] 30 | commands = {[testenv:py]commands} 31 | 32 | [testenv:pre-commit] 33 | commands = poetry run pre-commit run --all 34 | 35 | [testenv:mypy] 36 | commands = poetry run mypy src tests 37 | --------------------------------------------------------------------------------