From 6a734684fe775b915b6d41a07368006909bdc8a7 Mon Sep 17 00:00:00 2001 From: Guillaume Pujol Date: Fri, 19 Jan 2024 13:09:58 +0100 Subject: [PATCH] more convenience methods (#22) * BREAKING CHANGE: make more args kw-only * BREAKING CHANGE: JwtSigner now takes a `key` param instead of `jwk`. `issuer` becomes an optional kwarg. * BREAKING CHANGE: `InvalidSignature` exception is now defined in `jws` submodule and accepts any `SupportsBytes` as data. * add `JwsCompact.verify()` --- .pre-commit-config.yaml | 13 +- README.md | 52 +- jwskate/__init__.py | 4 +- jwskate/enums.py | 1 + jwskate/jwa/__init__.py | 1 + jwskate/jwa/base.py | 1 + jwskate/jwa/ec.py | 1 + jwskate/jwa/encryption/__init__.py | 1 + jwskate/jwa/encryption/aescbchmac.py | 1 + jwskate/jwa/encryption/aesgcm.py | 1 + jwskate/jwa/key_mgmt/__init__.py | 1 + jwskate/jwa/key_mgmt/aesgcmkw.py | 1 + jwskate/jwa/key_mgmt/aeskw.py | 1 + jwskate/jwa/key_mgmt/dir.py | 1 + jwskate/jwa/key_mgmt/ecdh.py | 17 +- jwskate/jwa/key_mgmt/pbes2.py | 1 + jwskate/jwa/key_mgmt/rsa.py | 1 + jwskate/jwa/okp.py | 7 +- jwskate/jwa/signature/__init__.py | 1 + jwskate/jwa/signature/ec.py | 1 + jwskate/jwa/signature/eddsa.py | 1 + jwskate/jwa/signature/hmac.py | 1 + jwskate/jwa/signature/rsa.py | 1 + jwskate/jwe/__init__.py | 1 + jwskate/jwe/compact.py | 4 +- jwskate/jwk/__init__.py | 1 + jwskate/jwk/alg.py | 1 + jwskate/jwk/base.py | 7 +- jwskate/jwk/ec.py | 1 + jwskate/jwk/jwks.py | 3 +- jwskate/jwk/oct.py | 1 + jwskate/jwk/okp.py | 1 + jwskate/jwk/rsa.py | 1 + jwskate/jws/__init__.py | 5 +- jwskate/jws/compact.py | 431 +++++++------ jwskate/jws/json.py | 3 +- jwskate/jws/signature.py | 18 +- jwskate/jwt/__init__.py | 4 +- jwskate/jwt/base.py | 14 +- jwskate/jwt/signed.py | 867 ++++++++++++++------------- jwskate/jwt/signer.py | 27 +- jwskate/jwt/verifier.py | 7 +- jwskate/token.py | 9 +- pyproject.toml | 1 + tests/__init__.py | 1 + tests/conftest.py | 29 +- tests/test_jwa/test_encryption.py | 1 + tests/test_jwa/test_examples.py | 33 +- tests/test_jwa/test_key_mgmt.py | 3 +- tests/test_jwa/test_signature.py | 1 + tests/test_jwe.py | 190 +++--- tests/test_jwk/test_ec.py | 16 +- tests/test_jwk/test_jwk.py | 78 ++- tests/test_jwk/test_jwks.py | 84 ++- tests/test_jwk/test_okp.py | 80 ++- tests/test_jwk/test_rsa.py | 28 +- tests/test_jws.py | 4 +- tests/test_jwt.py | 134 ++--- 58 files changed, 1144 insertions(+), 1056 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11bf779..c8d5bbe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] @@ -21,20 +21,18 @@ repos: - --in-place - --wrap-summaries=100 - --wrap-descriptions=100 -- repo: https://github.com/psf/black - rev: 23.9.1 - hooks: - - id: black - repo: https://github.com/asottile/blacken-docs rev: 1.16.0 hooks: - id: blacken-docs - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.292 + rev: v0.1.11 hooks: - id: ruff + args: [ --fix ] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.8.0 hooks: - id: mypy args: @@ -48,3 +46,4 @@ repos: - pytest-mypy==0.10.3 - binapy==0.7.0 - freezegun==1.2.2 + - jwcrypto==1.5.0 diff --git a/README.md b/README.md index 72dc3c5..95787cc 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,21 @@ from jwskate import Jwk # Let's generate a random private key, to use with alg 'RS256'. # Based on that alg, jwskate knows it must be an RSA key. -# RSA keys can be of variable size, so let's pass the requested key size as parameter +# RSA keys can be of any size, so let's pass the requested key size as parameter rsa_private_jwk = Jwk.generate(alg="RS256", key_size=2048) data = b"Signing is easy!" # we will sign this signature = rsa_private_jwk.sign(data) # done! +print(signature) +# b'-\xe89\x81\xc4\xb9.G\x11\xa6\x93/dm\xf0\xc8\x0f\xd....' + # now extract the public key, and verify the signature with it rsa_public_jwk = rsa_private_jwk.public_jwk() assert rsa_public_jwk.verify(data, signature) -# let's see what a Jwk looks like: -assert isinstance(rsa_private_jwk, dict) # Jwk are dict +# let's see what a `Jwk` looks like: +assert isinstance(rsa_private_jwk, dict) # Jwk are dict subclasses print(rsa_private_jwk.with_usage_parameters()) ``` @@ -98,13 +101,13 @@ assert jwt.verify_signature(private_jwk.public_jwk(), alg="ES256") # or with `alg` or `algs` params, and will ignore the 'alg' that is set in the JWT, for security reasons. ``` -Now let's sign a JWT with the standardised lifetime, subject, audience and ID claims, plus arbitrary custom claims: +Now let's sign a JWT with the standardized lifetime, subject, audience and ID claims, plus arbitrary custom claims: ```python from jwskate import Jwk, JwtSigner private_jwk = Jwk.generate(alg="ES256") -signer = JwtSigner(issuer="https://myissuer.com", jwk=private_jwk) +signer = JwtSigner(issuer="https://myissuer.com", key=private_jwk) jwt = signer.sign( subject="some_sub", audience="some_aud", @@ -114,7 +117,7 @@ jwt = signer.sign( print(jwt.claims) ``` -The generated JWT will include the standardised claims (`iss`, `aud`, `sub`, `iat`, `exp` and `jti`), +The generated JWT will include the standardized claims (`iss`, `aud`, `sub`, `iat`, `exp` and `jti`), together with the `extra_claims` provided to `.sign()`: ``` @@ -163,18 +166,31 @@ together with the `extra_claims` provided to `.sign()`: | `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] | Ed2219 and Ed448 are supported | +| `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] | | | `ES256K` | ECDSA using secp256k1 curve and SHA-256 | `EC` | [RFC8812, Section 3.2] | | +| `EdDSA` | EdDSA signature algorithms | `OKP` | [RFC8037, Section 3.1] | Ed2219 and Ed448 are supported | | `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 | +| `RS1` | RSASSA-PKCS1-v1_5 with SHA-1 | `RSA` | https://www.w3.org/TR/WebCryptoAPI | Validation Only | | `none` | No digital signature or MAC performed | | [RFC7518, Section 3.6] | Not usable by mistake | +### Supported 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] | + + ### Supported Key Management algorithms @@ -200,18 +216,8 @@ together with the `extra_claims` provided to `.sign()`: | `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 -| 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 @@ -220,13 +226,13 @@ together with the `extra_claims` provided to `.sign()`: | `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] | -| `secp256k1` | SECG secp256k1 curve | `EC` | signature, encryption | [RFC8812, Section 3.1] | -## Why a new lib ? +## Why a new lib? There are already multiple modules implementing JOSE and Json Web Crypto related specifications in Python. However, I have been dissatisfied by all of them so far, so I decided to come up with my own module. diff --git a/jwskate/__init__.py b/jwskate/__init__.py index f4dca40..ebd3569 100644 --- a/jwskate/__init__.py +++ b/jwskate/__init__.py @@ -9,6 +9,7 @@ provides a set of convenient wrappers around the `cryptography` module. """ + from __future__ import annotations __author__ = """Guillaume Pujol""" @@ -97,12 +98,11 @@ select_alg_classes, to_jwk, ) -from .jws import InvalidJws, JwsCompact, JwsJsonFlat, JwsJsonGeneral, JwsSignature +from .jws import InvalidJws, InvalidSignature, JwsCompact, JwsJsonFlat, JwsJsonGeneral, JwsSignature from .jwt import ( ExpiredJwt, InvalidClaim, InvalidJwt, - InvalidSignature, Jwt, JwtSigner, JwtVerifier, diff --git a/jwskate/enums.py b/jwskate/enums.py index 3b97841..99a9c5a 100644 --- a/jwskate/enums.py +++ b/jwskate/enums.py @@ -3,6 +3,7 @@ See [IANA JOSE](https://www.iana.org/assignments/jose/jose.xhtml). """ + from __future__ import annotations diff --git a/jwskate/jwa/__init__.py b/jwskate/jwa/__init__.py index 6da0b33..5d4fd20 100644 --- a/jwskate/jwa/__init__.py +++ b/jwskate/jwa/__init__.py @@ -7,6 +7,7 @@ [RFC7518]: https://www.rfc-editor.org/rfc/rfc7518 """ + from __future__ import annotations from .base import ( diff --git a/jwskate/jwa/base.py b/jwskate/jwa/base.py index faef622..8f24b75 100644 --- a/jwskate/jwa/base.py +++ b/jwskate/jwa/base.py @@ -1,4 +1,5 @@ """This module implement base classes for the algorithms defined in JWA.""" + from __future__ import annotations from contextlib import contextmanager diff --git a/jwskate/jwa/ec.py b/jwskate/jwa/ec.py index a169786..0c91f78 100644 --- a/jwskate/jwa/ec.py +++ b/jwskate/jwa/ec.py @@ -1,4 +1,5 @@ """This module contains classes that describe Elliptic Curves as described in RFC7518.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/jwskate/jwa/encryption/__init__.py b/jwskate/jwa/encryption/__init__.py index 1b857e6..5f28bb1 100644 --- a/jwskate/jwa/encryption/__init__.py +++ b/jwskate/jwa/encryption/__init__.py @@ -1,4 +1,5 @@ """This module exposes the Encryption algorithms that are available in `jwskate`.""" + from __future__ import annotations from .aescbchmac import A128CBC_HS256, A192CBC_HS384, A256CBC_HS512 diff --git a/jwskate/jwa/encryption/aescbchmac.py b/jwskate/jwa/encryption/aescbchmac.py index e8e3e2d..2f4a9f3 100644 --- a/jwskate/jwa/encryption/aescbchmac.py +++ b/jwskate/jwa/encryption/aescbchmac.py @@ -1,4 +1,5 @@ """This module implements AES-CBC with HMAC-SHA based Encryption algorithms.""" + from __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwa/encryption/aesgcm.py b/jwskate/jwa/encryption/aesgcm.py index 8ee784c..6612aa7 100644 --- a/jwskate/jwa/encryption/aesgcm.py +++ b/jwskate/jwa/encryption/aesgcm.py @@ -1,4 +1,5 @@ """This module implements AES-GCM based encryption algorithms.""" + from __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwa/key_mgmt/__init__.py b/jwskate/jwa/key_mgmt/__init__.py index 145b939..17aa781 100644 --- a/jwskate/jwa/key_mgmt/__init__.py +++ b/jwskate/jwa/key_mgmt/__init__.py @@ -1,4 +1,5 @@ """This module exposes all Key Management algorithms available in `jwskate`.""" + from __future__ import annotations from .aesgcmkw import A128GCMKW, A192GCMKW, A256GCMKW, BaseAesGcmKeyWrap diff --git a/jwskate/jwa/key_mgmt/aesgcmkw.py b/jwskate/jwa/key_mgmt/aesgcmkw.py index de03939..017a1cd 100644 --- a/jwskate/jwa/key_mgmt/aesgcmkw.py +++ b/jwskate/jwa/key_mgmt/aesgcmkw.py @@ -1,4 +1,5 @@ """This module implements AES-GCM based Key Management algorithms.""" + from __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwa/key_mgmt/aeskw.py b/jwskate/jwa/key_mgmt/aeskw.py index a0a62fd..3143f03 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 __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwa/key_mgmt/dir.py b/jwskate/jwa/key_mgmt/dir.py index e1cc96e..e6bb9f8 100644 --- a/jwskate/jwa/key_mgmt/dir.py +++ b/jwskate/jwa/key_mgmt/dir.py @@ -1,4 +1,5 @@ """This module implements direct use of a shared symmetric key as Key Management algorithm.""" + from __future__ import annotations from binapy import BinaPy diff --git a/jwskate/jwa/key_mgmt/ecdh.py b/jwskate/jwa/key_mgmt/ecdh.py index 12aa0a9..3567379 100644 --- a/jwskate/jwa/key_mgmt/ecdh.py +++ b/jwskate/jwa/key_mgmt/ecdh.py @@ -1,4 +1,5 @@ """This module implements Elliptic Curve Diffie-Hellman based Key Management algorithms.""" + from __future__ import annotations from typing import Any, SupportsBytes, Union @@ -65,8 +66,8 @@ def otherinfo(cls, alg: str, apu: bytes, apv: bytes, key_size: int) -> BinaPy: @classmethod def ecdh( cls, - private_key: (ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey), - public_key: (ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey), + private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey, + public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey, ) -> BinaPy: """Perform an Elliptic Curve Diffie-Hellman key exchange. @@ -104,8 +105,8 @@ def ecdh( def derive( cls, *, - private_key: (ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey), - public_key: (ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey), + private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey, + public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey, otherinfo: bytes, key_size: int, ) -> BinaPy: @@ -143,7 +144,7 @@ def generate_ephemeral_key( def sender_key( self, - ephemeral_private_key: (ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey), + ephemeral_private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey, *, alg: str, key_size: int, @@ -175,7 +176,7 @@ def sender_key( def recipient_key( self, - ephemeral_public_key: (ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey), + ephemeral_public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey, *, alg: str, key_size: int, @@ -214,7 +215,7 @@ class BaseEcdhEs_AesKw(EcdhEs): # noqa: N801 def wrap_key_with_epk( self, plainkey: bytes, - ephemeral_private_key: (ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey), + ephemeral_private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey, **headers: Any, ) -> BinaPy: """Wrap a key for content encryption. @@ -234,7 +235,7 @@ def wrap_key_with_epk( def unwrap_key_with_epk( self, cipherkey: bytes | SupportsBytes, - ephemeral_public_key: (ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey), + ephemeral_public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey, **headers: Any, ) -> BinaPy: """Unwrap a key for content decryption. diff --git a/jwskate/jwa/key_mgmt/pbes2.py b/jwskate/jwa/key_mgmt/pbes2.py index 1383d37..77ede69 100644 --- a/jwskate/jwa/key_mgmt/pbes2.py +++ b/jwskate/jwa/key_mgmt/pbes2.py @@ -1,4 +1,5 @@ """This module implements password-based Key Management Algorithms relying on PBES2.""" + from __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwa/key_mgmt/rsa.py b/jwskate/jwa/key_mgmt/rsa.py index 1762420..355f7a4 100644 --- a/jwskate/jwa/key_mgmt/rsa.py +++ b/jwskate/jwa/key_mgmt/rsa.py @@ -1,4 +1,5 @@ """This module implements RSA based Key Management algorithms.""" + from __future__ import annotations from typing import Any, SupportsBytes diff --git a/jwskate/jwa/okp.py b/jwskate/jwa/okp.py index 60f8968..fed4605 100644 --- a/jwskate/jwa/okp.py +++ b/jwskate/jwa/okp.py @@ -4,6 +4,7 @@ : https: //www.rfc-editor.org/rfc/rfc8037.html """ + from __future__ import annotations from dataclasses import dataclass @@ -22,7 +23,8 @@ def public_bytes( # noqa: D102 self, encoding: serialization.Encoding, format: serialization.PublicFormat, # noqa: A002 - ) -> bytes: ... + ) -> bytes: + ... @runtime_checkable @@ -34,7 +36,8 @@ def private_bytes( # noqa: D102 encoding: serialization.Encoding, format: serialization.PrivateFormat, # noqa: A002 encryption_algorithm: serialization.KeySerializationEncryption, - ) -> bytes: ... + ) -> bytes: + ... def public_key(self) -> PublicKeyProtocol: # noqa: D102 ... diff --git a/jwskate/jwa/signature/__init__.py b/jwskate/jwa/signature/__init__.py index 4ab1852..dca52f6 100644 --- a/jwskate/jwa/signature/__init__.py +++ b/jwskate/jwa/signature/__init__.py @@ -1,4 +1,5 @@ """This module exposes all the Signature algorithms available from `jwskate`.""" + from __future__ import annotations from .ec import ES256, ES256K, ES384, ES512, BaseECSignatureAlg diff --git a/jwskate/jwa/signature/ec.py b/jwskate/jwa/signature/ec.py index 1452a8d..44a3b3b 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 __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwa/signature/eddsa.py b/jwskate/jwa/signature/eddsa.py index c468fbc..042d738 100644 --- a/jwskate/jwa/signature/eddsa.py +++ b/jwskate/jwa/signature/eddsa.py @@ -1,4 +1,5 @@ """This module implements the Edwards-curve Digital Signature Algorithm (EdDSA).""" + from __future__ import annotations from typing import SupportsBytes, Union diff --git a/jwskate/jwa/signature/hmac.py b/jwskate/jwa/signature/hmac.py index c648621..aa5f315 100644 --- a/jwskate/jwa/signature/hmac.py +++ b/jwskate/jwa/signature/hmac.py @@ -1,4 +1,5 @@ """This module implements HMAC based signature algorithms.""" + from __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwa/signature/rsa.py b/jwskate/jwa/signature/rsa.py index 0615ac3..37a21bc 100644 --- a/jwskate/jwa/signature/rsa.py +++ b/jwskate/jwa/signature/rsa.py @@ -1,4 +1,5 @@ """This module implements RSA signature algorithms.""" + from __future__ import annotations from typing import SupportsBytes diff --git a/jwskate/jwe/__init__.py b/jwskate/jwe/__init__.py index 8b54337..cf975da 100644 --- a/jwskate/jwe/__init__.py +++ b/jwskate/jwe/__init__.py @@ -4,6 +4,7 @@ : https: //www.rfc-editor.org/rfc/rfc7516 """ + from __future__ import annotations from .compact import InvalidJwe, JweCompact diff --git a/jwskate/jwe/compact.py b/jwskate/jwe/compact.py index 16fa573..f98cb83 100644 --- a/jwskate/jwe/compact.py +++ b/jwskate/jwe/compact.py @@ -1,4 +1,5 @@ """This module implements the JWE Compact format.""" + from __future__ import annotations import warnings @@ -220,10 +221,11 @@ def unwrap_cek( def decrypt( self, key: Jwk | dict[str, Any] | Any, + *, alg: str | None = None, algs: Iterable[str] | None = None, ) -> BinaPy: - """Decrypts this `Jwe` payload using a `Jwk`. + """Decrypt the payload from this JWE using a decryption key. Args: key: the decryption key diff --git a/jwskate/jwk/__init__.py b/jwskate/jwk/__init__.py index e9ba1a0..825867d 100644 --- a/jwskate/jwk/__init__.py +++ b/jwskate/jwk/__init__.py @@ -1,4 +1,5 @@ """This module implements [Json Web Key RFC7517](https://tools.ietf.org/html/rfc7517).""" + from __future__ import annotations from .alg import ( diff --git a/jwskate/jwk/alg.py b/jwskate/jwk/alg.py index bd9a07a..5ba8950 100644 --- a/jwskate/jwk/alg.py +++ b/jwskate/jwk/alg.py @@ -1,4 +1,5 @@ """This module contains several utilities for algorithmic agility.""" + from __future__ import annotations import warnings diff --git a/jwskate/jwk/base.py b/jwskate/jwk/base.py index 833c4c1..987dbfb 100644 --- a/jwskate/jwk/base.py +++ b/jwskate/jwk/base.py @@ -8,6 +8,7 @@ need to use the interface from `Jwk`. """ + from __future__ import annotations import warnings @@ -178,7 +179,7 @@ def __new__(cls, key: Jwk | dict[str, Any] | Any, **kwargs: Any) -> Jwk: for jwk_class in Jwk.__subclasses__(): if kty == jwk_class.KTY: - return super().__new__(jwk_class) # type: ignore[type-var] + return super().__new__(jwk_class) msg = "Unsupported Key Type" raise InvalidJwk(msg, kty) @@ -187,7 +188,7 @@ def __new__(cls, key: Jwk | dict[str, Any] | Any, **kwargs: Any) -> Jwk: return cls.from_json(key) else: return cls.from_cryptography_key(key, **kwargs) - return super().__new__(cls, key, **kwargs) # type: ignore[type-var] + return super().__new__(cls, key, **kwargs) def __init__(self, params: dict[str, Any] | Any, *, include_kid_thumbprint: bool = False): if isinstance(params, dict): # this is to avoid double init due to the __new__ above @@ -646,7 +647,7 @@ def supported_encryption_algorithms(self) -> list[str]: return list(self.ENCRYPTION_ALGORITHMS) def sign(self, data: bytes | SupportsBytes, alg: str | None = None) -> BinaPy: - """Sign a data using this Jwk, and return the generated signature. + """Sign data using this Jwk, and return the generated signature. Args: data: the data to sign diff --git a/jwskate/jwk/ec.py b/jwskate/jwk/ec.py index bcf69ce..c476c86 100644 --- a/jwskate/jwk/ec.py +++ b/jwskate/jwk/ec.py @@ -1,4 +1,5 @@ """This module implements JWK representing Elliptic Curve keys.""" + from __future__ import annotations import warnings diff --git a/jwskate/jwk/jwks.py b/jwskate/jwk/jwks.py index 85f2844..7b760c9 100644 --- a/jwskate/jwk/jwks.py +++ b/jwskate/jwk/jwks.py @@ -1,4 +1,5 @@ """This module implements Json Web Key Sets (JWKS).""" + from __future__ import annotations from typing import Any, Iterable @@ -52,7 +53,7 @@ def jwks(self) -> list[Jwk]: a list of `Jwk` """ - return self.get("keys", []) + return self.get("keys", []) # type: ignore[no-any-return] def get_jwk_by_kid(self, kid: str) -> Jwk: """Return a Jwk from this JwkSet, based on its kid. diff --git a/jwskate/jwk/oct.py b/jwskate/jwk/oct.py index d8b04db..810101a 100644 --- a/jwskate/jwk/oct.py +++ b/jwskate/jwk/oct.py @@ -1,4 +1,5 @@ """This module implements JWK representing Symmetric keys.""" + from __future__ import annotations import warnings diff --git a/jwskate/jwk/okp.py b/jwskate/jwk/okp.py index b436a6d..7fa4096 100644 --- a/jwskate/jwk/okp.py +++ b/jwskate/jwk/okp.py @@ -4,6 +4,7 @@ : https: //www.rfc-editor.org/rfc/rfc8037.html """ + from __future__ import annotations from functools import cached_property diff --git a/jwskate/jwk/rsa.py b/jwskate/jwk/rsa.py index 0e98e0e..ceafc8f 100644 --- a/jwskate/jwk/rsa.py +++ b/jwskate/jwk/rsa.py @@ -1,4 +1,5 @@ """This module implements JWK representing RSA keys.""" + from __future__ import annotations from functools import cached_property diff --git a/jwskate/jws/__init__.py b/jwskate/jws/__init__.py index 0b9243c..c0abee1 100644 --- a/jwskate/jws/__init__.py +++ b/jwskate/jws/__init__.py @@ -1,8 +1,9 @@ """This module implements JWS token handling.""" + from __future__ import annotations from .compact import InvalidJws, JwsCompact from .json import JwsJsonFlat, JwsJsonGeneral -from .signature import JwsSignature +from .signature import InvalidSignature, JwsSignature -__all__ = ["InvalidJws", "JwsCompact", "JwsJsonFlat", "JwsJsonGeneral", "JwsSignature"] +__all__ = ["InvalidJws", "InvalidSignature", "JwsCompact", "JwsJsonFlat", "JwsJsonGeneral", "JwsSignature"] diff --git a/jwskate/jws/compact.py b/jwskate/jws/compact.py index bd3f12c..2dc46b2 100644 --- a/jwskate/jws/compact.py +++ b/jwskate/jws/compact.py @@ -1,192 +1,239 @@ -"""This module implements the JWS Compact format.""" -from __future__ import annotations - -from functools import cached_property -from typing import TYPE_CHECKING, Any, Iterable, SupportsBytes - -from binapy import BinaPy - -from jwskate.jwk.base import Jwk, to_jwk -from jwskate.token import BaseCompactToken - -from .signature import JwsSignature - -if TYPE_CHECKING: - from .json import JwsJsonFlat, JwsJsonGeneral # pragma: no cover - - -class InvalidJws(ValueError): - """Raised when an invalid Jws is parsed.""" - - -class JwsCompact(BaseCompactToken): - """Represents a Json Web Signature (JWS), using compact serialization, as defined in RFC7515. - - Args: - value: the JWS token value - - """ - - def __init__(self, value: bytes | str, max_size: int = 16 * 1024): - super().__init__(value, max_size) - - parts = BinaPy(self.value).split(b".") - - if len(parts) != 3: # noqa: PLR2004 - msg = "A JWS must contain a header, a payload and a signature, separated by dots" - raise InvalidJws(msg) - - header, payload, signature = parts - - try: - self.headers = header.decode_from("b64u").parse_from("json") - except ValueError as exc: - msg = "Invalid JWS header: it must be a Base64URL-encoded JSON object" - raise InvalidJws(msg) from exc - - try: - self.payload = payload.decode_from("b64u") - except ValueError as exc: - msg = "Invalid JWS payload: it must be a Base64URL-encoded binary data (bytes)" - raise InvalidJws(msg) from exc - - try: - self.signature = signature.decode_from("b64u") - except ValueError as exc: - msg = "Invalid JWS signature: it must be a Base64URL-encoded binary data (bytes)" - raise InvalidJws(msg) from exc - - @classmethod - def sign( - cls, - payload: bytes | SupportsBytes, - key: Jwk | dict[str, Any] | Any, - alg: str | None = None, - extra_headers: dict[str, Any] | None = None, - ) -> JwsCompact: - """Sign a payload and returns the resulting JwsCompact. - - Args: - payload: the payload to sign - key: the jwk to use to sign this payload - alg: the alg to use - extra_headers: additional headers to add to the Jws Headers - - Returns: - the resulting token - - """ - key = to_jwk(key) - - if not isinstance(payload, bytes): - payload = bytes(payload) - - headers = dict(extra_headers or {}, alg=alg) - kid = key.get("kid") - if kid: - headers["kid"] = kid - - signed_part = JwsSignature.assemble_signed_part(headers, payload) - signature = key.sign(signed_part, alg=alg) - return cls.from_parts(signed_part, signature) - - @classmethod - def from_parts( - cls, - signed_part: bytes | SupportsBytes | str, - signature: bytes | SupportsBytes, - ) -> JwsCompact: - """Construct 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. - - Args: - signed_part: the signed part - signature: the signature value - - Returns: - the resulting token - - """ - if isinstance(signed_part, str): - signed_part = signed_part.encode("ascii") - if not isinstance(signed_part, bytes): - signed_part = bytes(signed_part) - - if not isinstance(signature, bytes): - signature = bytes(signature) - - return cls(b".".join((signed_part, BinaPy(signature).to("b64u")))) - - @cached_property - def signed_part(self) -> bytes: - """Returns the signed part (header + payload) from this JwsCompact. - - Returns: - the signed part - - """ - return b".".join(self.value.split(b".", 2)[:2]) - - def verify_signature( - self, - key: Jwk | dict[str, Any] | Any, - *, - alg: str | None = None, - algs: Iterable[str] | None = None, - ) -> bool: - """Verify the signature from this JwsCompact using a Jwk. - - Args: - key: the Jwk to use to validate this signature - alg: the alg to use, if there is only 1 allowed - algs: the allowed algs, if here are several - - Returns: - `True` if the signature matches, `False` otherwise - - """ - key = to_jwk(key) - return key.verify(self.signed_part, self.signature, alg=alg, algs=algs) - - def flat_json(self, unprotected_header: Any = None) -> JwsJsonFlat: - """Create a JWS in JSON flat format based on this Compact JWS. - - Args: - unprotected_header: optional unprotected header to include in the JWS JSON - - Returns: - the resulting token - - """ - from .json import JwsJsonFlat - - protected, payload, signature = self.value.split(b".") - - content = { - "payload": payload.decode(), - "protected": protected.decode(), - "signature": signature.decode(), - } - if unprotected_header is not None: - content["header"] = unprotected_header - return JwsJsonFlat(content) - - def general_json(self, unprotected_header: Any = None) -> JwsJsonGeneral: - """Create a JWS in JSON General format based on this JWS Compact. - - The resulting token will have a single signature which is the one from this token. - - Args: - unprotected_header: optional unprotected header to include in the JWS JSON - - Returns: - the resulting token - - """ - jws = self.flat_json(unprotected_header) - return jws.generalize() - - def jws_signature(self, unprotected_header: Any = None) -> JwsSignature: - """Return a JwsSignature based on this JWS Compact token.""" - return JwsSignature.from_parts(protected=self.headers, signature=self.signature, header=unprotected_header) +"""This module implements the JWS Compact format.""" + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Any, Iterable, SupportsBytes + +from binapy import BinaPy +from typing_extensions import Self + +from jwskate.jwk.base import Jwk, to_jwk +from jwskate.token import BaseCompactToken + +from .signature import InvalidSignature, JwsSignature + +if TYPE_CHECKING: + from .json import JwsJsonFlat, JwsJsonGeneral # pragma: no cover + + +class InvalidJws(ValueError): + """Raised when an invalid Jws is parsed.""" + + +class JwsCompact(BaseCompactToken): + """Represents a Json Web Signature (JWS), using compact serialization, as defined in RFC7515. + + Args: + value: the JWS token value + + """ + + def __init__(self, value: bytes | str, max_size: int = 16 * 1024): + super().__init__(value, max_size) + + parts = BinaPy(self.value).split(b".") + + if len(parts) != 3: # noqa: PLR2004 + msg = "A JWS must contain a header, a payload and a signature, separated by dots" + raise InvalidJws(msg) + + header, payload, signature = parts + + try: + self.headers = header.decode_from("b64u").parse_from("json") + except ValueError as exc: + msg = "Invalid JWS header: it must be a Base64URL-encoded JSON object" + raise InvalidJws(msg) from exc + + try: + self.payload = payload.decode_from("b64u") + except ValueError as exc: + msg = "Invalid JWS payload: it must be a Base64URL-encoded binary data (bytes)" + raise InvalidJws(msg) from exc + + try: + self.signature = signature.decode_from("b64u") + except ValueError as exc: + msg = "Invalid JWS signature: it must be a Base64URL-encoded binary data (bytes)" + raise InvalidJws(msg) from exc + + @classmethod + def sign( + cls, + payload: bytes | SupportsBytes, + key: Jwk | dict[str, Any] | Any, + alg: str | None = None, + extra_headers: dict[str, Any] | None = None, + ) -> JwsCompact: + """Sign a payload and returns the resulting JwsCompact. + + Args: + payload: the payload to sign + key: the jwk to use to sign this payload + alg: the alg to use + extra_headers: additional headers to add to the Jws Headers + + Returns: + the resulting token + + """ + key = to_jwk(key) + + if not isinstance(payload, bytes): + payload = bytes(payload) + + headers = dict(extra_headers or {}, alg=alg) + kid = key.get("kid") + if kid: + headers["kid"] = kid + + signed_part = JwsSignature.assemble_signed_part(headers, payload) + signature = key.sign(signed_part, alg=alg) + return cls.from_parts(signed_part, signature) + + @classmethod + def from_parts( + cls, + signed_part: bytes | SupportsBytes | str, + signature: bytes | SupportsBytes, + ) -> JwsCompact: + """Construct 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. + + Args: + signed_part: the signed part + signature: the signature value + + Returns: + the resulting token + + """ + if isinstance(signed_part, str): + signed_part = signed_part.encode("ascii") + if not isinstance(signed_part, bytes): + signed_part = bytes(signed_part) + + if not isinstance(signature, bytes): + signature = bytes(signature) + + return cls(b".".join((signed_part, BinaPy(signature).to("b64u")))) + + @cached_property + def signed_part(self) -> bytes: + """Returns the signed part (header + payload) from this JwsCompact. + + Returns: + the signed part + + """ + return b".".join(self.value.split(b".", 2)[:2]) + + def verify_signature( + self, + key: Jwk | dict[str, Any] | Any, + *, + alg: str | None = None, + algs: Iterable[str] | None = None, + ) -> bool: + """Verify the signature from this JwsCompact using a key. + + Args: + key: the Jwk to use to validate this signature + alg: the alg to use, if there is only 1 allowed + algs: the allowed algs, if here are several + + Returns: + `True` if the signature matches, `False` otherwise + + """ + key = to_jwk(key) + return key.verify(self.signed_part, self.signature, alg=alg, algs=algs) + + def verify( + self, + key: Jwk | dict[str, Any] | Any, + *, + alg: str | None = None, + algs: Iterable[str] | None = None, + ) -> Self: + """Verify this JWS signature. + + This is an alternative to `.verify_signature()` that raises an exception if the signature is not + verified. + + Args: + key: the Jwk to use to validate this signature + alg: the alg to use, if there is only 1 allowed + algs: the allowed algs, if here are several + + Raises: + InvalidSignature: if the signature does not verify + + Returns: + The same JwsCompact + + Usage: + ```python + jws = JwsCompact( + "eyJhbGciOm51bGx9.SGVsbG8gV29ybGQh.rd61m4AQ6dOqexdZC9revgictOzRd7dmHiQ5UMa9g66BhAO8crw_E_5SkydE-PNNzRkdFdq4P2YzzM1HgfnWlw" + ).verify( + { + "kty": "EC", + "alg": "ES256", + "crv": "P-256", + "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU", + "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM", + } + ) + + assert jws.payload == b"Hello World!" + ``` + + """ + if self.verify_signature(key, alg=alg, algs=algs): + return self + raise InvalidSignature(data=self, key=key, alg=alg, algs=algs) + + def flat_json(self, unprotected_header: Any = None) -> JwsJsonFlat: + """Create a JWS in JSON flat format based on this Compact JWS. + + Args: + unprotected_header: optional unprotected header to include in the JWS JSON + + Returns: + the resulting token + + """ + from .json import JwsJsonFlat + + protected, payload, signature = self.value.split(b".") + + content = { + "payload": payload.decode(), + "protected": protected.decode(), + "signature": signature.decode(), + } + if unprotected_header is not None: + content["header"] = unprotected_header + return JwsJsonFlat(content) + + def general_json(self, unprotected_header: Any = None) -> JwsJsonGeneral: + """Create a JWS in JSON General format based on this JWS Compact. + + The resulting token will have a single signature which is the one from this token. + + Args: + unprotected_header: optional unprotected header to include in the JWS JSON + + Returns: + the resulting token + + """ + jws = self.flat_json(unprotected_header) + return jws.generalize() + + def jws_signature(self, unprotected_header: Any = None) -> JwsSignature: + """Return a JwsSignature based on this JWS Compact token.""" + return JwsSignature.from_parts(protected=self.headers, signature=self.signature, header=unprotected_header) diff --git a/jwskate/jws/json.py b/jwskate/jws/json.py index 83aadf8..69ce67d 100644 --- a/jwskate/jws/json.py +++ b/jwskate/jws/json.py @@ -1,4 +1,5 @@ -"""This module implement the JWS JSON flat and general formats.""" +"""This module implements the JWS JSON flat and general formats.""" + from __future__ import annotations from functools import cached_property diff --git a/jwskate/jws/signature.py b/jwskate/jws/signature.py index d214487..e213e28 100644 --- a/jwskate/jws/signature.py +++ b/jwskate/jws/signature.py @@ -1,21 +1,33 @@ """This module implement JWS signatures.""" + from __future__ import annotations from functools import cached_property -from typing import Any, Iterable, Mapping, TypeVar +from typing import Any, Iterable, Mapping, SupportsBytes, TypeVar from binapy import BinaPy from jwskate.jwk import Jwk, to_jwk from jwskate.token import BaseJsonDict + +class InvalidSignature(ValueError): + """Raised when trying to validate a token with an invalid signature.""" + + def __init__(self, data: SupportsBytes, key: Any, alg: str | None, algs: Iterable[str] | None) -> None: + self.data = data + self.key = key + self.alg = alg + self.algs = algs + + S = TypeVar("S", bound="JwsSignature") class JwsSignature(BaseJsonDict): - """Represents a JWS Signature. + """Represent a JWS Signature. - A JWS Signature has + A JWS Signature has: - a protected header (as a JSON object) - a signature value (as raw data) diff --git a/jwskate/jwt/__init__.py b/jwskate/jwt/__init__.py index 909a185..595daee 100644 --- a/jwskate/jwt/__init__.py +++ b/jwskate/jwt/__init__.py @@ -1,8 +1,9 @@ """This module contains all Json Web Key (Jwk) related classes and utilities.""" + from __future__ import annotations from .base import InvalidJwt, Jwt -from .signed import ExpiredJwt, InvalidClaim, InvalidSignature, SignedJwt +from .signed import ExpiredJwt, InvalidClaim, SignedJwt from .signer import JwtSigner from .verifier import JwtVerifier @@ -10,7 +11,6 @@ "ExpiredJwt", "InvalidClaim", "InvalidJwt", - "InvalidSignature", "Jwt", "JwtSigner", "JwtVerifier", diff --git a/jwskate/jwt/base.py b/jwskate/jwt/base.py index bec7f31..554987e 100644 --- a/jwskate/jwt/base.py +++ b/jwskate/jwt/base.py @@ -1,4 +1,5 @@ -"""This modules contains the `Jwt` base class.""" +"""This module contains the `Jwt` base class.""" + from __future__ import annotations from datetime import datetime, timezone @@ -21,7 +22,7 @@ class InvalidJwt(ValueError): class Jwt(BaseCompactToken): """Represents a Json Web Token.""" - def __new__(cls, value: bytes | str, max_size: int = 16 * 1024) -> SignedJwt | JweCompact | Jwt: # type: ignore[misc] # noqa: E501 + def __new__(cls, value: bytes | str, max_size: int = 16 * 1024) -> SignedJwt | JweCompact | Jwt: # type: ignore[misc] """Allow parsing both Signed and Encrypted JWTs. This returns the appropriate subclass or instance depending on the number of dots (.) in the serialized JWT. @@ -51,6 +52,7 @@ def sign( cls, claims: dict[str, Any], key: Jwk | dict[str, Any] | Any, + *, alg: str | None = None, typ: str | None = "JWT", extra_headers: dict[str, Any] | None = None, @@ -94,6 +96,7 @@ def sign_arbitrary( claims: dict[str, Any], headers: dict[str, Any], key: Jwk | dict[str, Any] | Any, + *, alg: str | None = None, ) -> SignedJwt: """Sign provided headers and claims with a private key and return the resulting `SignedJwt`. @@ -129,6 +132,7 @@ def sign_arbitrary( def unprotected( cls, claims: dict[str, Any], + *, typ: str | None = "JWT", extra_headers: dict[str, Any] | None = None, ) -> SignedJwt: @@ -188,7 +192,9 @@ def sign_and_encrypt( the resulting JWE token, with the signed JWT as payload """ - return cls.sign(claims, key=sign_key, alg=sign_alg, extra_headers=sign_extra_headers).encrypt(enc_key, enc=enc, alg=enc_alg, extra_headers=enc_extra_headers) + return cls.sign(claims, key=sign_key, alg=sign_alg, extra_headers=sign_extra_headers).encrypt( + enc_key, enc=enc, alg=enc_alg, extra_headers=enc_extra_headers + ) @classmethod def decrypt_nested_jwt(cls, jwe: str | JweCompact, key: Jwk | dict[str, Any] | Any) -> SignedJwt: @@ -243,7 +249,7 @@ def decrypt_and_verify( @classmethod def timestamp(cls, delta_seconds: int = 0) -> int: - """Return an integer timestamp that is suitable for use in Jwt tokens. + """Return an integer timestamp that is suitable for use in JWT tokens. Timestamps are used in particular for `iat`, `exp` and `nbf` claims. diff --git a/jwskate/jwt/signed.py b/jwskate/jwt/signed.py index a952724..f48a591 100644 --- a/jwskate/jwt/signed.py +++ b/jwskate/jwt/signed.py @@ -1,427 +1,440 @@ -"""This modules contains classes and utilities to generate and validate signed JWT.""" -from __future__ import annotations - -from datetime import datetime, timedelta, timezone -from functools import cached_property -from typing import Any, Iterable - -from binapy import BinaPy -from typing_extensions import Self - -from jwskate.jwe import JweCompact -from jwskate.jwk import Jwk, to_jwk - -from .base import InvalidJwt, Jwt - - -class ExpiredJwt(ValueError): - """Raised when trying to validate an expired JWT token.""" - - -class InvalidSignature(ValueError): - """Raised when trying to validate a JWT with an invalid signature.""" - - def __init__(self, jwt: SignedJwt, key: Any, alg: str | None, algs: Iterable[str] | None): - self.jwt = jwt - self.key = key - self.alg = alg - self.algs = algs - - -class InvalidClaim(ValueError): - """Raised when trying to validate a JWT with unexpected claims.""" - - -class SignedJwt(Jwt): - """Represent a Signed Json Web Token (JWT), as defined in RFC7519. - - A signed JWT contains a JSON object as payload, which represents claims. - - To sign a JWT, use [Jwt.sign][jwskate.jwt.Jwt.sign]. - - Args: - value: the token value. - - """ - - def __init__(self, value: bytes | str) -> None: - super().__init__(value) - - parts = BinaPy(self.value).split(b".") - if len(parts) != 3: # noqa: PLR2004 - msg = "A JWT must contain a header, a payload and a signature, separated by dots" - raise InvalidJwt( - msg, - value, - ) - - header, payload, signature = parts - try: - self.headers = header.decode_from("b64u").parse_from("json") - except ValueError as exc: - msg = "Invalid JWT header: it must be a Base64URL-encoded JSON object" - raise InvalidJwt(msg) from exc - - try: - self.claims = payload.decode_from("b64u").parse_from("json") - except ValueError as exc: - msg = "Invalid JWT payload: it must be a Base64URL-encoded JSON object" - raise InvalidJwt(msg) from exc - - try: - self.signature = signature.decode_from("b64u") - except ValueError as exc: - msg = "Invalid JWT signature: it must be a Base64URL-encoded binary data (bytes)" - raise InvalidJwt(msg) from exc - - @cached_property - def signed_part(self) -> bytes: - """Return the actual signed data from this token. - - The signed part is composed of the header and payload, encoded in Base64-Url, joined by a dot. - - Returns: - the signed part as bytes - - """ - return b".".join(self.value.split(b".", 2)[:2]) - - def verify_signature( - self, - key: Jwk | dict[str, Any] | Any, - alg: str | None = None, - algs: Iterable[str] | None = None, - ) -> bool: - """Verify this JWT signature using a given key and algorithm(s). - - Args: - key: the private Jwk to use to verify the signature - alg: the alg to use to verify the signature, if only 1 is allowed - algs: the allowed signature algs, if there are several - - Returns: - `True` if the token signature is verified, `False` otherwise - - """ - key = to_jwk(key) - - return key.verify(data=self.signed_part, signature=self.signature, alg=alg, algs=algs) - - def verify(self, key: Jwk | Any, *, alg: str | None = None, algs: Iterable[str] | None = None) -> Self: - """Convenience method to verify the signature inline. - - Returns `self` on success, raises an exception on failure. - - Raises: - InvalidSignature: if the signature does not verify. - - """ - if self.verify_signature(key, alg=alg, algs=algs): - return self - raise InvalidSignature(jwt=self, key=key, alg=alg, algs=algs) - - def is_expired(self, leeway: int = 0) -> bool | None: - """Check if this token is expired, based on its `exp` claim. - - Args: - leeway: additional number of seconds for leeway. - - Returns: - `True` if the token is expired, `False` if it's not, `None` if there is no `exp` claim. - - """ - exp = self.expires_at - if exp is None: - return None - return exp < (datetime.now(timezone.utc) + timedelta(seconds=leeway)) - - @cached_property - def expires_at(self) -> datetime | None: - """Get the *Expires At* (`exp`) date from this token. - - Returns: - a `datetime` initialized from the `exp` claim, or `None` if there is no `exp` claim - - Raises: - AttributeError: if the `exp` claim cannot be parsed to a date - - """ - exp = self.get_claim("exp") - if not exp: - return None - try: - exp_dt = Jwt.timestamp_to_datetime(exp) - except (TypeError, OSError): - msg = "invalid `exp `claim" - raise AttributeError(msg, exp) from None - else: - return exp_dt - - @cached_property - def issued_at(self) -> datetime | None: - """Get the *Issued At* (`iat`) date from this token. - - Returns: - a `datetime` initialized from the `iat` claim, or `None` if there is no `iat` claim - - Raises: - AttributeError: if the `iss` claim cannot be parsed to a date - - """ - iat = self.get_claim("iat") - if not iat: - return None - try: - iat_dt = Jwt.timestamp_to_datetime(iat) - except (TypeError, OSError): - msg = "invalid `iat `claim" - raise AttributeError(msg, iat) from None - else: - return iat_dt - - @cached_property - def not_before(self) -> datetime | None: - """Get the *Not Before* (nbf) date from this token. - - Returns: - a `datetime` initialized from the `nbf` claim, or `None` if there is no `nbf` claim - - Raises: - AttributeError: if the `nbf` claim cannot be parsed to a date - - """ - nbf = self.get_claim("nbf") - if not nbf: - return None - try: - nbf_dt = Jwt.timestamp_to_datetime(nbf) - except (TypeError, OSError): - msg = "invalid `nbf `claim" - raise AttributeError(msg, nbf) from None - else: - return nbf_dt - - @cached_property - def issuer(self) -> str | None: - """Get the *Issuer* (`iss`) claim from this token. - - Returns: - the issuer, as `str`, or `None` if there is no `ìss` claim - - Raises: - AttributeError: if the `ìss` claim value is not a string - - """ - iss = self.get_claim("iss") - if iss is None or isinstance(iss, str): - return iss - msg = "iss has an unexpected type" - raise AttributeError(msg, type(iss)) - - @cached_property - 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. - - Returns: - the list of audiences from this token, from the `aud` claim. - - Raises: - AttributeError: if the audience is an unexpected type - - """ - aud = self.get_claim("aud") - if aud is None: - return [] - if isinstance(aud, str): - return [aud] - if isinstance(aud, list): - return aud - msg = "aud has an unexpected type" - raise AttributeError(msg, type(aud)) - - @cached_property - def subject(self) -> str | None: - """Get the *Subject* (`sub`) from this token. - - Returns: - the subject, as `str`, or `None` if there is no `sub` claim - - Raises: - AttributeError: if the `sub` value is not a string - - """ - sub = self.get_claim("sub") - if sub is None or isinstance(sub, str): - return sub - msg = "sub has an unexpected type" - raise AttributeError(msg, type(sub)) - - @cached_property - def jwt_token_id(self) -> str | None: - """Get the *JWT Token ID* (`jti`) from this token. - - Returns: - the token identifier, as `str`, or `None` if there is no `jti` claim - - Raises: - AttributeError: if the `jti` value is not a string - - """ - jti = self.get_claim("jti") - if jti is None or isinstance(jti, str): - return jti - msg = "jti has an unexpected type" - raise AttributeError(msg, type(jti)) - - def get_claim(self, key: str, default: Any = None) -> Any: - """Get a claim by name from this Jwt. - - Args: - key: the claim name. - default: a default value if the claim is not found - - Returns: - the claim value if found, or `default` if not found - - """ - return self.claims.get(key, default) - - def __getitem__(self, item: str) -> Any: - """Allow access to claim by name with subscription. - - Args: - item: the claim name - - Returns: - the claim value - - """ - value = self.get_claim(item) - if value is None: - raise KeyError(item) - return value - - def __getattr__(self, item: str) -> Any: - """Allow claim access as attributes. - - Args: - item: the claim name - - Returns: - the claim value - - """ - value = self.get_claim(item) - if value is None: - raise AttributeError(item) - return value - - def __str__(self) -> str: - """Return the Jwt serialized value, as `str`. - - Returns: - the serialized token value. - - """ - return self.value.decode() - - def __bytes__(self) -> bytes: - """Return the Jwt serialized value, as `bytes`. - - Returns: - the serialized token value. - - """ - return self.value - - def validate( - self, - key: Jwk | dict[str, Any] | Any, - *, - alg: str | None = None, - algs: Iterable[str] | None = None, - issuer: str | None = None, - audience: None | str = None, - check_exp: bool = True, - **kwargs: Any, - ) -> None: - """Validate a `SignedJwt` signature and expected claims. - - This verifies the signature using the provided `jwk` and `alg`, then checks the token issuer, audience and - expiration date. - This can also check custom claims using extra `kwargs`, whose values can be: - - - a static value (`str`, `int`, etc.): the value from the token will be compared "as-is". - - a callable, taking the claim value as parameter: if that callable returns `True`, the claim is considered - as valid. - - Args: - key: the signing key to use to verify the signature. - alg: the signature alg to use to verify the signature. - algs: allowed signature algs, if several - issuer: the expected issuer for this token. - audience: the expected audience for this token. - check_exp: ìf `True` (default), check that the token is not expired. - **kwargs: additional claims to check - - Returns: - Raises exceptions if any validation check fails. - - Raises: - InvalidSignature: if the signature is not valid - InvalidClaim: if a claim doesn't validate - ExpiredJwt: if the expiration date is passed - - """ - self.verify(key, alg=alg, algs=algs) - - if issuer is not None and self.issuer != issuer: - msg = "Unexpected issuer" - raise InvalidClaim(msg, "iss", self.issuer) - - if audience is not None and (self.audiences is None or audience not in self.audiences): - msg = "Unexpected audience" - raise InvalidClaim(msg, "aud", self.audiences) - - if check_exp: - expired = self.is_expired() - if expired is True: - msg = f"This token expired at {self.expires_at}" - raise ExpiredJwt(msg) - elif expired is None: - msg = "This token does not contain an 'exp' claim." - raise InvalidClaim(msg, "exp") - - for key, value in kwargs.items(): - claim = self.get_claim(key) - if callable(value): - if not value(claim): - raise InvalidClaim( - key, - f"value of claim {key} doesn't validate with the provided validator", - claim, - ) - elif claim != value: - raise InvalidClaim(key, f"unexpected value for claim {key}", claim) - - def encrypt( - self, key: Any, enc: str, alg: str | None = None, extra_headers: dict[str, Any] | None = None - ) -> JweCompact: - """Encrypt this JWT into a JWE. - - The result is an encrypted (outer) JWT containing a signed (inner) JWT. - - Arguments: - key: the encryption key to use - enc: the encryption alg to use - alg: the key management alg to use - extra_headers: additional headers to include in the outer JWE. - - """ - extra_headers = extra_headers or {} - extra_headers.setdefault("cty", "JWT") - - jwe = JweCompact.encrypt(self, key, enc=enc, alg=alg, extra_headers=extra_headers) - return jwe +"""This modules contains classes and utilities to generate and validate signed JWT.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from functools import cached_property +from typing import Any, Iterable + +from binapy import BinaPy +from typing_extensions import Self + +from jwskate.jwe import JweCompact +from jwskate.jwk import Jwk, to_jwk +from jwskate.jws import InvalidSignature + +from .base import InvalidJwt, Jwt + + +class ExpiredJwt(ValueError): + """Raised when trying to validate an expired JWT token.""" + + +class InvalidClaim(ValueError): + """Raised when trying to validate a JWT with unexpected claims.""" + + +class SignedJwt(Jwt): + """Represent a Signed Json Web Token (JWT), as defined in RFC7519. + + A signed JWT contains a JSON object as payload, which represents claims. + + To sign a JWT, use [Jwt.sign][jwskate.jwt.Jwt.sign]. + + Args: + value: the token value. + + """ + + def __init__(self, value: bytes | str) -> None: + super().__init__(value) + + parts = BinaPy(self.value).split(b".") + if len(parts) != 3: # noqa: PLR2004 + msg = "A JWT must contain a header, a payload and a signature, separated by dots" + raise InvalidJwt( + msg, + value, + ) + + header, payload, signature = parts + try: + self.headers = header.decode_from("b64u").parse_from("json") + except ValueError as exc: + msg = "Invalid JWT header: it must be a Base64URL-encoded JSON object" + raise InvalidJwt(msg) from exc + + try: + self.claims = payload.decode_from("b64u").parse_from("json") + except ValueError as exc: + msg = "Invalid JWT payload: it must be a Base64URL-encoded JSON object" + raise InvalidJwt(msg) from exc + + try: + self.signature = signature.decode_from("b64u") + except ValueError as exc: + msg = "Invalid JWT signature: it must be a Base64URL-encoded binary data (bytes)" + raise InvalidJwt(msg) from exc + + @cached_property + def signed_part(self) -> bytes: + """Return the actual signed data from this token. + + The signed part is composed of the header and payload, encoded in Base64-Url, joined by a dot. + + Returns: + the signed part as bytes + + """ + return b".".join(self.value.split(b".", 2)[:2]) + + def verify_signature( + self, + key: Jwk | dict[str, Any] | Any, + alg: str | None = None, + algs: Iterable[str] | None = None, + ) -> bool: + """Verify this JWT signature using a given key and algorithm(s). + + Args: + key: the private Jwk to use to verify the signature + alg: the alg to use to verify the signature, if only 1 is allowed + algs: the allowed signature algs, if there are several + + Returns: + `True` if the token signature is verified, `False` otherwise + + """ + key = to_jwk(key) + + return key.verify(data=self.signed_part, signature=self.signature, alg=alg, algs=algs) + + def verify(self, key: Jwk | Any, *, alg: str | None = None, algs: Iterable[str] | None = None) -> Self: + """Convenience method to verify the signature inline. + + Returns `self` on success, raises an exception on failure. + + Raises: + InvalidSignature: if the signature does not verify. + + Return: + the same `SignedJwt`, if the signature is verified. + + Usage: + ```python + jwt = SignedJwt( + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJURVNUIn0.tIUFZqEZD12odEyBWscuxc4USspdYfJKhxPN0JXVMK97SUM69HrU5MGgocyyBbx1x9yIAkV7rNjcviqwGoVvsQ" + ).verify( + { + "kty": "EC", + "alg": "ES256", + "crv": "P-256", + "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU", + "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM", + } + ) + + # you can now do your business with this verified JWT: + assert jwt.claims == {"sub": "TEST"} + ``` + + """ + if self.verify_signature(key, alg=alg, algs=algs): + return self + raise InvalidSignature(data=self, key=key, alg=alg, algs=algs) + + def is_expired(self, leeway: int = 0) -> bool | None: + """Check if this token is expired, based on its `exp` claim. + + Args: + leeway: additional number of seconds for leeway. + + Returns: + `True` if the token is expired, `False` if it's not, `None` if there is no `exp` claim. + + """ + exp = self.expires_at + if exp is None: + return None + return exp < (datetime.now(timezone.utc) + timedelta(seconds=leeway)) + + @cached_property + def expires_at(self) -> datetime | None: + """Get the *Expires At* (`exp`) date from this token. + + Returns: + a `datetime` initialized from the `exp` claim, or `None` if there is no `exp` claim + + Raises: + AttributeError: if the `exp` claim cannot be parsed to a date + + """ + exp = self.get_claim("exp") + if not exp: + return None + try: + exp_dt = Jwt.timestamp_to_datetime(exp) + except (TypeError, OSError): + msg = "invalid `exp `claim" + raise AttributeError(msg, exp) from None + else: + return exp_dt + + @cached_property + def issued_at(self) -> datetime | None: + """Get the *Issued At* (`iat`) date from this token. + + Returns: + a `datetime` initialized from the `iat` claim, or `None` if there is no `iat` claim + + Raises: + AttributeError: if the `iss` claim cannot be parsed to a date + + """ + iat = self.get_claim("iat") + if not iat: + return None + try: + iat_dt = Jwt.timestamp_to_datetime(iat) + except (TypeError, OSError): + msg = "invalid `iat `claim" + raise AttributeError(msg, iat) from None + else: + return iat_dt + + @cached_property + def not_before(self) -> datetime | None: + """Get the *Not Before* (nbf) date from this token. + + Returns: + a `datetime` initialized from the `nbf` claim, or `None` if there is no `nbf` claim + + Raises: + AttributeError: if the `nbf` claim cannot be parsed to a date + + """ + nbf = self.get_claim("nbf") + if not nbf: + return None + try: + nbf_dt = Jwt.timestamp_to_datetime(nbf) + except (TypeError, OSError): + msg = "invalid `nbf `claim" + raise AttributeError(msg, nbf) from None + else: + return nbf_dt + + @cached_property + def issuer(self) -> str | None: + """Get the *Issuer* (`iss`) claim from this token. + + Returns: + the issuer, as `str`, or `None` if there is no `ìss` claim + + Raises: + AttributeError: if the `ìss` claim value is not a string + + """ + iss = self.get_claim("iss") + if iss is None or isinstance(iss, str): + return iss + msg = "iss has an unexpected type" + raise AttributeError(msg, type(iss)) + + @cached_property + 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. + + Returns: + the list of audiences from this token, from the `aud` claim. + + Raises: + AttributeError: if the audience is an unexpected type + + """ + aud = self.get_claim("aud") + if aud is None: + return [] + if isinstance(aud, str): + return [aud] + if isinstance(aud, list): + return aud + msg = "aud has an unexpected type" + raise AttributeError(msg, type(aud)) + + @cached_property + def subject(self) -> str | None: + """Get the *Subject* (`sub`) from this token. + + Returns: + the subject, as `str`, or `None` if there is no `sub` claim + + Raises: + AttributeError: if the `sub` value is not a string + + """ + sub = self.get_claim("sub") + if sub is None or isinstance(sub, str): + return sub + msg = "sub has an unexpected type" + raise AttributeError(msg, type(sub)) + + @cached_property + def jwt_token_id(self) -> str | None: + """Get the *JWT Token ID* (`jti`) from this token. + + Returns: + the token identifier, as `str`, or `None` if there is no `jti` claim + + Raises: + AttributeError: if the `jti` value is not a string + + """ + jti = self.get_claim("jti") + if jti is None or isinstance(jti, str): + return jti + msg = "jti has an unexpected type" + raise AttributeError(msg, type(jti)) + + def get_claim(self, key: str, default: Any = None) -> Any: + """Get a claim by name from this Jwt. + + Args: + key: the claim name. + default: a default value if the claim is not found + + Returns: + the claim value if found, or `default` if not found + + """ + return self.claims.get(key, default) + + def __getitem__(self, item: str) -> Any: + """Allow access to claim by name with subscription. + + Args: + item: the claim name + + Returns: + the claim value + + """ + value = self.get_claim(item) + if value is None: + raise KeyError(item) + return value + + def __getattr__(self, item: str) -> Any: + """Allow claim access as attributes. + + Args: + item: the claim name + + Returns: + the claim value + + """ + value = self.get_claim(item) + if value is None: + raise AttributeError(item) + return value + + def __str__(self) -> str: + """Return the Jwt serialized value, as `str`. + + Returns: + the serialized token value. + + """ + return self.value.decode() + + def __bytes__(self) -> bytes: + """Return the Jwt serialized value, as `bytes`. + + Returns: + the serialized token value. + + """ + return self.value + + def validate( + self, + key: Jwk | dict[str, Any] | Any, + *, + alg: str | None = None, + algs: Iterable[str] | None = None, + issuer: str | None = None, + audience: None | str = None, + check_exp: bool = True, + **kwargs: Any, + ) -> None: + """Validate a `SignedJwt` signature and expected claims. + + This verifies the signature using the provided `jwk` and `alg`, then checks the token issuer, audience and + expiration date. + This can also check custom claims using extra `kwargs`, whose values can be: + + - a static value (`str`, `int`, etc.): the value from the token will be compared "as-is". + - a callable, taking the claim value as parameter: if that callable returns `True`, the claim is considered + as valid. + + Args: + key: the signing key to use to verify the signature. + alg: the signature alg to use to verify the signature. + algs: allowed signature algs, if several + issuer: the expected issuer for this token. + audience: the expected audience for this token. + check_exp: ìf `True` (default), check that the token is not expired. + **kwargs: additional claims to check + + Returns: + Raises exceptions if any validation check fails. + + Raises: + InvalidSignature: if the signature is not valid + InvalidClaim: if a claim doesn't validate + ExpiredJwt: if the expiration date is passed + + """ + self.verify(key, alg=alg, algs=algs) + + if issuer is not None and self.issuer != issuer: + msg = "Unexpected issuer" + raise InvalidClaim(msg, "iss", self.issuer) + + if audience is not None and (self.audiences is None or audience not in self.audiences): + msg = "Unexpected audience" + raise InvalidClaim(msg, "aud", self.audiences) + + if check_exp: + expired = self.is_expired() + if expired is True: + msg = f"This token expired at {self.expires_at}" + raise ExpiredJwt(msg) + elif expired is None: + msg = "This token does not contain an 'exp' claim." + raise InvalidClaim(msg, "exp") + + for key, value in kwargs.items(): + claim = self.get_claim(key) + if callable(value): + if not value(claim): + raise InvalidClaim( + key, + f"value of claim {key} doesn't validate with the provided validator", + claim, + ) + elif claim != value: + raise InvalidClaim(key, f"unexpected value for claim {key}", claim) + + def encrypt( + self, key: Any, enc: str, alg: str | None = None, extra_headers: dict[str, Any] | None = None + ) -> JweCompact: + """Encrypt this JWT into a JWE. + + The result is an encrypted (outer) JWT containing a signed (inner) JWT. + + Arguments: + key: the encryption key to use + enc: the encryption alg to use + alg: the key management alg to use + extra_headers: additional headers to include in the outer JWE. + + """ + extra_headers = extra_headers or {} + extra_headers.setdefault("cty", "JWT") + + jwe = JweCompact.encrypt(self, key, enc=enc, alg=alg, extra_headers=extra_headers) + return jwe diff --git a/jwskate/jwt/signer.py b/jwskate/jwt/signer.py index fc867e4..a53b46d 100644 --- a/jwskate/jwt/signer.py +++ b/jwskate/jwt/signer.py @@ -23,6 +23,7 @@ ``` """ + from __future__ import annotations import uuid @@ -36,13 +37,13 @@ class JwtSigner: - """A helper class to easily sign JWTs with standardised claims. + """A helper class to easily sign JWTs with standardized claims. - The standardised claims include: + The standardized claims include: - - `ìat`: issued at date + - `Ìat`: issued at date - `exp`: expiration date - - `nbf`: not before date: + - `nbf`: not before date - `iss`: issuer identifier - `sub`: subject identifier - `aud`: audience identifier @@ -56,7 +57,7 @@ class JwtSigner: Args: issuer: the issuer string to use as `ìss` claim for signed tokens. - jwk: the private Jwk to use to sign tokens. + key: the private Jwk to use to sign tokens. alg: the signing alg to use to sign tokens. default_lifetime: the default lifetime, in seconds, to use for claim `exp`. This can be overridden when calling `.sign()` @@ -67,20 +68,22 @@ class JwtSigner: def __init__( self, - issuer: str, - jwk: Jwk, + key: Jwk | Any, + *, + issuer: str | None = None, alg: str | None = None, default_lifetime: int = 60, default_leeway: int | None = None, ): self.issuer = issuer - self.jwk = jwk - self.alg = jwk.alg or alg + self.jwk = Jwk(key) + self.alg = alg self.default_lifetime = default_lifetime self.default_leeway = default_leeway def sign( self, + *, subject: str | None = None, audience: str | Iterable[str] | None = None, extra_claims: dict[str, Any] | None = None, @@ -145,6 +148,7 @@ def generate_jti(self) -> str: @classmethod def with_random_key( cls, + *, issuer: str, alg: str, default_lifetime: int = 60, @@ -165,15 +169,16 @@ def with_random_key( """ jwk = Jwk.generate_for_alg(alg, kid=kid).with_kid_thumbprint() - return cls(issuer, jwk, alg, default_lifetime, default_leeway) + return cls(issuer=issuer, key=jwk, alg=alg, default_lifetime=default_lifetime, default_leeway=default_leeway) def verifier( self, + *, audience: str, verifiers: Iterable[Callable[[SignedJwt], None]] | None = None, **kwargs: Any, ) -> JwtVerifier: - """Return the matching JwtVerifier, initialized with the public key.""" + """Return the matching `JwtVerifier`, initialized with the public key.""" return JwtVerifier( issuer=self.issuer, jwkset=self.jwk.public_jwk().as_jwks(), diff --git a/jwskate/jwt/verifier.py b/jwskate/jwt/verifier.py index 02def72..06d57d8 100644 --- a/jwskate/jwt/verifier.py +++ b/jwskate/jwt/verifier.py @@ -1,11 +1,12 @@ """High-Level facility to verify JWT tokens signature, validity dates, issuer, audiences, etc.""" + from __future__ import annotations from typing import Any, Callable, Iterable -from jwskate import Jwk, JwkSet +from jwskate import InvalidSignature, Jwk, JwkSet -from .signed import ExpiredJwt, InvalidClaim, InvalidSignature, SignedJwt +from .signed import ExpiredJwt, InvalidClaim, SignedJwt class JwtVerifier: @@ -102,7 +103,7 @@ def verify(self, jwt: SignedJwt | str) -> None: if jwt.verify_signature(jwk, alg=self.alg, algs=self.algs): break else: - raise InvalidSignature(jwt=jwt, key=self.jwkset, alg=self.alg, algs=self.algs) + raise InvalidSignature(data=jwt, key=self.jwkset, alg=self.alg, algs=self.algs) if jwt.is_expired(self.leeway): msg = f"Jwt token expired at {jwt.expires_at}" diff --git a/jwskate/token.py b/jwskate/token.py index 6d4f96c..89556e6 100644 --- a/jwskate/token.py +++ b/jwskate/token.py @@ -1,4 +1,5 @@ """This module contains base classes for all tokens types handled by `jwskate`.""" + from __future__ import annotations import json @@ -81,7 +82,7 @@ def alg(self) -> str: if alg is None or not isinstance(alg, str): # pragma: no branch msg = "This token doesn't have a valid 'alg' header" raise AttributeError(msg) - return alg + return alg # type: ignore[no-any-return] @cached_property def kid(self) -> str: @@ -97,7 +98,7 @@ def kid(self) -> str: if kid is None or not isinstance(kid, str): msg = "This token doesn't have a valid 'kid' header" raise AttributeError(msg) - return kid + return kid # type: ignore[no-any-return] @cached_property def typ(self) -> str: @@ -113,7 +114,7 @@ def typ(self) -> str: if typ is None or not isinstance(typ, str): # pragma: no branch msg = "This token doesn't have a valid 'typ' header" raise AttributeError(msg) - return typ + return typ # type: ignore[no-any-return] @cached_property def cty(self) -> str: @@ -129,7 +130,7 @@ def cty(self) -> str: if cty is None or not isinstance(cty, str): # pragma: no branch msg = "This token doesn't have a valid 'cty' header" raise AttributeError(msg) - return cty + return cty # type: ignore[no-any-return] def __repr__(self) -> str: """Return the `str` representation of this token.""" diff --git a/pyproject.toml b/pyproject.toml index 6a0a2f6..ff44f7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,6 +208,7 @@ ignore = [ "N818", # Exception names should be named with an Error suffix "PLR0912", # Too many branches "D107", # Missing docstring in `__init__` + "ISC001", # Implicitly concatenated string literals on one line ] exclude = [ "tests" diff --git a/tests/__init__.py b/tests/__init__.py index 7beecc4..0f0361b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,2 +1,3 @@ """Unit test package for jwskate.""" + from __future__ import annotations diff --git a/tests/conftest.py b/tests/conftest.py index 82dc42f..854d9f7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for all tests.""" + from __future__ import annotations import pytest @@ -21,18 +22,16 @@ def kid() -> str: @pytest.fixture() def private_jwk(kid: str) -> Jwk: """Return a private Jwk, usable for signing or decryption.""" - return Jwk( - { - "kty": "RSA", - "kid": kid, - "alg": "RS256", - "n": "2jgK-5aws3_fjllgnAacPkwjbz3RCeAHni1pcHvReuTgk9qEiTmXWJiSS_F20VeI1zEwFM36e836ROCyOQ8cjjaPWpdzCajWC0koY7X8MPhZbdoSptOmDBseRCyYqmeMCp8mTTOD6Cs43SiIYSMNlPuio89qjf_4u32eVF_5YqOGtwfzC4p2NUPPCxpljYpAcf2BBG1tRX1mY4WP_8zwmx3ZH7Sy0V_fXI46tzDqfRXdMhHW7ARJAnEr_EJhlMgUaM7FUQKUNpi1ZdeeLxYv44eRx9-Roy5zTG1b0yRuaKaAG3559572quOcxISZzK5Iy7BhE7zxVa9jabEl-Y1Daw", - "e": "AQAB", - "d": "XCtpsCRQ1DBBm51yqdQ88C82lEjW30Xp0cy6iVEzBKZhmPGmI1PY8gnXWQ5PMlK3sLTM6yypDNvORoNlo6YXWJYA7LGlXEIczj2DOsJmF8T9-OEwGZixvNFDcmYnwWnlA6N_CQKmR0ziQr9ZAzZMCU5Tvr7f8cRZKdAALQEwk5FYpLnEbXOBduJtY9x2kddJSCJwRaEJhx0fG_pJAO3yLUZBY20dZK8UrxDoCgB9eiZV3N4uWGt367r1MDdaxGY6l6bC1HZCHkttBuTxfSUMCgooZevdU6ThQNpFrwZNY3KoP-OksEdqMs-neecfk_AQREkubDW2VPNFnaVEa38BKQ", - "p": "8QNZGwUINpkuZi8l2ZfQzKVeOeNe3aQ7UW0wperM-63DFEJDRO1UyNC1n6yeo8_RxPZKSTlr6xZDoilQq23mopeF6O0ZmYz6E2VWJuma65V-A7tB-6xjqUXPlSkCNA6Ia8kMeCmNpKs0r0ijTBf_2y2GSsNH4EcP7XzcDEeJIh0", - "q": "58nWgg-qRorRddwKM7qhLxJnEDsnCiYhbKJrP78OfBZ-839bNRvL5D5sfjJqxcKMQidgpYZVvVNL8oDEywcC5T7kKW0HK1JUdYiX9DuI40Mv9WzXQ8B8FBjp5wV4IX6_0KgyIiyoUiKpVHBvO0YFPUYuk0Ns4H9yEws93RWwhSc", - "dp": "zFsLZcaphSnzVr9pd4urhqo9MBZjbMmBZnSQCE8ECe729ymMQlh-SFv3dHF4feuLsVcn-9iNceMJ6-jeNs1T_s89wxevWixYKrQFDa-MJW83T1CrDQvJ4VCJR69i5-Let43cXdLWACcO4AVWOQIsdpquQJw-SKPYlIUHS_4n_90", - "dq": "fP79rNnhy3TlDBgDcG3-qjHUXo5nuTNi5wCXsaLInuZKw-k0OGmrBIUdYNizd744gRxXJCxTZGvdEwOaHJrFVvcZd7WSHiyh21g0CcNpSJVc8Y8mbyUIRJZC3RC3_egqbM2na4KFqvWCN0UC1wYloSuNxmCgAFj6HYb8b5NYxBU", - "qi": "hxXfLYgwrfZBvZ27nrPsm6mLuoO-V2rKdOj3-YDJzf0gnVGBLl0DZbgydZ8WZmSLn2290mO_J8XY-Ss8PjLYbz3JXPDNLMJ-da3iEPKTvh6OfliM_dBxhaW8sq5afLMUR0H8NeabbWkfPz5h0W11CCBYxsyPC6CzniFYCYXfByU", - } - ) + return Jwk({ + "kty": "RSA", + "kid": kid, + "alg": "RS256", + "n": "2jgK-5aws3_fjllgnAacPkwjbz3RCeAHni1pcHvReuTgk9qEiTmXWJiSS_F20VeI1zEwFM36e836ROCyOQ8cjjaPWpdzCajWC0koY7X8MPhZbdoSptOmDBseRCyYqmeMCp8mTTOD6Cs43SiIYSMNlPuio89qjf_4u32eVF_5YqOGtwfzC4p2NUPPCxpljYpAcf2BBG1tRX1mY4WP_8zwmx3ZH7Sy0V_fXI46tzDqfRXdMhHW7ARJAnEr_EJhlMgUaM7FUQKUNpi1ZdeeLxYv44eRx9-Roy5zTG1b0yRuaKaAG3559572quOcxISZzK5Iy7BhE7zxVa9jabEl-Y1Daw", + "e": "AQAB", + "d": "XCtpsCRQ1DBBm51yqdQ88C82lEjW30Xp0cy6iVEzBKZhmPGmI1PY8gnXWQ5PMlK3sLTM6yypDNvORoNlo6YXWJYA7LGlXEIczj2DOsJmF8T9-OEwGZixvNFDcmYnwWnlA6N_CQKmR0ziQr9ZAzZMCU5Tvr7f8cRZKdAALQEwk5FYpLnEbXOBduJtY9x2kddJSCJwRaEJhx0fG_pJAO3yLUZBY20dZK8UrxDoCgB9eiZV3N4uWGt367r1MDdaxGY6l6bC1HZCHkttBuTxfSUMCgooZevdU6ThQNpFrwZNY3KoP-OksEdqMs-neecfk_AQREkubDW2VPNFnaVEa38BKQ", + "p": "8QNZGwUINpkuZi8l2ZfQzKVeOeNe3aQ7UW0wperM-63DFEJDRO1UyNC1n6yeo8_RxPZKSTlr6xZDoilQq23mopeF6O0ZmYz6E2VWJuma65V-A7tB-6xjqUXPlSkCNA6Ia8kMeCmNpKs0r0ijTBf_2y2GSsNH4EcP7XzcDEeJIh0", + "q": "58nWgg-qRorRddwKM7qhLxJnEDsnCiYhbKJrP78OfBZ-839bNRvL5D5sfjJqxcKMQidgpYZVvVNL8oDEywcC5T7kKW0HK1JUdYiX9DuI40Mv9WzXQ8B8FBjp5wV4IX6_0KgyIiyoUiKpVHBvO0YFPUYuk0Ns4H9yEws93RWwhSc", + "dp": "zFsLZcaphSnzVr9pd4urhqo9MBZjbMmBZnSQCE8ECe729ymMQlh-SFv3dHF4feuLsVcn-9iNceMJ6-jeNs1T_s89wxevWixYKrQFDa-MJW83T1CrDQvJ4VCJR69i5-Let43cXdLWACcO4AVWOQIsdpquQJw-SKPYlIUHS_4n_90", + "dq": "fP79rNnhy3TlDBgDcG3-qjHUXo5nuTNi5wCXsaLInuZKw-k0OGmrBIUdYNizd744gRxXJCxTZGvdEwOaHJrFVvcZd7WSHiyh21g0CcNpSJVc8Y8mbyUIRJZC3RC3_egqbM2na4KFqvWCN0UC1wYloSuNxmCgAFj6HYb8b5NYxBU", + "qi": "hxXfLYgwrfZBvZ27nrPsm6mLuoO-V2rKdOj3-YDJzf0gnVGBLl0DZbgydZ8WZmSLn2290mO_J8XY-Ss8PjLYbz3JXPDNLMJ-da3iEPKTvh6OfliM_dBxhaW8sq5afLMUR0H8NeabbWkfPz5h0W11CCBYxsyPC6CzniFYCYXfByU", + }) diff --git a/tests/test_jwa/test_encryption.py b/tests/test_jwa/test_encryption.py index c2a5725..f2cf9d7 100644 --- a/tests/test_jwa/test_encryption.py +++ b/tests/test_jwa/test_encryption.py @@ -1,4 +1,5 @@ """Tests for jwskate.jwa.encryption submodule.""" + from __future__ import annotations import pytest diff --git a/tests/test_jwa/test_examples.py b/tests/test_jwa/test_examples.py index 8dc0c00..d6e88bc 100644 --- a/tests/test_jwa/test_examples.py +++ b/tests/test_jwa/test_examples.py @@ -1,4 +1,5 @@ """Tests for the jwkskate.jwa submodule.""" + from __future__ import annotations from binapy import BinaPy @@ -123,24 +124,20 @@ def test_aes_192_hmac_sha384() -> None: def test_ecdhes() -> None: """Test derived from [RFC7518](https://datatracker.ietf.org/doc/html/rfc7518#appendix-C).""" - alice_ephemeral_key = Jwk( - { - "kty": "EC", - "crv": "P-256", - "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", - "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", - "d": "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo", - } - ) - bob_private_key = Jwk( - { - "kty": "EC", - "crv": "P-256", - "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", - "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", - "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw", - } - ) + alice_ephemeral_key = Jwk({ + "kty": "EC", + "crv": "P-256", + "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", + "d": "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo", + }) + bob_private_key = Jwk({ + "kty": "EC", + "crv": "P-256", + "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", + "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw", + }) otherinfo = EcdhEs.otherinfo(alg="A128GCM", apu=b"Alice", apv=b"Bob", key_size=128) alice_cek = EcdhEs.derive( diff --git a/tests/test_jwa/test_key_mgmt.py b/tests/test_jwa/test_key_mgmt.py index aaa0905..e41bdf6 100644 --- a/tests/test_jwa/test_key_mgmt.py +++ b/tests/test_jwa/test_key_mgmt.py @@ -1,4 +1,5 @@ """Tests for jwskate.jwa.key_mgmt submodule.""" + from __future__ import annotations import pytest @@ -18,7 +19,7 @@ ], ) def test_ecdhes( - key_gen: (type[ec.EllipticCurvePrivateKey] | type[x25519.X25519PrivateKey] | type[x448.X448PrivateKey]), + key_gen: type[ec.EllipticCurvePrivateKey] | type[x25519.X25519PrivateKey] | type[x448.X448PrivateKey], ) -> None: private_key = key_gen() sender_ecdhes = EcdhEs(private_key.public_key()) diff --git a/tests/test_jwa/test_signature.py b/tests/test_jwa/test_signature.py index 90fab47..245ce5f 100644 --- a/tests/test_jwa/test_signature.py +++ b/tests/test_jwa/test_signature.py @@ -1,4 +1,5 @@ """Tests for jwskate.jwa.signature submodule.""" + from __future__ import annotations import pytest diff --git a/tests/test_jwe.py b/tests/test_jwe.py index 8887038..b9fabc6 100644 --- a/tests/test_jwe.py +++ b/tests/test_jwe.py @@ -25,53 +25,51 @@ def test_jwe() -> None: alg = "RSA-OAEP" enc = "A256GCM" cek = bytes.fromhex("b1a1f480548fe1733fb403ff6b9ad4f68a076e5b702e22692f82cb2e7aea40fc") - jwk = Jwk( - { - "kty": "RSA", - "n": ( - "oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" - "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" - "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" - "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" - "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" - "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw" - ), - "e": "AQAB", - "d": ( - "kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" - "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" - "3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk" - "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" - "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" - "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ" - ), - "p": ( - "1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-" - "SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf" - "fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0" - ), - "q": ( - "wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm" - "UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX" - "IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc" - ), - "dp": ( - "ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL" - "hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827" - "rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE" - ), - "dq": ( - "Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj" - "ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB" - "UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis" - ), - "qi": ( - "VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7" - "AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3" - "eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY" - ), - } - ) + jwk = Jwk({ + "kty": "RSA", + "n": ( + "oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw" + ), + "e": "AQAB", + "d": ( + "kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + "3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk" + "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ" + ), + "p": ( + "1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-" + "SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf" + "fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0" + ), + "q": ( + "wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm" + "UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX" + "IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc" + ), + "dp": ( + "ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL" + "hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827" + "rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE" + ), + "dq": ( + "Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj" + "ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB" + "UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis" + ), + "qi": ( + "VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7" + "AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3" + "eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY" + ), + }) iv = bytes.fromhex("e3c575fc02dbe944b4e14ddb") jwe = JweCompact.encrypt(plaintext, jwk.public_jwk(), alg=alg, enc=enc, cek=cek, iv=iv) @@ -100,53 +98,51 @@ def test_jwe_decrypt() -> None: "XFBoMYUZodetZdvTiFvSkQ" ) - jwk = Jwk( - { - "kty": "RSA", - "n": ( - "oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" - "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" - "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" - "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" - "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" - "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw" - ), - "e": "AQAB", - "d": ( - "kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" - "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" - "3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk" - "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" - "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" - "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ" - ), - "p": ( - "1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-" - "SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf" - "fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0" - ), - "q": ( - "wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm" - "UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX" - "IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc" - ), - "dp": ( - "ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL" - "hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827" - "rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE" - ), - "dq": ( - "Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj" - "ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB" - "UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis" - ), - "qi": ( - "VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7" - "AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3" - "eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY" - ), - } - ) + jwk = Jwk({ + "kty": "RSA", + "n": ( + "oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw" + ), + "e": "AQAB", + "d": ( + "kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + "3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk" + "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ" + ), + "p": ( + "1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-" + "SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf" + "fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0" + ), + "q": ( + "wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm" + "UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX" + "IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc" + ), + "dp": ( + "ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL" + "hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827" + "rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE" + ), + "dq": ( + "Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj" + "ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB" + "UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis" + ), + "qi": ( + "VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7" + "AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3" + "eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY" + ), + }) plaintext = b"The true sign of intelligence is not knowledge but imagination." alg = "RSA-OAEP" @@ -636,9 +632,9 @@ def test_decrypt_by_jwcrypto( encryption_alg: the encryption alg """ - import jwcrypto.jwe # type: ignore[import] - import jwcrypto.jwk # type: ignore[import] - from jwcrypto.common import InvalidJWEOperation, json_encode # type: ignore[import] + import jwcrypto.jwe # type: ignore[import-untyped] + import jwcrypto.jwk # type: ignore[import-untyped] + from jwcrypto.common import InvalidJWEOperation, json_encode # type: ignore[import-untyped] if key_management_alg in JWCRYPTO_UNSUPPORTED_ALGS: pytest.skip(f"jwcrypto doesn't support key management alg {key_management_alg}") diff --git a/tests/test_jwk/test_ec.py b/tests/test_jwk/test_ec.py index 92ef3ec..8ace855 100644 --- a/tests/test_jwk/test_ec.py +++ b/tests/test_jwk/test_ec.py @@ -113,15 +113,13 @@ def test_pem_key(crv: str) -> None: def test_public_private() -> None: - jwk = Jwk( - { - "kty": "EC", - "crv": "P-256", - "x": "WtjnvHG9b_IKBLn4QYTHz-AdoAiO_ork5LH1BL_5tyI", - "y": "C0YfOUDuCOvTCt7hAqO-f9z8_JdOnOPbfYmUk-RosHA", - "d": "EnGZlkoa4VUsnl72LcRRychNJ2FFknm_ph855tNuPZ8", - } - ) + jwk = Jwk({ + "kty": "EC", + "crv": "P-256", + "x": "WtjnvHG9b_IKBLn4QYTHz-AdoAiO_ork5LH1BL_5tyI", + "y": "C0YfOUDuCOvTCt7hAqO-f9z8_JdOnOPbfYmUk-RosHA", + "d": "EnGZlkoa4VUsnl72LcRRychNJ2FFknm_ph855tNuPZ8", + }) assert ( ECJwk.public( diff --git a/tests/test_jwk/test_jwk.py b/tests/test_jwk/test_jwk.py index 218a152..16a0e8f 100644 --- a/tests/test_jwk/test_jwk.py +++ b/tests/test_jwk/test_jwk.py @@ -61,14 +61,12 @@ def test_invalid_jwk() -> None: with pytest.raises(InvalidJwk): # attribute 'd' (private exponent) is missing - Jwk( - { - "kty": "RSA", - "n": "oRHn4oGv23ylRL3RSsL4p_e6Ywinnj2N2tT5OLe5pEZTg-LFBhjFxcJaB-p1dh6XX47EtSfa-JHffU0o5ZRK2ySyNDtlrFAkOpAHH6U83ayE2QPYGzrFrrvHDa8wIMUWymzxpPwGgKBwZZqtTT6d-iy4Ux3AWV-bUv6Z7WijHnOy7aVzZ4dFERLVf2FaaYXDET7GO4v-oQ5ss_guYdmewN039jxkjz_KrA-0Fyhalf9hL8IHfpdpSlHosrmjORG5y9LkYK0J6zxSBF5ZvLIBK33BTzPPiCMwKLyAcV6qdcAcvV4kthKO0iUKBK4eE8D0N8HcSPvA9F_PpLS_k5F2lw", - "e": "AQAB", - "p": "0mzP9sbFxU5YxNNLgUEdRQSO-ojqWrzbI02PfQLGyzXumvOh_Qr73OpHStU8CAAcUBaQdRGidsVdb5cq6JG2zvbEEYiX-dCHqTJs8wfktGCL7eV-ZVh7fhJ1sYVBN20yv8aSH63uUPZnJXR1AUyrvRumuerdPxp8X951PESrJd0", - } - ) + Jwk({ + "kty": "RSA", + "n": "oRHn4oGv23ylRL3RSsL4p_e6Ywinnj2N2tT5OLe5pEZTg-LFBhjFxcJaB-p1dh6XX47EtSfa-JHffU0o5ZRK2ySyNDtlrFAkOpAHH6U83ayE2QPYGzrFrrvHDa8wIMUWymzxpPwGgKBwZZqtTT6d-iy4Ux3AWV-bUv6Z7WijHnOy7aVzZ4dFERLVf2FaaYXDET7GO4v-oQ5ss_guYdmewN039jxkjz_KrA-0Fyhalf9hL8IHfpdpSlHosrmjORG5y9LkYK0J6zxSBF5ZvLIBK33BTzPPiCMwKLyAcV6qdcAcvV4kthKO0iUKBK4eE8D0N8HcSPvA9F_PpLS_k5F2lw", + "e": "AQAB", + "p": "0mzP9sbFxU5YxNNLgUEdRQSO-ojqWrzbI02PfQLGyzXumvOh_Qr73OpHStU8CAAcUBaQdRGidsVdb5cq6JG2zvbEEYiX-dCHqTJs8wfktGCL7eV-ZVh7fhJ1sYVBN20yv8aSH63uUPZnJXR1AUyrvRumuerdPxp8X951PESrJd0", + }) with pytest.raises(InvalidJwk): # k is not a str @@ -84,15 +82,13 @@ def test_invalid_jwk() -> None: with pytest.raises(InvalidJwk): # key is public and has key_ops: ["sign"] - Jwk( - { - "kty": "EC", - "key_ops": ["sign"], - "crv": "P-256", - "x": "vGVh-60pT34a0JLeiaers66I0JLRilpf5tbnZsa-q3U", - "y": "y99gwPgQH1lrIBQPwgJoHCoeQjF96M7XfxGXu_Pjyzk", - } - ) + Jwk({ + "kty": "EC", + "key_ops": ["sign"], + "crv": "P-256", + "x": "vGVh-60pT34a0JLeiaers66I0JLRilpf5tbnZsa-q3U", + "y": "y99gwPgQH1lrIBQPwgJoHCoeQjF96M7XfxGXu_Pjyzk", + }) with pytest.raises(TypeError): Jwk.from_cryptography_key(object()) @@ -185,16 +181,14 @@ def test_invalid_params() -> None: Jwk({"kty": "oct", "k": "foobar", "kid": 1.34}).kid with pytest.raises(InvalidJwk): - Jwk( - { - "kty": "EC", - "crv": "P-256", - "x": "SOlwe9_nRwz2f9Y2aSB9d7D-AXTSSBlAQd5HZUIEGLA", - "y": "Pzk9Gd4wwbx9STkK_RfWqxnfU9AwpvWZzf_K0GpaQZo", - "d": "Invalid-private-key--EHzgbRaNKRCMhk6jiCT-ZQ", - "alg": "ES256", - } - ) + Jwk({ + "kty": "EC", + "crv": "P-256", + "x": "SOlwe9_nRwz2f9Y2aSB9d7D-AXTSSBlAQd5HZUIEGLA", + "y": "Pzk9Gd4wwbx9STkK_RfWqxnfU9AwpvWZzf_K0GpaQZo", + "d": "Invalid-private-key--EHzgbRaNKRCMhk6jiCT-ZQ", + "alg": "ES256", + }) def test_invalid_class_for_kty() -> None: @@ -246,22 +240,20 @@ def test_use_key_ops_with_alg(alg: str, use: str, private_key_ops: tuple[str], p def test_thumbprint() -> None: # key from https://www.rfc-editor.org/rfc/rfc7638.html#section-3.1 - jwk = Jwk( - { - "kty": "RSA", - "n": ( - "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" - "VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" - "4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" - "W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" - "1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" - "aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw" - ), - "e": "AQAB", - "alg": "RS256", - "kid": "2011-04-29", - } - ) + jwk = Jwk({ + "kty": "RSA", + "n": ( + "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" + "VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" + "4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" + "W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" + "1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" + "aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw" + ), + "e": "AQAB", + "alg": "RS256", + "kid": "2011-04-29", + }) assert jwk.thumbprint() == "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" assert ( diff --git a/tests/test_jwk/test_jwks.py b/tests/test_jwk/test_jwks.py index 2f735dd..1ba89ee 100644 --- a/tests/test_jwk/test_jwks.py +++ b/tests/test_jwk/test_jwks.py @@ -90,49 +90,47 @@ def test_empty_jwkset() -> None: 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", - }, - ] - } - ) + 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() diff --git a/tests/test_jwk/test_okp.py b/tests/test_jwk/test_okp.py index 0c59b8c..953486d 100644 --- a/tests/test_jwk/test_okp.py +++ b/tests/test_jwk/test_okp.py @@ -47,14 +47,12 @@ def test_generate_unsupported() -> None: def test_rfc8037_ed25519() -> None: """Test from [RFC8037][https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A].""" - jwk = Jwk( - { - "kty": "OKP", - "crv": "Ed25519", - "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", - "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", - } - ) + jwk = Jwk({ + "kty": "OKP", + "crv": "Ed25519", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + }) assert isinstance(jwk, OKPJwk) assert jwk.is_private assert jwk.private_key == bytes.fromhex(""" @@ -87,14 +85,12 @@ def test_rfc8037_ed25519() -> None: def test_rfc8037_x25519() -> None: """Test from [RFC8037 $A.6][https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.6].""" - public_jwk = Jwk( - { - "kty": "OKP", - "crv": "X25519", - "kid": "Bob", - "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08", - } - ) + public_jwk = Jwk({ + "kty": "OKP", + "crv": "X25519", + "kid": "Bob", + "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08", + }) assert isinstance(public_jwk, OKPJwk) assert public_jwk.public_key == bytes.fromhex("""de 9e db 7d 7b 7d c1 b4 d3 5b 61 c2 ec e4 35 37 3f 83 43 c8 5b 78 67 4d ad fc 7e 14 6f 88 2b 4f""") @@ -104,13 +100,11 @@ def test_rfc8037_x25519() -> None: ephemeral_private_key = OKPJwk.from_bytes(ephemeral_secret, use="enc") - assert ephemeral_private_key.public_jwk() == Jwk( - { - "kty": "OKP", - "crv": "X25519", - "x": "hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo", - } - ) + assert ephemeral_private_key.public_jwk() == Jwk({ + "kty": "OKP", + "crv": "X25519", + "x": "hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo", + }) ephemeral_private_key["kid"] = "Bob" @@ -124,14 +118,12 @@ def test_rfc8037_x25519() -> None: def test_rfc8037_x448() -> None: """Test from [RFC8037 $A.7][https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.7].""" - public_jwk = Jwk( - { - "kty": "OKP", - "crv": "X448", - "kid": "Dave", - "x": "PreoKbDNIPW8_AtZm2_sz22kYnEHvbDU80W0MCfYuXL8PjT7QjKhPKcG3LV67D2uB73BxnvzNgk", - } - ) + public_jwk = Jwk({ + "kty": "OKP", + "crv": "X448", + "kid": "Dave", + "x": "PreoKbDNIPW8_AtZm2_sz22kYnEHvbDU80W0MCfYuXL8PjT7QjKhPKcG3LV67D2uB73BxnvzNgk", + }) assert isinstance(public_jwk, OKPJwk) assert public_jwk.public_key == bytes.fromhex(""" 3e b7 a8 29 b0 cd 20 f5 bc fc 0b 59 9b 6f ec cf @@ -147,13 +139,11 @@ def test_rfc8037_x448() -> None: ephemeral_private_key = OKPJwk.from_bytes(ephemeral_secret, use="enc") - assert ephemeral_private_key.public_jwk() == Jwk( - { - "kty": "OKP", - "crv": "X448", - "x": "mwj3zDG34-Z9ItWuoSEHSic70rg94Jxj-qc9LCLF2bvINmRyQdlT1AxbEtqIEg1TF3-A5TLEH6A", - } - ) + assert ephemeral_private_key.public_jwk() == Jwk({ + "kty": "OKP", + "crv": "X448", + "x": "mwj3zDG34-Z9ItWuoSEHSic70rg94Jxj-qc9LCLF2bvINmRyQdlT1AxbEtqIEg1TF3-A5TLEH6A", + }) ephemeral_private_key["kid"] = "Bob" @@ -302,14 +292,12 @@ def test_from_bytes(private_key: bytes, crv: str, use: str) -> None: def test_public_private() -> None: - jwk = Jwk( - { - "kty": "OKP", - "crv": "Ed25519", - "x": "SghwA3Kg8e1Z2v1xnfnexH7OE4G-cd1z__Q64RQR4EQ", - "d": "V7P8eIm8sZsvIlhOXMLiamWTUW68wpyFyW_1QrnzkAI", - } - ) + jwk = Jwk({ + "kty": "OKP", + "crv": "Ed25519", + "x": "SghwA3Kg8e1Z2v1xnfnexH7OE4G-cd1z__Q64RQR4EQ", + "d": "V7P8eIm8sZsvIlhOXMLiamWTUW68wpyFyW_1QrnzkAI", + }) assert ( OKPJwk.public( diff --git a/tests/test_jwk/test_rsa.py b/tests/test_jwk/test_rsa.py index dbc35e3..fbf25b7 100644 --- a/tests/test_jwk/test_rsa.py +++ b/tests/test_jwk/test_rsa.py @@ -154,14 +154,12 @@ def test_pem_key(key_size: int) -> None: 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", - } - ) + 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 @@ -253,14 +251,12 @@ def test_from_cryptography_key() -> None: def test_public_private() -> None: - jwk = Jwk( - { - "kty": "RSA", - "n": "jp5HPGxg9n6u0HhJaNuE_hzQIPJ_AUfalQ5rw3gZpRNTCNSIQ2aNyIwzyt58zt-YUttb5OirSkhwuiTBm--U4jHJdg57Yy4m_r06ps-3w08HI8XWh6MSAeQwiovtGLSvQ76WGdkwTSeVcST_G9qy3osJdc9xTNwyrPyyfxDef6h8s6_v2GS-xXrKzYy78eoGMezw4-uetIWV5aVfuQ3QpkVo8hN2G0YKEAwRR_zbrzY7HUiCdvbdX-RglT94MOU-_QjUcP1rBYn1z8nOZ0uA_E9UbEnIMTR0IptepaD5NpqDnYFqm_FfOhDw5-hwMxuHw-rHYnvYY78bE4g3RrV8HGMe2CltveUy6VxR4PIJ0-OY7La9ycsSWMDUN5tuluyyM5PdLvwq1aQEzNFhnR0QXe4XRVrFEoSeyAZ7Wd4I4UYKzkPzXDifgPCwFIJNYtQ69q_dSAmCVq_hxwfAV4WkjX4mjb42YmEgKr8Siz3IQ2ZRN78KNClgdSVpJYAnmk37Y9kxhvQMerwiBWKFK8y9OZsoW0jukKJx6jOH2o8nUj630AU5QhjvHfrwS_SAp_leJTpP0o1IW0Hja_AvnaDJNhkn-Jt_zOtH4K2bc1ADbce3kWAxe9Q6XtBRPPYK8BVqUuIaWPHJ6ciHWnE1lvF8dO5oALpnmstb0LfiFimm9yk", - "e": "AQAB", - "d": "FRFeGhezgi5IIj0msQH-pTA58agI6XZFHLBO7Ib1GNzgLwWAZJ6Fctr9Oqp_uuquXI0Rh-D0DsrhNipAXIn5jymGJnWwtf_HHGn1PFeigIxP1HHBBXPqMNPV9N2DRptIacRBdauPFlKy4Y4yzllSA4x79waQKOe9Z68DqkAici7ATyX-ExQc11zSkSdJS00EIcNr-Wtg3C-Aq3YwzAw1pp5JyLrlv1UrHuA9fEom5Lzo4iRIO40vuh7pQprn5Sc0VRpFEbTp5p1Q7eNUpY8yjHMmmEGU_GnQfx0_D84WCoIsT6vixQsUw2XlxIhibLZUKbWowwxi9KcyN4Ivkjc0kF-DCkB-27abRlDzWRDbImipZ_GzVl54A4nyQ_oiY4iY3Yg7Fn49bUHfi2kn2WkoJbXe0L_Ju-7OKXGyH94YSOMWtSNarZEFsZ2g7ZToTEwHj7rLlTx465mWbgJ2-XT-1_GDksdYWGfQQ8Ub_4r-Fd-KmJdnZxMG8TvqJAbPkrG6MpdHWriok8jIGq3F5ZqyQ1Htn2eSYMAY5VMdljEipQQ2Cua6Mnnl7BEp20nJXgU60dAeKRwC9pSYig1-c1TvhYNnjLUTWyGYaPdzTkJqbSgc8gU67156NESlmC7FIMAOr_nVjnL9SuvHpHzkW1EJvHRl1b5J9gv5ncU3PDQOuiU", - } - ) + jwk = Jwk({ + "kty": "RSA", + "n": "jp5HPGxg9n6u0HhJaNuE_hzQIPJ_AUfalQ5rw3gZpRNTCNSIQ2aNyIwzyt58zt-YUttb5OirSkhwuiTBm--U4jHJdg57Yy4m_r06ps-3w08HI8XWh6MSAeQwiovtGLSvQ76WGdkwTSeVcST_G9qy3osJdc9xTNwyrPyyfxDef6h8s6_v2GS-xXrKzYy78eoGMezw4-uetIWV5aVfuQ3QpkVo8hN2G0YKEAwRR_zbrzY7HUiCdvbdX-RglT94MOU-_QjUcP1rBYn1z8nOZ0uA_E9UbEnIMTR0IptepaD5NpqDnYFqm_FfOhDw5-hwMxuHw-rHYnvYY78bE4g3RrV8HGMe2CltveUy6VxR4PIJ0-OY7La9ycsSWMDUN5tuluyyM5PdLvwq1aQEzNFhnR0QXe4XRVrFEoSeyAZ7Wd4I4UYKzkPzXDifgPCwFIJNYtQ69q_dSAmCVq_hxwfAV4WkjX4mjb42YmEgKr8Siz3IQ2ZRN78KNClgdSVpJYAnmk37Y9kxhvQMerwiBWKFK8y9OZsoW0jukKJx6jOH2o8nUj630AU5QhjvHfrwS_SAp_leJTpP0o1IW0Hja_AvnaDJNhkn-Jt_zOtH4K2bc1ADbce3kWAxe9Q6XtBRPPYK8BVqUuIaWPHJ6ciHWnE1lvF8dO5oALpnmstb0LfiFimm9yk", + "e": "AQAB", + "d": "FRFeGhezgi5IIj0msQH-pTA58agI6XZFHLBO7Ib1GNzgLwWAZJ6Fctr9Oqp_uuquXI0Rh-D0DsrhNipAXIn5jymGJnWwtf_HHGn1PFeigIxP1HHBBXPqMNPV9N2DRptIacRBdauPFlKy4Y4yzllSA4x79waQKOe9Z68DqkAici7ATyX-ExQc11zSkSdJS00EIcNr-Wtg3C-Aq3YwzAw1pp5JyLrlv1UrHuA9fEom5Lzo4iRIO40vuh7pQprn5Sc0VRpFEbTp5p1Q7eNUpY8yjHMmmEGU_GnQfx0_D84WCoIsT6vixQsUw2XlxIhibLZUKbWowwxi9KcyN4Ivkjc0kF-DCkB-27abRlDzWRDbImipZ_GzVl54A4nyQ_oiY4iY3Yg7Fn49bUHfi2kn2WkoJbXe0L_Ju-7OKXGyH94YSOMWtSNarZEFsZ2g7ZToTEwHj7rLlTx465mWbgJ2-XT-1_GDksdYWGfQQ8Ub_4r-Fd-KmJdnZxMG8TvqJAbPkrG6MpdHWriok8jIGq3F5ZqyQ1Htn2eSYMAY5VMdljEipQQ2Cua6Mnnl7BEp20nJXgU60dAeKRwC9pSYig1-c1TvhYNnjLUTWyGYaPdzTkJqbSgc8gU67156NESlmC7FIMAOr_nVjnL9SuvHpHzkW1EJvHRl1b5J9gv5ncU3PDQOuiU", + }) assert ( RSAJwk.public( diff --git a/tests/test_jws.py b/tests/test_jws.py index 7f0df22..6257c7e 100644 --- a/tests/test_jws.py +++ b/tests/test_jws.py @@ -443,8 +443,8 @@ def test_verify_signature_by_jwcrypto( signature_alg: the signature alg """ - import jwcrypto.jwk # type: ignore[import] - import jwcrypto.jws # type: ignore[import] + import jwcrypto.jwk # type: ignore[import-untyped] + import jwcrypto.jws # type: ignore[import-untyped] jwk = jwcrypto.jwk.JWK(**verification_jwk) jws = jwcrypto.jws.JWS() diff --git a/tests/test_jwt.py b/tests/test_jwt.py index 8d95925..36b38f3 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -26,17 +26,15 @@ def test_signed_jwt() -> None: - jwk = Jwk( - { - "kty": "EC", - "alg": "ES256", - "kid": "my_key", - "crv": "P-256", - "x": "WtjnvHG9b_IKBLn4QYTHz-AdoAiO_ork5LH1BL_5tyI", - "y": "C0YfOUDuCOvTCt7hAqO-f9z8_JdOnOPbfYmUk-RosHA", - "d": "EnGZlkoa4VUsnl72LcRRychNJ2FFknm_ph855tNuPZ8", - } - ) + jwk = Jwk({ + "kty": "EC", + "alg": "ES256", + "kid": "my_key", + "crv": "P-256", + "x": "WtjnvHG9b_IKBLn4QYTHz-AdoAiO_ork5LH1BL_5tyI", + "y": "C0YfOUDuCOvTCt7hAqO-f9z8_JdOnOPbfYmUk-RosHA", + "d": "EnGZlkoa4VUsnl72LcRRychNJ2FFknm_ph855tNuPZ8", + }) jwt = Jwt( "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15X2tleSJ9.eyJhY3IiOiIyIiwiYW1yIjpbInB3ZCIsIm90cCJdLCJhdWQiOiJjbGllbnRfaWQiLCJhdXRoX3RpbWUiOjE2MjkyMDQ1NjAsImV4cCI6MTYyOTIwNDYyMCwiaWF0IjoxNjI5MjA0NTYwLCJuYmYiOjE2MjkyMDQ1NjAsImlzcyI6Imh0dHBzOi8vbXlhcy5sb2NhbCIsIm5vbmNlIjoibm9uY2UiLCJzdWIiOiIxMjM0NTYifQ.RhLqE8VGBjIRag4w9ps1oUQlxumma1fQzFH2UTrMDCjW2iTGdqhkOjpzb5bdI6tkQRRP64IGP4_CBa2BR7p26Q" @@ -98,7 +96,7 @@ def test_unprotected() -> None: def test_jwt_signer_and_verifier(issuer: str, freezer: FrozenDateTimeFactory) -> None: audience = "some_audience" - signer = JwtSigner.with_random_key(issuer, alg="ES256") + signer = JwtSigner.with_random_key(issuer=issuer, alg="ES256") now = datetime.now(timezone.utc) jwt = signer.sign(subject="some_id", audience=audience, extra_claims={"foo": "bar"}) assert isinstance(jwt, SignedJwt) @@ -241,53 +239,51 @@ def test_encrypted_jwt() -> None: ) 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" - ), - } - ) + 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", @@ -567,17 +563,15 @@ def test_verifier(freezer: FrozenDateTimeFactory) -> None: issuer = "https://my.issuer.local" audience = "myaudience" subject = "mysubject" - private_jwk = Jwk( - { - "kty": "EC", - "crv": "P-256", - "alg": "ES256", - "kid": "MUBAl25sdPAIlnA_8-BnMcIe5e8LnlI5pHF6Zy-icvw", - "x": "ftZqn6yrLR_4AytQz8Q_badHRTQ2Vc6Eg46ICsMuuMM", - "y": "C4wIeHH0aIW5Tf1_EPnJkse-vcoDNd-kh8P6-Ci2MI8", - "d": "3vyhseJLd51ZXdlrCHAPH1uv5Bp9IvnA8UB92ksu4MU", - } - ) + private_jwk = Jwk({ + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "kid": "MUBAl25sdPAIlnA_8-BnMcIe5e8LnlI5pHF6Zy-icvw", + "x": "ftZqn6yrLR_4AytQz8Q_badHRTQ2Vc6Eg46ICsMuuMM", + "y": "C4wIeHH0aIW5Tf1_EPnJkse-vcoDNd-kh8P6-Ci2MI8", + "d": "3vyhseJLd51ZXdlrCHAPH1uv5Bp9IvnA8UB92ksu4MU", + }) jwks = private_jwk.public_jwk().as_jwks() def suject_verifier(j: SignedJwt) -> None: