Skip to content

Use constant-time comparison for the JWE auth tag and the OIDC at_hash claim - #411

Open
Nexory wants to merge 1 commit into
mpdavis:masterfrom
Nexory:harden/constant-time-compare
Open

Use constant-time comparison for the JWE auth tag and the OIDC at_hash claim#411
Nexory wants to merge 1 commit into
mpdavis:masterfrom
Nexory:harden/constant-time-compare

Conversation

@Nexory

@Nexory Nexory commented Jun 18, 2026

Copy link
Copy Markdown

Use constant-time comparison for the JWE auth tag and the OIDC at_hash claim

Two authentication-gating comparisons in the library use plain !=, which is
timing-variable and leaks how many leading bytes match (CWE-208):

  • jose/jwe.py _decrypt_and_auth: if auth_tag != auth_tag_check: - 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; this != is the authentication gate, so a
    timing oracle helps an attacker forge a valid tag.
  • jose/jwt.py _validate_at_hash: if claims["at_hash"] != expected_hash: -
    claims["at_hash"] is attacker-influenced (the JWT payload) and
    expected_hash is the server-computed value that binds the ID token to the
    access token (OIDC).

Both are switched to hmac.compare_digest, which is the timing-safe primitive
already used elsewhere in this package (jose/backends/native.py
HMACKey.verify). For at_hash, a non-string claim is treated as invalid (the
previous != semantics) so compare_digest is never called with a non-string.

Behavior is unchanged for valid and invalid inputs; only the comparison is now
constant-time. tests/test_jwe.py and tests/test_jwt.py pass (180 passed, 6
skipped); flake8 is clean.

Refs the open issue #398, which lists both spots.

…h 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 mpdavis#398.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant