diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c8d5bbe..a3ff79b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,11 +26,11 @@ repos:
hooks:
- id: blacken-docs
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.11
+ rev: v0.1.13
hooks:
- id: ruff
args: [ --fix ]
- - id: ruff-format
+ - id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
@@ -44,6 +44,6 @@ repos:
additional_dependencies:
- types-cryptography==3.3.23.2
- pytest-mypy==0.10.3
- - binapy==0.7.0
+ - binapy==0.8.0
- freezegun==1.2.2
- jwcrypto==1.5.0
diff --git a/README.md b/README.md
index 95787cc..25f5b4a 100644
--- a/README.md
+++ b/README.md
@@ -1,327 +1,329 @@
-# JwSkate
-
-[![PyPi](https://img.shields.io/pypi/v/jwskate.svg)](https://pypi.python.org/pypi/jwskate)
-[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
-
-A Pythonic implementation of the JOSE set of IETF specifications: [Json Web Signature][rfc7515], [Keys][rfc7517],
-[Algorithms][rfc7518], [Tokens][rfc7519] and [Encryption][rfc7516] (RFC7515 to 7519), and their extensions
-[ECDH Signatures][rfc8037] (RFC8037), [JWK Thumbprints][rfc7638] (RFC7638), and [JWK Thumbprint URI][rfc9278] (RFC9278),
-and with respects to [JWT Best Current Practices][rfc8725] (RFC8725).
-
-- Free software: MIT
-- Documentation: [https://guillp.github.io/jwskate/](https://guillp.github.io/jwskate/)
-
-Here is a quick usage example: generating a private RSA key, signing some data, then validating that signature with the matching public key:
-
-```python
-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 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 subclasses
-
-print(rsa_private_jwk.with_usage_parameters())
-```
-
-The result of this print will look like this (with the random parts abbreviated to `...` for display purposes only):
-
-```
-{'kty': 'RSA',
- 'n': '...',
- 'e': 'AQAB',
- 'd': '...',
- 'p': '...',
- 'q': '...',
- 'dp': '...',
- 'dq': '...',
- 'qi': '...',
- 'alg': 'RS256',
- 'kid': '...',
- 'use': 'sig',
- 'key_ops': ['sign']}
-```
-
-Now let's sign a JWT containing arbitrary claims, this time using an Elliptic Curve (`EC`) key:
-
-```python
-from jwskate import Jwk, Jwt
-
-# This time let's try an EC key, based on `alg` parameter,
-# and let's specify an arbitrary Key ID (kid).
-# additional args are either options (like 'key_size' above for RSA keys)
-# or additional parameters to include in the JWK
-private_jwk = Jwk.generate(alg="ES256", kid="my_key")
-# note that based only on the `alg` value, the appropriate key type and curve
-# are automatically deduced and included in the JWK
-print(private_jwk)
-# {'kty': 'EC', 'crv': 'P-256', 'x': 'Ppe...', 'y': '9Si...', 'd': 'g09...', 'alg': 'ES256'}
-assert private_jwk.kty == "EC"
-assert private_jwk.crv == "P-256"
-assert private_jwk.alg == "ES256"
-# this is a private key and 'ES256' is a signature alg, so 'use' and 'key_ops' can also be deduced:
-assert private_jwk.use == "sig"
-assert private_jwk.key_ops == ("sign",)
-
-# here are the claims to sign in a JWT:
-claims = {"sub": "some_sub", "claim1": "value1"}
-
-jwt = Jwt.sign(claims, private_jwk)
-# that's it! we have a signed JWT.
-print(jwt)
-# eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lX3N1YiIsImNsYWltMSI6InZhbHVlMSJ9.SBQIlGlFdwoEMViWUFsBmCsXShtOq4lnp3Im5ZVh1PFCGJFdW-dTG9qJjlFSAA_BkM5PF9u38PL7Ai9cC2_DJw
-assert isinstance(jwt, Jwt) # Jwt are objects
-assert jwt.claims == claims # claims can be accessed as a dict
-assert jwt.headers == {"typ": "JWT", "alg": "ES256", "kid": "my_key"} # headers too
-assert jwt.sub == "some_sub" # individual claims can be accessed as attributes
-assert jwt["claim1"] == "value1" # or as dict items (with "subscription")
-assert jwt.alg == "ES256" # alg and kid headers are also accessible as attributes
-assert jwt.kid == private_jwk.kid
-# notice that alg and kid are automatically set with appropriate values taken from our private jwk
-assert isinstance(jwt.signature, bytes) # signature is accessible too
-# verifying the jwt signature is as easy as:
-assert jwt.verify_signature(private_jwk.public_jwk())
-# since our jwk contains an 'alg' parameter (here 'ES256'), the signature is automatically verified using that alg
-# you could also specify an alg manually, useful for keys with no "alg" hint:
-assert jwt.verify_signature(private_jwk.public_jwk(), alg="ES256")
-# note that jwskate will only trust the alg(s) you provide as parameter, either part of the JWK
-# 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 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", key=private_jwk)
-jwt = signer.sign(
- subject="some_sub",
- audience="some_aud",
- extra_claims={"custom_claim1": "value1", "custom_claim2": "value2"},
-)
-
-print(jwt.claims)
-```
-
-The generated JWT will include the standardized claims (`iss`, `aud`, `sub`, `iat`, `exp` and `jti`),
-together with the `extra_claims` provided to `.sign()`:
-
-```
-{'custom_claim1': 'value1',
- 'custom_claim2': 'value2',
- 'iss': 'https://myissuer.com',
- 'aud': 'some_aud',
- 'sub': 'some_sub',
- 'iat': 1648823184,
- 'exp': 1648823244,
- 'jti': '3b400e27-c111-4013-84e0-714acd76bf3a'
-}
-```
-
-## Features
-
-- Simple, Clean, Pythonic interface
-- Convenience wrappers around `cryptography` for all algorithms described in JWA
-- Json Web Keys (JWK) loading, dumping and generation
-- Arbitrary data signature and verification using Json Web Keys
-- Json Web Signatures (JWS) signing and verification
-- Json Web Encryption (JWE) encryption and decryption
-- Json Web Tokens (JWT) signing, verification and validation
-- 100% type annotated, verified with `mypy --strict`
-- nearly 100% code coverage
-- Relies on [cryptography](https://cryptography.io) for all cryptographic operations
-- Relies on [BinaPy](https://guillp.github.io/binapy/) for binary data manipulations
-
-### Supported Token Types
-
-
-| Token Type | Support |
-| ------------------------- | -------------------------------------------------------- |
-| Json Web Signature (JWS) | ☑ Compact
☑ JSON Flat
☑ JSON General
|
-| Json Web Encryption (JWE) | ☑ Compact
☐ JSON Flat
☐ JSON General
|
-| Json Web Tokens (JWT) | ☑ Signed
☑ Signed and Encrypted |
-
-### Supported Signature algorithms
-
-
-| Signature Alg | Description | Key Type | Reference | Note |
-|-----------------|--------------------------------------------------|----------| ---------------------------------- | ------------------------------ |
-| `HS256` | HMAC using SHA-256 | `oct` | [RFC7518, Section 3.2] | |
-| `HS384` | HMAC using SHA-384 | `oct` | [RFC7518, Section 3.2] | |
-| `HS512` | HMAC using SHA-512 | `oct` | [RFC7518, Section 3.2] | |
-| `RS256` | RSASSA-PKCS1-v1_5 using SHA-256 | `RSA` | [RFC7518, Section 3.3] | |
-| `RS384` | RSASSA-PKCS1-v1_5 using SHA-384 | `RSA` | [RFC7518, Section 3.3] | |
-| `RS512` | RSASSA-PKCS1-v1_5 using SHA-512 | `RSA` | [RFC7518, Section 3.3] | |
-| `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] | |
-| `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 | `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
-
-
-| Signature Alg | Description | Key Type | Reference | Note |
-| ------------------ | ---------------------------------------------- |---------------| ---------------------------------- | ----------- |
-| `RSA1_5` | RSAES-PKCS1-v1_5 | `RSA` | [RFC7518, Section 4.2] | Unwrap Only |
-| `RSA-OAEP` | RSAES OAEP using default parameters | `RSA` | [RFC7518, Section 4.3] | |
-| `RSA-OAEP-256` | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | `RSA` | [RFC7518, Section 4.3] | |
-| `RSA-OAEP-384` | RSA-OAEP using SHA-384 and MGF1 with SHA-384 | `RSA` | https://www.w3.org/TR/WebCryptoAPI | |
-| `RSA-OAEP-512` | RSA-OAEP using SHA-512 and MGF1 with SHA-512 | `RSA` | https://www.w3.org/TR/WebCryptoAPI | |
-| `A128KW` | AES Key Wrap using 128-bit key | `oct` | [RFC7518, Section 4.4] | |
-| `A192KW` | AES Key Wrap using 192-bit key | `oct` | [RFC7518, Section 4.4] | |
-| `A256KW` | AES Key Wrap using 256-bit key | `oct` | [RFC7518, Section 4.4] | |
-| `A128GCMKW` | Key wrapping with AES GCM using 128-bit key | `oct` | [RFC7518, Section 4.7] | |
-| `A192GCMKW` | Key wrapping with AES GCM using 192-bit key | `oct` | [RFC7518, Section 4.7] | |
-| `A256GCMKW` | Key wrapping with AES GCM using 256-bit key | `oct` | [RFC7518, Section 4.7] | |
-| `dir` | Direct use of a shared symmetric key | `oct` | [RFC7518, Section 4.5] | |
-| `ECDH-ES` | ECDH-ES using Concat KDF | `EC` | [RFC7518, Section 4.6] | |
-| `ECDH-ES+A128KW` | ECDH-ES using Concat KDF and "A128KW" wrapping | `EC` | [RFC7518, Section 4.6] | |
-| `ECDH-ES+A192KW` | ECDH-ES using Concat KDF and "A192KW" wrapping | `EC` | [RFC7518, Section 4.6] | |
-| `ECDH-ES+A256KW` | ECDH-ES using Concat KDF and "A256KW" wrapping | `EC` | [RFC7518, Section 4.6] | |
-| `PBES2-HS256+A128KW` | PBES2 with HMAC SHA-256 and "A128KW" wrapping | `password` | [RFC7518, Section 4.8] | |
-| `PBES2-HS384+A192KW` | PBES2 with HMAC SHA-384 and "A192KW" wrapping | `password` | [RFC7518, Section 4.8] | |
-| `PBES2-HS512+A256KW` | PBES2 with HMAC SHA-512 and "A256KW" wrapping | `password` | [RFC7518, Section 4.8] | |
-
-
-
-### Supported Elliptic Curves
-
-
-| Curve | Description | Key Type | Usage | Reference |
-|-------------|---------------------------------------|----------| --------------------- | -------------------------- |
-| `P-256` | P-256 Curve | `EC` | signature, encryption | [RFC7518, Section 6.2.1.1] |
-| `P-384` | P-384 Curve | `EC` | signature, encryption | [RFC7518, Section 6.2.1.1] |
-| `P-521` | P-521 Curve | `EC` | signature, encryption | [RFC7518, Section 6.2.1.1] |
-| `secp256k1` | SECG secp256k1 curve | `EC` | signature, encryption | [RFC8812, Section 3.1] |
-| `Ed25519` | Ed25519 signature algorithm key pairs | `OKP` | signature | [RFC8037, Section 3.1] |
-| `Ed448` | Ed448 signature algorithm key pairs | `OKP` | signature | [RFC8037, Section 3.1] |
-| `X25519` | X25519 function key pairs | `OKP` | encryption | [RFC8037, Section 3.2] |
-| `X448` | X448 function key pairs | `OKP` | encryption | [RFC8037, Section 3.2] |
-
-## 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.
-
-- [PyJWT](https://pyjwt.readthedocs.io)
-- [JWCrypto](https://jwcrypto.readthedocs.io/)
-- [Python-JOSE](https://python-jose.readthedocs.io/)
-- [AuthLib](https://docs.authlib.org/en/latest/jose/)
-
-Not to say that those are _bad_ libs (I actually use `jwcrypto` myself for `jwskate` unit tests), but they either don't
-support some important features, lack documentation, or more generally have APIs that don't feel easy-enough, Pythonic-enough
-to use.
-
-## Design
-
-### JWK are dicts
-
-JWK are specified as JSON objects, which are parsed as `dict` in Python. The `Jwk` class in `jwskate` is actually a
-`dict` subclass, so you can use it exactly like you would use a `dict`: you can access its members, dump it back as JSON,
-etc. The same is true for Signed or Encrypted Json Web tokens in JSON format. However, you cannot change the key cryptographic
-materials, since that would lead to unusable keys.
-
-### JWA Wrappers
-
-You can use `cryptography` to do the cryptographic operations that are described in
-[JWA](https://www.rfc-editor.org/info/rfc7518), but since `cryptography` is a general purpose library, its usage is not
-straightforward and gives you plenty of options to carefully select and combine, leaving room for mistakes, errors and
-confusion. It has also a quite inconsistent API to handle the different type of keys and algorithms. To work around
-this, `jwskate` comes with a set of consistent wrappers that implement the exact JWA specifications, with minimum risk
-of mistakes.
-
-### Safe Signature Verification
-
-As advised in [JWT Best Practices][rfc8725] $3.1:
-
-For every signature verification method in `jwskate`, the expected signature(s) algorithm(s) must be specified. That is
-to avoid a security flaw where your application accepts tokens with a weaker encryption scheme than what your security
-policy mandates; or even worse, where it accepts unsigned tokens, or tokens that are symmetrically signed with an
-improperly used public key, leaving your application exposed to exploitation by attackers.
-
-To specify which signature algorithms are accepted, each signature verification method accepts, in order of preference:
-
-- an `alg` parameter which contains the expected algorithm, or an `algs` parameter which contains a list of acceptable
- algorithms
-- the `alg` parameter from the signature verification `Jwk`, if present. This `alg` is the algorithm intended for use
- with that key.
-
-Note that you cannot use `alg` and `algs` at the same time. If your `Jwk` contains an `alg` parameter, and you provide
-an `alg` or `algs` which does not match that value, a `Warning` will be emitted.
-
-## TODO
-
-- Complete/enhance/proof-read documentation
-- Better exceptions (create dedicated exception classes, better messages, etc.)
-- Support for JWE in JSON format
-- Better tests
-- Support for Selective-Disclosure JWT
-
-## Credits
-
-All cryptographic operations are handled by [cryptography](https://cryptography.io).
-
-[rfc7515]: https://www.rfc-editor.org/rfc/rfc7515.html
-[rfc7516]: https://www.rfc-editor.org/rfc/rfc7516.html
-[rfc7517]: https://www.rfc-editor.org/rfc/rfc7517.html
-[rfc7518]: https://www.rfc-editor.org/rfc/rfc7518.html
-[rfc7518, section 3.2]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2
-[rfc7518, section 3.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.3
-[rfc7518, section 3.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4
-[rfc7518, section 3.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.5
-[rfc7518, section 3.6]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.6
-[rfc7518, section 4.2]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.2
-[rfc7518, section 4.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.3
-[rfc7518, section 4.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.4
-[rfc7518, section 4.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.5
-[rfc7518, section 4.6]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6
-[rfc7518, section 4.7]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.7
-[rfc7518, section 4.8]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8
-[rfc7518, section 5.2.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3
-[rfc7518, section 5.2.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4
-[rfc7518, section 5.2.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5
-[rfc7518, section 5.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.3
-[rfc7518, section 6.2.1.1]: https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1
-[rfc7519]: https://www.rfc-editor.org/rfc/rfc7519.html
-[rfc7638]: https://www.rfc-editor.org/rfc/rfc7638.html
-[rfc8037]: https://www.rfc-editor.org/rfc/rfc8037.html
-[rfc8037, section 3.1]: https://www.rfc-editor.org/rfc/rfc8037.html#section-3.1
-[rfc8037, section 3.2]: https://www.rfc-editor.org/rfc/rfc8037.html#section-3.2
-[rfc8725]: https://www.rfc-editor.org/rfc/rfc8725
-[rfc8812, section 3.1]: https://www.rfc-editor.org/rfc/rfc8812.html#section-3.1
-[rfc8812, section 3.2]: https://www.rfc-editor.org/rfc/rfc8812.html#name-ecdsa-signature-with-secp25
-[rfc9278]: https://www.rfc-editor.org/rfc/rfc9278.html
+# JwSkate
+
+[![PyPi](https://img.shields.io/pypi/v/jwskate.svg)](https://pypi.python.org/pypi/jwskate)
+[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+
+A Pythonic implementation of the JOSE set of IETF specifications: [Json Web Signature][rfc7515], [Keys][rfc7517],
+[Algorithms][rfc7518], [Tokens][rfc7519] and [Encryption][rfc7516] (RFC7515 to 7519), and their extensions
+[ECDH Signatures][rfc8037] (RFC8037), [JWK Thumbprints][rfc7638] (RFC7638), and [JWK Thumbprint URI][rfc9278] (RFC9278),
+and with respects to [JWT Best Current Practices][rfc8725] (RFC8725).
+
+- Free software: MIT
+- Documentation: [https://guillp.github.io/jwskate/](https://guillp.github.io/jwskate/)
+
+Here is a quick usage example: generating a private RSA key, signing some data, then validating that signature with the matching public key:
+
+```python
+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 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:
+from collections import UserDict
+
+assert isinstance(rsa_private_jwk, UserDict) # Jwk are UserDicts
+
+print(rsa_private_jwk.with_usage_parameters())
+```
+
+The result of this print will look like this (with the random parts abbreviated to `...` for display purposes only):
+
+```
+{'kty': 'RSA',
+ 'n': '...',
+ 'e': 'AQAB',
+ 'd': '...',
+ 'p': '...',
+ 'q': '...',
+ 'dp': '...',
+ 'dq': '...',
+ 'qi': '...',
+ 'alg': 'RS256',
+ 'kid': '...',
+ 'use': 'sig',
+ 'key_ops': ['sign']}
+```
+
+Now let's sign a JWT containing arbitrary claims, this time using an Elliptic Curve (`EC`) key:
+
+```python
+from jwskate import Jwk, Jwt
+
+# This time let's try an EC key, based on `alg` parameter,
+# and let's specify an arbitrary Key ID (kid).
+# additional args are either options (like 'key_size' above for RSA keys)
+# or additional parameters to include in the JWK
+private_jwk = Jwk.generate(alg="ES256", kid="my_key")
+# note that based only on the `alg` value, the appropriate key type and curve
+# are automatically deduced and included in the JWK
+print(private_jwk)
+# {'kty': 'EC', 'crv': 'P-256', 'x': 'Ppe...', 'y': '9Si...', 'd': 'g09...', 'alg': 'ES256'}
+assert private_jwk.kty == "EC"
+assert private_jwk.crv == "P-256"
+assert private_jwk.alg == "ES256"
+# this is a private key and 'ES256' is a signature alg, so 'use' and 'key_ops' can also be deduced:
+assert private_jwk.use == "sig"
+assert private_jwk.key_ops == ("sign",)
+
+# here are the claims to sign in a JWT:
+claims = {"sub": "some_sub", "claim1": "value1"}
+
+jwt = Jwt.sign(claims, private_jwk)
+# that's it! we have a signed JWT.
+print(jwt)
+# eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lX3N1YiIsImNsYWltMSI6InZhbHVlMSJ9.SBQIlGlFdwoEMViWUFsBmCsXShtOq4lnp3Im5ZVh1PFCGJFdW-dTG9qJjlFSAA_BkM5PF9u38PL7Ai9cC2_DJw
+assert isinstance(jwt, Jwt) # Jwt are objects
+assert jwt.claims == claims # claims can be accessed as a dict
+assert jwt.headers == {"typ": "JWT", "alg": "ES256", "kid": "my_key"} # headers too
+assert jwt.sub == "some_sub" # individual claims can be accessed as attributes
+assert jwt["claim1"] == "value1" # or as dict items (with "subscription")
+assert jwt.alg == "ES256" # alg and kid headers are also accessible as attributes
+assert jwt.kid == private_jwk.kid
+# notice that alg and kid are automatically set with appropriate values taken from our private jwk
+assert isinstance(jwt.signature, bytes) # signature is accessible too
+# verifying the jwt signature is as easy as:
+assert jwt.verify_signature(private_jwk.public_jwk())
+# since our jwk contains an 'alg' parameter (here 'ES256'), the signature is automatically verified using that alg
+# you could also specify an alg manually, useful for keys with no "alg" hint:
+assert jwt.verify_signature(private_jwk.public_jwk(), alg="ES256")
+# note that jwskate will only trust the alg(s) you provide as parameter, either part of the JWK
+# 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 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", key=private_jwk)
+jwt = signer.sign(
+ subject="some_sub",
+ audience="some_aud",
+ extra_claims={"custom_claim1": "value1", "custom_claim2": "value2"},
+)
+
+print(jwt.claims)
+```
+
+The generated JWT will include the standardized claims (`iss`, `aud`, `sub`, `iat`, `exp` and `jti`),
+together with the `extra_claims` provided to `.sign()`:
+
+```
+{'custom_claim1': 'value1',
+ 'custom_claim2': 'value2',
+ 'iss': 'https://myissuer.com',
+ 'aud': 'some_aud',
+ 'sub': 'some_sub',
+ 'iat': 1648823184,
+ 'exp': 1648823244,
+ 'jti': '3b400e27-c111-4013-84e0-714acd76bf3a'
+}
+```
+
+## Features
+
+- Simple, Clean, Pythonic interface
+- Convenience wrappers around `cryptography` for all algorithms described in JWA
+- Json Web Keys (JWK) loading, dumping and generation
+- Arbitrary data signature and verification using Json Web Keys
+- Json Web Signatures (JWS) signing and verification
+- Json Web Encryption (JWE) encryption and decryption
+- Json Web Tokens (JWT) signing, verification and validation
+- 100% type annotated, verified with `mypy --strict`
+- nearly 100% code coverage
+- Relies on [cryptography](https://cryptography.io) for all cryptographic operations
+- Relies on [BinaPy](https://guillp.github.io/binapy/) for binary data manipulations
+
+### Supported Token Types
+
+
+| Token Type | Support |
+| ------------------------- | -------------------------------------------------------- |
+| Json Web Signature (JWS) | ☑ Compact
☑ JSON Flat
☑ JSON General
|
+| Json Web Encryption (JWE) | ☑ Compact
☐ JSON Flat
☐ JSON General
|
+| Json Web Tokens (JWT) | ☑ Signed
☑ Signed and Encrypted |
+
+### Supported Signature algorithms
+
+
+| Signature Alg | Description | Key Type | Reference | Note |
+|-----------------|--------------------------------------------------|----------| ---------------------------------- | ------------------------------ |
+| `HS256` | HMAC using SHA-256 | `oct` | [RFC7518, Section 3.2] | |
+| `HS384` | HMAC using SHA-384 | `oct` | [RFC7518, Section 3.2] | |
+| `HS512` | HMAC using SHA-512 | `oct` | [RFC7518, Section 3.2] | |
+| `RS256` | RSASSA-PKCS1-v1_5 using SHA-256 | `RSA` | [RFC7518, Section 3.3] | |
+| `RS384` | RSASSA-PKCS1-v1_5 using SHA-384 | `RSA` | [RFC7518, Section 3.3] | |
+| `RS512` | RSASSA-PKCS1-v1_5 using SHA-512 | `RSA` | [RFC7518, Section 3.3] | |
+| `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] | |
+| `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 | `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
+
+
+| Signature Alg | Description | Key Type | Reference | Note |
+| ------------------ | ---------------------------------------------- |---------------| ---------------------------------- | ----------- |
+| `RSA1_5` | RSAES-PKCS1-v1_5 | `RSA` | [RFC7518, Section 4.2] | Unwrap Only |
+| `RSA-OAEP` | RSAES OAEP using default parameters | `RSA` | [RFC7518, Section 4.3] | |
+| `RSA-OAEP-256` | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | `RSA` | [RFC7518, Section 4.3] | |
+| `RSA-OAEP-384` | RSA-OAEP using SHA-384 and MGF1 with SHA-384 | `RSA` | https://www.w3.org/TR/WebCryptoAPI | |
+| `RSA-OAEP-512` | RSA-OAEP using SHA-512 and MGF1 with SHA-512 | `RSA` | https://www.w3.org/TR/WebCryptoAPI | |
+| `A128KW` | AES Key Wrap using 128-bit key | `oct` | [RFC7518, Section 4.4] | |
+| `A192KW` | AES Key Wrap using 192-bit key | `oct` | [RFC7518, Section 4.4] | |
+| `A256KW` | AES Key Wrap using 256-bit key | `oct` | [RFC7518, Section 4.4] | |
+| `A128GCMKW` | Key wrapping with AES GCM using 128-bit key | `oct` | [RFC7518, Section 4.7] | |
+| `A192GCMKW` | Key wrapping with AES GCM using 192-bit key | `oct` | [RFC7518, Section 4.7] | |
+| `A256GCMKW` | Key wrapping with AES GCM using 256-bit key | `oct` | [RFC7518, Section 4.7] | |
+| `dir` | Direct use of a shared symmetric key | `oct` | [RFC7518, Section 4.5] | |
+| `ECDH-ES` | ECDH-ES using Concat KDF | `EC` | [RFC7518, Section 4.6] | |
+| `ECDH-ES+A128KW` | ECDH-ES using Concat KDF and "A128KW" wrapping | `EC` | [RFC7518, Section 4.6] | |
+| `ECDH-ES+A192KW` | ECDH-ES using Concat KDF and "A192KW" wrapping | `EC` | [RFC7518, Section 4.6] | |
+| `ECDH-ES+A256KW` | ECDH-ES using Concat KDF and "A256KW" wrapping | `EC` | [RFC7518, Section 4.6] | |
+| `PBES2-HS256+A128KW` | PBES2 with HMAC SHA-256 and "A128KW" wrapping | `password` | [RFC7518, Section 4.8] | |
+| `PBES2-HS384+A192KW` | PBES2 with HMAC SHA-384 and "A192KW" wrapping | `password` | [RFC7518, Section 4.8] | |
+| `PBES2-HS512+A256KW` | PBES2 with HMAC SHA-512 and "A256KW" wrapping | `password` | [RFC7518, Section 4.8] | |
+
+
+
+### Supported Elliptic Curves
+
+
+| Curve | Description | Key Type | Usage | Reference |
+|-------------|---------------------------------------|----------| --------------------- | -------------------------- |
+| `P-256` | P-256 Curve | `EC` | signature, encryption | [RFC7518, Section 6.2.1.1] |
+| `P-384` | P-384 Curve | `EC` | signature, encryption | [RFC7518, Section 6.2.1.1] |
+| `P-521` | P-521 Curve | `EC` | signature, encryption | [RFC7518, Section 6.2.1.1] |
+| `secp256k1` | SECG secp256k1 curve | `EC` | signature, encryption | [RFC8812, Section 3.1] |
+| `Ed25519` | Ed25519 signature algorithm key pairs | `OKP` | signature | [RFC8037, Section 3.1] |
+| `Ed448` | Ed448 signature algorithm key pairs | `OKP` | signature | [RFC8037, Section 3.1] |
+| `X25519` | X25519 function key pairs | `OKP` | encryption | [RFC8037, Section 3.2] |
+| `X448` | X448 function key pairs | `OKP` | encryption | [RFC8037, Section 3.2] |
+
+## 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.
+
+- [PyJWT](https://pyjwt.readthedocs.io)
+- [JWCrypto](https://jwcrypto.readthedocs.io/)
+- [Python-JOSE](https://python-jose.readthedocs.io/)
+- [AuthLib](https://docs.authlib.org/en/latest/jose/)
+
+Not to say that those are _bad_ libs (I actually use `jwcrypto` myself for `jwskate` unit tests), but they either don't
+support some important features, lack documentation, or more generally have APIs that don't feel easy-enough, Pythonic-enough
+to use.
+
+## Design
+
+### JWK are UserDicts
+
+JWK are specified as JSON objects, which are parsed as `dict` in Python. The `Jwk` class in `jwskate` is actually a
+`UserDict` subclass, which is very similar to a standard `dict`. So you can use it exactly like you would use a `dict`:
+you can access its members, dump it back as JSON, etc. The same is true for Signed or Encrypted Json Web tokens in JSON
+format. However, you cannot change the key cryptographic materials, since that would lead to unusable keys.
+
+### JWA Wrappers
+
+You can use `cryptography` to do the cryptographic operations that are described in
+[JWA](https://www.rfc-editor.org/info/rfc7518), but since `cryptography` is a general purpose library, its usage is not
+straightforward and gives you plenty of options to carefully select and combine, leaving room for mistakes, errors and
+confusion. It has also a quite inconsistent API to handle the different type of keys and algorithms. To work around
+this, `jwskate` comes with a set of consistent wrappers that implement the exact JWA specifications, with minimum risk
+of mistakes.
+
+### Safe Signature Verification
+
+As advised in [JWT Best Practices][rfc8725] $3.1:
+
+For every signature verification method in `jwskate`, the expected signature(s) algorithm(s) must be specified. That is
+to avoid a security flaw where your application accepts tokens with a weaker encryption scheme than what your security
+policy mandates; or even worse, where it accepts unsigned tokens, or tokens that are symmetrically signed with an
+improperly used public key, leaving your application exposed to exploitation by attackers.
+
+To specify which signature algorithms are accepted, each signature verification method accepts, in order of preference:
+
+- an `alg` parameter which contains the expected algorithm, or an `algs` parameter which contains a list of acceptable
+ algorithms
+- the `alg` parameter from the signature verification `Jwk`, if present. This `alg` is the algorithm intended for use
+ with that key.
+
+Note that you cannot use `alg` and `algs` at the same time. If your `Jwk` contains an `alg` parameter, and you provide
+an `alg` or `algs` which does not match that value, a `Warning` will be emitted.
+
+## TODO
+
+- Complete/enhance/proof-read documentation
+- Better exceptions (create dedicated exception classes, better messages, etc.)
+- Support for JWE in JSON format
+- Better tests
+- Support for Selective-Disclosure JWT
+
+## Credits
+
+All cryptographic operations are handled by [cryptography](https://cryptography.io).
+
+[rfc7515]: https://www.rfc-editor.org/rfc/rfc7515.html
+[rfc7516]: https://www.rfc-editor.org/rfc/rfc7516.html
+[rfc7517]: https://www.rfc-editor.org/rfc/rfc7517.html
+[rfc7518]: https://www.rfc-editor.org/rfc/rfc7518.html
+[rfc7518, section 3.2]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2
+[rfc7518, section 3.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.3
+[rfc7518, section 3.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4
+[rfc7518, section 3.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.5
+[rfc7518, section 3.6]: https://www.rfc-editor.org/rfc/rfc7518.html#section-3.6
+[rfc7518, section 4.2]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.2
+[rfc7518, section 4.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.3
+[rfc7518, section 4.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.4
+[rfc7518, section 4.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.5
+[rfc7518, section 4.6]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6
+[rfc7518, section 4.7]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.7
+[rfc7518, section 4.8]: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8
+[rfc7518, section 5.2.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3
+[rfc7518, section 5.2.4]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4
+[rfc7518, section 5.2.5]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5
+[rfc7518, section 5.3]: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.3
+[rfc7518, section 6.2.1.1]: https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1
+[rfc7519]: https://www.rfc-editor.org/rfc/rfc7519.html
+[rfc7638]: https://www.rfc-editor.org/rfc/rfc7638.html
+[rfc8037]: https://www.rfc-editor.org/rfc/rfc8037.html
+[rfc8037, section 3.1]: https://www.rfc-editor.org/rfc/rfc8037.html#section-3.1
+[rfc8037, section 3.2]: https://www.rfc-editor.org/rfc/rfc8037.html#section-3.2
+[rfc8725]: https://www.rfc-editor.org/rfc/rfc8725
+[rfc8812, section 3.1]: https://www.rfc-editor.org/rfc/rfc8812.html#section-3.1
+[rfc8812, section 3.2]: https://www.rfc-editor.org/rfc/rfc8812.html#name-ecdsa-signature-with-secp25
+[rfc9278]: https://www.rfc-editor.org/rfc/rfc9278.html
diff --git a/jwskate/enums.py b/jwskate/enums.py
index 99a9c5a..a4a18d0 100644
--- a/jwskate/enums.py
+++ b/jwskate/enums.py
@@ -78,7 +78,7 @@ class KeyManagementAlgs:
A128GCMKW = "A128GCMKW"
A192GCMKW = "A192GCMKW"
A256GCMKW = "A256GCMKW"
- dir = "dir" # noqa: A003
+ dir = "dir"
PBES2_HS256_A128KW = "PBES2-HS256+A128KW"
PBES2_HS384_A192KW = "PBES2-HS384+A192KW"
diff --git a/jwskate/jwa/encryption/aesgcm.py b/jwskate/jwa/encryption/aesgcm.py
index 6612aa7..00a6048 100644
--- a/jwskate/jwa/encryption/aesgcm.py
+++ b/jwskate/jwa/encryption/aesgcm.py
@@ -50,7 +50,7 @@ def encrypt(
if not isinstance(plaintext, bytes):
plaintext = bytes(plaintext)
ciphertext_with_tag = BinaPy(aead.AESGCM(self.key).encrypt(iv, plaintext, aad))
- ciphertext, tag = ciphertext_with_tag.cut_at(-self.tag_size)
+ ciphertext, tag = ciphertext_with_tag.split_at(-self.tag_size)
return ciphertext, tag
def decrypt(
diff --git a/jwskate/jwa/signature/ec.py b/jwskate/jwa/signature/ec.py
index 44a3b3b..b493a88 100644
--- a/jwskate/jwa/signature/ec.py
+++ b/jwskate/jwa/signature/ec.py
@@ -44,7 +44,9 @@ def sign(self, data: bytes | SupportsBytes) -> BinaPy:
with self.private_key_required() as key:
dss_sig = key.sign(data, ec.ECDSA(self.hashing_alg))
r, s = asymmetric.utils.decode_dss_signature(dss_sig)
- return BinaPy.from_int(r, self.curve.coordinate_size) + BinaPy.from_int(s, self.curve.coordinate_size)
+ return BinaPy.from_int(r, length=self.curve.coordinate_size) + BinaPy.from_int(
+ s, length=self.curve.coordinate_size
+ )
@override
def verify(self, data: bytes | SupportsBytes, signature: bytes | SupportsBytes) -> bool:
diff --git a/jwskate/jwe/compact.py b/jwskate/jwe/compact.py
index f98cb83..bb804ee 100644
--- a/jwskate/jwe/compact.py
+++ b/jwskate/jwe/compact.py
@@ -140,11 +140,11 @@ def enc(self) -> str:
def encrypt(
cls,
plaintext: bytes | SupportsBytes,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
enc: str,
alg: str | None = None,
- extra_headers: dict[str, Any] | None = None,
+ extra_headers: Mapping[str, Any] | None = None,
cek: bytes | None = None,
iv: bytes | None = None,
epk: Jwk | None = None,
@@ -188,7 +188,7 @@ def encrypt(
def unwrap_cek(
self,
- key_or_password: Jwk | dict[str, Any] | bytes | str,
+ key_or_password: Jwk | Mapping[str, Any] | bytes | str,
alg: str | None = None,
algs: Iterable[str] | None = None,
) -> Jwk:
@@ -220,7 +220,7 @@ def unwrap_cek(
def decrypt(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
@@ -249,7 +249,7 @@ def decrypt(
def decrypt_jwt(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
diff --git a/jwskate/jwk/base.py b/jwskate/jwk/base.py
index 987dbfb..fd23a7d 100644
--- a/jwskate/jwk/base.py
+++ b/jwskate/jwk/base.py
@@ -12,6 +12,7 @@
from __future__ import annotations
import warnings
+from copy import copy
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Mapping, SupportsBytes
@@ -154,7 +155,7 @@ def generate_for_kty(cls, kty: str, **kwargs: Any) -> Jwk:
"shake256": "shake256",
}
- def __new__(cls, key: Jwk | dict[str, Any] | Any, **kwargs: Any) -> Jwk:
+ def __new__(cls, key: Jwk | Mapping[str, Any] | Any, **kwargs: Any) -> Jwk:
"""Overridden `__new__` to make the Jwk constructor smarter.
The `Jwk` constructor will accept:
@@ -171,7 +172,7 @@ def __new__(cls, key: Jwk | dict[str, Any] | Any, **kwargs: Any) -> Jwk:
if cls == Jwk:
if isinstance(key, Jwk):
return cls.from_cryptography_key(key.cryptography_key, **kwargs)
- if isinstance(key, dict):
+ if isinstance(key, Mapping):
kty: str | None = key.get("kty")
if kty is None:
msg = "A Json Web Key must have a Key Type (kty)"
@@ -188,9 +189,9 @@ 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)
+ return super().__new__(cls)
- def __init__(self, params: dict[str, Any] | Any, *, include_kid_thumbprint: bool = False):
+ def __init__(self, params: Mapping[str, Any] | Any, *, include_kid_thumbprint: bool = False):
if isinstance(params, dict): # this is to avoid double init due to the __new__ above
super().__init__({key: val for key, val in params.items() if val is not None})
self._validate()
@@ -275,7 +276,8 @@ def __setitem__(self, key: str, value: Any) -> None:
RuntimeError: when trying to modify cryptographic attributes
"""
- if key in self.PARAMS:
+ # don't allow modifying private attributes after the key has been initialized
+ if key in self.PARAMS and hasattr(self, "cryptography_key"):
msg = "JWK key attributes cannot be modified."
raise RuntimeError(msg)
super().__setitem__(key, value)
@@ -305,12 +307,18 @@ def alg(self) -> str | None:
return alg
@property
- def kid(self) -> str | None:
- """Return the JWK key ID (kid), if present."""
+ def kid(self) -> str:
+ """Return the JWK key ID (kid).
+
+ If the kid is not explicitly set, the RFC7638 key thumbprint is returned.
+
+ """
kid = self.get("kid")
if kid is not None and not isinstance(kid, str): # pragma: no branch
msg = f"invalid kid type {type(kid)}"
raise TypeError(msg, kid)
+ if kid is None:
+ return self.thumbprint()
return kid
@property
@@ -1220,7 +1228,7 @@ def copy(self) -> Jwk:
a copy of this key, with the same value
"""
- return Jwk(super().copy())
+ return Jwk(copy(self.data))
def with_kid_thumbprint(self, *, force: bool = False) -> Jwk:
"""Include the JWK thumbprint as `kid`.
diff --git a/jwskate/jwk/ec.py b/jwskate/jwk/ec.py
index c476c86..4e8a962 100644
--- a/jwskate/jwk/ec.py
+++ b/jwskate/jwk/ec.py
@@ -150,9 +150,9 @@ def private(cls, *, crv: str, x: int, y: int, d: int, **params: Any) -> ECJwk:
dict(
kty=cls.KTY,
crv=crv,
- x=BinaPy.from_int(x, coord_size).to("b64u").ascii(),
- y=BinaPy.from_int(y, coord_size).to("b64u").ascii(),
- d=BinaPy.from_int(d, coord_size).to("b64u").ascii(),
+ x=BinaPy.from_int(x, length=coord_size).to("b64u").ascii(),
+ y=BinaPy.from_int(y, length=coord_size).to("b64u").ascii(),
+ d=BinaPy.from_int(d, length=coord_size).to("b64u").ascii(),
**{k: v for k, v in params.items() if v is not None},
)
)
@@ -218,12 +218,12 @@ def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> ECJwk:
msg = f"Unsupported Curve {cryptography_key.curve.name}"
raise NotImplementedError(msg)
- x = BinaPy.from_int(public_numbers.x, crv.coordinate_size).to("b64u").ascii()
- y = BinaPy.from_int(public_numbers.y, crv.coordinate_size).to("b64u").ascii()
+ x = BinaPy.from_int(public_numbers.x, length=crv.coordinate_size).to("b64u").ascii()
+ y = BinaPy.from_int(public_numbers.y, length=crv.coordinate_size).to("b64u").ascii()
parameters = {"kty": KeyTypes.EC, "crv": crv.name, "x": x, "y": y}
if isinstance(cryptography_key, ec.EllipticCurvePrivateKey):
pn = cryptography_key.private_numbers() # type: ignore[attr-defined]
- d = BinaPy.from_int(pn.private_value, crv.coordinate_size).to("b64u").ascii()
+ d = BinaPy.from_int(pn.private_value, length=crv.coordinate_size).to("b64u").ascii()
parameters["d"] = d
return cls(parameters)
diff --git a/jwskate/jwk/jwks.py b/jwskate/jwk/jwks.py
index 7b760c9..a3fc22f 100644
--- a/jwskate/jwk/jwks.py
+++ b/jwskate/jwk/jwks.py
@@ -2,7 +2,9 @@
from __future__ import annotations
-from typing import Any, Iterable
+from typing import Any, Iterable, Mapping
+
+from typing_extensions import override
from jwskate.token import BaseJsonDict
@@ -17,8 +19,8 @@ class JwkSet(BaseJsonDict):
methods to get the keys, add or remove keys, and verify signatures
using keys from this set.
- - a `dict` from the parsed JSON object representing this JwkSet (in paramter `jwks`)
- - a list of `Jwk` (in parameter `keys`
+ - a `dict` from the parsed JSON object representing this JwkSet (in parameter `jwks`)
+ - a list of `Jwk` (in parameter `keys`)
- nothing, to initialize an empty JwkSet
Args:
@@ -29,21 +31,23 @@ class JwkSet(BaseJsonDict):
def __init__(
self,
- jwks: dict[str, Any] | None = None,
- keys: Iterable[Jwk | dict[str, Any]] | None = None,
+ jwks: Mapping[str, Any] | None = None,
+ keys: Iterable[Jwk | Mapping[str, Any]] | None = None,
):
- if jwks is None and keys is None:
- keys = []
-
- if jwks is not None:
- keys = jwks.pop("keys", [])
- super().__init__(jwks) # init the dict with all the dict content that is not keys
+ super().__init__({k: v for k, v in jwks.items() if k != "keys"} if jwks else {})
+ if keys is None and jwks is not None and "keys" in jwks:
+ keys = jwks.get("keys")
+ if keys:
+ for key in keys:
+ self.add_jwk(key)
+
+ @override
+ def __setitem__(self, name: str, value: Any) -> None:
+ if name == "keys":
+ for key in value:
+ self.add_jwk(key)
else:
- super().__init__()
-
- if keys is not None:
- for jwk in keys:
- self.add_jwk(jwk)
+ super().__setitem__(name, value)
@property
def jwks(self) -> list[Jwk]:
@@ -53,7 +57,7 @@ def jwks(self) -> list[Jwk]:
a list of `Jwk`
"""
- return self.get("keys", []) # type: ignore[no-any-return]
+ return self.get("keys", [])
def get_jwk_by_kid(self, kid: str) -> Jwk:
"""Return a Jwk from this JwkSet, based on its kid.
@@ -84,35 +88,23 @@ def __len__(self) -> int:
def add_jwk(
self,
- key: Jwk | dict[str, Any] | Any,
- kid: str | None = None,
- use: str | None = None,
+ key: Jwk | Mapping[str, Any] | Any,
) -> str:
"""Add a Jwk in this JwkSet.
Args:
key: the Jwk to add (either a `Jwk` instance, or a dict containing the Jwk parameters)
- kid: the kid to use, if `jwk` doesn't contain one
- use: the defined use for the added Jwk
Returns:
- the kid from the added Jwk (it may be generated if no kid is provided)
+ the key ID. It will be generated if missing from the given Jwk.
"""
- key = to_jwk(key)
-
- self.setdefault("keys", [])
+ key = to_jwk(key).with_kid_thumbprint()
- kid = key.get("kid", kid)
- if not kid:
- kid = key.thumbprint()
- key["kid"] = kid
- use = key.get("use", use)
- if use:
- key["use"] = use
- self.jwks.append(key)
+ self.data.setdefault("keys", [])
+ self.data["keys"].append(key)
- return kid
+ return key.kid
def remove_jwk(self, kid: str) -> None:
"""Remove a Jwk from this JwkSet, based on a `kid`.
@@ -198,7 +190,7 @@ def verify(
jwk = self.get_jwk_by_kid(kid)
return jwk.verify(data, signature, alg=alg, algs=algs)
- # otherwise, try all keys which support the given alg(s)
+ # otherwise, try all keys that support the given alg(s)
if algs is None:
if alg is not None:
algs = (alg,)
diff --git a/jwskate/jws/compact.py b/jwskate/jws/compact.py
index 2dc46b2..9ba7847 100644
--- a/jwskate/jws/compact.py
+++ b/jwskate/jws/compact.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from functools import cached_property
-from typing import TYPE_CHECKING, Any, Iterable, SupportsBytes
+from typing import TYPE_CHECKING, Any, Iterable, Mapping, SupportsBytes
from binapy import BinaPy
from typing_extensions import Self
@@ -62,9 +62,9 @@ def __init__(self, value: bytes | str, max_size: int = 16 * 1024):
def sign(
cls,
payload: bytes | SupportsBytes,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
alg: str | None = None,
- extra_headers: dict[str, Any] | None = None,
+ extra_headers: Mapping[str, Any] | None = None,
) -> JwsCompact:
"""Sign a payload and returns the resulting JwsCompact.
@@ -132,7 +132,7 @@ def signed_part(self) -> bytes:
def verify_signature(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
@@ -153,7 +153,7 @@ def verify_signature(
def verify(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
diff --git a/jwskate/jws/json.py b/jwskate/jws/json.py
index 69ce67d..a0cf063 100644
--- a/jwskate/jws/json.py
+++ b/jwskate/jws/json.py
@@ -52,7 +52,7 @@ def jws_signature(self) -> JwsSignature:
def sign(
cls,
payload: bytes,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
alg: str | None = None,
extra_protected_headers: Mapping[str, Any] | None = None,
header: Any | None = None,
@@ -115,7 +115,7 @@ def compact(self) -> JwsCompact:
def verify_signature(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
@@ -218,7 +218,7 @@ def signatures(self) -> list[JwsSignature]:
def add_signature(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
alg: str | None = None,
extra_protected_headers: Mapping[str, Any] | None = None,
header: Mapping[str, Any] | None = None,
@@ -306,7 +306,7 @@ def flatten(
def verify_signature(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
diff --git a/jwskate/jws/signature.py b/jwskate/jws/signature.py
index e213e28..73fb000 100644
--- a/jwskate/jws/signature.py
+++ b/jwskate/jws/signature.py
@@ -113,7 +113,7 @@ def signature(self) -> bytes:
def sign(
cls: type[S],
payload: bytes,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
alg: str | None = None,
extra_protected_headers: Mapping[str, Any] | None = None,
header: Any | None = None,
@@ -145,7 +145,7 @@ def sign(
return cls.from_parts(protected=headers, signature=signature, header=header, **kwargs)
@classmethod
- def assemble_signed_part(cls, headers: dict[str, Any], payload: bytes | str) -> bytes:
+ def assemble_signed_part(cls, headers: Mapping[str, Any], payload: bytes | str) -> bytes:
"""Assemble the protected header and payload to sign, as specified in.
[RFC7515
@@ -169,7 +169,7 @@ def assemble_signed_part(cls, headers: dict[str, Any], payload: bytes | str) ->
def verify(
self,
payload: bytes,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
diff --git a/jwskate/jwt/base.py b/jwskate/jwt/base.py
index 554987e..d23a6e1 100644
--- a/jwskate/jwt/base.py
+++ b/jwskate/jwt/base.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime, timezone
-from typing import TYPE_CHECKING, Any, Iterable
+from typing import TYPE_CHECKING, Any, Iterable, Mapping
from binapy import BinaPy
@@ -50,12 +50,12 @@ def __new__(cls, value: bytes | str, max_size: int = 16 * 1024) -> SignedJwt | J
@classmethod
def sign(
cls,
- claims: dict[str, Any],
- key: Jwk | dict[str, Any] | Any,
+ claims: Mapping[str, Any],
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
typ: str | None = "JWT",
- extra_headers: dict[str, Any] | None = None,
+ extra_headers: Mapping[str, Any] | None = None,
) -> SignedJwt:
"""Sign a JSON payload with a private key and return the resulting `SignedJwt`.
@@ -93,9 +93,9 @@ def sign(
@classmethod
def sign_arbitrary(
cls,
- claims: dict[str, Any],
- headers: dict[str, Any],
- key: Jwk | dict[str, Any] | Any,
+ claims: Mapping[str, Any],
+ headers: Mapping[str, Any],
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
) -> SignedJwt:
@@ -131,10 +131,10 @@ def sign_arbitrary(
@classmethod
def unprotected(
cls,
- claims: dict[str, Any],
+ claims: Mapping[str, Any],
*,
typ: str | None = "JWT",
- extra_headers: dict[str, Any] | None = None,
+ extra_headers: Mapping[str, Any] | None = None,
) -> SignedJwt:
"""Generate a JWT that is not signed and not encrypted (with alg=none).
@@ -162,15 +162,15 @@ def unprotected(
@classmethod
def sign_and_encrypt(
cls,
- claims: dict[str, Any],
- sign_key: Jwk | dict[str, Any] | Any,
- enc_key: Jwk | dict[str, Any] | Any,
+ claims: Mapping[str, Any],
+ sign_key: Jwk | Mapping[str, Any] | Any,
+ enc_key: Jwk | Mapping[str, Any] | Any,
enc: str,
*,
sign_alg: str | None = None,
enc_alg: str | None = None,
- sign_extra_headers: dict[str, Any] | None = None,
- enc_extra_headers: dict[str, Any] | None = None,
+ sign_extra_headers: Mapping[str, Any] | None = None,
+ enc_extra_headers: Mapping[str, Any] | None = None,
) -> JweCompact:
"""Sign a JWT, then encrypt it as JWE payload.
@@ -197,7 +197,7 @@ def sign_and_encrypt(
)
@classmethod
- def decrypt_nested_jwt(cls, jwe: str | JweCompact, key: Jwk | dict[str, Any] | Any) -> SignedJwt:
+ def decrypt_nested_jwt(cls, jwe: str | JweCompact, key: Jwk | Mapping[str, Any] | Any) -> SignedJwt:
"""Decrypt a JWE that contains a nested signed JWT.
It will return a [Jwt] instance for the inner JWT.
@@ -221,8 +221,8 @@ def decrypt_nested_jwt(cls, jwe: str | JweCompact, key: Jwk | dict[str, Any] | A
def decrypt_and_verify(
cls,
jwt: str | JweCompact,
- enc_key: Jwk | dict[str, Any] | Any,
- sig_key: Jwk | dict[str, Any] | Any,
+ enc_key: Jwk | Mapping[str, Any] | Any,
+ sig_key: Jwk | Mapping[str, Any] | Any,
sig_alg: str | None = None,
sig_algs: Iterable[str] | None = None,
) -> SignedJwt:
diff --git a/jwskate/jwt/signed.py b/jwskate/jwt/signed.py
index f48a591..6ee4fcf 100644
--- a/jwskate/jwt/signed.py
+++ b/jwskate/jwt/signed.py
@@ -4,7 +4,7 @@
from datetime import datetime, timedelta, timezone
from functools import cached_property
-from typing import Any, Iterable
+from typing import Any, Iterable, Mapping
from binapy import BinaPy
from typing_extensions import Self
@@ -80,7 +80,7 @@ def signed_part(self) -> bytes:
def verify_signature(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
alg: str | None = None,
algs: Iterable[str] | None = None,
) -> bool:
@@ -351,7 +351,7 @@ def __bytes__(self) -> bytes:
def validate(
self,
- key: Jwk | dict[str, Any] | Any,
+ key: Jwk | Mapping[str, Any] | Any,
*,
alg: str | None = None,
algs: Iterable[str] | None = None,
@@ -420,7 +420,7 @@ def validate(
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
+ self, key: Any, enc: str, alg: str | None = None, extra_headers: Mapping[str, Any] | None = None
) -> JweCompact:
"""Encrypt this JWT into a JWE.
@@ -433,7 +433,7 @@ def encrypt(
extra_headers: additional headers to include in the outer JWE.
"""
- extra_headers = extra_headers or {}
+ extra_headers = dict(extra_headers) if extra_headers else {}
extra_headers.setdefault("cty", "JWT")
jwe = JweCompact.encrypt(self, key, enc=enc, alg=alg, extra_headers=extra_headers)
diff --git a/jwskate/jwt/signer.py b/jwskate/jwt/signer.py
index a53b46d..051756e 100644
--- a/jwskate/jwt/signer.py
+++ b/jwskate/jwt/signer.py
@@ -27,7 +27,7 @@
from __future__ import annotations
import uuid
-from typing import Any, Callable, Iterable
+from typing import Any, Callable, Iterable, Mapping
from jwskate.jwk import Jwk
@@ -86,8 +86,8 @@ def sign(
*,
subject: str | None = None,
audience: str | Iterable[str] | None = None,
- extra_claims: dict[str, Any] | None = None,
- extra_headers: dict[str, Any] | None = None,
+ extra_claims: Mapping[str, Any] | None = None,
+ extra_headers: Mapping[str, Any] | None = None,
lifetime: int | None = None,
leeway: int | None = None,
) -> SignedJwt:
diff --git a/jwskate/jwt/verifier.py b/jwskate/jwt/verifier.py
index 06d57d8..5d8498b 100644
--- a/jwskate/jwt/verifier.py
+++ b/jwskate/jwt/verifier.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Any, Callable, Iterable
+from typing import Any, Callable, Iterable, Mapping
from jwskate import InvalidSignature, Jwk, JwkSet
@@ -45,7 +45,7 @@ class JwtVerifier:
def __init__(
self,
- jwkset: JwkSet | Jwk | dict[str, Any],
+ jwkset: JwkSet | Jwk | Mapping[str, Any],
*,
issuer: str | None,
audience: str | None = None,
diff --git a/jwskate/token.py b/jwskate/token.py
index 89556e6..7377e7c 100644
--- a/jwskate/token.py
+++ b/jwskate/token.py
@@ -2,9 +2,13 @@
from __future__ import annotations
-import json
+import sys
+from collections import UserDict
from functools import cached_property
-from typing import Any, Dict, TypeVar
+from typing import Any
+
+from binapy import BinaPy
+from typing_extensions import Self
class BaseCompactToken:
@@ -141,14 +145,17 @@ def __bytes__(self) -> bytes:
return self.value
-D = TypeVar("D", bound="BaseJsonDict")
+if sys.version_info[:2] > (3, 8):
+ BaseUserDict = UserDict[str, Any]
+else:
+ BaseUserDict = UserDict
-class BaseJsonDict(Dict[str, Any]):
+class BaseJsonDict(BaseUserDict):
"""Base class Jwk and tokens in JSON representation."""
@classmethod
- def from_json(cls: type[D], j: str) -> D:
+ def from_json(cls, j: str) -> Self:
"""Initialize an object based on a string containing a JSON representation.
Args:
@@ -158,17 +165,17 @@ def from_json(cls: type[D], j: str) -> D:
the resulting object
"""
- return cls(json.loads(j))
+ return cls(BinaPy(j).parse_from("json"))
- def to_json(self, *args: Any, **kwargs: Any) -> str:
+ def to_json(self, *, compact: bool = True, **kwargs: Any) -> str:
"""Serialize the current object into a JSON representation.
Args:
- *args: additional args for json.dumps()
+ compact: if True, don't include whitespaces or newlines in the result
**kwargs: additional kwargs for json.dumps()
Returns:
a JSON representation of the current object
"""
- return json.dumps(self, *args, **kwargs)
+ return BinaPy.serialize_to("json", self, compact=compact, **kwargs).decode()
diff --git a/pyproject.toml b/pyproject.toml
index ff44f7c..721c40b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ classifiers = [
dependencies = [
"cryptography>=3.4",
"typing-extensions>=4.3",
- "binapy>=0.7",
+ "binapy>=0.8",
]
[project.urls]
diff --git a/tests/test_jwk/test_jwk.py b/tests/test_jwk/test_jwk.py
index 16a0e8f..99ae45a 100644
--- a/tests/test_jwk/test_jwk.py
+++ b/tests/test_jwk/test_jwk.py
@@ -115,9 +115,24 @@ def test_json() -> None:
jwk = Jwk.from_json(j)
assert (
jwk.public_jwk().as_jwks().to_json()
- == '{"keys": [{"kty": "RSA", "kid": "client_assertion_key", "n":'
- ' "5nHd3aefARenRFQn-wVrjBLS-5A0uUiHWfOUt8EpjwE3wADAvVPKLbvRQJfugyrn_RpnLqqZFkwYrVD_u1Uzl9J17XJG75jGjCf-gVs1t9FPpgEsJGYK4RK2_f40AxAc6hKomB9q6_dqIxChDxVCrIrrWd9kRk0T86d8Ade3J4f_iMbremm3woSwI6QD056DkRtAD_v2PZQbUBgSru-PsrJ5l_pxxlGPxzAM4_XH8VfogXI8pWv2UDE1IguVeh371ESCbQbJ7SX2jgNzcvvZMMWs0syfF7P0BzGrh_ONsRcxmtjZgtcOA0TCu2-v8qx7GisgqOWOrzWs7ej5RUsu1sxtT53JG2Y3lrPrgajXTB56mSUaL9ivxEfUD17X_cUznGDNoVqcRdfa27rCtWqd8gL-C7M9bYYgcfpCRPllRvGmWP9oarrG4XoIO17QuhZ5tAoz8oFLM9o6pzR2CeDvmSqFbbTHXYdcpCuvYukIimZP6RruMU9O9YQjgCEGWx06WoTnDqWWjbrId8VqP0xJ_6w0j6av3EWGKLETBbaYRXys4OOy-JZRRHydg-es4tkir4xkMvIG8plxoz_mZbTyO9GA5tMHWzbQciUQFf95Gpiwsa5RDdGZx-guBAN56mtKnUzVG_PmUJ8-pzTATkjVpThBRLWaVPFi0eWLEc2NbF8",'
- ' "e": "AQAB"}]}'
+ == '{"keys":[{"kty":"RSA","kid":"client_assertion_key","n":'
+ '"5nHd3aefARenRFQn-wVrjBLS-5A0uUiHWfOUt8EpjwE3wADAvVPKLbvRQJfugyrn_RpnLqqZFkwYrVD_u1Uzl9J17XJG75jGjCf-gVs1t9FPpgEsJGYK4RK2_f40AxAc6hKomB9q6_dqIxChDxVCrIrrWd9kRk0T86d8Ade3J4f_iMbremm3woSwI6QD056DkRtAD_v2PZQbUBgSru-PsrJ5l_pxxlGPxzAM4_XH8VfogXI8pWv2UDE1IguVeh371ESCbQbJ7SX2jgNzcvvZMMWs0syfF7P0BzGrh_ONsRcxmtjZgtcOA0TCu2-v8qx7GisgqOWOrzWs7ej5RUsu1sxtT53JG2Y3lrPrgajXTB56mSUaL9ivxEfUD17X_cUznGDNoVqcRdfa27rCtWqd8gL-C7M9bYYgcfpCRPllRvGmWP9oarrG4XoIO17QuhZ5tAoz8oFLM9o6pzR2CeDvmSqFbbTHXYdcpCuvYukIimZP6RruMU9O9YQjgCEGWx06WoTnDqWWjbrId8VqP0xJ_6w0j6av3EWGKLETBbaYRXys4OOy-JZRRHydg-es4tkir4xkMvIG8plxoz_mZbTyO9GA5tMHWzbQciUQFf95Gpiwsa5RDdGZx-guBAN56mtKnUzVG_PmUJ8-pzTATkjVpThBRLWaVPFi0eWLEc2NbF8",'
+ '"e":"AQAB"}]}'
+ )
+
+ assert (
+ jwk.public_jwk().as_jwks().to_json(compact=False)
+ == ('{\n'
+ ' "keys": [\n'
+ ' {\n'
+ ' "kty": "RSA", \n'
+ ' "kid": "client_assertion_key", \n'
+ ' "n": '
+ '"5nHd3aefARenRFQn-wVrjBLS-5A0uUiHWfOUt8EpjwE3wADAvVPKLbvRQJfugyrn_RpnLqqZFkwYrVD_u1Uzl9J17XJG75jGjCf-gVs1t9FPpgEsJGYK4RK2_f40AxAc6hKomB9q6_dqIxChDxVCrIrrWd9kRk0T86d8Ade3J4f_iMbremm3woSwI6QD056DkRtAD_v2PZQbUBgSru-PsrJ5l_pxxlGPxzAM4_XH8VfogXI8pWv2UDE1IguVeh371ESCbQbJ7SX2jgNzcvvZMMWs0syfF7P0BzGrh_ONsRcxmtjZgtcOA0TCu2-v8qx7GisgqOWOrzWs7ej5RUsu1sxtT53JG2Y3lrPrgajXTB56mSUaL9ivxEfUD17X_cUznGDNoVqcRdfa27rCtWqd8gL-C7M9bYYgcfpCRPllRvGmWP9oarrG4XoIO17QuhZ5tAoz8oFLM9o6pzR2CeDvmSqFbbTHXYdcpCuvYukIimZP6RruMU9O9YQjgCEGWx06WoTnDqWWjbrId8VqP0xJ_6w0j6av3EWGKLETBbaYRXys4OOy-JZRRHydg-es4tkir4xkMvIG8plxoz_mZbTyO9GA5tMHWzbQciUQFf95Gpiwsa5RDdGZx-guBAN56mtKnUzVG_PmUJ8-pzTATkjVpThBRLWaVPFi0eWLEc2NbF8", \n'
+ ' "e": "AQAB"\n'
+ ' }\n'
+ ' ]\n'
+ '}')
)
@@ -144,11 +159,22 @@ def test_init_from_json() -> None:
"""
jwk = Jwk(j)
assert (
- jwk.public_jwk().as_jwks().to_json()
- == '{"keys": [{"kty": "RSA", "kid": "client_assertion_key", "n":'
- ' "5nHd3aefARenRFQn-wVrjBLS-5A0uUiHWfOUt8EpjwE3wADAvVPKLbvRQJfugyrn_RpnLqqZFkwYrVD_u1Uzl9J17XJG75jGjCf-gVs1t9FPpgEsJGYK4RK2_f40AxAc6hKomB9q6_dqIxChDxVCrIrrWd9kRk0T86d8Ade3J4f_iMbremm3woSwI6QD056DkRtAD_v2PZQbUBgSru-PsrJ5l_pxxlGPxzAM4_XH8VfogXI8pWv2UDE1IguVeh371ESCbQbJ7SX2jgNzcvvZMMWs0syfF7P0BzGrh_ONsRcxmtjZgtcOA0TCu2-v8qx7GisgqOWOrzWs7ej5RUsu1sxtT53JG2Y3lrPrgajXTB56mSUaL9ivxEfUD17X_cUznGDNoVqcRdfa27rCtWqd8gL-C7M9bYYgcfpCRPllRvGmWP9oarrG4XoIO17QuhZ5tAoz8oFLM9o6pzR2CeDvmSqFbbTHXYdcpCuvYukIimZP6RruMU9O9YQjgCEGWx06WoTnDqWWjbrId8VqP0xJ_6w0j6av3EWGKLETBbaYRXys4OOy-JZRRHydg-es4tkir4xkMvIG8plxoz_mZbTyO9GA5tMHWzbQciUQFf95Gpiwsa5RDdGZx-guBAN56mtKnUzVG_PmUJ8-pzTATkjVpThBRLWaVPFi0eWLEc2NbF8",'
- ' "e": "AQAB"}]}'
+ jwk.public_jwk().as_jwks().to_json(compact=True)
+ == '{"keys":[{"kty":"RSA","kid":"client_assertion_key","n":'
+ '"5nHd3aefARenRFQn-wVrjBLS-5A0uUiHWfOUt8EpjwE3wADAvVPKLbvRQJfugyrn_RpnLqqZFkwYrVD_u1Uzl9J17XJG75jGjCf-gVs1t9FPpgEsJGYK4RK2_f40AxAc6hKomB9q6_dqIxChDxVCrIrrWd9kRk0T86d8Ade3J4f_iMbremm3woSwI6QD056DkRtAD_v2PZQbUBgSru-PsrJ5l_pxxlGPxzAM4_XH8VfogXI8pWv2UDE1IguVeh371ESCbQbJ7SX2jgNzcvvZMMWs0syfF7P0BzGrh_ONsRcxmtjZgtcOA0TCu2-v8qx7GisgqOWOrzWs7ej5RUsu1sxtT53JG2Y3lrPrgajXTB56mSUaL9ivxEfUD17X_cUznGDNoVqcRdfa27rCtWqd8gL-C7M9bYYgcfpCRPllRvGmWP9oarrG4XoIO17QuhZ5tAoz8oFLM9o6pzR2CeDvmSqFbbTHXYdcpCuvYukIimZP6RruMU9O9YQjgCEGWx06WoTnDqWWjbrId8VqP0xJ_6w0j6av3EWGKLETBbaYRXys4OOy-JZRRHydg-es4tkir4xkMvIG8plxoz_mZbTyO9GA5tMHWzbQciUQFf95Gpiwsa5RDdGZx-guBAN56mtKnUzVG_PmUJ8-pzTATkjVpThBRLWaVPFi0eWLEc2NbF8",'
+ '"e":"AQAB"}]}'
)
+ assert jwk.public_jwk().as_jwks().to_json(compact=False) == ('{\n'
+ ' "keys": [\n'
+ ' {\n'
+ ' "kty": "RSA", \n'
+ ' "kid": "client_assertion_key", \n'
+ ' "n": '
+ '"5nHd3aefARenRFQn-wVrjBLS-5A0uUiHWfOUt8EpjwE3wADAvVPKLbvRQJfugyrn_RpnLqqZFkwYrVD_u1Uzl9J17XJG75jGjCf-gVs1t9FPpgEsJGYK4RK2_f40AxAc6hKomB9q6_dqIxChDxVCrIrrWd9kRk0T86d8Ade3J4f_iMbremm3woSwI6QD056DkRtAD_v2PZQbUBgSru-PsrJ5l_pxxlGPxzAM4_XH8VfogXI8pWv2UDE1IguVeh371ESCbQbJ7SX2jgNzcvvZMMWs0syfF7P0BzGrh_ONsRcxmtjZgtcOA0TCu2-v8qx7GisgqOWOrzWs7ej5RUsu1sxtT53JG2Y3lrPrgajXTB56mSUaL9ivxEfUD17X_cUznGDNoVqcRdfa27rCtWqd8gL-C7M9bYYgcfpCRPllRvGmWP9oarrG4XoIO17QuhZ5tAoz8oFLM9o6pzR2CeDvmSqFbbTHXYdcpCuvYukIimZP6RruMU9O9YQjgCEGWx06WoTnDqWWjbrId8VqP0xJ_6w0j6av3EWGKLETBbaYRXys4OOy-JZRRHydg-es4tkir4xkMvIG8plxoz_mZbTyO9GA5tMHWzbQciUQFf95Gpiwsa5RDdGZx-guBAN56mtKnUzVG_PmUJ8-pzTATkjVpThBRLWaVPFi0eWLEc2NbF8", \n'
+ ' "e": "AQAB"\n'
+ ' }\n'
+ ' ]\n'
+ '}')
def test_missing_kty() -> None:
diff --git a/tests/test_jwk/test_jwks.py b/tests/test_jwk/test_jwks.py
index 1ba89ee..427ead0 100644
--- a/tests/test_jwk/test_jwks.py
+++ b/tests/test_jwk/test_jwks.py
@@ -1,12 +1,14 @@
from __future__ import annotations
+from typing import Mapping, Any
+
import pytest
from jwskate import Jwk, JwkSet
def test_jwkset() -> None:
- keys = [
+ keys: list[Mapping[str, Any]] = [
{
"kty": "RSA",
"n": "mUdmf5vJ3svsPSQ8BCOQVfwQdP8AmAEW21sYYUC5eSKR-pdwnRDBuFrIEjon2ry8cU-uaMjAoEZikPXcCTErye2Sj8fWQ8Wyo8DoGacJlFOJvs_18-CmNBc7oL8gBlYax3-feZZnaVIiJjvxQwUw5GQA6JTFnO8n2pnKMOOd8Gf6YrG-r0T6NXdviw0-2IW4f2UMJApqlu37yF8sgRNGZwDljNOkUtPK76Uz5T513Va4ckOqsVfnt4WoAkAkCl3eVBwGw3TJIbp_DaLUq53go0pXBCNxCHRD9mst69ZuknBLqn0SwKbQ9zJH9QvoqrEZ2q7GzkFzw70F6qH5MDEx2-dxQz_QccFV0XBpq4pkfuWzS8qKVO4QjyC7A0vIJUzrRHE2_moOtWvKTDsa7gfvK6kpnAW0iKnNchzBV0fzXWIIxRJ3_cc8Ue-KPRU9Wxm3heBOx_Qh-bKv9s9fVY9X6rimyX-pIwf-jkgWG8_FgTBuGkKTRcLi-XnwsCFIVNOtolmakbQHlin_lgDQm9s0nHoDJbZgAtzQfkIorclBJBzr2t__xgaZCfpSCLdwZFQvGEh1mK4WbSMMt5-L3zKsNLCBfdMbn2fS9n2hylfRwU_NZCY8f2RHAdP-z402Vq1c9-m2Ew3_695OmV5HoinJQPagY9hI-_EW8nhNWf8l4FE",
@@ -165,3 +167,15 @@ def test_contains() -> None:
assert key1.public_jwk() not in jwkset.jwks
assert key2 not in jwkset.jwks
+
+
+def test_update() -> None:
+ jwks = JwkSet()
+ assert not jwks.jwks
+ key1 = Jwk.generate(alg="ES256")
+ jwks.update(key1.as_jwks())
+ assert key1 in jwks.jwks
+ key2 = Jwk.generate(alg="ES256")
+ jwks.update(key2.as_jwks())
+ assert key2 in jwks.jwks
+ assert key1 in jwks.jwks # don't remove old keys