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
[](https://pypi.org/project/jwcrypto/)
[](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
[](https://pypi.org/project/jwcrypto/)
[](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 =