diff --git a/.gitignore b/.gitignore index 1d8df6e..358c9b1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /jwcrypto-1.4.1.tar.gz /jwcrypto-1.4.2.tar.gz /jwcrypto-1.5.6.tar.gz +/jwcrypto-1.5.7.tar.gz diff --git a/0002-Limit-max-plaintext-size-for-JWE-decompression.patch b/0002-Limit-max-plaintext-size-for-JWE-decompression.patch deleted file mode 100644 index c94b181..0000000 --- a/0002-Limit-max-plaintext-size-for-JWE-decompression.patch +++ /dev/null @@ -1,153 +0,0 @@ -From 25db861d8b29434838669a94a843af03d29ea6ed Mon Sep 17 00:00:00 2001 -From: Simo Sorce -Date: Mon, 6 Apr 2026 10:37:20 -0400 -Subject: [PATCH] Limit max plaintext size for JWE decompression - -This change introduces a maximum plaintext size limit (defaulting to 100MB) -during JWE decryption and updates the decompression logic to enforce it safely -using zlib.decompressobj. The decrypt method now accepts a max_plaintext -parameter to allow overriding the default limit. - -This mitigates memory exhaustion and decompression bomb attacks when -processing highly compressed malicious JWE payloads. - -Fixes CVE-2026-39373 - -Signed-off-by: Simo Sorce ---- - jwcrypto/jwe.py | 27 +++++++++++++++++++++------ - jwcrypto/tests.py | 34 ++++++++++++++++++++++++++-------- - 2 files changed, 47 insertions(+), 14 deletions(-) - -diff --git a/jwcrypto/jwe.py b/jwcrypto/jwe.py -index e01aa1d..7895928 100644 ---- a/jwcrypto/jwe.py -+++ b/jwcrypto/jwe.py -@@ -12,7 +12,8 @@ - - # Limit the amount of data we are willing to decompress by default. - default_max_compressed_size = 256 * 1024 -- -+# Limit the maximum plaintext size to 100MB by default. -+default_max_plaintext_size = 100 * 1024 * 1024 - - # RFC 7516 - 4.1 - # name: (description, supported?) -@@ -376,7 +377,7 @@ def _unwrap_decrypt(self, alg, enc, key, enckey, header, - return data - - # FIXME: allow to specify which algorithms to accept as valid -- def _decrypt(self, key, ppe): -+ def _decrypt(self, key, ppe, max_plaintext=default_max_plaintext_size): - - jh = self._get_jose_header(ppe.get('header', None)) - -@@ -434,19 +435,29 @@ def _decrypt(self, key, ppe): - raise InvalidJWEData( - 'Compressed data exceeds maximum allowed' - 'size' + f' ({default_max_compressed_size})') -- self.plaintext = zlib.decompress(data, -zlib.MAX_WBITS) -+ do = zlib.decompressobj(wbits=-zlib.MAX_WBITS) -+ self.plaintext = do.decompress(data, max_plaintext) -+ if do.unconsumed_tail or not do.eof: -+ self.plaintext = None -+ raise InvalidJWEData( -+ 'Compressed data exceeds maximum allowed' -+ 'output size' + f' ({max_plaintext})') - elif compress is None: - self.plaintext = data - else: - raise ValueError('Unknown compression') - -- def decrypt(self, key): -+ def decrypt(self, key, max_plaintext=0): - """Decrypt a JWE token. - - :param key: The (:class:`jwcrypto.jwk.JWK`) decryption key. - :param key: A (:class:`jwcrypto.jwk.JWK`) decryption key, - or a (:class:`jwcrypto.jwk.JWKSet`) that contains a key indexed - by the 'kid' header or (deprecated) a string containing a password. -+ :param max_plaintext: Maximum plaintext size allowed, 0 means -+ the library default applies. Application writers are recommended -+ to set a limit here if they know what is the max plaintext size -+ for their application. - - :raises InvalidJWEOperation: if the key is not a JWK object. - :raises InvalidJWEData: if the ciphertext can't be decrypted or -@@ -454,6 +465,10 @@ def decrypt(self, key): - :raises JWKeyNotFound: if key is a JWKSet and the key is not found. - """ - -+ self.plaintext = None -+ if max_plaintext == 0: -+ max_plaintext = default_max_plaintext_size -+ - if 'ciphertext' not in self.objects: - raise InvalidJWEOperation("No available ciphertext") - self.decryptlog = [] -@@ -462,14 +477,14 @@ def decrypt(self, key): - if 'recipients' in self.objects: - for rec in self.objects['recipients']: - try: -- self._decrypt(key, rec) -+ self._decrypt(key, rec, max_plaintext=max_plaintext) - except Exception as e: # pylint: disable=broad-except - if isinstance(e, JWKeyNotFound): - missingkey = True - self.decryptlog.append('Failed: [%s]' % repr(e)) - else: - try: -- self._decrypt(key, self.objects) -+ self._decrypt(key, self.objects, max_plaintext=max_plaintext) - except Exception as e: # pylint: disable=broad-except - if isinstance(e, JWKeyNotFound): - missingkey = True -diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py -index cc612eb..3fc4b16 100644 ---- a/jwcrypto/tests.py -+++ b/jwcrypto/tests.py -@@ -2124,18 +2124,36 @@ def test_jwe_decompression_max(self): - enc = jwe.JWE(payload.encode('utf-8'), - recipient=key, - protected=protected_header).serialize(compact=True) -+ check = jwe.JWE() -+ check.deserialize(enc) - with self.assertRaises(jwe.InvalidJWEData): -- check = jwe.JWE() -- check.deserialize(enc) - check.decrypt(key) - -- defmax = jwe.default_max_compressed_size -- jwe.default_max_compressed_size = 1000000000 -- # ensure we can eraise the limit and decrypt -- check = jwe.JWE() -- check.deserialize(enc) -+ # raise the limit on compressed token size so we can decrypt -+ defcmax = jwe.default_max_compressed_size -+ jwe.default_max_compressed_size = 10 * 1024 * 1024 -+ -+ # this passes if we explicitly allow larger plaintext via API -+ check.decrypt(key, max_plaintext=1000000000) -+ -+ # this will still fail because the max plaintext length clamps this -+ with self.assertRaises(jwe.InvalidJWEData): -+ check.decrypt(key) -+ -+ # ensure that now this can work with changed defaults -+ defpmax = jwe.default_max_plaintext_size -+ jwe.default_max_plaintext_size = 1000000000 - check.decrypt(key) -- jwe.default_max_compressed_size = defmax -+ -+ # restore limits -+ jwe.default_max_compressed_size = defcmax -+ -+ # check that this fails the max compressed header limits -+ with self.assertRaises(jwe.InvalidJWEData): -+ check.decrypt(key) -+ -+ # restore plaintext limits -+ jwe.default_max_plaintext_size = defpmax - - - class JWATests(unittest.TestCase): diff --git a/0002-handle-unsafe-skip-rsa-key-validation-compat.patch b/0002-handle-unsafe-skip-rsa-key-validation-compat.patch new file mode 100644 index 0000000..047a65f --- /dev/null +++ b/0002-handle-unsafe-skip-rsa-key-validation-compat.patch @@ -0,0 +1,40 @@ +--- jwcrypto-1.5.7/jwcrypto/jwk.py 2026-06-03 13:07:15 ++++ jwcrypto-1.5.7-new/jwcrypto/jwk.py 2026-06-23 00:49:09 +@@ -839,9 +839,14 @@ + def _rsa_pri(self): + k = self._cache_pri_k + if k is None: +- u = self.unsafe_skip_rsa_key_validation +- k = self._rsa_pri_n().private_key(default_backend(), +- unsafe_skip_rsa_key_validation=u) ++ try: ++ u = self.unsafe_skip_rsa_key_validation ++ k = self._rsa_pri_n().private_key( ++ default_backend(), ++ unsafe_skip_rsa_key_validation=u) ++ except TypeError: ++ k = self._rsa_pri_n().private_key( ++ default_backend()) + self._cache_pri_k = k + return k + +@@ -997,10 +1002,15 @@ + """ + + try: +- u = self.unsafe_skip_rsa_key_validation +- key = serialization.load_pem_private_key( +- data, password=password, backend=default_backend(), +- unsafe_skip_rsa_key_validation=u) ++ try: ++ u = self.unsafe_skip_rsa_key_validation ++ key = serialization.load_pem_private_key( ++ data, password=password, backend=default_backend(), ++ unsafe_skip_rsa_key_validation=u) ++ except TypeError: ++ key = serialization.load_pem_private_key( ++ data, password=password, ++ backend=default_backend()) + except ValueError as e: + if password is not None: + raise e diff --git a/python-jwcrypto.spec b/python-jwcrypto.spec index 4522530..149965e 100644 --- a/python-jwcrypto.spec +++ b/python-jwcrypto.spec @@ -18,8 +18,8 @@ %global srcname jwcrypto Name: python-%{srcname} -Version: 1.5.6 -Release: 3%{?dist} +Version: 1.5.7 +Release: 1%{?dist} Summary: Implements JWK, JWS, JWE specifications using python-cryptography License: LGPLv3+ @@ -27,8 +27,7 @@ URL: https://github.com/latchset/%{srcname} Source0: https://github.com/latchset/%{srcname}/releases/download/v%{version}/%{srcname}-%{version}.tar.gz Patch1: 0001-ignore-deprecated-annotation.patch -# Security fix for CVE-2026-39373 -Patch2: 0002-Limit-max-plaintext-size-for-JWE-decompression.patch +Patch2: 0002-handle-unsafe-skip-rsa-key-validation-compat.patch BuildArch: noarch %if 0%{?with_python2} @@ -131,6 +130,10 @@ rm -rf %{buildroot}%{python3_sitelib}/%{srcname}/__pycache__/tests{,-cookbook}.* %changelog +* Tue Jun 23 2026 Rafael Jeffman - 1.5.7-1 +- Rebase to 1.5.7 + Resolves: RHEL-168728 + * Tue Apr 14 2026 Rafael Jeffman - 1.5.6-3 - Limit max plaintext size for JWE decompression Resolves: RHEL-166029 diff --git a/sources b/sources index 54b297b..ad2f739 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (jwcrypto-1.5.6.tar.gz) = 1db62cf247bc006f1737c4603b80e5ca87e1a3db3b3dc37183a9725a8b3cae4baba706b2ec596119877130ab4d56525a01fb9d7efca07e59811d78021aa7ebf5 +SHA512 (jwcrypto-1.5.7.tar.gz) = 9f05edc9c0969c3415fede22cc20bd0036187f7acd0477f82f7288809ccd9af9d00ec9f4bff7c6b47083c158395a3fc11024156ef5f2dc148820288002098ef9