From ba94526f5c53425e6a14bfa49c71fa969b30d767 Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Mon, 14 Nov 2022 17:21:11 +0100 Subject: [PATCH] better separation of duties between `jwa` and `jwk` submodules (#4) * rename `select_alg` to `select_alg_class` and `select_algs` to `select_alg_classes`, reorganize parameters * rename `BaseHMACSigAlg.hash_alg` to `hashing_alg` for consistency with the other signature classes * introduce Jwk methods to get JWA classes and wrappers * add `OKPJwk.from_bytes()` method * introduce Jwk.check() and `to_jwk()` methods * Jwk.key_ops returns a Tuple[str, ...] instead of a List * simplify jwks.verify() * misc fixes, more tests and improved code coverage --- .coveragerc | 3 + .github/ISSUE_TEMPLATE.md | 3 +- .github/workflows/dev.yml | 14 +- .github/workflows/release.yml | 8 +- .pre-commit-config.yaml | 12 +- CONTRIBUTING.md | 35 +- README.md | 42 +- docs/api.md | 78 +++- docs/authors.md | 4 +- docs/contributing.md | 4 +- docs/history.md | 4 +- docs/index.md | 4 +- docs/installation.md | 3 +- docs/usage.md | 62 ++- jwskate/__init__.py | 8 + jwskate/jwa/__init__.py | 6 + jwskate/jwa/base.py | 20 +- jwskate/jwa/key_mgmt/ecdh.py | 10 +- jwskate/jwa/key_mgmt/rsa.py | 14 +- jwskate/jwa/signature/__init__.py | 9 +- jwskate/jwa/signature/hmac.py | 12 +- jwskate/jwe/compact.py | 11 +- jwskate/jwk/__init__.py | 13 +- jwskate/jwk/alg.py | 32 +- jwskate/jwk/base.py | 379 +++++++++++++----- jwskate/jwk/ec.py | 5 +- jwskate/jwk/jwks.py | 44 +- jwskate/jwk/oct.py | 48 +-- jwskate/jwk/okp.py | 164 ++++++-- jwskate/jws/compact.py | 6 +- jwskate/jws/json.py | 2 +- jwskate/jws/signature.py | 6 +- jwskate/jwt/base.py | 11 +- jwskate/jwt/signed.py | 4 +- jwskate/jwt/signer.py | 2 +- poetry.lock | 323 ++++++++------- pyproject.toml | 14 +- tests/test_jwa/__init__.py | 0 tests/test_jwa/test_base.py | 24 ++ tests/test_jwa/test_encryption.py | 70 ++++ .../test_examples.py} | 80 +--- tests/test_jwa/test_key_mgmt.py | 41 ++ tests/test_jwa/test_signature.py | 12 + tests/test_jwe.py | 4 +- tests/test_jwk/test_alg.py | 102 ++++- tests/test_jwk/test_ec.py | 25 +- tests/test_jwk/test_generate.py | 30 +- tests/test_jwk/test_jwk.py | 139 ++++++- tests/test_jwk/test_jwks.py | 8 +- tests/test_jwk/test_okp.py | 183 ++++++++- tests/test_jwk/test_rsa.py | 29 ++ tests/test_jwk/test_symmetric.py | 48 ++- tests/test_jws.py | 15 +- tests/test_jwt.py | 62 ++- 54 files changed, 1596 insertions(+), 695 deletions(-) create mode 100644 tests/test_jwa/__init__.py create mode 100644 tests/test_jwa/test_base.py create mode 100644 tests/test_jwa/test_encryption.py rename tests/{test_jwa.py => test_jwa/test_examples.py} (74%) create mode 100644 tests/test_jwa/test_key_mgmt.py create mode 100644 tests/test_jwa/test_signature.py diff --git a/.coveragerc b/.coveragerc index 1a48dba..862cb7d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -13,3 +13,6 @@ exclude_lines = if __name__ == .__main__.: def main \.\.\. + assert False + pytest.skip + pass diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a3e32c6..dba969f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,8 +4,7 @@ ### Description -Describe what you were trying to get done. -Tell us what happened, what went wrong, and what you expected to happen. +Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. ### What I Did diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 99ca90a..a11367b 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -20,15 +20,15 @@ jobs: # The type of runner that the job will run on strategy: matrix: - python-versions: [3.7, 3.8, 3.9, '3.10'] + python-versions: [3.7, 3.8, 3.9, '3.10', '3.11'] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-versions }} @@ -49,10 +49,10 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - name: Install dependencies run: | @@ -66,7 +66,7 @@ jobs: - name: list files run: ls -l . - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: fail_ci_if_error: true files: coverage.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2262582..383adf8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,15 +22,15 @@ jobs: strategy: matrix: - python-versions: [3.8] + python-versions: ['3.10'] # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Generate Changelog - uses: heinrichreimer/github-changelog-generator-action@v2.1.1 + uses: heinrichreimer/github-changelog-generator-action@v2.3 with: token: ${{ secrets.GITHUB_TOKEN }} issues: true @@ -41,7 +41,7 @@ jobs: addSections: '{"documentation":{"prefix":"**Documentation:**","labels":["documentation"]}}' output: CHANGELOG.md - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-versions }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffc2fa8..fea175d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,14 +14,6 @@ repos: - id: check-merge-conflict - id: check-yaml args: [--unsafe] -- repo: https://github.com/executablebooks/mdformat - rev: 0.7.16 - hooks: - - id: mdformat - args: - - --number - additional_dependencies: - - mdformat-black - repo: https://github.com/hadialqattan/pycln rev: v2.1.1 hooks: @@ -32,7 +24,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 @@ -53,7 +45,7 @@ repos: args: - --add-ignore=D107 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.982 hooks: - id: mypy args: [--strict] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 323af1c..e1568bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,6 @@ # Contributing -Contributions are welcome, and they are greatly appreciated! Every little bit -helps, and credit will always be given. +Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: @@ -19,19 +18,18 @@ If you are reporting a bug, please include: ### Fix Bugs -Look through the GitHub issues for bugs. Anything tagged with "bug" and "help -wanted" is open to whoever wants to implement it. +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to +implement it. ### Implement Features -Look through the GitHub issues for features. Anything tagged with "enhancement" -and "help wanted" is open to whoever wants to implement it. +Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever +wants to implement it. ### Write Documentation -JsonWebSkate could always use more documentation, whether as part of the -official JsonWebSkate docs, in docstrings, or even on the web in blog posts, -articles, and such. +JsonWebSkate could always use more documentation, whether as part of the official JsonWebSkate docs, in docstrings, or +even on the web in blog posts, articles, and such. ### Submit Feedback @@ -41,8 +39,7 @@ If you are proposing a feature: - Explain in detail how it would work. - Keep the scope as narrow as possible, to make it easier to implement. -- Remember that this is a volunteer-driven project, and that contributions - are welcome :) +- Remember that this is a volunteer-driven project, and that contributions are welcome :) ## Get Started! @@ -70,8 +67,8 @@ Ready to contribute? Here's how to set up `jwskate` for local development. Now you can make your changes locally. -6. When you're done making changes, check that your changes pass the - tests, including testing other Python versions, with tox: +6. When you're done making changes, check that your changes pass the tests, including testing other Python versions, + with tox: ``` $ tox @@ -92,12 +89,10 @@ Now you can make your changes locally. Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put - your new functionality into a function with a docstring, and add the - feature to the list in README.md. +2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a + docstring, and add the feature to the list in README.md. 3. The pull request should work for Python 3.6, 3.7, 3.8, 3.9 and for PyPy. Check - https://github.com/guillp/jwskate/actions - and make sure that the tests pass for all supported Python versions. + https://github.com/guillp/jwskate/actions and make sure that the tests pass for all supported Python versions. ## Tips\`\`\` @@ -116,9 +111,7 @@ Then run: ``` -$ poetry patch # possible: major / minor / patch -$ git push -$ git push --tags +$ poetry patch # possible: major / minor / patch $ git push $ git push --tags ``` diff --git a/README.md b/README.md index b5e46f6..df7fd6c 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![PyPi](https://img.shields.io/pypi/v/jwskate.svg)](https://pypi.python.org/pypi/jwskate) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -A Pythonic implementation of the JOSE set of IETF specifications: [Json Web Signature][rfc7515], [Keys][rfc7517], [Algorithms][rfc7518], [Tokens][rfc7519] -and [Encryption][rfc7516] (RFC7515 to 7519), and their extensions [ECDH Signatures][rfc8037] (RFC8037), [JWK Thumbprints][rfc7638] (RFC7638), -and [JWK Thumbprint URI][rfc9278] (RFC9278). +A Pythonic implementation of the JOSE set of IETF specifications: [Json Web Signature][rfc7515], [Keys][rfc7517], +[Algorithms][rfc7518], [Tokens][rfc7519] and [Encryption][rfc7516] (RFC7515 to 7519), and their extensions +[ECDH Signatures][rfc8037] (RFC8037), [JWK Thumbprints][rfc7638] (RFC7638), and [JWK Thumbprint URI][rfc9278] (RFC9278). - Free software: MIT - Documentation: @@ -216,8 +216,8 @@ The generated JWT claims will include the standardised claims: ## Why a new lib ? -There are already multiple modules implementing JOSE and Json Web Crypto related specifications in Python. However, I have -been dissatisfied by all of them so far, so I decided to come up with my own module. +There are already multiple modules implementing JOSE and Json Web Crypto related specifications in Python. However, I +have been dissatisfied by all of them so far, so I decided to come up with my own module. - [PyJWT](https://pyjwt.readthedocs.io) - [JWCrypto](https://jwcrypto.readthedocs.io/) @@ -225,35 +225,39 @@ been dissatisfied by all of them so far, so I decided to come up with my own mod - [AuthLib](https://docs.authlib.org/en/latest/jose/) Not to say that those are _bad_ libs (I actually use `jwcrypto` myself for `jwskate` unit tests), but they either don't -support some important features, lack documentation, or generally have APIs that don't feel easy-enough, Pythonic-enough to use. +support some important features, lack documentation, or generally have APIs that don't feel easy-enough, Pythonic-enough +to use. ## Design ### JWK are dicts JWK are specified as JSON objects, which are parsed as `dict` in Python. The `Jwk` class in `jwskate` is actually a -`dict` subclass, so you can use it exactly like you would use a dict: you can access its members, dump it back as JSON, etc. -The same is true for Signed or Encrypted Json Web tokens in JSON format. +`dict` subclass, so you can use it exactly like you would use a dict: you can access its members, dump it back as JSON, +etc. The same is true for Signed or Encrypted Json Web tokens in JSON format. ### JWA Wrappers -You can use `cryptography` to do the cryptographic operations that are described in [JWA](https://www.rfc-editor.org/info/rfc7518), -but since `cryptography` is a general purpose library, its usage is not straightforward and gives you plenty of options -to carefully select and combine, leaving room for errors. -To work around this, `jwskate` comes with a set of wrappers that implement the exact JWA specifications, with minimum -risk of mistakes. +You can use `cryptography` to do the cryptographic operations that are described in +[JWA](https://www.rfc-editor.org/info/rfc7518), but since `cryptography` is a general purpose library, its usage is not +straightforward and gives you plenty of options to carefully select and combine, leaving room for errors. It has also a +quite inconsistent API to handle the different type of keys and algorithms. To work around +this, `jwskate` comes with a set of consistent wrappers that implement the exact JWA specifications, with minimum risk +of mistakes. ### Safe Signature Verification -For every signature verification method in `jwskate`, the expected signature(s) algorithm(s) must be specified. -That is to avoid a security flaw where your application accepts tokens with a weaker encryption scheme than what -your security policy mandates; or even worse, where it accepts unsigned tokens, or tokens that are symmetrically signed -with an improperly used public key, leaving your application exposed to exploitation by attackers. +For every signature verification method in `jwskate`, the expected signature(s) algorithm(s) must be specified. That is +to avoid a security flaw where your application accepts tokens with a weaker encryption scheme than what your security +policy mandates; or even worse, where it accepts unsigned tokens, or tokens that are symmetrically signed with an +improperly used public key, leaving your application exposed to exploitation by attackers. To specify which signature algorithms are accepted, each signature verification method accepts, in order of preference: -- an `alg` parameter which contains the expected algorithm, or an `algs` parameter which contains a list of acceptable algorithms -- the `alg` parameter from the signature verification `Jwk`, if present. This `alg` is the algorithm intended for use with that key. +- an `alg` parameter which contains the expected algorithm, or an `algs` parameter which contains a list of acceptable + algorithms +- the `alg` parameter from the signature verification `Jwk`, if present. This `alg` is the algorithm intended for use + with that key. Note that you cannot use `alg` and `algs` at the same time. If your `Jwk` contains an `alg` parameter, and you provide an `alg` or `algs` which does not match that value, a `Warning` will be emitted. diff --git a/docs/api.md b/docs/api.md index 638a2ac..aed3fba 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,10 +1,68 @@ -::: jwskate -selection: -docstring_style: google -filters: -\- "!^\_" -\- "^__init__" -rendering: -members_order: source -show_root_heading: true -heading_level: 2 +::: jwskate.jwk + options: + docstring_style: google + filters: + - "!^\_" + - "^__init__" + members_order: source + show_root_heading: true + heading_level: 1 + show_submodules: false + separate_signature: true + show_signature_annotations: true + + +::: jwskate.jwt + options: + docstring_style: google + filters: + - "!^\_" + - "^__init__" + members_order: source + show_root_heading: true + heading_level: 1 + show_submodules: false + separate_signature: true + show_signature_annotations: true + + +::: jwskate.jws + options: + docstring_style: google + filters: + - "!^\_" + - "^__init__" + members_order: source + show_root_heading: true + heading_level: 1 + show_submodules: false + separate_signature: true + show_signature_annotations: true + + +::: jwskate.jwe + options: + docstring_style: google + filters: + - "!^\_" + - "^__init__" + members_order: source + show_root_heading: true + heading_level: 1 + show_submodules: false + separate_signature: true + show_signature_annotations: true + + +::: jwskate.jwa + options: + docstring_style: google + filters: + - "!^\_" + - "^__init__" + members_order: source + show_root_heading: true + heading_level: 1 + show_submodules: false + separate_signature: true + show_signature_annotations: true diff --git a/docs/authors.md b/docs/authors.md index b42a650..4fdf4c7 100644 --- a/docs/authors.md +++ b/docs/authors.md @@ -1,3 +1 @@ -{% -include-markdown "../AUTHORS.md" -%} +{% include-markdown "../AUTHORS.md" %} diff --git a/docs/contributing.md b/docs/contributing.md index 0019ae1..e62f997 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,3 +1 @@ -{% -include-markdown "../CONTRIBUTING.md" -%} +{% include-markdown "../CONTRIBUTING.md" %} diff --git a/docs/history.md b/docs/history.md index 9f3c69f..e6b2d49 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,3 +1 @@ -{% -include-markdown "../HISTORY.md" -%} +{% include-markdown "../HISTORY.md" %} diff --git a/docs/index.md b/docs/index.md index 768aab5..75687bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1 @@ -{% -include-markdown "../README.md" -%} +{% include-markdown "../README.md" %} diff --git a/docs/installation.md b/docs/installation.md index fb0ab3d..935b791 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,8 +10,7 @@ $ pip install jwskate This is the preferred method to install `jwskate`, as it will always install the most recent stable release. -If you don't have [pip] installed, this [Python installation guide] -can guide you through the process. +If you don't have [pip] installed, this [Python installation guide] can guide you through the process. ## From source diff --git a/docs/usage.md b/docs/usage.md index da64bde..5c560ff 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -100,8 +100,8 @@ assert jwk.x == "WtjnvHG9b_IKBLn4QYTHz-AdoAiO_ork5LH1BL_5tyI" assert jwk["x"] == jwk.x ``` -Those will return the exact (usually base64url-encoded) value exactly as expressed in the JWK. -You can also get the real, decoded parameters with some special attributes: +Those will return the exact (usually base64url-encoded) value exactly as expressed in the JWK. You can also get the +real, decoded parameters with some special attributes: ```python from jwskate import Jwk @@ -135,8 +135,8 @@ The available special attributes vary depending on the key type. ### Based on a Key Type -You can generate a `Jwk` of a specific type (RSA, EC, etc.) using the class method `Jwk.generate_for_kty()`. It needs the key type as parameter, and -type-specific parameters: +You can generate a `Jwk` of a specific type (RSA, EC, etc.) using the class method `Jwk.generate_for_kty()`. It needs +the key type as parameter, and type-specific parameters: ```python from jwskate import Jwk @@ -159,8 +159,8 @@ assert jwk.use == "sig" ### Based on intended algorithm -You can generate a private key of the appropriate type for a given signature, key management or encryption algorithm -by using the method `Jwk.generate_for_alg()` this way: +You can generate a private key of the appropriate type for a given signature, key management or encryption algorithm by +using the method `Jwk.generate_for_alg()` this way: ```python from jwskate import Jwk @@ -218,8 +218,7 @@ Note that Symmetric keys are always considered private, so calling `.public_jwk( ### to JSON `Jwk` instances are dicts, so you can serialize it to JSON in the usual ways (with Python `json` module or any other -means). -You can also use the `to_json()` convenience method to serialize a Jwk: +means). You can also use the `to_json()` convenience method to serialize a Jwk: ```python from jwskate import Jwk @@ -320,22 +319,20 @@ Encrypting/decrypting arbitrary data requires a symmetric key. But it is possibl symmetric keys from asymmetric keys, using Key Management algorithms. Some of those Key Management algorithms rely on key wrapping, where a randomly-generated symmetric key (called a Content -Encryption Key or CEK) -is itself asymmetrically encrypted. It is also possible to use a symmetric key to "wrap" the CEK. +Encryption Key or CEK) is itself asymmetrically encrypted. It is also possible to use a symmetric key to "wrap" the CEK. Other algorithms rely on Diffie-Hellman, where the CEK is derived from a pair of keys, one private, the other public. -You can use the methods `sender_key()` and `receiver_key()` to handle all the key management stuff for you. -For `sender_key()`, which the message sender will use get a CEK, you just need to specify which encryption algorithm you -will use with the CEK, and the key management algorithm you want to wrap or derive that CEK. -It will return a tuple `(plaintext_message, encrypted_cek, extra_headers)`, with `plaintext_message` being the generated -CEK (as an instance of `SymmetricJwk`), -`encrypted_cek` is the wrapped CEK value (which can be empty for Diffie-Hellman based algorithms), -and `extra_headers` a dict of extra headers that are required for the key management algorithm (for example, `epk` for +You can use the methods `sender_key()` and `receiver_key()` to handle all the key management stuff for you. For +`sender_key()`, which the message sender will use get a CEK, you just need to specify which encryption algorithm you +will use with the CEK, and the key management algorithm you want to wrap or derive that CEK. It will return a tuple +`(plaintext_message, encrypted_cek, extra_headers)`, with `plaintext_message` being the generated CEK (as an instance of +`SymmetricJwk`), `encrypted_cek` is the wrapped CEK value (which can be empty for Diffie-Hellman based algorithms), and +`extra_headers` a dict of extra headers that are required for the key management algorithm (for example, `epk` for ECDH-ES based algorithms), -You can use `cleartext_cek` to encrypt your message with a given Encryption algorithm. You must then -send `encrypted_cek` and `extra_headers` to your recipient, along with the encrypted message, and both Key Management -and Encryption algorithms identifiers. +You can use `cleartext_cek` to encrypt your message with a given Encryption algorithm. You must then send +`encrypted_cek` and `extra_headers` to your recipient, along with the encrypted message, and both Key Management and +Encryption algorithms identifiers. ```python from jwskate import Jwk @@ -373,11 +370,10 @@ encrypted_message, iv, tag = plaintext_cek.encrypt(plaintext_message, alg=enc_al ``` On recipient side, in order to decrypt the message, you will need to obtain the same symmetric CEK that was used to -encrypt the message. That is done with `recipient_key()`. -You need to provide it with the `encrypted_cek` received from the sender (possibly empty for Diffie-Hellman based -algorithms), -the Key Management algorithm that is used to wrap the CEK, the Encryption algorithm that is used to encrypt/decrypt the -message, and the eventual extra headers depending on the Key Management algorithm. +encrypt the message. That is done with `recipient_key()`. You need to provide it with the `encrypted_cek` received from +the sender (possibly empty for Diffie-Hellman based algorithms), the Key Management algorithm that is used to wrap the +CEK, the Encryption algorithm that is used to encrypt/decrypt the message, and the eventual extra headers depending on +the Key Management algorithm. You can then use that CEK to decrypt the received message. @@ -619,15 +615,13 @@ The `Jwt` class and its subclasses represent a syntactically valid Jwt token. It and verify its signature. Note that a JWT token can optionally be encrypted. In that case, the signed JWT content will be the plaintext of a JWE -token. -Decrypting that JWE can then be achieved with the `JweCompact` class, then this plaintext can be manipulated with +token. Decrypting that JWE can then be achieved with the `JweCompact` class, then this plaintext can be manipulated with the `Jwt` class. ## Parsing JWT tokens -To parse an existing JWT token, simply provide its value to `Jwt`. It exposes all the JWT attributes, and -a `verify_signature()` method just like `JwsCompact()`. -Claims can be accessed either: +To parse an existing JWT token, simply provide its value to `Jwt`. It exposes all the JWT attributes, and a +`verify_signature()` method just like `JwsCompact()`. Claims can be accessed either: - with the `claims` attribute, which is a dict of the parsed JSON content - with subscription: `jwt['attribute']` does a key lookup inside the `claims` dict, just like `jwt.claims['attribute']` @@ -707,8 +701,8 @@ assert jwt.is_expired() ## Validating JWT tokens To validate a JWT token, verifying the signature is usually not enough. You probably want to validate the issuer, -audience, expiration date, and other claims. -To make things easier, use `SignedJwt.validate()`. It raises exceptions if one of the check fails: +audience, expiration date, and other claims. To make things easier, use `SignedJwt.validate()`. It raises exceptions if +one of the check fails: ```python from jwskate import Jwt @@ -747,8 +741,8 @@ print(jwt) ### JWT headers -The default header will contain the signing algorithm identifier (alg) and the JWK Key Identifier (kid), if there was one in the used JWK. -You can add additional headers by using the `extra_headers` parameter to `Jwt.sign()`: +The default header will contain the signing algorithm identifier (alg) and the JWK Key Identifier (kid), if there was +one in the used JWK. You can add additional headers by using the `extra_headers` parameter to `Jwt.sign()`: ```python from jwskate import Jwt, Jwk diff --git a/jwskate/__init__.py b/jwskate/__init__.py index 9522a64..db2c65c 100644 --- a/jwskate/__init__.py +++ b/jwskate/__init__.py @@ -88,7 +88,11 @@ SymmetricJwk, UnsupportedAlg, UnsupportedEllipticCurve, + UnsupportedKeyType, UnsupportedOKPCurve, + select_alg_class, + select_alg_classes, + to_jwk, ) from .jws import InvalidJws, JwsCompact, JwsJsonFlat, JwsJsonGeneral from .jwt import ( @@ -186,8 +190,12 @@ "SymmetricJwk", "UnsupportedAlg", "UnsupportedEllipticCurve", + "UnsupportedKeyType", "UnsupportedOKPCurve", "X25519", "X448", "secp256k1", + "select_alg_class", + "select_alg_classes", + "to_jwk", ] diff --git a/jwskate/jwa/__init__.py b/jwskate/jwa/__init__.py index f1c52b6..ac8e367 100644 --- a/jwskate/jwa/__init__.py +++ b/jwskate/jwa/__init__.py @@ -68,6 +68,9 @@ RS256, RS384, RS512, + BaseECSignatureAlg, + BaseHMACSigAlg, + BaseRSASigAlg, EdDsa, ) @@ -90,9 +93,12 @@ "BaseAlg", "BaseAsymmetricAlg", "BaseEcdhEs_AesKw", + "BaseECSignatureAlg", + "BaseHMACSigAlg", "BaseKeyManagementAlg", "BasePbes2", "BaseRsaKeyWrap", + "BaseRSASigAlg", "BaseSignatureAlg", "BaseSymmetricAlg", "DirectKeyUse", diff --git a/jwskate/jwa/base.py b/jwskate/jwa/base.py index 13414aa..da5400d 100644 --- a/jwskate/jwa/base.py +++ b/jwskate/jwa/base.py @@ -123,24 +123,6 @@ def check_key(cls, key: Union[Kpriv, Kpub]) -> None: Exception: if the key is not suitable for use with this alg class """ - @classmethod - def supports_key(cls, key: Union[Kpriv, Kpub]) -> bool: - """Return `True` if the given key is suitable for this alg class, or `False` otherwise. - - This is a convenience wrapper around `check_key(key)`. - - Args: - key: the key to check for this alg class - - Returns: - `True` if the key is suitable for this alg class, `False` otherwise - """ - try: - cls.check_key(key) - return True - except Exception: - return False - @contextmanager def private_key_required(self) -> Iterator[Kpriv]: """A context manager that checks if this alg is initialised with a private key. @@ -222,7 +204,7 @@ def check_key(cls, key: bytes) -> None: """ if len(key) * 8 != cls.key_size: raise ValueError( - f"This key size of {len(key) * 8} bits doesn't match the expected keysize of {cls.key_size} bits" + f"This key size of {len(key) * 8} bits doesn't match the expected key size of {cls.key_size} bits" ) @classmethod diff --git a/jwskate/jwa/key_mgmt/ecdh.py b/jwskate/jwa/key_mgmt/ecdh.py index 1c39b7e..c613a03 100644 --- a/jwskate/jwa/key_mgmt/ecdh.py +++ b/jwskate/jwa/key_mgmt/ecdh.py @@ -34,14 +34,14 @@ class EcdhEs( ) @classmethod - def otherinfo(cls, alg: str, apu: bytes, apv: bytes, keysize: int) -> BinaPy: + def otherinfo(cls, alg: str, apu: bytes, apv: bytes, key_size: int) -> BinaPy: """Build the "otherinfo" parameter for Concat KDF Hash. Args: alg: identifier for the encryption alg apu: Agreement PartyUInfo apv: Agreement PartyVInfo - keysize: length of the generated key + key_size: length of the generated key Returns: the "otherinfo" value @@ -49,7 +49,7 @@ def otherinfo(cls, alg: str, apu: bytes, apv: bytes, keysize: int) -> BinaPy: algorithm_id = BinaPy.from_int(len(alg), length=4) + BinaPy(alg) partyuinfo = BinaPy.from_int(len(apu), length=4) + apu partyvinfo = BinaPy.from_int(len(apv), length=4) + apv - supppubinfo = BinaPy.from_int(keysize or keysize, length=4) + supppubinfo = BinaPy.from_int(key_size or key_size, length=4) otherinfo = b"".join((algorithm_id, partyuinfo, partyvinfo, supppubinfo)) return BinaPy(otherinfo) @@ -91,8 +91,8 @@ def ecdh( else: raise ValueError( "Invalid or unsupported private/public key combination for ECDH", - private_key, - public_key, + type(private_key), + type(public_key), ) return BinaPy(shared_key) diff --git a/jwskate/jwa/key_mgmt/rsa.py b/jwskate/jwa/key_mgmt/rsa.py index 438c8cd..d2722d4 100644 --- a/jwskate/jwa/key_mgmt/rsa.py +++ b/jwskate/jwa/key_mgmt/rsa.py @@ -3,15 +3,15 @@ from typing import Any, SupportsBytes, Union from binapy import BinaPy -from cryptography.hazmat.primitives import asymmetric, hashes -from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa from ..base import BaseAsymmetricAlg, BaseKeyManagementAlg class BaseRsaKeyWrap( BaseKeyManagementAlg, - BaseAsymmetricAlg[asymmetric.rsa.RSAPrivateKey, asymmetric.rsa.RSAPublicKey], + BaseAsymmetricAlg[rsa.RSAPrivateKey, rsa.RSAPublicKey], ): """Base class for RSA Key Wrapping algorithms. @@ -24,12 +24,10 @@ class BaseRsaKeyWrap( name: str description: str - private_key_class = asymmetric.rsa.RSAPrivateKey - public_key_class = asymmetric.rsa.RSAPublicKey + private_key_class = rsa.RSAPrivateKey + public_key_class = rsa.RSAPublicKey - def __init__( - self, key: Union[asymmetric.rsa.RSAPublicKey, asymmetric.rsa.RSAPrivateKey] - ): + def __init__(self, key: Union[rsa.RSAPublicKey, rsa.RSAPrivateKey]): self.key = key def wrap_key(self, plainkey: bytes) -> BinaPy: diff --git a/jwskate/jwa/signature/__init__.py b/jwskate/jwa/signature/__init__.py index c6fce2d..5d85561 100644 --- a/jwskate/jwa/signature/__init__.py +++ b/jwskate/jwa/signature/__init__.py @@ -1,11 +1,14 @@ """This module exposes all the Signature algorithms available from `jwskate`.""" -from .ec import ES256, ES256K, ES384, ES512 +from .ec import ES256, ES256K, ES384, ES512, BaseECSignatureAlg from .eddsa import EdDsa -from .hmac import HS256, HS384, HS512 -from .rsa import PS256, PS384, PS512, RS256, RS384, RS512 +from .hmac import HS256, HS384, HS512, BaseHMACSigAlg +from .rsa import PS256, PS384, PS512, RS256, RS384, RS512, BaseRSASigAlg __all__ = [ + "BaseECSignatureAlg", + "BaseHMACSigAlg", + "BaseRSASigAlg", "ES256", "ES256K", "ES384", diff --git a/jwskate/jwa/signature/hmac.py b/jwskate/jwa/signature/hmac.py index 1b2f854..b1199c8 100644 --- a/jwskate/jwa/signature/hmac.py +++ b/jwskate/jwa/signature/hmac.py @@ -12,7 +12,7 @@ class BaseHMACSigAlg(BaseSymmetricAlg, BaseSignatureAlg): """Base class for HMAC signature algorithms.""" mac: Type[hmac.HMAC] = hmac.HMAC - hash_alg: hashes.HashAlgorithm + hashing_alg: hashes.HashAlgorithm min_key_size: int def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy: # noqa: D102 @@ -21,7 +21,7 @@ def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy: # noqa: D102 if self.read_only: raise NotImplementedError - m = self.mac(self.key, self.hash_alg) + m = self.mac(self.key, self.hashing_alg) m.update(data) signature = m.finalize() return BinaPy(signature) @@ -44,7 +44,7 @@ class HS256(BaseHMACSigAlg): # noqa: D415 name = "HS256" description = __doc__ - hash_alg = hashes.SHA256() + hashing_alg = hashes.SHA256() min_key_size = 256 @@ -53,7 +53,7 @@ class HS384(BaseHMACSigAlg): # noqa: D415 name = "HS384" description = __doc__ - hash_alg = hashes.SHA384() + hashing_alg = hashes.SHA384() min_key_size = 384 @@ -62,7 +62,7 @@ class HS512(BaseHMACSigAlg): # noqa: D415 name = "HS512" description = __doc__ - hash_alg = hashes.SHA512() + hashing_alg = hashes.SHA512() min_key_size = 512 @@ -73,4 +73,4 @@ class HS1(BaseHMACSigAlg): # noqa: D415 description = __doc__ read_only = True min_key_size = 160 - hash_alg = hashes.SHA1() + hashing_alg = hashes.SHA1() diff --git a/jwskate/jwe/compact.py b/jwskate/jwe/compact.py index 9b02cbb..c5802b3 100644 --- a/jwskate/jwe/compact.py +++ b/jwskate/jwe/compact.py @@ -12,8 +12,8 @@ Pbes2_HS384_A192KW, Pbes2_HS512_A256KW, ) -from jwskate.jwk import Jwk, SymmetricJwk -from jwskate.jwk.alg import UnsupportedAlg, select_alg +from jwskate.jwk import Jwk, SymmetricJwk, to_jwk +from jwskate.jwk.alg import UnsupportedAlg, select_alg_class from jwskate.token import BaseCompactToken @@ -157,9 +157,10 @@ def encrypt( the generated JweCompact instance """ extra_headers = extra_headers or {} - if not isinstance(jwk, Jwk): - jwk = Jwk(jwk) - alg = select_alg(jwk.alg, alg, jwk.KEY_MANAGEMENT_ALGORITHMS).name + jwk = to_jwk(jwk) + alg = select_alg_class( + jwk.KEY_MANAGEMENT_ALGORITHMS, jwk_alg=jwk.alg, alg=alg + ).name cek_jwk, wrapped_cek, cek_headers = jwk.sender_key( enc=enc, alg=alg, cek=cek, epk=epk, **extra_headers diff --git a/jwskate/jwk/__init__.py b/jwskate/jwk/__init__.py index f9c5291..cc1d124 100644 --- a/jwskate/jwk/__init__.py +++ b/jwskate/jwk/__init__.py @@ -1,7 +1,12 @@ """This module implements [Json Web Key RFC7517](https://tools.ietf.org/html/rfc7517).""" -from .alg import ExpectedAlgRequired, UnsupportedAlg -from .base import InvalidJwk, Jwk +from .alg import ( + ExpectedAlgRequired, + UnsupportedAlg, + select_alg_class, + select_alg_classes, +) +from .base import InvalidJwk, Jwk, UnsupportedKeyType, to_jwk from .ec import ECJwk, UnsupportedEllipticCurve from .jwks import JwkSet from .oct import SymmetricJwk @@ -19,5 +24,9 @@ "SymmetricJwk", "UnsupportedAlg", "UnsupportedEllipticCurve", + "UnsupportedKeyType", "UnsupportedOKPCurve", + "select_alg_class", + "select_alg_classes", + "to_jwk", ] diff --git a/jwskate/jwk/alg.py b/jwskate/jwk/alg.py index db82d4b..dfdd40a 100644 --- a/jwskate/jwk/alg.py +++ b/jwskate/jwk/alg.py @@ -7,7 +7,7 @@ class UnsupportedAlg(ValueError): - """Raised when an UnsupportedAlg is requested.""" + """Raised when a unsupported alg is requested.""" class ExpectedAlgRequired(ValueError): @@ -17,24 +17,27 @@ class ExpectedAlgRequired(ValueError): T = TypeVar("T", bound=Type[BaseAlg]) -def select_alg( - jwk_alg: Optional[str], alg: Optional[str], supported_algs: Mapping[str, T] +def select_alg_class( + supported_algs: Mapping[str, T], + *, + jwk_alg: Optional[str] = None, + alg: Optional[str] = None, ) -> T: - """Internal helper method to choose the appropriate alg to use for cryptographic operations. + """Internal helper method to choose the appropriate alg class to use for cryptographic operations. Given: - - an alg parameter from a JWK - - and/or a user-specified alg - a mapping of supported algs names to wrapper classes + - a preferred alg name (usually the one mentioned in a JWK) + - and/or a user-specified alg this returns the wrapper class to use. This checks the coherency between the user specified `alg` and the `jwk_alg`, and will emit a warning if the user specified alg is different from the `jwk_alg`. Args: + supported_algs: a mapping of supported alg names to alg wrapper jwk_alg: the alg from the JWK, if any alg: a user specified alg - supported_algs: a mapping of supported alg names to alg wrapper Returns: the alg to use @@ -74,31 +77,32 @@ def select_alg( ) -def select_algs( - jwk_alg: Optional[str], - alg: Optional[str], - algs: Optional[Iterable[str]], +def select_alg_classes( supported_algs: Mapping[str, T], + *, + jwk_alg: Optional[str] = None, + alg: Optional[str] = None, + algs: Optional[Iterable[str]] = None, ) -> List[T]: - """Internal helper method to select several appropriate algs to use on cryptographic operations. + """Internal helper method to select several appropriate algs classes to use on cryptographic operations. This method is typically used to get the list of valid algorithms when checking a signature, when several algorithms are allowed. Given: + - a mapping of supported algorithms name to wrapper classes - an alg parameter from a JWK - and/or a user-specified alg - and/or a user specified list of usable algs - - a mapping of supported algorithms name to wrapper classes this returns a list of supported alg wrapper classes that matches what the user specified, or, as default, the alg parameter from the JWK. This checks the coherency between the user specified `alg` and the `jwk_alg`, and will emit a warning if the user specified alg is different from the `jwk_alg`. Args: + supported_algs: a mapping of alg names to alg wrappers jwk_alg: the alg from the JWK, if any alg: a user specified alg to use, if any algs: a user specified list of algs to use, if several are allowed - supported_algs: a mapping of alg names to alg wrappers Returns: a list of possible algs to check diff --git a/jwskate/jwk/base.py b/jwskate/jwk/base.py index 9fa8a59..5f9c712 100644 --- a/jwskate/jwk/base.py +++ b/jwskate/jwk/base.py @@ -2,6 +2,7 @@ from __future__ import annotations +import warnings from dataclasses import dataclass from typing import ( TYPE_CHECKING, @@ -26,6 +27,7 @@ BaseAESEncryptionAlg, BaseAesGcmKeyWrap, BaseAesKeyWrap, + BaseAlg, BaseAsymmetricAlg, BaseEcdhEs_AesKw, BaseKeyManagementAlg, @@ -36,9 +38,8 @@ EcdhEs, ) -from .. import BaseAlg from ..token import BaseJsonDict -from .alg import UnsupportedAlg, select_alg, select_algs +from .alg import ExpectedAlgRequired, UnsupportedAlg, select_alg_class if TYPE_CHECKING: from .jwks import JwkSet # pragma: no cover @@ -165,7 +166,7 @@ def __init__( try: self.cryptography_key = self._to_cryptography_key() except AttributeError as exc: - raise InvalidJwk() from exc + raise InvalidJwk("Invalid JWK parameter", *exc.args) from exc @classmethod def _get_alg_class(cls, alg: str) -> Type[BaseAlg]: @@ -299,6 +300,103 @@ def alg(self) -> Optional[str]: raise TypeError(f"Invalid alg type {type(alg)}", alg) return alg + def signature_class(self, alg: Optional[str] = None) -> Type[BaseSignatureAlg]: + """Return the appropriate signature algorithm class (a `BaseSignatureAlg` subclass) to use with this key. + + If this key doesn't have an `alg` parameter, you must supply one as parameter to this method. + + Args: + alg: the algorithm identifier, if not already present in this Jwk + + Returns: + the appropriate `BaseSignatureAlg` subclass + """ + return select_alg_class(self.SIGNATURE_ALGORITHMS, jwk_alg=self.alg, alg=alg) + + def encryption_class(self, alg: Optional[str] = None) -> Type[BaseAESEncryptionAlg]: + """Return the appropriate encryption algorithm class (a `BaseAESEncryptionAlg` subclass) to use with this key. + + If this key doesn't have an `alg` parameter, you must supply one as parameter to this method. + + Args: + alg: the algorithm identifier, if not already present in this Jwk + + Returns: + the appropriate `BaseAESEncryptionAlg` subclass + """ + return select_alg_class(self.ENCRYPTION_ALGORITHMS, jwk_alg=self.alg, alg=alg) + + def key_management_class( + self, alg: Optional[str] = None + ) -> Type[BaseKeyManagementAlg]: + """Return the appropriate key management algorithm class (a `BaseKeyManagementAlg` subclass) to use with this key. + + If this key doesn't have an `alg` parameter, you must supply one as parameter to this method. + + Args: + alg: the algorithm identifier, if not already present in this Jwk + + Returns: + the appropriate `BaseKeyManagementAlg` subclass + """ + return select_alg_class( + self.KEY_MANAGEMENT_ALGORITHMS, jwk_alg=self.alg, alg=alg + ) + + def signature_wrapper(self, alg: Optional[str] = None) -> BaseSignatureAlg: + """Initialize a key management wrapper (an instance of a `BaseKeyManagementAlg` subclass) with this key. + + If this key doesn't have an `alg` parameter, you must supply one as parameter to this method. + + Args: + alg: the algorithm identifier, if not already present in this Jwk + + Returns: + a `BaseKeyManagementAlg` instance initialized with the current key + """ + alg_class = self.signature_class(alg) + if issubclass(alg_class, BaseSymmetricAlg): + return alg_class(self.key) + elif issubclass(alg_class, BaseAsymmetricAlg): + return alg_class(self.cryptography_key) + raise UnsupportedAlg(alg) # pragma: no cover + + def encryption_wrapper(self, alg: Optional[str] = None) -> BaseAESEncryptionAlg: + """Initialize an encryption wrapper (an instance of a `BaseAESEncryptionAlg` subclass) with this key. + + If this key doesn't have an `alg` parameter, you must supply one as parameter to this method. + + Args: + alg: the algorithm identifier, if not already present in this Jwk + + Returns: + a `BaseAESEncryptionAlg` instance initialized with the current key + """ + alg_class = self.encryption_class(alg) + if issubclass(alg_class, BaseSymmetricAlg): + return alg_class(self.key) + elif issubclass(alg_class, BaseAsymmetricAlg): # pragma: no cover + return alg_class(self.cryptography_key) # pragma: no cover + raise UnsupportedAlg(alg) # pragma: no cover + + def key_management_wrapper(self, alg: Optional[str] = None) -> BaseKeyManagementAlg: + """Initialize a key management wrapper (an instance of a `BaseKeyManagementAlg` subclass) with this key. + + If this key doesn't have an `alg` parameter, you must supply one as parameter to this method. + + Args: + alg: the algorithm identifier, if not already present in this Jwk + + Returns: + a `BaseKeyManagementAlg` instance initialized with the current key + """ + alg_class = self.key_management_class(alg) + if issubclass(alg_class, BaseSymmetricAlg): + return alg_class(self.key) + elif issubclass(alg_class, BaseAsymmetricAlg): + return alg_class(self.cryptography_key) + raise UnsupportedAlg(alg) # pragma: no cover + @property def kid(self) -> Optional[str]: """Return the JWK key ID (kid), if present.""" @@ -321,38 +419,39 @@ def use(self) -> Optional[str]: return self.get("use") @cached_property - def key_ops(self) -> List[str]: + def key_ops(self) -> Tuple[str, ...]: """Return the key operations. If no `alg` parameter is present, this returns the `key_ops` parameter from this JWK. If an `alg` parameter is present, the key operations are deduced from this alg. To check for the presence of the `key_ops` parameter, use `jwk.get('key_ops')`. """ + key_ops: Tuple[str, ...] if self.use == "sig": if self.is_symmetric: - key_ops = ["sign", "verify"] + key_ops = ("sign", "verify") elif self.is_private: - key_ops = ["sign"] + key_ops = ("sign",) else: - key_ops = ["verify"] + key_ops = ("verify",) elif self.use == "enc": if self.is_symmetric: if self.alg: alg_class = self._get_alg_class(self.alg) if issubclass(alg_class, BaseKeyManagementAlg): - key_ops = ["wrapKey", "unwrapKey"] + key_ops = ("wrapKey", "unwrapKey") elif issubclass(alg_class, BaseAESEncryptionAlg): - key_ops = ["encrypt", "decrypt"] + key_ops = ("encrypt", "decrypt") else: - key_ops = ["wrapKey", "unwrapKey", "encrypt", "decrypt"] + key_ops = ("wrapKey", "unwrapKey", "encrypt", "decrypt") elif self.is_private: - key_ops = ["unwrapKey"] + key_ops = ("unwrapKey",) else: - key_ops = ["wrapKey"] + key_ops = ("wrapKey",) else: - key_ops = self.get("key_ops", []) + key_ops = self.get("key_ops", ()) - return key_ops + return tuple(key_ops) def _validate(self) -> None: """Internal method used to validate a Jwk. @@ -499,14 +598,7 @@ def sign( Returns: the generated signature """ - sigalg = select_alg(self.alg, alg, self.SIGNATURE_ALGORITHMS) - wrapper: BaseSignatureAlg - if issubclass(sigalg, BaseAsymmetricAlg): - wrapper = sigalg(self.cryptography_key) - - elif issubclass(sigalg, BaseSymmetricAlg): - wrapper = sigalg(self.key) - + wrapper = self.signature_wrapper(alg) signature = wrapper.sign(data) return BinaPy(signature) @@ -529,14 +621,18 @@ def verify( Returns: `True` if the signature matches, `False` otherwise """ - wrapper: BaseSignatureAlg - for sigalg in select_algs(self.alg, alg, algs, self.SIGNATURE_ALGORITHMS): - if issubclass(sigalg, BaseAsymmetricAlg): - key = self.public_jwk().cryptography_key - wrapper = sigalg(key) - elif issubclass(sigalg, BaseSymmetricAlg): - key = self.key - wrapper = sigalg(key) + if not self.is_symmetric and self.is_private: + warnings.warn( + "You are trying to validate a signature with a private key. " + "Signature should always be verified with a public key." + ) + public_jwk = self.public_jwk() + else: + public_jwk = self + if algs is None and alg: + algs = [alg] + for alg in algs or (None,): + wrapper = public_jwk.signature_wrapper(alg) if wrapper.verify(data, signature): return True @@ -625,77 +721,84 @@ def sender_key( **headers: additional headers to include for the CEK derivation Returns: - Tuple[Jwk,BinaPy,Mapping[str,Any]]: a tuple (cek, wrapped_cek, additional_headers_map) + a tuple (cek, wrapped_cek, additional_headers_map) Raises: UnsupportedAlg: if the requested alg identifier is not supported """ from jwskate import SymmetricJwk - keyalg = select_alg(self.alg, alg, self.KEY_MANAGEMENT_ALGORITHMS) - encalg = select_alg(None, enc, SymmetricJwk.ENCRYPTION_ALGORITHMS) + if not self.is_symmetric and self.is_private: + warnings.warn( + "You are using a private key for sender key wrapping. Key wrapping should always be done using the recipient public key." + ) + key_alg_wrapper = self.public_jwk().key_management_wrapper(alg) + else: + key_alg_wrapper = self.key_management_wrapper(alg) + + enc_alg_class = select_alg_class(SymmetricJwk.ENCRYPTION_ALGORITHMS, alg=enc) cek_headers: Dict[str, Any] = {} - if issubclass(keyalg, BaseRsaKeyWrap): - rsa: BaseRsaKeyWrap = keyalg(self.public_jwk().cryptography_key) + if isinstance(key_alg_wrapper, BaseRsaKeyWrap): if cek: - encalg.check_key(cek) + enc_alg_class.check_key(cek) else: - cek = encalg.generate_key() + cek = enc_alg_class.generate_key() assert cek - wrapped_cek = rsa.wrap_key(cek) + wrapped_cek = key_alg_wrapper.wrap_key(cek) - elif issubclass(keyalg, EcdhEs): - ecdh: EcdhEs = keyalg(self.public_jwk().cryptography_key) - epk = epk or Jwk.from_cryptography_key(ecdh.generate_ephemeral_key()) + elif isinstance(key_alg_wrapper, EcdhEs): + epk = epk or Jwk.from_cryptography_key( + key_alg_wrapper.generate_ephemeral_key() + ) cek_headers = {"epk": epk.public_jwk()} - if isinstance(ecdh, BaseEcdhEs_AesKw): + if isinstance(key_alg_wrapper, BaseEcdhEs_AesKw): if cek: - encalg.check_key(cek) + enc_alg_class.check_key(cek) else: - cek = encalg.generate_key() + cek = enc_alg_class.generate_key() assert cek - wrapped_cek = ecdh.wrap_key_with_epk( - cek, epk.cryptography_key, alg=keyalg.name, **headers + wrapped_cek = key_alg_wrapper.wrap_key_with_epk( + cek, epk.cryptography_key, alg=key_alg_wrapper.name, **headers ) else: - cek = ecdh.sender_key( + cek = key_alg_wrapper.sender_key( epk.cryptography_key, - alg=encalg.name, - key_size=encalg.key_size, + alg=enc_alg_class.name, + key_size=enc_alg_class.key_size, **headers, ) wrapped_cek = BinaPy(b"") - elif issubclass(keyalg, BaseAesKeyWrap): - aes: BaseAesKeyWrap = keyalg(self.cryptography_key) + + elif isinstance(key_alg_wrapper, BaseAesKeyWrap): if cek: - encalg.check_key(cek) + enc_alg_class.check_key(cek) else: - cek = encalg.generate_key() + cek = enc_alg_class.generate_key() assert cek - wrapped_cek = aes.wrap_key(cek) + wrapped_cek = key_alg_wrapper.wrap_key(cek) - elif issubclass(keyalg, BaseAesGcmKeyWrap): - aesgcm: BaseAesGcmKeyWrap = keyalg(self.cryptography_key) + elif isinstance(key_alg_wrapper, BaseAesGcmKeyWrap): if cek: - encalg.check_key(cek) + enc_alg_class.check_key(cek) else: - cek = encalg.generate_key() + cek = enc_alg_class.generate_key() assert cek - iv = aesgcm.generate_iv() - wrapped_cek, tag = aesgcm.wrap_key(cek, iv=iv) + iv = key_alg_wrapper.generate_iv() + wrapped_cek, tag = key_alg_wrapper.wrap_key(cek, iv=iv) cek_headers = { "iv": iv.to("b64u").ascii(), "tag": tag.to("b64u").ascii(), } - elif issubclass(keyalg, DirectKeyUse): - dir: DirectKeyUse = keyalg(self.key) - cek = dir.direct_key(encalg) + elif isinstance(key_alg_wrapper, DirectKeyUse): + cek = key_alg_wrapper.direct_key(enc_alg_class) wrapped_cek = BinaPy(b"") else: - raise UnsupportedAlg(f"Unsupported Key Management Alg {keyalg}") + raise UnsupportedAlg( + f"Unsupported Key Management Alg {key_alg_wrapper}" + ) # pragma: no cover return SymmetricJwk.from_bytes(cek), wrapped_cek, cek_headers @@ -723,15 +826,18 @@ def recipient_key( """ from jwskate import SymmetricJwk - keyalg = select_alg(self.alg, alg, self.KEY_MANAGEMENT_ALGORITHMS) - encalg = select_alg(None, enc, SymmetricJwk.ENCRYPTION_ALGORITHMS) + if not self.is_symmetric and not self.is_private: + warnings.warn( + "You are using a public key for recipient key unwrapping. Key wrapping should always be done using the recipient private key." + ) + + key_alg_wrapper = self.key_management_wrapper(alg) + enc_alg_class = select_alg_class(SymmetricJwk.ENCRYPTION_ALGORITHMS, alg=enc) - if issubclass(keyalg, BaseRsaKeyWrap): - rsa = keyalg(self.cryptography_key) - cek = rsa.unwrap_key(wrapped_cek) + if isinstance(key_alg_wrapper, BaseRsaKeyWrap): + cek = key_alg_wrapper.unwrap_key(wrapped_cek) - elif issubclass(keyalg, EcdhEs): - ecdh = keyalg(self.cryptography_key) + elif isinstance(key_alg_wrapper, EcdhEs): epk = headers.get("epk") if epk is None: raise ValueError("No EPK in the headers!") @@ -739,20 +845,22 @@ def recipient_key( if epk_jwk.is_private: raise ValueError("The EPK present in the header is private.") epk = epk_jwk.cryptography_key - encalg = select_alg(None, enc, SymmetricJwk.ENCRYPTION_ALGORITHMS) - if isinstance(ecdh, BaseEcdhEs_AesKw): - cek = ecdh.unwrap_key_with_epk(wrapped_cek, epk, alg=keyalg.name) + if isinstance(key_alg_wrapper, BaseEcdhEs_AesKw): + cek = key_alg_wrapper.unwrap_key_with_epk( + wrapped_cek, epk, alg=key_alg_wrapper.name + ) else: - cek = ecdh.recipient_key( - epk, alg=encalg.name, key_size=encalg.key_size, **headers + cek = key_alg_wrapper.recipient_key( + epk, + alg=enc_alg_class.name, + key_size=enc_alg_class.key_size, + **headers, ) - elif issubclass(keyalg, BaseAesKeyWrap): - aes = keyalg(self.cryptography_key) - cek = aes.unwrap_key(wrapped_cek) + elif isinstance(key_alg_wrapper, BaseAesKeyWrap): + cek = key_alg_wrapper.unwrap_key(wrapped_cek) - elif issubclass(keyalg, BaseAesGcmKeyWrap): - aesgcm = keyalg(self.cryptography_key) + elif isinstance(key_alg_wrapper, BaseAesGcmKeyWrap): iv = headers.get("iv") if iv is None: raise ValueError("No 'iv' in headers!") @@ -761,13 +869,15 @@ def recipient_key( if tag is None: raise ValueError("No 'tag' in headers!") tag = BinaPy(tag).decode_from("b64u") - cek = aesgcm.unwrap_key(wrapped_cek, tag=tag, iv=iv) + cek = key_alg_wrapper.unwrap_key(wrapped_cek, tag=tag, iv=iv) + + elif isinstance(key_alg_wrapper, DirectKeyUse): + cek = key_alg_wrapper.direct_key(enc_alg_class) - elif issubclass(keyalg, DirectKeyUse): - dir_ = keyalg(self.key) - cek = dir_.direct_key(encalg) else: - raise UnsupportedAlg(f"Unsupported Key Management Alg {keyalg}") + raise UnsupportedAlg( + f"Unsupported Key Management Alg {key_alg_wrapper}" + ) # pragma: no cover return SymmetricJwk.from_bytes(cek) @@ -815,17 +925,14 @@ def from_pem_key( Args: data: the PEM encoded data to load - password: the password to decrypt the PEM, if required + password: the password to decrypt the PEM, if required. Should be bytes. If it is a string, it will be encoded with UTF-8. **kwargs: additional members to include in the Jwk (e.g. kid, use) Returns: a Jwk instance from the loaded key """ - if isinstance(data, str): - data = data.encode() - - if isinstance(password, str): - password = password.encode() + data = data.encode() if isinstance(data, str) else data + password = password.encode("UTF-8") if isinstance(password, str) else password try: cryptography_key = serialization.load_pem_private_key(data, password) @@ -851,13 +958,14 @@ def to_pem(self, password: Union[bytes, str, None] = None) -> bytes: accepted, and will be encoded to `bytes` using UTF-8 before it is used as encryption key. Args: - password: password to use to encrypt the PEM + password: password to use to encrypt the PEM. Should be bytes. If it is a string, it will be encoded with UTF-8. Returns: the PEM serialized key """ - if password is not None and not isinstance(password, bytes): - password = str(password).encode("UTF-8") + password = ( + str(password).encode("UTF-8") if isinstance(password, str) else password + ) if self.is_private: encryption: serialization.KeySerializationEncryption @@ -928,7 +1036,7 @@ def generate_for_alg(cls, alg: str, **kwargs: Any) -> Jwk: alg_class: Optional[Type[BaseAlg]] try: alg_class = jwk_class._get_alg_class(alg) - if isinstance(jwk_class, BaseAESEncryptionAlg): + if issubclass(jwk_class, BaseAESEncryptionAlg): kwargs.setdefault("key_size", alg_class.key_size) return jwk_class.generate(alg=alg, **kwargs) @@ -990,7 +1098,9 @@ def with_usage_parameters( alg = alg or self.alg if not alg: - raise ValueError("An algorithm is required to set the usage parameters") + raise ExpectedAlgRequired( + "An algorithm is required to set the usage parameters" + ) self._get_alg_class(alg) # raises an exception if alg is not supported @@ -1007,7 +1117,7 @@ def with_usage_parameters( def minimize(self) -> Jwk: """Strips out any optional or non-standard parameter from that key. - This will remove `alg`, `use`, `key_ops`, optional parameters from RSA keys, and unknown + This will remove `alg`, `use`, `key_ops`, optional parameters from RSA keys, and other unknown parameters. """ jwk = self.copy() @@ -1017,3 +1127,76 @@ def minimize(self) -> Jwk: del jwk[key] return jwk + + def check( + self, + *, + is_private: Optional[bool] = None, + is_symmetric: Optional[bool] = None, + kty: Optional[str] = None, + ) -> Jwk: + """Check this key for type, privateness and/or symmetricness. Raise a ValueError if it not as expected. + + Args: + is_private: if `True`, check if the key is private, if `False`, check if it is public, if `None`, do nothing + is_symmetric: if `True`, check if the key is symmetric, if `False`, check if it is asymmetric, if `None`, do nothing + kty: the expected key type, if any + + Returns: + this key, if all checks passed + + Raises: + ValueError: if any check fails + """ + if is_private is not None: + if is_private is True and self.is_private is False: + raise ValueError("This key is public while a private key is expected.") + elif is_private is False and self.is_private is True: + raise ValueError("This key is private while a public key is expected.") + + if is_symmetric is not None: + if is_symmetric is True and self.is_symmetric is False: + raise ValueError( + "This key is asymmetric while a symmetric key is expected." + ) + if is_symmetric is False and self.is_symmetric is True: + raise ValueError( + "This key is symmetric while an asymmetric key is expected." + ) + + if kty is not None: + if self.kty != kty: + raise ValueError( + f"This key has kty={self.kty} while a kty={kty} is expected." + ) + + return self + + +def to_jwk( + key: Any, + *, + kty: Optional[str] = None, + is_private: Optional[bool] = None, + is_symmetric: Optional[bool] = None, +) -> Jwk: + """Convert any supported kind of key to a Jwk, and optionally check if that key is private or symmetric. + + The key can be any type supported by Jwk: + - a `cryptography` key instance + - a bytes, to initialize a symmetric key + - a JWK, as a dict or as a JSON formatted string + - an existing Jwk instance + If the supplied param is already a Jwk, it is left untouched. + + Args: + key: the key material + kty: the expected key type + is_private: if `True`, check if the key is private, if `False`, check if it is public, if `None`, do nothing + is_symmetric: if `True`, check if the key is symmetric, if `False`, check if it is asymmetric, if `None`, do nothing + + Returns: + a Jwk key + """ + jwk = key if isinstance(key, Jwk) else Jwk(key) + return jwk.check(kty=kty, is_private=is_private, is_symmetric=is_symmetric) diff --git a/jwskate/jwk/ec.py b/jwskate/jwk/ec.py index d10c1e2..5779b74 100644 --- a/jwskate/jwk/ec.py +++ b/jwskate/jwk/ec.py @@ -7,7 +7,7 @@ from backports.cached_property import cached_property from binapy import BinaPy -from cryptography.hazmat.primitives import asymmetric, serialization +from cryptography.hazmat.primitives import asymmetric from cryptography.hazmat.primitives.asymmetric import ec from jwskate.jwa import ( @@ -73,8 +73,7 @@ def is_private(self) -> bool: # noqa: D102 return "d" in self def _validate(self) -> None: - if not isinstance(self.crv, str) or self.crv not in self.CURVES: - raise UnsupportedEllipticCurve(self.crv) + self.get_curve(self.crv) super()._validate() @classmethod diff --git a/jwskate/jwk/jwks.py b/jwskate/jwk/jwks.py index 98ed403..6c653d2 100644 --- a/jwskate/jwk/jwks.py +++ b/jwskate/jwk/jwks.py @@ -3,8 +3,7 @@ from typing import Any, Dict, Iterable, List, Optional, Union from ..token import BaseJsonDict -from .alg import UnsupportedAlg -from .base import Jwk +from .base import Jwk, to_jwk class JwkSet(BaseJsonDict): @@ -94,8 +93,7 @@ def add_jwk( Returns: the kid from the added Jwk (it may be generated if no kid is provided) """ - if not isinstance(jwk, Jwk): - jwk = Jwk(jwk) + jwk = to_jwk(jwk) if "keys" not in self: self["keys"] = [] @@ -170,39 +168,25 @@ def verify( Returns: `True` if the signature validates with any of the tried keys, `False` otherwise """ + if not alg and not algs: + raise ValueError("Please provide either 'alg' or 'algs' parameter") + # if a kid is provided, try only the key matching `kid` if kid is not None: jwk = self.get_jwk_by_kid(kid) return jwk.verify(data, signature, alg=alg, algs=algs) - # if one or several alg are provided, try only the keys that are compatible with one of the provided alg(s) - if alg: - for jwk in self.jwks: - if jwk.get("alg") == alg: - if jwk.verify(data, signature, alg=alg): - return True - - if algs: - for jwk in self.jwks: - alg = jwk.get("alg") - if alg is not None and alg in algs: - if jwk.verify(data, signature, algs=algs): - return True + if algs is None: + if alg is not None: + algs = (alg,) + else: + algs = list(algs) - # if no kid and no alg are provided, try first the keys flagged for signature verification (`"use": "verify"`) - for jwk in self.jwks: - if jwk.get("use") == "verify": - if jwk.verify(data, signature, alg=alg): - return True - - # then with the keys that have no defined `use` - for jwk in self.jwks: - if jwk.get("use") is None and jwk.get("alg") is not None: - try: - if jwk.verify(data, signature): + for jwk in self.verification_keys(): + for alg in algs or (None,): + if alg in jwk.supported_signing_algorithms(): + if jwk.verify(data, signature, alg=alg): return True - except UnsupportedAlg: - continue # no key matches, so consider the signature invalid return False diff --git a/jwskate/jwk/oct.py b/jwskate/jwk/oct.py index d9af024..b110b0b 100644 --- a/jwskate/jwk/oct.py +++ b/jwskate/jwk/oct.py @@ -23,10 +23,11 @@ HS384, HS512, BaseAESEncryptionAlg, + BaseAesKeyWrap, + BaseHMACSigAlg, DirectKeyUse, ) -from .alg import select_alg from .base import Jwk, JwkParameter @@ -98,7 +99,7 @@ def from_bytes(cls, k: Union[bytes, str], **params: Any) -> SymmetricJwk: @classmethod def from_cryptography_key( - cls, cryptography_key: Any, **kwargs: Any + cls, cryptography_key: Any, **params: Any ) -> SymmetricJwk: """Alias for `from_bytes()` since symmetric keys are simply bytes. @@ -109,14 +110,14 @@ def from_cryptography_key( Returns: the resulting SymmetricJwk """ - return cls.from_bytes(cryptography_key, **kwargs) + return cls.from_bytes(cryptography_key, **params) @classmethod - def generate(cls, key_size: int = 128, **params: str) -> SymmetricJwk: + def generate(cls, key_size: int = 128, **params: Any) -> SymmetricJwk: """Generate a random SymmetricJwk, with a given key size. Args: - key_size: the size of the generated key, in bits + key_size: size of the generated key, in bits **params: additional members to include in the Jwk Returns: @@ -126,26 +127,25 @@ def generate(cls, key_size: int = 128, **params: str) -> SymmetricJwk: return cls.from_bytes(key, **params) @classmethod - def generate_for_alg(cls, alg: str, **params: str) -> SymmetricJwk: + def generate_for_alg(cls, alg: str, **params: Any) -> SymmetricJwk: """Generate a SymmetricJwk that is suitable for use with the given alg. Args: - alg: the signing algorithm to use this key with + alg: the algorithm identifier **params: additional members to include in the Jwk Returns: - the resulting Jwk + the generated `Jwk` Raises: - ValueError: if the provided `alg` is not supported + UnsupportedAlg: if the provided `alg` is not supported """ - if alg in cls.SIGNATURE_ALGORITHMS: - sigalg = cls.SIGNATURE_ALGORITHMS[alg] - return cls.generate(sigalg.min_key_size, alg=alg, **params) - if alg in cls.ENCRYPTION_ALGORITHMS: - encalg = cls.ENCRYPTION_ALGORITHMS[alg] - return cls.generate(encalg.key_size, alg=alg, **params) - raise ValueError("Unsupported alg", alg) + alg_class = cls._get_alg_class(alg) + if issubclass(alg_class, BaseHMACSigAlg): + return cls.generate(key_size=alg_class.min_key_size, alg=alg, **params) + elif issubclass(alg_class, (BaseAESEncryptionAlg, BaseAesKeyWrap)): + return cls.generate(key_size=alg_class.key_size, alg=alg, **params) + return cls.generate(alg=alg, **params) def thumbprint(self, hashalg: str = "SHA256") -> str: """Return the key thumbprint as specified by RFC 7638. @@ -208,12 +208,10 @@ def encrypt( Returns: a (ciphertext, authentication_tag, iv) tuple """ - encalg = select_alg(self.alg, alg, self.ENCRYPTION_ALGORITHMS) - + wrapper = self.encryption_wrapper(alg) if iv is None: - iv = encalg.generate_iv() + iv = wrapper.generate_iv() - wrapper: BaseAESEncryptionAlg = encalg(self.cryptography_key) ciphertext, tag = wrapper.encrypt(plaintext, iv=iv, aad=aad) return ciphertext, BinaPy(iv), tag @@ -247,18 +245,16 @@ def decrypt( Returns: the decrypted clear-text """ - if aad is None: # pragma: no branch - aad = b"" - elif not isinstance(aad, bytes): + aad = b"" if aad is None else aad + if not isinstance(aad, bytes): aad = bytes(aad) if not isinstance(iv, bytes): iv = bytes(iv) if not isinstance(tag, bytes): tag = bytes(tag) - encalg = select_alg(self.alg, alg, self.ENCRYPTION_ALGORITHMS) - decryptor: BaseAESEncryptionAlg = encalg(self.cryptography_key) - plaintext: bytes = decryptor.decrypt(ciphertext, auth_tag=tag, iv=iv, aad=aad) + wrapper = self.encryption_wrapper(alg) + plaintext: bytes = wrapper.decrypt(ciphertext, auth_tag=tag, iv=iv, aad=aad) return BinaPy(plaintext) diff --git a/jwskate/jwk/okp.py b/jwskate/jwk/okp.py index 51f8fa6..e19dd41 100644 --- a/jwskate/jwk/okp.py +++ b/jwskate/jwk/okp.py @@ -9,17 +9,22 @@ from backports.cached_property import cached_property from binapy import BinaPy +from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed448, ed25519, x448, x25519 -from cryptography.hazmat.primitives.serialization import ( - Encoding, - NoEncryption, - PrivateFormat, - PublicFormat, -) -from jwskate.jwa import X448, X25519, Ed448, Ed25519, EdDsa, OKPCurve +from jwskate.jwa import ( + X448, + X25519, + EcdhEs, + EcdhEs_A128KW, + EcdhEs_A192KW, + EcdhEs_A256KW, + Ed448, + Ed25519, + EdDsa, + OKPCurve, +) -from .. import EcdhEs, EcdhEs_A128KW, EcdhEs_A192KW, EcdhEs_A256KW from .alg import UnsupportedAlg from .base import Jwk, JwkParameter @@ -122,9 +127,88 @@ def private_key(self) -> bytes: """ return BinaPy(self.d).decode_from("b64u") + @classmethod + def from_bytes( + cls, + private_key: bytes, + crv: Optional[str] = None, + use: Optional[str] = None, + **kwargs: Any, + ) -> OKPJwk: + """Initialize an `OKPJwk` from its private key. + + The public key will be automatically derived from the supplied private key, + + The appropriate curve will be guessed based on the key length or supplied `crv`/`use` hints: + - 56 bytes will use X448 + - 57 bytes will use Ed448 + - 32 bytes will use Ed25519 or X25519. Since there is no way to guess which one you want, it needs an hint with either a `crv` or `use` parameter. + + Args: + private_key: the 32, 56 or 57 bytes private key, as raw bytes + crv: the curve to use + use: the key usage + **kwargs: additional members to include in the Jwk + + Returns: + the matching OKPJwk + """ + if crv and use: + if (crv in ("Ed25519", "Ed448") and use != "sig") or ( + crv in ("X25519", "X448") and use != "enc" + ): + raise ValueError( + f"Inconsistent `crv={crv}` and `use={use}` parameters." + ) + elif crv: + if crv in ("Ed25519", "Ed448"): + use = "sig" + elif crv in ("X25519", "X448"): + use = "enc" + else: + raise UnsupportedOKPCurve(crv) + elif use: + if use not in ("sig", "enc"): + raise ValueError(f"Invalid `use={use}` parameter, need 'sig' or 'enc'.") + + cryptography_key: Any + if len(private_key) == 32: + if use == "sig": + cryptography_key = ed25519.Ed25519PrivateKey.from_private_bytes( + private_key + ) + elif use == "enc": + cryptography_key = x25519.X25519PrivateKey.from_private_bytes( + private_key + ) + else: + raise ValueError( + "You need to specify either crv={'Ed25519', 'X25519'} or use={'sig', 'enc'} when providing a 32 bytes private key." + ) + elif len(private_key) == 56: + cryptography_key = x448.X448PrivateKey.from_private_bytes(private_key) + if use and use != "enc": + raise ValueError( + f"Invalid `use={use}` parameter. Keys of length 56 bytes are for curve X448." + ) + use = "enc" + elif len(private_key) == 57: + cryptography_key = ed448.Ed448PrivateKey.from_private_bytes(private_key) + if use and use != "sig": + raise ValueError( + f"Invalid `use={use}` parameter. Keys of length 57 bytes are for curve Ed448." + ) + use = "sig" + else: + raise ValueError( + "Invalid private key. It must be bytes of length 32, 56 or 57." + ) + + return OKPJwk.from_cryptography_key(cryptography_key, use=use, **kwargs) + @classmethod def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: - """Initialize a OKPJwk from a `cryptography` key. + """Initialize an `OKPJwk` from a `cryptography` key. Args: cryptography_key: a `cryptography` key @@ -135,12 +219,13 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: """ if isinstance(cryptography_key, ed25519.Ed25519PrivateKey): priv = cryptography_key.private_bytes( - encoding=Encoding.Raw, - format=PrivateFormat.Raw, - encryption_algorithm=NoEncryption(), + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), ) pub = cryptography_key.public_key().public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.private( crv="Ed25519", @@ -149,7 +234,8 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: ) elif isinstance(cryptography_key, ed25519.Ed25519PublicKey): pub = cryptography_key.public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.public( crv="Ed25519", @@ -157,12 +243,13 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: ) elif isinstance(cryptography_key, ed448.Ed448PrivateKey): priv = cryptography_key.private_bytes( - encoding=Encoding.Raw, - format=PrivateFormat.Raw, - encryption_algorithm=NoEncryption(), + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), ) pub = cryptography_key.public_key().public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.private( crv="Ed448", @@ -171,17 +258,19 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: ) elif isinstance(cryptography_key, ed448.Ed448PublicKey): pub = cryptography_key.public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.public(crv="Ed448", x=pub) elif isinstance(cryptography_key, x25519.X25519PrivateKey): priv = cryptography_key.private_bytes( - encoding=Encoding.Raw, - format=PrivateFormat.Raw, - encryption_algorithm=NoEncryption(), + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), ) pub = cryptography_key.public_key().public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.private( crv="X25519", @@ -190,17 +279,19 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: ) elif isinstance(cryptography_key, x25519.X25519PublicKey): pub = cryptography_key.public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.public(crv="X25519", x=pub) elif isinstance(cryptography_key, x448.X448PrivateKey): priv = cryptography_key.private_bytes( - encoding=Encoding.Raw, - format=PrivateFormat.Raw, - encryption_algorithm=NoEncryption(), + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), ) pub = cryptography_key.public_key().public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.private( crv="X448", @@ -209,7 +300,8 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: ) elif isinstance(cryptography_key, x448.X448PublicKey): pub = cryptography_key.public_bytes( - encoding=Encoding.Raw, format=PublicFormat.Raw + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, ) return cls.public(crv="X448", x=pub) else: @@ -254,7 +346,7 @@ def _to_cryptography_key(self) -> Any: else: return x448.X448PublicKey.from_public_bytes(self.public_key) else: - raise UnsupportedOKPCurve(self.curve) + raise UnsupportedOKPCurve(self.curve) # pragma: no cover @classmethod def public(cls, crv: str, x: bytes, **params: Any) -> OKPJwk: @@ -328,3 +420,15 @@ def generate( x, d = curve.generate() return cls.private(crv=curve.name, x=x, d=d, alg=alg, **params) + + @cached_property + def use(self) -> Optional[str]: + """Return the key use. + + For OKP keys, this can be directly deduced from the curve. + """ + if self.curve in (Ed25519, Ed448): + return "sig" + elif self.curve in (X25519, X448): + return "enc" + return None # pragma: no cover diff --git a/jwskate/jws/compact.py b/jwskate/jws/compact.py index 34f8986..abcb7e6 100644 --- a/jwskate/jws/compact.py +++ b/jwskate/jws/compact.py @@ -7,7 +7,7 @@ from backports.cached_property import cached_property from binapy import BinaPy -from jwskate.jwk.base import Jwk +from jwskate.jwk.base import Jwk, to_jwk from jwskate.token import BaseCompactToken from .signature import JwsSignature @@ -77,7 +77,7 @@ def sign( Returns: the resulting token """ - jwk = Jwk(jwk) + jwk = to_jwk(jwk) if not isinstance(payload, bytes): payload = bytes(payload) @@ -146,7 +146,7 @@ def verify_signature( Returns: `True` if the signature matches, `False` otherwise """ - jwk = Jwk(jwk) + jwk = to_jwk(jwk) return jwk.verify(self.signed_part, self.signature, alg=alg, algs=algs) def flat_json(self, unprotected_header: Any = None) -> JwsJsonFlat: diff --git a/jwskate/jws/json.py b/jwskate/jws/json.py index d3a012d..53b4228 100644 --- a/jwskate/jws/json.py +++ b/jwskate/jws/json.py @@ -8,8 +8,8 @@ from binapy import BinaPy from jwskate.jwk.base import Jwk +from jwskate.token import BaseJsonDict -from ..token import BaseJsonDict from .compact import JwsCompact from .signature import JwsSignature diff --git a/jwskate/jws/signature.py b/jwskate/jws/signature.py index df11fa1..66dbc3e 100644 --- a/jwskate/jws/signature.py +++ b/jwskate/jws/signature.py @@ -7,7 +7,7 @@ from backports.cached_property import cached_property from binapy import BinaPy -from jwskate.jwk.base import Jwk +from jwskate.jwk import Jwk, to_jwk from jwskate.token import BaseJsonDict S = TypeVar("S", bound="JwsSignature") @@ -114,7 +114,7 @@ def sign( Returns: The generated signature. """ - jwk = Jwk(jwk) + jwk = to_jwk(jwk) headers = dict(extra_protected_headers or {}, alg=alg) kid = jwk.get("kid") @@ -169,6 +169,6 @@ def verify( Returns: `True` if the signature is verifier, `False` otherwise """ - jwk = Jwk(jwk) + jwk = to_jwk(jwk) signed_part = self.assemble_signed_part(self.protected, payload) return jwk.verify(signed_part, self.signature, alg=alg, algs=algs) diff --git a/jwskate/jwt/base.py b/jwskate/jwt/base.py index 1c63e2e..8cede98 100644 --- a/jwskate/jwt/base.py +++ b/jwskate/jwt/base.py @@ -8,9 +8,8 @@ from binapy import BinaPy from jwskate.jwe import JweCompact -from jwskate.jwk import Jwk - -from ..token import BaseCompactToken +from jwskate.jwk import Jwk, to_jwk +from jwskate.token import BaseCompactToken if TYPE_CHECKING: from jwskate import SignedJwt # pragma: no cover @@ -69,7 +68,7 @@ def sign( """ from .signed import SignedJwt - jwk = Jwk(jwk) + jwk = to_jwk(jwk) alg = alg or jwk.get("alg") @@ -159,10 +158,10 @@ def decrypt_nested_jwt( ) -> Jwt: """Convenience method to decrypt a nested JWT. - It will return a Jwt instance. + It will return a [Jwt] instance. Args: - jwt: the JWE containing a nested Token + jwe: the JWE containing a nested Token jwk: the decryption key Returns: diff --git a/jwskate/jwt/signed.py b/jwskate/jwt/signed.py index 6af7898..50dbd8b 100644 --- a/jwskate/jwt/signed.py +++ b/jwskate/jwt/signed.py @@ -6,7 +6,7 @@ from backports.cached_property import cached_property from binapy import BinaPy -from jwskate import Jwk +from jwskate.jwk import Jwk, to_jwk from .base import InvalidJwt, Jwt @@ -92,7 +92,7 @@ def verify_signature( Returns: `True` if the token signature is verified, `False` otherwise """ - jwk = Jwk(jwk) + jwk = to_jwk(jwk) return jwk.verify( data=self.signed_part, signature=self.signature, alg=alg, algs=algs diff --git a/jwskate/jwt/signer.py b/jwskate/jwt/signer.py index 1899026..6fdd681 100644 --- a/jwskate/jwt/signer.py +++ b/jwskate/jwt/signer.py @@ -3,7 +3,7 @@ import uuid from typing import Any, Dict, Iterable, Optional, Union -from jwskate import Jwk +from jwskate.jwk import Jwk from .base import Jwt from .signed import SignedJwt diff --git a/poetry.lock b/poetry.lock index cd330c8..e6a0ff1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -10,10 +10,10 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] -name = "backports.cached-property" +name = "backports-cached-property" version = "1.0.2" description = "cached_property() - computed once per instance, cached as attribute" category = "main" @@ -98,7 +98,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" @@ -114,11 +114,11 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" @@ -136,7 +136,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "38.0.1" +version = "38.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -154,7 +154,7 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] -name = "Deprecated" +name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "dev" @@ -175,6 +175,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "exceptiongroup" +version = "1.0.1" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "filelock" version = "3.8.0" @@ -214,7 +225,7 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "0.22.2" +version = "0.23.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." category = "dev" optional = false @@ -228,7 +239,7 @@ async = ["aiofiles (>=0.7,<1.0)"] [[package]] name = "identify" -version = "2.5.6" +version = "2.5.8" description = "File identification library for Python" category = "dev" optional = false @@ -280,12 +291,12 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] -name = "Jinja2" +name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "dev" @@ -323,7 +334,7 @@ six = "*" tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] -name = "Markdown" +name = "markdown" version = "3.3.7" description = "Python implementation of Markdown." category = "dev" @@ -337,7 +348,7 @@ importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} testing = ["coverage", "pyyaml"] [[package]] -name = "MarkupSafe" +name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" @@ -354,7 +365,7 @@ python-versions = ">=3.6" [[package]] name = "mkdocs" -version = "1.4.0" +version = "1.4.2" description = "Project documentation with Markdown." category = "dev" optional = false @@ -362,19 +373,21 @@ python-versions = ">=3.7" [package.dependencies] click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.11.1" -Markdown = ">=3.2.1,<3.4" +jinja2 = ">=2.11.1" +markdown = ">=3.2.1,<3.4" mergedeep = ">=1.3.4" packaging = ">=20.5" -PyYAML = ">=5.1" +pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -390,7 +403,7 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-include-markdown-plugin" -version = "3.9.0" +version = "3.9.1" description = "Mkdocs Markdown includer plugin." category = "dev" optional = false @@ -402,7 +415,7 @@ test = ["mkdocs (==1.4.0)", "pytest (==7.1.3)", "pytest-cov (==3.0.0)"] [[package]] name = "mkdocs-material" -version = "8.5.6" +version = "8.5.8" description = "Documentation that simply works" category = "dev" optional = false @@ -412,18 +425,18 @@ python-versions = ">=3.7" jinja2 = ">=3.0.2" markdown = ">=3.2" mkdocs = ">=1.4.0" -mkdocs-material-extensions = ">=1.0.3" +mkdocs-material-extensions = ">=1.1" pygments = ">=2.12" pymdown-extensions = ">=9.4" requests = ">=2.26" [[package]] name = "mkdocs-material-extensions" -version = "1.0.3" -description = "Extension pack for Python Markdown." +version = "1.1" +description = "Extension pack for Python Markdown and MkDocs Material." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mkdocstrings" @@ -461,7 +474,7 @@ mkdocstrings = ">=0.19" [[package]] name = "mypy" -version = "0.982" +version = "0.990" description = "Optional static typing for Python" category = "dev" optional = false @@ -475,6 +488,7 @@ typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] @@ -518,7 +532,7 @@ python-versions = ">=3.7" [[package]] name = "pip" -version = "22.2.2" +version = "22.3.1" description = "The PyPA recommended tool for installing Python packages." category = "dev" optional = false @@ -526,15 +540,15 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.5.3" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -585,7 +599,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "Pygments" +name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" @@ -597,7 +611,7 @@ plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "9.6" +version = "9.8" description = "Extension pack for Python Markdown." category = "dev" optional = false @@ -619,7 +633,7 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -628,12 +642,12 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -665,9 +679,9 @@ python-versions = ">=3.6" attrs = ">=19.0" filelock = ">=3.0" mypy = [ - {version = ">=0.780", markers = "python_version >= \"3.9\""}, - {version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""}, {version = ">=0.500", markers = "python_version < \"3.8\""}, + {version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""}, + {version = ">=0.780", markers = "python_version >= \"3.9\""}, ] pytest = [ {version = ">=4.6", markers = "python_version >= \"3.6\" and python_version < \"3.10\""}, @@ -686,7 +700,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" six = ">=1.5" [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" @@ -694,7 +708,7 @@ optional = false python-versions = ">=3.6" [[package]] -name = "pyyaml_env_tag" +name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " category = "dev" @@ -720,7 +734,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-mock" @@ -740,7 +754,7 @@ test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "tes [[package]] name = "setuptools" -version = "65.4.1" +version = "65.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false @@ -748,7 +762,7 @@ python-versions = ">=3.7" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -785,7 +799,7 @@ python-versions = ">= 3.7" [[package]] name = "tox" -version = "3.26.0" +version = "3.27.0" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -840,7 +854,7 @@ python-versions = "*" [[package]] name = "types-cryptography" -version = "3.3.23" +version = "3.3.23.1" description = "Typing stubs for cryptography" category = "dev" optional = false @@ -859,7 +873,7 @@ types-urllib3 = "<1.27" [[package]] name = "types-urllib3" -version = "1.26.25" +version = "1.26.25.1" description = "Typing stubs for urllib3" category = "dev" optional = false @@ -888,20 +902,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.16.6" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] @@ -925,15 +939,15 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.8.1" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] doc = [] @@ -942,14 +956,14 @@ test = [] [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0" -content-hash = "19e6f3f83d4119ec2a19fd246295bb8629670124ba23464816a58e38433f1686" +content-hash = "1d6418aebd187b4bb9a506602eeb3da925c3b8214b7d016be99bc44200dc34c5" [metadata.files] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] -"backports.cached-property" = [ +backports-cached-property = [ {file = "backports.cached-property-1.0.2.tar.gz", hash = "sha256:9306f9eed6ec55fd156ace6bc1094e2c86fae5fb2bf07b6a9c00745c656e75dd"}, {file = "backports.cached_property-1.0.2-py3-none-any.whl", hash = "sha256:baeb28e1cd619a3c9ab8941431fe34e8490861fb998c6c4590693d50171db0cc"}, ] @@ -958,14 +972,23 @@ binapy = [ {file = "binapy-0.6.0.tar.gz", hash = "sha256:1054b0ef1e6eccd941d4b23167ba4c5b1f5938960750686c421edc71af543fa7"}, ] black = [ + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, @@ -1058,8 +1081,8 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, @@ -1114,34 +1137,34 @@ coverage = [ {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] cryptography = [ - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, - {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, - {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, - {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, -] -Deprecated = [ + {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"}, + {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"}, + {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"}, + {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"}, + {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"}, + {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"}, + {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"}, +] +deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] @@ -1149,6 +1172,10 @@ distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] +exceptiongroup = [ + {file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"}, + {file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"}, +] filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, @@ -1162,12 +1189,12 @@ ghp-import = [ {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] griffe = [ - {file = "griffe-0.22.2-py3-none-any.whl", hash = "sha256:cea5415ac6a92f4a22638e3f1f2e661402bac09fb8e8266936d67185a7e0d0fb"}, - {file = "griffe-0.22.2.tar.gz", hash = "sha256:1408e336a4155392bbd81eed9f2f44bf144e71b9c664e905630affe83bbc088e"}, + {file = "griffe-0.23.0-py3-none-any.whl", hash = "sha256:cfca5f523808109da3f8cfaa46e325fa2e5bef51120d1146e908c121b56475f0"}, + {file = "griffe-0.23.0.tar.gz", hash = "sha256:a639e2968c8e27f56ebcc57f869a03cea7ac7e7f5684bd2429c665f761c4e7bd"}, ] identify = [ - {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, - {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, + {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, + {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1185,7 +1212,7 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -Jinja2 = [ +jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] @@ -1195,11 +1222,11 @@ jwcrypto = [ livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] -Markdown = [ +markdown = [ {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] -MarkupSafe = [ +markupsafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, @@ -1246,24 +1273,24 @@ mergedeep = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] mkdocs = [ - {file = "mkdocs-1.4.0-py3-none-any.whl", hash = "sha256:ce057e9992f017b8e1496b591b6c242cbd34c2d406e2f9af6a19b97dd6248faa"}, - {file = "mkdocs-1.4.0.tar.gz", hash = "sha256:e5549a22d59e7cb230d6a791edd2c3d06690908454c0af82edc31b35d57e3069"}, + {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, + {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, ] mkdocs-autorefs = [ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, ] mkdocs-include-markdown-plugin = [ - {file = "mkdocs_include_markdown_plugin-3.9.0-py3-none-any.whl", hash = "sha256:ed0f3c510f3ed3dad0b841125b447b725b588a356dc78000d40d140eddf7ddc5"}, - {file = "mkdocs_include_markdown_plugin-3.9.0.tar.gz", hash = "sha256:c265bef5c811a61e400fb8638da0fe026c299c207090dfc2300bb96080a25c85"}, + {file = "mkdocs_include_markdown_plugin-3.9.1-py3-none-any.whl", hash = "sha256:f33687e29ac66d045ba181ea50f054646b0090b42b0a4318f08e7f1d1235e6f6"}, + {file = "mkdocs_include_markdown_plugin-3.9.1.tar.gz", hash = "sha256:5e5698e78d7fea111be9873a456089daa333497988405acaac8eba2924a19152"}, ] mkdocs-material = [ - {file = "mkdocs_material-8.5.6-py3-none-any.whl", hash = "sha256:b473162c800321b9760453f301a91f7cb40a120a85a9d0464e1e484e74b76bb2"}, - {file = "mkdocs_material-8.5.6.tar.gz", hash = "sha256:38a21d817265d0c203ab3dad64996e45859c983f72180f6937bd5540a4eb84e4"}, + {file = "mkdocs_material-8.5.8-py3-none-any.whl", hash = "sha256:7ff092299e3a63cef99cd87e4a6cc7e7d9ec31fd190d766fd147c35572e6d593"}, + {file = "mkdocs_material-8.5.8.tar.gz", hash = "sha256:61396251819cf7f547f70a09ce6a7edb2ff5d32e47b9199769020b2d20a83d44"}, ] mkdocs-material-extensions = [ - {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, - {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, + {file = "mkdocs_material_extensions-1.1-py3-none-any.whl", hash = "sha256:bcc2e5fc70c0ec50e59703ee6e639d87c7e664c0c441c014ea84461a90f1e902"}, + {file = "mkdocs_material_extensions-1.1.tar.gz", hash = "sha256:96ca979dae66d65c2099eefe189b49d5ac62f76afb59c38e069ffc7cf3c131ec"}, ] mkdocstrings = [ {file = "mkdocstrings-0.19.0-py3-none-any.whl", hash = "sha256:3217d510d385c961f69385a670b2677e68e07b5fea4a504d86bf54c006c87c7d"}, @@ -1274,30 +1301,36 @@ mkdocstrings-python = [ {file = "mkdocstrings_python-0.7.1-py3-none-any.whl", hash = "sha256:a22060bfa374697678e9af4e62b020d990dad2711c98f7a9fac5c0345bef93c7"}, ] mypy = [ - {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, - {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, - {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, - {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, - {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, - {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, - {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, - {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, - {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, - {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, - {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, - {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, - {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, - {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, - {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, - {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, - {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, - {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, - {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, - {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, + {file = "mypy-0.990-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aaf1be63e0207d7d17be942dcf9a6b641745581fe6c64df9a38deb562a7dbafa"}, + {file = "mypy-0.990-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d555aa7f44cecb7ea3c0ac69d58b1a5afb92caa017285a8e9c4efbf0518b61b4"}, + {file = "mypy-0.990-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f694d6d09a460b117dccb6857dda269188e3437c880d7b60fa0014fa872d1e9"}, + {file = "mypy-0.990-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:269f0dfb6463b8780333310ff4b5134425157ef0d2b1d614015adaf6d6a7eabd"}, + {file = "mypy-0.990-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8798c8ed83aa809f053abff08664bdca056038f5a02af3660de00b7290b64c47"}, + {file = "mypy-0.990-cp310-cp310-win_amd64.whl", hash = "sha256:47a9955214615108c3480a500cfda8513a0b1cd3c09a1ed42764ca0dd7b931dd"}, + {file = "mypy-0.990-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a8a6c10f4c63fbf6ad6c03eba22c9331b3946a4cec97f008e9ffb4d3b31e8e2"}, + {file = "mypy-0.990-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd2dd3730ba894ec2a2082cc703fbf3e95a08479f7be84912e3131fc68809d46"}, + {file = "mypy-0.990-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7da0005e47975287a92b43276e460ac1831af3d23032c34e67d003388a0ce8d0"}, + {file = "mypy-0.990-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:262c543ef24deb10470a3c1c254bb986714e2b6b1a67d66daf836a548a9f316c"}, + {file = "mypy-0.990-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ff201a0c6d3ea029d73b1648943387d75aa052491365b101f6edd5570d018ea"}, + {file = "mypy-0.990-cp311-cp311-win_amd64.whl", hash = "sha256:1767830da2d1afa4e62b684647af0ff79b401f004d7fa08bc5b0ce2d45bcd5ec"}, + {file = "mypy-0.990-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6826d9c4d85bbf6d68cb279b561de6a4d8d778ca8e9ab2d00ee768ab501a9852"}, + {file = "mypy-0.990-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46897755f944176fbc504178422a5a2875bbf3f7436727374724842c0987b5af"}, + {file = "mypy-0.990-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0680389c34284287fe00e82fc8bccdea9aff318f7e7d55b90d967a13a9606013"}, + {file = "mypy-0.990-cp37-cp37m-win_amd64.whl", hash = "sha256:b08541a06eed35b543ae1a6b301590eb61826a1eb099417676ddc5a42aa151c5"}, + {file = "mypy-0.990-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:be88d665e76b452c26fb2bdc3d54555c01226fba062b004ede780b190a50f9db"}, + {file = "mypy-0.990-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8f4a8213b1fd4b751e26b59ae0e0c12896568d7e805861035c7a15ed6dc9eb"}, + {file = "mypy-0.990-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b6f85c2ad378e3224e017904a051b26660087b3b76490d533b7344f1546d3ff"}, + {file = "mypy-0.990-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee5f99817ee70254e7eb5cf97c1b11dda29c6893d846c8b07bce449184e9466"}, + {file = "mypy-0.990-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49082382f571c3186ce9ea0bd627cb1345d4da8d44a8377870f4442401f0a706"}, + {file = "mypy-0.990-cp38-cp38-win_amd64.whl", hash = "sha256:aba38e3dd66bdbafbbfe9c6e79637841928ea4c79b32e334099463c17b0d90ef"}, + {file = "mypy-0.990-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9d851c09b981a65d9d283a8ccb5b1d0b698e580493416a10942ef1a04b19fd37"}, + {file = "mypy-0.990-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d847dd23540e2912d9667602271e5ebf25e5788e7da46da5ffd98e7872616e8e"}, + {file = "mypy-0.990-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc6019808580565040cd2a561b593d7c3c646badd7e580e07d875eb1bf35c695"}, + {file = "mypy-0.990-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a3150d409609a775c8cb65dbe305c4edd7fe576c22ea79d77d1454acd9aeda8"}, + {file = "mypy-0.990-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3227f14fe943524f5794679156488f18bf8d34bfecd4623cf76bc55958d229c5"}, + {file = "mypy-0.990-cp39-cp39-win_amd64.whl", hash = "sha256:c76c769c46a1e6062a84837badcb2a7b0cdb153d68601a61f60739c37d41cc74"}, + {file = "mypy-0.990-py3-none-any.whl", hash = "sha256:8f1940325a8ed460ba03d19ab83742260fa9534804c317224e5d4e5aa588e2d6"}, + {file = "mypy-0.990.tar.gz", hash = "sha256:72382cb609142dba3f04140d016c94b4092bc7b4d98ca718740dc989e5271b8d"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1316,12 +1349,12 @@ pathspec = [ {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, ] pip = [ - {file = "pip-22.2.2-py3-none-any.whl", hash = "sha256:b61a374b5bc40a6e982426aede40c9b5a08ff20e640f5b56977f4f91fed1e39a"}, - {file = "pip-22.2.2.tar.gz", hash = "sha256:3fd1929db052f056d7a998439176d3333fa1b3f6c1ad881de1885c0717608a4b"}, + {file = "pip-22.3.1-py3-none-any.whl", hash = "sha256:908c78e6bc29b676ede1c4d57981d490cb892eb45cd8c214ab6298125119e077"}, + {file = "pip-22.3.1.tar.gz", hash = "sha256:65fd48317359f3af8e593943e6ae1506b66325085ea64b706a998c6e83eeaf38"}, ] platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"}, + {file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1339,21 +1372,21 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -Pygments = [ +pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pymdown-extensions = [ - {file = "pymdown_extensions-9.6-py3-none-any.whl", hash = "sha256:1e36490adc7bfcef1fdb21bb0306e93af99cff8ec2db199bd17e3bf009768c11"}, - {file = "pymdown_extensions-9.6.tar.gz", hash = "sha256:b956b806439bbff10f726103a941266beb03fbe99f897c7d5e774d7170339ad9"}, + {file = "pymdown_extensions-9.8-py3-none-any.whl", hash = "sha256:8e62688a8b1128acd42fa823f3d429d22f4284b5e6dd4d3cd56721559a5a211b"}, + {file = "pymdown_extensions-9.8.tar.gz", hash = "sha256:1bd4a173095ef8c433b831af1f3cb13c10883be0c100ae613560668e594651f7"}, ] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-cov = [ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, @@ -1367,7 +1400,7 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] -PyYAML = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1409,7 +1442,7 @@ PyYAML = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -pyyaml_env_tag = [ +pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] @@ -1422,8 +1455,8 @@ requests-mock = [ {file = "requests_mock-1.10.0-py2.py3-none-any.whl", hash = "sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699"}, ] setuptools = [ - {file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"}, - {file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"}, + {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"}, + {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1451,8 +1484,8 @@ tornado = [ {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, ] tox = [ - {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, - {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, + {file = "tox-3.27.0-py2.py3-none-any.whl", hash = "sha256:89e4bc6df3854e9fc5582462e328dd3660d7d865ba625ae5881bbc63836a6324"}, + {file = "tox-3.27.0.tar.gz", hash = "sha256:d2c945f02a03d4501374a3d5430877380deb69b218b1df9b7f1d2f2a10befaf9"}, ] tox-poetry = [ {file = "tox-poetry-0.4.1.tar.gz", hash = "sha256:2395808e1ce487b5894c10f2202e14702bfa6d6909c0d1e525170d14809ac7ef"}, @@ -1489,16 +1522,16 @@ types-backports = [ {file = "types_backports-0.1.3-py2.py3-none-any.whl", hash = "sha256:dafcd61848081503e738a7768872d1dd6c018401b4d2a1cfb608ea87ec9864b9"}, ] types-cryptography = [ - {file = "types-cryptography-3.3.23.tar.gz", hash = "sha256:b85c45fd4d3d92e8b18e9a5ee2da84517e8fff658e3ef5755c885b1c2a27c1fe"}, - {file = "types_cryptography-3.3.23-py3-none-any.whl", hash = "sha256:913b3e66a502edbf4bfc3bb45e33ab476040c56942164a7ff37bd1f0ef8ef783"}, + {file = "types-cryptography-3.3.23.1.tar.gz", hash = "sha256:f108e7a7161eedd9fe391e1a8ae5a98d40c1da2f0418bc0812a9119990272314"}, + {file = "types_cryptography-3.3.23.1-py3-none-any.whl", hash = "sha256:b9f8aa93d9e4d7ff920961cdce3dc7cca479946890924d68356b66f15f4f35e3"}, ] types-requests = [ {file = "types-requests-2.28.11.2.tar.gz", hash = "sha256:fdcd7bd148139fb8eef72cf4a41ac7273872cad9e6ada14b11ff5dfdeee60ed3"}, {file = "types_requests-2.28.11.2-py3-none-any.whl", hash = "sha256:14941f8023a80b16441b3b46caffcbfce5265fd14555844d6029697824b5a2ef"}, ] types-urllib3 = [ - {file = "types-urllib3-1.26.25.tar.gz", hash = "sha256:5aef0e663724eef924afa8b320b62ffef2c1736c1fa6caecfc9bc6c8ae2c3def"}, - {file = "types_urllib3-1.26.25-py3-none-any.whl", hash = "sha256:c1d78cef7bd581e162e46c20a57b2e1aa6ebecdcf01fd0713bb90978ff3e3427"}, + {file = "types-urllib3-1.26.25.1.tar.gz", hash = "sha256:a948584944b2412c9a74b9cf64f6c48caf8652cb88b38361316f6d15d8a184cd"}, + {file = "types_urllib3-1.26.25.1-py3-none-any.whl", hash = "sha256:f6422596cc9ee5fdf68f9d547f541096a20c2dcfd587e37c804c9ea720bf5cb2"}, ] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, @@ -1509,8 +1542,8 @@ urllib3 = [ {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, + {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, + {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, ] watchdog = [ {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, @@ -1606,6 +1639,6 @@ wrapt = [ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/pyproject.toml b/pyproject.toml index 96796d6..4c3a787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool] [tool.poetry] name = "jwskate" -version = "0.4.1" +version = "0.5.0" homepage = "https://github.com/guillp/jwskate" description = "A Pythonic implementation of Json Web Signature, Keys, Algorithms, Tokens and Encryption (RFC7514 to 7519), on top of the `cryptography` module." authors = ["Guillaume Pujol "] @@ -38,16 +38,16 @@ freezegun = "^1.2.2" isort = ">=5.0" jwcrypto = ">=1.0" livereload = ">=2.0" -mypy = ">=0.942" -mkdocs = ">=1.3" +mypy = ">=0.990" +mkdocs = ">=1.4.2" mkdocstrings = { version = ">=0.18.0", extras = ["python"] } mkdocs-autorefs = ">=0.4" -mkdocs-include-markdown-plugin = ">=3.6" -mkdocs-material = ">=8.2" -mkdocs-material-extensions = ">=1.0" +mkdocs-include-markdown-plugin = ">=3.9.1" +mkdocs-material = ">=8.5.8" +mkdocs-material-extensions = ">=1.1" pip = ">=22.0" pre-commit = ">=2.12.0" -pytest = ">=7.1" +pytest = ">=7.2" pytest-cov = ">=3.0" pytest-mypy = ">=0.9" requests-mock = ">=1.9" diff --git a/tests/test_jwa/__init__.py b/tests/test_jwa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_jwa/test_base.py b/tests/test_jwa/test_base.py new file mode 100644 index 0000000..afc00b1 --- /dev/null +++ b/tests/test_jwa/test_base.py @@ -0,0 +1,24 @@ +import pytest +from cryptography.hazmat.primitives.asymmetric import rsa + +from jwskate import PrivateKeyRequired, PublicKeyRequired, RsaEsOaepSha256 + + +def test_private_public_key_required() -> None: + key = rsa.generate_private_key(65537, 2048) + private_wrapper = RsaEsOaepSha256(key) + + with private_wrapper.private_key_required(): + assert True + + with pytest.raises(PublicKeyRequired): + with private_wrapper.public_key_required(): + pass + + public_wrapper = RsaEsOaepSha256(key.public_key()) + with public_wrapper.public_key_required(): + assert True + + with pytest.raises(PrivateKeyRequired): + with public_wrapper.private_key_required(): + pass diff --git a/tests/test_jwa/test_encryption.py b/tests/test_jwa/test_encryption.py new file mode 100644 index 0000000..8d97d9e --- /dev/null +++ b/tests/test_jwa/test_encryption.py @@ -0,0 +1,70 @@ +"""Tests for jwskate.jwa.encryption submodule.""" + +import pytest + +from jwskate import ( + A128CBC_HS256, + A128GCM, + A192CBC_HS384, + A192GCM, + A256CBC_HS512, + A256GCM, + BaseAESEncryptionAlg, + MismatchingAuthTag, +) + + +class SupportsBytesTester: + """A test class with a __bytes__ method to match SupportBytes interface.""" + + def __init__(self, payload: bytes) -> None: + self.payload = payload + + def __bytes__(self) -> bytes: # noqa: D105 + return self.payload + + +@pytest.mark.parametrize( + "alg", [A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512] +) +def test_encryption(alg: BaseAESEncryptionAlg) -> None: + jwa = alg.init_random_key() + plaintext = b"this is a test" + iv = alg.generate_iv() + assert len(iv) * 8 == alg.iv_size + ciphertext, tag = jwa.encrypt(plaintext, iv=iv) + assert (ciphertext, tag) == jwa.encrypt( + SupportsBytesTester(plaintext), iv=SupportsBytesTester(iv) + ) + assert ( + jwa.decrypt(ciphertext, iv=iv, auth_tag=tag) + == jwa.decrypt( + SupportsBytesTester(ciphertext), + iv=SupportsBytesTester(iv), + auth_tag=SupportsBytesTester(tag), + ) + == plaintext + ) + + with pytest.raises(ValueError): + jwa.encrypt(plaintext, iv=b"tooshort") + with pytest.raises(ValueError): + jwa.encrypt(plaintext, iv=b"toolong" * 50) + with pytest.raises(ValueError): + jwa.decrypt(ciphertext, iv=b"tooshort", auth_tag=tag) + with pytest.raises(ValueError): + jwa.decrypt(ciphertext, iv=b"toolong" * 50, auth_tag=tag) + + aad = b"this is an AAD" + ciphertext_aad, tag_aad = jwa.encrypt(plaintext, iv=iv, aad=aad) + assert ( + (ciphertext_aad, tag_aad) + == jwa.encrypt(plaintext, iv=iv, aad=SupportsBytesTester(aad)) + != jwa.encrypt(plaintext, iv=iv) + ) + assert jwa.decrypt(ciphertext_aad, auth_tag=tag_aad, iv=iv, aad=aad) == jwa.decrypt( + ciphertext_aad, auth_tag=tag_aad, iv=iv, aad=SupportsBytesTester(aad) + ) + + with pytest.raises(MismatchingAuthTag): + jwa.decrypt(ciphertext_aad, auth_tag=tag_aad, iv=iv) diff --git a/tests/test_jwa.py b/tests/test_jwa/test_examples.py similarity index 74% rename from tests/test_jwa.py rename to tests/test_jwa/test_examples.py index 131e3ad..33a5f5a 100644 --- a/tests/test_jwa.py +++ b/tests/test_jwa/test_examples.py @@ -1,21 +1,7 @@ """Tests for the jwkskate.jwa submodule.""" -import pytest from binapy import BinaPy -from cryptography.hazmat.primitives.asymmetric import ec -from jwskate import ( - A128CBC_HS256, - A128GCM, - A192CBC_HS384, - A192GCM, - A256CBC_HS512, - A256GCM, - ES256, - BaseAESEncryptionAlg, - EcdhEs, - Jwk, - MismatchingAuthTag, -) +from jwskate import A128CBC_HS256, A192CBC_HS384, EcdhEs, Jwk def test_aes_128_hmac_sha256() -> None: @@ -187,7 +173,7 @@ def test_ecdhes() -> None: } ) - otherinfo = EcdhEs.otherinfo("A128GCM", b"Alice", b"Bob", 128) + otherinfo = EcdhEs.otherinfo(alg="A128GCM", apu=b"Alice", apv=b"Bob", key_size=128) alice_cek = EcdhEs.derive( private_key=alice_ephemeral_key.cryptography_key, public_key=bob_private_key.public_jwk().cryptography_key, @@ -203,65 +189,3 @@ def test_ecdhes() -> None: key_size=128, ) assert BinaPy(bob_cek).to("b64u") == b"VqqN6vgjbSBcIijNcacQGg" - - -def test_ec_signature_invalid_size() -> None: - es256 = ES256(ec.generate_private_key(ec.SECP256R1()).public_key()) - with pytest.raises(ValueError): - es256.verify(b"foo", b"bar") - - -class SupportsBytesTester: - """A test class with a __bytes__ method to match SupportBytes interface.""" - - def __init__(self, payload: bytes) -> None: - self.payload = payload - - def __bytes__(self) -> bytes: # noqa: D105 - return self.payload - - -@pytest.mark.parametrize( - "alg", [A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512] -) -def test_encryption(alg: BaseAESEncryptionAlg) -> None: - jwa = alg.init_random_key() - plaintext = b"this is a test" - iv = alg.generate_iv() - assert len(iv) * 8 == alg.iv_size - ciphertext, tag = jwa.encrypt(plaintext, iv=iv) - assert (ciphertext, tag) == jwa.encrypt( - SupportsBytesTester(plaintext), iv=SupportsBytesTester(iv) - ) - assert ( - jwa.decrypt(ciphertext, iv=iv, auth_tag=tag) - == jwa.decrypt( - SupportsBytesTester(ciphertext), - iv=SupportsBytesTester(iv), - auth_tag=SupportsBytesTester(tag), - ) - == plaintext - ) - - with pytest.raises(ValueError): - jwa.encrypt(plaintext, iv=b"tooshort") - with pytest.raises(ValueError): - jwa.encrypt(plaintext, iv=b"toolong" * 50) - with pytest.raises(ValueError): - jwa.decrypt(ciphertext, iv=b"tooshort", auth_tag=tag) - with pytest.raises(ValueError): - jwa.decrypt(ciphertext, iv=b"toolong" * 50, auth_tag=tag) - - aad = b"this is an AAD" - ciphertext_aad, tag_aad = jwa.encrypt(plaintext, iv=iv, aad=aad) - assert ( - (ciphertext_aad, tag_aad) - == jwa.encrypt(plaintext, iv=iv, aad=SupportsBytesTester(aad)) - != jwa.encrypt(plaintext, iv=iv) - ) - assert jwa.decrypt(ciphertext_aad, auth_tag=tag_aad, iv=iv, aad=aad) == jwa.decrypt( - ciphertext_aad, auth_tag=tag_aad, iv=iv, aad=SupportsBytesTester(aad) - ) - - with pytest.raises(MismatchingAuthTag): - jwa.decrypt(ciphertext_aad, auth_tag=tag_aad, iv=iv) diff --git a/tests/test_jwa/test_key_mgmt.py b/tests/test_jwa/test_key_mgmt.py new file mode 100644 index 0000000..0ba452d --- /dev/null +++ b/tests/test_jwa/test_key_mgmt.py @@ -0,0 +1,41 @@ +"""Tests for jwskate.jwa.key_mgmt submodule.""" +from typing import Type, Union + +import pytest +from cryptography.hazmat.primitives.asymmetric import ec, x448, x25519 + +from jwskate import EcdhEs + + +@pytest.mark.parametrize( + "key_gen", + [ + lambda: ec.generate_private_key(ec.SECP256R1()), + lambda: ec.generate_private_key(ec.SECP384R1()), + lambda: ec.generate_private_key(ec.SECP521R1()), + x25519.X25519PrivateKey.generate, + x448.X448PrivateKey.generate, + ], +) +def test_ecdhes( + key_gen: Union[ + Type[ec.EllipticCurvePrivateKey], + Type[x25519.X25519PrivateKey], + Type[x448.X448PrivateKey], + ] +) -> None: + private_key = key_gen() + sender_ecdhes = EcdhEs(private_key.public_key()) + epk = sender_ecdhes.generate_ephemeral_key() + assert isinstance(epk, private_key.__class__) + sender_key = sender_ecdhes.sender_key(epk, alg="A128GCM", key_size=128) + + recipient_ecdhes = EcdhEs(private_key) + recipient_key = recipient_ecdhes.recipient_key( + epk.public_key(), alg="A128GCM", key_size=128 + ) + + assert sender_key == recipient_key + + with pytest.raises(ValueError): + sender_ecdhes.ecdh(private_key, b"foo") # type: ignore[arg-type] diff --git a/tests/test_jwa/test_signature.py b/tests/test_jwa/test_signature.py new file mode 100644 index 0000000..911a548 --- /dev/null +++ b/tests/test_jwa/test_signature.py @@ -0,0 +1,12 @@ +"""Tests for jwskate.jwa.signature submodule.""" + +import pytest +from cryptography.hazmat.primitives.asymmetric import ec + +from jwskate import ES256 + + +def test_ec_signature_invalid_size() -> None: + es256 = ES256(ec.generate_private_key(ec.SECP256R1()).public_key()) + with pytest.raises(ValueError): + es256.verify(b"foo", b"bar") diff --git a/tests/test_jwe.py b/tests/test_jwe.py index f8a5543..dc74d25 100644 --- a/tests/test_jwe.py +++ b/tests/test_jwe.py @@ -60,7 +60,9 @@ def test_jwe() -> None: ) iv = bytes.fromhex("e3c575fc02dbe944b4e14ddb") - jwe = JweCompact.encrypt(plaintext, jwk, alg=alg, enc=enc, cek=cek, iv=iv) + jwe = JweCompact.encrypt( + plaintext, jwk.public_jwk(), alg=alg, enc=enc, cek=cek, iv=iv + ) assert jwe.initialization_vector == bytes.fromhex("e3c575fc02dbe944b4e14ddb") diff --git a/tests/test_jwk/test_alg.py b/tests/test_jwk/test_alg.py index 9568678..4a1fd1e 100644 --- a/tests/test_jwk/test_alg.py +++ b/tests/test_jwk/test_alg.py @@ -1,57 +1,115 @@ import pytest -from jwskate import ExpectedAlgRequired, RSAJwk, UnsupportedAlg -from jwskate.jwk.alg import select_alg, select_algs +from jwskate import ( + ExpectedAlgRequired, + RSAJwk, + UnsupportedAlg, + select_alg_class, + select_alg_classes, +) -def test_select_alg() -> None: +def test_select_alg_class() -> None: + # select a supported alg of choice assert ( - select_alg( - jwk_alg=None, alg="RS256", supported_algs=RSAJwk.SIGNATURE_ALGORITHMS + select_alg_class( + RSAJwk.SIGNATURE_ALGORITHMS, + alg="RS256", + ) + == RSAJwk.SIGNATURE_ALGORITHMS["RS256"] + ) + + # select a supported alg based on a JWK alg parameter + assert ( + select_alg_class( + RSAJwk.SIGNATURE_ALGORITHMS, + jwk_alg="RS256", ) == RSAJwk.SIGNATURE_ALGORITHMS["RS256"] ) + # if alg and jwk_alg are inconsistent, a warning is raised, value from alg is selected with pytest.warns(): assert ( - select_alg( - jwk_alg="RS256", alg="RS512", supported_algs=RSAJwk.SIGNATURE_ALGORITHMS - ) + select_alg_class(RSAJwk.SIGNATURE_ALGORITHMS, jwk_alg="RS256", alg="RS512") == RSAJwk.SIGNATURE_ALGORITHMS["RS512"] ) + # if no jwk_alg or alg are passed as parameter, raise an ExpectedAlgRequired with pytest.raises(ExpectedAlgRequired): - select_alg(jwk_alg=None, alg=None, supported_algs=RSAJwk.SIGNATURE_ALGORITHMS) + select_alg_class(RSAJwk.SIGNATURE_ALGORITHMS) + + # if the requested alg is not supported, raise UnsupportedAlg + with pytest.raises(UnsupportedAlg): + select_alg_class( + RSAJwk.KEY_MANAGEMENT_ALGORITHMS, + alg="HS256", + ) with pytest.raises(UnsupportedAlg): - select_alg( - jwk_alg=None, alg="HS256", supported_algs=RSAJwk.KEY_MANAGEMENT_ALGORITHMS + select_alg_class( + RSAJwk.KEY_MANAGEMENT_ALGORITHMS, + jwk_alg="HS256", ) + # no supported algs: raise a ValueError with pytest.raises(ValueError): - select_alg(jwk_alg=None, alg="HS256", supported_algs={}) + select_alg_class({}, alg="HS256") -def test_select_algs() -> None: - assert select_algs( - jwk_alg=None, alg="RS256", algs=None, supported_algs=RSAJwk.SIGNATURE_ALGORITHMS +def test_select_alg_classes() -> None: + # selecting a single alg from the supported algs + assert select_alg_classes( + RSAJwk.SIGNATURE_ALGORITHMS, + alg="RS256", ) == [RSAJwk.SIGNATURE_ALGORITHMS["RS256"]] + # selecting multiple algs from the supported algs + assert select_alg_classes( + RSAJwk.SIGNATURE_ALGORITHMS, + algs=["RS256", "ES256", "ES512", "PS384", "HS512"], + ) == [RSAJwk.SIGNATURE_ALGORITHMS["RS256"], RSAJwk.SIGNATURE_ALGORITHMS["PS384"]] + + # selecting based on the JWK alg parameter + assert select_alg_classes( + RSAJwk.SIGNATURE_ALGORITHMS, + jwk_alg="RS256", + ) == [RSAJwk.SIGNATURE_ALGORITHMS["RS256"]] + + # selecting an unsupported alg + with pytest.raises(UnsupportedAlg): + select_alg_classes(RSAJwk.SIGNATURE_ALGORITHMS, alg="ES256") + + # you need to specify at least one of jwk_alg, alg or algs + with pytest.raises(ExpectedAlgRequired): + select_alg_classes(RSAJwk.SIGNATURE_ALGORITHMS) + + # if jwk_alg and alg/algs is inconsistent, a warning is fired with pytest.warns(): - assert select_algs( + assert select_alg_classes( + RSAJwk.SIGNATURE_ALGORITHMS, jwk_alg="RS256", alg="RS512", - algs=None, - supported_algs=RSAJwk.SIGNATURE_ALGORITHMS, ) == [RSAJwk.SIGNATURE_ALGORITHMS["RS512"]] + with pytest.warns(): + assert select_alg_classes( + RSAJwk.SIGNATURE_ALGORITHMS, + jwk_alg="RS256", + algs=["RS512", "PS512"], + ) == [ + RSAJwk.SIGNATURE_ALGORITHMS["RS512"], + RSAJwk.SIGNATURE_ALGORITHMS["PS512"], + ] + + # you cannot use both 'alg' and 'algs' at the same time (raises ValueError) with pytest.raises(ValueError): - select_algs( - jwk_alg=None, + select_alg_classes( + RSAJwk.SIGNATURE_ALGORITHMS, alg="RS256", algs=["RS256", "RS512"], - supported_algs=RSAJwk.SIGNATURE_ALGORITHMS, ) + # if no algs are supported, a ValueError is raised with pytest.raises(ValueError): - select_algs(jwk_alg=None, alg="HS256", algs=None, supported_algs={}) + select_alg_classes({}, alg="HS256") diff --git a/tests/test_jwk/test_ec.py b/tests/test_jwk/test_ec.py index 5e902f5..0d38731 100644 --- a/tests/test_jwk/test_ec.py +++ b/tests/test_jwk/test_ec.py @@ -1,17 +1,22 @@ import pytest -from jwskate import A128CBC_HS256, EcdhEs, ECJwk, Jwk +from jwskate import A128CBC_HS256, EcdhEs, ECJwk, Jwk, UnsupportedEllipticCurve + + +def test_ec_jwk() -> None: + with pytest.raises(UnsupportedEllipticCurve): + Jwk({"kty": "EC", "crv": "foo"}) def test_jwk_ec_generate() -> None: - with pytest.warns(): - jwk = ECJwk.generate(kid="myeckey") + jwk = ECJwk.generate(kid="myeckey", crv="P-256") assert jwk.kty == "EC" assert jwk.kid == "myeckey" assert jwk.crv == "P-256" assert "x" in jwk assert "y" in jwk assert "d" in jwk + assert jwk.coordinate_size == 32 public_jwk = jwk.public_jwk() assert public_jwk.kty == "EC" @@ -23,11 +28,14 @@ def test_jwk_ec_generate() -> None: assert jwk.supported_encryption_algorithms() == [] + with pytest.raises(UnsupportedEllipticCurve): + ECJwk.generate(crv="foo") + def test_ecdh_es() -> None: alg = "ECDH-ES+A128KW" enc = "A128CBC-HS256" - private_jwk = ECJwk.generate(alg=alg) + private_jwk = Jwk.generate_for_alg(alg) assert private_jwk.crv == "P-256" public_jwk = private_jwk.public_jwk() sender_cek, wrapped_cek, headers = public_jwk.sender_key(enc) @@ -40,6 +48,10 @@ def test_ecdh_es() -> None: recipient_cek = private_jwk.recipient_key(wrapped_cek, enc, **headers) assert recipient_cek == sender_cek + # no 'epk' in headers + with pytest.raises(ValueError): + private_jwk.recipient_key(wrapped_cek, enc) + def test_ecdh_es_with_controlled_cek_and_epk() -> None: # now try to generate the CEK and EPK ourselves, this should not be done outside of (pen)testing code!!! @@ -56,10 +68,7 @@ def test_ecdh_es_with_controlled_cek_and_epk() -> None: recipient_cek = private_jwk.recipient_key(wrapped_cek, enc, **headers) assert recipient_cek == sender_cek - # EPK is mandatory for recipient_key() to work - with pytest.raises(ValueError): - private_jwk.recipient_key(wrapped_cek, enc) - # try passing the private EPK to recipient key + # try passing the private EPK with pytest.raises(ValueError): private_jwk.recipient_key(wrapped_cek, enc, epk=epk) diff --git a/tests/test_jwk/test_generate.py b/tests/test_jwk/test_generate.py index 371e0a7..3deb91d 100644 --- a/tests/test_jwk/test_generate.py +++ b/tests/test_jwk/test_generate.py @@ -1,6 +1,14 @@ import pytest -from jwskate import EncryptionAlgs, Jwk, KeyManagementAlgs, RSAJwk, SignatureAlgs +from jwskate import ( + EncryptionAlgs, + ExpectedAlgRequired, + Jwk, + KeyManagementAlgs, + RSAJwk, + SignatureAlgs, + UnsupportedAlg, +) @pytest.mark.parametrize( @@ -12,27 +20,27 @@ def test_generate_for_alg(alg: str) -> None: if alg in SignatureAlgs.ALL_SYMMETRIC: assert jwk.kty == "oct" assert jwk.use == "sig" - assert jwk.key_ops == ["sign", "verify"] + assert jwk.key_ops == ("sign", "verify") assert jwk.is_symmetric elif alg in SignatureAlgs.ALL_ASYMMETRIC: assert jwk.kty in ("EC", "RSA", "OKP") assert jwk.use == "sig" - assert jwk.key_ops == ["sign"] + assert jwk.key_ops == ("sign",) assert not jwk.is_symmetric elif alg in EncryptionAlgs.ALL: assert jwk.kty == "oct" assert jwk.use == "enc" - assert jwk.key_ops == ["encrypt", "decrypt"] + assert jwk.key_ops == ("encrypt", "decrypt") assert jwk.is_symmetric elif alg in KeyManagementAlgs.ALL_SYMMETRIC: assert jwk.kty == "oct" assert jwk.use == "enc" - assert jwk.key_ops == ["wrapKey", "unwrapKey"] + assert jwk.key_ops == ("wrapKey", "unwrapKey") assert jwk.is_symmetric elif alg in KeyManagementAlgs.ALL_ASYMMETRIC: assert jwk.kty in ("EC", "RSA", "OKP") assert jwk.use == "enc" - assert jwk.key_ops == ["unwrapKey"] + assert jwk.key_ops == ("unwrapKey",) assert not jwk.is_symmetric jwk_mini = jwk.minimize() @@ -43,3 +51,13 @@ def test_generate_for_alg(alg: str) -> None: jwk_mini = jwk_mini.with_optional_private_parameters() assert jwk_mini.with_usage_parameters(alg) == jwk + + # cannot guess usage parameters if there is no 'alg' parameter in the Jwk + with pytest.raises(ExpectedAlgRequired): + jwk_mini.with_usage_parameters() + + +def test_unsupported_alg() -> None: + # trying to generate a Jwk with an unsupported alg raises a UnsupportedAlg + with pytest.raises(UnsupportedAlg): + Jwk.generate_for_alg("unknown_alg") diff --git a/tests/test_jwk/test_jwk.py b/tests/test_jwk/test_jwk.py index a62d6f9..e113226 100644 --- a/tests/test_jwk/test_jwk.py +++ b/tests/test_jwk/test_jwk.py @@ -1,8 +1,27 @@ +from typing import Tuple + import pytest from cryptography.hazmat.primitives.asymmetric import rsa -from jwskate import InvalidJwk, Jwk, RSAJwk -from jwskate.jwk.base import UnsupportedKeyType +from jwskate import ( + A128GCM, + ES256, + EcdhEs_A128KW, + InvalidJwk, + Jwk, + RSAJwk, + SymmetricJwk, + UnsupportedKeyType, + to_jwk, +) + + +def test_public_jwk() -> None: + private_jwk = Jwk.generate_for_alg("RS256") + assert private_jwk.is_private + public_jwk = private_jwk.public_jwk() + assert not public_jwk.is_private + assert public_jwk is public_jwk.public_jwk() def test_jwk_copy() -> None: @@ -69,6 +88,9 @@ def test_invalid_jwk() -> None: } ) + with pytest.raises(TypeError): + Jwk.from_cryptography_key(object()) + def test_json() -> None: j = '{"kty": "RSA", "n": "5nHd3aefARenRFQn-wVrjBLS-5A0uUiHWfOUt8EpjwE3wADAvVPKLbvRQJfugyrn_RpnLqqZFkwYrVD_u1Uzl9J17XJG75jGjCf-gVs1t9FPpgEsJGYK4RK2_f40AxAc6hKomB9q6_dqIxChDxVCrIrrWd9kRk0T86d8Ade3J4f_iMbremm3woSwI6QD056DkRtAD_v2PZQbUBgSru-PsrJ5l_pxxlGPxzAM4_XH8VfogXI8pWv2UDE1IguVeh371ESCbQbJ7SX2jgNzcvvZMMWs0syfF7P0BzGrh_ONsRcxmtjZgtcOA0TCu2-v8qx7GisgqOWOrzWs7ej5RUsu1sxtT53JG2Y3lrPrgajXTB56mSUaL9ivxEfUD17X_cUznGDNoVqcRdfa27rCtWqd8gL-C7M9bYYgcfpCRPllRvGmWP9oarrG4XoIO17QuhZ5tAoz8oFLM9o6pzR2CeDvmSqFbbTHXYdcpCuvYukIimZP6RruMU9O9YQjgCEGWx06WoTnDqWWjbrId8VqP0xJ_6w0j6av3EWGKLETBbaYRXys4OOy-JZRRHydg-es4tkir4xkMvIG8plxoz_mZbTyO9GA5tMHWzbQciUQFf95Gpiwsa5RDdGZx-guBAN56mtKnUzVG_PmUJ8-pzTATkjVpThBRLWaVPFi0eWLEc2NbF8", "e": "AQAB", "d": "QD----2gB03w9LwB6Zq5QXR-HmU2TfJKyml_LTxNufE8-es70Y1q8RVMYhX6CdgMliSmX_xwcicFOk06_5_hopKrDmiuHl8Z_DmqW6Zyc62H52CsTfmlTttI6cfV-ISlix1opAEebmusy9n3DZ1_2VtiAsylyHP0_BnWGS1rgzoHpLrRLHANK86SzV0NNBMd23cdhVdacBC7DLmpe9yO0-OuwxCwZ_qVe5OKBISiGkfIK08OWZCgO_t8NOHF6yYw8B2pv3wFEHllJHMR3R0akN6MMLmcIZ-qidbvXBgriRROYD3VZwVOgtrI8DlTdPVh3QnpMS3PekoczLgHx6oOhI0ytDvoQ0phZd1D4OJKpPtObBcaDiLcKqPJaV7cYBwHwV2D-DvMkvg9LG1fTZoCUZzVU8NBef8nX8tp3e5qJYYgusdexchdyDwxOigCKQcCSbFYk6cO0vy_XxwGGbqCKeaOxsC-laHR_wEIhrp_NNwVYfDVhvvj70TVNCvA5IWdIiNPaPYSH3klvNwdv6GRXn2RwYwReozgV9wG2dhaA0Sff0WXSgPRw_vF_cLoNIr59bS9X9jxp1-NcU2tei-VxC2g9U1RMFB09hbcXy8pBHoOsxA5UyDTDscZLFgxOiTr_D2GQBRvJch364p0RT0BHMak1Zkfq0LobAB9YWLwCAE", "p": "_AdO8_Y-mLzSnm65JI7Sleoxtf1Ex4LaJuNAzSlUdd5LQeUE-aAQ5qfdP_fjGFwY9uu2EQQ0n8jKKAxWT2z2sj4hEwPoT93SU-pZBA0zm0xeIo9QYPDgIw1ejPnAIdGzuW_F0Oh1ELdZ3XQGskNFywZXjoo4WtCbcwn5sLGlhZRfCFJ9dtJFB3OP3SbxKXWyWOcG6KIB2VivzH5NLz8-9sxiv4mJJj7AMvWA9gt0Qo1Alz_O6Xeg3iiV4qIzf0ioPJ7iy6UCVWpohdcwk2Cb6DHswICqlcOtKKRujLGhxtKJ98woSe-v76PuhZ7MWmS6GuF3iRud4AfsZGZWbue1oQ", "q": "6hN9GJygBBxjQxTLkZJZK3yS2GLJCWjNb5Egf6sWz3J-xvXBmBAszgC3C-OM7EMHr537nzxmLEN-wmIvx-U0OQrzqa5M9zCPdR2SMncRyQdw-nEBLcSqh9gocERjvdROrbZsCz_iLltNoCwCDlZQVE6HJkoVZ5AhAVhj50sQ9jlgWyhYnZMdDdKUfaJpfoADlsYmI49UZKCg2H5yJq1fws_Zh9Mi1z-we7rupOGp8rzgoQkzv68ljbY1yG-TF2Z_W99I32QseNzBRjFp99yXTgp3YcJxNtFnDBdvHLNnRHBwASlD_ZbDxg2p2xILVB_bUHZFW0W3CBzRNFi2bv3h_w", "dp": "aLsYwiSYCpyc4Z2dbmWzePzjP39J76aexP421YrRQFHp8C4djSZJH7CuLoDybBMJhMKa3CNlQukLqOzHiSX8tkE_OUmsZlQFrT17VEWwJl7r12y6uC4g1jAeFHNMtkEQcITULWYMD7BBtdcbWUS_Ygj2pZMmrAZ4Mqv4iMapxALOIwU0ggYLDXemVv5xxQrV3D_VDSMVpZ5HH7F0naeooKJ6fqHGzo_RCtwehSBpZaaRKsknULmXrfornwxMXh5xWw-jq4CcoaYgXU35L6U75JeqjKxrNuUjtfnuvqSqV5byInlCXMcv02PKINjGjuHAvJ7pL568Una4c1hbnqbHQQ", "dq": "RcEtBEqYfOEgy3rE90qPfCARep5lnoI2xkqPTrxjfcp28T-HQ5N-Zp1b7xUOh9Gp1rHTrC5JnGM4wSCVcJJjL6SN3EDu-rLj7Vi0molVKX0oM9m9KjBzSSwnUN1wg79i-u1j4S5Wbs4Soeq7ah5areUA7W4iVsxiqY33p5N9KIMMrd2mGr8eZ2Ibkhz2JxZq-2FtOCecVKhxhlKYHeKIqPtbrdhDh7WZGCYqu8Pr60RSBGtDmpnNLR_hgyuMv-pxhaVSiA_IGPRgPFS5aX25MS55SQ6ywk1A0h-howHrgj-ngREVC9sD2F92AKyt55Hev2mfXYW295nu1hShuQ27bQ", "qi": "ZzWzSSYiJHyLRpqUw-GDluHaIYgDV1w70yYtwl222tvJt0TCQkcZmc3tWmC6qYu_7UfFfVDoCVwBu592p6IC0ZD39_eilHaKbDMZ3dOIS9n9h4yfJvvY-4Jd7o5i_LyK5RktvdyZARKrKfL3mYlWCmHC7yYZC88hDh5qkehRT1d52QBKo928mmrkgJZcuuzEVTygTrnCiiFzcd6A7o8wLbtJPBg4WY793xLipiuSEZ8aWQ50hO98MauBO5MJl_C3kZOkEKiq8JYTU-cUHO6kMqlQ866MOccBsco__frxA8yZlZrfMIDOql8z6oS5tpxR5O_acl9fvJ_thwUAVTk3Ow", "kid": "client_assertion_key"}' @@ -118,10 +140,12 @@ def test_setattr() -> None: jwk["k"] = "foo" -def test_invalid_alg() -> None: - jwk = Jwk({"kty": "oct", "k": "foobar", "alg": 1.34}) +def test_invalid_params() -> None: + with pytest.raises(TypeError): + Jwk({"kty": "oct", "k": "foobar", "alg": 1.34}).alg + with pytest.raises(TypeError): - jwk.alg + Jwk({"kty": "oct", "k": "foobar", "kid": 1.34}).kid def test_invalid_class_for_kty() -> None: @@ -130,17 +154,51 @@ def test_invalid_class_for_kty() -> None: @pytest.mark.parametrize( - "private_key_ops, public_key_ops", + "kty, private_key_ops, public_key_ops", [ - ("sign", "verify"), - ("unwrapKey", "wrapKey"), + ("RSA", ("sign",), ("verify",)), + ("RSA", ("unwrapKey",), ("wrapKey",)), + ("oct", ("sign",), ("verify",)), + ("oct", ("unwrapKey",), ("wrapKey",)), ], ) -def test_key_ops(private_key_ops: str, public_key_ops: str) -> None: - private_jwk = Jwk.generate_for_kty("RSA", key_ops=[private_key_ops]) +def test_key_ops_without_alg( + kty: str, private_key_ops: Tuple[str], public_key_ops: Tuple[str] +) -> None: + # with a key with no alg or use, we can only trust the key_ops from the key + private_jwk = Jwk.generate_for_kty("RSA", key_ops=private_key_ops) + assert private_jwk.key_ops == private_key_ops + public_jwk = private_jwk.public_jwk() - assert public_key_ops in public_jwk.key_ops - assert private_key_ops not in public_jwk.key_ops + assert public_key_ops == public_jwk.key_ops + + +@pytest.mark.parametrize( + "alg, use, private_key_ops, public_key_ops", + [ + ("RS256", "sig", ("sign",), ("verify",)), + ("HS256", "sig", ("sign", "verify"), ("sign", "verify")), + ("A128GCMKW", "enc", ("wrapKey", "unwrapKey"), ("wrapKey", "unwrapKey")), + ("A128GCM", "enc", ("encrypt", "decrypt"), ("encrypt", "decrypt")), + ], +) +def test_use_key_ops_with_alg( + alg: str, use: str, private_key_ops: Tuple[str], public_key_ops: Tuple[str] +) -> None: + # if key has an 'alg' parameter, we can deduce the use and key ops + private_jwk = Jwk.generate_for_alg(alg) + assert "use" not in private_jwk + assert "key_ops" not in private_jwk + assert private_jwk.use == use + assert private_jwk.key_ops == private_key_ops + + public_jwk = ( + private_jwk.public_jwk() if not private_jwk.is_symmetric else private_jwk + ) + assert "use" not in public_jwk + assert "key_ops" not in public_jwk + assert public_jwk.use == use + assert public_jwk.key_ops == public_key_ops def test_thumbprint() -> None: @@ -194,3 +252,60 @@ def test_generate_for_alg() -> None: rsa15_jwk = Jwk.generate_for_alg("RSA1_5") assert isinstance(rsa15_jwk, RSAJwk) assert rsa15_jwk.alg == "RSA1_5" + + +def test_signature_wrapper() -> None: + signature_jwk = Jwk.generate_for_alg("ES256") + signature_wrapper = signature_jwk.signature_wrapper() + assert isinstance(signature_wrapper, ES256) + assert signature_wrapper.key == signature_jwk.cryptography_key + + +def test_encryption_wrapper() -> None: + encryption_jwk = Jwk.generate_for_alg("A128GCM") + encryption_wrapper = encryption_jwk.encryption_wrapper() + assert isinstance(encryption_wrapper, A128GCM) + assert encryption_wrapper.key == encryption_jwk.cryptography_key + + +def test_key_management_wrapper() -> None: + key_mgmt_jwk = Jwk.generate_for_alg("ECDH-ES+A128KW") + key_mgmt_wrapper = key_mgmt_jwk.key_management_wrapper() + assert isinstance(key_mgmt_wrapper, EcdhEs_A128KW) + assert key_mgmt_wrapper.key == key_mgmt_jwk.cryptography_key + + +def test_to_jwk() -> None: + # symmetric key + SYMMETRIC_KEY = b"this is a symmetric key" + sym_key = to_jwk(SYMMETRIC_KEY, is_symmetric=True, kty="oct") + assert isinstance(sym_key, SymmetricJwk) + assert sym_key.key == b"this is a symmetric key" + + with pytest.raises(ValueError): + to_jwk(SYMMETRIC_KEY, is_symmetric=False) + + with pytest.raises(ValueError): + to_jwk(SYMMETRIC_KEY, is_private=False) + + # test using a Google public key + GOOGLE_KEY = """{ + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "f451345fad08101bfb345cf642a2da9267b9ebeb", + "n": "ppFPAZUqIVqCf_SffT6xDCXu1R7aRoT6TNT5_Q8PKxkkqbOVysJPNwliF-486VeM8KNW8onFOv0GkP0lJ2ASrVgyMG1qmlGUlKug64dMQXPxSlVUCXCPN676W5IZTvT0tD2byM_29HZXnOifRg-d7PRRvIBLSUWe-fGb1-tP2w65SOW-W6LuOjGzLNPJFYQvHyUx_uXHOCfIoSb8kaMwx8bCWvKc76yT0DG1wcygGXKuFQHW-Sdi1j_6bF19lVu30DX-jhYsNMUnGUr6g2iycQ50pWMORZqvcHVOH1bbDrWuz0b564sK0ET2B3XDR37djNQ305PxiQZaBStm-hM8Aw", + "alg": "RS256" + }""" + asym_key = to_jwk(GOOGLE_KEY, kty="RSA", is_private=False) + assert not asym_key.is_private + assert isinstance(asym_key, RSAJwk) + + with pytest.raises(ValueError): + to_jwk(GOOGLE_KEY, kty="EC") + + with pytest.raises(ValueError): + to_jwk(GOOGLE_KEY, is_private=True) + + with pytest.raises(ValueError): + to_jwk(GOOGLE_KEY, is_symmetric=True) diff --git a/tests/test_jwk/test_jwks.py b/tests/test_jwk/test_jwks.py index 4c985f9..a1a72c6 100644 --- a/tests/test_jwk/test_jwks.py +++ b/tests/test_jwk/test_jwks.py @@ -24,18 +24,18 @@ def test_jwkset() -> None: assert jwks.jwks == keys jwk = Jwk.generate_for_kty("EC", alg="ES256", kid="my_ec_key") - keys.append(jwk) - kid = jwks.add_jwk(jwk) + keys.append(jwk.public_jwk()) + kid = jwks.add_jwk(jwk.public_jwk()) assert kid == jwk.kid assert jwks.jwks == keys data = b"this is a test" signature = jwk.sign(data) - assert jwks.verify(data, signature, kid="my_ec_key") + assert jwks.verify(data, signature, kid="my_ec_key", alg="ES256") assert jwks.verify(data, signature, alg="ES256") assert jwks.verify(data, signature, algs=("ES256",)) - assert jwks.verify(data, signature) + assert not jwks.verify(data, signature, algs=("HS256",)) jwks.remove_jwk(jwk.kid) diff --git a/tests/test_jwk/test_okp.py b/tests/test_jwk/test_okp.py index 9073339..e90b13c 100644 --- a/tests/test_jwk/test_okp.py +++ b/tests/test_jwk/test_okp.py @@ -3,7 +3,7 @@ import pytest from cryptography.hazmat.primitives.asymmetric import ed448, ed25519, x448, x25519 -from jwskate import Jwk, JwsCompact, OKPJwk, UnsupportedAlg, UnsupportedOKPCurve +from jwskate import EcdhEs, Jwk, JwsCompact, OKPJwk, UnsupportedAlg, UnsupportedOKPCurve @pytest.mark.parametrize("crv", ["Ed25519", "Ed448", "X25519", "X448"]) @@ -42,7 +42,8 @@ def test_generate_unsuppored_alg() -> None: OKPJwk.generate(alg="foo") -def test_okp_ed25519_sign() -> None: +def test_rfc8037_ed25519() -> None: + """Test from [RFC8037][https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A].""" jwk = Jwk( { "kty": "OKP", @@ -53,6 +54,26 @@ def test_okp_ed25519_sign() -> None: ) assert isinstance(jwk, OKPJwk) assert jwk.is_private + assert jwk.private_key == bytes.fromhex( + """9d 61 b1 9d ef fd 5a 60 ba 84 4a f4 92 ec 2c c4 + 44 49 c5 69 7b 32 69 19 70 3b ac 03 1c ae 7f 60""".replace( + " ", "" + ) + ) + assert jwk.public_key == bytes.fromhex( + """d7 5a 98 01 82 b1 0a b7 d5 4b fe d3 c9 64 07 3a + 0e e1 72 f3 da a6 23 25 af 02 1a 68 f7 07 51 1a""".replace( + " ", "" + ) + ) + assert jwk.public_jwk() == { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + } + + assert jwk.thumbprint() == "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k" + payload = "Example of Ed25519 signing".encode() jws = JwsCompact.sign(payload, jwk=jwk, alg="EdDSA") @@ -67,6 +88,98 @@ def test_okp_ed25519_sign() -> None: assert jws.verify_signature(jwk=jwk.public_jwk(), alg="EdDSA") +def test_rfc8037_x25519() -> None: + """Test from [RFC8037 $A.6][https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.6].""" + public_jwk = Jwk( + { + "kty": "OKP", + "crv": "X25519", + "kid": "Bob", + "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08", + } + ) + assert isinstance(public_jwk, OKPJwk) + assert public_jwk.public_key == bytes.fromhex( + """de 9e db 7d 7b 7d c1 b4 d3 5b 61 c2 ec e4 35 37 + 3f 83 43 c8 5b 78 67 4d ad fc 7e 14 6f 88 2b 4f""" + ) + + ephemeral_secret = bytes.fromhex( + """77 07 6d 0a 73 18 a5 7d 3c 16 c1 72 51 b2 66 45 + df 4c 2f 87 eb c0 99 2a b1 77 fb a5 1d b9 2c 2a""" + ) + + ephemeral_private_key = OKPJwk.from_bytes(ephemeral_secret, use="enc") + + assert ephemeral_private_key.public_jwk() == Jwk( + { + "kty": "OKP", + "crv": "X25519", + "x": "hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo", + } + ) + + ephemeral_private_key["kid"] = "Bob" + + sender_shared_key = EcdhEs.ecdh( + private_key=ephemeral_private_key.cryptography_key, + public_key=public_jwk.cryptography_key, + ) + assert sender_shared_key == bytes.fromhex( + """4a 5d 9d 5b a4 ce 2d e1 72 8e 3b f4 80 35 0f 25 + e0 7e 21 c9 47 d1 9e 33 76 f0 9b 3c 1e 16 17 42""" + ) + + +def test_rfc8037_x448() -> None: + """Test from [RFC8037 $A.7][https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.7].""" + public_jwk = Jwk( + { + "kty": "OKP", + "crv": "X448", + "kid": "Dave", + "x": "PreoKbDNIPW8_AtZm2_sz22kYnEHvbDU80W0MCfYuXL8PjT7QjKhPKcG3LV67D2uB73BxnvzNgk", + } + ) + assert isinstance(public_jwk, OKPJwk) + assert public_jwk.public_key == bytes.fromhex( + """3e b7 a8 29 b0 cd 20 f5 bc fc 0b 59 9b 6f ec cf + 6d a4 62 71 07 bd b0 d4 f3 45 b4 30 27 d8 b9 72 + fc 3e 34 fb 42 32 a1 3c a7 06 dc b5 7a ec 3d ae + 07 bd c1 c6 7b f3 36 09""" + ) + + ephemeral_secret = bytes.fromhex( + """9a 8f 49 25 d1 51 9f 57 75 cf 46 b0 4b 58 00 d4 + ee 9e e8 ba e8 bc 55 65 d4 98 c2 8d d9 c9 ba f5 + 74 a9 41 97 44 89 73 91 00 63 82 a6 f1 27 ab 1d + 9a c2 d8 c0 a5 98 72 6b""" + ) + + ephemeral_private_key = OKPJwk.from_bytes(ephemeral_secret, use="enc") + + assert ephemeral_private_key.public_jwk() == Jwk( + { + "kty": "OKP", + "crv": "X448", + "x": "mwj3zDG34-Z9ItWuoSEHSic70rg94Jxj-qc9LCLF2bvINmRyQdlT1AxbEtqIEg1TF3-A5TLEH6A", + } + ) + + ephemeral_private_key["kid"] = "Bob" + + sender_shared_key = EcdhEs.ecdh( + private_key=ephemeral_private_key.cryptography_key, + public_key=public_jwk.cryptography_key, + ) + assert sender_shared_key == bytes.fromhex( + """07 ff f4 18 1a c6 cc 95 ec 1c 16 a9 4a 0f 74 d1 + 2d a2 32 ce 40 a7 75 52 28 1d 28 2b b6 0c 0b 56 + fd 24 64 c3 35 54 39 36 52 1c 24 40 30 85 d5 9a + 44 9a 50 37 51 4a 87 9d""" + ) + + def test_unknown_curve() -> None: with pytest.raises(UnsupportedOKPCurve): Jwk({"kty": "OKP", "crv": "foobar", "x": "abcd"}) @@ -126,3 +239,69 @@ def test_pem_key(crv: str) -> None: def test_from_cryptography_key_unknown_type() -> None: with pytest.raises(TypeError): OKPJwk.from_cryptography_key("this is not a cryptography key") + + +@pytest.mark.parametrize( + "private_key, crv, use", + [ + (b"a" * 32, "Ed25519", "sig"), + (b"a" * 32, "X25519", "enc"), + (b"a" * 57, "Ed448", "sig"), + (b"a" * 56, "X448", "enc"), + ], +) +def test_from_bytes(private_key: bytes, crv: str, use: str) -> None: + # initializing an OKP with a private key of the appropriate length, and with 'crv' and 'use' parameters will always work + jwk = OKPJwk.from_bytes(private_key, crv=crv, use=use) + assert isinstance(jwk, OKPJwk) + assert jwk.crv == crv + assert jwk.use == use + + # initializing X448 and Ed448 keys with just a private key and no other hint will work + if len(private_key) != 32: + jwk = OKPJwk.from_bytes(private_key) + assert isinstance(jwk, OKPJwk) + assert jwk.crv == crv + assert jwk.use == use + else: + # initializing a key with a 32 bytes key need a crv or use hint + with pytest.raises(ValueError): + OKPJwk.from_bytes(private_key) + + jwk = OKPJwk.from_bytes(private_key, crv=crv) + assert isinstance(jwk, OKPJwk) + assert jwk.crv == crv + assert jwk.use == use + + jwk = OKPJwk.from_bytes(private_key, use=use) + assert isinstance(jwk, OKPJwk) + assert jwk.crv == crv + assert jwk.use == use + + # trying to initialize an OKPJwk with inconsistent hints will not work + with pytest.raises(ValueError): + OKPJwk.from_bytes(private_key, crv=crv, use="sig" if use == "enc" else "enc") + + with pytest.raises(ValueError): + OKPJwk.from_bytes( + private_key, + crv={ + "Ed25519": "X25519", + "X25519": "Ed25519", + "Ed448": "X448", + "X448": "Ed448", + }.get(crv), + use=use, + ) + + # trying an unknown crv will raise an UnsupportedOKPCurve + with pytest.raises(UnsupportedOKPCurve): + OKPJwk.from_bytes(private_key, crv="foo") + + # trying an unknown use with raise a ValueError: + with pytest.raises(ValueError): + OKPJwk.from_bytes(private_key, use="foo") + + # trying to initialize an OKPJwk with a wrong key size will not work + with pytest.raises(ValueError): + OKPJwk.from_bytes(private_key + b"bb") diff --git a/tests/test_jwk/test_rsa.py b/tests/test_jwk/test_rsa.py index 7b3bae5..747af40 100644 --- a/tests/test_jwk/test_rsa.py +++ b/tests/test_jwk/test_rsa.py @@ -1,5 +1,7 @@ import pytest from binapy import BinaPy +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa from jwskate import InvalidJwk, Jwk, RSAJwk @@ -223,3 +225,30 @@ def test_optional_parameters() -> None: with pytest.raises(ValueError): jwk.public_jwk().with_optional_private_parameters() + + +def test_from_cryptography_key() -> None: + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + private_jwk = RSAJwk.from_cryptography_key(private_key) + assert private_jwk.is_private + assert private_jwk.cryptography_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) == private_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + + public_key = private_key.public_key() + public_jwk = RSAJwk.from_cryptography_key(public_key) + assert not public_jwk.is_private + assert public_jwk.cryptography_key.public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.PKCS1 + ) == public_key.public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.PKCS1 + ) + + with pytest.raises(TypeError): + RSAJwk.from_cryptography_key(b"foo") diff --git a/tests/test_jwk/test_symmetric.py b/tests/test_jwk/test_symmetric.py index b54bffe..fbe218d 100644 --- a/tests/test_jwk/test_symmetric.py +++ b/tests/test_jwk/test_symmetric.py @@ -1,6 +1,6 @@ import pytest -from jwskate import SymmetricJwk +from jwskate import Jwk, SymmetricJwk @pytest.fixture( @@ -42,3 +42,49 @@ def test_pem_key() -> None: private_jwk = SymmetricJwk.generate(key_size=128) with pytest.raises(TypeError): private_jwk.to_pem() + + +def test_aesgcmkw() -> None: + alg = "A128GCMKW" + enc = "A128GCM" + jwk = Jwk.generate_for_alg(alg) + sender_cek, wrapped_cek, headers = jwk.sender_key(enc) + assert sender_cek + assert wrapped_cek + assert "iv" in headers + assert "tag" in headers + + recipient_cek = jwk.recipient_key(wrapped_cek, enc, **headers) + assert recipient_cek == sender_cek + + # missing 'iv' in headers + with pytest.raises(ValueError): + jwk.recipient_key(wrapped_cek, enc, **{"tag": headers["tag"]}) + + # missing 'tag' in headers + with pytest.raises(ValueError): + jwk.recipient_key(wrapped_cek, enc, **{"iv": headers["iv"]}) + + +@pytest.mark.parametrize( + "alg, key_size", + [ + ("HS256", 256), + ("HS384", 384), + ("HS512", 512), + ("A128CBC-HS256", 256), + ("A192CBC-HS384", 384), + ("A256CBC-HS512", 512), + ("A128GCM", 128), + ("A192GCM", 192), + ("A256GCM", 256), + ("A128KW", 128), + ("A192KW", 192), + ("A256KW", 256), + ("A128GCMKW", 128), + ("A192GCMKW", 192), + ("A256GCMKW", 256), + ], +) +def test_generate_for_alg(alg: str, key_size: int) -> None: + assert SymmetricJwk.generate_for_alg(alg).key_size == key_size diff --git a/tests/test_jws.py b/tests/test_jws.py index 60211b5..df6ceec 100644 --- a/tests/test_jws.py +++ b/tests/test_jws.py @@ -287,7 +287,9 @@ def signature_jwk( if signature_alg in key.supported_signing_algorithms(): return key - pytest.skip(f"No key supports this signature alg: {signature_alg}") + pytest.skip( + f"No key supports this signature alg: {signature_alg}" + ) # pragma: no cover @pytest.fixture() @@ -492,7 +494,7 @@ def test_verify_signature_from_jwcrypto( ) -def test_invalid_jws() -> None: +def test_invalid_jws_compact() -> None: with pytest.raises(ValueError): JwsCompact( "ey.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cOUKU1ijv3KiN2KK_o50RU978I9MzQ4lNw2y7nOGAdM" @@ -505,3 +507,12 @@ def test_invalid_jws() -> None: JwsCompact( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.!!" ) + + +def test_invalid_jws_json() -> None: + with pytest.raises(AttributeError): + JwsJsonFlat({}).payload + with pytest.raises(AttributeError): + JwsJsonGeneral({}).payload + with pytest.raises(AttributeError): + JwsJsonGeneral({}).signatures diff --git a/tests/test_jwt.py b/tests/test_jwt.py index 0205309..805665b 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -48,29 +48,24 @@ def test_signed_jwt() -> None: assert jwt.expires_at == datetime.fromtimestamp(1629204620, tz=timezone.utc) assert jwt.issued_at == datetime.fromtimestamp(1629204560, tz=timezone.utc) assert jwt.nonce == jwt["nonce"] + + # validating with the appropriate key must work jwt.validate( - jwk=Jwk( - { - "kty": "RSA", - "alg": "RS256", - "kid": "my_key", - "n": "2m4QVSHdUo2DFSbGY24cJbxE10KbgdkSCtm0YZ1q0Zmna8pJg8YhaWCJHV7D5AxQ_L1b1PK0jsdpGYWc5-Pys0FB2hyABGPxXIdg1mjxn6geHLpWzsA3MHD29oqfl0Rt7g6AFc5St3lBgJCyWtci6QYBmBkX9oIMOx9pgv4BaT6y1DdrNh27-oSMXZ0a58KwnC6jbCpdA3V3Eume-Be1Tx9lJN3j6S8ydT7CGY1Xd-sc3oB8pXfkr1_EYf0Sgb9EwOJfqlNK_kVjT3GZ-1JJMKJ6zkU7H0yXe2SKXAzfayvJaIcYrk-sYwmf-u7yioOLLvjlGjysN7SOSM8socACcw", - "e": "AQAB", - "d": "RldleRTzwi8CRKB9CO4fsGNFxBCWJaWy8r2TIlBgYulZihPVwtLeVaIZ5dRrvxfcSNfuJ9CVJtm-1dI6ak71DJb6TvQYodFRm9uY6tNW5HRuZg_3_pLV8wqd7V1M8Zi-0gfnZZ5Q8vbgijeOyEQ54NLnVoTWO7M7nxqJjv6fk7Vd1vd6Gy8jI_soA6AMFCSAF-Vab07jGklBaLyow_TdczYufQ1737RNsFra2l43esAKeavxxkr7Js6OpgUkrXPEOc19GAwJLDdfkZ6yJLR8poWwX_OD-Opmvqmq6BT0s0mAyjBKZUxTGJuD3hm6mKOxXrbJOKY_UXRN7EAuH6U0gQ", - "p": "9WQs9id-xB2AhrpHgyt4nfljXFXjaDqRHzUydw15HAOoSZzYMZJW-GT8g2hB3oH3EsSCuMh70eiE1ohTLeipYdJ-s7Gy5qTH5-CblT_OfLXxi2hIumdTx53w-AtDEWl2PRt_qGHZ0B83NjVU2fo96kp9bgJWYh_iWWtSJyabXbM", - "q": "499_fCUhh5zL-3a4WGENy_yrsAa5C1sylZUtokyJNYBz68kWRFHFsArXnwZifBD_GWBgJQtldsouqvvPxzAlHQB9kfhxaRbaugwVePSjgHYmhd-NhAySq7rBURvRquAxJmoBmN2lS54YyN_X-VAKgfHDNsN7f7LIw9ISrLeR6EE", - "dp": "Cfxwo_fJfduhfloYTOs49lzOwVQxc-1mOHnmuteOhShU8eHzHllRNryNVh-pBpANaPMcSr7F4y3uMfjMQcMFGZkCVPe3SxGLnRET48f79DFHSiANTaCk1SvFQaLbsNq02BnFYSnSPlj22zriYBiB6oXrgs2PjGC1ymPGrRcyHWc", - "dq": "hL-4AfeTn_AtORJBdGMd6X8J-eMAu-fmARRF4G3b5Qou_eZIjYZhtxup31-V0hcItZzahdoswtYn9734nl6i0FFv1bC5SPJie838WFmUQosSCB1i0NGORHLombquG3C90VYiFg7Rc8rnP2Z_6CLD7E2OXwHkmVDq-oEQFgRfAME", - "qi": "riPJlv9XNjdheryQWGr7Rhlvp9rxeNyWfVzj3y_IGh3tpe--Cd6-1GUrF00HLTTc-5iKVIa-FWOeMPTYc2_Uldi_0qWlrKjM5teIpUlDJbz7Ha-bfed9-eTbG8cI5F57KdDjbjB8YgqWYKz4YPMwqZFbWxZi4W_X79Bs3htXcXA", - } - ), + jwk={ + "kty": "RSA", + "alg": "RS256", + "kid": "my_key", + "n": "2m4QVSHdUo2DFSbGY24cJbxE10KbgdkSCtm0YZ1q0Zmna8pJg8YhaWCJHV7D5AxQ_L1b1PK0jsdpGYWc5-Pys0FB2hyABGPxXIdg1mjxn6geHLpWzsA3MHD29oqfl0Rt7g6AFc5St3lBgJCyWtci6QYBmBkX9oIMOx9pgv4BaT6y1DdrNh27-oSMXZ0a58KwnC6jbCpdA3V3Eume-Be1Tx9lJN3j6S8ydT7CGY1Xd-sc3oB8pXfkr1_EYf0Sgb9EwOJfqlNK_kVjT3GZ-1JJMKJ6zkU7H0yXe2SKXAzfayvJaIcYrk-sYwmf-u7yioOLLvjlGjysN7SOSM8socACcw", + "e": "AQAB", + }, issuer="https://myas.local", audience="client_id", check_exp=False, ) + # validating with another key must fail with pytest.raises(InvalidSignature): - jwt.validate(Jwk.generate_for_kty("RSA"), alg="RS256") + jwt.validate(Jwk.generate_for_alg("RS256").public_jwk()) def test_unprotected() -> None: @@ -128,10 +123,10 @@ def test_empty_jwt(private_jwk: Jwk) -> None: assert bytes(jwt) == str(jwt).encode() assert jwt.signed_part == b"eyJhbGciOiJSUzI1NiIsImtpZCI6IkpXSy1BQkNEIn0.e30" - jwt.validate(jwk=private_jwk, check_exp=False) + jwt.validate(jwk=private_jwk.public_jwk(), check_exp=False) with pytest.raises(InvalidClaim): - jwt.validate(jwk=private_jwk) + jwt.validate(jwk=private_jwk.public_jwk()) def test_validate() -> None: @@ -335,7 +330,7 @@ def test_sign_and_encrypt() -> None: assert isinstance(inner_jwt, SignedJwt) assert inner_jwt.alg == sign_alg assert inner_jwt.claims == claims - assert inner_jwt.verify_signature(sign_jwk) + assert inner_jwt.verify_signature(sign_jwk.public_jwk()) assert inner_jwt.kid == sign_jwk.kid verified_inner_jwt = Jwt.decrypt_and_verify( @@ -343,15 +338,30 @@ def test_sign_and_encrypt() -> None: ) assert isinstance(verified_inner_jwt, SignedJwt) + # try to encrypt a JWT with an altered signature altered_inner_jwt = bytes(verified_inner_jwt)[:-4] + ( b"aaaa" if not verified_inner_jwt.value.endswith(b"aaaa") else b"bbbb" ) - enc_altered_jwe = JweCompact.encrypt(altered_inner_jwt, jwk=enc_jwk, enc=enc) + enc_altered_jwe = JweCompact.encrypt( + altered_inner_jwt, jwk=enc_jwk.public_jwk(), enc=enc + ) with pytest.raises(InvalidSignature): Jwt.decrypt_and_verify( enc_altered_jwe, enc_jwk=enc_jwk, sig_jwk=sign_jwk.public_jwk() ) + # trying to decrypt and verify a JWE nested in a JWE will raise a ValueError + inner_jwe = JweCompact.encrypt( + b"this_is_a_test", + jwk=Jwk.generate_for_alg("ECDH-ES+A128KW").public_jwk(), + enc="A128GCM", + ) + nested_inner_jwe = JweCompact.encrypt(inner_jwe, jwk=enc_jwk.public_jwk(), enc=enc) + with pytest.raises(ValueError): + Jwt.decrypt_and_verify( + nested_inner_jwe, enc_jwk=enc_jwk, sig_jwk=sign_jwk.public_jwk() + ) + def test_sign_without_alg() -> None: jwk = Jwk.generate_for_kty("RSA") @@ -363,7 +373,7 @@ def test_large_jwt() -> None: with pytest.raises(ValueError): Jwt( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." - f"{'alargevalue'*16*1024}" + f"{'alargevalue' * 16 * 1024}" "bl5iNgXfkbmgDXItaUx7_1lUMNtOffihsShVP8MeE1g" ) @@ -377,6 +387,16 @@ def test_eq() -> None: assert Jwt(jwt) != 1 +def test_headers() -> None: + jwt = Jwt( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im15X2tpZCIsImN0eSI6Im15X2N0eSJ9.e30.XYERQ3ODkqLEnQvcak8wHEVJMtEqNNUmzRGRtjmqcdE" + ) + assert jwt.alg == "HS256" + assert jwt.typ == "JWT" + assert jwt.kid == "my_kid" + assert jwt.cty == "my_cty" + + def test_invalid_headers() -> None: jwt = Jwt( "eyJhbGciOjEsImtpZCI6MSwidHlwIjoxLCJjdHkiOjF9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cOUKU1ijv3KiN2KK_o50RU978I9MzQ4lNw2y7nOGAdM"