Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-jwcrypto for openSUSE:Factory 
checked in at 2026-04-16 18:45:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jwcrypto (Old)
 and      /work/SRC/openSUSE:Factory/.python-jwcrypto.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jwcrypto"

Thu Apr 16 18:45:05 2026 rev:18 rq:1345071 version:1.5.7

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-jwcrypto/python-jwcrypto.changes  
2024-03-26 19:26:20.246896688 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-jwcrypto.new.11940/python-jwcrypto.changes   
    2026-04-16 18:45:06.496785496 +0200
@@ -1,0 +2,13 @@
+Tue Apr  7 21:26:44 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.5.7:
+  * JWE: allow general (non flattened) serialization syntax
+  * Update CI actions
+  * Allow to pass through pem loading unsafe option
+  * Add support for 'scope' claim with multiple scopes
+  * Set default kid when importing keys from pyca.
+  * Hardening: Enforce length of keys for HMAC operations
+  * Add Ed25519 and Ed448 signature algorithms
+  * Migrate jwcrypto packaging to Hatch
+
+-------------------------------------------------------------------

Old:
----
  jwcrypto-1.5.6.tar.gz

New:
----
  jwcrypto-1.5.7.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jwcrypto.spec ++++++
--- /var/tmp/diff_new_pack.MpLNWM/_old  2026-04-16 18:45:08.356862257 +0200
+++ /var/tmp/diff_new_pack.MpLNWM/_new  2026-04-16 18:45:08.380863248 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-jwcrypto
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-jwcrypto
-Version:        1.5.6
+Version:        1.5.7
 Release:        0
 Summary:        Python module package implementing JOSE Web standards
 License:        LGPL-3.0-only
@@ -26,11 +26,10 @@
 Source:         
https://files.pythonhosted.org/packages/source/j/jwcrypto/jwcrypto-%{version}.tar.gz
 BuildRequires:  %{python_module base >= 3.8}
 BuildRequires:  %{python_module cryptography >= 3.4}
+BuildRequires:  %{python_module hatchling}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module pytest}
-BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module typing-extensions >= 4.5.0}
-BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-cryptography >= 3.4

++++++ jwcrypto-1.5.6.tar.gz -> jwcrypto-1.5.7.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/MANIFEST.in 
new/jwcrypto-1.5.7/MANIFEST.in
--- old/jwcrypto-1.5.6/MANIFEST.in      2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/MANIFEST.in      2026-04-07 02:35:02.000000000 +0200
@@ -1,3 +1,2 @@
 include LICENSE README.md
 include tox.ini setup.cfg
-include jwcrypto/VERSION
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/PKG-INFO new/jwcrypto-1.5.7/PKG-INFO
--- old/jwcrypto-1.5.6/PKG-INFO 2024-03-06 20:58:26.596289400 +0100
+++ new/jwcrypto-1.5.7/PKG-INFO 2026-04-07 02:35:27.992449300 +0200
@@ -1,21 +1,28 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: jwcrypto
-Version: 1.5.6
+Version: 1.5.7
 Summary: Implementation of JOSE Web standards
 Home-page: https://github.com/latchset/jwcrypto
 Maintainer: JWCrypto Project Contributors
-Maintainer-email: [email protected]
-License: LGPLv3+
+Maintainer-email: JWCrypto Project Contributors <[email protected]>
+License-Expression: LGPL-3.0-or-later
+Project-URL: Homepage, https://github.com/latchset/jwcrypto
+Classifier: Intended Audience :: Developers
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
-Classifier: Intended Audience :: Developers
 Classifier: Topic :: Security
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Requires-Python: >= 3.8
 Description-Content-Type: text/markdown
 License-File: LICENSE
