diff --git a/CVE-2024-33663.patch b/CVE-2024-33663.patch new file mode 100644 index 0000000..2bd58a9 --- /dev/null +++ b/CVE-2024-33663.patch @@ -0,0 +1,599 @@ +From 34bd82c43ea31da5b9deaa25ff591905a180bdf7 Mon Sep 17 00:00:00 2001 +From: Daniel Garcia Moreno +Date: Thu, 2 May 2024 09:29:54 +0200 +Subject: [PATCH 1/4] Improve asymmetric key check in CryptographyHMACKey + +This change should fix https://github.com/mpdavis/python-jose/issues/346 +security issue. + +The code is based on pyjwt change: +https://github.com/jpadilla/pyjwt/commit/9c528670c455b8d948aff95ed50e22940d1ad3fc +--- + jose/backends/cryptography_backend.py | 72 ++++++++++++++++++++++++--- + tests/test_jwt.py | 35 ++++++++++++- + 2 files changed, 98 insertions(+), 9 deletions(-) + +Index: python-jose-3.3.0/jose/backends/cryptography_backend.py +=================================================================== +--- python-jose-3.3.0.orig/jose/backends/cryptography_backend.py ++++ python-jose-3.3.0/jose/backends/cryptography_backend.py +@@ -17,6 +17,7 @@ from cryptography.x509 import load_pem_x + from ..constants import ALGORITHMS + from ..exceptions import JWEError, JWKError + from ..utils import base64_to_long, base64url_decode, base64url_encode, ensure_binary, long_to_base64 ++from ..utils import is_pem_format, is_ssh_key + from .base import Key + + _binding = None +@@ -552,14 +553,7 @@ class CryptographyHMACKey(Key): + if isinstance(key, str): + key = key.encode("utf-8") + +- invalid_strings = [ +- b"-----BEGIN PUBLIC KEY-----", +- b"-----BEGIN RSA PUBLIC KEY-----", +- b"-----BEGIN CERTIFICATE-----", +- b"ssh-rsa", +- ] +- +- if any(string_value in key for string_value in invalid_strings): ++ if is_pem_format(key) or is_ssh_key(key): + raise JWKError( + "The specified key is an asymmetric key or x509 certificate and" + " should not be used as an HMAC secret." +Index: python-jose-3.3.0/tests/test_jwt.py +=================================================================== +--- python-jose-3.3.0.orig/tests/test_jwt.py ++++ python-jose-3.3.0/tests/test_jwt.py +@@ -5,7 +5,8 @@ from datetime import datetime, timedelta + import pytest + + from jose import jws, jwt +-from jose.exceptions import JWTError ++from jose.constants import ALGORITHMS ++from jose.exceptions import JWTError, JWKError + + + @pytest.fixture +@@ -56,7 +57,7 @@ class TestJWT: + ], + ) + def test_numeric_key(self, key, token): +- token_info = jwt.decode(token, key) ++ token_info = jwt.decode(token, key, algorithms=ALGORITHMS.SUPPORTED) + assert token_info == {"name": "test"} + + def test_invalid_claims_json(self): +@@ -108,7 +109,7 @@ class TestJWT: + + def test_non_default_headers(self, claims, key, headers): + encoded = jwt.encode(claims, key, headers=headers) +- decoded = jwt.decode(encoded, key) ++ decoded = jwt.decode(encoded, key, algorithms=ALGORITHMS.HS256) + assert claims == decoded + all_headers = jwt.get_unverified_headers(encoded) + for k, v in headers.items(): +@@ -161,7 +162,7 @@ class TestJWT: + + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJhIjoiYiJ9" ".jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8" + +- decoded = jwt.decode(token, key) ++ decoded = jwt.decode(token, key, algorithms=ALGORITHMS.SUPPORTED) + + assert decoded == claims + +@@ -193,7 +194,7 @@ class TestJWT: + options = {"leeway": leeway} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, options=options) ++ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256) + + def test_iat_not_int(self, key): + +@@ -202,7 +203,7 @@ class TestJWT: + token = jwt.encode(claims, key) + + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_nbf_not_int(self, key): + +@@ -211,7 +212,7 @@ class TestJWT: + token = jwt.encode(claims, key) + + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_nbf_datetime(self, key): + +@@ -220,7 +221,7 @@ class TestJWT: + claims = {"nbf": nbf} + + token = jwt.encode(claims, key) +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_nbf_with_leeway(self, key): + +@@ -233,7 +234,7 @@ class TestJWT: + options = {"leeway": 10} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, options=options) ++ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256) + + def test_nbf_in_future(self, key): + +@@ -244,7 +245,7 @@ class TestJWT: + token = jwt.encode(claims, key) + + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_nbf_skip(self, key): + +@@ -255,11 +256,11 @@ class TestJWT: + token = jwt.encode(claims, key) + + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + options = {"verify_nbf": False} + +- jwt.decode(token, key, options=options) ++ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256) + + def test_exp_not_int(self, key): + +@@ -268,7 +269,7 @@ class TestJWT: + token = jwt.encode(claims, key) + + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_exp_datetime(self, key): + +@@ -277,7 +278,7 @@ class TestJWT: + claims = {"exp": exp} + + token = jwt.encode(claims, key) +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_exp_with_leeway(self, key): + +@@ -290,7 +291,7 @@ class TestJWT: + options = {"leeway": 10} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, options=options) ++ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256) + + def test_exp_in_past(self, key): + +@@ -301,7 +302,7 @@ class TestJWT: + token = jwt.encode(claims, key) + + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_exp_skip(self, key): + +@@ -312,11 +313,11 @@ class TestJWT: + token = jwt.encode(claims, key) + + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + options = {"verify_exp": False} + +- jwt.decode(token, key, options=options) ++ jwt.decode(token, key, options=options, algorithms=ALGORITHMS.HS256) + + def test_aud_string(self, key): + +@@ -325,7 +326,7 @@ class TestJWT: + claims = {"aud": aud} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, audience=aud) ++ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256) + + def test_aud_list(self, key): + +@@ -334,7 +335,7 @@ class TestJWT: + claims = {"aud": [aud]} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, audience=aud) ++ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256) + + def test_aud_list_multiple(self, key): + +@@ -343,7 +344,7 @@ class TestJWT: + claims = {"aud": [aud, "another"]} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, audience=aud) ++ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256) + + def test_aud_list_is_strings(self, key): + +@@ -353,7 +354,7 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key, audience=aud) ++ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256) + + def test_aud_case_sensitive(self, key): + +@@ -363,14 +364,14 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key, audience="AUDIENCE") ++ jwt.decode(token, key, audience="AUDIENCE", algorithms=ALGORITHMS.HS256) + + def test_aud_empty_claim(self, claims, key): + + aud = "audience" + + token = jwt.encode(claims, key) +- jwt.decode(token, key, audience=aud) ++ jwt.decode(token, key, audience=aud, algorithms=ALGORITHMS.HS256) + + def test_aud_not_string_or_list(self, key): + +@@ -380,7 +381,7 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_aud_given_number(self, key): + +@@ -390,7 +391,7 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key, audience=1) ++ jwt.decode(token, key, audience=1, algorithms=ALGORITHMS.HS256) + + def test_iss_string(self, key): + +@@ -399,7 +400,7 @@ class TestJWT: + claims = {"iss": iss} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, issuer=iss) ++ jwt.decode(token, key, issuer=iss, algorithms=ALGORITHMS.HS256) + + def test_iss_list(self, key): + +@@ -408,7 +409,7 @@ class TestJWT: + claims = {"iss": iss} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, issuer=["https://issuer", "issuer"]) ++ jwt.decode(token, key, issuer=["https://issuer", "issuer"], algorithms=ALGORITHMS.HS256) + + def test_iss_tuple(self, key): + +@@ -417,7 +418,7 @@ class TestJWT: + claims = {"iss": iss} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, issuer=("https://issuer", "issuer")) ++ jwt.decode(token, key, issuer=("https://issuer", "issuer"), algorithms=ALGORITHMS.HS256) + + def test_iss_invalid(self, key): + +@@ -427,7 +428,7 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key, issuer="another") ++ jwt.decode(token, key, issuer="another", algorithms=ALGORITHMS.HS256) + + def test_sub_string(self, key): + +@@ -436,7 +437,7 @@ class TestJWT: + claims = {"sub": sub} + + token = jwt.encode(claims, key) +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_sub_invalid(self, key): + +@@ -446,7 +447,7 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_sub_correct(self, key): + +@@ -455,7 +456,7 @@ class TestJWT: + claims = {"sub": sub} + + token = jwt.encode(claims, key) +- jwt.decode(token, key, subject=sub) ++ jwt.decode(token, key, subject=sub, algorithms=ALGORITHMS.HS256) + + def test_sub_incorrect(self, key): + +@@ -465,7 +466,7 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key, subject="another") ++ jwt.decode(token, key, subject="another", algorithms=ALGORITHMS.HS256) + + def test_jti_string(self, key): + +@@ -474,7 +475,7 @@ class TestJWT: + claims = {"jti": jti} + + token = jwt.encode(claims, key) +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_jti_invalid(self, key): + +@@ -484,33 +485,33 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_at_hash(self, claims, key): + access_token = "" + token = jwt.encode(claims, key, access_token=access_token) +- payload = jwt.decode(token, key, access_token=access_token) ++ payload = jwt.decode(token, key, access_token=access_token, algorithms=ALGORITHMS.HS256) + assert "at_hash" in payload + + def test_at_hash_invalid(self, claims, key): + token = jwt.encode(claims, key, access_token="") + with pytest.raises(JWTError): +- jwt.decode(token, key, access_token="") ++ jwt.decode(token, key, access_token="", algorithms=ALGORITHMS.HS256) + + def test_at_hash_missing_access_token(self, claims, key): + token = jwt.encode(claims, key, access_token="") + with pytest.raises(JWTError): +- jwt.decode(token, key) ++ jwt.decode(token, key, algorithms=ALGORITHMS.HS256) + + def test_at_hash_missing_claim(self, claims, key): + token = jwt.encode(claims, key) +- payload = jwt.decode(token, key, access_token="") ++ payload = jwt.decode(token, key, access_token="", algorithms=ALGORITHMS.HS256) + assert "at_hash" not in payload + + def test_at_hash_unable_to_calculate(self, claims, key): + token = jwt.encode(claims, key, access_token="") + with pytest.raises(JWTError): +- jwt.decode(token, key, access_token="\xe2") ++ jwt.decode(token, key, access_token="\xe2", algorithms=ALGORITHMS.HS256) + + def test_bad_claims(self): + bad_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.iOJ5SiNfaNO_pa2J4Umtb3b3zmk5C18-mhTCVNsjnck" +@@ -548,9 +549,48 @@ class TestJWT: + + token = jwt.encode(claims, key) + with pytest.raises(JWTError): +- jwt.decode(token, key, options=options, audience=str(value)) ++ jwt.decode(token, key, options=options, audience=str(value), algorithms=ALGORITHMS.HS256) + + new_claims = dict(claims) + new_claims[claim] = value + token = jwt.encode(new_claims, key) +- jwt.decode(token, key, options=options, audience=str(value)) ++ jwt.decode(token, key, options=options, audience=str(value), algorithms=ALGORITHMS.HS256) ++ ++ def test_CVE_2024_33663(self): ++ """Test based on https://github.com/mpdavis/python-jose/issues/346""" ++ try: ++ from Crypto.PublicKey import ECC ++ from Crypto.Hash import HMAC, SHA256 ++ except ModuleNotFoundError: ++ pytest.skip("pycryptodome module not installed") ++ ++ # ----- SETUP ----- ++ # generate an asymmetric ECC keypair ++ # !! signing should only be possible with the private key !! ++ KEY = ECC.generate(curve='P-256') ++ ++ # PUBLIC KEY, AVAILABLE TO USER ++ # CAN BE RECOVERED THROUGH E.G. PUBKEY RECOVERY WITH TWO SIGNATURES: ++ # https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery ++ # https://github.com/FlorianPicca/JWT-Key-Recovery ++ PUBKEY = KEY.public_key().export_key(format='OpenSSH').encode() ++ ++ # ---- CLIENT SIDE ----- ++ # without knowing the private key, a valid token can be constructed ++ # YIKES!! ++ ++ b64 = lambda x:base64.urlsafe_b64encode(x).replace(b'=',b'') ++ payload = b64(b'{"alg":"HS256"}') + b'.' + b64(b'{"pwned":true}') ++ hasher = HMAC.new(PUBKEY, digestmod=SHA256) ++ hasher.update(payload) ++ evil_token = payload + b'.' + b64(hasher.digest()) ++ ++ # ---- SERVER SIDE ----- ++ # verify and decode the token using the public key, as is custom ++ # algorithm field is left unspecified ++ # but the library will happily still verify without warning, trusting the user-controlled alg field of the token header ++ with pytest.raises(JWKError): ++ data = jwt.decode(evil_token, PUBKEY, algorithms=ALGORITHMS.HS256) ++ ++ with pytest.raises(JWTError, match='.*required.*"algorithms".*'): ++ data = jwt.decode(evil_token, PUBKEY) +Index: python-jose-3.3.0/jose/jwt.py +=================================================================== +--- python-jose-3.3.0.orig/jose/jwt.py ++++ python-jose-3.3.0/jose/jwt.py +@@ -138,6 +138,14 @@ def decode(token, key, algorithms=None, + + verify_signature = defaults.get("verify_signature", True) + ++ # Forbid the usage of the jwt.decode without alogrightms parameter ++ # See https://github.com/mpdavis/python-jose/issues/346 for more ++ # information CVE-2024-33663 ++ if verify_signature and algorithms is None: ++ raise JWTError("It is required that you pass in a value for " ++ 'the "algorithms" argument when calling ' ++ "decode().") ++ + try: + payload = jws.verify(token, key, algorithms, verify=verify_signature) + except JWSError as e: +Index: python-jose-3.3.0/jose/backends/native.py +=================================================================== +--- python-jose-3.3.0.orig/jose/backends/native.py ++++ python-jose-3.3.0/jose/backends/native.py +@@ -6,6 +6,7 @@ from jose.backends.base import Key + from jose.constants import ALGORITHMS + from jose.exceptions import JWKError + from jose.utils import base64url_decode, base64url_encode ++from jose.utils import is_pem_format, is_ssh_key + + + def get_random_bytes(num_bytes): +@@ -36,14 +37,7 @@ class HMACKey(Key): + if isinstance(key, str): + key = key.encode("utf-8") + +- invalid_strings = [ +- b"-----BEGIN PUBLIC KEY-----", +- b"-----BEGIN RSA PUBLIC KEY-----", +- b"-----BEGIN CERTIFICATE-----", +- b"ssh-rsa", +- ] +- +- if any(string_value in key for string_value in invalid_strings): ++ if is_pem_format(key) or is_ssh_key(key): + raise JWKError( + "The specified key is an asymmetric key or x509 certificate and" + " should not be used as an HMAC secret." +Index: python-jose-3.3.0/jose/utils.py +=================================================================== +--- python-jose-3.3.0.orig/jose/utils.py ++++ python-jose-3.3.0/jose/utils.py +@@ -1,3 +1,4 @@ ++import re + import base64 + import struct + +@@ -106,3 +107,75 @@ def ensure_binary(s): + if isinstance(s, str): + return s.encode("utf-8", "strict") + raise TypeError(f"not expecting type '{type(s)}'") ++ ++ ++# Based on https://github.com/jpadilla/pyjwt/commit/9c528670c455b8d948aff95ed50e22940d1ad3fc ++# Based on https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252 ++_PEMS = { ++ b"CERTIFICATE", ++ b"TRUSTED CERTIFICATE", ++ b"PRIVATE KEY", ++ b"PUBLIC KEY", ++ b"ENCRYPTED PRIVATE KEY", ++ b"OPENSSH PRIVATE KEY", ++ b"DSA PRIVATE KEY", ++ b"RSA PRIVATE KEY", ++ b"RSA PUBLIC KEY", ++ b"EC PRIVATE KEY", ++ b"DH PARAMETERS", ++ b"NEW CERTIFICATE REQUEST", ++ b"CERTIFICATE REQUEST", ++ b"SSH2 PUBLIC KEY", ++ b"SSH2 ENCRYPTED PRIVATE KEY", ++ b"X509 CRL", ++} ++ ++ ++_PEM_RE = re.compile( ++ b"----[- ]BEGIN (" ++ + b"|".join(_PEMS) ++ + b""")[- ]----\r? ++.+?\r? ++----[- ]END \\1[- ]----\r?\n?""", ++ re.DOTALL, ++) ++ ++ ++def is_pem_format(key): ++ """ ++ Return True if the key is PEM format ++ This function uses the list of valid PEM headers defined in ++ _PEMS dict. ++ """ ++ return bool(_PEM_RE.search(key)) ++ ++ ++# Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46 ++_CERT_SUFFIX = b"-cert-v01@openssh.com" ++_SSH_PUBKEY_RC = re.compile(br"\A(\S+)[ \t]+(\S+)") ++_SSH_KEY_FORMATS = [ ++ b"ssh-ed25519", ++ b"ssh-rsa", ++ b"ssh-dss", ++ b"ecdsa-sha2-nistp256", ++ b"ecdsa-sha2-nistp384", ++ b"ecdsa-sha2-nistp521", ++] ++ ++ ++def is_ssh_key(key): ++ """ ++ Return True if the key is a SSH key ++ This function uses the list of valid SSH key format defined in ++ _SSH_KEY_FORMATS dict. ++ """ ++ if any(string_value in key for string_value in _SSH_KEY_FORMATS): ++ return True ++ ++ ssh_pubkey_match = _SSH_PUBKEY_RC.match(key) ++ if ssh_pubkey_match: ++ key_type = ssh_pubkey_match.group(1) ++ if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]: ++ return True ++ ++ return False +Index: python-jose-3.3.0/tests/algorithms/test_HMAC.py +=================================================================== +--- python-jose-3.3.0.orig/tests/algorithms/test_HMAC.py ++++ python-jose-3.3.0/tests/algorithms/test_HMAC.py +@@ -14,14 +14,17 @@ class TestHMACAlgorithm: + + def test_RSA_key(self): + key = "-----BEGIN PUBLIC KEY-----" ++ key += "\n\n\n-----END PUBLIC KEY-----" + with pytest.raises(JOSEError): + HMACKey(key, ALGORITHMS.HS256) + + key = "-----BEGIN RSA PUBLIC KEY-----" ++ key += "\n\n\n-----END RSA PUBLIC KEY-----" + with pytest.raises(JOSEError): + HMACKey(key, ALGORITHMS.HS256) + + key = "-----BEGIN CERTIFICATE-----" ++ key += "\n\n\n-----END CERTIFICATE-----" + with pytest.raises(JOSEError): + HMACKey(key, ALGORITHMS.HS256) + diff --git a/CVE-2024-33664.patch b/CVE-2024-33664.patch new file mode 100644 index 0000000..e04b6a9 --- /dev/null +++ b/CVE-2024-33664.patch @@ -0,0 +1,135 @@ +From ff3357d9f91b93bc957aac9bc5a447c5c0bb74da Mon Sep 17 00:00:00 2001 +From: "alistair.watts@groupbc.com" +Date: Tue, 7 May 2024 14:50:53 +0100 +Subject: [PATCH] Fix for CVE-2024-33664. JWE limited to 250K + +--- + jose/constants.py | 2 ++ + jose/jwe.py | 24 ++++++++++++++++++------ + tests/test_jwe.py | 34 +++++++++++++++++++++++++++++++++- + 3 files changed, 53 insertions(+), 7 deletions(-) + +diff --git a/jose/constants.py b/jose/constants.py +index ab4d74d3..58787d46 100644 +--- a/jose/constants.py ++++ b/jose/constants.py +@@ -96,3 +96,5 @@ class Zips: + + + ZIPS = Zips() ++ ++JWE_SIZE_LIMIT = 250 * 1024 +diff --git a/jose/jwe.py b/jose/jwe.py +index 2c387ff4..04923873 100644 +--- a/jose/jwe.py ++++ b/jose/jwe.py +@@ -6,7 +6,7 @@ + + from . import jwk + from .backends import get_random_bytes +-from .constants import ALGORITHMS, ZIPS ++from .constants import ALGORITHMS, ZIPS, JWE_SIZE_LIMIT + from .exceptions import JWEError, JWEParseError + from .utils import base64url_decode, base64url_encode, ensure_binary + +@@ -76,6 +76,13 @@ def decrypt(jwe_str, key): + >>> jwe.decrypt(jwe_string, 'asecret128bitkey') + 'Hello, World!' + """ ++ ++ # Limit the token size - if the data is compressed then decompressing the ++ # data could lead to large memory usage. This helps address This addresses ++ # CVE-2024-33664. Also see _decompress() ++ if len(jwe_str) > JWE_SIZE_LIMIT: ++ raise JWEError("JWE string exceeds {JWE_SIZE_LIMIT} bytes") ++ + header, encoded_header, encrypted_key, iv, cipher_text, auth_tag = _jwe_compact_deserialize(jwe_str) + + # Verify that the implementation understands and can process all +@@ -424,13 +431,13 @@ def _compress(zip, plaintext): + (bytes): Compressed plaintext + """ + if zip not in ZIPS.SUPPORTED: +- raise NotImplementedError("ZIP {} is not supported!") ++ raise NotImplementedError(f"ZIP {zip} is not supported!") + if zip is None: + compressed = plaintext + elif zip == ZIPS.DEF: + compressed = zlib.compress(plaintext) + else: +- raise NotImplementedError("ZIP {} is not implemented!") ++ raise NotImplementedError(f"ZIP {zip} is not implemented!") + return compressed + + +@@ -446,13 +453,18 @@ def _decompress(zip, compressed): + (bytes): Compressed plaintext + """ + if zip not in ZIPS.SUPPORTED: +- raise NotImplementedError("ZIP {} is not supported!") ++ raise NotImplementedError(f"ZIP {zip} is not supported!") + if zip is None: + decompressed = compressed + elif zip == ZIPS.DEF: +- decompressed = zlib.decompress(compressed) ++ # If, during decompression, there is more data than expected, the ++ # decompression halts and raise an error. This addresses CVE-2024-33664 ++ decompressor = zlib.decompressobj() ++ decompressed = decompressor.decompress(compressed, max_length=JWE_SIZE_LIMIT) ++ if decompressor.unconsumed_tail: ++ raise JWEError(f"Decompressed JWE string exceeds {JWE_SIZE_LIMIT} bytes") + else: +- raise NotImplementedError("ZIP {} is not implemented!") ++ raise NotImplementedError(f"ZIP {zip} is not implemented!") + return decompressed + + +diff --git a/tests/test_jwe.py b/tests/test_jwe.py +index f089d565..8c5ff387 100644 +--- a/tests/test_jwe.py ++++ b/tests/test_jwe.py +@@ -5,7 +5,7 @@ + import jose.backends + from jose import jwe + from jose.constants import ALGORITHMS, ZIPS +-from jose.exceptions import JWEParseError ++from jose.exceptions import JWEParseError, JWEError + from jose.jwk import AESKey, RSAKey + from jose.utils import base64url_decode + +@@ -525,3 +525,35 @@ def test_kid_header_not_present_when_not_provided(self): + encrypted = jwe.encrypt("Text", PUBLIC_KEY_PEM, enc, alg) + header = json.loads(base64url_decode(encrypted.split(b".")[0])) + assert "kid" not in header ++ ++ @pytest.mark.skipif(AESKey is None, reason="No AES backend") ++ def test_jwe_with_excessive_data(self): ++ enc = ALGORITHMS.A256CBC_HS512 ++ alg = ALGORITHMS.RSA_OAEP_256 ++ import jose.constants ++ old_limit = jose.constants.JWE_SIZE_LIMIT ++ try: ++ jose.constants.JWE_SIZE_LIMIT = 1024 ++ encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg) ++ header = json.loads(base64url_decode(encrypted.split(b".")[0])) ++ with pytest.raises(JWEError): ++ actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) ++ finally: ++ jose.constants.JWE_SIZE_LIMIT = old_limit ++ ++ @pytest.mark.skipif(AESKey is None, reason="No AES backend") ++ def test_jwe_zip_with_excessive_data(self): ++ # Test that a fix for CVE-2024-33664 is in place. ++ enc = ALGORITHMS.A256CBC_HS512 ++ alg = ALGORITHMS.RSA_OAEP_256 ++ import jose.constants ++ old_limit = jose.constants.JWE_SIZE_LIMIT ++ try: ++ jose.constants.JWE_SIZE_LIMIT = 1024 ++ encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg, zip=ZIPS.DEF) ++ assert len(encrypted) < jose.constants.JWE_SIZE_LIMIT ++ header = json.loads(base64url_decode(encrypted.split(b".")[0])) ++ with pytest.raises(JWEError): ++ actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) ++ finally: ++ jose.constants.JWE_SIZE_LIMIT = old_limit diff --git a/fix-tests-ecdsa-019.patch b/fix-tests-ecdsa-019.patch new file mode 100644 index 0000000..147481a --- /dev/null +++ b/fix-tests-ecdsa-019.patch @@ -0,0 +1,60 @@ +From ec5c62249b4f67b15376d3cbc96d2b1d272d0552 Mon Sep 17 00:00:00 2001 +From: Daniel Garcia Moreno +Date: Thu, 2 May 2024 18:47:43 +0200 +Subject: [PATCH] test: Fix tests with ecdsa 0.19.0 + +Fix https://github.com/mpdavis/python-jose/issues/348 +--- + tests/algorithms/test_EC_compat.py | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +Index: python-jose-3.3.0/tests/algorithms/test_EC_compat.py +=================================================================== +--- python-jose-3.3.0.orig/tests/algorithms/test_EC_compat.py ++++ python-jose-3.3.0/tests/algorithms/test_EC_compat.py +@@ -37,7 +37,7 @@ class TestBackendEcdsaCompatibility: + key = BackendFrom(private_key, ALGORITHMS.ES256) + key2 = BackendTo(private_key, ALGORITHMS.ES256) + +- assert key.public_key().to_pem().strip() == key2.public_key().to_pem().strip() ++ assert key.public_key().to_pem().strip().replace(b"\n", b"") == key2.public_key().to_pem().strip().replace(b"\n", b"") + + @pytest.mark.parametrize("BackendFrom", [ECDSAECKey, CryptographyECKey]) + @pytest.mark.parametrize("BackendTo", [ECDSAECKey, CryptographyECKey]) +@@ -45,7 +45,7 @@ class TestBackendEcdsaCompatibility: + key = BackendFrom(private_key, ALGORITHMS.ES256) + key2 = BackendTo(private_key, ALGORITHMS.ES256) + +- assert key.to_pem().strip() == key2.to_pem().strip() ++ assert key.to_pem().strip().replace(b"\n", b"") == key2.to_pem().strip().replace(b"\n", b"") + + @pytest.mark.parametrize("BackendFrom", [ECDSAECKey, CryptographyECKey]) + @pytest.mark.parametrize("BackendTo", [ECDSAECKey, CryptographyECKey]) +@@ -57,7 +57,7 @@ class TestBackendEcdsaCompatibility: + + pub_target = BackendTo(pub_pem_source, ALGORITHMS.ES256) + +- assert pub_pem_source == pub_target.to_pem().strip() ++ assert pub_pem_source.replace(b"\n", b"") == pub_target.to_pem().strip().replace(b"\n", b"") + + @pytest.mark.parametrize("BackendFrom", [ECDSAECKey, CryptographyECKey]) + @pytest.mark.parametrize("BackendTo", [ECDSAECKey, CryptographyECKey]) +@@ -68,4 +68,4 @@ class TestBackendEcdsaCompatibility: + + target = BackendTo(pem_source, ALGORITHMS.ES256) + +- assert pem_source == target.to_pem().strip() ++ assert pem_source.replace(b"\n", b"") == target.to_pem().strip().replace(b"\n", b"") +Index: python-jose-3.3.0/tests/algorithms/test_EC.py +=================================================================== +--- python-jose-3.3.0.orig/tests/algorithms/test_EC.py ++++ python-jose-3.3.0/tests/algorithms/test_EC.py +@@ -104,7 +104,7 @@ class TestECAlgorithm: + def test_to_pem(self): + key = ECKey(private_key, ALGORITHMS.ES256) + assert not key.is_public() +- assert key.to_pem().strip() == private_key.strip().encode("utf-8") ++ assert key.to_pem().strip().replace(b"\n", b"") == private_key.strip().encode("utf-8").replace(b"\n", b"") + + public_pem = key.public_key().to_pem() + assert ECKey(public_pem, ALGORITHMS.ES256).is_public() diff --git a/python-python-jose.changes b/python-python-jose.changes index 81e178a..2535e6f 100644 --- a/python-python-jose.changes +++ b/python-python-jose.changes @@ -1,3 +1,28 @@ +------------------------------------------------------------------- +Mon Jun 3 07:38:00 UTC 2024 - Daniel Garcia + +- Update CVE-2024-33664.patch with upstream + https://github.com/mpdavis/python-jose/pull/352 + bsc#1223422 + +------------------------------------------------------------------- +Tue May 7 09:58:08 UTC 2024 - Daniel Garcia + +- Make python-pycryptodome dependency only required for Factory. + +------------------------------------------------------------------- +Mon May 6 07:11:18 UTC 2024 - Daniel Garcia + +- Add upstream patches: + * CVE-2024-33663.patch, bsc#1223417, gh#mpdavis/python-jose#349 + * CVE-2024-33664.patch, bsc#1223422, gh#mpdavis/python-jose#345 + * fix-tests-ecdsa-019.patch, gh#mpdavis/python-jose#350 + +------------------------------------------------------------------- +Tue Jun 13 12:18:28 UTC 2023 - ecsos + +- Add %{?sle15_python_module_pythons} + ------------------------------------------------------------------- Sun May 29 19:45:02 UTC 2022 - Ben Greiner diff --git a/python-python-jose.spec b/python-python-jose.spec index adb6db9..9b8567e 100644 --- a/python-python-jose.spec +++ b/python-python-jose.spec @@ -1,7 +1,7 @@ # -# spec file +# spec file for package python-python-jose # -# Copyright (c) 2022 SUSE LLC +# Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -36,8 +36,7 @@ %bcond_with testnative %endif -%{?!python_module:%define python_module() python3-%{**}} -%define skip_python2 1 +%{?sle15_python_module_pythons} Name: python-python-jose%{psuffix} Version: 3.3.0 Release: 0 @@ -46,6 +45,12 @@ License: MIT URL: https://github.com/mpdavis/python-jose Source: https://files.pythonhosted.org/packages/source/p/python-jose/python-jose-%{version}.tar.gz Patch0: unpin-deps.patch +# PATCH-FIX-UPSTREAM CVE-2024-33664.patch gh#mpdavis/python-jose#352 +Patch1: CVE-2024-33664.patch +# PATCH-FIX-UPSTREAM CVE-2024-33663.patch gh#mpdavis/python-jose#349 +Patch2: CVE-2024-33663.patch +# PATCH-FIX-UPSTREAM fix-tests-ecdsa-019.patch gh#mpdavis/python-jose#350 +Patch3: fix-tests-ecdsa-019.patch BuildRequires: %{python_module setuptools >= 39.2.0} BuildRequires: fdupes BuildRequires: python-rpm-macros @@ -54,6 +59,11 @@ Requires: python-pyasn1 Requires: python-rsa BuildArch: noarch %if %{with test} +# pycryptodome is needed just for one test added in CVE-2024-33663. +# This package is not in Leap, so do not require for other versions. +%if 0%{?suse_version} > 1600 +BuildRequires: %{python_module pycryptodome} +%endif BuildRequires: %{python_module pytest} %if %{with testcryptography} BuildRequires: %{python_module python-jose-cryptography = %{version}}