Package: jwskate
A Pythonic implementation of the JOSE set of IETF specifications:Json Web Signature,Keys,Algorithms,TokensandEncryption(RFC7515 to 7519), hence the nameJWSKATE, and
their extensionsECDH Signatures(RFC8037),JWK Thumbprints(RFC7638), andJWK Thumbprint URI(RFC9278), with respects toJWT Best Current Practices(RFC8725).Free software: MITRepository: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:fromjwskateimportJwk# 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 parameterrsa_private_jwk=Jwk.generate(alg="RS256",key_size=2048)data=b"Signing is easy!"# we will sign thissignature=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 itrsa_public_jwk=rsa_private_jwk.public_jwk()assertrsa_public_jwk.verify(data,signature)# let's see what a `Jwk` looks like:fromcollectionsimportUserDictassertisinstance(rsa_private_jwk,UserDict)# Jwk are UserDictsprint(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:fromjwskateimportJwk,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 JWKprivate_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 JWKprint(private_jwk)# {'kty': 'EC', 'crv': 'P-256', 'x': 'Ppe...', 'y': '9Si...', 'd': 'g09...', 'alg': 'ES256'}assertprivate_jwk.kty=="EC"assertprivate_jwk.crv=="P-256"assertprivate_jwk.alg=="ES256"# this is a private key and 'ES256' is a signature alg, so 'use' and 'key_ops' can also be deduced:assertprivate_jwk.use=="sig"assertprivate_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_DJwassertisinstance(jwt,Jwt)# Jwt are objectsassertjwt.claims==claims# claims can be accessed as a dictassertjwt.headers=={"typ":"JWT","alg":"ES256","kid":"my_key"}# headers tooassertjwt.sub=="some_sub"# individual claims can be accessed as attributesassertjwt["claim1"]=="value1"# or as dict items (with "subscription")assertjwt.alg=="ES256"# alg and kid headers are also accessible as attributesassertjwt.kid==private_jwk.kid# notice that alg and kid are automatically set with appropriate values taken from our private jwkassertisinstance(jwt.signature,bytes)# signature is accessible too# verifying the jwt signature is as easy as:assertjwt.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:assertjwt.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:fromjwskateimportJwk,JwtSignerprivate_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,expandjti), together with theextra_claimsprovided 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'
}FeaturesSimple, Clean, Pythonic interfaceConvenience wrappers aroundcryptographyfor all algorithms described in JWAJson Web Keys (JWK) loading, dumping and generationArbitrary data signature and verification using Json Web KeysJson Web Signatures (JWS) signing and verificationJson Web Encryption (JWE) encryption and decryptionJson Web Tokens (JWT) signing, verification and validation100% type annotated, verified withmypy --strictnearly 100% code coverageRelies oncryptographyfor all cryptographic operationsRelies onBinaPyfor binary data manipulationsSupported Token TypesToken TypeSupportJson Web Signature (JWS)☑ Compact☑ JSON Flat☑ JSON GeneralJson Web Encryption (JWE)☑ Compact☐ JSON Flat☐ JSON GeneralJson Web Tokens (JWT)☑ Signed☑ Signed and EncryptedSupported Signature algorithmsSignature AlgDescriptionKey TypeReferenceNoteHS256HMAC using SHA-256octRFC7518, Section 3.2HS384HMAC using SHA-384octRFC7518, Section 3.2HS512HMAC using SHA-512octRFC7518, Section 3.2RS256RSASSA-PKCS1-v1_5 using SHA-256RSARFC7518, Section 3.3RS384RSASSA-PKCS1-v1_5 using SHA-384RSARFC7518, Section 3.3RS512RSASSA-PKCS1-v1_5 using SHA-512RSARFC7518, Section 3.3PS256RSASSA-PSS using SHA-256 and MGF1 with SHA-256RSARFC7518, Section 3.5PS384RSASSA-PSS using SHA-384 and MGF1 with SHA-384RSARFC7518, Section 3.5PS512RSASSA-PSS using SHA-512 and MGF1 with SHA-512RSARFC7518, Section 3.5ES256ECDSA using P-256 and SHA-256ECRFC7518, Section 3.4ES384ECDSA using P-384 and SHA-384ECRFC7518, Section 3.4ES512ECDSA using P-521 and SHA-512ECRFC7518, Section 3.4ES256KECDSA using secp256k1 curve and SHA-256ECRFC8812, Section 3.2EdDSAEdDSA signature algorithmsOKPRFC8037, Section 3.1Ed2219 and Ed448 are supportedHS1HMAC using SHA-1octhttps://www.w3.org/TR/WebCryptoAPIValidation OnlyRS1RSASSA-PKCS1-v1_5 with SHA-1RSAhttps://www.w3.org/TR/WebCryptoAPIValidation OnlynoneNo digital signature or MAC performedRFC7518, Section 3.6Not usable by mistakeSupported Encryption algorithmsSignature AlgDescriptionReferenceA128CBC-HS256AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithmRFC7518, Section 5.2.3A192CBC-HS384AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithmRFC7518, Section 5.2.4A256CBC-HS512AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithmRFC7518, Section 5.2.5A128GCMAES GCM using 128-bit keyRFC7518, Section 5.3A192GCMAES GCM using 192-bit keyRFC7518, Section 5.3A256GCMAES GCM using 256-bit keyRFC7518, Section 5.3Supported Key Management algorithmsSignature AlgDescriptionKey TypeReferenceNoteRSA1_5RSAES-PKCS1-v1_5RSARFC7518, Section 4.2Unwrap OnlyRSA-OAEPRSAES OAEP using default parametersRSARFC7518, Section 4.3RSA-OAEP-256RSAES OAEP using SHA-256 and MGF1 with SHA-256RSARFC7518, Section 4.3RSA-OAEP-384RSA-OAEP using SHA-384 and MGF1 with SHA-384RSAhttps://www.w3.org/TR/WebCryptoAPIRSA-OAEP-512RSA-OAEP using SHA-512 and MGF1 with SHA-512RSAhttps://www.w3.org/TR/WebCryptoAPIA128KWAES Key Wrap using 128-bit keyoctRFC7518, Section 4.4A192KWAES Key Wrap using 192-bit keyoctRFC7518, Section 4.4A256KWAES Key Wrap using 256-bit keyoctRFC7518, Section 4.4A128GCMKWKey wrapping with AES GCM using 128-bit keyoctRFC7518, Section 4.7A192GCMKWKey wrapping with AES GCM using 192-bit keyoctRFC7518, Section 4.7A256GCMKWKey wrapping with AES GCM using 256-bit keyoctRFC7518, Section 4.7dirDirect use of a shared symmetric keyoctRFC7518, Section 4.5ECDH-ESECDH-ES using Concat KDFECRFC7518, Section 4.6ECDH-ES+A128KWECDH-ES using Concat KDF and "A128KW" wrappingECRFC7518, Section 4.6ECDH-ES+A192KWECDH-ES using Concat KDF and "A192KW" wrappingECRFC7518, Section 4.6ECDH-ES+A256KWECDH-ES using Concat KDF and "A256KW" wrappingECRFC7518, Section 4.6PBES2-HS256+A128KWPBES2 with HMAC SHA-256 and "A128KW" wrappingpasswordRFC7518, Section 4.8PBES2-HS384+A192KWPBES2 with HMAC SHA-384 and "A192KW" wrappingpasswordRFC7518, Section 4.8PBES2-HS512+A256KWPBES2 with HMAC SHA-512 and "A256KW" wrappingpasswordRFC7518, Section 4.8Supported Elliptic CurvesCurveDescriptionKey TypeUsageReferenceP-256P-256 CurveECsignature, encryptionRFC7518, Section 6.2.1.1P-384P-384 CurveECsignature, encryptionRFC7518, Section 6.2.1.1P-521P-521 CurveECsignature, encryptionRFC7518, Section 6.2.1.1secp256k1SECG secp256k1 curveECsignature, encryptionRFC8812, Section 3.1Ed25519Ed25519 signature algorithm key pairsOKPsignatureRFC8037, Section 3.1Ed448Ed448 signature algorithm key pairsOKPsignatureRFC8037, Section 3.1X25519X25519 function key pairsOKPencryptionRFC8037, Section 3.2X448X448 function key pairsOKPencryptionRFC8037, Section 3.2Why 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.PyJWTJWCryptoPython-JOSEAuthLibNot to say that those arebadlibs (I actually usejwcryptomyself forjwskateunit 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. SeeDesignbelow for some of the design decisions that lead tojwskate.DesignTokens are objectsSince JSON Web Tokens (JWT) are more and more used, JWT generation and validation must be as easy to do as possible. TheJwtclass wraps around a JWT value to allow easy access to its headers, claims and signature, and exposes methods to
easily verify the signature.fromjwskateimportJwtjwt=Jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")assertjwt.headers=={"alg":"HS256","typ":"JWT"}assertjwt.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")Jwtinstances always represent a syntactically valid JWT. If you try to initialize one with a malformed value, you
will get aInvalidJwtexception, with an helpful error message:jwt=Jwt("eyJhbGci-malformedheader.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")# jwskate.jwt.base.InvalidJwt: Invalid JWT header: it must be a Base64URL-encoded JSON objectJwtmay be objects, but they are easy to serialize into their representation. Use eitherstr()orbytes()depending on what type of value you need, or thevalueattribute:jwt=Jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")str(jwt)# 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'bytes(jwt)# b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'assertjwt.value==bytes(jwt)The same is true for JWS and JWE tokens.Headers are auto-generatedWhen 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 theextra_headersparameter, and/or set a customtypheader with the parameter of the same name:fromjwskateimportSymmetricJwk,Jwtjwk=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-mdCXLoy5NiEdPTSUlIFEjrzexA6mKHrMAcprint(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 useJwt.sign_arbitrary():fromjwskateimportSymmetricJwk,Jwtjwk=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.bcTFqCSiVIbyJhxClgsBDIyhbvLXTOXOV55QGqo2mhwprint(jwt.headers)# you asked for inconsistent headers, you have them:# {'custom_header': 'custom_value', 'typ': 'WeirdJWT', 'kid': 'R@nd0m_KID', 'alg': 'WeirdAlg'}Jwkas thin wrapper aroundcryptographykeysJwkkeys are justthinwrappers around keys from thecryptographymodule, or, in the case of symmetric keys,
aroundbytes. But, unlikecryptographykeys, 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 rawcryptographykey instance, or aJwkinstance
(which is actually a thin wrapper around a cryptography key), or aMappingrepresenting the JWK key.JwkareUserDictinstancesJWK are specified as JSON objects, which are parsed asdictin Python. TheJwkclass injwskateis actually aUserDictsubclass, which is very similar to a standarddict. So you can use it exactly like you would use adict:
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 aJwkSetare converted to instances ofJwkon initialization. This may introduce an issue
if you try to serialize it to JSON with the standardjsonmodule, which does not handleUserDictby default. You may
either useJwkSet.to_json()to get a JSON-serialized string, orJwkSet.to_dict()to get a standarddict, that is
serializable by the standardjsonmodule.JWA WrappersYou can usecryptographyto do the cryptographic operations that are described inJWA, but sincecryptographyis 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,jwskatecomes with a set of consistent wrappers that implement the exact JWA specifications, with minimum risk of
mistakes.Safe Signature VerificationAs advised inJWT Best Practices$3.1:For every signature verification method injwskate, 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:analgparameter which contains the expected algorithm, or analgsparameter which contains a list of acceptable
algorithmsthealgparameter from the signature verificationJwk, if present. Thisalgis the algorithm intended for use
with that key.Note that you cannot usealgandalgsat the same time. If yourJwkcontains analgparameter, and you provide
analgoralgswhich does not match that value, aWarningwill be emitted.TODOComplete/enhance/proof-read documentationBetter exceptions (create dedicated exception classes, better messages, etc.)Support for JWE in JSON formatBetter testsSupport for Selective-Disclosure JWTCreditsAll cryptographic operations are handled bycryptography.