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 2021-11-06 18:15:41 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-PyJWT (Old) and /work/SRC/openSUSE:Factory/.python-PyJWT.new.1890 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-PyJWT" Sat Nov 6 18:15:41 2021 rev:23 rq:929638 version:2.3.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-PyJWT/python-PyJWT.changes 2021-05-20 19:23:53.862232039 +0200 +++ /work/SRC/openSUSE:Factory/.python-PyJWT.new.1890/python-PyJWT.changes 2021-11-06 18:18:03.956885640 +0100 @@ -1,0 +2,17 @@ +Wed Nov 3 08:57:35 UTC 2021 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to 2.3.0 + * Revert "Remove arbitrary kwargs." (#701) + * Add exception chaining (#702) +- from version 2.2.0 + * Remove arbitrary kwargs. (#657) + * Use timezone package as Python 3.5+ is required. (#694) + * Assume JWK without the "use" claim is valid for signing + as per RFC7517 (#668) + * Prefer `headers["alg"]` to `algorithm` in `jwt.encode()`. (#673) + * Fix aud validation to support {'aud': null} case. (#670) + * Make `typ` optional in JWT to be compliant with RFC7519. (#644) + * Remove upper bound on cryptography version. (#693) + * Add support for Ed448/EdDSA. (#675) + +------------------------------------------------------------------- Old: ---- PyJWT-2.1.0.tar.gz New: ---- PyJWT-2.3.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-PyJWT.spec ++++++ --- /var/tmp/diff_new_pack.qER18i/_old 2021-11-06 18:18:04.404885872 +0100 +++ /var/tmp/diff_new_pack.qER18i/_new 2021-11-06 18:18:04.408885873 +0100 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %global skip_python2 1 Name: python-PyJWT -Version: 2.1.0 +Version: 2.3.0 Release: 0 Summary: JSON Web Token implementation in Python License: MIT ++++++ PyJWT-2.1.0.tar.gz -> PyJWT-2.3.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/.pre-commit-config.yaml new/PyJWT-2.3.0/.pre-commit-config.yaml --- old/PyJWT-2.1.0/.pre-commit-config.yaml 2021-04-28 13:47:41.000000000 +0200 +++ new/PyJWT-2.3.0/.pre-commit-config.yaml 2021-10-16 14:23:39.000000000 +0200 @@ -1,36 +1,36 @@ repos: - repo: https://github.com/psf/black - rev: 21.4b0 + rev: 21.9b0 hooks: - id: black args: ["--target-version=py36"] - repo: https://github.com/asottile/blacken-docs - rev: v1.10.0 + rev: v1.11.0 hooks: - id: blacken-docs args: ["--target-version=py36"] - repo: https://github.com/PyCQA/flake8 - rev: 3.9.1 + rev: 4.0.1 hooks: - id: flake8 language_version: python3.8 - repo: https://github.com/PyCQA/isort - rev: 5.8.0 + rev: 5.9.3 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/mgedmin/check-manifest - rev: "0.46" + rev: "0.47" hooks: - id: check-manifest args: [--no-build-isolation] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/CHANGELOG.rst new/PyJWT-2.3.0/CHANGELOG.rst --- old/PyJWT-2.1.0/CHANGELOG.rst 2021-04-28 13:59:20.000000000 +0200 +++ new/PyJWT-2.3.0/CHANGELOG.rst 2021-10-16 17:53:51.000000000 +0200 @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. This project adheres to `Semantic Versioning <https://semver.org/>`__. -`Unreleased <https://github.com/jpadilla/pyjwt/compare/2.1.0...HEAD>`__ +`Unreleased <https://github.com/jpadilla/pyjwt/compare/2.3.0...HEAD>`__ ----------------------------------------------------------------------- Changed @@ -16,6 +16,41 @@ Added ~~~~~ +`v2.3.0 <https://github.com/jpadilla/pyjwt/compare/2.2.0...2.3.0>`__ +----------------------------------------------------------------------- + +Fixed +~~~~~ + +- Revert "Remove arbitrary kwargs." `#701 <https://github.com/jpadilla/pyjwt/pull/701>`__ + +Added +~~~~~ + +- Add exception chaining `#702 <https://github.com/jpadilla/pyjwt/pull/702>`__ + +`v2.2.0 <https://github.com/jpadilla/pyjwt/compare/2.1.0...2.2.0>`__ +----------------------------------------------------------------------- + +Changed +~~~~~~~ + +- Remove arbitrary kwargs. `#657 <https://github.com/jpadilla/pyjwt/pull/657>`__ +- Use timezone package as Python 3.5+ is required. `#694 <https://github.com/jpadilla/pyjwt/pull/694>`__ + +Fixed +~~~~~ +- Assume JWK without the "use" claim is valid for signing as per RFC7517 `#668 <https://github.com/jpadilla/pyjwt/pull/668>`__ +- Prefer `headers["alg"]` to `algorithm` in `jwt.encode()`. `#673 <https://github.com/jpadilla/pyjwt/pull/673>`__ +- Fix aud validation to support {'aud': null} case. `#670 <https://github.com/jpadilla/pyjwt/pull/670>`__ +- Make `typ` optional in JWT to be compliant with RFC7519. `#644 <https://github.com/jpadilla/pyjwt/pull/644>`__ +- Remove upper bound on cryptography version. `#693 <https://github.com/jpadilla/pyjwt/pull/693>`__ + +Added +~~~~~ + +- Add support for Ed448/EdDSA. `#675 <https://github.com/jpadilla/pyjwt/pull/675>`__ + `v2.1.0 <https://github.com/jpadilla/pyjwt/compare/2.0.1...2.1.0>`__ -------------------------------------------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/PKG-INFO new/PyJWT-2.3.0/PKG-INFO --- old/PyJWT-2.1.0/PKG-INFO 2021-04-28 14:01:11.608931300 +0200 +++ new/PyJWT-2.3.0/PKG-INFO 2021-10-16 17:54:33.864155000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: PyJWT -Version: 2.1.0 +Version: 2.3.0 Summary: JSON Web Token implementation in Python Home-page: https://github.com/jpadilla/pyjwt Author: Jose Padilla @@ -86,6 +86,6 @@ 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.1.0/PyJWT.egg-info/PKG-INFO new/PyJWT-2.3.0/PyJWT.egg-info/PKG-INFO --- old/PyJWT-2.1.0/PyJWT.egg-info/PKG-INFO 2021-04-28 14:01:11.000000000 +0200 +++ new/PyJWT-2.3.0/PyJWT.egg-info/PKG-INFO 2021-10-16 17:54:33.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: PyJWT -Version: 2.1.0 +Version: 2.3.0 Summary: JSON Web Token implementation in Python Home-page: https://github.com/jpadilla/pyjwt Author: Jose Padilla @@ -86,6 +86,6 @@ 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.1.0/PyJWT.egg-info/SOURCES.txt new/PyJWT-2.3.0/PyJWT.egg-info/SOURCES.txt --- old/PyJWT-2.1.0/PyJWT.egg-info/SOURCES.txt 2021-04-28 14:01:11.000000000 +0200 +++ new/PyJWT-2.3.0/PyJWT.egg-info/SOURCES.txt 2021-10-16 17:54:33.000000000 +0200 @@ -56,7 +56,9 @@ tests/keys/jwk_ec_pub_secp256k1.json tests/keys/jwk_hmac.json tests/keys/jwk_okp_key_Ed25519.json +tests/keys/jwk_okp_key_Ed448.json tests/keys/jwk_okp_pub_Ed25519.json +tests/keys/jwk_okp_pub_Ed448.json tests/keys/jwk_rsa_key.json tests/keys/jwk_rsa_pub.json tests/keys/testkey2_rsa.pub.pem diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/PyJWT.egg-info/requires.txt new/PyJWT-2.3.0/PyJWT.egg-info/requires.txt --- old/PyJWT-2.1.0/PyJWT.egg-info/requires.txt 2021-04-28 14:01:11.000000000 +0200 +++ new/PyJWT-2.3.0/PyJWT.egg-info/requires.txt 2021-10-16 17:54:33.000000000 +0200 @@ -1,12 +1,12 @@ [crypto] -cryptography<4.0.0,>=3.3.1 +cryptography>=3.3.1 [dev] sphinx sphinx-rtd-theme zope.interface -cryptography<4.0.0,>=3.3.1 +cryptography>=3.3.1 pytest<7.0.0,>=6.0.0 coverage[toml]==5.0.4 mypy diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/docs/algorithms.rst new/PyJWT-2.3.0/docs/algorithms.rst --- old/PyJWT-2.1.0/docs/algorithms.rst 2021-04-28 13:43:46.000000000 +0200 +++ new/PyJWT-2.3.0/docs/algorithms.rst 2021-10-06 12:37:16.000000000 +0200 @@ -17,7 +17,7 @@ * PS256 - RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256 * PS384 - RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384 * PS512 - RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512 -* EdDSA - Ed25519 signature using SHA-512. Provides 128-bit security +* EdDSA - Both Ed25519 signature using SHA-512 and Ed448 signature using SHA-3 are supported. Ed25519 and Ed448 provide 128-bit and 224-bit security respectively. Asymmetric (Public-key) Algorithms ---------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/docs/api.rst new/PyJWT-2.3.0/docs/api.rst --- old/PyJWT-2.1.0/docs/api.rst 2021-04-28 13:43:48.000000000 +0200 +++ new/PyJWT-2.3.0/docs/api.rst 2021-08-08 21:28:32.000000000 +0200 @@ -13,8 +13,9 @@ * for **asymmetric algorithms**: PEM-formatted private key, a multiline string * for **symmetric algorithms**: plain string, sufficiently long for security - :param str algorithm: algorithm to sign the token with, e.g. ``"ES256"`` - :param dict headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")`` + :param str algorithm: algorithm to sign the token with, e.g. ``"ES256"``. + If ``headers`` includes ``alg``, it will be preferred to this parameter. + :param dict headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")``. :param json.JSONEncoder json_encoder: custom JSON encoder for ``payload`` and ``headers`` :rtype: str :returns: a JSON Web Token @@ -43,19 +44,23 @@ :param dict options: extended decoding and validation options - * ``require=[]`` list of claims that must be present. E.g. ``require=["exp", "iat", "nbf"]``. - Only verifies that the claims exists. Does NOT verify that the claims are valid. - * ``verify_aud=True`` but will be ignored if ``verify_signature`` is ``False``. - Check that ``aud`` (audience) claim matches ``audience`` - * ``verify_iat=True`` but will be ignored if ``verify_signature`` is ``False``. - Check that ``iat`` (issued at) claim value is an integer - * ``verify_exp=True`` but will be ignored if ``verify_signature`` is ``False``. - Check that ``exp`` (expiration) claim value is OK - * ``verify_iss=True`` but will be ignored if ``verify_signature`` is ``False``. - Check that ``iss`` (issuer) claim matches ``issuer`` - * ``verify_nbf=True`` but will be ignored if ``verify_signature`` is ``False``. - Check that ``nbf`` (not before) is in the past * ``verify_signature=True`` verify the JWT cryptographic signature + * ``require=[]`` list of claims that must be present. + Example: ``require=["exp", "iat", "nbf"]``. + **Only verifies that the claims exists**. Does not verify that the claims are valid. + * ``verify_aud=verify_signature`` check that ``aud`` (audience) claim matches ``audience`` + * ``verify_iss=verify_signature`` check that ``iss`` (issuer) claim matches ``issuer`` + * ``verify_exp=verify_signature`` check that ``exp`` (expiration) claim value is in the future + * ``verify_iat=verify_signature`` check that ``iat`` (issued at) claim value is an integer + * ``verify_nbf=verify_signature`` check that ``nbf`` (not before) claim value is in the past + + .. warning:: + + ``exp``, ``iat`` and ``nbf`` will only be verified if present. + Please pass respective value to ``require`` if you want to make + sure that they are always present (and therefore always verified + if ``verify_exp``, ``verify_iat``, and ``verify_nbf`` respectively + is set to ``True``). :param Iterable audience: optional, the value for ``verify_aud`` check :param str issuer: optional, the value for ``verify_iss`` check @@ -63,7 +68,58 @@ :rtype: dict :returns: the JWT claims -.. note:: TODO: Document PyJWS / PyJWT classes +.. 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), + the token payload (JWT Payload), and token signature (JWT Signature) on the keys "header", "payload", + and "signature" respectively. + + :param str jwt: the token to be decoded + :param str key: the key suitable for the allowed algorithm + + :param list algorithms: allowed algorithms, e.g. ``["ES256"]`` + + .. warning:: + + Do **not** compute the ``algorithms`` parameter based on + the ``alg`` from the token itself, or on any other data + that an attacker may be able to influence, as that might + expose you to various vulnerabilities (see `RFC 8725 ??2.1 + <https://www.rfc-editor.org/rfc/rfc8725.html#section-2.1>`_). Instead, + either hard-code a fixed value for ``algorithms``, or + configure it in the same place you configure the + ``key``. Make sure not to mix symmetric and asymmetric + algorithms that interpret the ``key`` in different ways + (e.g. HS\* and RS\*). + + :param dict options: extended decoding and validation options + + * ``verify_signature=True`` verify the JWT cryptographic signature + * ``require=[]`` list of claims that must be present. + Example: ``require=["exp", "iat", "nbf"]``. + **Only verifies that the claims exists**. Does not verify that the claims are valid. + * ``verify_aud=verify_signature`` check that ``aud`` (audience) claim matches ``audience`` + * ``verify_iss=verify_signature`` check that ``iss`` (issuer) claim matches ``issuer`` + * ``verify_exp=verify_signature`` check that ``exp`` (expiration) claim value is in the future + * ``verify_iat=verify_signature`` check that ``iat`` (issued at) claim value is an integer + * ``verify_nbf=verify_signature`` check that ``nbf`` (not before) claim value is in the past + + .. warning:: + + ``exp``, ``iat`` and ``nbf`` will only be verified if present. + Please pass respective value to ``require`` if you want to make + sure that they are always present (and therefore always verified + if ``verify_exp``, ``verify_iat``, and ``verify_nbf`` respectively + is set to ``True``). + + :param Iterable audience: optional, the value for ``verify_aud`` check + :param str issuer: optional, the value for ``verify_iss`` check + :param float leeway: a time margin in seconds for the expiration check + :rtype: dict + :returns: Decoded JWT with the JOSE Header on the key ``header``, the JWS + Payload on the key ``payload``, and the JWS Signature on the key ``signature``. + +.. note:: TODO: Document PyJWS class Exceptions ---------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/docs/installation.rst new/PyJWT-2.3.0/docs/installation.rst --- old/PyJWT-2.1.0/docs/installation.rst 2020-08-24 16:24:03.000000000 +0200 +++ new/PyJWT-2.3.0/docs/installation.rst 2021-08-08 21:55:24.000000000 +0200 @@ -7,6 +7,9 @@ $ pip install pyjwt + +.. _installation_cryptography: + Cryptographic Dependencies (Optional) ------------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/docs/usage.rst new/PyJWT-2.3.0/docs/usage.rst --- old/PyJWT-2.1.0/docs/usage.rst 2021-04-28 13:23:40.000000000 +0200 +++ new/PyJWT-2.3.0/docs/usage.rst 2021-10-06 12:37:16.000000000 +0200 @@ -17,6 +17,8 @@ Encoding & Decoding Tokens with RS256 (RSA) ------------------------------------------- +RSA encoding and decoding require the ``cryptography`` module. See :ref:`installation_cryptography`. + .. code-block:: pycon >>> import jwt @@ -117,7 +119,7 @@ .. code-block:: python jwt.encode({"exp": 1371720939}, "secret") - jwt.encode({"exp": datetime.utcnow()}, "secret") + jwt.encode({"exp": datetime.now(tz=timezone.utc)}, "secret") Expiration time is automatically verified in `jwt.decode()` and raises `jwt.ExpiredSignatureError` if the expiration time is in the past: @@ -131,7 +133,7 @@ ... Expiration time will be compared to the current UTC time (as given by -`timegm(datetime.utcnow().utctimetuple())`), so be sure to use a UTC timestamp +`timegm(datetime.now(tz=timezone.utc).utctimetuple())`), so be sure to use a UTC timestamp or datetime in encoding. You can turn off expiration time verification with the `verify_exp` parameter in the options argument. @@ -145,7 +147,8 @@ .. code-block:: python jwt_payload = jwt.encode( - {"exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=30)}, "secret" + {"exp": datetime.datetime.now(tz=timezone.utc) + datetime.timedelta(seconds=30)}, + "secret", ) time.sleep(32) @@ -179,7 +182,7 @@ .. code-block:: python jwt.encode({"nbf": 1371720939}, "secret") - jwt.encode({"nbf": datetime.utcnow()}, "secret") + jwt.encode({"nbf": datetime.now(tz=timezone.utc)}, "secret") Issuer Claim (iss) ~~~~~~~~~~~~~~~~~~ @@ -257,7 +260,7 @@ .. code-block:: python jwt.encode({"iat": 1371720939}, "secret") - jwt.encode({"iat": datetime.utcnow()}, "secret") + jwt.encode({"iat": datetime.now(tz=timezone.utc)}, "secret") Requiring Presence of Claims ---------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/jwt/__init__.py new/PyJWT-2.3.0/jwt/__init__.py --- old/PyJWT-2.1.0/jwt/__init__.py 2021-04-28 13:59:20.000000000 +0200 +++ new/PyJWT-2.3.0/jwt/__init__.py 2021-10-16 17:53:51.000000000 +0200 @@ -25,7 +25,7 @@ ) from .jwks_client import PyJWKClient -__version__ = "2.1.0" +__version__ = "2.3.0" __title__ = "PyJWT" __description__ = "JSON Web Token implementation in Python" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/jwt/algorithms.py new/PyJWT-2.3.0/jwt/algorithms.py --- old/PyJWT-2.1.0/jwt/algorithms.py 2021-04-28 13:43:48.000000000 +0200 +++ new/PyJWT-2.3.0/jwt/algorithms.py 2021-10-06 12:37:16.000000000 +0200 @@ -22,6 +22,10 @@ EllipticCurvePrivateKey, EllipticCurvePublicKey, ) + from cryptography.hazmat.primitives.asymmetric.ed448 import ( + Ed448PrivateKey, + Ed448PublicKey, + ) from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, @@ -93,7 +97,7 @@ "PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256), "PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384), "PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512), - "EdDSA": Ed25519Algorithm(), + "EdDSA": OKPAlgorithm(), } ) @@ -243,22 +247,21 @@ self.hash_alg = hash_alg def prepare_key(self, key): - if isinstance(key, RSAPrivateKey) or isinstance(key, RSAPublicKey): + if isinstance(key, (RSAPrivateKey, RSAPublicKey)): return key - if isinstance(key, (bytes, str)): - key = force_bytes(key) - - try: - if key.startswith(b"ssh-rsa"): - key = load_ssh_public_key(key) - else: - key = load_pem_private_key(key, password=None) - except ValueError: - key = load_pem_public_key(key) - else: + if not isinstance(key, (bytes, str)): raise TypeError("Expecting a PEM-formatted key.") + key = force_bytes(key) + + try: + if key.startswith(b"ssh-rsa"): + key = load_ssh_public_key(key) + else: + key = load_pem_private_key(key, password=None) + except ValueError: + key = load_pem_public_key(key) return key @staticmethod @@ -395,27 +398,24 @@ self.hash_alg = hash_alg def prepare_key(self, key): - if isinstance(key, EllipticCurvePrivateKey) or isinstance( - key, EllipticCurvePublicKey - ): + if isinstance(key, (EllipticCurvePrivateKey, EllipticCurvePublicKey)): return key - if isinstance(key, (bytes, str)): - key = force_bytes(key) + if not isinstance(key, (bytes, str)): + raise TypeError("Expecting a PEM-formatted key.") - # Attempt to load key. We don't know if it's - # a Signing Key or a Verifying Key, so we try - # the Verifying Key first. - try: - if key.startswith(b"ecdsa-sha2-"): - key = load_ssh_public_key(key) - else: - key = load_pem_public_key(key) - except ValueError: - key = load_pem_private_key(key, password=None) + key = force_bytes(key) - else: - raise TypeError("Expecting a PEM-formatted key.") + # Attempt to load key. We don't know if it's + # a Signing Key or a Verifying Key, so we try + # the Verifying Key first. + try: + if key.startswith(b"ecdsa-sha2-"): + key = load_ssh_public_key(key) + else: + key = load_pem_public_key(key) + except ValueError: + key = load_pem_private_key(key, password=None) return key @@ -534,9 +534,9 @@ except InvalidSignature: return False - class Ed25519Algorithm(Algorithm): + class OKPAlgorithm(Algorithm): """ - Performs signing and verification operations using Ed25519 + Performs signing and verification operations using EdDSA This class requires ``cryptography>=2.6`` to be installed. """ @@ -546,7 +546,10 @@ def prepare_key(self, key): - if isinstance(key, (Ed25519PrivateKey, Ed25519PublicKey)): + if isinstance( + key, + (Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey), + ): return key if isinstance(key, (bytes, str)): @@ -565,9 +568,10 @@ def sign(self, msg, key): """ - Sign a message ``msg`` using the Ed25519 private key ``key`` + Sign a message ``msg`` using the EdDSA private key ``key`` :param str|bytes msg: Message to sign - :param Ed25519PrivateKey key: A :class:`.Ed25519PrivateKey` instance + :param Ed25519PrivateKey}Ed448PrivateKey key: A :class:`.Ed25519PrivateKey` + or :class:`.Ed448PrivateKey` iinstance :return bytes signature: The signature, as bytes """ msg = bytes(msg, "utf-8") if type(msg) is not bytes else msg @@ -575,18 +579,19 @@ def verify(self, msg, key, sig): """ - Verify a given ``msg`` against a signature ``sig`` using the Ed25519 key ``key`` + Verify a given ``msg`` against a signature ``sig`` using the EdDSA key ``key`` - :param str|bytes sig: Ed25519 signature to check ``msg`` against + :param str|bytes sig: EdDSA signature to check ``msg`` against :param str|bytes msg: Message to sign - :param Ed25519PrivateKey|Ed25519PublicKey key: A private or public Ed25519 key instance + :param Ed25519PrivateKey|Ed25519PublicKey|Ed448PrivateKey|Ed448PublicKey key: + A private or public EdDSA key instance :return bool verified: True if signature is valid, False if not. """ try: msg = bytes(msg, "utf-8") if type(msg) is not bytes else msg sig = bytes(sig, "utf-8") if type(sig) is not bytes else sig - if isinstance(key, Ed25519PrivateKey): + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): key = key.public_key() key.verify(sig, msg) return True # If no exception was raised, the signature is valid. @@ -595,21 +600,21 @@ @staticmethod def to_jwk(key): - if isinstance(key, Ed25519PublicKey): + if isinstance(key, (Ed25519PublicKey, Ed448PublicKey)): x = key.public_bytes( encoding=Encoding.Raw, format=PublicFormat.Raw, ) - + crv = "Ed25519" if isinstance(key, Ed25519PublicKey) else "Ed448" return json.dumps( { "x": base64url_encode(force_bytes(x)).decode(), "kty": "OKP", - "crv": "Ed25519", + "crv": crv, } ) - if isinstance(key, Ed25519PrivateKey): + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): d = key.private_bytes( encoding=Encoding.Raw, format=PrivateFormat.Raw, @@ -621,12 +626,13 @@ format=PublicFormat.Raw, ) + crv = "Ed25519" if isinstance(key, Ed25519PrivateKey) else "Ed448" return json.dumps( { "x": base64url_encode(force_bytes(x)).decode(), "d": base64url_encode(force_bytes(d)).decode(), "kty": "OKP", - "crv": "Ed25519", + "crv": crv, } ) @@ -648,7 +654,7 @@ raise InvalidKeyError("Not an Octet Key Pair") curve = obj.get("crv") - if curve != "Ed25519": + if curve != "Ed25519" and curve != "Ed448": raise InvalidKeyError(f"Invalid curve: {curve}") if "x" not in obj: @@ -657,8 +663,12 @@ try: if "d" not in obj: - return Ed25519PublicKey.from_public_bytes(x) + if curve == "Ed25519": + return Ed25519PublicKey.from_public_bytes(x) + return Ed448PublicKey.from_public_bytes(x) d = base64url_decode(obj.get("d")) - return Ed25519PrivateKey.from_private_bytes(d) + if curve == "Ed25519": + return Ed25519PrivateKey.from_private_bytes(d) + return Ed448PrivateKey.from_private_bytes(d) except ValueError as err: raise InvalidKeyError("Invalid key parameter") from err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/jwt/api_jws.py new/PyJWT-2.3.0/jwt/api_jws.py --- old/PyJWT-2.1.0/jwt/api_jws.py 2020-12-21 17:55:46.000000000 +0100 +++ new/PyJWT-2.3.0/jwt/api_jws.py 2021-10-16 14:23:39.000000000 +0200 @@ -77,7 +77,7 @@ self, payload: bytes, key: str, - algorithm: str = "HS256", + algorithm: Optional[str] = "HS256", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, ) -> str: @@ -86,8 +86,9 @@ if algorithm is None: algorithm = "none" - if algorithm not in self._valid_algs: - pass + # Prefer headers["alg"] if present to algorithm parameter. + if headers and "alg" in headers and headers["alg"]: + algorithm = headers["alg"] # Header header = {"typ": self.header_typ, "alg": algorithm} @@ -95,6 +96,8 @@ if headers: self._validate_headers(headers) header.update(headers) + if not header["typ"]: + del header["typ"] json_header = json.dumps( header, separators=(",", ":"), cls=json_encoder @@ -110,14 +113,14 @@ key = alg_obj.prepare_key(key) signature = alg_obj.sign(signing_input, key) - except KeyError: + 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 - ) + ) from e else: - raise NotImplementedError("Algorithm not supported") + raise NotImplementedError("Algorithm not supported") from e segments.append(base64url_encode(signature)) @@ -235,8 +238,8 @@ if not alg_obj.verify(signing_input, key, signature): raise InvalidSignatureError("Signature verification failed") - except KeyError: - raise InvalidAlgorithmError("Algorithm not supported") + except KeyError as e: + raise InvalidAlgorithmError("Algorithm not supported") from e def _validate_headers(self, headers): if "kid" in headers: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/jwt/api_jwt.py new/PyJWT-2.3.0/jwt/api_jwt.py --- old/PyJWT-2.1.0/jwt/api_jwt.py 2021-04-28 13:23:40.000000000 +0200 +++ new/PyJWT-2.3.0/jwt/api_jwt.py 2021-10-16 14:23:39.000000000 +0200 @@ -1,7 +1,7 @@ import json from calendar import timegm from collections.abc import Iterable, Mapping -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Type, Union from . import api_jws @@ -38,7 +38,7 @@ self, payload: Dict[str, Any], key: str, - algorithm: str = "HS256", + algorithm: Optional[str] = "HS256", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, ) -> str: @@ -130,7 +130,7 @@ self._validate_required_claims(payload, options) - now = timegm(datetime.utcnow().utctimetuple()) + now = timegm(datetime.now(tz=timezone.utc).utctimetuple()) if "iat" in payload and options["verify_iat"]: self._validate_iat(payload, now, leeway) @@ -177,19 +177,18 @@ raise ExpiredSignatureError("Signature has expired") def _validate_aud(self, payload, audience): - if audience is None and "aud" not in payload: - return + if audience is None: + if "aud" not in payload or not payload["aud"]: + return + # Application did not specify an audience, but + # the token has the 'aud' claim + raise InvalidAudienceError("Invalid audience") - if audience is not None and "aud" not in payload: + if "aud" not in payload or not payload["aud"]: # Application specified an audience, but it could not be # verified since the token does not contain a claim. raise MissingRequiredClaimError("aud") - if audience is None and "aud" in payload: - # Application did not specify an audience, but - # the token has the 'aud' claim - raise InvalidAudienceError("Invalid audience") - audience_claims = payload["aud"] if isinstance(audience_claims, str): @@ -202,7 +201,7 @@ if isinstance(audience, str): audience = [audience] - if not any(aud in audience_claims for aud in audience): + if all(aud not in audience_claims for aud in audience): raise InvalidAudienceError("Invalid audience") def _validate_iss(self, payload, issuer): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/jwt/jwks_client.py new/PyJWT-2.3.0/jwt/jwks_client.py --- old/PyJWT-2.1.0/jwt/jwks_client.py 2021-04-28 13:23:40.000000000 +0200 +++ new/PyJWT-2.3.0/jwt/jwks_client.py 2021-10-06 12:37:16.000000000 +0200 @@ -26,13 +26,13 @@ def get_signing_keys(self) -> List[PyJWK]: jwk_set = self.get_jwk_set() - signing_keys = [] + signing_keys = [ + jwk_set_key + for jwk_set_key in jwk_set.keys + if jwk_set_key.public_key_use in ["sig", None] and jwk_set_key.key_id + ] - for jwk_set_key in jwk_set.keys: - if jwk_set_key.public_key_use == "sig" and jwk_set_key.key_id: - signing_keys.append(jwk_set_key) - - if len(signing_keys) == 0: + if not signing_keys: raise PyJWKClientError("The JWKS endpoint did not contain any signing keys") return signing_keys diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/jwt/utils.py new/PyJWT-2.3.0/jwt/utils.py --- old/PyJWT-2.1.0/jwt/utils.py 2020-12-21 17:55:46.000000000 +0100 +++ new/PyJWT-2.3.0/jwt/utils.py 2021-10-06 12:37:16.000000000 +0200 @@ -59,8 +59,7 @@ def number_to_bytes(num: int, num_bytes: int) -> bytes: padded_hex = "%0*x" % (2 * num_bytes, num) - big_endian = binascii.a2b_hex(padded_hex.encode("ascii")) - return big_endian + return binascii.a2b_hex(padded_hex.encode("ascii")) def bytes_to_number(string: bytes) -> int: @@ -72,7 +71,7 @@ byte_length = 0 while remaining != 0: - remaining = remaining >> 8 + remaining >>= 8 byte_length += 1 return val.to_bytes(byte_length, "big", signed=False) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/setup.cfg new/PyJWT-2.3.0/setup.cfg --- old/PyJWT-2.1.0/setup.cfg 2021-04-28 14:01:11.610274000 +0200 +++ new/PyJWT-2.3.0/setup.cfg 2021-10-16 17:54:33.865428000 +0200 @@ -44,7 +44,7 @@ sphinx-rtd-theme zope.interface crypto = - cryptography>=3.3.1,<4.0.0 + cryptography>=3.3.1 tests = pytest>=6.0.0,<7.0.0 coverage[toml]==5.0.4 @@ -52,7 +52,7 @@ sphinx sphinx-rtd-theme zope.interface - cryptography>=3.3.1,<4.0.0 + cryptography>=3.3.1 pytest>=6.0.0,<7.0.0 coverage[toml]==5.0.4 mypy diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/keys/jwk_okp_key_Ed448.json new/PyJWT-2.3.0/tests/keys/jwk_okp_key_Ed448.json --- old/PyJWT-2.1.0/tests/keys/jwk_okp_key_Ed448.json 1970-01-01 01:00:00.000000000 +0100 +++ new/PyJWT-2.3.0/tests/keys/jwk_okp_key_Ed448.json 2021-10-06 12:37:16.000000000 +0200 @@ -0,0 +1,9 @@ +{ + "kty": "OKP", + "kid": "sig_ed448_01", + "crv": "Ed448", + "use": "sig", + "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", + "d": "Zh5xx0r_0tq39xj-8jGuCwAA6wsDim2ME7cX_iXzqDRgPN8lsZZHu60AO7m31Fa4NtHO07eU63q8", + "alg": "EdDSA" +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/keys/jwk_okp_pub_Ed448.json new/PyJWT-2.3.0/tests/keys/jwk_okp_pub_Ed448.json --- old/PyJWT-2.1.0/tests/keys/jwk_okp_pub_Ed448.json 1970-01-01 01:00:00.000000000 +0100 +++ new/PyJWT-2.3.0/tests/keys/jwk_okp_pub_Ed448.json 2021-10-06 12:37:16.000000000 +0200 @@ -0,0 +1,8 @@ +{ + "kty": "OKP", + "kid": "sig_ed448_01", + "crv": "Ed448", + "use": "sig", + "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", + "alg": "EdDSA" +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/test_algorithms.py new/PyJWT-2.3.0/tests/test_algorithms.py --- old/PyJWT-2.1.0/tests/test_algorithms.py 2021-04-28 13:43:48.000000000 +0200 +++ new/PyJWT-2.3.0/tests/test_algorithms.py 2021-10-06 12:37:16.000000000 +0200 @@ -11,12 +11,7 @@ from .utils import crypto_required, key_path if has_crypto: - from jwt.algorithms import ( - ECAlgorithm, - Ed25519Algorithm, - RSAAlgorithm, - RSAPSSAlgorithm, - ) + from jwt.algorithms import ECAlgorithm, OKPAlgorithm, RSAAlgorithm, RSAPSSAlgorithm class TestAlgorithms: @@ -667,12 +662,12 @@ @crypto_required -class TestEd25519Algorithms: +class TestOKPAlgorithms: hello_world_sig = b"Qxa47mk/azzUgmY2StAOguAd4P7YBLpyCfU3JdbaiWnXM4o4WibXwmIHvNYgN3frtE2fcyd8OYEaOiD/KiwkCg==" hello_world = b"Hello World!" - def test_ed25519_should_reject_non_string_key(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_should_reject_non_string_key(self): + algo = OKPAlgorithm() with pytest.raises(TypeError): algo.prepare_key(None) @@ -683,14 +678,14 @@ with open(key_path("testkey_ed25519.pub")) as keyfile: algo.prepare_key(keyfile.read()) - def test_ed25519_should_accept_unicode_key(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_should_accept_unicode_key(self): + algo = OKPAlgorithm() with open(key_path("testkey_ed25519")) as ec_key: algo.prepare_key(ec_key.read()) - def test_ed25519_sign_should_generate_correct_signature_value(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_sign_should_generate_correct_signature_value(self): + algo = OKPAlgorithm() jwt_message = self.hello_world @@ -706,8 +701,8 @@ result = algo.verify(jwt_message, jwt_pub_key, expected_sig) assert result - def test_ed25519_verify_should_return_false_if_signature_invalid(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_verify_should_return_false_if_signature_invalid(self): + algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(self.hello_world_sig) @@ -720,8 +715,8 @@ result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert not result - def test_ed25519_verify_should_return_true_if_signature_valid(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_verify_should_return_true_if_signature_valid(self): + algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(self.hello_world_sig) @@ -732,8 +727,8 @@ result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert result - def test_ed25519_prepare_key_should_be_idempotent(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_prepare_key_should_be_idempotent(self): + algo = OKPAlgorithm() with open(key_path("testkey_ed25519.pub")) as keyfile: jwt_pub_key_first = algo.prepare_key(keyfile.read()) @@ -741,8 +736,8 @@ assert jwt_pub_key_first == jwt_pub_key_second - def test_ed25519_jwk_private_key_should_parse_and_verify(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_jwk_private_key_should_parse_and_verify(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: key = algo.from_jwk(keyfile.read()) @@ -750,8 +745,19 @@ signature = algo.sign(b"Hello World!", key) assert algo.verify(b"Hello World!", key.public_key(), signature) - def test_ed25519_jwk_public_key_should_parse_and_verify(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_jwk_private_key_should_parse_and_verify_with_private_key_as_is( + self, + ): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: + key = algo.from_jwk(keyfile.read()) + + signature = algo.sign(b"Hello World!", key) + assert algo.verify(b"Hello World!", key, signature) + + def test_okp_ed25519_jwk_public_key_should_parse_and_verify(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: priv_key = algo.from_jwk(keyfile.read()) @@ -762,8 +768,8 @@ signature = algo.sign(b"Hello World!", priv_key) assert algo.verify(b"Hello World!", pub_key, signature) - def test_ed25519_jwk_fails_on_invalid_json(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_jwk_fails_on_invalid_json(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile: valid_pub = json.loads(keyfile.read()) @@ -790,6 +796,12 @@ with pytest.raises(InvalidKeyError): algo.from_jwk(v) + # Invalid crv, "Ed448" + v = valid_pub.copy() + v["crv"] = "Ed448" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + # Missing x v = valid_pub.copy() del v["x"] @@ -808,8 +820,8 @@ with pytest.raises(InvalidKeyError): algo.from_jwk(v) - def test_ed25519_to_jwk_works_with_from_jwk(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_to_jwk_works_with_from_jwk(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: priv_key_1 = algo.from_jwk(keyfile.read()) @@ -827,8 +839,111 @@ assert algo.verify(b"Hello World!", pub_key_2, signature_1) assert algo.verify(b"Hello World!", pub_key_2, signature_2) - def test_ed25519_to_jwk_raises_exception_on_invalid_key(self): - algo = Ed25519Algorithm() + def test_okp_to_jwk_raises_exception_on_invalid_key(self): + algo = OKPAlgorithm() with pytest.raises(InvalidKeyError): algo.to_jwk({"not": "a valid key"}) + + def test_okp_ed448_jwk_private_key_should_parse_and_verify(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + key = algo.from_jwk(keyfile.read()) + + signature = algo.sign(b"Hello World!", key) + assert algo.verify(b"Hello World!", key.public_key(), signature) + + def test_okp_ed448_jwk_private_key_should_parse_and_verify_with_private_key_as_is( + self, + ): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + key = algo.from_jwk(keyfile.read()) + + signature = algo.sign(b"Hello World!", key) + assert algo.verify(b"Hello World!", key, signature) + + def test_okp_ed448_jwk_public_key_should_parse_and_verify(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + priv_key = algo.from_jwk(keyfile.read()) + + with open(key_path("jwk_okp_pub_Ed448.json")) as keyfile: + pub_key = algo.from_jwk(keyfile.read()) + + signature = algo.sign(b"Hello World!", priv_key) + assert algo.verify(b"Hello World!", pub_key, signature) + + def test_okp_ed448_jwk_fails_on_invalid_json(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_pub_Ed448.json")) as keyfile: + valid_pub = json.loads(keyfile.read()) + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + valid_key = json.loads(keyfile.read()) + + # Invalid instance type + with pytest.raises(InvalidKeyError): + algo.from_jwk(123) + + # Invalid JSON + with pytest.raises(InvalidKeyError): + algo.from_jwk("<this isn't json>") + + # Invalid kty, not "OKP" + v = valid_pub.copy() + v["kty"] = "oct" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + + # Invalid crv, not "Ed448" + v = valid_pub.copy() + v["crv"] = "P-256" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + + # Invalid crv, "Ed25519" + v = valid_pub.copy() + v["crv"] = "Ed25519" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + + # Missing x + v = valid_pub.copy() + del v["x"] + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + + # Invalid x + v = valid_pub.copy() + v["x"] = "123" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + + # Invalid d + v = valid_key.copy() + v["d"] = "123" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + + def test_okp_ed448_to_jwk_works_with_from_jwk(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + priv_key_1 = algo.from_jwk(keyfile.read()) + + with open(key_path("jwk_okp_pub_Ed448.json")) as keyfile: + pub_key_1 = algo.from_jwk(keyfile.read()) + + pub = algo.to_jwk(pub_key_1) + pub_key_2 = algo.from_jwk(pub) + pri = algo.to_jwk(priv_key_1) + priv_key_2 = algo.from_jwk(pri) + + signature_1 = algo.sign(b"Hello World!", priv_key_1) + signature_2 = algo.sign(b"Hello World!", priv_key_2) + assert algo.verify(b"Hello World!", pub_key_2, signature_1) + assert algo.verify(b"Hello World!", pub_key_2, signature_2) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/test_api_jwk.py new/PyJWT-2.3.0/tests/test_api_jwk.py --- old/PyJWT-2.1.0/tests/test_api_jwk.py 2021-04-28 13:23:40.000000000 +0200 +++ new/PyJWT-2.3.0/tests/test_api_jwk.py 2021-10-06 12:37:16.000000000 +0200 @@ -9,12 +9,7 @@ from .utils import crypto_required, key_path if has_crypto: - from jwt.algorithms import ( - ECAlgorithm, - Ed25519Algorithm, - HMACAlgorithm, - RSAAlgorithm, - ) + from jwt.algorithms import ECAlgorithm, HMACAlgorithm, OKPAlgorithm, RSAAlgorithm class TestPyJWK: @@ -166,7 +161,7 @@ jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "OKP" - assert isinstance(jwk.Algorithm, Ed25519Algorithm) + assert isinstance(jwk.Algorithm, OKPAlgorithm) @crypto_required def test_from_dict_should_throw_exception_if_arg_is_invalid(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/test_api_jws.py new/PyJWT-2.3.0/tests/test_api_jws.py --- old/PyJWT-2.1.0/tests/test_api_jws.py 2021-04-28 13:47:41.000000000 +0200 +++ new/PyJWT-2.3.0/tests/test_api_jws.py 2021-08-08 21:28:32.000000000 +0200 @@ -166,6 +166,32 @@ exception = context.value assert str(exception) == "Algorithm not supported" + def test_encode_with_headers_alg_none(self, jws, payload): + msg = jws.encode(payload, key=None, headers={"alg": "none"}) + with pytest.raises(DecodeError) as context: + jws.decode(msg, algorithms=["none"]) + assert str(context.value) == "Signature verification failed" + + @crypto_required + def test_encode_with_headers_alg_es256(self, jws, payload): + with open(key_path("testkey_ec.priv"), "rb") as ec_priv_file: + priv_key = load_pem_private_key(ec_priv_file.read(), password=None) + with open(key_path("testkey_ec.pub"), "rb") as ec_pub_file: + pub_key = load_pem_public_key(ec_pub_file.read()) + + msg = jws.encode(payload, priv_key, headers={"alg": "ES256"}) + assert b"hello world" == jws.decode(msg, pub_key, algorithms=["ES256"]) + + @crypto_required + def test_encode_with_alg_hs256_and_headers_alg_es256(self, jws, payload): + with open(key_path("testkey_ec.priv"), "rb") as ec_priv_file: + priv_key = load_pem_private_key(ec_priv_file.read(), password=None) + with open(key_path("testkey_ec.pub"), "rb") as ec_pub_file: + pub_key = load_pem_public_key(ec_pub_file.read()) + + msg = jws.encode(payload, priv_key, algorithm="HS256", headers={"alg": "ES256"}) + assert b"hello world" == jws.decode(msg, pub_key, algorithms=["ES256"]) + def test_decode_algorithm_param_should_be_case_sensitive(self, jws): example_jws = ( "eyJhbGciOiJoczI1NiIsInR5cCI6IkpXVCJ9" # alg = hs256 @@ -624,6 +650,65 @@ assert "testheader" in header_obj assert header_obj["testheader"] == headers["testheader"] + def test_encode_with_typ(self, jws): + payload = """ + { + "iss": "https://scim.example.com", + "iat": 1458496404, + "jti": "4d3559ec67504aaba65d40b0363faad8", + "aud": [ + "https://scim.example.com/Feeds/98d52461fa5bbc879593b7754", + "https://scim.example.com/Feeds/5d7604516b1d08641d7676ee7" + ], + "events": { + "urn:ietf:params:scim:event:create": { + "ref": + "https://scim.example.com/Users/44f6142df96bd6ab61e7521d9", + "attributes": ["id", "name", "userName", "password", "emails"] + } + } + } + """ + token = jws.encode( + payload.encode("utf-8"), "secret", headers={"typ": "secevent+jwt"} + ) + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" in header_obj + assert header_obj["typ"] == "secevent+jwt" + + def test_encode_with_typ_empty_string(self, jws, payload): + token = jws.encode(payload, "secret", headers={"typ": ""}) + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" not in header_obj + + def test_encode_with_typ_none(self, jws, payload): + token = jws.encode(payload, "secret", headers={"typ": None}) + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" not in header_obj + + def test_encode_with_typ_without_keywords(self, jws, payload): + headers = {"foo": "bar"} + token = jws.encode(payload, "secret", "HS256", headers, None) + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "foo" in header_obj + assert header_obj["foo"] == "bar" + def test_encode_fails_on_invalid_kid_types(self, jws, payload): with pytest.raises(InvalidTokenError) as exc: jws.encode(payload, "secret", headers={"kid": 123}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/test_api_jwt.py new/PyJWT-2.3.0/tests/test_api_jwt.py --- old/PyJWT-2.1.0/tests/test_api_jwt.py 2021-04-28 13:47:41.000000000 +0200 +++ new/PyJWT-2.3.0/tests/test_api_jwt.py 2021-10-16 14:23:39.000000000 +0200 @@ -1,7 +1,7 @@ import json import time from calendar import timegm -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest @@ -16,6 +16,7 @@ InvalidIssuerError, MissingRequiredClaimError, ) +from jwt.utils import base64url_decode from .utils import crypto_required, key_path, utc_timestamp @@ -167,6 +168,32 @@ lambda: jwt.encode(t, "secret", algorithms=["HS256"]), ) + def test_encode_with_typ(self, jwt): + payload = { + "iss": "https://scim.example.com", + "iat": 1458496404, + "jti": "4d3559ec67504aaba65d40b0363faad8", + "aud": [ + "https://scim.example.com/Feeds/98d52461fa5bbc879593b7754", + "https://scim.example.com/Feeds/5d7604516b1d08641d7676ee7", + ], + "events": { + "urn:ietf:params:scim:event:create": { + "ref": "https://scim.example.com/Users/44f6142df96bd6ab61e7521d9", + "attributes": ["id", "name", "userName", "password", "emails"], + } + }, + } + token = jwt.encode( + payload, "secret", algorithm="HS256", headers={"typ": "secevent+jwt"} + ) + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" in header_obj + assert header_obj["typ"] == "secevent+jwt" + def test_decode_raises_exception_if_exp_is_not_int(self, jwt): # >>> jwt.encode({'exp': 'not-an-int'}, 'secret') example_jwt = ( @@ -202,9 +229,19 @@ with pytest.raises(DecodeError): jwt.decode(example_jwt, "secret", algorithms=["HS256"]) + def test_decode_raises_exception_if_aud_is_none(self, jwt): + # >>> jwt.encode({'aud': None}, 'secret') + example_jwt = ( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + "eyJhdWQiOm51bGx9." + "-Peqc-pTugGvrc5C8Bnl0-X1V_5fv-aVb_7y7nGBVvQ" + ) + decoded = jwt.decode(example_jwt, "secret", algorithms=["HS256"]) + assert decoded["aud"] is None + def test_encode_datetime(self, jwt): secret = "secret" - current_datetime = datetime.utcnow() + current_datetime = datetime.now(tz=timezone.utc) payload = { "exp": current_datetime, "iat": current_datetime, @@ -413,6 +450,15 @@ assert exc.value.claim == "aud" + def test_raise_exception_token_with_aud_none_and_without_audience(self, jwt): + payload = {"some": "payload", "aud": None} + token = jwt.encode(payload, "secret") + + with pytest.raises(MissingRequiredClaimError) as exc: + jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) + + assert exc.value.claim == "aud" + def test_check_issuer_when_valid(self, jwt): issuer = "urn:foo" payload = {"some": "payload", "iss": "urn:foo"} @@ -442,7 +488,7 @@ def test_skip_check_exp(self, jwt): payload = { "some": "payload", - "exp": datetime.utcnow() - timedelta(days=1), + "exp": datetime.now(tz=timezone.utc) - timedelta(days=1), } token = jwt.encode(payload, "secret") jwt.decode( @@ -519,7 +565,7 @@ def test_skip_check_iat(self, jwt): payload = { "some": "payload", - "iat": datetime.utcnow() + timedelta(days=1), + "iat": datetime.now(tz=timezone.utc) + timedelta(days=1), } token = jwt.encode(payload, "secret") jwt.decode( @@ -532,7 +578,7 @@ def test_skip_check_nbf(self, jwt): payload = { "some": "payload", - "nbf": datetime.utcnow() + timedelta(days=1), + "nbf": datetime.now(tz=timezone.utc) + timedelta(days=1), } token = jwt.encode(payload, "secret") jwt.decode( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/test_jwks_client.py new/PyJWT-2.3.0/tests/test_jwks_client.py --- old/PyJWT-2.1.0/tests/test_jwks_client.py 2021-04-28 13:23:40.000000000 +0200 +++ new/PyJWT-2.3.0/tests/test_jwks_client.py 2021-10-06 12:37:16.000000000 +0200 @@ -61,6 +61,20 @@ assert len(signing_keys) == 1 assert isinstance(signing_keys[0], PyJWK) + def test_get_signing_keys_if_no_use_provided(self): + url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" + + mocked_key = RESPONSE_DATA["keys"][0].copy() + del mocked_key["use"] + response = {"keys": [mocked_key]} + + with mocked_response(response): + jwks_client = PyJWKClient(url) + signing_keys = jwks_client.get_signing_keys() + + assert len(signing_keys) == 1 + assert isinstance(signing_keys[0], PyJWK) + def test_get_signing_keys_raises_if_none_found(self): url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyJWT-2.1.0/tests/utils.py new/PyJWT-2.3.0/tests/utils.py --- old/PyJWT-2.1.0/tests/utils.py 2020-12-21 17:55:46.000000000 +0100 +++ new/PyJWT-2.3.0/tests/utils.py 2021-10-06 12:37:16.000000000 +0200 @@ -1,6 +1,6 @@ import os from calendar import timegm -from datetime import datetime +from datetime import datetime, timezone import pytest @@ -8,7 +8,7 @@ def utc_timestamp(): - return timegm(datetime.utcnow().utctimetuple()) + return timegm(datetime.now(tz=timezone.utc).utctimetuple()) def key_path(key_name):