Skip to content
ecdsa.com
Python

JWCrypto

Red Hat's JWCrypto, the JOSE implementation used by FreeIPA and many enterprise Python services: full JWS/JWE/JWK coverage including ML-DSA, a 60-second default leeway, and alg: none only on request.
License
LGPL-3.0-or-later
Checked version
1.6.1 ·
Source
github.com/latchset/jwcrypto
Documentation
jwcrypto.readthedocs.io/en/latest/

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512ES256KEdDSAEd25519Ed448ML-DSA-44ML-DSA-65ML-DSA-87

JWCrypto implements the JOSE family on top of pyca/cryptography: JWK and JWK sets, JWS, JWE and JWT. Everything is key-object based — a jwk.JWK built from PEM, JSON or generate() — which makes the key type explicit at every call. It is licensed LGPL-3.0-or-later, which matters if you redistribute modified copies.

Its algorithm coverage is the broadest of the Python JOSE libraries: the HS, RS, PS and ES families, ES256K, EdDSA and the fully specified Ed25519 and Ed448, and in 1.6.1 also ML-DSA-44, ML-DSA-65 and ML-DSA-87 with AKP keys from the post-quantum JOSE draft — all signed and verified in the check, and all on the default allowed list. That default list is the catch: unless you pass algs, any of them is accepted for a matching key, so narrow it to what your issuer actually uses, as the sample does.

Unsigned tokens are refused with Algorithm not allowed unless none is listed, and an HS256 token presented with an EC public JWK fails, because the key type cannot perform HMAC. ES256 refuses a P-384 key with Curve requested is "P-256", but key curve is "P-384". ES256 signatures are the 64-byte raw r‖s form; a DER signature fails with Verification failed for all signatures — check the encoding in the JWT debugger.

Two defaults differ from other libraries. Expiry checks allow 60 seconds of clock skew: a token that expired 57 seconds earlier still validated, one that expired 63 seconds earlier did not — more forgiving than PyJWT or jose, which allow none. And exp is optional: a token without it validated unless check_claims names it.

Build key sets with the JWKS builder and inspect encrypted tokens with the JWE inspector; for ML-DSA keys and signatures, the post-quantum signature playground shows the sizes involved.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/jwcrypto/run.sh ran JWCrypto 1.6.1 on cryptography 50.0.2 in the container image python:3.14.8-slim-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512ES256KEdDSAEd25519Ed448ML-DSA-44ML-DSA-65ML-DSA-87

    HS, RS, PS and ES at 256/384/512 bits, ES256K, EdDSA, Ed25519, Ed448 and ML-DSA-44/65/87 (with AKP keys) signed and verified; all are on the default allowed list.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, ES256K, EdDSA, Ed25519, Ed448, ML-DSA-44, ML-DSA-65, ML-DSA-87
    evidence jws-algorithms: ML-DSA-44: JWK kty AKP
    evidence jws-algorithms: ML-DSA-65: JWK kty AKP
    evidence jws-algorithms: ML-DSA-87: JWK kty AKP
    evidence jws-algorithms: default allowed list: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, EdDSA, ES256K, Ed25519, Ed448, ML-DSA-44, ML-DSA-65, ML-DSA-87
  • ES256 signature encoding

    raw

    ES256 signatures are 64 bytes of raw r‖s.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A DER-encoded ES256 signature fails with InvalidJWSSignature: Verification failed for all signatures.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: InvalidJWSSignature: Verification failed for all signatures["Failed: [InvalidJWSSignature('Verification failed')]"]
  • Algorithm allow-list

    from-key

    Without algs, the default list applies and the key type decides: an ES256 token verified with an EC key, and an RS256 token against it failed.

    Recorded by the check
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: no algs argument: verified; RS256 token with the EC key: InvalidJWSSignature: Verification failed for all signatures["Failed: [InvalidJWSSignature('Verification failed')]"]
  • Unsigned tokens (alg none)

    opt-in

    An unsigned token fails with Algorithm not allowed by default and verifies only with algs=["none"].

    Recorded by the check
    fact jwt-alg-none = opt-in
    evidence jwt-alg-none: with the EC key: InvalidJWSSignature: Verification failed for all signatures["Failed: [InvalidJWSOperation('Algorithm not allowed')]"]
    evidence jwt-alg-none: algs ['none'] with an empty oct key: verified
  • Public key used as an HMAC secret

    rejected

    An HS256 token checked against the EC public JWK failed; only a deliberately built oct JWK containing the PEM text verified it.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: HS256 token checked with the EC public JWK: InvalidJWSSignature: Verification failed for all signatures["Failed: [InvalidJWSSignature('Verification failed')]"]
    evidence jwt-hmac-public-key: only an explicit oct JWK made from the PEM text verifies it: True
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key raises Curve requested is "P-256", but key curve is "P-384".

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: InvalidJWKValue: Curve requested is "P-256", but key curve is "P-384"
  • Default clock skew

    60s

    Expiry allows 60 seconds of skew by default: a token expired 57 seconds earlier validated, one expired 63 seconds earlier did not.

    Recorded by the check
    fact jwt-clock-skew = 60s
  • Expiry claim required

    no

    A token without exp validated; list exp in check_claims to require it.

    Recorded by the check
    fact jwt-exp-required = no
    evidence jwt-exp-required: verified

Usage

Sign an ES256 token with a key id and verify it with the algorithm list narrowed and iss and exp required.
import json
import time

from jwcrypto import jwk, jwt

key = jwk.JWK.generate(kty="EC", crv="P-256", kid="2026-10")
public = jwk.JWK.from_json(key.export_public())

token = jwt.JWT(header={"alg": "ES256", "kid": "2026-10"}, claims={"sub": "user-42", "iss": "https://auth.example.com", "exp": int(time.time()) + 600})
token.make_signed_token(key)

# algs narrows the default list; check_claims makes iss and exp mandatory.
checked = jwt.JWT(
    jwt=token.serialize(),
    key=public,
    algs=["ES256"],
    check_claims={"iss": "https://auth.example.com", "exp": None},
)
print("sub:", json.loads(checked.claims)["sub"])

The snippet harness runs every sample verbatim in the container image python:3.14.8-slim-trixie, and the run fails unless the output matches.

Setup
pip install --quiet --root-user-action=ignore jwcrypto==1.6.1 cryptography==50.0.2
Command
python usage.py
Output must match (regular expression)
sub: user-42

Libraries