+Requires-Dist: cryptography>=3.4
+Requires-Dist: typing_extensions>=4.5.0
+Dynamic: home-page
+Dynamic: license-file
+Dynamic: maintainer
+Dynamic: requires-python
 
 
[![PyPI](https://img.shields.io/pypi/v/jwcrypto.svg)](https://pypi.org/project/jwcrypto/)
 
[![Changelog](https://img.shields.io/github/v/release/latchset/jwcrypto?label=changelog)](https://github.com/latchset/jwcrypto/releases)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/VERSION 
new/jwcrypto-1.5.7/jwcrypto/VERSION
--- old/jwcrypto-1.5.6/jwcrypto/VERSION 2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto/VERSION 1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-1.5.6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/jwa.py 
new/jwcrypto-1.5.7/jwcrypto/jwa.py
--- old/jwcrypto-1.5.6/jwcrypto/jwa.py  2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto/jwa.py  2026-04-07 02:35:02.000000000 +0200
@@ -26,9 +26,28 @@
 from jwcrypto.common import json_decode
 from jwcrypto.jwk import JWK
 
-# Implements RFC 7518 - JSON Web Algorithms (JWA)
+# Implements:
+# - RFC 7518: JSON Web Algorithms (JWA)
+# - RFC 8037: CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures
+#             in JSON Object Signing and Encryption (JOSE)
+# - RFC 9864: Fully-Specified Algorithms for JSON Object Signing and
+#             Encryption (JOSE) and CBOR Object Signing and Encryption
+#             (COSE)
 
 default_max_pbkdf2_iterations = 16384
+"""The maximum number of iterations allowed for PBKDF2 key derivation.
+
+This is a security measure to prevent denial-of-service attacks by malicious
+actors providing a very high iteration count.
+"""
+
+default_enforce_hmac_key_length = True
+"""Enforces that the HMAC key length is at least the size of the hash
+function's output, as recommended by RFC 7518.
+
+This can be disabled for compatibility with legacy or non-compliant systems
+that use shorter keys.
+"""
 
 
 class JWAAlgorithm(metaclass=ABCMeta):
@@ -103,11 +122,16 @@
 
 class _RawHMAC(_RawJWS):
 
+    keysize = None
+
     def __init__(self, hashfn):
         self.backend = default_backend()
         self.hashfn = hashfn
 
     def _hmac_setup(self, key, payload):
+        # Ensure key size matches RFC 7518 requirements
+        if default_enforce_hmac_key_length and _bitsize(key) < self.keysize:
+            raise InvalidJWEKeyLength(self.keysize, _bitsize(key))
         h = hmac.HMAC(key, self.hashfn, backend=self.backend)
         h.update(payload)
         return h
@@ -873,6 +897,48 @@
         raise NotImplementedError
 
 
+class _Ed25519(_RawJWS, JWAAlgorithm):
+
+    name = 'Ed25519'
+    description = 'EdDSA using Ed25519'
+    algorithm_usage_location = 'alg'
+    algorithm_use = 'sig'
+    keysize = None
+
+    def sign(self, key, payload):
+        if key['crv'] != 'Ed25519':
+            raise InvalidJWEKeyType('Ed25519', key['crv'])
+        skey = key.get_op_key('sign')
+        return skey.sign(payload)
+
+    def verify(self, key, payload, signature):
+        if key['crv'] != 'Ed25519':
+            raise InvalidJWEKeyType('Ed25519', key['crv'])
+        pkey = key.get_op_key('verify')
+        return pkey.verify(signature, payload)
+
+
+class _Ed448(_RawJWS, JWAAlgorithm):
+
+    name = 'Ed448'
+    description = 'EdDSA using Ed448'
+    algorithm_usage_location = 'alg'
+    algorithm_use = 'sig'
+    keysize = None
+
+    def sign(self, key, payload):
+        if key['crv'] != 'Ed448':
+            raise InvalidJWEKeyType('Ed448', key['crv'])
+        skey = key.get_op_key('sign')
+        return skey.sign(payload)
+
+    def verify(self, key, payload, signature):
+        if key['crv'] != 'Ed448':
+            raise InvalidJWEKeyType('Ed448', key['crv'])
+        pkey = key.get_op_key('verify')
+        return pkey.verify(signature, payload)
+
+
 class _RawJWE:
 
     def encrypt(self, k, aad, m):
@@ -1167,7 +1233,9 @@
         'A256GCM': _A256Gcm,
         'BP256R1': _BP256R1,
         'BP384R1': _BP384R1,
-        'BP512R1': _BP512R1
+        'BP512R1': _BP512R1,
+        'Ed25519': _Ed25519,
+        'Ed448': _Ed448
     }
 
     @classmethod
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/jwe.py 
new/jwcrypto-1.5.7/jwcrypto/jwe.py
--- old/jwcrypto-1.5.6/jwcrypto/jwe.py  2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto/jwe.py  2026-04-07 02:35:02.000000000 +0200
@@ -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?)
@@ -82,7 +83,7 @@
 
     def __init__(self, plaintext=None, protected=None, unprotected=None,
                  aad=None, algs=None, recipient=None, header=None,
-                 header_registry=None):
+                 header_registry=None, flattened=True):
         """Creates a JWE token.
 
         :param plaintext(bytes): An arbitrary plaintext to be encrypted.
@@ -93,11 +94,13 @@
         :param recipient: An optional, default recipient key
         :param header: An optional header for the default recipient
         :param header_registry: Optional additions to the header registry
+        :param flattened: Use flattened serialization syntax (default True)
         """
         self._allowed_algs = None
         self.objects = {}
         self.plaintext = None
         self.header_registry = JWSEHeaderRegistry(JWEHeaderRegistry)
+        self.flattened = flattened
         if header_registry:
             self.header_registry.update(header_registry)
         if plaintext is not None:
@@ -253,17 +256,20 @@
 
         if 'recipients' in self.objects:
             self.objects['recipients'].append(rec)
-        elif 'encrypted_key' in self.objects or 'header' in self.objects:
-            self.objects['recipients'] = []
-            n = {}
-            if 'encrypted_key' in self.objects:
-                n['encrypted_key'] = self.objects.pop('encrypted_key')
-            if 'header' in self.objects:
-                n['header'] = self.objects.pop('header')
-            self.objects['recipients'].append(n)
-            self.objects['recipients'].append(rec)
+        elif self.flattened:
+            if 'encrypted_key' in self.objects or 'header' in self.objects:
+                self.objects['recipients'] = []
+                n = {}
+                if 'encrypted_key' in self.objects:
+                    n['encrypted_key'] = self.objects.pop('encrypted_key')
+                if 'header' in self.objects:
+                    n['header'] = self.objects.pop('header')
+                self.objects['recipients'].append(n)
+                self.objects['recipients'].append(rec)
+            else:
+                self.objects.update(rec)
         else:
-            self.objects.update(rec)
+            self.objects['recipients'] = [rec]
 
     def serialize(self, compact=False):
         """Serializes the object into a JWE token.
@@ -371,7 +377,7 @@
         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))
 
@@ -429,19 +435,29 @@
                 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
@@ -449,6 +465,10 @@
         :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 = []
@@ -457,14 +477,14 @@
         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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/jwk.py 
new/jwcrypto-1.5.7/jwcrypto/jwk.py
--- old/jwcrypto-1.5.6/jwcrypto/jwk.py  2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto/jwk.py  2026-04-07 02:35:02.000000000 +0200
@@ -339,6 +339,7 @@
         super(JWK, self).__init__()
         self._cache_pub_k = None
         self._cache_pri_k = None
+        self.unsafe_skip_rsa_key_validation = False
 
         if 'generate' in kwargs:
             self.generate_key(**kwargs)
@@ -838,7 +839,9 @@
     def _rsa_pri(self):
         k = self._cache_pri_k
         if k is None:
-            k = self._rsa_pri_n().private_key(default_backend())
+            u = self.unsafe_skip_rsa_key_validation
+            k = self._rsa_pri_n().private_key(default_backend(),
+                                              unsafe_skip_rsa_key_validation=u)
             self._cache_pri_k = k
         return k
 
@@ -981,6 +984,7 @@
             self._import_pyca_pub_okp(key)
         else:
             raise InvalidJWKValue('Unknown key object %r' % key)
+        self.__setitem__('kid', self.thumbprint())
 
     def import_from_pem(self, data, password=None, kid=None):
         """Imports a key from data loaded from a PEM file.
@@ -993,8 +997,10 @@
         """
 
         try:
+            u = self.unsafe_skip_rsa_key_validation
             key = serialization.load_pem_private_key(
-                data, password=password, backend=default_backend())
+                data, password=password, backend=default_backend(),
+                unsafe_skip_rsa_key_validation=u)
         except ValueError as e:
             if password is not None:
                 raise e
@@ -1011,9 +1017,8 @@
                     raise e
 
         self.import_from_pyca(key)
-        if kid is None:
-            kid = self.thumbprint()
-        self.__setitem__('kid', kid)
+        if kid is not None:
+            self.__setitem__('kid', kid)
 
     def export_to_pem(self, private_key=False, password=False):
         """Exports keys to a data buffer suitable to be stored as a PEM file.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/jws.py 
new/jwcrypto-1.5.7/jwcrypto/jws.py
--- old/jwcrypto-1.5.6/jwcrypto/jws.py  2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto/jws.py  2026-04-07 02:35:02.000000000 +0200
@@ -30,7 +30,8 @@
     'RS256', 'RS384', 'RS512',
     'ES256', 'ES384', 'ES512',
     'PS256', 'PS384', 'PS512',
-    'EdDSA', 'ES256K']
+    'EdDSA', 'ES256K', 'Ed25519',
+    'Ed448']
 """Default allowed algorithms"""
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/jwt.py 
new/jwcrypto-1.5.7/jwcrypto/jwt.py
--- old/jwcrypto-1.5.6/jwcrypto/jwt.py  2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto/jwt.py  2026-04-07 02:35:02.000000000 +0200
@@ -479,6 +479,7 @@
         self._check_string_claim('iss', check_claims)
         self._check_string_claim('sub', check_claims)
         self._check_array_or_string_claim('aud', check_claims)
