diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..65b0a11 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +core.autocrlf=input +core.eol=lf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1168bb6..e57d687 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: push: branches: [ release ] tags: - - 'v*' + - 'v*.*.*' # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -42,6 +42,9 @@ jobs: addSections: '{"documentation":{"prefix":"**Documentation:**","labels":["documentation"]}}' output: CHANGELOG.md + - name: Install uv + uses: astral-sh/setup-uv@v3 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-versions }} diff --git a/README.md b/README.md index ac3808d..706fce2 100644 --- a/README.md +++ b/README.md @@ -1,441 +1,446 @@ -# ![jwskate](docs/logo.png) - -[![PyPi](https://img.shields.io/pypi/v/jwskate.svg)](https://pypi.python.org/pypi/jwskate) -[![PyPi - License](https://img.shields.io/pypi/l/jwskate)](https://pypi.python.org/pypi/jwskate) -[![PyPI - Downloads](https://img.shields.io/pypi/dw/jwskate)](https://pypi.python.org/pypi/jwskate) -[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) - -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), hence the name **JWSKATE**, and -their extensions [ECDH Signatures][rfc8037] (RFC8037), [JWK Thumbprints][rfc7638] (RFC7638), and -[JWK Thumbprint URI][rfc9278] (RFC9278), with respects to [JWT Best Current Practices][rfc8725] (RFC8725). - -- Free software: MIT -- Repository: https://github.com/guillp/jwskate/ -- Documentation: 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. See [Design](#Design) below for some of the design decisions that lead to `jwskate`. - -## Design - -### Tokens are objects - -Since JSON Web Tokens (JWT) are more and more used, JWT generation and validation must be as easy to do as possible. The -`Jwt` class wraps around a JWT value to allow easy access to its headers, claims and signature, and exposes methods to -easily verify the signature. - -```python -from jwskate import Jwt - -jwt = Jwt( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" -) -assert jwt.headers == {"alg": "HS256", "typ": "JWT"} - -assert jwt.claims == {"sub": "1234567890", "name": "John Doe", "iat": 1516239022} - -assert ( - jwt.signature - == b"I\xf9J\xc7\x04IH\xc7\x8a(]\x90O\x87\xf0\xa4\xc7\x89\x7f~\x8f:N\xb2%_\xdau\x0b,\xc3\x97" -) -``` - -`Jwt` instances always represent a syntactically valid JWT. If you try to initialize one with a malformed value, you -will get a `InvalidJwt` exception, with an helpful error message: - -```python -jwt = Jwt( - "eyJhbGci-malformedheader.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" -) -# jwskate.jwt.base.InvalidJwt: Invalid JWT header: it must be a Base64URL-encoded JSON object -``` - -`Jwt` may be objects, but they are easy to serialize into their representation. Use either `str()` or `bytes()` -depending on what type of value you need, or the `value` attribute: - -```python -jwt = Jwt( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" -) -str(jwt) -# 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' -bytes(jwt) -# b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' -assert jwt.value == bytes(jwt) -``` - -The same is true for JWS and JWE tokens. - -### Headers are auto-generated - -When signing a JWS, JWE or JWT, the headers are autogenerated by default, based on the used key and algorithm. - -You may add your own custom headers using the `extra_headers` parameter, and/or set a custom `typ` header with the parameter of the same name: - -```python -from jwskate import SymmetricJwk, Jwt - -jwk = SymmetricJwk.from_bytes(b"T0t4llyR@nd0M", kid="symmetric_key1") -jwt = Jwt.sign( - claims={"my": "claims"}, - key=jwk, - alg="HS256", - typ="CustomJWT", - extra_headers={"custom_header": "custom_value"}, -) -print(jwt) -# eyJhbGciOiJIUzI1NiIsImN1c3RvbV9oZWFkZXIiOiJjdXN0b21fdmFsdWUiLCJ0eXAiOiJDdXN0b21KV1QiLCJraWQiOiJzeW1tZXRyaWNfa2V5MSJ9.eyJteSI6ImNsYWltcyJ9.ZqCp8Crq-mdCXLoy5NiEdPTSUlIFEjrzexA6mKHrMAc -print(jwt.headers) -# {'alg': 'HS256', 'custom_header': 'custom_value', 'typ': 'CustomJWT', 'kid': 'symmetric_key1'} -``` - -If, for testing purposes, you need to fully control which headers are included in the JWT, even if they are inconsistent, -you can use `Jwt.sign_arbitrary()`: - -```python -from jwskate import SymmetricJwk, Jwt - -jwk = SymmetricJwk.from_bytes(b"T0t4llyR@nd0M", kid="symmetric_key1") -jwt = Jwt.sign_arbitrary( - headers={ - "custom_header": "custom_value", - "typ": "WeirdJWT", - "kid": "R@nd0m_KID", - "alg": "WeirdAlg", - }, - claims={"my": "claims"}, - key=jwk, - alg="HS256", -) -print(jwt) -# eyJjdXN0b21faGVhZGVyIjoiY3VzdG9tX3ZhbHVlIiwidHlwIjoiV2VpcmRKV1QiLCJraWQiOiJSQG5kMG1fS0lEIiwiYWxnIjoiV2VpcmRBbGcifQ.eyJteSI6ImNsYWltcyJ9.bcTFqCSiVIbyJhxClgsBDIyhbvLXTOXOV55QGqo2mhw -print(jwt.headers) # you asked for inconsistent headers, you have them: -# {'custom_header': 'custom_value', 'typ': 'WeirdJWT', 'kid': 'R@nd0m_KID', 'alg': 'WeirdAlg'} -``` - -### `Jwk` as thin wrapper around `cryptography` keys - -`Jwk` keys are just _thin_ wrappers around keys from the `cryptography` module, or, in the case of symmetric keys, -around `bytes`. But, unlike `cryptography`keys, they present a consistent interface for signature creation/verification, -key management, and encryption/decryption, with all available algorithms. - -Everywhere a key is required as parameter, you may pass either a raw `cryptography` key instance, or a `Jwk` instance -(which is actually a thin wrapper around a cryptography key), or a `Mapping` representing the JWK key. - -### `Jwk` are `UserDict` instances - -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. - -Note that the keys with a `JwkSet` are converted to instances of `Jwk` on initialization. This may introduce an issue -if you try to serialize it to JSON with the standard `json` module, which does not handle `UserDict` by default. You may -either use `JwkSet.to_json()` to get a JSON-serialized string, or `JwkSet.to_dict()` to get a standard `dict`, that is -serializable by the standard `json` module. - -### 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 also has a quite inconsistent API to handle the different key types 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](docs/logo.png) + +[![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) +[![PyPi](https://img.shields.io/pypi/v/jwskate.svg)](https://pypi.python.org/pypi/jwskate) +[![PyPi - License](https://img.shields.io/pypi/l/jwskate)](https://pypi.python.org/pypi/jwskate) +[![PyPI - Downloads](https://img.shields.io/pypi/dw/jwskate)](https://pypi.python.org/pypi/jwskate) +[![Supported Versions](https://img.shields.io/pypi/pyversions/jwskate.svg)](https://pypi.org/project/jwskate) +[![PyPI status](https://img.shields.io/pypi/status/jwskate.svg)](https://pypi.python.org/pypi/jwskate/) +[![GitHub commits](https://badgen.net/github/commits/guillp/jwskate)](https://github.com/guillp/jwskate/commit/) +[![GitHub latest commit](https://badgen.net/github/last-commit/guillp/jwskate)](https://github.com/guillp/jwskate/commit/) +[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) + +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), hence the name **JWSKATE**, and +their extensions [ECDH Signatures][rfc8037] (RFC8037), [JWK Thumbprints][rfc7638] (RFC7638), and +[JWK Thumbprint URI][rfc9278] (RFC9278), with respects to [JWT Best Current Practices][rfc8725] (RFC8725). + +- Free software: MIT +- Repository: https://github.com/guillp/jwskate/ +- Documentation: 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. See [Design](#Design) below for some of the design decisions that lead to `jwskate`. + +## Design + +### Tokens are objects + +Since JSON Web Tokens (JWT) are more and more used, JWT generation and validation must be as easy to do as possible. The +`Jwt` class wraps around a JWT value to allow easy access to its headers, claims and signature, and exposes methods to +easily verify the signature. + +```python +from jwskate import Jwt + +jwt = Jwt( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" +) +assert jwt.headers == {"alg": "HS256", "typ": "JWT"} + +assert jwt.claims == {"sub": "1234567890", "name": "John Doe", "iat": 1516239022} + +assert ( + jwt.signature + == b"I\xf9J\xc7\x04IH\xc7\x8a(]\x90O\x87\xf0\xa4\xc7\x89\x7f~\x8f:N\xb2%_\xdau\x0b,\xc3\x97" +) +``` + +`Jwt` instances always represent a syntactically valid JWT. If you try to initialize one with a malformed value, you +will get a `InvalidJwt` exception, with an helpful error message: + +```python +jwt = Jwt( + "eyJhbGci-malformedheader.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" +) +# jwskate.jwt.base.InvalidJwt: Invalid JWT header: it must be a Base64URL-encoded JSON object +``` + +`Jwt` may be objects, but they are easy to serialize into their representation. Use either `str()` or `bytes()` +depending on what type of value you need, or the `value` attribute: + +```python +jwt = Jwt( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" +) +str(jwt) +# 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +bytes(jwt) +# b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +assert jwt.value == bytes(jwt) +``` + +The same is true for JWS and JWE tokens. + +### Headers are auto-generated + +When signing a JWS, JWE or JWT, the headers are autogenerated by default, based on the used key and algorithm. + +You may add your own custom headers using the `extra_headers` parameter, and/or set a custom `typ` header with the parameter of the same name: + +```python +from jwskate import SymmetricJwk, Jwt + +jwk = SymmetricJwk.from_bytes(b"T0t4llyR@nd0M", kid="symmetric_key1") +jwt = Jwt.sign( + claims={"my": "claims"}, + key=jwk, + alg="HS256", + typ="CustomJWT", + extra_headers={"custom_header": "custom_value"}, +) +print(jwt) +# eyJhbGciOiJIUzI1NiIsImN1c3RvbV9oZWFkZXIiOiJjdXN0b21fdmFsdWUiLCJ0eXAiOiJDdXN0b21KV1QiLCJraWQiOiJzeW1tZXRyaWNfa2V5MSJ9.eyJteSI6ImNsYWltcyJ9.ZqCp8Crq-mdCXLoy5NiEdPTSUlIFEjrzexA6mKHrMAc +print(jwt.headers) +# {'alg': 'HS256', 'custom_header': 'custom_value', 'typ': 'CustomJWT', 'kid': 'symmetric_key1'} +``` + +If, for testing purposes, you need to fully control which headers are included in the JWT, even if they are inconsistent, +you can use `Jwt.sign_arbitrary()`: + +```python +from jwskate import SymmetricJwk, Jwt + +jwk = SymmetricJwk.from_bytes(b"T0t4llyR@nd0M", kid="symmetric_key1") +jwt = Jwt.sign_arbitrary( + headers={ + "custom_header": "custom_value", + "typ": "WeirdJWT", + "kid": "R@nd0m_KID", + "alg": "WeirdAlg", + }, + claims={"my": "claims"}, + key=jwk, + alg="HS256", +) +print(jwt) +# eyJjdXN0b21faGVhZGVyIjoiY3VzdG9tX3ZhbHVlIiwidHlwIjoiV2VpcmRKV1QiLCJraWQiOiJSQG5kMG1fS0lEIiwiYWxnIjoiV2VpcmRBbGcifQ.eyJteSI6ImNsYWltcyJ9.bcTFqCSiVIbyJhxClgsBDIyhbvLXTOXOV55QGqo2mhw +print(jwt.headers) # you asked for inconsistent headers, you have them: +# {'custom_header': 'custom_value', 'typ': 'WeirdJWT', 'kid': 'R@nd0m_KID', 'alg': 'WeirdAlg'} +``` + +### `Jwk` as thin wrapper around `cryptography` keys + +`Jwk` keys are just _thin_ wrappers around keys from the `cryptography` module, or, in the case of symmetric keys, +around `bytes`. But, unlike `cryptography`keys, they present a consistent interface for signature creation/verification, +key management, and encryption/decryption, with all available algorithms. + +Everywhere a key is required as parameter, you may pass either a raw `cryptography` key instance, or a `Jwk` instance +(which is actually a thin wrapper around a cryptography key), or a `Mapping` representing the JWK key. + +### `Jwk` are `UserDict` instances + +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. + +Note that the keys with a `JwkSet` are converted to instances of `Jwk` on initialization. This may introduce an issue +if you try to serialize it to JSON with the standard `json` module, which does not handle `UserDict` by default. You may +either use `JwkSet.to_json()` to get a JSON-serialized string, or `JwkSet.to_dict()` to get a standard `dict`, that is +serializable by the standard `json` module. + +### 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 also has a quite inconsistent API to handle the different key types 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