Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions jose/jwe.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ def decrypt(jwe_str, key):
try:
cek_bytes = key.unwrap_key(encrypted_key)

# An unwrap that returns the wrong number of bytes is a padding
# failure that did not raise: PKCS1v15 implementations may return
# arbitrary bytes from their constant-time path instead of raising.
# Treat it exactly like a raised error so that length errors stay
# indistinguishable from format and padding errors (RFC 7516 §11.5).
if len(cek_bytes) != len(_get_random_cek_bytes_for_enc(enc)):
raise JWEError("Invalid CEK length")

# Record whether the CEK could be successfully determined for this
# recipient or not.
cek_valid = True
Expand Down
35 changes: 34 additions & 1 deletion tests/test_jwe.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os

import pytest

Expand All @@ -7,7 +8,7 @@
from jose.constants import ALGORITHMS, ZIPS
from jose.exceptions import JWEError, JWEParseError
from jose.jwk import AESKey, RSAKey
from jose.utils import base64url_decode
from jose.utils import base64url_decode, base64url_encode

backends = []
try:
Expand Down Expand Up @@ -357,6 +358,38 @@ def test_non_json_header_is_parse_error(self):
with pytest.raises(JWEParseError):
jwe.decrypt(jwe_str, "key")

@pytest.mark.skipif(RSAKey is None, reason="No RSA backend")
def test_rsa1_5_malformed_keys_are_indistinguishable(self):
"""RFC 7516 §11.5: format, padding and length errors must not be distinguishable.

PKCS1v15 unwrapping does not raise for every malformed encrypted key —
the constant-time path can return arbitrary bytes instead. Those must be
rejected the same way a raised error is, or the resulting length-specific
message becomes a padding oracle.
"""
header = base64url_encode(json.dumps({"alg": "RSA1_5", "enc": "A256GCM"}).encode())

def malformed_token():
return b".".join(
[
header,
base64url_encode(os.urandom(256)),
base64url_encode(os.urandom(12)),
base64url_encode(os.urandom(32)),
base64url_encode(os.urandom(16)),
]
).decode()

messages = set()
for _ in range(50):
with pytest.raises(JWEError) as exc:
jwe.decrypt(malformed_token(), PRIVATE_KEY_PEM)
messages.add(str(exc.value))

assert messages == {
"Invalid JWE Auth Tag"
}, f"malformed keys produced distinguishable errors: {sorted(messages)}"


class TestEncrypt:
@pytest.mark.skipif(AESKey is None, reason="No AES backend")
Expand Down