Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-PyJWT for openSUSE:Factory checked in at 2022-06-09 14:09:16 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-PyJWT (Old) and /work/SRC/openSUSE:Factory/.python-PyJWT.new.1548 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-PyJWT" Thu Jun 9 14:09:16 2022 rev:24 rq:981206 version:2.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-PyJWT/python-PyJWT.changes 2021-11-06 18:18:03.956885640 +0100 +++ /work/SRC/openSUSE:Factory/.python-PyJWT.new.1548/python-PyJWT.changes 2022-06-09 14:09:17.612326995 +0200 @@ -1,0 +2,36 @@ +Tue Jun 7 17:27:32 UTC 2022 - Marcus Rueckert <mrueck...@suse.de> + +- Update to 2.4.0 (CVE-2022-29217 boo#1199756) + - Security + - [CVE-2022-29217] Prevent key confusion through + non-blocklisted public key formats. GHSA-ffqj-6fqr-9h24 + - Other changes: + - Explicit check the key for ECAlgorithm by @estin in + https://github.com/jpadilla/pyjwt/pull/713 + - Raise DeprecationWarning for jwt.decode(verify=...) by @akx + in https://github.com/jpadilla/pyjwt/pull/742 + - Don't use implicit optionals by @rekyungmin in + https://github.com/jpadilla/pyjwt/pull/705 + - documentation fix: show correct scope for decode_complete() + by @sseering in https://github.com/jpadilla/pyjwt/pull/661 + - fix: Update copyright information by @kkirsche in + https://github.com/jpadilla/pyjwt/pull/729 + - Don't mutate options dictionary in .decode_complete() by @akx + in https://github.com/jpadilla/pyjwt/pull/743 + - Add support for Python 3.10 by @hugovk in + https://github.com/jpadilla/pyjwt/pull/699 + - api_jwk: Add PyJWKSet.__getitem__ by @woodruffw in + https://github.com/jpadilla/pyjwt/pull/725 + - Update usage.rst by @guneybilen in + https://github.com/jpadilla/pyjwt/pull/727 + - Docs: mention performance reasons for reusing RSAPrivateKey + when encoding by @dmahr1 in + https://github.com/jpadilla/pyjwt/pull/734 + - Fixed typo in usage.rst by @israelabraham in + https://github.com/jpadilla/pyjwt/pull/738 + - Add detached payload support for JWS encoding and decoding by + @fviard in https://github.com/jpadilla/pyjwt/pull/723 + - Replace various string interpolations with f-strings by @akx + in https://github.com/jpadilla/pyjwt/pull/744 + +------------------------------------------------------------------- Old: ---- PyJWT-2.3.0.tar.gz New: ---- PyJWT-2.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-PyJWT.spec ++++++ --- /var/tmp/diff_new_pack.N9AfK9/_old 2022-06-09 14:09:18.900328722 +0200 +++ /var/tmp/diff_new_pack.N9AfK9/_new 2022-06-09 14:09:18.904328727 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-PyJWT # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2022 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %global skip_python2 1 Name: python-PyJWT -Version: 2.3.0 +Version: 2.4.0 Release: 0 Summary: JSON Web Token implementation in Python License: MIT ++++++ PyJWT-2.3.0.tar.gz -> PyJWT-2.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/.pre-commit-config.yaml new/PyJWT-2.4.0/.pre-commit-config.yaml --- old/PyJWT-2.3.0/.pre-commit-config.yaml 2021-10-16 14:23:39.000000000 +0200 +++ new/PyJWT-2.4.0/.pre-commit-config.yaml 2022-05-12 20:44:24.000000000 +0200 @@ -1,12 +1,12 @@ repos: - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 22.3.0 hooks: - id: black args: ["--target-version=py36"] - repo: https://github.com/asottile/blacken-docs - rev: v1.11.0 + rev: v1.12.1 hooks: - id: blacken-docs args: ["--target-version=py36"] @@ -18,19 +18,19 @@ language_version: python3.8 - repo: https://github.com/PyCQA/isort - rev: 5.9.3 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/mgedmin/check-manifest - rev: "0.47" + rev: "0.48" hooks: - id: check-manifest args: [--no-build-isolation] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/CHANGELOG.rst new/PyJWT-2.4.0/CHANGELOG.rst --- old/PyJWT-2.3.0/CHANGELOG.rst 2021-10-16 17:53:51.000000000 +0200 +++ new/PyJWT-2.4.0/CHANGELOG.rst 2022-05-12 20:51:37.000000000 +0200 @@ -16,6 +16,40 @@ Added ~~~~~ +`v2.4.0 <https://github.com/jpadilla/pyjwt/compare/2.3.0...2.4.0>`__ +----------------------------------------------------------------------- + +Security +~~~~~~~~ + +- [CVE-2022-29217] Prevent key confusion through non-blocklisted public key formats. https://github.com/jpadilla/pyjwt/security/advisories/GHSA-ffqj-6fqr-9h24 + +Changed +~~~~~~~ + +- Explicit check the key for ECAlgorithm by @estin in https://github.com/jpadilla/pyjwt/pull/713 +- Raise DeprecationWarning for jwt.decode(verify=...) by @akx in https://github.com/jpadilla/pyjwt/pull/742 + +Fixed +~~~~~ + +- Don't use implicit optionals by @rekyungmin in https://github.com/jpadilla/pyjwt/pull/705 +- documentation fix: show correct scope for decode_complete() by @sseering in https://github.com/jpadilla/pyjwt/pull/661 +- fix: Update copyright information by @kkirsche in https://github.com/jpadilla/pyjwt/pull/729 +- Don't mutate options dictionary in .decode_complete() by @akx in https://github.com/jpadilla/pyjwt/pull/743 + +Added +~~~~~ + +- Add support for Python 3.10 by @hugovk in https://github.com/jpadilla/pyjwt/pull/699 +- api_jwk: Add PyJWKSet.__getitem__ by @woodruffw in https://github.com/jpadilla/pyjwt/pull/725 +- Update usage.rst by @guneybilen in https://github.com/jpadilla/pyjwt/pull/727 +- Docs: mention performance reasons for reusing RSAPrivateKey when encoding by @dmahr1 in https://github.com/jpadilla/pyjwt/pull/734 +- Fixed typo in usage.rst by @israelabraham in https://github.com/jpadilla/pyjwt/pull/738 +- Add detached payload support for JWS encoding and decoding by @fviard in https://github.com/jpadilla/pyjwt/pull/723 +- Replace various string interpolations with f-strings by @akx in https://github.com/jpadilla/pyjwt/pull/744 +- Update CHANGELOG.rst by @hipertracker in https://github.com/jpadilla/pyjwt/pull/751 + `v2.3.0 <https://github.com/jpadilla/pyjwt/compare/2.2.0...2.3.0>`__ ----------------------------------------------------------------------- @@ -162,6 +196,11 @@ use ``jwt.decode(encoded, key, algorithms=["HS256"], options={"require": ["exp"]})``. +And the old v1.x syntax +``jwt.decode(token, verify=False)`` +is now: +``jwt.decode(jwt=token, key='secret', algorithms=['HS256'], options={"verify_signature": False, "verify_exp": True})`` + Added ~~~~~ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/LICENSE new/PyJWT-2.4.0/LICENSE --- old/PyJWT-2.3.0/LICENSE 2018-11-26 04:04:17.000000000 +0100 +++ new/PyJWT-2.4.0/LICENSE 2022-05-12 20:31:26.000000000 +0200 @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Jos?? Padilla +Copyright (c) 2015-2022 Jos?? Padilla Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/PKG-INFO new/PyJWT-2.4.0/PKG-INFO --- old/PyJWT-2.3.0/PKG-INFO 2021-10-16 17:54:33.864155000 +0200 +++ new/PyJWT-2.4.0/PKG-INFO 2022-05-12 20:55:44.584056100 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: PyJWT -Version: 2.3.0 +Version: 2.4.0 Summary: JSON Web Token implementation in Python Home-page: https://github.com/jpadilla/pyjwt Author: Jose Padilla @@ -82,10 +82,11 @@ Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Utilities Requires-Python: >=3.6 Description-Content-Type: text/x-rst Provides-Extra: crypto +Provides-Extra: dev Provides-Extra: tests Provides-Extra: docs -Provides-Extra: dev diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/PyJWT.egg-info/PKG-INFO new/PyJWT-2.4.0/PyJWT.egg-info/PKG-INFO --- old/PyJWT-2.3.0/PyJWT.egg-info/PKG-INFO 2021-10-16 17:54:33.000000000 +0200 +++ new/PyJWT-2.4.0/PyJWT.egg-info/PKG-INFO 2022-05-12 20:55:44.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: PyJWT -Version: 2.3.0 +Version: 2.4.0 Summary: JSON Web Token implementation in Python Home-page: https://github.com/jpadilla/pyjwt Author: Jose Padilla @@ -82,10 +82,11 @@ Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Utilities Requires-Python: >=3.6 Description-Content-Type: text/x-rst Provides-Extra: crypto +Provides-Extra: dev Provides-Extra: tests Provides-Extra: docs -Provides-Extra: dev diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/PyJWT.egg-info/SOURCES.txt new/PyJWT-2.4.0/PyJWT.egg-info/SOURCES.txt --- old/PyJWT-2.3.0/PyJWT.egg-info/SOURCES.txt 2021-10-16 17:54:33.000000000 +0200 +++ new/PyJWT-2.4.0/PyJWT.egg-info/SOURCES.txt 2022-05-12 20:55:44.000000000 +0200 @@ -36,6 +36,7 @@ jwt/py.typed jwt/utils.py tests/__init__.py +tests/test_advisory.py tests/test_algorithms.py tests/test_api_jwk.py tests/test_api_jws.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/docs/api.rst new/PyJWT-2.4.0/docs/api.rst --- old/PyJWT-2.3.0/docs/api.rst 2021-08-08 21:28:32.000000000 +0200 +++ new/PyJWT-2.4.0/docs/api.rst 2022-05-12 20:31:26.000000000 +0200 @@ -68,6 +68,8 @@ :rtype: dict :returns: the JWT claims +.. module:: jwt.api_jwt + .. function:: decode_complete(jwt, key="", algorithms=None, options=None, audience=None, issuer=None, leeway=0) Identical to ``jwt.decode`` except for return value which is a dictionary containing the token header (JOSE Header), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/docs/conf.py new/PyJWT-2.4.0/docs/conf.py --- old/PyJWT-2.3.0/docs/conf.py 2020-12-21 17:55:46.000000000 +0100 +++ new/PyJWT-2.4.0/docs/conf.py 2022-05-12 20:31:26.000000000 +0200 @@ -51,7 +51,7 @@ # General information about the project. project = "PyJWT" -copyright = "2015, Jos?? Padilla" +copyright = "2015-2022, Jos?? Padilla" author = "Jos?? Padilla" # The version info for the project you're documenting, acts as replacement for diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/docs/usage.rst new/PyJWT-2.4.0/docs/usage.rst --- old/PyJWT-2.3.0/docs/usage.rst 2021-10-06 12:37:16.000000000 +0200 +++ new/PyJWT-2.4.0/docs/usage.rst 2022-05-12 20:31:26.000000000 +0200 @@ -45,6 +45,9 @@ ) encoded = jwt.encode({"some": "payload"}, private_key, algorithm="RS256") +If you are repeatedly encoding with the same private key, reusing the same +``RSAPrivateKey`` also has performance benefits because it avoids the +CPU-intensive ``RSA_check_key`` primality test. Specifying Additional Headers ----------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/__init__.py new/PyJWT-2.4.0/jwt/__init__.py --- old/PyJWT-2.3.0/jwt/__init__.py 2021-10-16 17:53:51.000000000 +0200 +++ new/PyJWT-2.4.0/jwt/__init__.py 2022-05-12 20:31:26.000000000 +0200 @@ -25,19 +25,19 @@ ) from .jwks_client import PyJWKClient -__version__ = "2.3.0" +__version__ = "2.4.0" __title__ = "PyJWT" __description__ = "JSON Web Token implementation in Python" __url__ = "https://pyjwt.readthedocs.io" __uri__ = __url__ -__doc__ = __description__ + " <" + __uri__ + ">" +__doc__ = f"{__description__} <{__uri__}>" __author__ = "Jos?? Padilla" __email__ = "he...@jpadilla.com" __license__ = "MIT" -__copyright__ = "Copyright 2015-2020 Jos?? Padilla" +__copyright__ = "Copyright 2015-2022 Jos?? Padilla" __all__ = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/algorithms.py new/PyJWT-2.4.0/jwt/algorithms.py --- old/PyJWT-2.3.0/jwt/algorithms.py 2021-10-06 12:37:16.000000000 +0200 +++ new/PyJWT-2.4.0/jwt/algorithms.py 2022-05-12 20:31:26.000000000 +0200 @@ -9,6 +9,8 @@ der_to_raw_signature, force_bytes, from_base64url_uint, + is_pem_format, + is_ssh_key, raw_to_der_signature, to_base64url_uint, ) @@ -183,14 +185,7 @@ def prepare_key(self, key): key = force_bytes(key) - invalid_strings = [ - b"-----BEGIN PUBLIC KEY-----", - b"-----BEGIN CERTIFICATE-----", - b"-----BEGIN RSA PUBLIC KEY-----", - 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 InvalidKeyError( "The specified key is an asymmetric key or x509 certificate and" " should not be used as an HMAC secret." @@ -417,6 +412,12 @@ except ValueError: key = load_pem_private_key(key, password=None) + # Explicit check the key to prevent confusing errors from cryptography + if not isinstance(key, (EllipticCurvePrivateKey, EllipticCurvePublicKey)): + raise InvalidKeyError( + "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for ECDSA algorithms" + ) + return key def sign(self, msg, key): @@ -545,26 +546,28 @@ pass def prepare_key(self, key): - - if isinstance( - key, - (Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey), - ): - return key - if isinstance(key, (bytes, str)): if isinstance(key, str): key = key.encode("utf-8") str_key = key.decode("utf-8") if "-----BEGIN PUBLIC" in str_key: - return load_pem_public_key(key) - if "-----BEGIN PRIVATE" in str_key: - return load_pem_private_key(key, password=None) - if str_key[0:4] == "ssh-": - return load_ssh_public_key(key) + key = load_pem_public_key(key) + elif "-----BEGIN PRIVATE" in str_key: + key = load_pem_private_key(key, password=None) + elif str_key[0:4] == "ssh-": + key = load_ssh_public_key(key) + + # Explicit check the key to prevent confusing errors from cryptography + if not isinstance( + key, + (Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey), + ): + raise InvalidKeyError( + "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for EdDSA algorithms" + ) - raise TypeError("Expecting a PEM-formatted or OpenSSH key.") + return key def sign(self, msg, key): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/api_jwk.py new/PyJWT-2.4.0/jwt/api_jwk.py --- old/PyJWT-2.3.0/jwt/api_jwk.py 2021-04-28 13:23:40.000000000 +0200 +++ new/PyJWT-2.4.0/jwt/api_jwk.py 2022-05-12 20:31:26.000000000 +0200 @@ -11,7 +11,7 @@ kty = self._jwk_data.get("kty", None) if not kty: - raise InvalidKeyError("kty is not found: %s" % self._jwk_data) + raise InvalidKeyError(f"kty is not found: {self._jwk_data}") if not algorithm and isinstance(self._jwk_data, dict): algorithm = self._jwk_data.get("alg", None) @@ -29,25 +29,25 @@ elif crv == "secp256k1": algorithm = "ES256K" else: - raise InvalidKeyError("Unsupported crv: %s" % crv) + raise InvalidKeyError(f"Unsupported crv: {crv}") elif kty == "RSA": algorithm = "RS256" elif kty == "oct": algorithm = "HS256" elif kty == "OKP": if not crv: - raise InvalidKeyError("crv is not found: %s" % self._jwk_data) + raise InvalidKeyError(f"crv is not found: {self._jwk_data}") if crv == "Ed25519": algorithm = "EdDSA" else: - raise InvalidKeyError("Unsupported crv: %s" % crv) + raise InvalidKeyError(f"Unsupported crv: {crv}") else: - raise InvalidKeyError("Unsupported kty: %s" % kty) + raise InvalidKeyError(f"Unsupported kty: {kty}") self.Algorithm = self._algorithms.get(algorithm) if not self.Algorithm: - raise PyJWKError("Unable to find a algorithm for key: %s" % self._jwk_data) + raise PyJWKError(f"Unable to find a algorithm for key: {self._jwk_data}") self.key = self.Algorithm.from_jwk(self._jwk_data) @@ -95,3 +95,9 @@ def from_json(data): obj = json.loads(data) return PyJWKSet.from_dict(obj) + + def __getitem__(self, kid): + for key in self.keys: + if key.key_id == kid: + return key + raise KeyError(f"keyset has no key for kid: {kid}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/api_jws.py new/PyJWT-2.4.0/jwt/api_jws.py --- old/PyJWT-2.3.0/jwt/api_jws.py 2021-10-16 14:23:39.000000000 +0200 +++ new/PyJWT-2.4.0/jwt/api_jws.py 2022-05-12 20:31:26.000000000 +0200 @@ -80,34 +80,54 @@ algorithm: Optional[str] = "HS256", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, + is_payload_detached: bool = False, ) -> str: segments = [] if algorithm is None: algorithm = "none" - # Prefer headers["alg"] if present to algorithm parameter. - if headers and "alg" in headers and headers["alg"]: - algorithm = headers["alg"] + # Prefer headers values if present to function parameters. + if headers: + headers_alg = headers.get("alg") + if headers_alg: + algorithm = headers["alg"] + + headers_b64 = headers.get("b64") + if headers_b64 is False: + is_payload_detached = True # Header - header = {"typ": self.header_typ, "alg": algorithm} + header = {"typ": self.header_typ, "alg": algorithm} # type: Dict[str, Any] if headers: self._validate_headers(headers) header.update(headers) - if not header["typ"]: - del header["typ"] + + if not header["typ"]: + del header["typ"] + + if is_payload_detached: + header["b64"] = False + elif "b64" in header: + # True is the standard value for b64, so no need for it + del header["b64"] json_header = json.dumps( header, separators=(",", ":"), cls=json_encoder ).encode() segments.append(base64url_encode(json_header)) - segments.append(base64url_encode(payload)) + + if is_payload_detached: + msg_payload = payload + else: + msg_payload = base64url_encode(payload) + segments.append(msg_payload) # Segments signing_input = b".".join(segments) + try: alg_obj = self._algorithms[algorithm] key = alg_obj.prepare_key(key) @@ -116,14 +136,15 @@ except KeyError as e: if not has_crypto and algorithm in requires_cryptography: raise NotImplementedError( - "Algorithm '%s' could not be found. Do you have cryptography " - "installed?" % algorithm + f"Algorithm '{algorithm}' could not be found. Do you have cryptography installed?" ) from e - else: - raise NotImplementedError("Algorithm not supported") from e + raise NotImplementedError("Algorithm not supported") from e segments.append(base64url_encode(signature)) + # Don't put the payload content inside the encoded token when detached + if is_payload_detached: + segments[1] = b"" encoded_string = b".".join(segments) return encoded_string.decode("utf-8") @@ -132,8 +153,9 @@ self, jwt: str, key: str = "", - algorithms: List[str] = None, - options: Dict = None, + algorithms: Optional[List[str]] = None, + options: Optional[Dict] = None, + detached_payload: Optional[bytes] = None, **kwargs, ) -> Dict[str, Any]: if options is None: @@ -148,6 +170,14 @@ payload, signing_input, header, signature = self._load(jwt) + if header.get("b64", True) is False: + if detached_payload is None: + raise DecodeError( + 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' + ) + payload = detached_payload + signing_input = b".".join([signing_input.rsplit(b".", 1)[0], payload]) + if verify_signature: self._verify_signature(signing_input, header, signature, key, algorithms) @@ -161,8 +191,8 @@ self, jwt: str, key: str = "", - algorithms: List[str] = None, - options: Dict = None, + algorithms: Optional[List[str]] = None, + options: Optional[Dict] = None, **kwargs, ) -> str: decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs) @@ -200,7 +230,7 @@ try: header = json.loads(header_data) except ValueError as e: - raise DecodeError("Invalid header string: %s" % e) from e + raise DecodeError(f"Invalid header string: {e}") from e if not isinstance(header, Mapping): raise DecodeError("Invalid header string: must be a json object") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/api_jwt.py new/PyJWT-2.4.0/jwt/api_jwt.py --- old/PyJWT-2.3.0/jwt/api_jwt.py 2021-10-16 14:23:39.000000000 +0200 +++ new/PyJWT-2.4.0/jwt/api_jwt.py 2022-05-12 20:31:26.000000000 +0200 @@ -1,4 +1,5 @@ import json +import warnings from calendar import timegm from collections.abc import Iterable, Mapping from datetime import datetime, timedelta, timezone @@ -66,14 +67,23 @@ self, jwt: str, key: str = "", - algorithms: List[str] = None, - options: Dict = None, + algorithms: Optional[List[str]] = None, + options: Optional[Dict] = None, **kwargs, ) -> Dict[str, Any]: - if options is None: - options = {"verify_signature": True} - else: - options.setdefault("verify_signature", True) + options = dict(options or {}) # shallow-copy or initialize an empty dict + options.setdefault("verify_signature", True) + + # If the user has set the legacy `verify` argument, and it doesn't match + # what the relevant `options` entry for the argument is, inform the user + # that they're likely making a mistake. + if "verify" in kwargs and kwargs["verify"] != options["verify_signature"]: + warnings.warn( + "The `verify` argument to `decode` does nothing in PyJWT 2.0 and newer. " + "The equivalent is setting `verify_signature` to False in the `options` dictionary. " + "This invocation has a mismatch between the kwarg and the option entry.", + category=DeprecationWarning, + ) if not options["verify_signature"]: options.setdefault("verify_exp", False) @@ -98,7 +108,7 @@ try: payload = json.loads(decoded["payload"]) except ValueError as e: - raise DecodeError("Invalid payload string: %s" % e) + raise DecodeError(f"Invalid payload string: {e}") if not isinstance(payload, dict): raise DecodeError("Invalid payload string: must be a json object") @@ -112,8 +122,8 @@ self, jwt: str, key: str = "", - algorithms: List[str] = None, - options: Dict = None, + algorithms: Optional[List[str]] = None, + options: Optional[Dict] = None, **kwargs, ) -> Dict[str, Any]: decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/exceptions.py new/PyJWT-2.4.0/jwt/exceptions.py --- old/PyJWT-2.3.0/jwt/exceptions.py 2020-08-24 18:22:55.000000000 +0200 +++ new/PyJWT-2.4.0/jwt/exceptions.py 2022-05-12 20:31:26.000000000 +0200 @@ -51,7 +51,7 @@ self.claim = claim def __str__(self): - return 'Token is missing the "%s" claim' % self.claim + return f'Token is missing the "{self.claim}" claim' class PyJWKError(PyJWTError): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/help.py new/PyJWT-2.4.0/jwt/help.py --- old/PyJWT-2.3.0/jwt/help.py 2020-12-21 17:55:46.000000000 +0100 +++ new/PyJWT-2.4.0/jwt/help.py 2022-05-12 20:31:26.000000000 +0200 @@ -28,10 +28,10 @@ if implementation == "CPython": implementation_version = platform.python_version() elif implementation == "PyPy": - implementation_version = "{}.{}.{}".format( - sys.pypy_version_info.major, - sys.pypy_version_info.minor, - sys.pypy_version_info.micro, + implementation_version = ( + f"{sys.pypy_version_info.major}." + f"{sys.pypy_version_info.minor}." + f"{sys.pypy_version_info.micro}" ) if sys.pypy_version_info.releaselevel != "final": implementation_version = "".join( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/jwt/utils.py new/PyJWT-2.4.0/jwt/utils.py --- old/PyJWT-2.3.0/jwt/utils.py 2021-10-06 12:37:16.000000000 +0200 +++ new/PyJWT-2.4.0/jwt/utils.py 2022-05-12 20:31:26.000000000 +0200 @@ -1,5 +1,6 @@ import base64 import binascii +import re from typing import Any, Union try: @@ -97,3 +98,63 @@ s = bytes_to_number(raw_sig[num_bytes:]) return encode_dss_signature(r, s) + + +# 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: bytes) -> bool: + 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-...@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: bytes) -> bool: + 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/setup.cfg new/PyJWT-2.4.0/setup.cfg --- old/PyJWT-2.3.0/setup.cfg 2021-10-16 17:54:33.865428000 +0200 +++ new/PyJWT-2.4.0/setup.cfg 2022-05-12 20:55:44.584843400 +0200 @@ -27,6 +27,7 @@ Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Topic :: Utilities [options] @@ -70,6 +71,7 @@ python_version = 3.6 ignore_missing_imports = true warn_unused_ignores = true +no_implicit_optional = true [egg_info] tag_build = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/tests/test_advisory.py new/PyJWT-2.4.0/tests/test_advisory.py --- old/PyJWT-2.3.0/tests/test_advisory.py 1970-01-01 01:00:00.000000000 +0100 +++ new/PyJWT-2.4.0/tests/test_advisory.py 2022-05-12 20:34:28.000000000 +0200 @@ -0,0 +1,112 @@ +import jwt +import pytest +from jwt.exceptions import InvalidKeyError + +from .utils import crypto_required + +priv_key_bytes = b'''-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIIbBhdo2ah7X32i50GOzrCr4acZTe6BezUdRIixjTAdL +-----END PRIVATE KEY-----''' + +pub_key_bytes = b'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPL1I9oiq+B8crkmuV4YViiUnhdLjCp3hvy1bNGuGfNL' + +ssh_priv_key_bytes = b"""-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIOWc7RbaNswMtNtc+n6WZDlUblMr2FBPo79fcGXsJlGQoAoGCCqGSM49 +AwEHoUQDQgAElcy2RSSSgn2RA/xCGko79N+7FwoLZr3Z0ij/ENjow2XpUDwwKEKk +Ak3TDXC9U8nipMlGcY7sDpXp2XyhHEM+Rw== +-----END EC PRIVATE KEY-----""" + +ssh_key_bytes = b"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJXMtkUkkoJ9kQP8QhpKO/TfuxcKC2a92dIo/xDY6MNl6VA8MChCpAJN0w1wvVPJ4qTJRnGO7A6V6dl8oRxDPkc=""" + + +class TestAdvisory: + @crypto_required + def test_ghsa_ffqj_6fqr_9h24(self): + # Generate ed25519 private key + # private_key = ed25519.Ed25519PrivateKey.generate() + + # Get private key bytes as they would be stored in a file + # priv_key_bytes = private_key.private_bytes( + # encoding=serialization.Encoding.PEM, + # format=serialization.PrivateFormat.PKCS8, + # encryption_algorithm=serialization.NoEncryption(), + # ) + + # Get public key bytes as they would be stored in a file + # pub_key_bytes = private_key.public_key().public_bytes( + # encoding=serialization.Encoding.OpenSSH, + # format=serialization.PublicFormat.OpenSSH, + # ) + + # Making a good jwt token that should work by signing it + # with the private key + # encoded_good = jwt.encode({"test": 1234}, priv_key_bytes, algorithm="EdDSA") + encoded_good = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJ0ZXN0IjoxMjM0fQ.M5y1EEavZkHSlj9i8yi9nXKKyPBSAUhDRTOYZi3zZY11tZItDaR3qwAye8pc74_lZY3Ogt9KPNFbVOSGnUBHDg' + + # Using HMAC with the public key to trick the receiver to think that the + # public key is a HMAC secret + encoded_bad = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoxMjM0fQ.6ulDpqSlbHmQ8bZXhZRLFko9SwcHrghCwh8d-exJEE4' + + # Both of the jwt tokens are validated as valid + jwt.decode( + encoded_good, + pub_key_bytes, + algorithms=jwt.algorithms.get_default_algorithms(), + ) + + with pytest.raises(InvalidKeyError): + jwt.decode( + encoded_bad, + pub_key_bytes, + algorithms=jwt.algorithms.get_default_algorithms(), + ) + + # Of course the receiver should specify ed25519 algorithm to be used if + # they specify ed25519 public key. However, if other algorithms are used, + # the POC does not work + # HMAC specifies illegal strings for the HMAC secret in jwt/algorithms.py + # + # invalid_str ings = [ + # b"-----BEGIN PUBLIC KEY-----", + # b"-----BEGIN CERTIFICATE-----", + # b"-----BEGIN RSA PUBLIC KEY-----", + # b"ssh-rsa", + # ] + # + # However, OKPAlgorithm (ed25519) accepts the following in jwt/algorithms.py: + # + # if "-----BEGIN PUBLIC" in str_key: + # return load_pem_public_key(key) + # if "-----BEGIN PRIVATE" in str_key: + # return load_pem_private_key(key, password=None) + # if str_key[0:4] == "ssh-": + # return load_ssh_public_key(key) + # + # These should most likely made to match each other to prevent this behavior + + # POC for the ecdsa-sha2-nistp256 format. + # openssl ecparam -genkey -name prime256v1 -noout -out ec256-key-priv.pem + # openssl ec -in ec256-key-priv.pem -pubout > ec256-key-pub.pem + # ssh-keygen -y -f ec256-key-priv.pem > ec256-key-ssh.pub + + # Making a good jwt token that should work by signing it with the private key + # encoded_good = jwt.encode({"test": 1234}, ssh_priv_key_bytes, algorithm="ES256") + encoded_good = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoxMjM0fQ.NX42mS8cNqYoL3FOW9ZcKw8Nfq2mb6GqJVADeMA1-kyHAclilYo_edhdM_5eav9tBRQTlL0XMeu_WFE_mz3OXg" + + # Using HMAC with the ssh public key to trick the receiver to think that the public key is a HMAC secret + # encoded_bad = jwt.encode({"test": 1234}, ssh_key_bytes, algorithm="HS256") + encoded_bad = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoxMjM0fQ.5eYfbrbeGYmWfypQ6rMWXNZ8bdHcqKng5GPr9MJZITU" + + # Both of the jwt tokens are validated as valid + jwt.decode( + encoded_good, + ssh_key_bytes, + algorithms=jwt.algorithms.get_default_algorithms() + ) + + with pytest.raises(InvalidKeyError): + jwt.decode( + encoded_bad, + ssh_key_bytes, + algorithms=jwt.algorithms.get_default_algorithms() + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/tests/test_algorithms.py new/PyJWT-2.4.0/tests/test_algorithms.py --- old/PyJWT-2.3.0/tests/test_algorithms.py 2021-10-06 12:37:16.000000000 +0200 +++ new/PyJWT-2.4.0/tests/test_algorithms.py 2022-05-12 20:31:26.000000000 +0200 @@ -226,16 +226,14 @@ for curve in ("P-256", "P-384", "P-521", "secp256k1"): with pytest.raises(InvalidKeyError): algo.from_jwk( - '{{"kty": "EC", "crv": "{}", "x": "dGVzdA==", ' - '"y": "dGVzdA=="}}'.format(curve) + f'{{"kty": "EC", "crv": "{curve}", "x": "dGVzdA==", "y": "dGVzdA=="}}' ) # EC private key length invalid for (curve, point) in valid_points.items(): with pytest.raises(InvalidKeyError): algo.from_jwk( - '{{"kty": "EC", "crv": "{}", "x": "{}", "y": "{}", ' - '"d": "dGVzdA=="}}'.format(curve, point["x"], point["y"]) + f'{{"kty": "EC", "crv": "{curve}", "x": "{point["x"]}", "y": "{point["y"]}", "d": "dGVzdA=="}}' ) @crypto_required @@ -495,6 +493,18 @@ assert not result @crypto_required + def test_ec_should_throw_exception_on_wrong_key(self): + algo = ECAlgorithm(ECAlgorithm.SHA256) + + with pytest.raises(InvalidKeyError): + with open(key_path("testkey_rsa.priv")) as keyfile: + algo.prepare_key(keyfile.read()) + + with pytest.raises(InvalidKeyError): + with open(key_path("testkey2_rsa.pub.pem")) as pem_key: + algo.prepare_key(pem_key.read()) + + @crypto_required def test_rsa_pss_sign_then_verify_should_return_true(self): algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256) @@ -669,7 +679,7 @@ def test_okp_ed25519_should_reject_non_string_key(self): algo = OKPAlgorithm() - with pytest.raises(TypeError): + with pytest.raises(InvalidKeyError): algo.prepare_key(None) with open(key_path("testkey_ed25519")) as keyfile: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/tests/test_api_jwk.py new/PyJWT-2.4.0/tests/test_api_jwk.py --- old/PyJWT-2.3.0/tests/test_api_jwk.py 2021-10-06 12:37:16.000000000 +0200 +++ new/PyJWT-2.4.0/tests/test_api_jwk.py 2022-05-12 20:31:26.000000000 +0200 @@ -252,3 +252,26 @@ assert jwk.key_type == "RSA" assert jwk.key_id == "keyid-abc123" assert jwk.public_key_use == "sig" + + @crypto_required + def test_keyset_should_index_by_kid(self): + algo = RSAAlgorithm(RSAAlgorithm.SHA256) + + with open(key_path("jwk_rsa_pub.json")) as keyfile: + pub_key = algo.from_jwk(keyfile.read()) + + key_data_str = algo.to_jwk(pub_key) + key_data = json.loads(key_data_str) + + # TODO Should `to_jwk` set these? + key_data["alg"] = "RS256" + key_data["use"] = "sig" + key_data["kid"] = "keyid-abc123" + + jwk_set = PyJWKSet.from_dict({"keys": [key_data]}) + + jwk = jwk_set.keys[0] + assert jwk == jwk_set["keyid-abc123"] + + with pytest.raises(KeyError): + jwk_set["this-kid-does-not-exist"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/tests/test_api_jws.py new/PyJWT-2.4.0/tests/test_api_jws.py --- old/PyJWT-2.3.0/tests/test_api_jws.py 2021-08-08 21:28:32.000000000 +0200 +++ new/PyJWT-2.4.0/tests/test_api_jws.py 2022-05-12 20:31:26.000000000 +0200 @@ -719,3 +719,54 @@ jws.encode(payload, "secret", headers={"kid": None}) assert "Key ID header parameter must be a string" == str(exc.value) + + def test_encode_decode_with_detached_content(self, jws, payload): + secret = "secret" + jws_message = jws.encode( + payload, secret, algorithm="HS256", is_payload_detached=True + ) + + jws.decode(jws_message, secret, algorithms=["HS256"], detached_payload=payload) + + def test_encode_detached_content_with_b64_header(self, jws, payload): + secret = "secret" + + # Check that detached content is automatically detected when b64 is false + headers = {"b64": False} + token = jws.encode(payload, secret, "HS256", headers) + + msg_header, msg_payload, _ = token.split(".") + msg_header = base64url_decode(msg_header.encode()) + msg_header_obj = json.loads(msg_header) + + assert "b64" in msg_header_obj + assert msg_header_obj["b64"] is False + # Check that the payload is not inside the token + assert not msg_payload + + # Check that content is not detached and b64 header removed when b64 is true + headers = {"b64": True} + token = jws.encode(payload, secret, "HS256", headers) + + msg_header, msg_payload, _ = token.split(".") + msg_header = base64url_decode(msg_header.encode()) + msg_header_obj = json.loads(msg_header) + + assert "b64" not in msg_header_obj + assert msg_payload + + def test_decode_detached_content_without_proper_argument(self, jws): + example_jws = ( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2V9" + "." + ".65yNkX_ZH4A_6pHaTL_eI84OXOHtfl4K0k5UnlXZ8f4" + ) + example_secret = "secret" + + with pytest.raises(DecodeError) as exc: + jws.decode(example_jws, example_secret, algorithms=["HS256"]) + + assert ( + 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' + in str(exc.value) + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/tests/test_api_jwt.py new/PyJWT-2.4.0/tests/test_api_jwt.py --- old/PyJWT-2.3.0/tests/test_api_jwt.py 2021-10-16 14:23:39.000000000 +0200 +++ new/PyJWT-2.4.0/tests/test_api_jwt.py 2022-05-12 20:31:26.000000000 +0200 @@ -658,3 +658,27 @@ jwt_message = jwt.encode(payload, secret) jwt.decode(jwt_message, secret, options={"verify_signature": False}) + + def test_decode_legacy_verify_warning(self, jwt, payload): + secret = "secret" + jwt_message = jwt.encode(payload, secret) + + with pytest.deprecated_call(): + # The implicit default for options.verify_signature is True, + # but the user sets verify to False. + jwt.decode(jwt_message, secret, verify=False, algorithms=["HS256"]) + + with pytest.deprecated_call(): + # The user explicitly sets verify=True, + # but contradicts it in verify_signature. + jwt.decode( + jwt_message, secret, verify=True, options={"verify_signature": False} + ) + + def test_decode_no_options_mutation(self, jwt, payload): + options = {"verify_signature": True} + orig_options = options.copy() + secret = "secret" + jwt_message = jwt.encode(payload, secret) + jwt.decode(jwt_message, secret, options=options, algorithms=["HS256"]) + assert options == orig_options diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.3.0/tox.ini new/PyJWT-2.4.0/tox.ini --- old/PyJWT-2.3.0/tox.ini 2021-01-12 14:06:52.000000000 +0100 +++ new/PyJWT-2.4.0/tox.ini 2022-05-12 20:31:26.000000000 +0200 @@ -12,6 +12,7 @@ 3.7: py37, docs 3.8: py38, typing 3.9: py39 + 3.10: py310 [tox]