+        self._check_string_claim('scope', check_claims)
         self._check_integer_claim('exp', check_claims)
         self._check_integer_claim('nbf', check_claims)
         self._check_integer_claim('iat', check_claims)
@@ -556,7 +557,26 @@
                                                    "'%s'" % (name,
                                                              claims[name],
                                                              value))
+            elif name == 'scope':
+                if value is not None:
+                    if not isinstance(claims[name], str):
+                        raise JWTInvalidClaimValue(
+                            "Invalid '%s' value. Scope list has to be "
+                            "a string, got a %s instead: %s" % (
+                                name, type(claims[name]), str(claims[name])))
 
+                    found = False
+                    got_scopes = claims[name].split()
+                    for s in got_scopes:
+                        if s == value:
+                            found = True
+                            break
+
+                    if not found:
+                        raise JWTInvalidClaimValue(
+                            "Invalid '%s' value. Scope list '%s' does not "
+                            "contain the required scope '%s'" % (
+                                name, claims[name], value))
             else:
                 if value is not None and value != claims[name]:
                     raise JWTInvalidClaimValue(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/tests.py 
new/jwcrypto-1.5.7/jwcrypto/tests.py
--- old/jwcrypto-1.5.6/jwcrypto/tests.py        2024-03-06 20:46:25.000000000 
+0100
+++ new/jwcrypto-1.5.7/jwcrypto/tests.py        2026-04-07 02:35:02.000000000 
+0200
@@ -757,6 +757,16 @@
             "urn:ietf:params:oauth:jwk-thumbprint:sha-256:{}".format(
                 PublicKeys['thumbprints'][1]))
 
+    def test_unsafe_rsa(self):
+        key = jwk.JWK()
+        key.unsafe_skip_rsa_key_validation = True
+        key.import_from_pem(RSAPrivatePEM, password=RSAPrivatePassword)
+        self.assertTrue(key.has_private)
+        # finally check private works
+        s = jws.JWS(payload='plaintext')
+        s.add_signature(key, None, {"alg": "PS256"})
+        s.serialize()
+
 
 # RFC 7515 - A.1
 A1_protected = \
@@ -1122,7 +1132,7 @@
             self.assertEqual(jws_verify.payload, payload)
 
     def test_jws_issue_224(self):
-        key = jwk.JWK().generate(kty='oct')
+        key = jwk.JWK().generate(kty='oct', alg='HS256')
 
         # Test Empty payload is supported for creating and verifying signatures
         s = jws.JWS(payload='')
@@ -1140,7 +1150,7 @@
         header = {"alg": "HS256"}
         header_copy = copy.deepcopy(header)
 
-        key = jwk.JWK().generate(kty='oct')
+        key = jwk.JWK().generate(kty='oct', alg='HS256')
 
         s = jws.JWS(payload='test')
         s.add_signature(key, protected=header,
@@ -1509,6 +1519,18 @@
         with self.assertRaises(JWKeyNotFound):
             e4.deserialize(e3.serialize(), ks)
 
+    def test_serialize_not_flattened(self):
+        # JWE with flattened=False adds recipients in objects and in serialized
+        e = jwe.JWE(E_A1_ex['plaintext'], flattened=False)
+        e.add_recipient(E_A1_ex['key'], E_A1_ex['protected'])
+        self.assertIn('recipients', e.objects)
+        self.assertIn('recipients', e.serialize())
+
+        e = jwe.JWE(E_A1_ex['plaintext'])
+        e.add_recipient(E_A1_ex['key'], E_A1_ex['protected'])
+        self.assertNotIn('recipients', e.objects)
+        self.assertNotIn('recipients', e.serialize())
+
 
 MMA_vector_key = jwk.JWK(**E_A2_key)
 MMA_vector_ok_cek =  \
@@ -1778,7 +1800,7 @@
                               "string_claim": "test"})
 
     def test_claims_typ(self):
-        key = jwk.JWK().generate(kty='oct')
+        key = jwk.JWK().generate(kty='oct', alg='HS256')
         claims = '{"typ":"application/test"}'
         string_header = '{"alg":"HS256"}'
         t = jwt.JWT(string_header, claims)
@@ -1810,7 +1832,7 @@
         jwt.JWT(jwt=token, key=key)
 
     def test_empty_claims(self):
-        key = jwk.JWK().generate(kty='oct')
+        key = jwk.JWK().generate(kty='oct', alg='HS256')
 
         # empty dict is valid
         t = jwt.JWT('{"alg":"HS256"}', {})
@@ -1955,6 +1977,68 @@
         jwt.JWT(jwt=enctok, key=key)
         key.key_ops = None
 
+    def test_claims_scope(self):
+        key = jwk.JWK().generate(kty='oct', alg='HS256')
+
+        string_header = '{"alg":"HS256"}'
+
+        # no scopes provided
+        claims = '{}'
+        t = jwt.JWT(string_header, claims)
+        t.make_signed_token(key)
+        token = t.serialize()
+        self.assertRaises(jwt.JWTMissingClaim, jwt.JWT, jwt=token,
+                          key=key, check_claims={"scope": "read"})
+
+        # non-string scopes
+        claims = '{"scope": 12345}'
+        t = jwt.JWT(string_header, claims)
+        t.make_signed_token(key)
+        token = t.serialize()
+        self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+                          key=key, check_claims={"scope": "read"})
+
+        # empty scopes
+        claims = '{"scope": ""}'
+        t = jwt.JWT(string_header, claims)
+        t.make_signed_token(key)
+        token = t.serialize()
+        self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+                          key=key, check_claims={"scope": "read"})
+
+        # one correct scope
+        claims = '{"scope":"read"}'
+        t = jwt.JWT(string_header, claims)
+        t.make_signed_token(key)
+        token = t.serialize()
+        jwt.JWT(jwt=token, key=key, check_claims={"scope": "read"})
+        self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+                          key=key, check_claims={"scope": "write"})
+
+        # multiple scopes including the correct one
+        claims = '{"scope":"view read write"}'
+        t = jwt.JWT(string_header, claims)
+        t.make_signed_token(key)
+        token = t.serialize()
+        jwt.JWT(jwt=token, key=key, check_claims={"scope": "view"})
+        jwt.JWT(jwt=token, key=key, check_claims={"scope": "read"})
+        jwt.JWT(jwt=token, key=key, check_claims={"scope": "write"})
+        self.assertRaises(jwt.JWTInvalidClaimValue, jwt.JWT, jwt=token,
+                          key=key, check_claims={"scope": "wrong"})
+
+        # one correct scope, invalid value
+        claims = '{"scope":"read"}'
+        t = jwt.JWT(string_header, claims)
+        t.make_signed_token(key)
+        token = t.serialize()
+        self.assertRaises(jwt.JWTInvalidClaimFormat, jwt.JWT, jwt=token,
+                          key=key, check_claims={"scope": 123})
+        self.assertRaises(jwt.JWTInvalidClaimFormat, jwt.JWT, jwt=token,
+                          key=key, check_claims={"scope": ["test", "wrong"]})
+
+        # finally make sure it doesn't raise if not checked.
+        jwt.JWT(jwt=token, key=key)
+
 
 class ConformanceTests(unittest.TestCase):
 
@@ -2005,17 +2089,17 @@
 
     def test_jws_loopback(self):
         sign = jws.JWS(payload='message')
-        sign.add_signature(jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16)),
+        sign.add_signature(jwk.JWK(kty='oct', k=base64url_encode(b'A' * 64)),
                            alg="HS512")
         o = sign.serialize()
         check = jws.JWS()
-        check.deserialize(o, jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16)),
+        check.deserialize(o, jwk.JWK(kty='oct', k=base64url_encode(b'A' * 64)),
                           alg="HS512")
         self.assertTrue(check.objects['valid'])
 
     def test_jws_headers_as_dicts(self):
         sign = jws.JWS(payload='message')
-        key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16))
+        key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * 64))
         sign.add_signature(key, protected={'alg': 'HS512'},
                            header={'kid': key.thumbprint()})
         o = sign.serialize()
@@ -2124,18 +2208,59 @@
         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
+
+    def test_jws_small_hmac_key_rejected(self):
+        sign = jws.JWS(payload='message')
+        # HS256 requires a 256 bit key, this is 128
+        key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16))
+        with self.assertRaises(jwe.InvalidJWEKeyLength):
+            sign.add_signature(key, alg="HS256")
+
+    def test_jws_small_hmac_key_allowed(self):
+        # This is a bad idea, but we allow it if the user
+        # explicitly asks for it.
+        self.addCleanup(setattr, jwa, 'default_enforce_hmac_key_length',
+                        jwa.default_enforce_hmac_key_length)
+        jwa.default_enforce_hmac_key_length = False
+
+        sign = jws.JWS(payload='message')
+        # HS256 requires a 256 bit key, this is 128
+        key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * 16))
+        sign.add_signature(key, alg="HS256")
+        o = sign.serialize()
+        check = jws.JWS()
+        check.deserialize(o, key, alg="HS256")
+        self.assertTrue(check.objects['valid'])
 
 
 class JWATests(unittest.TestCase):
@@ -2143,9 +2268,7 @@
         for name, cls in jwa.JWA.algorithms_registry.items():
             self.assertEqual(cls.name, name)
             self.assertIn(cls.algorithm_usage_location, {'alg', 'enc'})
-            if name == 'ECDH-ES':
-                self.assertIs(cls.keysize, None)
-            elif name == 'EdDSA':
+            if name in ('ECDH-ES', 'EdDSA', 'Ed25519', 'Ed448'):
                 self.assertIs(cls.keysize, None)
             else:
                 self.assertIsInstance(cls.keysize, int)
@@ -2237,7 +2360,7 @@
 
     def test_mismatching_encoding(self):
         s = jws.JWS(rfc7797_payload)
