1import base64
2import json
3import time
4
5from cryptography.hazmat.primitives import hashes
6from cryptography.hazmat.primitives.asymmetric import ec
7from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
8from cryptography.hazmat.primitives.serialization import load_pem_public_key
9
10
11def b64url_decode(part: str) -> bytes:
12 return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))
13
14
15def verify_es256(token: str, public_key_pem: bytes) -> dict:
16 header_b64, payload_b64, sig_b64 = token.split(".")
17
18 header = json.loads(b64url_decode(header_b64))
19 if header["alg"] != "ES256":
20 raise ValueError(f"expected ES256, got {header['alg']}")
21
22 sig = b64url_decode(sig_b64) # 64 raw bytes: r ‖ s
23 der = encode_dss_signature(
24 int.from_bytes(sig[:32], "big"),
25 int.from_bytes(sig[32:], "big"),
26 )
27
28 key = load_pem_public_key(public_key_pem)
29 signed = f"{header_b64}.{payload_b64}".encode("ascii")
30 key.verify(der, signed, ec.ECDSA(hashes.SHA256())) # raises InvalidSignature
31
32 claims = json.loads(b64url_decode(payload_b64))
33 if "exp" in claims and claims["exp"] < time.time():
34 raise ValueError("token expired")
35 return claims