From be9ac805b96f1df6e58906bb89dbfb07d80983a9 Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Fri, 30 Sep 2022 14:17:32 +0200 Subject: [PATCH] 0.4 (#2) _Breaking Changes:_ * rename `include_kid_thumbprint()` to `with_kid_thumbprint()` * rename AES CBC HMAC classes from Aes???CbcHmacSha??? to A???CBC_HS??? * SignedJwt.audiences will always be a list (empty if no aud claim in token) _Other changes and improvements:_ * support for signed then encrypted JWTs * add `RSAJwk.key_size` property * added tests for `RSAJwk.key_size` property * `Jwk.include_kid_thumbprint()` only returns a new instance when it modifies the kid * add `to_pem()` in `Jwk` base class * add `Jwk.is_symmetric` property * add `use` and `key_ops` properties * add methods to add or remove key usage parameters * RSAJwk can compute the optional parameters if not present in the JWK. Add methods to add or remove those optional parameters. * make sure key_ops are kept when using `public_jwk()` only when the param existed in the private JWK. * add methods to get signature verification and encryption keys from JwkSet * warn when generating EC keys with an implicit curve * misc fixes and improvements --- .coveragerc | 1 + .github/workflows/dev.yml | 4 +- .pre-commit-config.yaml | 8 +- README.md | 243 ++++++----- docs/recipes.md | 189 +++++++++ docs/usage.md | 19 +- jwskate/__init__.py | 16 +- jwskate/enums.py | 11 +- jwskate/jwa/__init__.py | 24 +- jwskate/jwa/base.py | 59 ++- jwskate/jwa/ec.py | 7 +- jwskate/jwa/encryption/__init__.py | 8 +- jwskate/jwa/encryption/aescbchmac.py | 76 +++- jwskate/jwa/encryption/aesgcm.py | 42 +- jwskate/jwa/key_mgmt/aesgcmkw.py | 35 +- jwskate/jwa/key_mgmt/aeskw.py | 6 +- jwskate/jwa/key_mgmt/ecdh.py | 4 +- jwskate/jwa/key_mgmt/pbes2.py | 8 +- jwskate/jwa/key_mgmt/rsa.py | 21 +- jwskate/jwa/okp.py | 5 +- jwskate/jwa/signature/ec.py | 33 +- jwskate/jwa/signature/eddsa.py | 16 +- jwskate/jwa/signature/hmac.py | 25 +- jwskate/jwa/signature/rsa.py | 31 +- jwskate/jwe/__init__.py | 5 +- jwskate/jwe/compact.py | 28 +- jwskate/jwk/alg.py | 158 ++----- jwskate/jwk/base.py | 299 +++++++++++--- jwskate/jwk/ec.py | 75 ++-- jwskate/jwk/jwks.py | 41 +- jwskate/jwk/oct.py | 71 +++- jwskate/jwk/okp.py | 56 ++- jwskate/jwk/rsa.py | 106 ++++- jwskate/jws/compact.py | 61 ++- jwskate/jws/json.py | 2 +- jwskate/jwt/__init__.py | 2 - jwskate/jwt/base.py | 112 ++++- jwskate/jwt/encrypted.py | 16 - jwskate/jwt/signed.py | 4 +- jwskate/token.py | 38 +- mkdocs.yml | 1 + poetry.lock | 597 +++++++++++++++++++-------- pyproject.toml | 12 +- tests/test_jwa.py | 84 +++- tests/test_jwe.py | 72 +++- tests/test_jwk/test_alg.py | 57 +++ tests/test_jwk/test_ec.py | 22 +- tests/test_jwk/test_generate.py | 45 ++ tests/test_jwk/test_jwk.py | 17 +- tests/test_jwk/test_jwks.py | 118 +++++- tests/test_jwk/test_okp.py | 61 ++- tests/test_jwk/test_rsa.py | 102 ++++- tests/test_jwk/test_symmetric.py | 8 +- tests/test_jws.py | 146 +++++-- tests/test_jwt.py | 235 ++++++++++- 55 files changed, 2733 insertions(+), 809 deletions(-) create mode 100644 docs/recipes.md delete mode 100644 jwskate/jwt/encrypted.py create mode 100644 tests/test_jwk/test_alg.py create mode 100644 tests/test_jwk/test_generate.py diff --git a/.coveragerc b/.coveragerc index 9275e88..1a48dba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,3 +12,4 @@ exclude_lines = if 0: if __name__ == .__main__.: def main + \.\.\. diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 0401fe8..99ca90a 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: python-versions: [3.7, 3.8, 3.9, '3.10'] - os: [ubuntu-18.04, macos-latest, windows-latest] + 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 @@ -78,7 +78,7 @@ jobs: poetry build - name: publish to Test PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.TEST_PYPI_API_TOKEN}} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9ad84d..ffc2fa8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.0 + rev: v1.3.1 hooks: - id: forbid-crlf - id: remove-crlf @@ -15,7 +15,7 @@ repos: - id: check-yaml args: [--unsafe] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.14 + rev: 0.7.16 hooks: - id: mdformat args: @@ -32,7 +32,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 @@ -53,7 +53,7 @@ repos: args: - --add-ignore=D107 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.981 hooks: - id: mypy args: [--strict] diff --git a/README.md b/README.md index 61845ad..b5e46f6 100644 --- a/README.md +++ b/README.md @@ -3,8 +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 Json Web Signature, Keys, Algorithms, Tokens and Encryption (RFC7514 to 7519), and their -extensions ECDH Signatures (RFC8037), and JWK Thumbprints (RFC7638). +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: @@ -14,8 +15,14 @@ A quick usage example, generating an RSA private key, signing some data, then va ```python from jwskate import Jwk -# generate a RSA Jwk and sign a plaintext with it -rsa_private_jwk = Jwk.generate_for_kty("RSA", key_size=2048, kid="my_key", alg="RS256") +# let's generate a random private key +rsa_private_jwk = ( + Jwk.generate_for_alg( # generated key will automatically be RSA, based on the required 'alg' + alg="RS256", key_size=2048 + ) + .with_kid_thumbprint() # include an RFC7638 thumbprint as key id + .with_usage_parameters() # include the appropriate 'use' and 'key_ops' parameters in the JWK, based on the 'alg' +) data = b"Signing is easy!" signature = rsa_private_jwk.sign(data) @@ -30,57 +37,60 @@ assert isinstance(rsa_private_jwk, dict) # Jwk are dict print(rsa_private_jwk) ``` -The result of this print JWK will look like this: +The result of this print will look like this (with the random parts abbreviated to `...` for display purposes only): ``` -{ 'kty': 'RSA', - 'n': '...', - 'e': 'AQAB', - 'd': '...', - 'p': '...', - 'q': '...', - 'dp': '...', - 'dq': '...', - 'qi': '...', - 'kid': 'my_key', - 'alg': 'RS256', -} +{'kty': 'RSA', + 'n': '...', + 'e': 'AQAB', + 'd': '...', + 'p': '...', + 'q': '...', + 'dp': '...', + 'dq': '...', + 'qi': '...', + 'alg': 'RS256', + 'kid': '...', + 'use': 'sig', + 'key_ops': ['sign']} ``` -Now let's sign a JWT containing arbitrary claims: +Now let's sign a JWT containing arbitrary claims, this time using an EC key: ```python from jwskate import Jwk, Jwt -private_jwk = Jwk.generate_for_kty("EC", kid="my_key") +private_jwk = Jwk.generate_for_alg( + "ES256", kid="my_key" +) # let's specify a kid manually claims = {"sub": "some_sub", "claim1": "value1"} -sign_alg = "ES256" -jwt = Jwt.sign(claims, private_jwk, sign_alg) +jwt = Jwt.sign(claims, private_jwk) # that's it! we have a signed JWT assert jwt.claims == claims # claims can be accessed as a dict assert jwt.sub == "some_sub" # or individual claims can be accessed as attributes assert jwt["claim1"] == "value1" # or as dict items -assert jwt.alg == sign_alg # alg and kid headers are also accessible as attributes +assert jwt.alg == "ES256" # alg and kid headers are also accessible as attributes assert jwt.kid == private_jwk.kid -assert jwt.verify_signature(private_jwk.public_jwk(), sign_alg) +assert jwt.verify_signature(private_jwk.public_jwk()) print(jwt) -``` +# eyJhbGciOiJFUzI1NiIsImtpZCI6Im15a2V5In0.eyJzdWIiOiJzb21lX3N1YiIsImNsYWltMSI6InZhbHVlMSJ9.C1KcDyDT8qXwUqcWzPKkQD7f6xai-gCgaRFMdKPe80Vk7XeYNa8ovuLwvdXgGW4ZZ_lL73QIyncY7tHGXUthag +# This will output the full JWT compact representation. You can inspect it for example at -This will output the full JWT compact representation. You can inspect it for example at - -``` -eyJhbGciOiJFUzI1NiIsImtpZCI6Im15a2V5In0.eyJzdWIiOiJzb21lX3N1YiIsImNsYWltMSI6InZhbHVlMSJ9.C1KcDyDT8qXwUqcWzPKkQD7f6xai-gCgaRFMdKPe80Vk7XeYNa8ovuLwvdXgGW4ZZ_lL73QIyncY7tHGXUthag +print(jwt.headers) +# {'alg': 'ES256', 'kid': 'my_key'} ``` +Note above that the JWT headers are automatically generated with the appropriate values. + Or let's sign a JWT with the standardised lifetime, subject, audience and ID claims: ```python from jwskate import Jwk, JwtSigner -private_jwk = Jwk.generate_for_kty("EC") -signer = JwtSigner(issuer="https://myissuer.com", jwk=private_jwk, alg="ES256") +private_jwk = Jwk.generate_for_alg("ES256") +signer = JwtSigner(issuer="https://myissuer.com", jwk=private_jwk) jwt = signer.sign( subject="some_sub", audience="some_aud", @@ -118,29 +128,37 @@ The generated JWT claims will include the standardised claims: - Relies on [cryptography](https://cryptography.io) for all cryptographic operations - Relies on [BinaPy](https://guillp.github.io/binapy/) for binary data manipulations +### Supported Token Types + +| Token Type | Support | +|---------------------------|--------------------------------------------------------------------------| +| Json Web Signature (JWS) | ☑ Compact
☑ JSON Flat
☑ JSON General
| +| Json Web Encryption (JWE) | ☑ Compact
☐ JSON Flat
☐ JSON General
| +| Json Web Tokens (JWT) | ☑ Signed
☑ Signed and Encrypted | + ### Supported Signature algorithms `jwskate` supports the following signature algorithms: | Signature Alg | Description | Key Type | Reference | Note | |---------------|------------------------------------------------|----------|------------------------------------|-----------------------| -| HS256 | HMAC using SHA-256 | oct | RFC7518, Section 3.2 | | -| HS384 | HMAC using SHA-384 | oct | RFC7518, Section 3.2 | | -| HS512 | HMAC using SHA-512 | oct | RFC7518, Section 3.2 | | -| RS256 | RSASSA-PKCS1-v1_5 using SHA-256 | RSA | RFC7518, Section 3.3 | | -| RS384 | RSASSA-PKCS1-v1_5 using SHA-384 | RSA | RFC7518, Section 3.3 | | -| RS512 | RSASSA-PKCS1-v1_5 using SHA-512 | RSA | RFC7518, Section 3.3 | | -| ES256 | ECDSA using P-256 and SHA-256 | EC | RFC7518, Section 3.4 | | -| ES384 | ECDSA using P-384 and SHA-384 | EC | RFC7518, Section 3.4 | | -| ES512 | ECDSA using P-521 and SHA-512 | EC | RFC7518, Section 3.4 | | -| PS256 | RSASSA-PSS using SHA-256 and MGF1 with SHA-256 | RSA | RFC7518, Section 3.5 | | -| PS384 | RSASSA-PSS using SHA-384 and MGF1 with SHA-384 | RSA | RFC7518, Section 3.5 | | -| PS512 | RSASSA-PSS using SHA-512 and MGF1 with SHA-512 | RSA | RFC7518, Section 3.5 | | -| EdDSA | EdDSA signature algorithms | OKP | RFC8037, Section 3.1 | | -| ES256K | ECDSA using secp256k1 curve and SHA-256 | EC | RFC8812, Section 3.2 | | +| HS256 | HMAC using SHA-256 | oct | [RFC7518, Section 3.2] | | +| HS384 | HMAC using SHA-384 | oct | [RFC7518, Section 3.2] | | +| HS512 | HMAC using SHA-512 | oct | [RFC7518, Section 3.2] | | +| RS256 | RSASSA-PKCS1-v1_5 using SHA-256 | RSA | [RFC7518, Section 3.3] | | +| RS384 | RSASSA-PKCS1-v1_5 using SHA-384 | RSA | [RFC7518, Section 3.3] | | +| RS512 | RSASSA-PKCS1-v1_5 using SHA-512 | RSA | [RFC7518, Section 3.3] | | +| ES256 | ECDSA using P-256 and SHA-256 | EC | [RFC7518, Section 3.4] | | +| ES384 | ECDSA using P-384 and SHA-384 | EC | [RFC7518, Section 3.4] | | +| ES512 | ECDSA using P-521 and SHA-512 | EC | [RFC7518, Section 3.4] | | +| PS256 | RSASSA-PSS using SHA-256 and MGF1 with SHA-256 | RSA | [RFC7518, Section 3.5] | | +| PS384 | RSASSA-PSS using SHA-384 and MGF1 with SHA-384 | RSA | [RFC7518, Section 3.5] | | +| PS512 | RSASSA-PSS using SHA-512 and MGF1 with SHA-512 | RSA | [RFC7518, Section 3.5] | | +| EdDSA | EdDSA signature algorithms | OKP | [RFC8037, Section 3.1] | | +| ES256K | ECDSA using secp256k1 curve and SHA-256 | EC | [RFC8812, Section 3.2] | | | HS1 | HMAC using SHA-1 | oct | https://www.w3.org/TR/WebCryptoAPI | Validation Only | | RS1 | RSASSA-PKCS1-v1_5 with SHA-1 | oct | https://www.w3.org/TR/WebCryptoAPI | Validation Only | -| none | No digital signature or MAC performed | | RFC7518, Section 3.6 | Not usable by mistake | +| none | No digital signature or MAC performed | | [RFC7518, Section 3.6] | Not usable by mistake | ### Supported Key Management algorithms @@ -148,66 +166,66 @@ The generated JWT claims will include the standardised claims: | Signature Alg | Description | Key Type | Reference | Note | |--------------------|-------------------------------------------------|----------|------------------------------------|-------------| -| RSA1_5 | RSAES-PKCS1-v1_5 | RSA | RFC7518, Section 4.2 | Unwrap Only | -| RSA-OAEP | RSAES OAEP using default parameters | RSA | RFC7518, Section 4.3 | | -| RSA-OAEP-256 | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | RSA | RFC7518, Section 4.3 | | +| RSA1_5 | RSAES-PKCS1-v1_5 | RSA | [RFC7518, Section 4.2] | Unwrap Only | +| RSA-OAEP | RSAES OAEP using default parameters | RSA | [RFC7518, Section 4.3] | | +| RSA-OAEP-256 | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | RSA | [RFC7518, Section 4.3] | | | RSA-OAEP-384 | RSA-OAEP using SHA-384 and MGF1 with SHA-384 | RSA | https://www.w3.org/TR/WebCryptoAPI | | | RSA-OAEP-512 | RSA-OAEP using SHA-512 and MGF1 with SHA-512 | RSA | https://www.w3.org/TR/WebCryptoAPI | | -| A128KW | AES Key Wrap using 128-bit key | oct | RFC7518, Section 4.4 | | -| A192KW | AES Key Wrap using 192-bit key | oct | RFC7518, Section 4.4 | | -| A256KW | AES Key Wrap using 256-bit key | oct | RFC7518, Section 4.4 | | -| dir | Direct use of a shared symmetric key | oct | RFC7518, Section 4.5 | | -| ECDH-ES | ECDH-ES using Concat KDF | EC | RFC7518, Section 4.6 | | -| ECDH-ES+A128KW | ECDH-ES using Concat KDF and "A128KW" wrapping | EC | RFC7518, Section 4.6 | | -| ECDH-ES+A192KW | ECDH-ES using Concat KDF and "A192KW" wrapping | EC | RFC7518, Section 4.6 | | -| ECDH-ES+A256KW | ECDH-ES using Concat KDF and "A256KW" wrapping | EC | RFC7518, Section 4.6 | | -| A128GCMKW | Key wrapping with AES GCM using 128-bit key | oct | RFC7518, Section 4.7 | | -| A192GCMKW | Key wrapping with AES GCM using 192-bit key | oct | RFC7518, Section 4.7 | | -| A256GCMKW | Key wrapping with AES GCM using 256-bit key | oct | RFC7518, Section 4.7 | | -| PBES2-HS256+A128KW | PBES2 with HMAC SHA-256 and "A128KW" wrapping | password | RFC7518, Section 4.8 | | -| PBES2-HS384+A192KW | PBES2 with HMAC SHA-384 and "A192KW" wrapping | password | RFC7518, Section 4.8 | | -| PBES2-HS512+A256KW | PBES2 with HMAC SHA-512 and "A256KW" wrapping | password | RFC7518, Section 4.8 | | +| A128KW | AES Key Wrap using 128-bit key | oct | [RFC7518, Section 4.4] | | +| A192KW | AES Key Wrap using 192-bit key | oct | [RFC7518, Section 4.4] | | +| A256KW | AES Key Wrap using 256-bit key | oct | [RFC7518, Section 4.4] | | +| dir | Direct use of a shared symmetric key | oct | [RFC7518, Section 4.5] | | +| ECDH-ES | ECDH-ES using Concat KDF | EC | [RFC7518, Section 4.6] | | +| ECDH-ES+A128KW | ECDH-ES using Concat KDF and "A128KW" wrapping | EC | [RFC7518, Section 4.6] | | +| ECDH-ES+A192KW | ECDH-ES using Concat KDF and "A192KW" wrapping | EC | [RFC7518, Section 4.6] | | +| ECDH-ES+A256KW | ECDH-ES using Concat KDF and "A256KW" wrapping | EC | [RFC7518, Section 4.6] | | +| A128GCMKW | Key wrapping with AES GCM using 128-bit key | oct | [RFC7518, Section 4.7] | | +| A192GCMKW | Key wrapping with AES GCM using 192-bit key | oct | [RFC7518, Section 4.7] | | +| A256GCMKW | Key wrapping with AES GCM using 256-bit key | oct | [RFC7518, Section 4.7] | | +| PBES2-HS256+A128KW | PBES2 with HMAC SHA-256 and "A128KW" wrapping | password | [RFC7518, Section 4.8] | | +| PBES2-HS384+A192KW | PBES2 with HMAC SHA-384 and "A192KW" wrapping | password | [RFC7518, Section 4.8] | | +| PBES2-HS512+A256KW | PBES2 with HMAC SHA-512 and "A256KW" wrapping | password | [RFC7518, Section 4.8] | | ### Supported Encryption algorithms `jwskate` supports the following encryption algorithms: -| Signature Alg | Description | Reference | -|----------------|-------------------------------------------------------------|-------------------------| -| A128CBC-HS256 | AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm | RFC7518, Section 5.2.3 | -| A192CBC-HS384 | AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm | RFC7518, Section 5.2.4 | -| A256CBC-HS512 | AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm | RFC7518, Section 5.2.5 | -| A128GCM | AES GCM using 128-bit key | RFC7518, Section 5.3 | -| A192GCM | AES GCM using 192-bit key | RFC7518, Section 5.3 | -| A256GCM | AES GCM using 256-bit key | RFC7518, Section 5.3 | +| Signature Alg | Description | Reference | +|----------------|-------------------------------------------------------------|--------------------------| +| A128CBC-HS256 | AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm | [RFC7518, Section 5.2.3] | +| A192CBC-HS384 | AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm | [RFC7518, Section 5.2.4] | +| A256CBC-HS512 | AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm | [RFC7518, Section 5.2.5] | +| A128GCM | AES GCM using 128-bit key | [RFC7518, Section 5.3] | +| A192GCM | AES GCM using 192-bit key | [RFC7518, Section 5.3] | +| A256GCM | AES GCM using 256-bit key | [RFC7518, Section 5.3] | ### Supported Elliptic Curves `jwskate` supports the following Elliptic Curves: -| Curve | Description | Key Type | Usage | Reference | -|-----------|---------------------------------------|----------|-----------------------|--------------------------| -| P-256 | P-256 Curve | EC | signature, encryption | RFC7518, Section 6.2.1.1 | -| P-384 | P-384 Curve | EC | signature, encryption | RFC7518, Section 6.2.1.1 | -| P-521 | P-521 Curve | EC | signature, encryption | RFC7518, Section 6.2.1.1 | -| secp256k1 | SECG secp256k1 curve | EC | signature, encryption | RFC8812, Section 3.1 | -| Ed25519 | Ed25519 signature algorithm key pairs | OKP | signature | RFC8037, Section 3.1 | -| Ed448 | Ed448 signature algorithm key pairs | OKP | signature | RFC8037, Section 3.1 | -| X25519 | X25519 function key pairs | OKP | encryption | RFC8037, Section 3.2 | -| X448 | X448 function key pairs | OKP | encryption | RFC8037, Section 3.2 | +| Curve | Description | Key Type | Usage | Reference | +|-----------|---------------------------------------|----------|-----------------------|----------------------------| +| P-256 | P-256 Curve | EC | signature, encryption | [RFC7518, Section 6.2.1.1] | +| P-384 | P-384 Curve | EC | signature, encryption | [RFC7518, Section 6.2.1.1] | +| P-521 | P-521 Curve | EC | signature, encryption | [RFC7518, Section 6.2.1.1] | +| Ed25519 | Ed25519 signature algorithm key pairs | OKP | signature | [RFC8037, Section 3.1] | +| Ed448 | Ed448 signature algorithm key pairs | OKP | signature | [RFC8037, Section 3.1] | +| X25519 | X25519 function key pairs | OKP | encryption | [RFC8037, Section 3.2] | +| X448 | X448 function key pairs | OKP | encryption | [RFC8037, Section 3.2] | +| secp256k1 | SECG secp256k1 curve | EC | signature, encryption | [RFC8812, Section 3.1] | ## Why a new lib ? -There are already multiple implementations of JOSE and Json Web Crypto related specifications in Python. However, I have +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): lacks support for JWK, JWE, JWS, requires keys in PEM format. -- [JWCrypto](https://jwcrypto.readthedocs.io/): very inconsistent and complex API. -- [Python-JOSE](https://python-jose.readthedocs.io/): lacks easy support for JWT validation - (checking the standard claims like iss, exp, etc.), lacks easy access to claims +- [PyJWT](https://pyjwt.readthedocs.io) +- [JWCrypto](https://jwcrypto.readthedocs.io/) +- [Python-JOSE](https://python-jose.readthedocs.io/) +- [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, or they just 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 @@ -215,30 +233,61 @@ support some important features, or they just don't feel easy-enough, Pythonic-e 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 Json Web tokens in JSON format. +The same is true for Signed or Encrypted Json Web tokens in JSON format. ### JWA Wrappers -While you can directly use `cryptography` to do the cryptographic operations that are described in [JWA](https://www.rfc-editor.org/info/rfc7518), -its usage is not straightforward and gives you plenty of options to carefully select, leaving room for errors. -To work around this, `jwskate` comes with a set of wrappers that implement the exact JWA specification, with minimum +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. ### Safe Signature Verification -For every signature verification method in `jwskate`, you have to provide the expected signature(s) algorithm(s). +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. -Each signature verification accepts 2 args `alg` and `algs`. If you always expect to verify tokens signed with a single -signature algorithm, pass that algorithm ID to alg. If you accept multiple algs (for example, any asymmetric alg that -you consider strong enough), you can instead pass an iterable of allowed algorithms with `algs`. The signature will be -validated as long as it is signed with one of the provided algs. +To specify which signature algorithms are accepted, each signature verification method accepts, in order of preference: -For verification methods that accept a `Jwk` key, you don't have to provide an `alg` or `algs` if that Jwk has the -appropriate `alg` member that define which algorithm is supposed to be used 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. ## Credits All cryptographic operations are handled by [cryptography](https://cryptography.io). + +[rfc7515]: https://www.rfc-editor.org/rfc/rfc7515.html +[rfc7516]: https://www.rfc-editor.org/rfc/rfc7516.html +[rfc7517]: https://www.rfc-editor.org/rfc/rfc7517.html +[rfc7518]: https://www.rfc-editor.org/rfc/rfc7518.html +[rfc7518, section 3.2]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2 +[rfc7518, section 3.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.3 +[rfc7518, section 3.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4 +[rfc7518, section 3.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.5 +[rfc7518, section 3.6]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.6 +[rfc7518, section 4.2]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.2 +[rfc7518, section 4.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.3 +[rfc7518, section 4.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.4 +[rfc7518, section 4.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.5 +[rfc7518, section 4.6]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6 +[rfc7518, section 4.7]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.7 +[rfc7518, section 4.8]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8 +[rfc7518, section 5.2.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3 +[rfc7518, section 5.2.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4 +[rfc7518, section 5.2.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5 +[rfc7518, section 5.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.3 +[rfc7518, section 6.2.1.1]: https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1 +[rfc7519]: https://www.rfc-editor.org/rfc/rfc7519.html +[rfc7638]: https://www.rfc-editor.org/rfc/rfc7638.html +[rfc8037]: https://www.rfc-editor.org/rfc/rfc8037.html +[rfc8037, section 3.1]: https://www.rfc-editor.org/rfc/rfc8037.html#section-3.1 +[rfc8037, section 3.2]: https://www.rfc-editor.org/rfc/rfc8037.html#section-3.2 +[rfc8812, section 3.1]: https://www.rfc-editor.org/rfc/rfc8812.html#section-3.1 +[rfc8812, section 3.2]: https://www.rfc-editor.org/rfc/rfc8812.html#name-ecdsa-signature-with-secp25 +[rfc9278]: https://www.rfc-editor.org/rfc/rfc9278.html diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 0000000..b181215 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,189 @@ +This is a collection of recipes related to `jwskate` usage. + +# JWK + +## Generate private/public key pairs + +```python +from jwskate import Jwk + +private_jwk = ( + Jwk.generate_for_alg("ES256") # select the signature or encryption alg here + .with_kid_thumbprint() # optionally, include a RFC7638 compliant thumbprint as kid + .with_usage_parameters() # optionally, include 'use' and 'key_ops' +) + +print(private_jwk) +# { +# "kty": "EC", +# "crv": "P-256", +# "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY", +# "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk", +# "d": "Lce_08inNOEe6Q9xEGrR9T0CJNQa1o4EhGtDQYAI0N8", +# "alg": "ES256", +# "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA", +# "use": "sig", +# "key_ops": [ +# "sign" +# ] +# } + +public_jwk = private_jwk.public_jwk() +print(public_jwk.to_json(indent=2)) +# { +# "kty": "EC", +# "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA", +# "alg": "ES256", +# "use": "sig", +# "key_ops": [ +# "verify" +# ], +# "crv": "P-256", +# "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY", +# "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk" +# } + +# let's expose this public key as a JWKS: +print(public_jwk.as_jwks().to_json(indent=2)) +# { +# "keys": [ +# { +# "kty": "EC", +# "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA", +# "alg": "ES256", +# "use": "sig", +# "key_ops": [ +# "verify" +# ], +# "crv": "P-256", +# "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY", +# "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk" +# } +# ] +# } +``` + +## Fetching a JWKS from a remote endpoint + +```python +from jwskate import JwkSet +import requests + +raw_jwks = requests.get("https://www.googleapis.com/oauth2/v3/certs").json() +jwkset = JwkSet(raw_jwks) + +print(jwkset) +# { +# "keys": [ +# ... +# ] +# } + +# compared to a raw dict, a JwkSet offers convenience methods like: +if jwkset.is_private: # returns True if the jwks contains at least one private key + raise ValueError( + "JWKS contains private keys!" + ) # an exposed JWKS should only contain public keys + +my_jwk = jwkset.get_jwk_by_kid("my_key_id") # gets a key by key id (kid) +# select keys that is suitable for signature verification +verification_jwks = jwkset.verification_keys() +# select keys that are suitable for encryption +encryption_jwks = jwkset.encryption_keys() +``` + +## Converting between PEM key, JWK and `cryptography` keys + +```python +from jwskate import Jwk + +# generate a sample JWK, any asymmetric type will do: +private_jwk = ( + Jwk.generate_for_alg("ES256") # generates the key + .with_usage_parameters() # adds use and key_ops + .with_kid_thumbprint() # adds the key thumbprint as kid +) +print(private_jwk.to_json(indent=2)) +# {'kty': 'EC', +# 'crv': 'P-256', +# 'x': '8xX1CEhDNNjEySUKLw88YeiVwEOW34BWm0hBkAxqlVU', +# 'y': 'UfZ0JKT7MxdNMyqMKzKcAcYTcuqoXeplcJ3jNfnj3tM', +# 'd': 'T45KDokOKyuhEA92ri5a951c5kjmQfGyh1SrEkonb4s', +# 'alg': 'ES256', +# 'use': 'sig', +# 'key_ops': ['sign'], +# 'kid': '_E8_LoT4QEwctEkGNbiP9dogVDz6Lq9i8G_fj9nnEo0'} + +# get the cryptography key that is wrapped in the Jwk: +cryptography_private_key = private_jwk.cryptography_key +print(type(cryptography_private_key)) +# + +# create the PEM for the private key (encrypted with a password) +private_pem = private_jwk.to_pem("Th1s_P@ssW0rD_iS_5o_5tr0nG!") +print(private_pem.decode()) +# -----BEGIN ENCRYPTED PRIVATE KEY----- +# MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhFd4nINf0/8QICCAAw +# DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJNdsyMjSx3d6RqBBTuI5LoEgZD4 +# qdPHcTZhKAuzQ9mkM1SlaZfiydWM2KFqPCYPLwoX+3kuCHPanMLlDxwOGN9XMRYl +# hG3eO0Gu4eWdc/2QEcXIyBCbyKnSwhaHUSSfkhyK9eh8diHQw+blOIImIldLPxnp +# +ABOhO6pCjQxM7I5op7RZuxLNWGLyAlfOOvawLfnM/wKLW6GXmlywu7PZ5qk9Bk= +# -----END ENCRYPTED PRIVATE KEY----- + +# write this private PEM to a file: +with open("my_private_key.pem", "wb") as foutput: + foutput.write(private_pem) + +# create the PEM for the public key (unencrypted) +public_jwk = private_jwk.public_jwk() +print(public_jwk) +# { +# "kty": "EC", +# "kid": "m-oFw9zA2YPFyqm265jbHnzXRa3SQ1ESdCE1AtAqO1U", +# "alg": "ES256", +# "use": "sig", +# "key_ops": [ +# "verify" +# ], +# "crv": "P-256", +# "x": "VVbLOXwIgIFsYQSpnbLm5hr-ibfnIK0EeWYj2HXWvks", +# "y": "7f24WIqwHGr-jU9dH8GHpPEHMtAuXiwsedFnS6xayhk" +# } + +# get the cryptography public key +cryptography_public_key = public_jwk.cryptography_key +print(type(cryptography_public_key)) +# + +# get the public PEM +public_pem = public_jwk.to_pem() +print(public_pem.decode()) +# -----BEGIN PUBLIC KEY----- +# MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8xX1CEhDNNjEySUKLw88YeiVwEOW +# 34BWm0hBkAxqlVVR9nQkpPszF00zKowrMpwBxhNy6qhd6mVwneM1+ePe0w== +# -----END PUBLIC KEY----- + +# write this public PEM to a file: +with open("my_public_key.pem", "wb") as foutput: + foutput.write(public_pem) + +# read the private PEM from file and load it as a Jwk: +with open("my_private_key.pem", "rb") as finput: + private_pem_from_file = finput.read() +private_jwk_from_file = ( + Jwk.from_pem_key(private_pem_from_file, password="Th1s_P@ssW0rD_iS_5o_5tr0nG!") + .with_usage_parameters(alg="ES256") # adds back the alg, use and key_ops parameters + .with_kid_thumbprint() # adds back the thumbprint as kid +) +assert private_jwk_from_file == private_jwk + +# read the public PEM from file and load it as a Jwk: +with open("my_public_key.pem", "rb") as finput: + public_pem_from_file = finput.read() +public_jwk_from_file = ( + Jwk.from_pem_key(public_pem_from_file) + .with_usage_parameters(alg="ES256") # adds back the alg, use and key_ops parameters + .with_kid_thumbprint() # adds back the thumbprint as kid +) +assert public_jwk_from_file == public_jwk +``` diff --git a/docs/usage.md b/docs/usage.md index cde1d66..da64bde 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -133,7 +133,9 @@ The available special attributes vary depending on the key type. ## Generating keys -You can generate a `Jwk` with the class method `Jwk.generate_for_kty()`. It needs the key type as parameter, and +### 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: ```python @@ -155,6 +157,21 @@ jwk = Jwk.generate_for_kty("EC", crv="P-256", use="sig") 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: + +```python +from jwskate import Jwk + +ec_jwk = Jwk.generate_for_alg("ES512") # key of type EC, with crv=P-521 +rsa_jwk = Jwk.generate_for_alg("RSA-OAEP-256", key_size=4096) # RSA +okp_jwk = Jwk.generate_for_alg("EdDSA") # EC, default to crv=P-256 +okp_jwk = Jwk.generate_for_alg("EdDSA", crv="P-521") # EC, default to crv=P-256 +okp_jwk = Jwk.generate_for_alg("HS512") +``` + ## Private and Public Keys You can check if a key is public or private with the `is_private` property: diff --git a/jwskate/__init__.py b/jwskate/__init__.py index f065286..fb5a337 100644 --- a/jwskate/__init__.py +++ b/jwskate/__init__.py @@ -14,12 +14,15 @@ from .enums import EncryptionAlgs, KeyManagementAlgs, SignatureAlgs from .jwa import ( + A128CBC_HS256, A128GCM, A128GCMKW, A128KW, + A192CBC_HS384, A192GCM, A192GCMKW, A192KW, + A256CBC_HS512, A256GCM, A256GCMKW, A256KW, @@ -41,9 +44,6 @@ RS512, X448, X25519, - Aes128CbcHmacSha256, - Aes192CbcHmacSha384, - Aes256CbcHmacSha512, BaseAESEncryptionAlg, BaseAlg, BaseAsymmetricAlg, @@ -62,6 +62,7 @@ Ed25519, EdDsa, EllipticCurve, + MismatchingAuthTag, OKPCurve, Pbes2_HS256_A128KW, Pbes2_HS384_A192KW, @@ -91,7 +92,6 @@ ) from .jws import InvalidJws, JwsCompact from .jwt import ( - EncryptedJwt, ExpiredJwt, InvalidClaim, InvalidJwt, @@ -132,9 +132,9 @@ "RS512", "X448", "X25519", - "Aes128CbcHmacSha256", - "Aes192CbcHmacSha384", - "Aes256CbcHmacSha512", + "A128CBC_HS256", + "A192CBC_HS384", + "A256CBC_HS512", "BaseAESEncryptionAlg", "BaseAlg", "BaseAsymmetricAlg", @@ -153,6 +153,7 @@ "Ed25519", "EdDsa", "EllipticCurve", + "MismatchingAuthTag", "OKPCurve", "Pbes2_HS256_A128KW", "Pbes2_HS384_A192KW", @@ -180,7 +181,6 @@ "UnsupportedOKPCurve", "InvalidJws", "JwsCompact", - "EncryptedJwt", "ExpiredJwt", "InvalidClaim", "InvalidJwt", diff --git a/jwskate/enums.py b/jwskate/enums.py index b322d09..37996c3 100644 --- a/jwskate/enums.py +++ b/jwskate/enums.py @@ -35,6 +35,7 @@ class SignatureAlgs: PS512, EdDSA, ] + ALL = ALL_ASYMMETRIC + ALL_SYMMETRIC class EncryptionAlgs: @@ -83,9 +84,6 @@ class KeyManagementAlgs: A192GCMKW, A256GCMKW, dir, - PBES2_HS256_A128KW, - PBES2_HS384_A192KW, - PBES2_HS512_A256KW, ] ALL_ASYMMETRIC = [ RSA1_5, @@ -98,3 +96,10 @@ class KeyManagementAlgs: ECDH_ES_A192KW, ECDH_ES_A256KW, ] + ALL_PASSWORD_BASED = [ + PBES2_HS256_A128KW, + PBES2_HS384_A192KW, + PBES2_HS512_A256KW, + ] + ALL_KEY_BASED = ALL_ASYMMETRIC + ALL_SYMMETRIC + ALL = ALL_ASYMMETRIC + ALL_SYMMETRIC + ALL_PASSWORD_BASED diff --git a/jwskate/jwa/__init__.py b/jwskate/jwa/__init__.py index becbd6d..33a7de5 100644 --- a/jwskate/jwa/__init__.py +++ b/jwskate/jwa/__init__.py @@ -1,8 +1,10 @@ -"""This module implements the Json Web Algorithms as defined in RFC7518. +"""This module implements the Json Web Algorithms as defined in [RFC7518]. -Each algorithm is represented as a wrapper around a symmetric or -asymmetric key, and exposes the cryptographic operations as methods. The -cryptographic operations themselves are delegated to `cryptography`. +Each algorithm is represented as a wrapper around a symmetric or asymmetric key, and exposes the +cryptographic operations as methods. The cryptographic operations themselves are delegated to +`cryptography`. + +[RFC7518]: https://www.rfc-editor.org/rfc/rfc7518 """ from .base import ( @@ -12,17 +14,18 @@ BaseKeyManagementAlg, BaseSignatureAlg, BaseSymmetricAlg, + MismatchingAuthTag, PrivateKeyRequired, PublicKeyRequired, ) from .ec import P_256, P_384, P_521, EllipticCurve, secp256k1 from .encryption import ( + A128CBC_HS256, A128GCM, + A192CBC_HS384, A192GCM, + A256CBC_HS512, A256GCM, - Aes128CbcHmacSha256, - Aes192CbcHmacSha384, - Aes256CbcHmacSha512, ) from .key_mgmt import ( A128GCMKW, @@ -80,9 +83,9 @@ "P_521", "EllipticCurve", "secp256k1", - "Aes128CbcHmacSha256", - "Aes192CbcHmacSha384", - "Aes256CbcHmacSha512", + "A128CBC_HS256", + "A192CBC_HS384", + "A256CBC_HS512", "A128GCMKW", "A192GCMKW", "A256GCMKW", @@ -131,4 +134,5 @@ "X25519", "PrivateKeyRequired", "PublicKeyRequired", + "MismatchingAuthTag", ] diff --git a/jwskate/jwa/base.py b/jwskate/jwa/base.py index 9921215..13414aa 100644 --- a/jwskate/jwa/base.py +++ b/jwskate/jwa/base.py @@ -3,8 +3,9 @@ from __future__ import annotations from contextlib import contextmanager -from typing import Generic, Iterator, Optional, Tuple, Type, TypeVar, Union +from typing import Generic, Iterator, SupportsBytes, Tuple, Type, TypeVar, Union +import cryptography.exceptions from binapy import BinaPy @@ -19,15 +20,21 @@ class PublicKeyRequired(AttributeError): class BaseAlg: """Base class for all algorithms. - An algorithm has a `name` and a `description`, whose reference is here: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms + An algorithm has a `name` and a `description`, whose reference is found in [IANA JOSE registry][IANA]. + + [IANA]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms """ + use: str + """Alg use ('sig' or 'enc')""" + name: str """Technical name of the algorithm""" description: str """Description of the algorithm (human readable)""" read_only: bool = False - """For algs that are considered insecure, allow only signature verification or decryption of existing data, but don't allow new signatures or new encryptions.""" + """For algs that are considered insecure, set to True to allow only signature verification + or decryption of existing data, but don't allow new signatures or encryption.""" def __repr__(self) -> str: """Use the name of the alg as repr.""" @@ -166,7 +173,9 @@ def public_key_required(self) -> Iterator[Kpub]: class BaseSignatureAlg(BaseAlg): """Base class for signature algorithms.""" - def sign(self, data: bytes) -> BinaPy: + use = "sig" + + def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy: """Sign arbitrary data, return the signature. Args: @@ -177,7 +186,9 @@ def sign(self, data: bytes) -> BinaPy: """ raise NotImplementedError - def verify(self, data: bytes, signature: bytes) -> bool: + def verify( + self, data: Union[bytes, SupportsBytes], signature: Union[bytes, SupportsBytes] + ) -> bool: """Verify a signature against some data. Args: @@ -193,6 +204,8 @@ def verify(self, data: bytes, signature: bytes) -> bool: class BaseAESEncryptionAlg(BaseSymmetricAlg): """Base class for AES encryption algorithms.""" + use = "enc" + key_size: int tag_size: int iv_size: int @@ -231,9 +244,19 @@ def generate_iv(cls) -> BinaPy: return BinaPy.random_bits(cls.iv_size) def encrypt( - self, plaintext: bytes, *, iv: bytes, aad: Optional[bytes] + self, + plaintext: Union[bytes, SupportsBytes], + *, + iv: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, ) -> Tuple[BinaPy, BinaPy]: - """Encrypt arbitrary data (`plaintext`) with the given Initialisation Vector (`iv`) and optional Additional Authentication Data (`aad`), return the ciphered text and authentication tag. + """Encrypt arbitrary data, with [Authenticated Encryption (with optional Associated Data)][AEAD]. + + This needs: + - the raw data to encrypt (`plaintext`) + - a given random Initialisation Vector (`iv`) of the appropriate size + - optional Additional Authentication Data (`aad`) + And returns a tuple (ciphered_data, authentication_tag). Args: plaintext: the data to encrypt @@ -242,13 +265,25 @@ def encrypt( Returns: a tuple of ciphered data and authentication tag + + [AEAD]: https://wikipedia.org/wiki/Authenticated_encryption """ raise NotImplementedError def decrypt( - self, ciphertext: bytes, *, iv: bytes, auth_tag: bytes, aad: Optional[bytes] + self, + ciphertext: Union[bytes, SupportsBytes], + *, + iv: Union[bytes, SupportsBytes], + auth_tag: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, ) -> BinaPy: - """Decrypt a ciphertext with a given Initialisation Vector (iv) and optional Additional Authentication Data (aad), returns the resulting clear text. + """Decrypt and verify a ciphertext with Authenticated Encryption. + + This needs: + - the raw encrypted Data (`ciphertext`) and Authentication Tag (`auth_tag`) that were produced by encryption, + - the same Initialisation Vector (`iv`) and optional Additional Authentication Data that were provided for encryption. + and returns the resulting clear text data. Args: ciphertext: the data to decrypt @@ -273,3 +308,9 @@ def init_random_key(cls) -> BaseAESEncryptionAlg: class BaseKeyManagementAlg(BaseAlg): """Base class for Key Management algorithms.""" + + use = "enc" + + +class MismatchingAuthTag(cryptography.exceptions.InvalidTag): + """Raised when trying to decrypt with an Authentication Tag that doesn't match the expected value.""" diff --git a/jwskate/jwa/ec.py b/jwskate/jwa/ec.py index 1ea6a6d..8a40464 100644 --- a/jwskate/jwa/ec.py +++ b/jwskate/jwa/ec.py @@ -13,8 +13,7 @@ class EllipticCurve: """A descriptive class for Elliptic Curves. - Elliptic Curves have a name, a `cryptography.ec.EllipticCurve`, and - a coordinate size. + Elliptic Curves have a name, a `cryptography.ec.EllipticCurve`, and a coordinate size. """ name: str @@ -72,7 +71,9 @@ def get_curve( def get_jwk_parameters( cls, key: Union[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] ) -> Dict[str, Any]: - """Extract all private and public parameters from a given `cryptography` `EllipticCurvePrivateKey` or `EllipticCurvePublicKey`. + """Extract all private and public parameters from the given `cryptography` key. + + Key must be an instance of `EllipticCurvePrivateKey` or `EllipticCurvePublicKey`. Args: key: an Elliptic Curve public or private key from `cryptography`. diff --git a/jwskate/jwa/encryption/__init__.py b/jwskate/jwa/encryption/__init__.py index 2fb19b1..81ee24e 100644 --- a/jwskate/jwa/encryption/__init__.py +++ b/jwskate/jwa/encryption/__init__.py @@ -1,12 +1,12 @@ """This module exposes the Encryption algorithms that are available in `jwskate`.""" -from .aescbchmac import Aes128CbcHmacSha256, Aes192CbcHmacSha384, Aes256CbcHmacSha512 +from .aescbchmac import A128CBC_HS256, A192CBC_HS384, A256CBC_HS512 from .aesgcm import A128GCM, A192GCM, A256GCM __all__ = [ - "Aes128CbcHmacSha256", - "Aes192CbcHmacSha384", - "Aes256CbcHmacSha512", + "A128CBC_HS256", + "A192CBC_HS384", + "A256CBC_HS512", "A128GCM", "A192GCM", "A256GCM", diff --git a/jwskate/jwa/encryption/aescbchmac.py b/jwskate/jwa/encryption/aescbchmac.py index f4daa0d..896d083 100644 --- a/jwskate/jwa/encryption/aescbchmac.py +++ b/jwskate/jwa/encryption/aescbchmac.py @@ -1,13 +1,13 @@ """This module implements AES-CBC with HMAC-SHA based Encryption algorithms.""" -from typing import Optional, Tuple +from typing import SupportsBytes, Tuple, Union from binapy import BinaPy from cryptography import exceptions from cryptography.hazmat.primitives import ciphers, constant_time, hashes, hmac, padding from cryptography.hazmat.primitives.ciphers import algorithms, modes -from ..base import BaseAESEncryptionAlg +from ..base import BaseAESEncryptionAlg, MismatchingAuthTag class BaseAesCbcHmacSha2(BaseAESEncryptionAlg): @@ -41,7 +41,11 @@ def __init__(self, key: bytes) -> None: self.padding = padding.PKCS7(algorithms.AES.block_size) def mac( - self, ciphertext: bytes, *, iv: bytes, aad: Optional[bytes] = None + self, + ciphertext: Union[bytes, SupportsBytes], + *, + iv: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, ) -> BinaPy: """Produce a Message Authentication Code for the given `ciphertext`, `iv` and `aad`. @@ -53,10 +57,18 @@ def mac( Returns: the resulting MAC. """ - if aad is None: + if not isinstance(ciphertext, bytes): + ciphertext = bytes(ciphertext) + if not isinstance(iv, bytes): + iv = bytes(iv) + if aad is None: # pragma: no branch aad = b"" + elif not isinstance(aad, bytes): + aad = bytes(aad) + al = BinaPy.from_int(len(aad) * 8, length=8, byteorder="big", signed=False) hasher = hmac.HMAC(self.mac_key, self.hash_alg) + for param in (aad, iv, ciphertext, al): hasher.update(param) digest = hasher.finalize() @@ -64,9 +76,16 @@ def mac( return BinaPy(mac) def encrypt( - self, plaintext: bytes, *, iv: bytes, aad: Optional[bytes] = None + self, + plaintext: Union[bytes, SupportsBytes], + *, + iv: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, ) -> Tuple[BinaPy, BinaPy]: - """Encrypt and MAC the given `plaintext`, using the given Initialization Vector (`iv`) and optional Additional Authenticated Data (`aad`). + """Encrypt and MAC the given `plaintext`. + + This requires a given Initialization Vector (`iv`). + An optional Additional Authenticated Data can be passed as parameter `aad`. Args: plaintext: the plain data to encrypt @@ -76,6 +95,18 @@ def encrypt( Returns: a tuple (encrypted_data, authentication_tag) """ + if not isinstance(plaintext, bytes): + plaintext = bytes(plaintext) + if not isinstance(iv, bytes): + iv = bytes(iv) + if aad is None: + aad = b"" + elif not isinstance(aad, bytes): + aad = bytes(aad) + + if len(iv) * 8 != self.iv_size: + raise ValueError(f"Invalid IV size, must be {self.iv_size} bits") + cipher = ciphers.Cipher(algorithms.AES(self.aes_key), modes.CBC(iv)).encryptor() padder = self.padding.padder() padded_text = padder.update(plaintext) + padder.finalize() @@ -84,9 +115,16 @@ def encrypt( return BinaPy(ciphertext), BinaPy(mac) def decrypt( - self, ciphertext: bytes, *, iv: bytes, auth_tag: bytes, aad: Optional[bytes] + self, + ciphertext: Union[bytes, SupportsBytes], + *, + iv: Union[bytes, SupportsBytes], + auth_tag: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, ) -> BinaPy: - """Decrypt and authenticate the given ciphertext with authentication tag (`ciphertext_with_tag`), as produced by `encrypt()`. + """Decrypt and authenticate a given ciphertext. + + An optional Additional Authenticated Data must be authentication tag must be supplied, if the text was encryptwith authentication tag, as produced by `encrypt()`. Args: ciphertext: the ciphertext @@ -97,9 +135,23 @@ def decrypt( Returns: the decrypted data """ + if not isinstance(ciphertext, bytes): + ciphertext = bytes(ciphertext) + if not isinstance(iv, bytes): + iv = bytes(iv) + if not isinstance(auth_tag, bytes): + auth_tag = bytes(auth_tag) + if aad is None: # pragma: no branch + aad = b"" + elif not isinstance(aad, bytes): + aad = bytes(aad) + + if len(iv) * 8 != self.iv_size: + raise ValueError(f"Invalid IV size, must be {self.iv_size} bits") + mac = self.mac(ciphertext, iv=iv, aad=aad) if not constant_time.bytes_eq(mac, auth_tag): - raise exceptions.InvalidSignature() + raise MismatchingAuthTag() cipher = ciphers.Cipher(algorithms.AES(self.aes_key), modes.CBC(iv)).decryptor() padded_text = cipher.update(ciphertext) + cipher.finalize() @@ -107,7 +159,7 @@ def decrypt( return BinaPy(unpadder.update(padded_text) + unpadder.finalize()) -class Aes128CbcHmacSha256(BaseAesCbcHmacSha2): +class A128CBC_HS256(BaseAesCbcHmacSha2): """AES_128_CBC_HMAC_SHA_256.""" name = "A128CBC-HS256" @@ -118,7 +170,7 @@ class Aes128CbcHmacSha256(BaseAesCbcHmacSha2): hash_alg = hashes.SHA256() -class Aes192CbcHmacSha384(BaseAesCbcHmacSha2): +class A192CBC_HS384(BaseAesCbcHmacSha2): """AES_192_CBC_HMAC_SHA_384.""" name = "A192CBC-HS384" @@ -129,7 +181,7 @@ class Aes192CbcHmacSha384(BaseAesCbcHmacSha2): hash_alg = hashes.SHA384() -class Aes256CbcHmacSha512(BaseAesCbcHmacSha2): +class A256CBC_HS512(BaseAesCbcHmacSha2): """AES_256_CBC_HMAC_SHA_512.""" name = "A256CBC-HS512" diff --git a/jwskate/jwa/encryption/aesgcm.py b/jwskate/jwa/encryption/aesgcm.py index 71b7751..d0673e6 100644 --- a/jwskate/jwa/encryption/aesgcm.py +++ b/jwskate/jwa/encryption/aesgcm.py @@ -1,11 +1,12 @@ """This module implements AES-GCM based encryption algorithms.""" -from typing import Optional, Tuple +from typing import SupportsBytes, Tuple, Union +import cryptography.exceptions from binapy import BinaPy from cryptography.hazmat.primitives.ciphers import aead -from ..base import BaseAESEncryptionAlg +from ..base import BaseAESEncryptionAlg, MismatchingAuthTag class BaseAESGCM(BaseAESEncryptionAlg): @@ -15,7 +16,11 @@ class BaseAESGCM(BaseAESEncryptionAlg): tag_size = 16 def encrypt( - self, plaintext: bytes, *, iv: bytes, aad: Optional[bytes] + self, + plaintext: Union[bytes, SupportsBytes], + *, + iv: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, ) -> Tuple[BinaPy, BinaPy]: """Encrypt a plaintext, with the given IV and Additional Authenticated Data.". @@ -30,14 +35,27 @@ def encrypt( Raises: ValueError: if the IV size is not appropriate """ + if not isinstance(iv, bytes): + iv = bytes(iv) if len(iv) * 8 != self.iv_size: raise ValueError(f"Invalid IV size, must be {self.iv_size} bits") + if aad is None: + aad = b"" + elif not isinstance(aad, bytes): + aad = bytes(aad) + if not isinstance(plaintext, bytes): + plaintext = bytes(plaintext) ciphertext_with_tag = BinaPy(aead.AESGCM(self.key).encrypt(iv, plaintext, aad)) ciphertext, tag = ciphertext_with_tag.cut_at(-self.tag_size) return ciphertext, tag def decrypt( - self, ciphertext: bytes, *, iv: bytes, auth_tag: bytes, aad: Optional[bytes] + self, + ciphertext: Union[bytes, SupportsBytes], + *, + iv: Union[bytes, SupportsBytes], + auth_tag: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, ) -> BinaPy: """Decrypt a ciphertext. @@ -53,10 +71,24 @@ def decrypt( Raises: ValueError: if the IV size is not appropriate """ + if not isinstance(ciphertext, bytes): + ciphertext = bytes(ciphertext) + if not isinstance(iv, bytes): + iv = bytes(iv) + if not isinstance(auth_tag, bytes): + auth_tag = bytes(auth_tag) + if aad is None: + aad = b"" + elif not isinstance(aad, bytes): + aad = bytes(aad) + if len(iv) * 8 != self.iv_size: raise ValueError(f"Invalid IV size, must be {self.iv_size} bits") ciphertext_with_tag = ciphertext + auth_tag - return BinaPy(aead.AESGCM(self.key).decrypt(iv, ciphertext_with_tag, aad)) + try: + return BinaPy(aead.AESGCM(self.key).decrypt(iv, ciphertext_with_tag, aad)) + except cryptography.exceptions.InvalidTag: + raise MismatchingAuthTag() class A128GCM(BaseAESGCM): diff --git a/jwskate/jwa/key_mgmt/aesgcmkw.py b/jwskate/jwa/key_mgmt/aesgcmkw.py index 029de3f..71f7859 100644 --- a/jwskate/jwa/key_mgmt/aesgcmkw.py +++ b/jwskate/jwa/key_mgmt/aesgcmkw.py @@ -1,6 +1,6 @@ """This module implements AES-GCM based Key Management algorithms.""" -from typing import Tuple +from typing import SupportsBytes, Tuple, Union from binapy import BinaPy @@ -11,6 +11,8 @@ class BaseAesGcmKeyWrap(BaseAESGCM, BaseKeyManagementAlg): """Base class for AES-GCM Key wrapping algorithms.""" + use = "enc" + key_size: int """Required key size, in bits.""" tag_size: int = 16 @@ -18,8 +20,15 @@ class BaseAesGcmKeyWrap(BaseAESGCM, BaseKeyManagementAlg): iv_size: int = 96 """Initialisation Vector size, in bits.""" - def wrap_key(self, plainkey: bytes, *, iv: bytes) -> Tuple[BinaPy, BinaPy]: - """Wrap a key using the given Initialisation Vector (`iv`). + def wrap_key( + self, plainkey: Union[bytes, SupportsBytes], *, iv: Union[bytes, SupportsBytes] + ) -> Tuple[BinaPy, BinaPy]: + """Wrap a symmetric key, which is typically used as Content Encryption Key (CEK). + + This method is used by the sender of the encrypted message. + + This needs a random Initialisation Vector (`iv`) of the appropriate size, + which you can generate using the classmethod `generate_iv()`. Args: plainkey: the key to wrap @@ -28,10 +37,22 @@ def wrap_key(self, plainkey: bytes, *, iv: bytes) -> Tuple[BinaPy, BinaPy]: Returns: a tuple (wrapped_key, authentication_tag) """ - return self.encrypt(plainkey, iv=iv, aad=b"") + return self.encrypt(plainkey, iv=iv) + + def unwrap_key( + self, + cipherkey: Union[bytes, SupportsBytes], + *, + tag: Union[bytes, SupportsBytes], + iv: Union[bytes, SupportsBytes] + ) -> BinaPy: + """Unwrap a symmetric key. + + This method is used by the recipient of an encrypted message. - def unwrap_key(self, cipherkey: bytes, *, tag: bytes, iv: bytes) -> BinaPy: - """Unwrap a key and authenticates it with the authentication `tag`, using the given Initialisation Vector (`iv`). + This requires: + - the same IV that was provided during encryption + - the same Authentication Tag that was generated during encryption Args: cipherkey: the wrapped key @@ -41,7 +62,7 @@ def unwrap_key(self, cipherkey: bytes, *, tag: bytes, iv: bytes) -> BinaPy: Returns: the unwrapped key. """ - return self.decrypt(cipherkey, auth_tag=tag, iv=iv, aad=b"") + return self.decrypt(cipherkey, auth_tag=tag, iv=iv) class A128GCMKW(BaseAesGcmKeyWrap): diff --git a/jwskate/jwa/key_mgmt/aeskw.py b/jwskate/jwa/key_mgmt/aeskw.py index 2e80912..cb727da 100644 --- a/jwskate/jwa/key_mgmt/aeskw.py +++ b/jwskate/jwa/key_mgmt/aeskw.py @@ -1,4 +1,5 @@ """This module implements AES based Key Management algorithms.""" +from typing import SupportsBytes, Union from binapy import BinaPy from cryptography.hazmat.primitives import keywrap @@ -38,7 +39,7 @@ def wrap_key(self, plainkey: bytes) -> BinaPy: """ return BinaPy(keywrap.aes_key_wrap(self.key, plainkey)) - def unwrap_key(self, cipherkey: bytes) -> BinaPy: + def unwrap_key(self, cipherkey: Union[bytes, SupportsBytes]) -> BinaPy: """Unwrap a key. Args: @@ -47,6 +48,9 @@ def unwrap_key(self, cipherkey: bytes) -> BinaPy: Returns: BinaPy: the unwrapped key. """ + if not isinstance(cipherkey, bytes): + cipherkey = bytes(cipherkey) + return BinaPy(keywrap.aes_key_unwrap(self.key, cipherkey)) diff --git a/jwskate/jwa/key_mgmt/ecdh.py b/jwskate/jwa/key_mgmt/ecdh.py index faf3ed8..1c39b7e 100644 --- a/jwskate/jwa/key_mgmt/ecdh.py +++ b/jwskate/jwa/key_mgmt/ecdh.py @@ -1,6 +1,6 @@ """This module implements Elliptic Curve Diffie-Hellman based Key Management algorithms.""" -from typing import Any, Type, Union +from typing import Any, SupportsBytes, Type, Union from binapy import BinaPy from cryptography.hazmat.primitives import hashes @@ -242,7 +242,7 @@ def wrap_key_with_epk( def unwrap_key_with_epk( self, - cipherkey: bytes, + cipherkey: Union[bytes, SupportsBytes], ephemeral_public_key: Union[ ec.EllipticCurvePublicKey, x25519.X25519PublicKey, x448.X448PublicKey ], diff --git a/jwskate/jwa/key_mgmt/pbes2.py b/jwskate/jwa/key_mgmt/pbes2.py index 8a89e9d..1b9cce3 100644 --- a/jwskate/jwa/key_mgmt/pbes2.py +++ b/jwskate/jwa/key_mgmt/pbes2.py @@ -1,6 +1,6 @@ """This module implements password-based Key Management Algorithms relying on PBES2.""" -from typing import Type, Union +from typing import SupportsBytes, Type, Union from binapy import BinaPy from cryptography.hazmat.primitives import hashes @@ -22,9 +22,11 @@ class BasePbes2(BaseKeyManagementAlg): kwalg: Type[BaseAesKeyWrap] hash_alg: hashes.HashAlgorithm - def __init__(self, password: Union[bytes, str]): + def __init__(self, password: Union[SupportsBytes, bytes, str]): if isinstance(password, str): password = password.encode("utf-8") + if not isinstance(password, bytes): + password = bytes(password) self.password = password def generate_salt(self, size: int = 12) -> BinaPy: @@ -44,7 +46,7 @@ def generate_salt(self, size: int = 12) -> BinaPy: return BinaPy.random(size) def derive(self, *, salt: bytes, count: int) -> BinaPy: - """Derive the encryption key based on the configured password, the given salt and the number of PBKDF iterations. + """Derive an encryption key based on the configured password, a given salt and the number of PBKDF iterations. Args: salt: the generated salt diff --git a/jwskate/jwa/key_mgmt/rsa.py b/jwskate/jwa/key_mgmt/rsa.py index 2593072..438c8cd 100644 --- a/jwskate/jwa/key_mgmt/rsa.py +++ b/jwskate/jwa/key_mgmt/rsa.py @@ -1,6 +1,6 @@ """This module implements RSA based Key Management algorithms.""" -from typing import Any, Union +from typing import Any, SupportsBytes, Union from binapy import BinaPy from cryptography.hazmat.primitives import asymmetric, hashes @@ -45,11 +45,13 @@ def wrap_key(self, plainkey: bytes) -> BinaPy: PublicKeyRequired: if this algorithm is initialized with a private key instead of a public key """ if self.read_only: - raise NotImplementedError + raise NotImplementedError( + "Due to security reasons, this algorithm is only usable for decryption." + ) with self.public_key_required() as key: return BinaPy(key.encrypt(plainkey, self.padding)) - def unwrap_key(self, cipherkey: bytes) -> BinaPy: + def unwrap_key(self, cipherkey: Union[bytes, SupportsBytes]) -> BinaPy: """Unwrap a symmetric key with this alg. Args: @@ -60,12 +62,15 @@ def unwrap_key(self, cipherkey: bytes) -> BinaPy: Raises: PrivateKeyRequired: if this alg is initialized with a public key instead of a private key """ + if not isinstance(cipherkey, bytes): + cipherkey = bytes(cipherkey) + with self.private_key_required() as key: return BinaPy(key.decrypt(cipherkey, self.padding)) class RsaEsPcks1v1_5(BaseRsaKeyWrap): # noqa: D415 - """RSAES-PKCS1-v1_5""" + """RSAES-PKCS1-v1_5.""" name = "RSA1_5" description = __doc__ @@ -75,7 +80,7 @@ class RsaEsPcks1v1_5(BaseRsaKeyWrap): # noqa: D415 class RsaEsOaep(BaseRsaKeyWrap): # noqa: D415 - """RSAES OAEP using default parameters""" + """RSAES OAEP using default parameters.""" name = "RSA-OAEP" description = __doc__ @@ -88,7 +93,7 @@ class RsaEsOaep(BaseRsaKeyWrap): # noqa: D415 class RsaEsOaepSha256(BaseRsaKeyWrap): # noqa: D415 - """RSAES OAEP using SHA-256 and MGF1 with SHA-256""" + """RSAES OAEP using SHA-256 and MGF1 with SHA-256.""" name = "RSA-OAEP-256" description = __doc__ @@ -101,7 +106,7 @@ class RsaEsOaepSha256(BaseRsaKeyWrap): # noqa: D415 class RsaEsOaepSha384(BaseRsaKeyWrap): # noqa: D415 - """RSA-OAEP using SHA-384 and MGF1 with SHA-384""" + """RSA-OAEP using SHA-384 and MGF1 with SHA-384.""" name = "RSA-OAEP-384" description = __doc__ @@ -114,7 +119,7 @@ class RsaEsOaepSha384(BaseRsaKeyWrap): # noqa: D415 class RsaEsOaepSha512(BaseRsaKeyWrap): # noqa: D415 - """RSA-OAEP using SHA-512 and MGF1 with SHA-512""" + """RSA-OAEP using SHA-512 and MGF1 with SHA-512.""" name = "RSA-OAEP-512" description = __doc__ diff --git a/jwskate/jwa/okp.py b/jwskate/jwa/okp.py index e830d84..ec09a49 100644 --- a/jwskate/jwa/okp.py +++ b/jwskate/jwa/okp.py @@ -1,4 +1,7 @@ -"""This module contains classes that describe CFRG Elliptic Curve Diffie- Hellman algorithms as specified in RFC8037.""" +"""This module contains classes that describe CFRG Elliptic Curve Diffie-Hellman algorithms as specified in [RFC8037]. + +[RFC8037]: https://www.rfc-editor.org/rfc/rfc8037.html +""" from __future__ import annotations diff --git a/jwskate/jwa/signature/ec.py b/jwskate/jwa/signature/ec.py index 01546ef..9ff7069 100644 --- a/jwskate/jwa/signature/ec.py +++ b/jwskate/jwa/signature/ec.py @@ -1,4 +1,5 @@ """This module implement Elliptic Curve signature algorithms.""" +from typing import SupportsBytes, Union from binapy import BinaPy from cryptography import exceptions @@ -20,7 +21,19 @@ class BaseECSignatureAlg( public_key_class = ec.EllipticCurvePublicKey private_key_class = ec.EllipticCurvePrivateKey - def sign(self, data: bytes) -> BinaPy: # noqa: D102 + @classmethod + def check_key( + cls, key: Union[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] + ) -> None: # noqa: D102 + if key.curve.name != cls.curve.cryptography_curve.name: + raise ValueError( + f"This key is on curve {key.curve.name}. An EC key on curve {cls.curve.name} is expected." + ) + + def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy: # noqa: D102 + if not isinstance(data, bytes): + data = bytes(data) + with self.private_key_required() as key: dss_sig = key.sign(data, ec.ECDSA(self.hashing_alg)) r, s = asymmetric.utils.decode_dss_signature(dss_sig) @@ -28,7 +41,15 @@ def sign(self, data: bytes) -> BinaPy: # noqa: D102 s, self.curve.coordinate_size ) - def verify(self, data: bytes, signature: bytes) -> bool: # noqa: D102 + def verify( + self, data: Union[bytes, SupportsBytes], signature: Union[bytes, SupportsBytes] + ) -> bool: # noqa: D102 + if not isinstance(data, bytes): + data = bytes(data) + + if not isinstance(signature, bytes): + signature = bytes(signature) + with self.public_key_required() as key: if len(signature) != self.curve.coordinate_size * 2: raise ValueError( @@ -55,7 +76,7 @@ def verify(self, data: bytes, signature: bytes) -> bool: # noqa: D102 class ES256(BaseECSignatureAlg): # noqa: D415 - """ECDSA using P-256 and SHA-256""" + """ECDSA using P-256 and SHA-256.""" name = "ES256" description = __doc__ @@ -64,7 +85,7 @@ class ES256(BaseECSignatureAlg): # noqa: D415 class ES384(BaseECSignatureAlg): # noqa: D415 - """ECDSA using P-384 and SHA-384""" + """ECDSA using P-384 and SHA-384.""" name = "ES384" description = __doc__ @@ -73,7 +94,7 @@ class ES384(BaseECSignatureAlg): # noqa: D415 class ES512(BaseECSignatureAlg): # noqa: D415 - """ECDSA using P-521 and SHA-512""" + """ECDSA using P-521 and SHA-512.""" name = "ES512" description = __doc__ @@ -82,7 +103,7 @@ class ES512(BaseECSignatureAlg): # noqa: D415 class ES256K(BaseECSignatureAlg): # noqa: D415 - """ECDSA using secp256k1 and SHA-256""" + """ECDSA using secp256k1 and SHA-256.""" name = "ES256k" description = __doc__ diff --git a/jwskate/jwa/signature/eddsa.py b/jwskate/jwa/signature/eddsa.py index 32bee59..6db07af 100644 --- a/jwskate/jwa/signature/eddsa.py +++ b/jwskate/jwa/signature/eddsa.py @@ -1,6 +1,6 @@ """This module implements the Edwards-curve Digital Signature Algorithm (EdDSA).""" -from typing import Union +from typing import SupportsBytes, Union from binapy import BinaPy from cryptography import exceptions @@ -24,11 +24,21 @@ class EdDsa( name = "EdDSA" description = __doc__ - def sign(self, data: bytes) -> BinaPy: # noqa: D102 + def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy: # noqa: D102 + if not isinstance(data, bytes): + data = bytes(data) + with self.private_key_required() as key: return BinaPy(key.sign(data)) - def verify(self, data: bytes, signature: bytes) -> bool: # noqa: D102 + def verify( + self, data: Union[bytes, SupportsBytes], signature: Union[bytes, SupportsBytes] + ) -> bool: # noqa: D102 + if not isinstance(data, bytes): + data = bytes(data) + if not isinstance(signature, bytes): + signature = bytes(signature) + with self.public_key_required() as key: try: key.verify(signature, data) diff --git a/jwskate/jwa/signature/hmac.py b/jwskate/jwa/signature/hmac.py index d4f9266..1b2f854 100644 --- a/jwskate/jwa/signature/hmac.py +++ b/jwskate/jwa/signature/hmac.py @@ -1,6 +1,6 @@ """This module implements HMAC based signature algorithms.""" -from typing import Type +from typing import SupportsBytes, Type, Union from binapy import BinaPy from cryptography.hazmat.primitives import hashes, hmac @@ -15,7 +15,10 @@ class BaseHMACSigAlg(BaseSymmetricAlg, BaseSignatureAlg): hash_alg: hashes.HashAlgorithm min_key_size: int - def sign(self, data: bytes) -> BinaPy: # noqa: D102 + def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy: # noqa: D102 + if not isinstance(data, bytes): + data = bytes(data) + if self.read_only: raise NotImplementedError m = self.mac(self.key, self.hash_alg) @@ -23,13 +26,21 @@ def sign(self, data: bytes) -> BinaPy: # noqa: D102 signature = m.finalize() return BinaPy(signature) - def verify(self, data: bytes, signature: bytes) -> bool: # noqa: D102 + def verify( + self, data: Union[bytes, SupportsBytes], signature: Union[bytes, SupportsBytes] + ) -> bool: # noqa: D102 + if not isinstance(data, bytes): + data = bytes(data) + + if not isinstance(signature, bytes): + signature = bytes(signature) + candidate_signature = self.sign(data) return candidate_signature == signature class HS256(BaseHMACSigAlg): # noqa: D415 - """HMAC using SHA-256""" + """HMAC using SHA-256.""" name = "HS256" description = __doc__ @@ -38,7 +49,7 @@ class HS256(BaseHMACSigAlg): # noqa: D415 class HS384(BaseHMACSigAlg): # noqa: D415 - """HMAC using SHA-384""" + """HMAC using SHA-384.""" name = "HS384" description = __doc__ @@ -47,7 +58,7 @@ class HS384(BaseHMACSigAlg): # noqa: D415 class HS512(BaseHMACSigAlg): # noqa: D415 - """HMAC using SHA-512""" + """HMAC using SHA-512.""" name = "HS512" description = __doc__ @@ -56,7 +67,7 @@ class HS512(BaseHMACSigAlg): # noqa: D415 class HS1(BaseHMACSigAlg): # noqa: D415 - """HMAC using SHA-1""" + """HMAC using SHA-1.""" name = "HS1" description = __doc__ diff --git a/jwskate/jwa/signature/rsa.py b/jwskate/jwa/signature/rsa.py index 77aa2c2..607dc5d 100644 --- a/jwskate/jwa/signature/rsa.py +++ b/jwskate/jwa/signature/rsa.py @@ -1,4 +1,5 @@ """This module implements RSA signature algorithms.""" +from typing import SupportsBytes, Union from binapy import BinaPy from cryptography import exceptions @@ -21,7 +22,7 @@ class BaseRSASigAlg( private_key_class = asymmetric.rsa.RSAPrivateKey public_key_class = asymmetric.rsa.RSAPublicKey - def sign(self, data: bytes) -> BinaPy: + def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy: """Sign arbitrary data. Args: @@ -36,10 +37,16 @@ def sign(self, data: bytes) -> BinaPy: """ if self.read_only: raise NotImplementedError + + if not isinstance(data, bytes): + data = bytes(data) + with self.private_key_required() as key: return BinaPy(key.sign(data, self.padding_alg, self.hashing_alg)) - def verify(self, data: bytes, signature: bytes) -> bool: + def verify( + self, data: Union[bytes, SupportsBytes], signature: Union[bytes, SupportsBytes] + ) -> bool: """Verify a signature against some data. Args: @@ -49,6 +56,12 @@ def verify(self, data: bytes, signature: bytes) -> bool: Returns: `True` if the signature is valid, `False` otherwise """ + if not isinstance(data, bytes): + data = bytes(data) + + if not isinstance(signature, bytes): + signature = bytes(signature) + with self.public_key_required() as key: try: key.verify( @@ -63,7 +76,7 @@ def verify(self, data: bytes, signature: bytes) -> bool: class RS256(BaseRSASigAlg): # noqa: D415 - """RSASSA-PKCS1-v1_5 using SHA-256""" + """RSASSA-PKCS1-v1_5 using SHA-256.""" name = "RS256" description = __doc__ @@ -71,7 +84,7 @@ class RS256(BaseRSASigAlg): # noqa: D415 class RS384(BaseRSASigAlg): # noqa: D415 - """RSASSA-PKCS1-v1_5 using SHA-384""" + """RSASSA-PKCS1-v1_5 using SHA-384.""" name = "RS384" description = __doc__ @@ -79,7 +92,7 @@ class RS384(BaseRSASigAlg): # noqa: D415 class RS512(BaseRSASigAlg): # noqa: D415 - """RSASSA-PKCS1-v1_5 using SHA-256""" + """RSASSA-PKCS1-v1_5 using SHA-256.""" name = "RS512" description = __doc__ @@ -87,7 +100,7 @@ class RS512(BaseRSASigAlg): # noqa: D415 class PS256(BaseRSASigAlg): # noqa: D415 - """RSASSA-PSS using SHA-256 and MGF1 with SHA-256""" + """RSASSA-PSS using SHA-256 and MGF1 with SHA-256.""" name = "PS256" description = __doc__ @@ -96,7 +109,7 @@ class PS256(BaseRSASigAlg): # noqa: D415 class PS384(BaseRSASigAlg): # noqa: D415 - """RSASSA-PSS using SHA-384 and MGF1 with SHA-384""" + """RSASSA-PSS using SHA-384 and MGF1 with SHA-384.""" name = "PS384" description = __doc__ @@ -105,7 +118,7 @@ class PS384(BaseRSASigAlg): # noqa: D415 class PS512(BaseRSASigAlg): # noqa: D415 - """RSASSA-PSS using SHA-512 and MGF1 with SHA-512""" + """RSASSA-PSS using SHA-512 and MGF1 with SHA-512.""" name = "PS512" description = __doc__ @@ -114,7 +127,7 @@ class PS512(BaseRSASigAlg): # noqa: D415 class RS1(BaseRSASigAlg): # noqa: D415 - """RSASSA-PKCS1-v1_5 with SHA-1""" + """RSASSA-PKCS1-v1_5 with SHA-1.""" name = "RS1" description = __doc__ diff --git a/jwskate/jwe/__init__.py b/jwskate/jwe/__init__.py index 983d90c..930b0b9 100644 --- a/jwskate/jwe/__init__.py +++ b/jwskate/jwe/__init__.py @@ -1,4 +1,7 @@ -"""This module implements Json Web Encryption [RFC7516](https://tools.ietf.org/html/rfc7516).""" +"""This module implements Json Web Encryption as described in [RFC7516]. + +[RFC7516]: https://www.rfc-editor.org/rfc/rfc7516 +""" from .compact import InvalidJwe, JweCompact diff --git a/jwskate/jwe/compact.py b/jwskate/jwe/compact.py index d004888..9b02cbb 100644 --- a/jwskate/jwe/compact.py +++ b/jwskate/jwe/compact.py @@ -1,7 +1,7 @@ """This module implements the JWE Compact format.""" import warnings -from typing import Any, Dict, Mapping, Optional, Type, Union +from typing import Any, Dict, Mapping, Optional, SupportsBytes, Type, Union from backports.cached_property import cached_property from binapy import BinaPy @@ -13,7 +13,7 @@ Pbes2_HS512_A256KW, ) from jwskate.jwk import Jwk, SymmetricJwk -from jwskate.jwk.alg import UnsupportedAlg +from jwskate.jwk.alg import UnsupportedAlg, select_alg from jwskate.token import BaseCompactToken @@ -131,7 +131,7 @@ def enc(self) -> str: @classmethod def encrypt( cls, - plaintext: bytes, + plaintext: Union[bytes, SupportsBytes], jwk: Union[Jwk, Dict[str, Any]], *, enc: str, @@ -141,12 +141,12 @@ def encrypt( iv: Optional[bytes] = None, epk: Optional[Jwk] = None, ) -> "JweCompact": - """Encrypt an arbitrary plaintext and return the resulting JweCompact. + """Encrypt an arbitrary plaintext into a JweCompact. Args: plaintext: the raw plaintext to encrypt - jwk: the public or symmetric key to use - enc: the CEK encryption algorithm to use + jwk: the public or symmetric key to use for encryption + enc: the encryption algorithm to use alg: the Key Management algorithm to use, if there is no 'alg' header defined in the Jwk extra_headers: additional headers to include in the generated token cek: the CEK to force use, for algorithms relying on a random CEK. Leave `None` to have a safe value generated by `jwskate`. @@ -156,13 +156,19 @@ def encrypt( Returns: the generated JweCompact instance """ - jwk = Jwk(jwk) extra_headers = extra_headers or {} + if not isinstance(jwk, Jwk): + jwk = Jwk(jwk) + alg = select_alg(jwk.alg, alg, jwk.KEY_MANAGEMENT_ALGORITHMS).name + cek_jwk, wrapped_cek, cek_headers = jwk.sender_key( enc=enc, alg=alg, cek=cek, epk=epk, **extra_headers ) headers = dict(extra_headers, **cek_headers, alg=alg, enc=enc) + if jwk.kid is not None: + headers["kid"] = jwk.kid + aad = BinaPy.serialize_to("json", headers).to("b64u") ciphertext, iv, tag = cek_jwk.encrypt(plaintext, aad=aad, iv=iv, alg=enc) @@ -198,14 +204,14 @@ def unwrap_cek( def decrypt( self, jwk: Union[Jwk, Dict[str, Any]], - ) -> bytes: + ) -> BinaPy: """Decrypts this Jwe payload using a JWK. Args: jwk: the decryption key Returns: - bytes: the decrypted payload + the decrypted payload """ cek_jwk = self.unwrap_cek(jwk) @@ -221,8 +227,8 @@ def decrypt( @classmethod def encrypt_with_password( cls, - plaintext: bytes, - password: Union[bytes, str], + plaintext: Union[SupportsBytes, bytes], + password: Union[SupportsBytes, bytes, str], *, alg: str, enc: str, diff --git a/jwskate/jwk/alg.py b/jwskate/jwk/alg.py index db93b49..db82d4b 100644 --- a/jwskate/jwk/alg.py +++ b/jwskate/jwk/alg.py @@ -1,7 +1,7 @@ """This module contains several utilities for algorithmic agility.""" import warnings -from typing import Any, Iterable, List, Mapping, Optional, Type, TypeVar +from typing import Iterable, List, Mapping, Optional, Type, TypeVar from jwskate.jwa import BaseAlg @@ -20,7 +20,13 @@ class ExpectedAlgRequired(ValueError): def select_alg( jwk_alg: Optional[str], alg: Optional[str], supported_algs: Mapping[str, T] ) -> T: - """Given an alg parameter from a JWK, and/or a user-specified alg, return the alg to use. + """Internal helper method to choose the appropriate alg 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 + 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`. @@ -33,9 +39,16 @@ def select_alg( Returns: the alg to use + Warnings: + A warning is emitted if `jwk_alg` is supplied and `alg` doesn't match its value. + Raises: UnsupportedAlg: if the requested alg is not supported + ValueError: if supported_algs is empty """ + if not supported_algs: + raise ValueError("No possible algorithms to choose from!") + choosen_alg: str if jwk_alg is not None: if alg is not None: @@ -67,13 +80,23 @@ def select_algs( algs: Optional[Iterable[str]], supported_algs: Mapping[str, T], ) -> List[T]: - """Given an alg parameter from a JWK, and/or a user-specified alg, and/or a user specified list of useable algs, return a list of algorithms. + """Internal helper method to select several appropriate algs 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. - This method is typically used to get the list of possible algs when checking a signature. + Given: + - 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: jwk_alg: the alg from the JWK, if any - alg: a user specified alg to use + 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 @@ -90,12 +113,11 @@ def select_algs( if alg and algs: raise ValueError("Please use either parameter 'alg' or 'algs', not both.") + if not supported_algs: + raise ValueError("No possible algorithms to choose from!") + if jwk_alg is not None: - if alg and alg != jwk_alg: - warnings.warn( - "This key has an 'alg' parameter, you should use that alg for each operation." - ) - if algs and jwk_alg not in algs: + if (alg and alg != jwk_alg) or (algs and jwk_alg not in algs): warnings.warn( "This key has an 'alg' parameter, you should use that alg for each operation." ) @@ -122,119 +144,3 @@ def select_algs( raise ExpectedAlgRequired( "This key doesn't have an 'alg' parameter, so you need to provide the expected signing alg(s) for each operation." ) - - -KEY_PARAMS_FOR_ALG: Mapping[str, Mapping[str, Any]] = { - "HS256": {"kty": "oct", "key_size": 256, "use": "sig", "key_ops": ["sign"]}, - "HS384": {"kty": "oct", "key_size": 384, "use": "sig", "key_ops": ["sign"]}, - "HS512": {"kty": "oct", "key_size": 512, "use": "sig", "key_ops": ["sign"]}, - "RS256": {"kty": "RSA", "key_size": 2048, "use": "sig", "key_ops": ["sign"]}, - "RS384": {"kty": "RSA", "key_size": 3064, "use": "sig", "key_ops": ["sign"]}, - "RS512": {"kty": "RSA", "key_size": 4096, "use": "sig", "key_ops": ["sign"]}, - "PS256": {"kty": "RSA", "key_size": 2048, "use": "sig", "key_ops": ["sign"]}, - "PS384": {"kty": "RSA", "key_size": 3064, "use": "sig", "key_ops": ["sign"]}, - "PS512": {"kty": "RSA", "key_size": 4096, "use": "sig", "key_ops": ["sign"]}, - "ES256": {"kty": "EC", "crv": "P-256", "use": "sig", "key_ops": ["sign"]}, - "ES384": {"kty": "EC", "crv": "P-384", "use": "sig", "key_ops": ["sign"]}, - "ES512": {"kty": "EC", "crv": "P-521", "use": "sig", "key_ops": ["sign"]}, - "RSA1_5": { - "kty": "RSA", - "key_size": 2048, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "RSA-OAEP": { - "kty": "RSA", - "key_size": 3064, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "RSA-OAEP-256": { - "kty": "RSA", - "key_size": 4096, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "RSA-OAEP-384": { - "kty": "RSA", - "key_size": 4096, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "RSA-OAEP-512": { - "kty": "RSA", - "key_size": 4096, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "A128KW": {"kty": "oct", "key_size": 128, "use": "enc", "key_ops": ["wrapKey"]}, - "A192KW": {"kty": "oct", "key_size": 192, "use": "enc", "key_ops": ["wrapKey"]}, - "A256KW": {"kty": "oct", "key_size": 256, "use": "enc", "key_ops": ["wrapKey"]}, - "A128GCMKW": { - "kty": "oct", - "key_size": 128, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "A192GCMKW": { - "kty": "oct", - "key_size": 192, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "A256GCMKW": { - "kty": "oct", - "key_size": 256, - "use": "enc", - "key_ops": ["wrapKey"], - }, - "ECDH-ES": {"kty": "EC", "crv": "X25519", "use": "enc", "key_ops": ["wrapKey"]}, - "A128CBC-HS256": { - "kty": "oct", - "key_size": 256, - "use": "enc", - "key_ops": ["encrypt"], - }, - "A192CBC-HS384": { - "kty": "oct", - "key_size": 384, - "use": "enc", - "key_ops": ["encrypt"], - }, - "A256CBC-HS512": { - "kty": "oct", - "key_size": 512, - "use": "enc", - "key_ops": ["encrypt"], - }, - "A128GCM": { - "kty": "oct", - "key_size": 128, - "use": "enc", - "key_ops": ["encrypt"], - }, - "A192GCM": { - "kty": "oct", - "key_size": 192, - "use": "enc", - "key_ops": ["encrypt"], - }, - "A256GCM": { - "kty": "oct", - "key_size": 256, - "use": "enc", - "key_ops": ["encrypt"], - }, -} - - -def get_kty_for_alg(alg: str) -> Mapping[str, Any]: - """Given an alg identifier, return key parameters for a key that is suitable for use with that alg. - - Args: - alg: a signature or keymanagement or encryption alg identifier - - Returns: - a dict of key parameters - """ - return KEY_PARAMS_FOR_ALG[alg] diff --git a/jwskate/jwk/base.py b/jwskate/jwk/base.py index db0742b..9fa8a59 100644 --- a/jwskate/jwk/base.py +++ b/jwskate/jwk/base.py @@ -12,11 +12,13 @@ List, Mapping, Optional, + SupportsBytes, Tuple, Type, Union, ) +from backports.cached_property import cached_property from binapy import BinaPy from cryptography.hazmat.primitives import serialization @@ -34,6 +36,7 @@ EcdhEs, ) +from .. import BaseAlg from ..token import BaseJsonDict from .alg import UnsupportedAlg, select_alg, select_algs @@ -86,7 +89,8 @@ class Jwk(BaseJsonDict): KTY: ClassVar[str] """The Key Type associated with this JWK.""" - CRYPTOGRAPHY_KEY_CLASSES: ClassVar[Iterable[Any]] + CRYPTOGRAPHY_PRIVATE_KEY_CLASSES: ClassVar[Iterable[Any]] + CRYPTOGRAPHY_PUBLIC_KEY_CLASSES: ClassVar[Iterable[Any]] SIGNATURE_ALGORITHMS: Mapping[str, Type[BaseSignatureAlg]] = {} KEY_MANAGEMENT_ALGORITHMS: Mapping[str, Type[BaseKeyManagementAlg]] = {} @@ -106,11 +110,12 @@ class Jwk(BaseJsonDict): def __init_subclass__(cls) -> None: """Automatically add subclasses to the registry. - This allows __new__ to pick the appropriate subclass when - creating a Jwk + This allows `__new__` to pick the appropriate subclass when creating a Jwk. """ Jwk.subclasses[cls.KTY] = cls - for klass in cls.CRYPTOGRAPHY_KEY_CLASSES: + for klass in cls.CRYPTOGRAPHY_PRIVATE_KEY_CLASSES: + Jwk.cryptography_key_types[klass] = cls + for klass in cls.CRYPTOGRAPHY_PUBLIC_KEY_CLASSES: Jwk.cryptography_key_types[klass] = cls def __new__(cls, key: Union[Jwk, Dict[str, Any], Any], **kwargs: Any): # type: ignore @@ -162,6 +167,32 @@ def __init__( except AttributeError as exc: raise InvalidJwk() from exc + @classmethod + def _get_alg_class(cls, alg: str) -> Type[BaseAlg]: + """Given an alg identifier, return the matching JWA wrapper. + + Args: + alg: an alg identifier + + Returns: + the matching JWA wrapper + """ + alg_class: Optional[Type[BaseAlg]] + + alg_class = cls.SIGNATURE_ALGORITHMS.get(alg) + if alg_class is not None: + return alg_class + + alg_class = cls.KEY_MANAGEMENT_ALGORITHMS.get(alg) + if alg_class is not None: + return alg_class + + alg_class = cls.ENCRYPTION_ALGORITHMS.get(alg) + if alg_class is not None: + return alg_class + + raise UnsupportedAlg(alg) + @property def is_private(self) -> bool: """Return `True` if the key is private, `False` otherwise. @@ -171,21 +202,28 @@ def is_private(self) -> bool: """ return True - def __getattr__(self, item: str) -> Any: - """Allows access to key parameters as attributes, like `jwk.kid`, `jwk.kty`, instead of `jwk['kid']`, `jwk['kty']`, etc. + @property + def is_symmetric(self) -> bool: + """Return `True` if the key is symmetric, `False` otherwise.""" + return False + + def __getattr__(self, param: str) -> Any: + """Allow access to key parameters as attributes. + + This is a convenience to allow `jwk.param` instead of `jwk['param']`. Args: - item: the member to access + param: the parameter name to access Return: - the member value + the param value Raises: - AttributeError: if the member is not found + AttributeError: if the param is not found """ - value = self.get(item) + value = self.get(param) if value is None: - raise AttributeError(item) + raise AttributeError(param) return value def __setitem__(self, key: str, value: Any) -> None: @@ -197,7 +235,6 @@ def __setitem__(self, key: str, value: Any) -> None: Raises: RuntimeError: when trying to modify cryptographic attributes - """ if key in self.PARAMS: raise RuntimeError("JWK key attributes cannot be modified.") @@ -258,12 +295,69 @@ def alg(self) -> Optional[str]: the key alg """ alg = self.get("alg") - if alg is not None and not isinstance(alg, str): - raise TypeError(f"Invalid alg type {type(str)}", alg) + if alg is not None and not isinstance(alg, str): # pragma: no branch + raise TypeError(f"Invalid alg type {type(alg)}", alg) return alg + @property + def kid(self) -> Optional[str]: + """Return the JWK key ID (kid), if present.""" + kid = self.get("kid") + if kid is not None and not isinstance(kid, str): # pragma: no branch + raise TypeError(f"invalid kid type {type(kid)}", kid) + return kid + + @cached_property + def use(self) -> Optional[str]: + """Return the key use. + + If no `alg` parameter is present, this returns the `use` parameter from this JWK. If an + `alg` parameter is present, the use is deduced from this alg. To check for the presence of + the `use` parameter, use `jwk.get('use')`. + """ + if self.alg: + return self._get_alg_class(self.alg).use + else: + return self.get("use") + + @cached_property + def key_ops(self) -> List[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')`. + """ + if self.use == "sig": + if self.is_symmetric: + key_ops = ["sign", "verify"] + elif self.is_private: + key_ops = ["sign"] + else: + 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"] + elif issubclass(alg_class, BaseAESEncryptionAlg): + key_ops = ["encrypt", "decrypt"] + else: + key_ops = ["wrapKey", "unwrapKey", "encrypt", "decrypt"] + elif self.is_private: + key_ops = ["unwrapKey"] + else: + key_ops = ["wrapKey"] + else: + key_ops = self.get("key_ops", []) + + return key_ops + def _validate(self) -> None: - """Internal method used to validate a Jwk. It checks that all required parameters are present and well-formed. If the key is private, it sets the `is_private` flag to `True`. + """Internal method used to validate a Jwk. + + It checks that all required parameters are present and well-formed. If the key is private, it sets the `is_private` flag to `True`. Raises: TypeError: if the key type doesn't match the subclass @@ -319,7 +413,7 @@ def _validate(self) -> None: # if key is used for signing, it must be private for op in self.get("key_ops", []): - if op in ("sign", "decrypt", "unwrapKey") and not self.is_private: + if op in ("sign", "unwrapKey") and not self.is_private: raise InvalidJwk(f"Key Operation is '{op}' but the key is public") def supported_signing_algorithms(self) -> List[str]: @@ -361,17 +455,16 @@ def public_jwk(self) -> Jwk: if not param.is_private } - key_ops = self.get("key_ops") - if key_ops: + if "key_ops" in self: + key_ops = list(self.key_ops) if "sign" in key_ops: key_ops.remove("sign") key_ops.append("verify") - if "decrypt" in key_ops: - key_ops.remove("decrypt") - key_ops.append("encrypt") if "unwrapKey" in key_ops: key_ops.remove("unwrapKey") key_ops.append("wrapKey") + else: + key_ops = None return Jwk( dict( @@ -394,7 +487,9 @@ def as_jwks(self) -> JwkSet: return JwkSet(keys=(self,)) - def sign(self, data: bytes, alg: Optional[str] = None) -> BinaPy: + def sign( + self, data: Union[bytes, SupportsBytes], alg: Optional[str] = None + ) -> BinaPy: """Sign a data using this Jwk, and return the generated signature. Args: @@ -417,8 +512,8 @@ def sign(self, data: bytes, alg: Optional[str] = None) -> BinaPy: def verify( self, - data: bytes, - signature: bytes, + data: Union[bytes, SupportsBytes], + signature: Union[bytes, SupportsBytes], *, alg: Optional[str] = None, algs: Optional[Iterable[str]] = None, @@ -449,20 +544,25 @@ def verify( def encrypt( self, - plaintext: bytes, + plaintext: Union[bytes, SupportsBytes], *, aad: Optional[bytes] = None, alg: Optional[str] = None, iv: Optional[bytes] = None, ) -> Tuple[BinaPy, BinaPy, BinaPy]: - """Encrypt a plaintext, with an optional Additional Authenticated Data (AAD) using this JWK, and return the Encrypted Data, the Initialization Vector and the Authentication Tag. + """Encrypt a plaintext with Authenticated Encryption using this key. + + Authenticated Encryption with Associated Data (AEAD) is supported, by passing Additional Authenticated Data (`aad`). + This returns a tuple with 3 raw data, in order: + - the encrypted Data + - the Initialization Vector that was used to encrypt data + - the generated Authentication Tag Args: plaintext: the data to encrypt. aad: the Additional Authenticated Data (AAD) to include in the authentication tag alg: the alg to use to encrypt the data - iv: the Initialization Vector that was used to encrypt the data. If `iv` is passed as parameter, this - will return that same value. Otherwise, an IV is generated. + iv: the Initialization Vector to use. If not provided, an IV is generated. If provided, the returned IV will be the same. Returns: a tuple (ciphertext, iv, authentication_tag), as raw data @@ -471,11 +571,11 @@ def encrypt( def decrypt( self, - ciphertext: bytes, + ciphertext: Union[bytes, SupportsBytes], *, - iv: bytes, - tag: bytes, - aad: Optional[bytes] = None, + iv: Union[bytes, SupportsBytes], + tag: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, alg: Optional[str] = None, ) -> BinaPy: """Decrypt an encrypted data using this Jwk, and return the encrypted result. @@ -503,18 +603,19 @@ def sender_key( epk: Optional[Jwk] = None, **headers: Any, ) -> Tuple[Jwk, BinaPy, Mapping[str, Any]]: - """For DH-based algs. As a token issuer, derive a EPK and CEK from the recipient public key. + """Used by encrypted token senders to produce a Content Encryption Key. Returns a tuple with 3 items: - - the clear text CEK, as a SymmetricJwk instance. Use this key to encrypt your message, but do not communicate this key! - - the encrypted CEK, as bytes. You must send this to your recipient. This may be empty for algs which derive a CEK instead of generating one. - - extra headers depending on the Key Management algorithm, as a dict of name to values: you must send this to your recipient as well. + - the clear text CEK, as a SymmetricJwk instance. Use this key to encrypt your message, but do not communicate this key to anyone! + - the encrypted CEK, as bytes. You must send this to your recipient. This may be `None` for Key Management algs which derive a CEK instead of generating one. + - extra headers depending on the Key Management algorithm, as a dict of name to values: you must send those to your recipient as well. - For algorithms that rely on a randomly generated CEK, you can provide that value instead of letting `jwskate` generate a - safe, unique random value for you. Likewise, for algorithms that rely on an ephemeral key, you can provide an - EPK that you generated yourself, instead of letting `jwskate` generate an appropriate value for you. - Only use this if you know what you are doing! + For algorithms that rely on a randomly generated CEK, such as RSAES or AES, you can provide that CEK instead + of letting `jwskate` generate a safe, unique random value for you. + Likewise, for algorithms that rely on an ephemeral key, you can provide an EPK that you generated yourself, + instead of letting `jwskate` generate an appropriate value for you. + Only do this if you know what you are doing! Args: enc: the encryption algorithm to use with the CEK @@ -599,9 +700,14 @@ def sender_key( return SymmetricJwk.from_bytes(cek), wrapped_cek, cek_headers def recipient_key( - self, wrapped_cek: bytes, enc: str, *, alg: Optional[str] = None, **headers: Any + self, + wrapped_cek: Union[bytes, SupportsBytes], + enc: str, + *, + alg: Optional[str] = None, + **headers: Any, ) -> Jwk: - """For DH-based algs. As a token recipient, derive the same CEK that was used for encryption, based on the recipient private key and the sender ephemeral public key. + """Used by token recipients to obtain the CEK, which then allows decryption of the payload. Args: wrapped_cek: the wrapped CEK @@ -738,10 +844,11 @@ def from_pem_key( return cls.from_cryptography_key(cryptography_key, **kwargs) - def to_pem_key(self, password: Optional[bytes] = None) -> bytes: + def to_pem(self, password: Union[bytes, str, None] = None) -> bytes: """Serialize this key to PEM format. - For private keys, you can provide a password for encryption. + For private keys, you can provide a password for encryption. This password should be bytes. A `str` is also + 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 @@ -749,11 +856,35 @@ def to_pem_key(self, password: Optional[bytes] = None) -> bytes: Returns: the PEM serialized key """ - raise NotImplementedError + if password is not None and not isinstance(password, bytes): + password = str(password).encode("UTF-8") + + if self.is_private: + encryption: serialization.KeySerializationEncryption + if password: + encryption = serialization.BestAvailableEncryption(password) + else: + encryption = serialization.NoEncryption() + return self.cryptography_key.private_bytes( # type: ignore[no-any-return] + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + encryption, + ) + else: + if password: + raise ValueError( + "Public keys cannot be encrypted when serialized in PEM format." + ) + return self.cryptography_key.public_bytes( # type: ignore[no-any-return] + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) @classmethod def generate(cls, **kwargs: Any) -> Jwk: - """Generates a Private Key. This method is implemented by subclasses for specific Key Types and returns an instance of that specific subclass. + """Generate a Private Key and return it as a `Jwk` instance. + + This method is implemented by subclasses for specific Key Types and returns an instance of that subclass. Args: **kwargs: specific parameters depending on the type of key, or additional members to include in the Jwk @@ -784,7 +915,7 @@ def generate_for_kty(cls, kty: str, **kwargs: Any) -> Jwk: @classmethod def generate_for_alg(cls, alg: str, **kwargs: Any) -> Jwk: - """Generate a key for usage with a specific alg and return the resuting Jwk. + """Generate a key for usage with a specific alg and return the resulting Jwk. Args: alg: a signature or key management alg @@ -793,7 +924,18 @@ def generate_for_alg(cls, alg: str, **kwargs: Any) -> Jwk: Returns: the resulting Jwk """ - raise NotImplementedError + for kty, jwk_class in cls.subclasses.items(): + alg_class: Optional[Type[BaseAlg]] + try: + alg_class = jwk_class._get_alg_class(alg) + if isinstance(jwk_class, BaseAESEncryptionAlg): + kwargs.setdefault("key_size", alg_class.key_size) + + return jwk_class.generate(alg=alg, **kwargs) + except UnsupportedAlg: + continue + + raise UnsupportedAlg(alg) def copy(self) -> Jwk: """Creates a copy of this key. @@ -803,7 +945,7 @@ def copy(self) -> Jwk: """ return Jwk(super().copy()) - def include_kid_thumbprint(self, force: bool = False) -> Jwk: + def with_kid_thumbprint(self, force: bool = False) -> Jwk: """Includes the JWK thumbprint as "kid". If key already has a "kid": @@ -816,7 +958,62 @@ def include_kid_thumbprint(self, force: bool = False) -> Jwk: Returns: a copy of this key with a "kid" (either the previous one or the existing one, depending on `force`). """ + if self.get("kid") is not None and not force: + return self + jwk = self.copy() + jwk["kid"] = self.thumbprint() + return jwk + + def with_usage_parameters( + self, + alg: Optional[str] = None, + with_alg: bool = True, + with_use: bool = True, + with_key_ops: bool = True, + ) -> Jwk: + """Copy this Jwk and add the `use` and `key_ops` parameters. + + The returned jwk `alg` parameter will be the one passed as parameter to this method, or as dfault the one declared as `alg` parameter in this Jwk. + The `use` (Public Key Use) param is deduced based on this `alg` value. + The `key_ops` (Key Operations) param is deduced based on the key `use` and if the key is public, private, + or symmetric. + + Args: + alg: the alg to use, if not present in this Jwk + with_alg: whether to include an `alg` parameter + with_use: whether to include a `use` parameter + with_key_ops: whether to include a `key_ops` parameter + + Returns: + a Jwk with the same key, with `alg`, `use` and `key_ops` parameters. + """ + alg = alg or self.alg + + if not alg: + raise ValueError("An algorithm is required to set the usage parameters") + + self._get_alg_class(alg) # raises an exception if alg is not supported + jwk = self.copy() - if self.kid is None or force: - jwk["kid"] = self.thumbprint() + if with_alg: + jwk["alg"] = alg + if with_use: + jwk["use"] = jwk.use + if with_key_ops: + jwk["key_ops"] = jwk.key_ops + + return jwk + + 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 + parameters. + """ + jwk = self.copy() + for key in self.keys(): + if key == "kty" or key in self.PARAMS and self.PARAMS[key].is_required: + continue + del jwk[key] + return jwk diff --git a/jwskate/jwk/ec.py b/jwskate/jwk/ec.py index ae84298..d10c1e2 100644 --- a/jwskate/jwk/ec.py +++ b/jwskate/jwk/ec.py @@ -2,6 +2,7 @@ from __future__ import annotations +import warnings from typing import Any, List, Mapping, Optional, Union from backports.cached_property import cached_property @@ -37,10 +38,9 @@ class ECJwk(Jwk): KTY = "EC" - CRYPTOGRAPHY_KEY_CLASSES = ( - asymmetric.ec.EllipticCurvePrivateKey, - asymmetric.ec.EllipticCurvePublicKey, - ) + CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = (asymmetric.ec.EllipticCurvePrivateKey,) + + CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = (asymmetric.ec.EllipticCurvePublicKey,) PARAMS: Mapping[str, JwkParameter] = { "crv": JwkParameter("Curve", is_private=False, is_required=True, kind="name"), @@ -124,7 +124,7 @@ def public(cls, crv: str, x: int, y: int, **params: str) -> "ECJwk": crv=crv, x=BinaPy.from_int(x, length=coord_size).to("b64u"), y=BinaPy.from_int(y, length=coord_size).to("b64u"), - **params, + **{k: v for k, v in params.items() if v is not None}, ) ) @@ -150,7 +150,7 @@ def private(cls, crv: str, x: int, y: int, d: int, **params: Any) -> "ECJwk": x=BinaPy.from_int(x, coord_size).to("b64u").ascii(), y=BinaPy.from_int(y, coord_size).to("b64u").ascii(), d=BinaPy.from_int(d, coord_size).to("b64u").ascii(), - **params, + **{k: v for k, v in params.items() if v is not None}, ) ) @@ -205,10 +205,13 @@ def _to_cryptography_key( ).public_key() @classmethod - def generate(cls, crv: str = "P-256", **params: str) -> "ECJwk": - """Generates a random ECJwk. + def generate( + cls, crv: Optional[str] = None, alg: Optional[str] = None, **params: str + ) -> "ECJwk": + """Generate a random ECJwk. Args: + alg: the alg crv: the curve to use **params: @@ -218,12 +221,30 @@ def generate(cls, crv: str = "P-256", **params: str) -> "ECJwk": Raises: UnsupportedEllipticCurve: if the provided curve identifier is not supported. """ - curve = cls.get_curve(crv) + if crv is None and alg is None: + warnings.warn( + "No Curve identifier (crv) or an Algorithm identifier (alg) have been provided " + "when generating an Elliptic Curve JWK. So there is no hint to determine which curve to use. " + "Curve 'P-256' is used by default. You should explicitly pass an 'alg' or 'crv' parameter " + "to explicitly select the appropriate Curve and avoid this warning." + ) + crv = "P-256" + curve: Optional[EllipticCurve] = None + if crv: + curve = cls.get_curve(crv) + elif alg: + if alg in cls.SIGNATURE_ALGORITHMS: + curve = cls.SIGNATURE_ALGORITHMS[alg].curve + elif alg in cls.KEY_MANAGEMENT_ALGORITHMS: + curve = P_256 + if curve is None: raise UnsupportedEllipticCurve(crv) + x, y, d = curve.generate() return cls.private( - crv=crv, + crv=curve.name, + alg=alg, x=x, y=y, d=d, @@ -284,37 +305,3 @@ def supported_encryption_algorithms(self) -> List[str]: a list of supported algorithms identifiers """ return list(self.ENCRYPTION_ALGORITHMS) - - def to_pem_key(self, password: Optional[bytes] = None) -> bytes: - """Serialize this key to PEM format. - - For private keys, you can provide a password for encryption. - - Args: - password: password to use to encrypt the PEM - - Returns: - the PEM encrypted key - """ - if self.is_private: - assert isinstance(self.cryptography_key, ec.EllipticCurvePrivateKey) - encryption: serialization.KeySerializationEncryption - if password: - encryption = serialization.BestAvailableEncryption(password) - else: - encryption = serialization.NoEncryption() - return self.cryptography_key.private_bytes( # type: ignore[no-any-return, attr-defined] - serialization.Encoding.PEM, - serialization.PrivateFormat.PKCS8, - encryption, - ) - else: - assert isinstance(self.cryptography_key, ec.EllipticCurvePublicKey) - if password: - raise ValueError( - "Public keys cannot be encrypted when serialized in PEM format." - ) - return self.cryptography_key.public_bytes( - serialization.Encoding.PEM, - serialization.PublicFormat.SubjectPublicKeyInfo, - ) diff --git a/jwskate/jwk/jwks.py b/jwskate/jwk/jwks.py index 563099b..98ed403 100644 --- a/jwskate/jwk/jwks.py +++ b/jwskate/jwk/jwks.py @@ -27,7 +27,7 @@ class JwkSet(BaseJsonDict): def __init__( self, jwks: Optional[Dict[str, Any]] = None, - keys: Optional[Iterable[Jwk]] = None, + keys: Optional[Iterable[Union[Jwk, Dict[str, Any]]]] = None, ): if jwks is None and keys is None: keys = [] @@ -132,7 +132,6 @@ def is_private(self) -> bool: Returns: `True` if this JwkSet contains at least one private key - """ return any(key.is_private for key in self.jwks) @@ -198,12 +197,46 @@ def verify( # 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 None: + if jwk.get("use") is None and jwk.get("alg") is not None: try: - if jwk.verify(data, signature, alg=alg): + if jwk.verify(data, signature): return True except UnsupportedAlg: continue # no key matches, so consider the signature invalid return False + + def verification_keys(self) -> List[Jwk]: + """Return the list of keys from this JWKS that a usable for signature verification. + + To be usable for signature verification, a key must: + - be asymmetric + - be public + - have an "alg" parameter that is a signature alg + + Returns: + a list of `Jwk` that are usable for signature verification + """ + return [ + jwk + for jwk in self.jwks + if not jwk.is_symmetric and not jwk.is_private and jwk.use == "sig" + ] + + def encryption_keys(self) -> List[Jwk]: + """Return the list of keys from this JWKS that are usable for encryption. + + To be usable for encryption, a key must: + - be asymmetric + - be public + - have an "alg" parameter that is an encryption alg + + Returns: + a list of `Jwk` that are suitable for encryption + """ + return [ + jwk + for jwk in self.jwks + if not jwk.is_symmetric and not jwk.is_private and jwk.use == "enc" + ] diff --git a/jwskate/jwk/oct.py b/jwskate/jwk/oct.py index 8cf0104..d9af024 100644 --- a/jwskate/jwk/oct.py +++ b/jwskate/jwk/oct.py @@ -2,26 +2,26 @@ from __future__ import annotations -from typing import Any, List, Optional, Tuple, Union +from typing import Any, List, Optional, SupportsBytes, Tuple, Union from binapy import BinaPy from jwskate.jwa import ( + A128CBC_HS256, A128GCM, A128GCMKW, A128KW, + A192CBC_HS384, A192GCM, A192GCMKW, A192KW, + A256CBC_HS512, A256GCM, A256GCMKW, A256KW, HS256, HS384, HS512, - Aes128CbcHmacSha256, - Aes192CbcHmacSha384, - Aes256CbcHmacSha512, BaseAESEncryptionAlg, DirectKeyUse, ) @@ -31,10 +31,11 @@ class SymmetricJwk(Jwk): - """Implement Symetric keys, with `kty=oct`.""" + """Implement Symmetric keys, with `kty=oct`.""" KTY = "oct" - CRYPTOGRAPHY_KEY_CLASSES = (bytes,) + CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = (bytes,) + CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = (bytes,) PARAMS = { "k": JwkParameter("Key Value", is_private=True, is_required=True, kind="b64u"), @@ -58,15 +59,20 @@ class SymmetricJwk(Jwk): ENCRYPTION_ALGORITHMS = { keyalg.name: keyalg for keyalg in [ - Aes128CbcHmacSha256, - Aes192CbcHmacSha384, - Aes256CbcHmacSha512, + A128CBC_HS256, + A192CBC_HS384, + A256CBC_HS512, A128GCM, A192GCM, A256GCM, ] } + @property + def is_symmetric(self) -> bool: + """Always returns `True`.""" + return True + def public_jwk(self) -> Jwk: """This always raises a ValueError since SymmetricKeys are always private. @@ -77,7 +83,9 @@ def public_jwk(self) -> Jwk: @classmethod def from_bytes(cls, k: Union[bytes, str], **params: Any) -> SymmetricJwk: - """Initializes a SymmetricJwk from a raw secret key. The provided secret key is encoded and used as the `k` parameter for the returned SymetricKey. + """Initialize a `SymmetricJwk` from a raw secret key. + + The provided secret key is encoded and used as the `k` parameter for the returned SymmetricKey. Args: k: the key to use @@ -178,15 +186,18 @@ def key(self) -> BinaPy: def encrypt( self, - plaintext: bytes, + plaintext: Union[bytes, SupportsBytes], *, aad: Optional[bytes] = None, alg: Optional[str] = None, iv: Optional[bytes] = None, ) -> Tuple[BinaPy, BinaPy, BinaPy]: - """Encrypt arbitrary data using this key. Supports Authenticated Encryption with the Additional Authenticated Data (`aad`). An Initializatin Vector (IV) will be generated automatically. You can choose your own IV by providing the `iv` parameter (only use this if you know what you are doing). + """Encrypt arbitrary data using this key. + + Supports Authenticated Encryption with Additional Authenticated Data (`aad`). + An Initialization Vector (IV) will be generated automatically. You can choose your own IV by providing the `iv` parameter (only use this if you know what you are doing). - This return the ciphertext, the authentication tag, and the used IV (if an IV was provided as parameter, the same IV is returned). + This returns the ciphertext, the authentication tag, and the used IV (if an IV was provided as parameter, the same IV is returned). Args: plaintext: the plaintext to encrypt @@ -217,11 +228,11 @@ def key_size(self) -> int: def decrypt( self, - ciphertext: bytes, + ciphertext: Union[bytes, SupportsBytes], *, - iv: bytes, - tag: bytes, - aad: Optional[bytes] = None, + iv: Union[bytes, SupportsBytes], + tag: Union[bytes, SupportsBytes], + aad: Union[bytes, SupportsBytes, None] = None, alg: Optional[str] = None, ) -> BinaPy: """Decrypt arbitrary data. @@ -236,6 +247,15 @@ def decrypt( Returns: the decrypted clear-text """ + if aad is None: # pragma: no branch + aad = b"" + elif 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) @@ -243,7 +263,9 @@ def decrypt( return BinaPy(plaintext) def supported_key_management_algorithms(self) -> List[str]: - """Return the list of supported Key Management algorithms, usable for key (un)wrapping with this key. + """Return the list of Key Management algorithms that this key supports. + + Key Management algorithms are used to generate or wrap Content Encryption Keys (CEK). Returns: a list of supported algorithms identifiers @@ -265,3 +287,16 @@ def supported_encryption_algorithms(self) -> List[str]: for name, alg in self.ENCRYPTION_ALGORITHMS.items() if alg.supports_key(self.cryptography_key) ] + + def to_pem(self, password: Union[bytes, str, None] = None) -> bytes: + """Serialize this key to PEM format. + + Symmetric keys are not serializable to PEM so this will raise a TypeError. + + Args: + password: password to use to encrypt the PEM. + + Raises: + TypeError: always + """ + raise TypeError("Symmetric keys are not serializable to PEM.") diff --git a/jwskate/jwk/okp.py b/jwskate/jwk/okp.py index 2b1b09f..51f8fa6 100644 --- a/jwskate/jwk/okp.py +++ b/jwskate/jwk/okp.py @@ -1,8 +1,11 @@ -"""This module implements JWK representing Octet Key Pairs from [RFC8037](https://datatracker.ietf.org/doc/rfc8037/).""" +"""This module implements JWK representing Octet Key Pairs from [RFC8037]. + +[RFC8037]: https://www.rfc-editor.org/rfc/rfc8037.html +""" from __future__ import annotations -from typing import Any, Mapping +from typing import Any, Mapping, Optional from backports.cached_property import cached_property from binapy import BinaPy @@ -17,6 +20,7 @@ from jwskate.jwa import X448, X25519, Ed448, Ed25519, EdDsa, OKPCurve from .. import EcdhEs, EcdhEs_A128KW, EcdhEs_A192KW, EcdhEs_A256KW +from .alg import UnsupportedAlg from .base import Jwk, JwkParameter @@ -29,14 +33,17 @@ class OKPJwk(Jwk): KTY = "OKP" - CRYPTOGRAPHY_KEY_CLASSES = ( + CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = ( ed25519.Ed25519PrivateKey, - ed25519.Ed25519PublicKey, ed448.Ed448PrivateKey, - ed448.Ed448PublicKey, x25519.X25519PrivateKey, - x25519.X25519PublicKey, x448.X448PrivateKey, + ) + + CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = ( + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + x25519.X25519PublicKey, x448.X448PublicKey, ) @@ -46,7 +53,7 @@ class OKPJwk(Jwk): "Public Key", is_private=False, is_required=True, kind="b64u" ), "d": JwkParameter( - "Private Key", is_private=True, is_required=False, kind="b64u" + "Private Key", is_private=True, is_required=True, kind="b64u" ), } @@ -208,7 +215,13 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk: else: raise TypeError( "Unsupported key type for OKP. Supported key types are: " - + ", ".join(kls.__name__ for kls in cls.CRYPTOGRAPHY_KEY_CLASSES) + + ", ".join( + kls.__name__ + for kls in ( + cls.CRYPTOGRAPHY_PRIVATE_KEY_CLASSES + + cls.CRYPTOGRAPHY_PUBLIC_KEY_CLASSES + ) + ) ) def _to_cryptography_key(self) -> Any: @@ -281,16 +294,37 @@ def private(cls, crv: str, x: bytes, d: bytes, **params: Any) -> OKPJwk: ) @classmethod - def generate(cls, crv: str = "Ed25519", **params: Any) -> OKPJwk: + def generate( + cls, crv: Optional[str] = None, alg: Optional[str] = None, **params: Any + ) -> OKPJwk: """Generate a private OKPJwk on a given curve. + You can specify either a curve or an algorithm identifier, or both. + If using an alg identifier, crv will default to Ed25519 for signature algs, + or X25519 for encryption algs. + Args: crv: the curve to use + alg: algorithm to use **params: additional members to include in the Jwk Returns: the resulting OKPJwk """ - curve = cls.get_curve(crv) + if crv: + curve = cls.get_curve(crv) + elif alg: + if alg in cls.SIGNATURE_ALGORITHMS: + curve = Ed25519 + elif alg in cls.KEY_MANAGEMENT_ALGORITHMS: + curve = X25519 + else: + raise UnsupportedAlg(alg) + else: + raise ValueError( + "You must supply at least a Curve identifier (crv) or an Algorithm identifier (alg) " + "in order to generate an OKP JWK." + ) + x, d = curve.generate() - return cls.private(crv=crv, x=x, d=d, **params) + return cls.private(crv=curve.name, x=x, d=d, alg=alg, **params) diff --git a/jwskate/jwk/rsa.py b/jwskate/jwk/rsa.py index 2307ddc..0854810 100644 --- a/jwskate/jwk/rsa.py +++ b/jwskate/jwk/rsa.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Optional, Union +from typing import Any, Optional, Tuple, Union from backports.cached_property import cached_property from binapy import BinaPy @@ -29,7 +29,8 @@ class RSAJwk(Jwk): """Represent a RSA Jwk, with `kty=RSA`.""" KTY = "RSA" - CRYPTOGRAPHY_KEY_CLASSES = (rsa.RSAPrivateKey, rsa.RSAPublicKey) + CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = (rsa.RSAPrivateKey,) + CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = (rsa.RSAPublicKey,) PARAMS = { "n": JwkParameter("Modulus", is_private=False, is_required=True, kind="b64u"), @@ -136,7 +137,7 @@ def _to_cryptography_key(self) -> Union[rsa.RSAPrivateKey, rsa.RSAPublicKey]: @classmethod def public(cls, n: int, e: int, **params: Any) -> RSAJwk: - """Initialize a public RsaJwk from a modulus and an exponent. + """Initialize a public `RsaJwk` from a modulus and an exponent. Args: n: the modulus @@ -168,7 +169,7 @@ def private( qi: Optional[int] = None, **params: Any, ) -> RSAJwk: - """Initializes a Private RsaJwk from its required parameters. + """Initialize a private `RsaJwk` from its required parameters. Args: n: the modulus @@ -201,7 +202,7 @@ def private( @classmethod def generate(cls, key_size: int = 4096, **params: Any) -> RSAJwk: - """Generates a new random private RSAJwk. + """Generate a new random private `RSAJwk`. Args: key_size: the key size to use for the generated key, in bits @@ -226,7 +227,7 @@ def generate(cls, key_size: int = 4096, **params: Any) -> RSAJwk: @cached_property def modulus(self) -> int: - """Returns the modulus from this Jwk. + """Return the modulus `n` from this Jwk. Returns: the key modulus (from parameter `n`) @@ -235,7 +236,7 @@ def modulus(self) -> int: @cached_property def exponent(self) -> int: - """Returns the exponent from this Jwk. + """Return the public exponent `e` from this Jwk. Returns: the key exponent (from parameter `e`) @@ -244,7 +245,7 @@ def exponent(self) -> int: @cached_property def private_exponent(self) -> int: - """Returns the private exponent from this Jwk. + """Return the private exponent `d` from this Jwk. Returns: the key private exponent (from parameter `d`) @@ -253,45 +254,114 @@ def private_exponent(self) -> int: @cached_property def first_prime_factor(self) -> int: - """Returns the first prime factor from this Jwk. + """Return the first prime factor `p` from this Jwk. Returns: the first prime factor (from parameter `p`) """ - return BinaPy(self.p).decode_from("b64u").to_int() + return self.prime_factors[0] @cached_property def second_prime_factor(self) -> int: - """Returns the second prime factor from this Jwk. + """Return the second prime factor `q` from this Jwk. Returns: the second prime factor (from parameter `q`) """ - return BinaPy(self.q).decode_from("b64u").to_int() + return self.prime_factors[1] + + @cached_property + def prime_factors(self) -> Tuple[int, int]: + """Return the 2 prime factors `p` and `q` from this Jwk.""" + if "p" not in self or "q" not in self: + p, q = rsa.rsa_recover_prime_factors( + self.modulus, self.exponent, self.private_exponent + ) + return (p, q) if p < q else (q, p) + return ( + BinaPy(self.p).decode_from("b64u").to_int(), + BinaPy(self.q).decode_from("b64u").to_int(), + ) @cached_property def first_factor_crt_exponent(self) -> int: - """Returns the first factor CRT exponent from this Jwk. + """Return the first factor CRT exponent `dp` from this Jwk. Returns: the first factor CRT coefficient (from parameter `dp`) """ - return BinaPy(self.dp).decode_from("b64u").to_int() + if "dp" in self: + return BinaPy(self.dp).decode_from("b64u").to_int() + return rsa.rsa_crt_dmp1(self.private_exponent, self.first_prime_factor) @cached_property def second_factor_crt_exponent(self) -> int: - """Returns the second factor CRT exponent from this Jwk. + """Return the second factor CRT exponent `dq` from this Jwk. Returns: the second factor CRT coefficient (from parameter `dq`) """ - return BinaPy(self.dq).decode_from("b64u").to_int() + if "dq" in self: + return BinaPy(self.dq).decode_from("b64u").to_int() + return rsa.rsa_crt_dmq1(self.private_exponent, self.second_prime_factor) @cached_property def first_crt_coefficient(self) -> int: - """Returns the first CRT coefficient from this Jwk. + """Return the first CRT coefficient `qi` from this Jwk. Returns: the first CRT coefficient (from parameter `qi`) """ - return BinaPy(self.qi).decode_from("b64u").to_int() + if "qi" in self: + return BinaPy(self.qi).decode_from("b64u").to_int() + return rsa.rsa_crt_iqmp(self.first_prime_factor, self.second_prime_factor) + + @cached_property + def key_size(self) -> int: + """Key size, in bits. + + Returns: + the key size + """ + return len(BinaPy(self.n).decode_from("b64u")) * 8 + + def with_optional_private_parameters(self) -> RSAJwk: + """Compute the optional RSA private parameters and add them into the JWK. + + The optional parameters are: + - p: first prime factor + - q: second prime factor + - dp: first factor Chinese Remainder Theorem exponent + - dq: second factor Chinese Remainder Theorem exponent + - qi: first Chinese Remainder Theorem coefficient + """ + if not self.is_private: + raise ValueError( + "Optional private parameters can only be computed for private RSA keys." + ) + + jwk = dict(self) + + jwk.update( + { + "p": BinaPy.from_int(self.first_prime_factor).to("b64u").ascii(), + "q": BinaPy.from_int(self.second_prime_factor).to("b64u").ascii(), + "dp": BinaPy.from_int(self.first_factor_crt_exponent) + .to("b64u") + .ascii(), + "dq": BinaPy.from_int(self.second_factor_crt_exponent) + .to("b64u") + .ascii(), + "qi": BinaPy.from_int(self.first_crt_coefficient).to("b64u").ascii(), + } + ) + + return RSAJwk(jwk) + + def without_optional_private_parameters(self) -> RSAJwk: + """Remove the optional private parameters and return another Jwk instance without them.""" + jwk = dict(self) + for param in "p", "q", "dp", "dq", "qi": + jwk.pop(param, None) + + return RSAJwk(jwk) diff --git a/jwskate/jws/compact.py b/jwskate/jws/compact.py index 903f1b9..34f8986 100644 --- a/jwskate/jws/compact.py +++ b/jwskate/jws/compact.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, SupportsBytes, Union from backports.cached_property import cached_property from binapy import BinaPy @@ -30,7 +30,12 @@ class JwsCompact(BaseCompactToken): def __init__(self, value: Union[bytes, str]): super().__init__(value) - header, payload, signature = self.split(self.value) + if self.value.count(b".") != 2: + raise InvalidJws( + "A JWS must contain a header, a payload and a signature, separated by dots" + ) + + header, payload, signature = BinaPy(self.value).split(b".") try: self.headers = BinaPy(header).decode_from("b64u").parse_from("json") @@ -53,35 +58,14 @@ def __init__(self, value: Union[bytes, str]): "Invalid JWS signature: it must be a Base64URL-encoded binary data (bytes)" ) - @classmethod - def split(cls, value: bytes) -> Tuple[BinaPy, BinaPy, BinaPy]: - """Splits a JWS token value into its (header, payload, signature) parts. - - Args: - value: the JWS token value - - Returns: - a (header, payload, signature) - - Raises: - InvalidJws: if the provided value doesn't have 2 dots. - """ - if value.count(b".") != 2: - raise InvalidJws( - "A JWS must contain a header, a payload and a signature, separated by dots" - ) - - header, payload, signature = value.split(b".") - return BinaPy(header), BinaPy(payload), BinaPy(signature) - @classmethod def sign( cls, - payload: bytes, + payload: Union[bytes, SupportsBytes], jwk: Union[Jwk, Dict[str, Any]], alg: Optional[str] = None, extra_headers: Optional[Dict[str, Any]] = None, - ) -> "JwsCompact": + ) -> JwsCompact: """Sign a payload and returns the resulting JwsCompact. Args: @@ -95,6 +79,9 @@ def sign( """ jwk = Jwk(jwk) + if not isinstance(payload, bytes): + payload = bytes(payload) + headers = dict(extra_headers or {}, alg=alg) kid = jwk.get("kid") if kid: @@ -106,8 +93,10 @@ def sign( @classmethod def from_parts( - cls, signed_part: Union[bytes, str], signature: Union[bytes, str] - ) -> "JwsCompact": + cls, + signed_part: Union[bytes, SupportsBytes, str], + signature: Union[bytes, SupportsBytes, str], + ) -> JwsCompact: """Constructs a JWS token based on its signed part and signature values. Signed part is the concatenation of the header and payload, both encoded in Base64-Url, and joined by a dot. @@ -119,8 +108,15 @@ def from_parts( Returns: the resulting token """ - if not isinstance(signed_part, bytes): + if isinstance(signed_part, str): signed_part = signed_part.encode("ascii") + if not isinstance(signed_part, bytes): + signed_part = bytes(signed_part) + + if isinstance(signature, str): + signature = signature.encode("ascii") + if not isinstance(signature, bytes): + signature = bytes(signature) return cls(b".".join((signed_part, BinaPy(signature).to("b64u")))) @@ -164,11 +160,12 @@ def flat_json(self, unprotected_header: Any = None) -> JwsJsonFlat: """ from .json import JwsJsonFlat - protected, payload, signature = self.split(self.value) + protected, payload, signature = self.value.split(b".") + content = { - "payload": payload.ascii(), - "protected": protected.ascii(), - "signature": signature.ascii(), + "payload": payload.decode(), + "protected": protected.decode(), + "signature": signature.decode(), } if unprotected_header is not None: content["header"] = unprotected_header diff --git a/jwskate/jws/json.py b/jwskate/jws/json.py index c66003e..d3a012d 100644 --- a/jwskate/jws/json.py +++ b/jwskate/jws/json.py @@ -166,7 +166,7 @@ def sign( Union[Jwk, Mapping[str, Any]], ], ) -> JwsJsonGeneral: - """Sign a payload with several keys and return the resulting JWS JSON in general format. + """Sign a payload with several keys and return the resulting JWS in JSON general format. Args: payload: the data to sign diff --git a/jwskate/jwt/__init__.py b/jwskate/jwt/__init__.py index 1fa2ee5..be34c49 100644 --- a/jwskate/jwt/__init__.py +++ b/jwskate/jwt/__init__.py @@ -1,14 +1,12 @@ """This module contains all Json Web Key (Jwk) related classes and utilities.""" from .base import InvalidJwt, Jwt -from .encrypted import EncryptedJwt from .signed import ExpiredJwt, InvalidClaim, InvalidSignature, SignedJwt from .signer import JwtSigner __all__ = [ "Jwt", "InvalidJwt", - "EncryptedJwt", "ExpiredJwt", "InvalidClaim", "InvalidSignature", diff --git a/jwskate/jwt/base.py b/jwskate/jwt/base.py index f99e46e..dbc4080 100644 --- a/jwskate/jwt/base.py +++ b/jwskate/jwt/base.py @@ -2,16 +2,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Union from binapy import BinaPy +from jwskate.jwe import JweCompact from jwskate.jwk import Jwk from ..token import BaseCompactToken if TYPE_CHECKING: - from jwskate import EncryptedJwt, SignedJwt # pragma: no cover + from jwskate import SignedJwt # pragma: no cover class InvalidJwt(ValueError): @@ -22,7 +23,9 @@ class Jwt(BaseCompactToken): """Represents a Json Web Token.""" def __new__(cls, value: Union[bytes, str]): # type: ignore - """Allow parsing both Signed and Encrypted JWTs. Returns the appropriate subclass instance. + """Allow parsing both Signed and Encrypted JWTs. + + This returns the appropriate subclass or instance depending on the number of dots (.) in the serialized JWT. Args: value: the token value @@ -35,10 +38,10 @@ def __new__(cls, value: Union[bytes, str]): # type: ignore from .signed import SignedJwt return super().__new__(SignedJwt) - elif value.count(b".") == 3: - from .encrypted import EncryptedJwt + elif value.count(b".") == 4: + from ..jwe import JweCompact - return super().__new__(EncryptedJwt) + return JweCompact(value) return super().__new__(cls) @classmethod @@ -68,15 +71,14 @@ def sign( jwk = Jwk(jwk) alg = alg or jwk.get("alg") - kid = jwk.get("kid") if alg is None: raise ValueError("a signing alg is required") extra_headers = extra_headers or {} headers = dict(alg=alg, **extra_headers) - if kid: - headers["kid"] = kid + if jwk.kid: + headers["kid"] = jwk.kid headers_part = BinaPy.serialize_to("json", headers).to("b64u") claims_part = BinaPy.serialize_to("json", claims).to("b64u") @@ -114,24 +116,100 @@ def sign_and_encrypt( cls, claims: Dict[str, Any], sign_jwk: Union[Jwk, Dict[str, Any]], - sign_alg: Optional[str], enc_jwk: Union[Jwk, Dict[str, Any]], - enc_alg: Optional[str], - enc: Optional[str], - ) -> "EncryptedJwt": - """Sign then encrypt a payload with a `Jwk` and returns the resulting `EncryptedJwt`. + enc: str, + *, + sign_alg: Optional[str] = None, + enc_alg: Optional[str] = None, + sign_extra_headers: Optional[Dict[str, Any]] = None, + enc_extra_headers: Optional[Dict[str, Any]] = None, + ) -> JweCompact: + """Sign a JWT, then encrypt it as JWE payload. - NOT IMPLEMENTED YET. + This is a convenience method to do both the signing and encryption, in appropriate order. Args: claims: the payload to encrypt sign_jwk: the Jwk to use for signature sign_alg: the alg to use for signature + sign_extra_headers: additional headers for the inner signed JWT enc_jwk: the Jwk to use for encryption enc_alg: the alg to use for CEK encryption enc: the alg to use for payload encryption + enc_extra_headers: additional headers for the outer encrypted JWE Returns: - the resulting JWE token, with signed JWT as payload + the resulting JWE token, with the signed JWT as payload """ - raise NotImplementedError + enc_extra_headers = enc_extra_headers or {} + enc_extra_headers.setdefault("cty", "JWT") + + inner_jwt = cls.sign( + claims, jwk=sign_jwk, alg=sign_alg, extra_headers=sign_extra_headers + ) + jwe = JweCompact.encrypt( + inner_jwt, enc_jwk, enc=enc, alg=enc_alg, extra_headers=enc_extra_headers + ) + return jwe + + @classmethod + def decrypt_nested_jwt( + cls, jwe: Union[str, JweCompact], jwk: Union[Jwk, Dict[str, Any]] + ) -> Jwt: + """Convenience method to decrypt a nested JWT. + + It will return a Jwt instance. + + Args: + jwt: the JWE containing a nested Token + jwk: the decryption key + + Returns: + the inner token + + Raises: + InvalidJwt: if the inner JWT is not valid + """ + if not isinstance(jwe, JweCompact): + jwe = JweCompact(jwe) + cleartext = jwe.decrypt(jwk) + return Jwt(cleartext) + + @classmethod + def decrypt_and_verify( + cls, + jwt: Union[str, JweCompact], + enc_jwk: Union[Jwk, Dict[str, Any]], + sig_jwk: Union[Jwk, Dict[str, Any], None], + sig_alg: Optional[str] = None, + sig_algs: Optional[Iterable[str]] = None, + ) -> SignedJwt: + """Decrypt then verify the signature of a JWT nested in a JWE. + + This can only be used with signed then encrypted Jwt, such as those produce by `Jwt.sign_and_encrypt()`. + + Args: + jwt: the JWE containing a nested signed JWT + enc_jwk: the decryption key + sig_jwk: the signature verification key + sig_alg: the signature verification alg, if only 1 is allowed + sig_algs: the signature verifications algs, if several are allowed + + Returns: + the nested signed JWT, in clear-text, signature already verified + + Raises: + InvalidJwt: if the JWT is not valid + InvalidSignature: if the nested JWT signature is not valid + """ + from .signed import InvalidSignature, SignedJwt + + nested_jwt = cls.decrypt_nested_jwt(jwt, enc_jwk) + if not isinstance(nested_jwt, SignedJwt): + raise ValueError("Nested JWT is not signed", nested_jwt) + + if sig_jwk: + if nested_jwt.verify_signature(sig_jwk, sig_alg, sig_algs): + return nested_jwt + + raise InvalidSignature() diff --git a/jwskate/jwt/encrypted.py b/jwskate/jwt/encrypted.py deleted file mode 100644 index 8943a40..0000000 --- a/jwskate/jwt/encrypted.py +++ /dev/null @@ -1,16 +0,0 @@ -"""This module implements Encrypted JWT token handling.""" - -from typing import Union - -from .base import Jwt - - -class EncryptedJwt(Jwt): - """Represent an encrypted JWT. - - Args: - value: the serialized JWT value - """ - - def __init__(self, value: Union[bytes, str]): - raise NotImplementedError diff --git a/jwskate/jwt/signed.py b/jwskate/jwt/signed.py index 3df9bbb..936d061 100644 --- a/jwskate/jwt/signed.py +++ b/jwskate/jwt/signed.py @@ -185,7 +185,7 @@ def issuer(self) -> Optional[str]: raise AttributeError("iss has an unexpected type", type(iss)) @cached_property - def audiences(self) -> Optional[List[str]]: + def audiences(self) -> List[str]: """Get the audience(s) (aud) claim from this token. If this token has a single audience, this will return a `list` anyway. @@ -198,7 +198,7 @@ def audiences(self) -> Optional[List[str]]: """ aud = self.get_claim("aud") if aud is None: - return None + return [] if isinstance(aud, str): return [aud] if isinstance(aud, list): diff --git a/jwskate/token.py b/jwskate/token.py index a787385..4f74f0b 100644 --- a/jwskate/token.py +++ b/jwskate/token.py @@ -24,9 +24,11 @@ def __init__(self, value: Union[bytes, str], max_size: int = 16 * 1024): "You can increase this limit by passing a different `max_size` value as parameter." ) - if not isinstance(value, bytes): + if isinstance(value, str): value = value.encode("ascii") + value = b"".join(value.split()) + self.value = value self.headers: Dict[str, Any] @@ -70,8 +72,8 @@ def alg(self) -> str: AttributeError: if the `alg` header value is not a string """ alg = self.get_header("alg") - if alg is None or not isinstance(alg, str): - raise AttributeError("This JWS doesn't have a valid 'alg' header") + if alg is None or not isinstance(alg, str): # pragma: no branch + raise AttributeError("This token doesn't have a valid 'alg' header") return alg @cached_property @@ -85,9 +87,37 @@ def kid(self) -> str: """ kid = self.get_header("kid") if kid is None or not isinstance(kid, str): - raise AttributeError("This JWS doesn't have a valid 'kid' header") + raise AttributeError("This token doesn't have a valid 'kid' header") return kid + @cached_property + def typ(self) -> str: + """Get the Type (typ) from this token headers. + + Returns: + the `typ` value + Raises: + AttributeError: if the `typ` header value is not a string + """ + typ = self.get_header("typ") + if typ is None or not isinstance(typ, str): # pragma: no branch + raise AttributeError("This token doesn't have a valid 'typ' header") + return typ + + @cached_property + def cty(self) -> str: + """Get the Type (typ) from this token headers. + + Returns: + the `typ` value + Raises: + AttributeError: if the `typ` header value is not a string + """ + cty = self.get_header("cty") + if cty is None or not isinstance(cty, str): # pragma: no branch + raise AttributeError("This token doesn't have a valid 'cty' header") + return cty + def __repr__(self) -> str: """Returns the `str` representation of this token.""" return self.value.decode() diff --git a/mkdocs.yml b/mkdocs.yml index 62fcfd1..0dbe4ee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,7 @@ nav: - home: index.md - installation: installation.md - usage: usage.md + - examples: recipes.md - API: api.md - contributing: contributing.md #- authors: authors.md diff --git a/poetry.lock b/poetry.lock index 82a3ee1..846f226 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,11 +1,3 @@ -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "attrs" version = "22.1.0" @@ -15,10 +7,10 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +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"] [[package]] name = "backports.cached-property" @@ -41,7 +33,7 @@ typing-extensions = ">=4.3.0,<5.0.0" [[package]] name = "black" -version = "22.6.0" +version = "22.8.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -72,7 +64,7 @@ python-versions = "*" [[package]] name = "certifi" -version = "2022.6.15" +version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false @@ -99,7 +91,7 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.1.0" +version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false @@ -130,7 +122,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.2" +version = "6.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -144,7 +136,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "37.0.4" +version = "38.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -155,14 +147,14 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +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" @@ -173,11 +165,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" wrapt = ">=1.10,<2" [package.extras] -dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] +dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] [[package]] name = "distlib" -version = "0.3.5" +version = "0.3.6" description = "Distribution utilities" category = "dev" optional = false @@ -185,15 +177,15 @@ python-versions = "*" [[package]] name = "filelock" -version = "3.7.1" +version = "3.8.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "ghp-import" @@ -207,11 +199,11 @@ python-versions = "*" python-dateutil = ">=2.8.1" [package.extras] -dev = ["twine", "markdown", "flake8", "wheel"] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "0.22.0" +version = "0.22.2" 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 @@ -225,7 +217,7 @@ async = ["aiofiles (>=0.7,<1.0)"] [[package]] name = "identify" -version = "2.5.2" +version = "2.5.5" description = "File identification library for Python" category = "dev" optional = false @@ -236,7 +228,7 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false @@ -255,9 +247,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "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 (>=0.9.2)"] [[package]] name = "iniconfig" @@ -276,13 +268,13 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +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" @@ -297,7 +289,7 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jwcrypto" -version = "1.3.1" +version = "1.4.2" description = "Implementation of JOSE Web standards" category = "dev" optional = false @@ -320,7 +312,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" @@ -334,7 +326,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" @@ -351,22 +343,23 @@ python-versions = ">=3.6" [[package]] name = "mkdocs" -version = "1.3.1" +version = "1.4.0" description = "Project documentation with Markdown." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -click = ">=3.3" +click = ">=7.0" ghp-import = ">=1.0" -importlib-metadata = ">=4.3" -Jinja2 = ">=2.10.2" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.11.1" Markdown = ">=3.2.1,<3.4" mergedeep = ">=1.3.4" packaging = ">=20.5" -PyYAML = ">=3.10" +PyYAML = ">=5.1" pyyaml-env-tag = ">=0.1" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} watchdog = ">=2.0" [package.extras] @@ -386,19 +379,19 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-include-markdown-plugin" -version = "3.6.1" +version = "3.9.0" description = "Mkdocs Markdown includer plugin." category = "dev" optional = false python-versions = ">=3.6" [package.extras] -dev = ["bump2version (==1.0.1)", "flake8 (==3.9.2)", "flake8-implicit-str-concat (==0.2.0)", "flake8-print (==4.0.0)", "isort (==5.9.1)", "mdpo (==0.3.61)", "mkdocs (==1.2.3)", "pre-commit (==2.13.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "pyupgrade (==2.19.4)", "yamllint (==1.26.1)"] -test = ["pytest (==6.2.5)", "pytest-cov (==3.0.0)"] +dev = ["bump2version (==1.0.1)", "mkdocs (==1.4.0)", "pre-commit", "pytest (==7.1.3)", "pytest-cov (==3.0.0)", "tox"] +test = ["mkdocs (==1.4.0)", "pytest (==7.1.3)", "pytest-cov (==3.0.0)"] [[package]] name = "mkdocs-material" -version = "8.3.9" +version = "8.5.3" description = "Documentation that simply works" category = "dev" optional = false @@ -411,6 +404,7 @@ mkdocs = ">=1.3.0" mkdocs-material-extensions = ">=1.0.3" pygments = ">=2.12" pymdown-extensions = ">=9.4" +requests = ">=2.26" [[package]] name = "mkdocs-material-extensions" @@ -456,11 +450,11 @@ mkdocstrings = ">=0.19" [[package]] name = "mypy" -version = "0.971" +version = "0.981" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] mypy-extensions = ">=0.4.3" @@ -489,6 +483,9 @@ category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "21.3" @@ -502,11 +499,19 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.9.0" +version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" + +[[package]] +name = "pip" +version = "22.2.2" +description = "The PyPA recommended tool for installing Python packages." +category = "dev" +optional = false +python-versions = ">=3.7" [[package]] name = "platformdirs" @@ -517,8 +522,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +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)"] [[package]] name = "pluggy" @@ -569,13 +574,16 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "pygments" -version = "2.12.0" +name = "Pygments" +version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.6" +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "pymdown-extensions" version = "9.5" @@ -596,18 +604,17 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.2" +version = "7.1.3" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -622,7 +629,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. [[package]] name = "pytest-cov" -version = "3.0.0" +version = "4.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false @@ -633,27 +640,27 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-mypy" -version = "0.9.1" +version = "0.10.0" description = "Mypy static type checker plugin for Pytest" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] attrs = ">=19.0" filelock = ">=3.0" mypy = [ - {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\""}, + {version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""}, + {version = ">=0.500", markers = "python_version < \"3.8\""}, ] pytest = [ - {version = ">=6.2", markers = "python_version >= \"3.10\""}, {version = ">=4.6", markers = "python_version >= \"3.6\" and python_version < \"3.10\""}, + {version = ">=6.2", markers = "python_version >= \"3.10\""}, ] [[package]] @@ -668,7 +675,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" @@ -676,7 +683,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" @@ -706,7 +713,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-mock" -version = "1.9.3" +version = "1.10.0" description = "Mock out responses from the requests package" category = "dev" optional = false @@ -718,7 +725,20 @@ six = "*" [package.extras] fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.18)", "testtools"] +test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testrepository (>=0.0.18)", "testtools"] + +[[package]] +name = "setuptools" +version = "65.4.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +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", "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-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]] name = "six" @@ -754,7 +774,7 @@ python-versions = ">= 3.7" [[package]] name = "tox" -version = "3.25.1" +version = "3.26.0" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -768,12 +788,12 @@ packaging = ">=14" pluggy = ">=0.12.0" py = ">=1.4.17" six = ">=1.14.0" -toml = ">=0.9.4" +tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "tox-poetry" @@ -789,7 +809,7 @@ toml = "*" tox = {version = ">=3.7.0", markers = "python_version >= \"3\""} [package.extras] -test = ["coverage", "pytest", "pycodestyle", "pylint"] +test = ["coverage", "pycodestyle", "pylint", "pytest"] [[package]] name = "typed-ast" @@ -809,7 +829,7 @@ python-versions = "*" [[package]] name = "types-cryptography" -version = "3.3.21" +version = "3.3.23" description = "Typing stubs for cryptography" category = "dev" optional = false @@ -817,7 +837,7 @@ python-versions = "*" [[package]] name = "types-requests" -version = "2.28.6" +version = "2.28.11" description = "Typing stubs for requests" category = "dev" optional = false @@ -828,7 +848,7 @@ types-urllib3 = "<1.27" [[package]] name = "types-urllib3" -version = "1.26.18" +version = "1.26.25" description = "Typing stubs for urllib3" category = "dev" optional = false @@ -844,34 +864,34 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.11" +version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.2" +version = "20.16.5" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.1,<1" -filelock = ">=3.2,<4" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -platformdirs = ">=2,<3" +distlib = ">=0.3.5,<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.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +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]] name = "watchdog" @@ -901,8 +921,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +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)"] [extras] doc = [] @@ -911,10 +931,9 @@ test = [] [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0" -content-hash = "b71e73411bde27c9765059cbc0b7a42c76904e80f95e9223dc0e58e8d507b530" +content-hash = "fb6d064c45313d81343b9b218cd6424e57f98e7a61892d977155b454aa2adb71" [metadata.files] -atomicwrites = [] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, @@ -927,48 +946,233 @@ binapy = [ {file = "binapy-0.6.0-py3-none-any.whl", hash = "sha256:6ffc2812df7f43a50ed45f451af8a6f851c87e347e3a071bb0f423a1a60d8a39"}, {file = "binapy-0.6.0.tar.gz", hash = "sha256:1054b0ef1e6eccd941d4b23167ba4c5b1f5938960750686c421edc71af543fa7"}, ] -black = [] +black = [ + {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, + {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, + {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, + {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, + {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, + {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, + {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, + {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, + {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, + {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, + {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, + {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, + {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, + {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, + {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, + {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, + {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, + {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, + {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, +] cached-property = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] -certifi = [] -cffi = [] +certifi = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -charset-normalizer = [] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] -colorama = [] -coverage = [] -cryptography = [] -deprecated = [ +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +coverage = [ + {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, + {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, + {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, + {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, + {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, + {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, + {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, + {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, + {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, + {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, + {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, + {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, + {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, + {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, + {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, + {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, +] +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 = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] -distlib = [] +distlib = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] filelock = [ - {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, - {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, + {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, + {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] ghp-import = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] -griffe = [] +griffe = [ + {file = "griffe-0.22.2-py3-none-any.whl", hash = "sha256:cea5415ac6a92f4a22638e3f1f2e661402bac09fb8e8266936d67185a7e0d0fb"}, + {file = "griffe-0.22.2.tar.gz", hash = "sha256:1408e336a4155392bbd81eed9f2f44bf144e71b9c664e905630affe83bbc088e"}, +] identify = [ - {file = "identify-2.5.2-py2.py3-none-any.whl", hash = "sha256:feaa9db2dc0ce333b453ce171c0cf1247bbfde2c55fc6bb785022d411a1b78b5"}, - {file = "identify-2.5.2.tar.gz", hash = "sha256:a3d4c096b384d50d5e6dc5bc8b9bc44f1f61cefebd750a7b3e9f939b53fb214d"}, + {file = "identify-2.5.5-py2.py3-none-any.whl", hash = "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97"}, + {file = "identify-2.5.5.tar.gz", hash = "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6"}, ] idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, + {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, ] -importlib-metadata = [] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -977,21 +1181,21 @@ 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"}, ] jwcrypto = [ - {file = "jwcrypto-1.3.1.tar.gz", hash = "sha256:54b551b115ffb4d12b1f1ee93b8ba2a71bb8556ba3d85d62f707549613da877c"}, + {file = "jwcrypto-1.4.2.tar.gz", hash = "sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b"}, ] 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"}, @@ -1038,18 +1242,21 @@ mergedeep = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] mkdocs = [ - {file = "mkdocs-1.3.1-py3-none-any.whl", hash = "sha256:fda92466393127d2da830bc6edc3a625a14b436316d1caf347690648e774c4f0"}, - {file = "mkdocs-1.3.1.tar.gz", hash = "sha256:a41a2ff25ce3bbacc953f9844ba07d106233cd76c88bac1f59cb1564ac0d87ed"}, + {file = "mkdocs-1.4.0-py3-none-any.whl", hash = "sha256:ce057e9992f017b8e1496b591b6c242cbd34c2d406e2f9af6a19b97dd6248faa"}, + {file = "mkdocs-1.4.0.tar.gz", hash = "sha256:e5549a22d59e7cb230d6a791edd2c3d06690908454c0af82edc31b35d57e3069"}, ] 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.6.1-py3-none-any.whl", hash = "sha256:ea36a7d50ee98028f03574d7bf40a307e16211ad3013a4e42f64494b3c106e9e"}, - {file = "mkdocs_include_markdown_plugin-3.6.1.tar.gz", hash = "sha256:5e7416f23081085a220f7534b2fc7456e74c5a65f3b401da1f29b9e9132b46e5"}, + {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"}, +] +mkdocs-material = [ + {file = "mkdocs_material-8.5.3-py3-none-any.whl", hash = "sha256:d194c38041d1e83560221022b3f85eec4604b35e44f5c3a488c24b88542074ed"}, + {file = "mkdocs_material-8.5.3.tar.gz", hash = "sha256:43b0aa707d6f9acd836024cab2dce9330957c94a4e1e41c23ee6c8ce67b4d8c5"}, ] -mkdocs-material = [] 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"}, @@ -1063,42 +1270,50 @@ mkdocstrings-python = [ {file = "mkdocstrings_python-0.7.1-py3-none-any.whl", hash = "sha256:a22060bfa374697678e9af4e62b020d990dad2711c98f7a9fac5c0345bef93c7"}, ] mypy = [ - {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, - {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, - {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, - {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, - {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, - {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, - {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, - {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, - {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, - {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, - {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, - {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, - {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, - {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, - {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, - {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, - {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, - {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, - {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, + {file = "mypy-0.981-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4bc460e43b7785f78862dab78674e62ec3cd523485baecfdf81a555ed29ecfa0"}, + {file = "mypy-0.981-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:756fad8b263b3ba39e4e204ee53042671b660c36c9017412b43af210ddee7b08"}, + {file = "mypy-0.981-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16a0145d6d7d00fbede2da3a3096dcc9ecea091adfa8da48fa6a7b75d35562d"}, + {file = "mypy-0.981-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce65f70b14a21fdac84c294cde75e6dbdabbcff22975335e20827b3b94bdbf49"}, + {file = "mypy-0.981-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e35d764784b42c3e256848fb8ed1d4292c9fc0098413adb28d84974c095b279"}, + {file = "mypy-0.981-cp310-cp310-win_amd64.whl", hash = "sha256:e53773073c864d5f5cec7f3fc72fbbcef65410cde8cc18d4f7242dea60dac52e"}, + {file = "mypy-0.981-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ee196b1d10b8b215e835f438e06965d7a480f6fe016eddbc285f13955cca659"}, + {file = "mypy-0.981-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad21d4c9d3673726cf986ea1d0c9fb66905258709550ddf7944c8f885f208be"}, + {file = "mypy-0.981-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1debb09043e1f5ee845fa1e96d180e89115b30e47c5d3ce53bc967bab53f62d"}, + {file = "mypy-0.981-cp37-cp37m-win_amd64.whl", hash = "sha256:9f362470a3480165c4c6151786b5379351b790d56952005be18bdbdd4c7ce0ae"}, + {file = "mypy-0.981-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c9e0efb95ed6ca1654951bd5ec2f3fa91b295d78bf6527e026529d4aaa1e0c30"}, + {file = "mypy-0.981-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e178eaffc3c5cd211a87965c8c0df6da91ed7d258b5fc72b8e047c3771317ddb"}, + {file = "mypy-0.981-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:06e1eac8d99bd404ed8dd34ca29673c4346e76dd8e612ea507763dccd7e13c7a"}, + {file = "mypy-0.981-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa38f82f53e1e7beb45557ff167c177802ba7b387ad017eab1663d567017c8ee"}, + {file = "mypy-0.981-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:64e1f6af81c003f85f0dfed52db632817dabb51b65c0318ffbf5ff51995bbb08"}, + {file = "mypy-0.981-cp38-cp38-win_amd64.whl", hash = "sha256:e1acf62a8c4f7c092462c738aa2c2489e275ed386320c10b2e9bff31f6f7e8d6"}, + {file = "mypy-0.981-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6ede64e52257931315826fdbfc6ea878d89a965580d1a65638ef77cb551f56d"}, + {file = "mypy-0.981-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eb3978b191b9fa0488524bb4ffedf2c573340e8c2b4206fc191d44c7093abfb7"}, + {file = "mypy-0.981-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f8fcf7b4b3cc0c74fb33ae54a4cd00bb854d65645c48beccf65fa10b17882c"}, + {file = "mypy-0.981-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64d2ce043a209a297df322eb4054dfbaa9de9e8738291706eaafda81ab2b362"}, + {file = "mypy-0.981-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2ee3dbc53d4df7e6e3b1c68ac6a971d3a4fb2852bf10a05fda228721dd44fae1"}, + {file = "mypy-0.981-cp39-cp39-win_amd64.whl", hash = "sha256:8e8e49aa9cc23aa4c926dc200ce32959d3501c4905147a66ce032f05cb5ecb92"}, + {file = "mypy-0.981-py3-none-any.whl", hash = "sha256:794f385653e2b749387a42afb1e14c2135e18daeb027e0d97162e4b7031210f8"}, + {file = "mypy-0.981.tar.gz", hash = "sha256:ad77c13037d3402fbeffda07d51e3f228ba078d1c7096a73759c9419ea031bf4"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -nodeenv = [] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, + {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, + {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"}, ] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, @@ -1108,7 +1323,10 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pre-commit = [] +pre-commit = [ + {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, + {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1117,9 +1335,9 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, +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.5-py3-none-any.whl", hash = "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"}, @@ -1130,22 +1348,22 @@ pyparsing = [ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, + {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, + {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, ] pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] pytest-mypy = [ - {file = "pytest-mypy-0.9.1.tar.gz", hash = "sha256:9ffa3bf405c12c5c6be9e92e22bebb6ab2c91b9c32f45b0f0c93af473269ab5c"}, - {file = "pytest_mypy-0.9.1-py3-none-any.whl", hash = "sha256:a2505fcf61f1c0c51f950d4623ea8ca2daf6fb2101a5603554bad2e130202083"}, + {file = "pytest-mypy-0.10.0.tar.gz", hash = "sha256:e74d632685f15a39c31c551a9d8cec4619e24bd396245a6335c5db0ec6d17b6f"}, + {file = "pytest_mypy-0.10.0-py3-none-any.whl", hash = "sha256:83843dce75a7ce055efb264ff40dad2ecf7abd4e7bd5e5eda015261d11616abb"}, ] 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"}, @@ -1153,6 +1371,13 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -1180,14 +1405,21 @@ 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"}, ] -requests = [] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] requests-mock = [ - {file = "requests-mock-1.9.3.tar.gz", hash = "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"}, - {file = "requests_mock-1.9.3-py2.py3-none-any.whl", hash = "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970"}, + {file = "requests-mock-1.10.0.tar.gz", hash = "sha256:59c9c32419a9fb1ae83ec242d98e889c45bd7d7a65d48375cc243ec08441658b"}, + {file = "requests_mock-1.10.0-py2.py3-none-any.whl", hash = "sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699"}, +] +setuptools = [ + {file = "setuptools-65.4.0-py3-none-any.whl", hash = "sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1"}, + {file = "setuptools-65.4.0.tar.gz", hash = "sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1201,8 +1433,23 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tornado = [] -tox = [] +tornado = [ + {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, + {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, + {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, + {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, + {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"}, +] tox-poetry = [ {file = "tox-poetry-0.4.1.tar.gz", hash = "sha256:2395808e1ce487b5894c10f2202e14702bfa6d6909c0d1e525170d14809ac7ef"}, {file = "tox_poetry-0.4.1-py2.py3-none-any.whl", hash = "sha256:11d9cd4e51d4cd9484b3ba63f2650ab4cfb4096e5f0682ecf561ddfc3c8e8c92"}, @@ -1238,25 +1485,28 @@ types-backports = [ {file = "types_backports-0.1.3-py2.py3-none-any.whl", hash = "sha256:dafcd61848081503e738a7768872d1dd6c018401b4d2a1cfb608ea87ec9864b9"}, ] types-cryptography = [ - {file = "types-cryptography-3.3.21.tar.gz", hash = "sha256:ad1b9c63159c009f8676c7e41a4d595dfb96e8c03affa2e693e1617908bb409e"}, - {file = "types_cryptography-3.3.21-py3-none-any.whl", hash = "sha256:bdeb6dd07280ac724e05f02e0d8ef01fdef729b18bb07d635d64de83171a4e70"}, + {file = "types-cryptography-3.3.23.tar.gz", hash = "sha256:b85c45fd4d3d92e8b18e9a5ee2da84517e8fff658e3ef5755c885b1c2a27c1fe"}, + {file = "types_cryptography-3.3.23-py3-none-any.whl", hash = "sha256:913b3e66a502edbf4bfc3bb45e33ab476040c56942164a7ff37bd1f0ef8ef783"}, ] types-requests = [ - {file = "types-requests-2.28.6.tar.gz", hash = "sha256:cf3383bbd79394bf051a0a9202d6831fa962f186f923c178f7c059e3424bd00e"}, - {file = "types_requests-2.28.6-py3-none-any.whl", hash = "sha256:d8d7607419cd4b41a7b9497e15e8c0bad78d50df43c48ad25bc526a11518c3a9"}, + {file = "types-requests-2.28.11.tar.gz", hash = "sha256:7ee827eb8ce611b02b5117cfec5da6455365b6a575f5e3ff19f655ba603e6b4e"}, + {file = "types_requests-2.28.11-py3-none-any.whl", hash = "sha256:af5f55e803cabcfb836dad752bd6d8a0fc8ef1cd84243061c0e27dee04ccf4fd"}, ] types-urllib3 = [ - {file = "types-urllib3-1.26.18.tar.gz", hash = "sha256:8d65dfc04e9250447899843633c445a1f60f99c30b4807e96a4e1a707568eeda"}, - {file = "types_urllib3-1.26.18-py3-none-any.whl", hash = "sha256:10ac26d8d4921bde84fa2b7ead6b3061ea81e52e9fa5ed3382538eb49354ac2a"}, + {file = "types-urllib3-1.26.25.tar.gz", hash = "sha256:5aef0e663724eef924afa8b320b62ffef2c1736c1fa6caecfc9bc6c8ae2c3def"}, + {file = "types_urllib3-1.26.25-py3-none-any.whl", hash = "sha256:c1d78cef7bd581e162e46c20a57b2e1aa6ebecdcf01fd0713bb90978ff3e3427"}, +] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] -typing-extensions = [] urllib3 = [ - {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, - {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] virtualenv = [ - {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, - {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, + {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, + {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, ] watchdog = [ {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, @@ -1351,4 +1601,7 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -zipp = [] +zipp = [ + {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, + {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, +] diff --git a/pyproject.toml b/pyproject.toml index 24ea7cc..2830425 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [tool] [tool.poetry] name = "jwskate" -version = "0.3.0" +version = "0.4.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 "] readme = "README.md" license = "MIT" classifiers=[ - 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', @@ -17,6 +17,7 @@ classifiers=[ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', ] packages = [ { include = "jwskate" }, @@ -98,3 +99,10 @@ warn_redundant_casts = true [tool.pydocstyle] convention = "google" match_dir = 'jwskate' + + +[tool.docformatter] +recursive = true +wrap-summaries = 120 +wrap-descriptions = 120 +blank = true diff --git a/tests/test_jwa.py b/tests/test_jwa.py index b3af258..131e3ad 100644 --- a/tests/test_jwa.py +++ b/tests/test_jwa.py @@ -1,9 +1,21 @@ """Tests for the jwkskate.jwa submodule.""" - +import pytest from binapy import BinaPy +from cryptography.hazmat.primitives.asymmetric import ec -from jwskate import Jwk -from jwskate.jwa import Aes128CbcHmacSha256, Aes192CbcHmacSha384, EcdhEs +from jwskate import ( + A128CBC_HS256, + A128GCM, + A192CBC_HS384, + A192GCM, + A256CBC_HS512, + A256GCM, + ES256, + BaseAESEncryptionAlg, + EcdhEs, + Jwk, + MismatchingAuthTag, +) def test_aes_128_hmac_sha256() -> None: @@ -68,7 +80,7 @@ def test_aes_128_hmac_sha256() -> None: "65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4".replace(" ", "") ) - cipher = Aes128CbcHmacSha256(key) + cipher = A128CBC_HS256(key) assert cipher.aes_key == enc_key assert cipher.mac_key == mac_key result_ciphertext, result_tag = cipher.encrypt(plaintext, iv=iv, aad=aad) @@ -146,7 +158,7 @@ def test_aes_192_hmac_sha384() -> None: ).replace(" ", "") ) - cipher = Aes192CbcHmacSha384(key) + cipher = A192CBC_HS384(key) assert cipher.aes_key == enc_key assert cipher.mac_key == mac_key result_ciphertext, result_tag = cipher.encrypt(plaintext, iv=iv, aad=aad) @@ -191,3 +203,65 @@ 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_jwe.py b/tests/test_jwe.py index a57be7a..f8a5543 100644 --- a/tests/test_jwe.py +++ b/tests/test_jwe.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import SupportsBytes, Union import pytest @@ -238,7 +238,6 @@ def test_invalid_jwe() -> None: "kty": "oct", "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "use": "enc", - "alg": "A256KW", "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg", } @@ -348,7 +347,6 @@ def symmetric_256_encryption_jwk() -> Jwk: assert jwk.kty == "oct" assert jwk.kid == "018c0ae5-4d9b-471b-bfd6-eef314bc7037" assert jwk.use == "enc" - assert jwk.alg == "A256KW" assert ( jwk.key.hex() == "849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188" @@ -402,7 +400,7 @@ def encryption_plaintext() -> bytes: ], ) def key_management_alg(request: pytest.FixtureRequest) -> str: - return request.param # type: ignore[attr-defined,no-any-return] + return request.param # type: ignore[no-any-return] @pytest.fixture( @@ -417,7 +415,7 @@ def key_management_alg(request: pytest.FixtureRequest) -> str: ], ) def encryption_alg(request: pytest.FixtureRequest) -> str: - alg: str = request.param # type: ignore[attr-defined] + alg: str = request.param if alg in SymmetricJwk.ENCRYPTION_ALGORITHMS: return alg pytest.skip(f"Encryption alg {alg} is not supported yet!") @@ -496,8 +494,8 @@ def encryption_jwk(decryption_jwk: Union[Jwk, bytes]) -> Union[Jwk, bytes]: @pytest.fixture(scope="module") def encrypted_jwe( - encryption_plaintext: bytes, - encryption_jwk: Union[Jwk, bytes], + encryption_plaintext: SupportsBytes, + encryption_jwk: Union[Jwk, SupportsBytes], key_management_alg: str, encryption_alg: str, ) -> JweCompact: @@ -508,20 +506,70 @@ def encrypted_jwe( alg=key_management_alg, enc=encryption_alg, ) - elif isinstance(encryption_jwk, bytes): + else: + password = bytes(encryption_jwk) jwe = JweCompact.encrypt_with_password( plaintext=encryption_plaintext, - password=encryption_jwk, + password=password, alg=key_management_alg, enc=encryption_alg, ) - else: - assert False, "Unsupported encryption key type" assert isinstance(jwe, JweCompact) assert jwe.enc == encryption_alg return jwe +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 + + +def test_supportsbytes( + encryption_plaintext: bytes, + encryption_jwk: Union[Jwk, SupportsBytes], + key_management_alg: str, + encryption_alg: str, + encrypted_jwe: JweCompact, + decryption_jwk: Jwk, +) -> None: + if isinstance(encryption_jwk, Jwk): + jwe = JweCompact.encrypt( + plaintext=SupportsBytesTester(encryption_plaintext), + jwk=encryption_jwk, + alg=key_management_alg, + enc=encryption_alg, + ) + else: + password = bytes(encryption_jwk) + jwe = JweCompact.encrypt_with_password( + plaintext=SupportsBytesTester(encryption_plaintext), + password=password, + alg=key_management_alg, + enc=encryption_alg, + ) + + assert jwe.decrypt(decryption_jwk) == encrypted_jwe.decrypt(decryption_jwk) + if not isinstance(decryption_jwk, bytes): + cek = decryption_jwk.recipient_key( + SupportsBytesTester(jwe.wrapped_cek), **jwe.headers + ) + assert ( + cek.decrypt( + SupportsBytesTester(jwe.ciphertext), + iv=SupportsBytesTester(jwe.initialization_vector), + tag=SupportsBytesTester(jwe.authentication_tag), + aad=SupportsBytesTester(jwe.additional_authenticated_data), + alg=encryption_alg, + ) + == encryption_plaintext + ) + + def test_decrypt( encryption_plaintext: bytes, encrypted_jwe: JweCompact, @@ -685,7 +733,7 @@ def test_invalid_password_encryption() -> None: "password", alg="PBES2-HS256+A128KW", enc="A128GCM", - count=50, + count=5000, cek=b"foo" * 8, ) diff --git a/tests/test_jwk/test_alg.py b/tests/test_jwk/test_alg.py new file mode 100644 index 0000000..9568678 --- /dev/null +++ b/tests/test_jwk/test_alg.py @@ -0,0 +1,57 @@ +import pytest + +from jwskate import ExpectedAlgRequired, RSAJwk, UnsupportedAlg +from jwskate.jwk.alg import select_alg, select_algs + + +def test_select_alg() -> None: + assert ( + select_alg( + jwk_alg=None, alg="RS256", supported_algs=RSAJwk.SIGNATURE_ALGORITHMS + ) + == RSAJwk.SIGNATURE_ALGORITHMS["RS256"] + ) + + with pytest.warns(): + assert ( + select_alg( + jwk_alg="RS256", alg="RS512", supported_algs=RSAJwk.SIGNATURE_ALGORITHMS + ) + == RSAJwk.SIGNATURE_ALGORITHMS["RS512"] + ) + + with pytest.raises(ExpectedAlgRequired): + select_alg(jwk_alg=None, alg=None, supported_algs=RSAJwk.SIGNATURE_ALGORITHMS) + + with pytest.raises(UnsupportedAlg): + select_alg( + jwk_alg=None, alg="HS256", supported_algs=RSAJwk.KEY_MANAGEMENT_ALGORITHMS + ) + + with pytest.raises(ValueError): + select_alg(jwk_alg=None, alg="HS256", supported_algs={}) + + +def test_select_algs() -> None: + assert select_algs( + jwk_alg=None, alg="RS256", algs=None, supported_algs=RSAJwk.SIGNATURE_ALGORITHMS + ) == [RSAJwk.SIGNATURE_ALGORITHMS["RS256"]] + + with pytest.warns(): + assert select_algs( + jwk_alg="RS256", + alg="RS512", + algs=None, + supported_algs=RSAJwk.SIGNATURE_ALGORITHMS, + ) == [RSAJwk.SIGNATURE_ALGORITHMS["RS512"]] + + with pytest.raises(ValueError): + select_algs( + jwk_alg=None, + alg="RS256", + algs=["RS256", "RS512"], + supported_algs=RSAJwk.SIGNATURE_ALGORITHMS, + ) + + with pytest.raises(ValueError): + select_algs(jwk_alg=None, alg="HS256", algs=None, supported_algs={}) diff --git a/tests/test_jwk/test_ec.py b/tests/test_jwk/test_ec.py index 5b53803..5e902f5 100644 --- a/tests/test_jwk/test_ec.py +++ b/tests/test_jwk/test_ec.py @@ -1,10 +1,11 @@ import pytest -from jwskate import Aes128CbcHmacSha256, EcdhEs, ECJwk, Jwk +from jwskate import A128CBC_HS256, EcdhEs, ECJwk, Jwk def test_jwk_ec_generate() -> None: - jwk = ECJwk.generate(kid="myeckey") + with pytest.warns(): + jwk = ECJwk.generate(kid="myeckey") assert jwk.kty == "EC" assert jwk.kid == "myeckey" assert jwk.crv == "P-256" @@ -18,6 +19,7 @@ def test_jwk_ec_generate() -> None: assert public_jwk.crv == "P-256" assert "x" in public_jwk assert "y" in public_jwk + assert "d" not in public_jwk assert jwk.supported_encryption_algorithms() == [] @@ -26,6 +28,7 @@ def test_ecdh_es() -> None: alg = "ECDH-ES+A128KW" enc = "A128CBC-HS256" private_jwk = ECJwk.generate(alg=alg) + assert private_jwk.crv == "P-256" public_jwk = private_jwk.public_jwk() sender_cek, wrapped_cek, headers = public_jwk.sender_key(enc) assert sender_cek @@ -44,7 +47,7 @@ def test_ecdh_es_with_controlled_cek_and_epk() -> None: enc = "A128CBC-HS256" private_jwk = ECJwk.generate(alg=alg, crv="P-256") public_jwk = private_jwk.public_jwk() - cek = Aes128CbcHmacSha256.generate_key() + cek = A128CBC_HS256.generate_key() epk = Jwk(EcdhEs(public_jwk.cryptography_key).generate_ephemeral_key()) sender_cek, wrapped_cek, headers = public_jwk.sender_key(enc, cek=cek, epk=epk) assert sender_cek.cryptography_key == cek @@ -61,23 +64,24 @@ def test_ecdh_es_with_controlled_cek_and_epk() -> None: private_jwk.recipient_key(wrapped_cek, enc, epk=epk) -def test_pem_key() -> None: - private_jwk = ECJwk.generate() - private_pem = private_jwk.to_pem_key() +@pytest.mark.parametrize("crv", ["P-256", "P-384", "P-521"]) +def test_pem_key(crv: str) -> None: + private_jwk = ECJwk.generate(crv=crv) + private_pem = private_jwk.to_pem() assert Jwk.from_pem_key(private_pem) == private_jwk public_jwk = private_jwk.public_jwk() - public_pem = public_jwk.to_pem_key() + public_pem = public_jwk.to_pem() assert Jwk.from_pem_key(public_pem) == public_jwk # serialize private key with password password = b"th1s_i5_a_p4ssW0rd!" - private_pem = private_jwk.to_pem_key(password) + private_pem = private_jwk.to_pem(password) assert Jwk.from_pem_key(private_pem, password) == private_jwk # try to serialize the public key with password with pytest.raises(ValueError): - public_jwk.to_pem_key(password) + public_jwk.to_pem(password) with pytest.raises(ValueError): assert Jwk.from_pem_key(public_pem, password) == public_jwk diff --git a/tests/test_jwk/test_generate.py b/tests/test_jwk/test_generate.py new file mode 100644 index 0000000..371e0a7 --- /dev/null +++ b/tests/test_jwk/test_generate.py @@ -0,0 +1,45 @@ +import pytest + +from jwskate import EncryptionAlgs, Jwk, KeyManagementAlgs, RSAJwk, SignatureAlgs + + +@pytest.mark.parametrize( + "alg", SignatureAlgs.ALL + EncryptionAlgs.ALL + KeyManagementAlgs.ALL_KEY_BASED +) +def test_generate_for_alg(alg: str) -> None: + jwk = Jwk.generate_for_alg(alg).with_usage_parameters() + assert jwk.is_private + if alg in SignatureAlgs.ALL_SYMMETRIC: + assert jwk.kty == "oct" + assert jwk.use == "sig" + 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 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.is_symmetric + elif alg in KeyManagementAlgs.ALL_SYMMETRIC: + assert jwk.kty == "oct" + assert jwk.use == "enc" + 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 not jwk.is_symmetric + + jwk_mini = jwk.minimize() + assert "alg" not in jwk_mini + assert "use" not in jwk_mini + assert "key_ops" not in jwk_mini + if isinstance(jwk_mini, RSAJwk): + jwk_mini = jwk_mini.with_optional_private_parameters() + + assert jwk_mini.with_usage_parameters(alg) == jwk diff --git a/tests/test_jwk/test_jwk.py b/tests/test_jwk/test_jwk.py index 355f759..a62d6f9 100644 --- a/tests/test_jwk/test_jwk.py +++ b/tests/test_jwk/test_jwk.py @@ -133,12 +133,11 @@ def test_invalid_class_for_kty() -> None: "private_key_ops, public_key_ops", [ ("sign", "verify"), - ("decrypt", "encrypt"), ("unwrapKey", "wrapKey"), ], ) def test_key_ops(private_key_ops: str, public_key_ops: str) -> None: - private_jwk = Jwk.generate_for_kty("EC", key_ops=[private_key_ops]) + private_jwk = Jwk.generate_for_kty("RSA", 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 @@ -167,21 +166,21 @@ def test_thumbprint() -> None: == "urn:ietf:params:oauth:jwk-thumbprint:sha-256:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" ) - jwk_with_thumbprint_kid = jwk.include_kid_thumbprint(force=True) + jwk_with_thumbprint_kid = jwk.with_kid_thumbprint(force=True) assert jwk_with_thumbprint_kid.kid == "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" assert isinstance(jwk_with_thumbprint_kid, Jwk) assert jwk_with_thumbprint_kid is not jwk assert jwk_with_thumbprint_kid.n == jwk.n - jwk_with_initial_kid = jwk.include_kid_thumbprint(force=False) + jwk_with_initial_kid = jwk.with_kid_thumbprint(force=False) assert jwk_with_initial_kid.kid == "2011-04-29" assert isinstance(jwk_with_initial_kid, Jwk) - assert jwk_with_initial_kid is not jwk + assert jwk_with_initial_kid is jwk assert jwk_with_initial_kid.n == jwk.n def test_invalid_thumbprint_hash() -> None: - jwk = Jwk.generate_for_kty("EC") + jwk = Jwk.generate_for_kty("EC", crv="P-256") with pytest.raises(ValueError): jwk.thumbprint("foo") @@ -189,3 +188,9 @@ def test_invalid_thumbprint_hash() -> None: def test_generate_invalid_kty() -> None: with pytest.raises(UnsupportedKeyType): Jwk.generate_for_kty("foobar") + + +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" diff --git a/tests/test_jwk/test_jwks.py b/tests/test_jwk/test_jwks.py index 965772c..4c985f9 100644 --- a/tests/test_jwk/test_jwks.py +++ b/tests/test_jwk/test_jwks.py @@ -3,32 +3,68 @@ from jwskate import Jwk, JwkSet -def test_jwks() -> None: - keys = [Jwk.generate_for_kty("RSA"), Jwk.generate_for_kty("RSA")] +def test_jwkset() -> None: + keys = [ + { + "kty": "RSA", + "n": "mUdmf5vJ3svsPSQ8BCOQVfwQdP8AmAEW21sYYUC5eSKR-pdwnRDBuFrIEjon2ry8cU-uaMjAoEZikPXcCTErye2Sj8fWQ8Wyo8DoGacJlFOJvs_18-CmNBc7oL8gBlYax3-feZZnaVIiJjvxQwUw5GQA6JTFnO8n2pnKMOOd8Gf6YrG-r0T6NXdviw0-2IW4f2UMJApqlu37yF8sgRNGZwDljNOkUtPK76Uz5T513Va4ckOqsVfnt4WoAkAkCl3eVBwGw3TJIbp_DaLUq53go0pXBCNxCHRD9mst69ZuknBLqn0SwKbQ9zJH9QvoqrEZ2q7GzkFzw70F6qH5MDEx2-dxQz_QccFV0XBpq4pkfuWzS8qKVO4QjyC7A0vIJUzrRHE2_moOtWvKTDsa7gfvK6kpnAW0iKnNchzBV0fzXWIIxRJ3_cc8Ue-KPRU9Wxm3heBOx_Qh-bKv9s9fVY9X6rimyX-pIwf-jkgWG8_FgTBuGkKTRcLi-XnwsCFIVNOtolmakbQHlin_lgDQm9s0nHoDJbZgAtzQfkIorclBJBzr2t__xgaZCfpSCLdwZFQvGEh1mK4WbSMMt5-L3zKsNLCBfdMbn2fS9n2hylfRwU_NZCY8f2RHAdP-z402Vq1c9-m2Ew3_695OmV5HoinJQPagY9hI-_EW8nhNWf8l4FE", + "e": "AQAB", + "d": "EGYw-3SrybxII3eBu1Chw-1Sxm20_s5ZB3GM33TZEzKFa0nyGL_e9g4yay7RLkg0oivsBVZ7M3qsV4WSe-JIpNNX862mCTy44ufD_WCfeADjEyj1T9kodxjIPqfMMZlbRp5rLctPd5d7w1r08l53D2x6o2etZ9-3hB4hoY7syjiZs58AP4jR-0_yvW4Wm_xck7a4MI_zvP-ryVGTdaDeDq2sIZ_QLDNwOikS7xM6cYqyc7k1JUG6Lyqr4cfCg2BtJdMUzysqzMLDDq6t8cmTq-zLeAwhrw2fatknQd0AmgbNNampjLacU2JMDBXw1_hYQ4shBpa-n8HUxPh87HDK_Gl4v7BZJHlzRQ73vkjHExcssZ3gkqEi90T4TNov-9uBXOtbVQCmut1IK6TnzBpaHepIqDovNmuzpc2gD3HFI78wdgHgzzV117lFgmLLIfg-mR9lj-qsJn-mdtKauXCXuZX24dBjGpknyBABx850px5nVQ2X9dWMFLxQomLhlxJAfhHQUsFlnpbw4StMDTz96zx9k_rgeNMTuj8VVTZ6DpOwhItYCvk41uIEbQTuehBRhOk7n6R8GELSizzLd6Yjw7FCxwWYavbnpx1sM9s3oOmxhQmm3BpcSRcochhPr14CA0SkTl1nQY3Z4Q_rIBTNtkDngrRBagjJGgtfN95cSIU", + "kid": "uzPODAa44gSUXvSI6zilPwxMJCULUHm0FmXoqXRfYdI", + }, + { + "kty": "RSA", + "n": "ycSaZ7HWpKCY4vfmoLWCxsg2KvpD-3dfFlz-2YsrfnMwpJF0nuRrceAxhFF34NlzwTn-JgVc8-lUrOJmcrBcxQNtr7eaJ0QhcRxu_Wre72QhqTddJgNt4V5Zn9P8l8CplSqiLruVI4nKGoiPFQ7mftGY39wxYiiFB6BMb04lAshvCuUOIP8WFFvCb5Afqpugl-NKpO2-C5vWeN_4xNkSO0fEq49Do01oUkMM5t1D4kvE6_BDVA2jxPHSSulXwcnwHmGuhXEfpVh-ME9MW0S0g7v-uNfpJmjVl7VDV87au6C9GKXHQ3NEXt8DWM8HXqRyRQ0XidJLymDOdUnF1_DMtS_wOCDrPFO-4rG_ZH3dwm-fBRSl_chu324OBi_ER6XIQpjkFiQo2pqbD8bI4X6kH8kUASq5gVsBU_hfQUZljSJoR2gQtorNw5EK4rlAQ5jS-ww16WiSna6jzgT_M4ZLr9IPCloe7isVxSk35-gD87xXtl8PzmE1vdpFC9aArkdFL_PK7MOqDvYIb6MRIOBvoxOV524tFKZhNrZUowTLH3a_-sWFLmdvd14iGxuv_DaC-hCmgI0eq7rzE2wUNP5IVmZASoI-3rKb22QdDlAPHNzdsyJCX4JVIPAJrpLOwptObN30404_C4UUD0IN9OXHD-16NaX5SY1thge94shJpTE", + "e": "AQAB", + "d": "1I1426RNKkDEztW477RHgIGKDtx2oYKveS-eii5CM4PFypgw8vJO_jff8jSgxQ5PE0-0nPkpYwp7WWVn54pDMIjcFDCnBJaRZEc_5VegYzBpBYp9Zn5WUwTCBc2cW4FrJOk00WZrRnTxo-IYWWbJCvBiy_F7VJy7B72mx9rawoPD9wY2TCxtZiUEP-LkeSZZl6iqCfUqL7CLz-qidzz2J90DInhaGL6DF6XrAYo26T5IxQTm6LU1wVO-5YvMFypU-qyM3aa-X8FJrjrbhYprYBu7y54oz33BBYC-4NHZO6-phT2fHT9g11C4heYTLXCvsG6KTXZswVYaKRT-hu31t0mgD8myErBQYcbityrKzdqKNdE9pBiaGMUngUMuaInLRbr2ihXeLSTzgQv6LrEOBxyDeyW43kPTzHtkFOoArOT_xpTjYobIYUTnFOer2rFpetG-B-yRMGSq5hMQ9067cBLfBoOAvJc9MrFTzM6ynPuTh2ZPRV7jZAR0cymtYb2CK_-6eKju2-bqQ0awjb9VkZolYgDccDZWJiM5TuiOBb-HRIdJSkI8KPGlWC-p14Eo3xeMFNjVJo_-lrT91IIaQC-WDSiRva3HZZGVjQPUiABji62wkC9QPD8VwLou044fnBqkiY7whAbDIGRQHpPiN5Co0_ZUEJdKFdVnncS74Q", + "kid": "IYIB72QYGIUGP5lYlGmnrBeVOFOxTk9SO_5ajWBu1QE", + }, + ] jwks = JwkSet(keys=keys) assert jwks.jwks == keys - jwk = Jwk.generate_for_kty("EC") + jwk = Jwk.generate_for_kty("EC", alg="ES256", kid="my_ec_key") keys.append(jwk) kid = jwks.add_jwk(jwk) assert kid == jwk.kid assert jwks.jwks == keys data = b"this is a test" - signature = jwk.sign(data, "ES256") + signature = jwk.sign(data) - assert jwks.verify(data, signature, "ES256") + assert jwks.verify(data, signature, kid="my_ec_key") + assert jwks.verify(data, signature, alg="ES256") + assert jwks.verify(data, signature, algs=("ES256",)) + assert jwks.verify(data, signature) jwks.remove_jwk(jwk.kid) - jwks.remove_jwk("foo") - assert jwks.is_private assert not jwks.verify(data, signature, "ES256") + assert not jwks.verify(data, signature, "ES256") + + assert jwks.public_jwks() == { + "keys": [ + { + "kty": "RSA", + "n": "mUdmf5vJ3svsPSQ8BCOQVfwQdP8AmAEW21sYYUC5eSKR-pdwnRDBuFrIEjon2ry8cU-uaMjAoEZikPXcCTErye2Sj8fWQ8Wyo8DoGacJlFOJvs_18-CmNBc7oL8gBlYax3-feZZnaVIiJjvxQwUw5GQA6JTFnO8n2pnKMOOd8Gf6YrG-r0T6NXdviw0-2IW4f2UMJApqlu37yF8sgRNGZwDljNOkUtPK76Uz5T513Va4ckOqsVfnt4WoAkAkCl3eVBwGw3TJIbp_DaLUq53go0pXBCNxCHRD9mst69ZuknBLqn0SwKbQ9zJH9QvoqrEZ2q7GzkFzw70F6qH5MDEx2-dxQz_QccFV0XBpq4pkfuWzS8qKVO4QjyC7A0vIJUzrRHE2_moOtWvKTDsa7gfvK6kpnAW0iKnNchzBV0fzXWIIxRJ3_cc8Ue-KPRU9Wxm3heBOx_Qh-bKv9s9fVY9X6rimyX-pIwf-jkgWG8_FgTBuGkKTRcLi-XnwsCFIVNOtolmakbQHlin_lgDQm9s0nHoDJbZgAtzQfkIorclBJBzr2t__xgaZCfpSCLdwZFQvGEh1mK4WbSMMt5-L3zKsNLCBfdMbn2fS9n2hylfRwU_NZCY8f2RHAdP-z402Vq1c9-m2Ew3_695OmV5HoinJQPagY9hI-_EW8nhNWf8l4FE", + "e": "AQAB", + "kid": "uzPODAa44gSUXvSI6zilPwxMJCULUHm0FmXoqXRfYdI", + }, + { + "kty": "RSA", + "n": "ycSaZ7HWpKCY4vfmoLWCxsg2KvpD-3dfFlz-2YsrfnMwpJF0nuRrceAxhFF34NlzwTn-JgVc8-lUrOJmcrBcxQNtr7eaJ0QhcRxu_Wre72QhqTddJgNt4V5Zn9P8l8CplSqiLruVI4nKGoiPFQ7mftGY39wxYiiFB6BMb04lAshvCuUOIP8WFFvCb5Afqpugl-NKpO2-C5vWeN_4xNkSO0fEq49Do01oUkMM5t1D4kvE6_BDVA2jxPHSSulXwcnwHmGuhXEfpVh-ME9MW0S0g7v-uNfpJmjVl7VDV87au6C9GKXHQ3NEXt8DWM8HXqRyRQ0XidJLymDOdUnF1_DMtS_wOCDrPFO-4rG_ZH3dwm-fBRSl_chu324OBi_ER6XIQpjkFiQo2pqbD8bI4X6kH8kUASq5gVsBU_hfQUZljSJoR2gQtorNw5EK4rlAQ5jS-ww16WiSna6jzgT_M4ZLr9IPCloe7isVxSk35-gD87xXtl8PzmE1vdpFC9aArkdFL_PK7MOqDvYIb6MRIOBvoxOV524tFKZhNrZUowTLH3a_-sWFLmdvd14iGxuv_DaC-hCmgI0eq7rzE2wUNP5IVmZASoI-3rKb22QdDlAPHNzdsyJCX4JVIPAJrpLOwptObN30404_C4UUD0IN9OXHD-16NaX5SY1thge94shJpTE", + "e": "AQAB", + "kid": "IYIB72QYGIUGP5lYlGmnrBeVOFOxTk9SO_5ajWBu1QE", + }, + ] + } + + jwks.remove_jwk("foo") # this is a no op since there is not key 'foo' -def test_empty_jwks() -> None: +def test_empty_jwkset() -> None: jwks = JwkSet() assert len(jwks) == 0 @@ -41,3 +77,69 @@ def test_empty_jwks() -> None: with pytest.raises(KeyError): jwks.get_jwk_by_kid("foo") + + +def test_public_jwkset() -> None: + jwks = JwkSet( + { + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "n": "iA7fKKnBz724Yqhe6ejEckSpqPCW0O1_3hUcW9GFC8OVgwWIG6Z6gwjLJFJPHQ7D-JT_Bc7UJ_3iBpUmEO_600SQu9jg8fVcf-OlDRvnMRuXMKYyyjWn50mfMZH9eHTBuw4h96rdIVm9N8ml0VsouJc59O7PjLi93HvzpV1PQM0m6it7oHfVPX_Gdm6cg6qWcc6yQ1jdW-YzkOp_nRCy81cVAvp_tKapaiXGIrpWipgBDObXSDeQ5qbArvL0P8N176g4Hia1WtpJoe7H1b_Km2e-gkl8UZVGN5-vSKryh1CKifD6uwLEvoHlHUvWdIqsSx7dPLchz07S81Qp2YpexulnfdA2VoZsH9AKrRtkf1_a3OSx0wFDxfOoRyTQblC1MZ8Dvf6PQ_stsc0-zBOjHa4jdunjneMUOuJmw3jaUl7MFAjcBS951mSqWoNUSOL8QgDEj3-jDghFGZmHZkjXfoflAmjbCUH6mRSTKgu9LzeKEWeKc9lSjwDRo9BNzq0X2qEzEqVexd96wJ7FkZ_zjyDWJlIElMy81fDcaX2Lh0AS0VOPDlAm6D5Py931V6jylI-Uz-3rQHuWiWUjZWp9ZB1OlYDC-nNFdJPqPxDQIgrODAvYMphK_R1NObdXjbwhr8qxZNRNXmqGB4FH-v6CGeNLOTB5FgmfndzX3utjk40", + "e": "AQAB", + "kid": "7KJgpwNvHJp_zb6SybahlC7506kvAm2cvMG_EY6jmx8", + }, + { + "kty": "EC", + "alg": "ES256", + "crv": "P-256", + "x": "Y3oM13zJ47UkSxuYRFP86cknI-JjMc9Nz39SA6R5EjQ", + "y": "pvA66-cfoNxK4TMTo3Wq-o7npJe5yRow9FPdP8N1s5Y", + "kid": "ojHE_6b7DXtOwLKYTmeao38CV_7P9F9rYTLGm8BuJnk", + }, + { + "kty": "RSA", + "alg": "PS256", + "n": "rhvS1HWLDPKP2_I4a1_RhLwnRjVDT_0tGDScPO67fBH7ImzkK_BrDnBzY2Fsz9VozWrCh0G_SnAKimmIGcfLI-AL2lFX2413y28jfHf5uGGKSXnaYu1lUtX51MlvbbQnhEtsVkJUcBEFZgzl4EztZ1YeXGuY0gbeqBUOmudWA5yBHvB_wkMg4vNX4H6Aa7jRdfVA1xUM43BHl6zIXpjsxAlfmjCd7Ifh9gOxj5skDd1rYLBcQsaF2Qmh_KWYrWagQH_WN0JDF9vSBRK5nNSfyHSAv72WhL4p_2Jz1fkYUsIOcoaoP3JTjZYYH4ht2QpqjfYXB50rJlUwX-DQ7-SyclXJwf571gspv9aJ4ahnux1g-26ByXyasBGzrJfhbOUGN2QC7O1OWk903vV6VtqYZjxLuW5pi7GFTB1psROtgBCdX-2sXjAr_Up4DFwNWc4AwQqfuuXAIjc9NS7x66ar1Dsj32YsiowfwB-raLZYm4H8A1AuN7zOX6A0JosVENuJT9e7Fl3wermpIWx4QRN76WOLYba_8uyTP0-R2kmgoxI2Xzt1RqtRXnwqD6_dqnMRdyx2zSnBFU3y5ICtWqOCjM9bm2Orym3ZfPpBbpZkkMLXYik7oae0kpA6yJf3FMQSTY_-66sPWgeT9jgpwC_qFxDrBEy9hOCO5VJ6NUkmdnk", + "e": "AQAB", + "kid": "m7XoZRBgXXjEFxGhWvb_urskl4rCLmOhhPRdC6278-E", + }, + { + "kty": "EC", + "alg": "ECDH-ES", + "crv": "P-256", + "x": "m98Qjjc1OlN1dVD0q7yetQfOVl0iHtcqHZpJ0ZeOkZQ", + "y": "g3PoI3YykxNj4H4Ffc8NF8Sf4MYXIkZzMN2wFBfD0fc", + "kid": "xAgzqjWdBD8cRifXbpmcv-9vIgjKHTdjelI-Vvu0K9Q", + }, + { + "kty": "RSA", + "alg": "RSA-OAEP-256", + "n": "tGeChePEEOjo3j9SL15OjqL67w_SBaN4H5LxhXFMEcnIoAVpuGGwu18NuN2oRPabsuvJ3yDg0v4WZck5keftfs5cgal8P9J_MB8greSErmLRTDRmSqFlysEJaGuFABbbUXZZk1bO_Ea-dSKJgeNUEpJf4n_JiTtxEFgB8fTeh1RWsESOqB7tYaQNaSy4Ckt_0TF3000BL92SsvepFIyTKoL77ZnRxbAd0WQ-H7flIKbuyex_5JuTZ4amI2xJE-TThEU_KN-yVbLDWaIhUAEE-51bC2DtceyuWSBO4QmToLG9oefaF49VdxaKMWeUnrfJ9pfM2AM12S8G6k_fQPpyFflXrBlRvWEC769RECucBRDzkgBnLGQPeUKwsKfvjiQC-Eat_WFE5t3D7OiZISDEBjrW728PGMEKcHzQq1ut5Eu7BOpC95emJgattURmGSSI5988_6vebsD37iRdBlqQrcYAq7SUI9-aaPL0CEDCWZ_vC0Rnxx0BRHM32JwVb-Ac0gJcTo6WaL1NKzp1CdixXBXdVBFEyB1pGDfi9-bAcM1YMTLylmmkxUagSHVvQnPqbO2djwI2koFH305Oa5ABAlgenpNb8BSGnRC0h5yzaKn0D8e_JgNv--JIhGTOeMmIG69CMQwONdzEuhCy4wGBChhjEQaiI_pTKhah0U5hIHM", + "e": "AQAB", + "kid": "zjY2pjFnBc4rOHWEwfS5Cjyxsjo2aprsctM-4oS1r8I", + }, + ] + } + ) + assert not jwks.is_private + sig_keys = jwks.verification_keys() + enc_keys = jwks.encryption_keys() + + sig_kids = set(jwk.kid for jwk in sig_keys) + assert sig_kids == set( + ( + "7KJgpwNvHJp_zb6SybahlC7506kvAm2cvMG_EY6jmx8", + "ojHE_6b7DXtOwLKYTmeao38CV_7P9F9rYTLGm8BuJnk", + "m7XoZRBgXXjEFxGhWvb_urskl4rCLmOhhPRdC6278-E", + ) + ) + + enc_kids = set(jwk.kid for jwk in enc_keys) + assert enc_kids == set( + ( + "xAgzqjWdBD8cRifXbpmcv-9vIgjKHTdjelI-Vvu0K9Q", + "zjY2pjFnBc4rOHWEwfS5Cjyxsjo2aprsctM-4oS1r8I", + ) + ) diff --git a/tests/test_jwk/test_okp.py b/tests/test_jwk/test_okp.py index 3dcd057..9073339 100644 --- a/tests/test_jwk/test_okp.py +++ b/tests/test_jwk/test_okp.py @@ -3,13 +3,14 @@ import pytest from cryptography.hazmat.primitives.asymmetric import ed448, ed25519, x448, x25519 -from jwskate import Jwk, JwsCompact, OKPJwk, UnsupportedOKPCurve +from jwskate import Jwk, JwsCompact, OKPJwk, UnsupportedAlg, UnsupportedOKPCurve -@pytest.mark.parametrize("curve", ["Ed25519", "Ed448", "X25519", "X448"]) -def test_jwk_okp_generate(curve: str) -> None: - jwk = OKPJwk.generate(crv=curve, kid="myokpkey") +@pytest.mark.parametrize("crv", ["Ed25519", "Ed448", "X25519", "X448"]) +def test_jwk_okp_generate_with_crv(crv: str) -> None: + jwk = OKPJwk.generate(crv=crv, kid="myokpkey") assert jwk.kty == "OKP" + assert jwk.crv == crv assert jwk.kid == "myokpkey" assert "x" in jwk assert "d" in jwk @@ -17,6 +18,30 @@ def test_jwk_okp_generate(curve: str) -> None: assert jwk.supported_encryption_algorithms() == [] +@pytest.mark.parametrize( + "alg", ["ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"] +) +def test_jwk_okp_generate_with_alg(alg: str) -> None: + jwk = OKPJwk.generate(alg=alg, kid="myokpkey") + assert jwk.kty == "OKP" + assert jwk.crv == "X25519" + assert jwk.kid == "myokpkey" + assert "x" in jwk + assert "d" in jwk + + assert jwk.supported_encryption_algorithms() == [] + + +def test_generate_no_crv_no_alg() -> None: + with pytest.raises(ValueError): + OKPJwk.generate() + + +def test_generate_unsuppored_alg() -> None: + with pytest.raises(UnsupportedAlg): + OKPJwk.generate(alg="foo") + + def test_okp_ed25519_sign() -> None: jwk = Jwk( { @@ -73,3 +98,31 @@ def test_from_to_cryptography( assert not public_jwk.is_private cryptography_public_key = public_jwk.cryptography_key assert isinstance(cryptography_public_key, public_key_class) + + +@pytest.mark.parametrize("crv", ["Ed25519", "Ed448", "X25519", "X448"]) +def test_pem_key(crv: str) -> None: + private_jwk = OKPJwk.generate(crv=crv) + private_pem = private_jwk.to_pem() + assert Jwk.from_pem_key(private_pem) == private_jwk + + public_jwk = private_jwk.public_jwk() + public_pem = public_jwk.to_pem() + assert Jwk.from_pem_key(public_pem) == public_jwk + + # serialize private key with password + password = b"th1s_i5_a_p4ssW0rd!" + private_pem = private_jwk.to_pem(password) + assert Jwk.from_pem_key(private_pem, password) == private_jwk + + # try to serialize the public key with password + with pytest.raises(ValueError): + public_jwk.to_pem(password) + + with pytest.raises(ValueError): + assert Jwk.from_pem_key(public_pem, password) == public_jwk + + +def test_from_cryptography_key_unknown_type() -> None: + with pytest.raises(TypeError): + OKPJwk.from_cryptography_key("this is not a cryptography key") diff --git a/tests/test_jwk/test_rsa.py b/tests/test_jwk/test_rsa.py index 85b05e4..7b3bae5 100644 --- a/tests/test_jwk/test_rsa.py +++ b/tests/test_jwk/test_rsa.py @@ -32,6 +32,8 @@ def rsa_private_jwk() -> Jwk: assert jwk.thumbprint() == "Qfq9DOLKNRyptzTJBhCFlzccbA0ac7Ag9GVFL11GAfM" + assert jwk.key_size == 2048 + assert jwk.supported_encryption_algorithms() == [] return jwk @@ -88,7 +90,7 @@ def test_public_jwk(rsa_private_jwk: Jwk) -> None: def test_generate() -> None: - jwk = RSAJwk.generate(kid="myrsakey") + jwk = RSAJwk.generate(kid="myrsakey", key_size=3096) assert jwk.kty == "RSA" assert jwk.kid == "myrsakey" assert "n" in jwk @@ -98,6 +100,7 @@ def test_generate() -> None: assert "dp" in jwk assert "dq" in jwk assert "qi" in jwk + assert jwk.key_size == 3096 public_jwk = jwk.public_jwk() assert public_jwk.kty == "RSA" @@ -123,3 +126,100 @@ def test_thumbprint(rsa_private_jwk: Jwk) -> None: rsa_private_jwk.thumbprint_uri() == "urn:ietf:params:oauth:jwk-thumbprint:sha-256:Qfq9DOLKNRyptzTJBhCFlzccbA0ac7Ag9GVFL11GAfM" ) + + +@pytest.mark.parametrize("key_size", (1024, 2048, 4096, 1678)) +def test_pem_key(key_size: int) -> None: + private_jwk = RSAJwk.generate(key_size=key_size) + private_pem = private_jwk.to_pem() + assert Jwk.from_pem_key(private_pem) == private_jwk + + public_jwk = private_jwk.public_jwk() + public_pem = public_jwk.to_pem() + assert Jwk.from_pem_key(public_pem) == public_jwk + + # serialize private key with password + password = b"th1s_i5_a_p4ssW0rd!" + private_pem = private_jwk.to_pem(password) + assert Jwk.from_pem_key(private_pem, password) == private_jwk + + # try to serialize the public key with password + with pytest.raises(ValueError): + public_jwk.to_pem(password) + + with pytest.raises(ValueError): + assert Jwk.from_pem_key(public_pem, password) == public_jwk + + +def test_optional_parameters() -> None: + jwk = RSAJwk( + { + "kty": "RSA", + "n": "vrTLXnpOv8Fe5stFYhmYrFKYUBcHpZU6GdtbXYRNPjBTAl2FMWE_chq5OMaM2QHBaAVLy62_xDV4AoUHydAlUoPtCtrxb9ViQnBpDytfXuhVEvAl0-K3zkWNVlOuLxDjp85cImbcPzmwrFADqAREPkCQh31V7tnlttlXlEYqDC_Cra8OnnPFwxRqcpcIWQmj2zy95TdJ1TQLv2HOYAbb1Ql1HhPhYJBFHcX4fhTVM0g-7JKOWRN7CBVudW3s5jqxgzykfkTopLDS0frP2ivz8p1vgHrXQKJr0M-dnj7FZzYiam8zBoTzOFRQ3-_QgWdu9Z9BCvJfpXhepZWu4Ryjiw", + "e": "AQAB", + "d": "AxJHWjivDwCOxjnM3sUZw-C6qkOMsHqESolRYeKxGcjOdXHLJN3zlyNeC0-LUi1oj4PSUi_0sDTKP4Qj-XicOUV9qliXXd06bWaBEqj4qr8kK59phI2Ytz5AhfzoB8MGX5v_uOAeOPh1Y3kQbgLPlI8WpM_8c9HXlMfQVMeCgtq08Vv15-eC6xeLqkNajQ8eEz3ZTt8eVuY5ElwiVAx8dl833_AV5E7s27mCoFWsd73zMk3ej1-eq0y4lwL7nHPPrM6JEdCrhMQgyR8BKmFZT14Ozm7W7p0W6llKY6SWV8VUEpnDbZrbm2Bpq_fvEptICE-byzIMVEN53KF9Mwo09Q", + } + ) + assert "qi" not in jwk + assert "p" not in jwk + assert "q" not in jwk + assert "dp" not in jwk + assert "dq" not in jwk + + assert ( + jwk.first_prime_factor + == BinaPy( + "1W78w6KeVoikPeFMH1E7ot6QzOmZEIv8DxYzJ440XIcY_6cvko34igTWS7x-XdapedbjeER1EBkR0_E_dUXos8HmRCTvO33SZ9R-w1HSm9VSx5JqHdBSbpDJtM2mbSKRaW7p_-KxJF2cvvnTyN8cawgDaPiEl8YpgGsize7MHvU" + ) + .decode_from("b64u") + .to_int() + ) + assert ( + jwk.second_prime_factor + == BinaPy( + "5L1yUc691HaYvK6iohvlAzQ7RRcp_xKC0TntmwMaSUCtVkKiYOrkFxrJgtWlTx7p-M4ecdKaxO9njnRHDiMwXmDgiEhri5NellgfsXy0IQFugnW7BQBBEOkk5Y6CRvYE7WNd5sWnISO3b9nV7RDtHna6_CL1t8oDC1COU6kXKH8" + ) + .decode_from("b64u") + .to_int() + ) + assert ( + jwk.first_factor_crt_exponent + == BinaPy( + "oCaf08x6M0RkuWoMzJMPxK5syNWf3SKtCEUILW4vLB7TS0IQGFAfZrEqe7n8uD0S_jGYje4QSPwGvJoRm9XRPtEID6oHOQS4lOCGHdmPxw7TBp1-stBWilBqihimAM4nfo2TWEap1TfJHiQoHloL4OQqauHP3HL9QTci7pN45uE" + ) + .decode_from("b64u") + .to_int() + ) + assert ( + jwk.second_factor_crt_exponent + == BinaPy( + "BMK8ol8_LDDbtPGdiOoztgFcSm_U--4SsvAVteg2n9esw-LXJlU9Mg3oq8RukFsAW6FOmOfdOMQSz7Az2mN5Gj3B7pQzSNBkY5Sp9DO4PAefmS-CGPSMZiG0FuMEax2rtJUg2zC57cKkirtp7GkxxjSKZ70CiDS4I4AltjAKv1k" + ) + .decode_from("b64u") + .to_int() + ) + assert ( + jwk.first_crt_coefficient + == BinaPy( + "1W3UxRlpxu2H4rcalHlQN0i5pq4Cei55CSjXkvewithAi_kmkcEaqzD07YKMdfjS9oKCKozzSklS_9XoeD-orPlszZ1dHwKbH8xn2_0QExazgvptSBF-br3xHoj9jbQ-4_DD1RQS1tXwA2nex5VAlvFGC-uHQhGRTnsmU3NNUcs" + ) + .decode_from("b64u") + .to_int() + ) + + jwk_with_opp = jwk.with_optional_private_parameters() + assert jwk_with_opp == { + "kty": "RSA", + "n": "vrTLXnpOv8Fe5stFYhmYrFKYUBcHpZU6GdtbXYRNPjBTAl2FMWE_chq5OMaM2QHBaAVLy62_xDV4AoUHydAlUoPtCtrxb9ViQnBpDytfXuhVEvAl0-K3zkWNVlOuLxDjp85cImbcPzmwrFADqAREPkCQh31V7tnlttlXlEYqDC_Cra8OnnPFwxRqcpcIWQmj2zy95TdJ1TQLv2HOYAbb1Ql1HhPhYJBFHcX4fhTVM0g-7JKOWRN7CBVudW3s5jqxgzykfkTopLDS0frP2ivz8p1vgHrXQKJr0M-dnj7FZzYiam8zBoTzOFRQ3-_QgWdu9Z9BCvJfpXhepZWu4Ryjiw", + "e": "AQAB", + "d": "AxJHWjivDwCOxjnM3sUZw-C6qkOMsHqESolRYeKxGcjOdXHLJN3zlyNeC0-LUi1oj4PSUi_0sDTKP4Qj-XicOUV9qliXXd06bWaBEqj4qr8kK59phI2Ytz5AhfzoB8MGX5v_uOAeOPh1Y3kQbgLPlI8WpM_8c9HXlMfQVMeCgtq08Vv15-eC6xeLqkNajQ8eEz3ZTt8eVuY5ElwiVAx8dl833_AV5E7s27mCoFWsd73zMk3ej1-eq0y4lwL7nHPPrM6JEdCrhMQgyR8BKmFZT14Ozm7W7p0W6llKY6SWV8VUEpnDbZrbm2Bpq_fvEptICE-byzIMVEN53KF9Mwo09Q", + "p": "1W78w6KeVoikPeFMH1E7ot6QzOmZEIv8DxYzJ440XIcY_6cvko34igTWS7x-XdapedbjeER1EBkR0_E_dUXos8HmRCTvO33SZ9R-w1HSm9VSx5JqHdBSbpDJtM2mbSKRaW7p_-KxJF2cvvnTyN8cawgDaPiEl8YpgGsize7MHvU", + "q": "5L1yUc691HaYvK6iohvlAzQ7RRcp_xKC0TntmwMaSUCtVkKiYOrkFxrJgtWlTx7p-M4ecdKaxO9njnRHDiMwXmDgiEhri5NellgfsXy0IQFugnW7BQBBEOkk5Y6CRvYE7WNd5sWnISO3b9nV7RDtHna6_CL1t8oDC1COU6kXKH8", + "dp": "oCaf08x6M0RkuWoMzJMPxK5syNWf3SKtCEUILW4vLB7TS0IQGFAfZrEqe7n8uD0S_jGYje4QSPwGvJoRm9XRPtEID6oHOQS4lOCGHdmPxw7TBp1-stBWilBqihimAM4nfo2TWEap1TfJHiQoHloL4OQqauHP3HL9QTci7pN45uE", + "dq": "BMK8ol8_LDDbtPGdiOoztgFcSm_U--4SsvAVteg2n9esw-LXJlU9Mg3oq8RukFsAW6FOmOfdOMQSz7Az2mN5Gj3B7pQzSNBkY5Sp9DO4PAefmS-CGPSMZiG0FuMEax2rtJUg2zC57cKkirtp7GkxxjSKZ70CiDS4I4AltjAKv1k", + "qi": "1W3UxRlpxu2H4rcalHlQN0i5pq4Cei55CSjXkvewithAi_kmkcEaqzD07YKMdfjS9oKCKozzSklS_9XoeD-orPlszZ1dHwKbH8xn2_0QExazgvptSBF-br3xHoj9jbQ-4_DD1RQS1tXwA2nex5VAlvFGC-uHQhGRTnsmU3NNUcs", + } + assert jwk_with_opp.without_optional_private_parameters() == jwk + + with pytest.raises(ValueError): + jwk.public_jwk().with_optional_private_parameters() diff --git a/tests/test_jwk/test_symmetric.py b/tests/test_jwk/test_symmetric.py index f11fa37..b54bffe 100644 --- a/tests/test_jwk/test_symmetric.py +++ b/tests/test_jwk/test_symmetric.py @@ -13,7 +13,7 @@ ids=("256bits", "384bits", "512bits"), ) def symmetric_jwk(request: pytest.FixtureRequest) -> SymmetricJwk: - alg, min_key_size = request.param # type: ignore + alg, min_key_size = request.param kid = f"my_{alg}_jwk" jwk = SymmetricJwk.generate_for_alg(alg, kid=kid) assert jwk.kty == "oct" @@ -36,3 +36,9 @@ def test_jwk_symmetric_sign(symmetric_jwk: SymmetricJwk) -> None: def test_dir_alg(symmetric_jwk: SymmetricJwk) -> None: assert "dir" in symmetric_jwk.supported_key_management_algorithms() + + +def test_pem_key() -> None: + private_jwk = SymmetricJwk.generate(key_size=128) + with pytest.raises(TypeError): + private_jwk.to_pem() diff --git a/tests/test_jws.py b/tests/test_jws.py index aad9685..5d90379 100644 --- a/tests/test_jws.py +++ b/tests/test_jws.py @@ -80,11 +80,17 @@ def test_jws_compact(private_jwk: Jwk) -> None: "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", } +OKP_ED448_PRIVATE_KEY = { + "kty": "OKP", + "crv": "Ed448", + "x": "Cg5TBDGx0VUzIsTBy7-1ipgpdbn1URt9Ahb4tKwzav788lold5nGfmuqMcdyBOBMnc-kVdtBew4A", + "d": "8AW_tfr1kQkyMqOjoGzM3yiLgu6zbN2Nlcpc50b4lwh4bVE1b1EwyJJJqJ4J3zhXLRmUB3REz1y0", +} + SYMMETRIC_SIGNATURE_KEY = { "kty": "oct", "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "use": "sig", - "alg": "HS256", "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg", } @@ -183,6 +189,24 @@ def okp_ed25519_signature_jwk() -> Jwk: return jwk +@pytest.fixture(scope="module") +def okp_ed448_signature_jwk() -> Jwk: + jwk = Jwk(OKP_ED448_PRIVATE_KEY) + assert isinstance(jwk, OKPJwk) + assert jwk.is_private + assert jwk.kty == "OKP" + assert ( + jwk.private_key.hex() + == "f005bfb5faf591093232a3a3a06cccdf288b82eeb36cdd8d95ca5ce746f89708786d51356f5130c89249a89e09df38572d1994077444cf5cb4" + ) + assert ( + jwk.public_key.hex() + == "0a0e530431b1d1553322c4c1cbbfb58a982975b9f5511b7d0216f8b4ac336afefcf25a257799c67e6baa31c77204e04c9dcfa455db417b0e00" + ) + assert jwk.thumbprint() == "tNxVYGfEeBGXEG7N8YAvNhBlZ1mSKVjc3tMP_t_3-t0" + return jwk + + @pytest.fixture(scope="session") def symmetric_signature_jwk() -> Jwk: """This is the key from [https://datatracker.ietf.org/doc/html/rfc7520#section-3.5].""" @@ -192,7 +216,6 @@ def symmetric_signature_jwk() -> Jwk: assert jwk.kty == "oct" assert jwk.kid == "018c0ae5-4d9b-471b-bfd6-eef314bc7037" assert jwk.use == "sig" - assert jwk.alg == "HS256" assert ( jwk.key.hex() == "849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188" @@ -229,7 +252,7 @@ def signature_payload() -> bytes: ] ) def signature_alg(request: pytest.FixtureRequest) -> str: - return request.param # type: ignore[attr-defined,no-any-return] + return request.param # type: ignore[no-any-return] @pytest.fixture() @@ -240,6 +263,7 @@ def signature_jwk( ec_p384_private_jwk: Jwk, ec_p521_private_jwk: Jwk, okp_ed25519_signature_jwk: Jwk, + okp_ed448_signature_jwk: Jwk, symmetric_signature_jwk: Jwk, ) -> Jwk: for key in ( @@ -248,6 +272,7 @@ def signature_jwk( ec_p384_private_jwk, ec_p256_private_jwk, okp_ed25519_signature_jwk, + okp_ed448_signature_jwk, symmetric_signature_jwk, ): if signature_alg in key.supported_signing_algorithms(): @@ -257,7 +282,7 @@ def signature_jwk( @pytest.fixture() -def validation_jwk(signature_jwk: Jwk) -> Jwk: +def verification_jwk(signature_jwk: Jwk) -> Jwk: if isinstance(signature_jwk, SymmetricJwk): return signature_jwk public_jwk = signature_jwk.public_jwk() @@ -277,6 +302,40 @@ def signed_jws_compact( return jws +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 + + +def test_supportsbytes( + signature_payload: bytes, + signature_jwk: Jwk, + signature_alg: str, + signed_jws_compact: JwsCompact, + verification_jwk: Jwk, +) -> None: + jws = JwsCompact.sign( + payload=SupportsBytesTester(signature_payload), + jwk=signature_jwk, + alg=signature_alg, + ) + if signature_alg not in ("ES256", "ES384", "ES512", "PS256", "PS384", "PS512"): + # those algs have non deterministic signatures + assert jws == signed_jws_compact + + assert jws.payload == signed_jws_compact.payload + assert verification_jwk.verify( + SupportsBytesTester(jws.signed_part), + SupportsBytesTester(jws.signature), + alg=signature_alg, + ) + + @pytest.fixture() def signed_jws_json_flat( signature_payload: bytes, signature_jwk: Jwk, signature_alg: str @@ -297,22 +356,46 @@ def signed_jws_json_general( return jws -def test_validate_signature( - signed_jws_compact: JwsCompact, validation_jwk: Jwk, signature_alg: str +def test_verify_signature( + signed_jws_compact: JwsCompact, verification_jwk: Jwk, signature_alg: str ) -> None: - assert signed_jws_compact.verify_signature(validation_jwk, alg=signature_alg) + assert signed_jws_compact.verify_signature(verification_jwk, alg=signature_alg) + altered_jws = bytes(signed_jws_compact)[:-4] + ( + b"aaaa" if not signed_jws_compact.value.endswith(b"aaaa") else b"bbbb" + ) + assert not JwsCompact(altered_jws).verify_signature( + verification_jwk, alg=signature_alg + ) -def test_validate_signature_json_flat( - signed_jws_json_flat: JwsJsonFlat, validation_jwk: Jwk, signature_alg: str +def test_verify_signature_json_flat( + signed_jws_json_flat: JwsJsonFlat, verification_jwk: Jwk, signature_alg: str ) -> None: - assert signed_jws_json_flat.verify_signature(validation_jwk, alg=signature_alg) + assert signed_jws_json_flat.verify_signature(verification_jwk, alg=signature_alg) + altered_jws = dict(signed_jws_json_flat) + altered_jws["signature"] = signed_jws_json_flat["signature"][:-4] + ( + "aaaa" if not signed_jws_json_flat["signature"].endswith("aaaa") else "bbbb" + ) + assert not JwsJsonFlat(altered_jws).verify_signature( + verification_jwk, alg=signature_alg + ) -def test_validate_signature_json_general( - signed_jws_json_general: JwsJsonGeneral, validation_jwk: Jwk, signature_alg: str +def test_verify_signature_json_general( + signed_jws_json_general: JwsJsonGeneral, verification_jwk: Jwk, signature_alg: str ) -> None: - assert signed_jws_json_general.verify_signature(validation_jwk, alg=signature_alg) + assert signed_jws_json_general.verify_signature(verification_jwk, alg=signature_alg) + altered_jws = dict(signed_jws_json_general) + altered_jws["signatures"][0]["signature"] = signed_jws_json_general["signatures"][ + 0 + ]["signature"][:-4] + ( + "aaaa" + if not signed_jws_json_general["signatures"][0]["signature"].endswith("aaaa") + else "bbbb" + ) + assert not JwsJsonGeneral(altered_jws).verify_signature( + verification_jwk, alg=signature_alg + ) def test_jws_format_transformation( @@ -337,20 +420,20 @@ def test_jws_format_transformation( assert signed_jws_json_flat.compact() == signed_jws_compact -def test_validate_signature_by_jwcrypto( - signed_jws_compact: JwsCompact, validation_jwk: Jwk, signature_alg: str +def test_verify_signature_by_jwcrypto( + signed_jws_compact: JwsCompact, verification_jwk: Jwk, signature_alg: str ) -> None: """This test verifies tokens generated by `jwskate` using another lib `jwcrypto`. Args: signed_jws_compact: the Jws signed by jwskate to verify - validation_jwk: the Jwk containing the verification key + verification_jwk: the Jwk containing the verification key signature_alg: the signature alg """ import jwcrypto.jwk # type: ignore[import] import jwcrypto.jws # type: ignore[import] - jwk = jwcrypto.jwk.JWK(**validation_jwk) + jwk = jwcrypto.jwk.JWK(**verification_jwk) jws = jwcrypto.jws.JWS() jws.deserialize(str(signed_jws_compact)) jws.verify(jwk) @@ -360,7 +443,7 @@ def test_validate_signature_by_jwcrypto( def jwcrypto_signed_jws( signature_payload: bytes, signature_jwk: Jwk, signature_alg: str ) -> str: - """Sign a JWS using `jwcrypto`, to make sure it validates with `jwskate`. + """Sign a JWS using `jwcrypto`, to make sure it verifies with `jwskate`. Args: signature_payload: the payload to sign @@ -385,16 +468,31 @@ def jwcrypto_signed_jws( return token -def test_validate_signature_from_jwcrypto( - jwcrypto_signed_jws: str, validation_jwk: Jwk, signature_alg: str +def test_verify_signature_from_jwcrypto( + jwcrypto_signed_jws: str, verification_jwk: Jwk, signature_alg: str ) -> None: - """Check that `jwskate`validates tokens signed by `jwcrypto`. + """Check that `jwskate` verifies tokens signed by `jwcrypto`. Args: - jwcrypto_signed_jws: the JWS to validate - validation_jwk: the public key to validate the signature + jwcrypto_signed_jws: the JWS to verify + verification_jwk: the public key to verify the signature signature_alg: the alg to use """ assert JwsCompact(jwcrypto_signed_jws).verify_signature( - validation_jwk, alg=signature_alg + verification_jwk, alg=signature_alg ) + + +def test_invalid_jws() -> None: + with pytest.raises(ValueError): + JwsCompact( + "ey.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cOUKU1ijv3KiN2KK_o50RU978I9MzQ4lNw2y7nOGAdM" + ) + with pytest.raises(ValueError): + JwsCompact( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.!!.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + ) + with pytest.raises(ValueError): + JwsCompact( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.!!" + ) diff --git a/tests/test_jwt.py b/tests/test_jwt.py index f9d224f..1bb4ae4 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -8,6 +8,7 @@ InvalidClaim, InvalidJwt, InvalidSignature, + JweCompact, Jwk, Jwt, JwtSigner, @@ -18,7 +19,7 @@ ) -def test_jwt() -> None: +def test_signed_jwt() -> None: jwt = Jwt( "eyJhbGciOiJSUzI1NiIsImtpZCI6Im15X2tleSJ9.eyJhY3IiOiIyIiwiYW1yIjpbInB3ZCIsIm90cCJdLCJhdWQiOiJjbGllbnRfaWQiLCJhdXRoX3RpbWUiOjE2MjkyMDQ1NjAsImV4cCI6MTYyOTIwNDYyMCwiaWF0IjoxNjI5MjA0NTYwLCJpc3MiOiJodHRwczovL215YXMubG9jYWwiLCJub25jZSI6Im5vbmNlIiwic3ViIjoiMTIzNDU2In0.wUfjMyjlOSdvbFGFP8O8wGcNBK7akeyOUBMvYcNZclFUtokOyxhLUPxmo1THo1DV1BHUVd6AWfeKUnyTxl_8-G3E_a9u5wJfDyfghPDhCmfkYARvqQnnV_3aIbfTfUBC4f0bHr08d_q0fED88RLu77wESIPCVqQYy2bk4FLucc63yGBvaCskqzthZ85DbBJYWLlR8qBUk_NA8bWATYEtjwTrxoZe-uA-vB6NwUv1h8DKRsDF-9HSVHeWXXAeoG9UW7zgxoY3KbDIVzemvGzs2R9OgDBRRafBBVeAkDV6CdbdMNJDmHzcjase5jX6LE-3YCy7c7AMM1uWRCnK3f-azA" ) @@ -107,7 +108,7 @@ def test_empty_jwt(private_jwk: Jwk) -> None: assert jwt.expires_at is None assert jwt.not_before is None assert jwt.issuer is None - assert jwt.audiences is None + assert jwt.audiences == [] assert jwt.subject is None assert jwt.jwt_token_id is None assert jwt.kid == private_jwk.kid @@ -175,3 +176,233 @@ def test_validate() -> None: SignedJwt( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIubG9jYWwiLCJhdWQiOiJodHRwczovL2F1ZGllbmNlLmxvY2FsIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjIsImNsYWltMSI6IkkgaGF2ZSBhIDEifQ.k4qhY14C0sJYTaUiAIc2kkybmaIxaUMkirIkln10SG4" ).validate(jwk, algs=SignatureAlgs.ALL_SYMMETRIC) + + +def test_encrypted_jwt() -> None: + jwt = Jwt( + """eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0. + QR1Owv2ug2WyPBnbQrRARTeEk9kDO2w8qDcjiHnSJflSdv1iNqhWXaKH4MqAkQtM + oNfABIPJaZm0HaA415sv3aeuBWnD8J-Ui7Ah6cWafs3ZwwFKDFUUsWHSK-IPKxLG + TkND09XyjORj_CHAgOPJ-Sd8ONQRnJvWn_hXV1BNMHzUjPyYwEsRhDhzjAD26ima + sOTsgruobpYGoQcXUwFDn7moXPRfDE8-NoQX7N7ZYMmpUDkR-Cx9obNGwJQ3nM52 + YCitxoQVPzjbl7WBuB7AohdBoZOdZ24WlN1lVIeh8v1K4krB8xgKvRU8kgFrEn_a + 1rZgN5TiysnmzTROF869lQ. + AxY8DCtDaGlsbGljb3RoZQ. + MKOle7UQrG6nSxTLX6Mqwt0orbHvAKeWnDYvpIAeZ72deHxz3roJDXQyhxx0wKaM + HDjUEOKIwrtkHthpqEanSBNYHZgmNOV7sln1Eu9g3J8. + fiK51VwhsxJ-siBMR-YFiA""" + ) + + assert isinstance(jwt, JweCompact) + assert jwt.headers == {"alg": "RSA1_5", "enc": "A128CBC-HS256"} + assert jwt.alg == "RSA1_5" + assert jwt.enc == "A128CBC-HS256" + assert ( + jwt.wrapped_cek.to("b64u") + == b"QR1Owv2ug2WyPBnbQrRARTeEk9kDO2w8qDcjiHnSJflSdv1iNqhWXaKH4MqAkQtMoNfABIPJaZm0HaA415sv3aeuBWnD8J-Ui7Ah6cWafs3ZwwFKDFUUsWHSK-IPKxLGTkND09XyjORj_CHAgOPJ-Sd8ONQRnJvWn_hXV1BNMHzUjPyYwEsRhDhzjAD26imasOTsgruobpYGoQcXUwFDn7moXPRfDE8-NoQX7N7ZYMmpUDkR-Cx9obNGwJQ3nM52YCitxoQVPzjbl7WBuB7AohdBoZOdZ24WlN1lVIeh8v1K4krB8xgKvRU8kgFrEn_a1rZgN5TiysnmzTROF869lQ" + ) + assert jwt.initialization_vector.to("b64u") == b"AxY8DCtDaGlsbGljb3RoZQ" + assert ( + jwt.ciphertext.to("b64u") + == b"MKOle7UQrG6nSxTLX6Mqwt0orbHvAKeWnDYvpIAeZ72deHxz3roJDXQyhxx0wKaMHDjUEOKIwrtkHthpqEanSBNYHZgmNOV7sln1Eu9g3J8" + ) + assert jwt.authentication_tag.to("b64u") == b"fiK51VwhsxJ-siBMR-YFiA" + + jwk = Jwk( + { + "kty": "RSA", + "n": "sXchDaQebHnPiGvyDOAT4saGEUetSyo9MKLOoWFsueri23bOdgWp4Dy1Wl" + "UzewbgBHod5pcM9H95GQRV3JDXboIRROSBigeC5yjU1hGzHHyXss8UDpre" + "cbAYxknTcQkhslANGRUZmdTOQ5qTRsLAt6BTYuyvVRdhS8exSZEy_c4gs_" + "7svlJJQ4H9_NxsiIoLwAEk7-Q3UXERGYw_75IDrGA84-lA_-Ct4eTlXHBI" + "Y2EaV7t7LjJaynVJCpkv4LKjTTAumiGUIuQhrNhZLuF_RJLqHpM2kgWFLU" + "7-VTdL1VbC2tejvcI2BlMkEpk1BzBZI0KQB0GaDWFLN-aEAw3vRw", + "e": "AQAB", + "d": "VFCWOqXr8nvZNyaaJLXdnNPXZKRaWCjkU5Q2egQQpTBMwhprMzWzpR8Sxq" + "1OPThh_J6MUD8Z35wky9b8eEO0pwNS8xlh1lOFRRBoNqDIKVOku0aZb-ry" + "nq8cxjDTLZQ6Fz7jSjR1Klop-YKaUHc9GsEofQqYruPhzSA-QgajZGPbE_" + "0ZaVDJHfyd7UUBUKunFMScbflYAAOYJqVIVwaYR5zWEEceUjNnTNo_CVSj" + "-VvXLO5VZfCUAVLgW4dpf1SrtZjSt34YLsRarSb127reG_DUwg9Ch-Kyvj" + "T1SkHgUWRVGcyly7uvVGRSDwsXypdrNinPA4jlhoNdizK2zF2CWQ", + "p": "9gY2w6I6S6L0juEKsbeDAwpd9WMfgqFoeA9vEyEUuk4kLwBKcoe1x4HG68" + "ik918hdDSE9vDQSccA3xXHOAFOPJ8R9EeIAbTi1VwBYnbTp87X-xcPWlEP" + "krdoUKW60tgs1aNd_Nnc9LEVVPMS390zbFxt8TN_biaBgelNgbC95sM", + "q": "uKlCKvKv_ZJMVcdIs5vVSU_6cPtYI1ljWytExV_skstvRSNi9r66jdd9-y" + "BhVfuG4shsp2j7rGnIio901RBeHo6TPKWVVykPu1iYhQXw1jIABfw-MVsN" + "-3bQ76WLdt2SDxsHs7q7zPyUyHXmps7ycZ5c72wGkUwNOjYelmkiNS0", + "dp": "w0kZbV63cVRvVX6yk3C8cMxo2qCM4Y8nsq1lmMSYhG4EcL6FWbX5h9yuv" + "ngs4iLEFk6eALoUS4vIWEwcL4txw9LsWH_zKI-hwoReoP77cOdSL4AVcra" + "Hawlkpyd2TWjE5evgbhWtOxnZee3cXJBkAi64Ik6jZxbvk-RR3pEhnCs", + "dq": "o_8V14SezckO6CNLKs_btPdFiO9_kC1DsuUTd2LAfIIVeMZ7jn1Gus_Ff" + "7B7IVx3p5KuBGOVF8L-qifLb6nQnLysgHDh132NDioZkhH7mI7hPG-PYE_" + "odApKdnqECHWw0J-F0JWnUd6D2B_1TvF9mXA2Qx-iGYn8OVV1Bsmp6qU", + "qi": "eNho5yRBEBxhGBtQRww9QirZsB66TrfFReG_CcteI1aCneT0ELGhYlRlC" + "tUkTRclIfuEPmNsNDPbLoLqqCVznFbvdB7x-Tl-m0l_eFTj2KiqwGqE9PZ" + "B9nNTwMVvH3VRRSLWACvPnSiwP8N5Usy-WRXS-V7TbpxIhvepTfE0NNo", + } + ) + + assert jwt.decrypt(jwk).parse_from("json") == { + "iss": "joe", + "exp": 1300819380, + "http://example.com/is_root": True, + } + + +def test_decrypt_nested_jwt() -> None: + jwt = """ + eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiSldUIn0. + g_hEwksO1Ax8Qn7HoN-BVeBoa8FXe0kpyk_XdcSmxvcM5_P296JXXtoHISr_DD_M + qewaQSH4dZOQHoUgKLeFly-9RI11TG-_Ge1bZFazBPwKC5lJ6OLANLMd0QSL4fYE + b9ERe-epKYE3xb2jfY1AltHqBO-PM6j23Guj2yDKnFv6WO72tteVzm_2n17SBFvh + DuR9a2nHTE67pe0XGBUS_TK7ecA-iVq5COeVdJR4U4VZGGlxRGPLRHvolVLEHx6D + YyLpw30Ay9R6d68YCLi9FYTq3hIXPK_-dmPlOUlKvPr1GgJzRoeC9G5qCvdcHWsq + JGTO_z3Wfo5zsqwkxruxwA. + UmVkbW9uZCBXQSA5ODA1Mg. + VwHERHPvCNcHHpTjkoigx3_ExK0Qc71RMEParpatm0X_qpg-w8kozSjfNIPPXiTB + BLXR65CIPkFqz4l1Ae9w_uowKiwyi9acgVztAi-pSL8GQSXnaamh9kX1mdh3M_TT + -FZGQFQsFhu0Z72gJKGdfGE-OE7hS1zuBD5oEUfk0Dmb0VzWEzpxxiSSBbBAzP10 + l56pPfAtrjEYw-7ygeMkwBl6Z_mLS6w6xUgKlvW6ULmkV-uLC4FUiyKECK4e3WZY + Kw1bpgIqGYsw2v_grHjszJZ-_I5uM-9RA8ycX9KqPRp9gc6pXmoU_-27ATs9XCvr + ZXUtK2902AUzqpeEUJYjWWxSNsS-r1TJ1I-FMJ4XyAiGrfmo9hQPcNBYxPz3GQb2 + 8Y5CLSQfNgKSGt0A4isp1hBUXBHAndgtcslt7ZoQJaKe_nNJgNliWtWpJ_ebuOpE + l8jdhehdccnRMIwAmU1n7SPkmhIl1HlSOpvcvDfhUN5wuqU955vOBvfkBOh5A11U + zBuo2WlgZ6hYi9-e3w29bR0C2-pp3jbqxEDw3iWaf2dc5b-LnR0FEYXvI_tYk5rd + _J9N0mg0tQ6RbpxNEMNoA9QWk5lgdPvbh9BaO195abQ. + AVO9iT5AV4CzvDJCdhSFlQ""" + + jwk = { + "kty": "RSA", + "n": "sXchDaQebHnPiGvyDOAT4saGEUetSyo9MKLOoWFsueri23bOdgWp4Dy1Wl" + "UzewbgBHod5pcM9H95GQRV3JDXboIRROSBigeC5yjU1hGzHHyXss8UDpre" + "cbAYxknTcQkhslANGRUZmdTOQ5qTRsLAt6BTYuyvVRdhS8exSZEy_c4gs_" + "7svlJJQ4H9_NxsiIoLwAEk7-Q3UXERGYw_75IDrGA84-lA_-Ct4eTlXHBI" + "Y2EaV7t7LjJaynVJCpkv4LKjTTAumiGUIuQhrNhZLuF_RJLqHpM2kgWFLU" + "7-VTdL1VbC2tejvcI2BlMkEpk1BzBZI0KQB0GaDWFLN-aEAw3vRw", + "e": "AQAB", + "d": "VFCWOqXr8nvZNyaaJLXdnNPXZKRaWCjkU5Q2egQQpTBMwhprMzWzpR8Sxq" + "1OPThh_J6MUD8Z35wky9b8eEO0pwNS8xlh1lOFRRBoNqDIKVOku0aZb-ry" + "nq8cxjDTLZQ6Fz7jSjR1Klop-YKaUHc9GsEofQqYruPhzSA-QgajZGPbE_" + "0ZaVDJHfyd7UUBUKunFMScbflYAAOYJqVIVwaYR5zWEEceUjNnTNo_CVSj" + "-VvXLO5VZfCUAVLgW4dpf1SrtZjSt34YLsRarSb127reG_DUwg9Ch-Kyvj" + "T1SkHgUWRVGcyly7uvVGRSDwsXypdrNinPA4jlhoNdizK2zF2CWQ", + "p": "9gY2w6I6S6L0juEKsbeDAwpd9WMfgqFoeA9vEyEUuk4kLwBKcoe1x4HG68" + "ik918hdDSE9vDQSccA3xXHOAFOPJ8R9EeIAbTi1VwBYnbTp87X-xcPWlEP" + "krdoUKW60tgs1aNd_Nnc9LEVVPMS390zbFxt8TN_biaBgelNgbC95sM", + "q": "uKlCKvKv_ZJMVcdIs5vVSU_6cPtYI1ljWytExV_skstvRSNi9r66jdd9-y" + "BhVfuG4shsp2j7rGnIio901RBeHo6TPKWVVykPu1iYhQXw1jIABfw-MVsN" + "-3bQ76WLdt2SDxsHs7q7zPyUyHXmps7ycZ5c72wGkUwNOjYelmkiNS0", + "dp": "w0kZbV63cVRvVX6yk3C8cMxo2qCM4Y8nsq1lmMSYhG4EcL6FWbX5h9yuv" + "ngs4iLEFk6eALoUS4vIWEwcL4txw9LsWH_zKI-hwoReoP77cOdSL4AVcra" + "Hawlkpyd2TWjE5evgbhWtOxnZee3cXJBkAi64Ik6jZxbvk-RR3pEhnCs", + "dq": "o_8V14SezckO6CNLKs_btPdFiO9_kC1DsuUTd2LAfIIVeMZ7jn1Gus_Ff" + "7B7IVx3p5KuBGOVF8L-qifLb6nQnLysgHDh132NDioZkhH7mI7hPG-PYE_" + "odApKdnqECHWw0J-F0JWnUd6D2B_1TvF9mXA2Qx-iGYn8OVV1Bsmp6qU", + "qi": "eNho5yRBEBxhGBtQRww9QirZsB66TrfFReG_CcteI1aCneT0ELGhYlRlC" + "tUkTRclIfuEPmNsNDPbLoLqqCVznFbvdB7x-Tl-m0l_eFTj2KiqwGqE9PZ" + "B9nNTwMVvH3VRRSLWACvPnSiwP8N5Usy-WRXS-V7TbpxIhvepTfE0NNo", + } + + inner_jwt = Jwt.decrypt_nested_jwt(jwt, jwk) + assert ( + inner_jwt + == "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw" + ) + + +def test_sign_and_encrypt() -> None: + sign_alg = "ES256" + enc_alg = "RSA-OAEP-256" + enc = "A128GCM" + + sign_jwk = ( + Jwk.generate_for_alg(sign_alg).with_kid_thumbprint().with_usage_parameters() + ) + enc_jwk = ( + Jwk.generate_for_alg(enc_alg).with_kid_thumbprint().with_usage_parameters() + ) + + claims = {"iat": 1661759343, "exp": 1661759403, "sub": "mysub"} + enc_jwt = Jwt.sign_and_encrypt(claims, sign_jwk, enc_jwk.public_jwk(), enc) + assert isinstance(enc_jwt, JweCompact) + assert enc_jwt.cty == "JWT" + assert enc_jwt.alg == enc_alg + assert enc_jwt.enc == enc + assert enc_jwt.kid == enc_jwk.kid + + inner_jwt = Jwt(enc_jwt.decrypt(enc_jwk)) + 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.kid == sign_jwk.kid + + verified_inner_jwt = Jwt.decrypt_and_verify( + enc_jwt, enc_jwk=enc_jwk, sig_jwk=sign_jwk.public_jwk() + ) + assert isinstance(verified_inner_jwt, SignedJwt) + + 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) + with pytest.raises(InvalidSignature): + Jwt.decrypt_and_verify( + enc_altered_jwe, enc_jwk=enc_jwk, sig_jwk=sign_jwk.public_jwk() + ) + + +def test_sign_without_alg() -> None: + jwk = Jwk.generate_for_kty("RSA") + with pytest.raises(ValueError): + Jwt.sign({"foo": "bar"}, jwk) + + +def test_large_jwt() -> None: + with pytest.raises(ValueError): + Jwt( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + f"{'alargevalue'*16*1024}" + "bl5iNgXfkbmgDXItaUx7_1lUMNtOffihsShVP8MeE1g" + ) + + +def test_eq() -> None: + jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + assert Jwt(jwt) == Jwt(jwt) + assert Jwt(jwt) == jwt + assert Jwt(jwt) == jwt.encode() + + assert Jwt(jwt) != 1 + + +def test_invalid_headers() -> None: + jwt = Jwt( + "eyJhbGciOjEsImtpZCI6MSwidHlwIjoxLCJjdHkiOjF9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cOUKU1ijv3KiN2KK_o50RU978I9MzQ4lNw2y7nOGAdM" + ) + with pytest.raises(AttributeError): + jwt.alg + with pytest.raises(AttributeError): + jwt.kid + with pytest.raises(AttributeError): + jwt.typ + with pytest.raises(AttributeError): + jwt.cty + + +def test_invalid_claims() -> None: + jwt = SignedJwt( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImlhdCI6ImZvbyIsImV4cCI6ImZvbyIsIm5iZiI6ImZvbyIsImF1ZCI6MSwianRpIjoxfQ.lcNMSH9LNXbIpQUAqtbIjMv-kSWXeC0VamsrHNESTq0" + ) + with pytest.raises(AttributeError): + jwt.subject + with pytest.raises(AttributeError): + jwt.issued_at + with pytest.raises(AttributeError): + jwt.expires_at + with pytest.raises(AttributeError): + jwt.not_before + with pytest.raises(AttributeError): + jwt.audiences + with pytest.raises(AttributeError): + jwt.jwt_token_id