From a315910d2649659cdb351a6604b9abcc67b0a3b6 Mon Sep 17 00:00:00 2001 From: Nexory Date: Thu, 18 Jun 2026 14:40:19 +0200 Subject: [PATCH] Use constant-time comparison for the JWE auth tag and the OIDC at_hash claim Two authentication-gating comparisons used a plain `!=`, which is timing-variable and leaks how many leading bytes match (CWE-208): - jose/jwe.py `_decrypt_and_auth`: for the HMAC enc modes (A128CBC-HS256 / A192CBC-HS384 / A256CBC-HS512) `auth_tag_check` is the freshly computed HMAC and `auth_tag` is the attacker-supplied tag from the JWE, so this is the authentication gate. - jose/jwt.py `_validate_at_hash`: `claims["at_hash"]` is attacker-influenced (the JWT payload) and `expected_hash` binds the ID token to the access token (OIDC). Both now use hmac.compare_digest, the timing-safe primitive already used in jose/backends/native.py (HMACKey.verify). For at_hash a non-string claim is treated as invalid (preserving the previous semantics) so compare_digest is never called with a non-string. Refs #398. --- jose/jwe.py | 3 ++- jose/jwt.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jose/jwe.py b/jose/jwe.py index 09e5c32..3e12e95 100644 --- a/jose/jwe.py +++ b/jose/jwe.py @@ -1,4 +1,5 @@ import binascii +import hmac import json import zlib from collections.abc import Mapping @@ -244,7 +245,7 @@ def _decrypt_and_auth(cek_bytes, enc, cipher_text, iv, aad, auth_tag): raise NotImplementedError(f"enc {enc} is not implemented!") plaintext = encryption_key.decrypt(cipher_text, iv, aad, auth_tag) - if auth_tag != auth_tag_check: + if not hmac.compare_digest(auth_tag, auth_tag_check): raise JWEError("Invalid JWE Auth Tag") return plaintext diff --git a/jose/jwt.py b/jose/jwt.py index f47e4dd..01bfb9f 100644 --- a/jose/jwt.py +++ b/jose/jwt.py @@ -1,3 +1,4 @@ +import hmac import json from calendar import timegm from datetime import datetime, timedelta @@ -468,7 +469,8 @@ def _validate_at_hash(claims, access_token, algorithm): msg = "Unable to calculate at_hash to verify against token claims." raise JWTClaimsError(msg) - if claims["at_hash"] != expected_hash: + at_hash = claims["at_hash"] + if not isinstance(at_hash, str) or not hmac.compare_digest(at_hash, expected_hash): raise JWTClaimsError("at_hash claim does not match access_token.")