-        s.add_signature(jwk.JWK(**SymmetricKeys['keys'][0]),
+        s.add_signature(jwk.JWK(**SymmetricKeys['keys'][1]),
                         protected=rfc7797_e_header)
         with self.assertRaises(jws.InvalidJWSObject):
             s.add_signature(jwk.JWK(**SymmetricKeys['keys'][1]),
@@ -2386,3 +2509,36 @@
                   f'jwt=JWS.from_json_token("{ser2}"), key=None, ' + \
                   'algs=None, default_claims=None, check_claims=None)'
         self.assertEqual(repr(token), reprrep)
+
+
+class TestRfc9864(unittest.TestCase):
+
+    def test_jws_ed25519(self):
+        payload = b'My Integrity protected message'
+        if 'Ed25519' not in jwk.ImplementedOkpCurves:
+            self.skipTest('Ed25519 not supported')
+        key = jwk.JWK.generate(kty='OKP', crv='Ed25519')
+        protected_header = {"alg": "Ed25519"}
+        jws_token = jws.JWS(payload)
+        jws_token.add_signature(key, None,
+                                json_encode(protected_header), None)
+        serialized_jws = jws_token.serialize(compact=True)
+        jws_obj = jws.JWS()
+        jws_obj.deserialize(serialized_jws, key)
+        self.assertTrue(jws_obj.is_valid)
+        self.assertEqual(jws_obj.payload, payload)
+
+    def test_jws_ed448(self):
+        payload = b'My Integrity protected message'
+        if 'Ed448' not in jwk.ImplementedOkpCurves:
+            self.skipTest('Ed448 not supported')
+        key = jwk.JWK.generate(kty='OKP', crv='Ed448')
+        protected_header = {"alg": "Ed448"}
+        jws_token = jws.JWS(payload)
+        jws_token.add_signature(key, None,
+                                json_encode(protected_header), None)
+        serialized_jws = jws_token.serialize(compact=True)
+        jws_obj = jws.JWS()
+        jws_obj.deserialize(serialized_jws, key)
+        self.assertTrue(jws_obj.is_valid)
+        self.assertEqual(jws_obj.payload, payload)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto/version.py 
new/jwcrypto-1.5.7/jwcrypto/version.py
--- old/jwcrypto-1.5.6/jwcrypto/version.py      1970-01-01 01:00:00.000000000 
+0100
+++ new/jwcrypto-1.5.7/jwcrypto/version.py      2026-04-07 02:35:02.000000000 
+0200
@@ -0,0 +1 @@
+__version__ = "1.5.7"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto.egg-info/PKG-INFO 
new/jwcrypto-1.5.7/jwcrypto.egg-info/PKG-INFO
--- old/jwcrypto-1.5.6/jwcrypto.egg-info/PKG-INFO       2024-03-06 
20:58:26.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto.egg-info/PKG-INFO       2026-04-07 
02:35:27.000000000 +0200
@@ -1,21 +1,28 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: jwcrypto
-Version: 1.5.6
+Version: 1.5.7
 Summary: Implementation of JOSE Web standards
 Home-page: https://github.com/latchset/jwcrypto
 Maintainer: JWCrypto Project Contributors
-Maintainer-email: [email protected]
-License: LGPLv3+
+Maintainer-email: JWCrypto Project Contributors <[email protected]>
+License-Expression: LGPL-3.0-or-later
+Project-URL: Homepage, https://github.com/latchset/jwcrypto
+Classifier: Intended Audience :: Developers
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
-Classifier: Intended Audience :: Developers
 Classifier: Topic :: Security
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Requires-Python: >= 3.8
 Description-Content-Type: text/markdown
 License-File: LICENSE
+Requires-Dist: cryptography>=3.4
+Requires-Dist: typing_extensions>=4.5.0
+Dynamic: home-page
+Dynamic: license-file
+Dynamic: maintainer
+Dynamic: requires-python
 
 
[![PyPI](https://img.shields.io/pypi/v/jwcrypto.svg)](https://pypi.org/project/jwcrypto/)
 
[![Changelog](https://img.shields.io/github/v/release/latchset/jwcrypto?label=changelog)](https://github.com/latchset/jwcrypto/releases)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/jwcrypto.egg-info/SOURCES.txt 
new/jwcrypto-1.5.7/jwcrypto.egg-info/SOURCES.txt
--- old/jwcrypto-1.5.6/jwcrypto.egg-info/SOURCES.txt    2024-03-06 
20:58:26.000000000 +0100
+++ new/jwcrypto-1.5.7/jwcrypto.egg-info/SOURCES.txt    2026-04-07 
02:35:27.000000000 +0200
@@ -1,10 +1,10 @@
 LICENSE
 MANIFEST.in
 README.md
+pyproject.toml
 setup.cfg
 setup.py
 tox.ini
-jwcrypto/VERSION
 jwcrypto/__init__.py
 jwcrypto/common.py
 jwcrypto/jwa.py
@@ -14,6 +14,7 @@
 jwcrypto/jwt.py
 jwcrypto/tests-cookbook.py
 jwcrypto/tests.py
+jwcrypto/version.py
 jwcrypto.egg-info/PKG-INFO
 jwcrypto.egg-info/SOURCES.txt
 jwcrypto.egg-info/dependency_links.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/pyproject.toml 
new/jwcrypto-1.5.7/pyproject.toml
--- old/jwcrypto-1.5.6/pyproject.toml   1970-01-01 01:00:00.000000000 +0100
+++ new/jwcrypto-1.5.7/pyproject.toml   2026-04-07 02:35:02.000000000 +0200
@@ -0,0 +1,42 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "jwcrypto"
+dynamic = ["version"]
+description = "Implementation of JOSE Web standards"
+readme = "README.md"
+license = "LGPL-3.0-or-later"
+requires-python = ">= 3.8"
+maintainers = [
+    { name = "JWCrypto Project Contributors", email = "[email protected]" },
+]
+classifiers = [
+    "Intended Audience :: Developers",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Topic :: Security",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
+dependencies = [
+    "cryptography >= 3.4",
+    "typing_extensions >= 4.5.0",
+]
+
+[project.urls]
+Homepage = "https://github.com/latchset/jwcrypto";
+
+[tool.hatch.version]
+path = "jwcrypto/version.py"
+
+[tool.hatch.build.targets.wheel.shared-data]
+LICENSE = "share/doc/jwcrypto/LICENSE"
+"README.md" = "share/doc/jwcrypto/README.md"
+
+[tool.hatch.build.targets.sdist]
+include = [
+    "/jwcrypto",
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/setup.py new/jwcrypto-1.5.7/setup.py
--- old/jwcrypto-1.5.6/setup.py 2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/setup.py 2026-04-07 02:35:02.000000000 +0200
@@ -2,17 +2,17 @@
 #
 # Copyright (C) 2015  JWCrypto Project Contributors, see  LICENSE file
 
-import os
-from setuptools import setup
-
 # read the contents of your README file
 from pathlib import Path
+
+from setuptools import setup
+
+from jwcrypto import version
+
 this_directory = Path(__file__).parent
 long_description = (this_directory / "README.md").read_text()
 
-version = None
-with open(os.path.join('jwcrypto', 'VERSION')) as verfile:
-    version = verfile.read().strip()
+version = version.__version__
 
 setup(
     name = 'jwcrypto',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jwcrypto-1.5.6/tox.ini new/jwcrypto-1.5.7/tox.ini
--- old/jwcrypto-1.5.6/tox.ini  2024-03-06 20:46:25.000000000 +0100
+++ new/jwcrypto-1.5.7/tox.ini  2026-04-07 02:35:02.000000000 +0200
@@ -1,5 +1,5 @@
 [tox]
-envlist = lint,py38,py39,py310,py311,pep8,doc,sphinx,doctest
+envlist = lint,py38,py39,py310,py311,py312,py313,pep8,doc,sphinx,doctest
 skip_missing_interpreters = true
 
 [testenv]
@@ -16,7 +16,7 @@
     {envpython} -m coverage report -m
 
 [testenv:lint]
-basepython = python3.11
+basepython = python3.13
 deps =
     pylint
 #sitepackages = True
@@ -24,7 +24,7 @@
     {envpython} -m pylint -d c,r,i,W0613 -r n -f colorized --notes= 
--disable=star-args ./jwcrypto
 
 [testenv:pep8]
-basepython = python3.11
+basepython = python3.13
 deps =
     flake8
     flake8-import-order
@@ -37,21 +37,22 @@
     doc8
     docutils
     markdown
-basepython = python3.11
+basepython = python3.13
 commands =
     doc8 --allow-long-titles README.md
     markdown_py README.md -f {toxworkdir}/README.md.html
 
 [testenv:sphinx]
-basepython = python3.11
+basepython = python3.13
 changedir = docs/source
 deps =
     sphinx
+    sphinx-rtd-theme
 commands =
     sphinx-build -n -v -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
 
 [testenv:doctest]
-basepython = python3.11
+basepython = python3.13
 changedir = docs/source
 deps =
     sphinx
@@ -59,7 +60,7 @@
     sphinx-build -v -W -b doctest -d {envtmpdir}/doctrees . {envtmpdir}/doctest
 
 [testenv:codespell]
-basepython = python3.11
+basepython = python3.13
 deps =
     codespell
 commands =

Reply via email to