Repository: jpadilla/pyjwt
Pull request #952 (2024-04-05 22:22:35)

Handle load_pem_public_key ValueError
This is a stab at addressing the issue discussed here: [Consider cryptography 42.x.x new validation #948](https://github.com/jpadilla/pyjwt/issues/948)

All I did is raise a `InvalidKeyError` when there is a ValueError and `return cast(RSAPublicKey, load_pem_public_key(key_bytes))` fails.

fix https://github.com/jpadilla/pyjwt/issues/948


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index b6a538a..aa29add 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -54,9 +54,11 @@ jobs:
 
       - name: "Upload coverage to Codecov"
         if: "contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest'"
-        uses: "codecov/codecov-action@v3"
+        uses: "codecov/codecov-action@v4"
         with:
           fail_ci_if_error: true
+          token: ${{ secrets.CODECOV_TOKEN }}
+          verbose: true
 
   package:
     name: "Build & verify package"
diff --git a/.github/workflows/pypi-package.yml b/.github/workflows/pypi-package.yml
index 1b97c54..7d45039 100644
--- a/.github/workflows/pypi-package.yml
+++ b/.github/workflows/pypi-package.yml
@@ -40,7 +40,7 @@ jobs:
 
     steps:
       - name: Download packages built by build-and-inspect-python-package
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: Packages
           path: dist
@@ -60,7 +60,7 @@ jobs:
 
     steps:
       - name: Download packages built by build-and-inspect-python-package
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: Packages
           path: dist
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ac45a70..bcc2ffc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 23.11.0
+    rev: 24.4.2
     hooks:
       - id: black
         args: ["--target-version=py38"]
@@ -12,18 +12,18 @@ repos:
         args: ["--target-version=py38"]
 
   - repo: https://github.com/PyCQA/flake8
-    rev: 6.1.0
+    rev: 7.0.0
     hooks:
       - id: flake8
         language_version: python3.9
 
   - repo: https://github.com/PyCQA/isort
-    rev: 5.12.0
+    rev: 5.13.2
     hooks:
       - id: isort
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
+    rev: v4.6.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
@@ -36,7 +36,7 @@ repos:
         args: [--no-build-isolation]
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: "v1.7.0"
+    rev: "v1.10.0"
     hooks:
       - id: mypy
         additional_dependencies: [cryptography>=3.4.0]
diff --git a/docs/usage.rst b/docs/usage.rst
index 9a673c3..43de2c5 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -284,15 +284,13 @@ Retrieve RSA signing keys from a JWKS endpoint
     >>> import jwt
     >>> from jwt import PyJWKClient
     >>> token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA"
-    >>> kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw"
     >>> url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
     >>> optional_custom_headers = {"User-agent": "custom-user-agent"}
     >>> jwks_client = PyJWKClient(url, headers=optional_custom_headers)
     >>> signing_key = jwks_client.get_signing_key_from_jwt(token)
     >>> data = jwt.decode(
     ...     token,
-    ...     signing_key.key,
-    ...     algorithms=["RS256"],
+    ...     signing_key,
     ...     audience="https://expenses-api",
     ...     options={"verify_exp": False},
     ... )
@@ -356,8 +354,7 @@ is not built into pyjwt.
     # now, decode_complete to get payload + header
     data = jwt.decode_complete(
         id_token,
-        key=signing_key.key,
-        algorithms=signing_algos,
+        key=signing_key,
         audience=client_id,
     )
     payload, header = data["payload"], data["header"]
diff --git a/jwt/algorithms.py b/jwt/algorithms.py
index ece86b4..9be50b2 100644
--- a/jwt/algorithms.py
+++ b/jwt/algorithms.py
@@ -21,7 +21,7 @@ from .utils import (
 )
 
 try:
-    from cryptography.exceptions import InvalidSignature
+    from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
     from cryptography.hazmat.backends import default_backend
     from cryptography.hazmat.primitives import hashes
     from cryptography.hazmat.primitives.asymmetric import padding
@@ -187,14 +187,12 @@ class Algorithm(ABC):
     @overload
     @staticmethod
     @abstractmethod
-    def to_jwk(key_obj, as_dict: Literal[True]) -> JWKDict:
-        ...  # pragma: no cover
+    def to_jwk(key_obj, as_dict: Literal[True]) -> JWKDict: ...  # pragma: no cover
 
     @overload
     @staticmethod
     @abstractmethod
-    def to_jwk(key_obj, as_dict: Literal[False] = False) -> str:
-        ...  # pragma: no cover
+    def to_jwk(key_obj, as_dict: Literal[False] = False) -> str: ...  # pragma: no cover
 
     @staticmethod
     @abstractmethod
@@ -267,13 +265,15 @@ class HMACAlgorithm(Algorithm):
 
     @overload
     @staticmethod
-    def to_jwk(key_obj: str | bytes, as_dict: Literal[True]) -> JWKDict:
-        ...  # pragma: no cover
+    def to_jwk(
+        key_obj: str | bytes, as_dict: Literal[True]
+    ) -> JWKDict: ...  # pragma: no cover
 
     @overload
     @staticmethod
-    def to_jwk(key_obj: str | bytes, as_dict: Literal[False] = False) -> str:
-        ...  # pragma: no cover
+    def to_jwk(
+        key_obj: str | bytes, as_dict: Literal[False] = False
+    ) -> str: ...  # pragma: no cover
 
     @staticmethod
     def to_jwk(key_obj: str | bytes, as_dict: bool = False) -> JWKDict | str:
@@ -343,17 +343,22 @@ if has_crypto:
                         RSAPrivateKey, load_pem_private_key(key_bytes, password=None)
                     )
             except ValueError:
-                return cast(RSAPublicKey, load_pem_public_key(key_bytes))
+                try:
+                    return cast(RSAPublicKey, load_pem_public_key(key_bytes))
+                except (ValueError, UnsupportedAlgorithm):
+                    raise InvalidKeyError("Could not parse the provided public key.")
 
         @overload
         @staticmethod
-        def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[True]) -> JWKDict:
-            ...  # pragma: no cover
+        def to_jwk(
+            key_obj: AllowedRSAKeys, as_dict: Literal[True]
+        ) -> JWKDict: ...  # pragma: no cover
 
         @overload
         @staticmethod
-        def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[False] = False) -> str:
-            ...  # pragma: no cover
+        def to_jwk(
+            key_obj: AllowedRSAKeys, as_dict: Literal[False] = False
+        ) -> str: ...  # pragma: no cover
 
         @staticmethod
         def to_jwk(key_obj: AllowedRSAKeys, as_dict: bool = False) -> JWKDict | str:
@@ -543,13 +548,15 @@ if has_crypto:
 
         @overload
         @staticmethod
-        def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[True]) -> JWKDict:
-            ...  # pragma: no cover
+        def to_jwk(
+            key_obj: AllowedECKeys, as_dict: Literal[True]
+        ) -> JWKDict: ...  # pragma: no cover
 
         @overload
         @staticmethod
-        def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[False] = False) -> str:
-            ...  # pragma: no cover
+        def to_jwk(
+            key_obj: AllowedECKeys, as_dict: Literal[False] = False
+        ) -> str: ...  # pragma: no cover
 
         @staticmethod
         def to_jwk(key_obj: AllowedECKeys, as_dict: bool = False) -> JWKDict | str:
@@ -760,13 +767,15 @@ if has_crypto:
 
         @overload
         @staticmethod
-        def to_jwk(key: AllowedOKPKeys, as_dict: Literal[True]) -> JWKDict:
-            ...  # pragma: no cover
+        def to_jwk(
+            key: AllowedOKPKeys, as_dict: Literal[True]
+        ) -> JWKDict: ...  # pragma: no cover
 
         @overload
         @staticmethod
-        def to_jwk(key: AllowedOKPKeys, as_dict: Literal[False] = False) -> str:
-            ...  # pragma: no cover
+        def to_jwk(
+            key: AllowedOKPKeys, as_dict: Literal[False] = False
+        ) -> str: ...  # pragma: no cover
 
         @staticmethod
         def to_jwk(key: AllowedOKPKeys, as_dict: bool = False) -> JWKDict | str:
diff --git a/jwt/api_jwk.py b/jwt/api_jwk.py
index af74578..d35b955 100644
--- a/jwt/api_jwk.py
+++ b/jwt/api_jwk.py
@@ -52,9 +52,11 @@ class PyJWK:
         if not has_crypto and algorithm in requires_cryptography:
             raise PyJWKError(f"{algorithm} requires 'cryptography' to be installed.")
 
-        self.Algorithm = self._algorithms.get(algorithm)
+        self.algorithm_name = algorithm
 
-        if not self.Algorithm:
+        if algorithm in self._algorithms:
+            self.Algorithm = self._algorithms[algorithm]
+        else:
             raise PyJWKError(f"Unable to find an algorithm for key: {self._jwk_data}")
 
         self.key = self.Algorithm.from_jwk(self._jwk_data)
diff --git a/jwt/api_jws.py b/jwt/api_jws.py
index fa6708c..5822ebf 100644
--- a/jwt/api_jws.py
+++ b/jwt/api_jws.py
@@ -11,6 +11,7 @@ from .algorithms import (
     has_crypto,
     requires_cryptography,
 )
+from .api_jwk import PyJWK
 from .exceptions import (
     DecodeError,
     InvalidAlgorithmError,
@@ -172,7 +173,7 @@ class PyJWS:
     def decode_complete(
         self,
         jwt: str | bytes,
-        key: AllowedPublicKeys | str | bytes = "",
+        key: AllowedPublicKeys | PyJWK | str | bytes = "",
         algorithms: list[str] | None = None,
         options: dict[str, Any] | None = None,
         detached_payload: bytes | None = None,
@@ -190,7 +191,7 @@ class PyJWS:
         merged_options = {**self.options, **options}
         verify_signature = merged_options["verify_signature"]
 
-        if verify_signature and not algorithms:
+        if verify_signature and not algorithms and not isinstance(key, PyJWK):
             raise DecodeError(
                 'It is required that you pass in a value for the "algorithms" argument when calling decode().'
             )
@@ -217,7 +218,7 @@ class PyJWS:
     def decode(
         self,
         jwt: str | bytes,
-        key: AllowedPublicKeys | str | bytes = "",
+        key: AllowedPublicKeys | PyJWK | str | bytes = "",
         algorithms: list[str] | None = None,
         options: dict[str, Any] | None = None,
         detached_payload: bytes | None = None,
@@ -289,9 +290,11 @@ class PyJWS:
         signing_input: bytes,
         header: dict[str, Any],
         signature: bytes,
-        key: AllowedPublicKeys | str | bytes = "",
+        key: AllowedPublicKeys | PyJWK | str | bytes = "",
         algorithms: list[str] | None = None,
     ) -> None:
+        if algorithms is None and isinstance(key, PyJWK):
+            algorithms = [key.algorithm_name]
         try:
             alg = header["alg"]
         except KeyError:
@@ -300,11 +303,15 @@ class PyJWS:
         if not alg or (algorithms is not None and alg not in algorithms):
             raise InvalidAlgorithmError("The specified alg value is not allowed")
 
-        try:
-            alg_obj = self.get_algorithm_by_name(alg)
-        except NotImplementedError as e:
-            raise InvalidAlgorithmError("Algorithm not supported") from e
-        prepared_key = alg_obj.prepare_key(key)
+        if isinstance(key, PyJWK):
+            alg_obj = key.Algorithm
+            prepared_key = key.key
+        else:
+            try:
+                alg_obj = self.get_algorithm_by_name(alg)
+            except NotImplementedError as e:
+                raise InvalidAlgorithmError("Algorithm not supported") from e
+            prepared_key = alg_obj.prepare_key(key)
 
         if not alg_obj.verify(signing_input, prepared_key, signature):
             raise InvalidSignatureError("Signature verification failed")
diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py
index 87883bc..7a07c33 100644
--- a/jwt/api_jwt.py
+++ b/jwt/api_jwt.py
@@ -21,6 +21,7 @@ from .warnings import RemovedInPyjwt3Warning
 
 if TYPE_CHECKING:
     from .algorithms import AllowedPrivateKeys, AllowedPublicKeys
+    from .api_jwk import PyJWK
 
 
 class PyJWT:
@@ -100,7 +101,7 @@ class PyJWT:
     def decode_complete(
         self,
         jwt: str | bytes,
-        key: AllowedPublicKeys | str | bytes = "",
+        key: AllowedPublicKeys | PyJWK | str | bytes = "",
         algorithms: list[str] | None = None,
         options: dict[str, Any] | None = None,
         # deprecated arg, remove in pyjwt3
@@ -185,7 +186,7 @@ class PyJWT:
     def decode(
         self,
         jwt: str | bytes,
-        key: AllowedPublicKeys | str | bytes = "",
+        key: AllowedPublicKeys | PyJWK | str | bytes = "",
         algorithms: list[str] | None = None,
         options: dict[str, Any] | None = None,
         # deprecated arg, remove in pyjwt3
diff --git a/jwt/utils.py b/jwt/utils.py
index 81c5ee4..d469139 100644
--- a/jwt/utils.py
+++ b/jwt/utils.py
@@ -131,26 +131,15 @@ def is_pem_format(key: bytes) -> bool:
 
 
 # Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46
-_CERT_SUFFIX = b"-cert-v01@openssh.com"
-_SSH_PUBKEY_RC = re.compile(rb"\A(\S+)[ \t]+(\S+)")
-_SSH_KEY_FORMATS = [
+_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
+    return key.startswith(_SSH_KEY_FORMATS)


Test patch:
diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py
index 1a39552..337de96 100644
--- a/tests/test_algorithms.py
+++ b/tests/test_algorithms.py
@@ -1100,3 +1100,14 @@ class TestOKPAlgorithms:
         algo = HMACAlgorithm(HMACAlgorithm.SHA256)
         computed_hash = algo.compute_hash_digest(b"foo")
         assert computed_hash == foo_hash
+
+    @crypto_required
+    def test_rsa_prepare_key_raises_invalid_key_error_on_invalid_pem(self):
+        algo = RSAAlgorithm(RSAAlgorithm.SHA256)
+        invalid_key = "invalid key"
+
+        with pytest.raises(InvalidKeyError) as excinfo:
+            algo.prepare_key(invalid_key)
+
+        # Check that the exception message is correct
+        assert "Could not parse the provided public key." in str(excinfo.value)
diff --git a/tests/test_api_jwk.py b/tests/test_api_jwk.py
index 4754db6..326e012 100644
--- a/tests/test_api_jwk.py
+++ b/tests/test_api_jwk.py
@@ -65,6 +65,7 @@ class TestPyJWK:
         assert jwk.key_type == "RSA"
         assert isinstance(jwk.Algorithm, RSAAlgorithm)
         assert jwk.Algorithm.hash_alg == RSAAlgorithm.SHA256
+        assert jwk.algorithm_name == "RS256"
 
     @crypto_required
     def test_should_load_key_from_dict_with_algorithm(self):
@@ -76,6 +77,7 @@ class TestPyJWK:
         assert jwk.key_type == "RSA"
         assert isinstance(jwk.Algorithm, RSAAlgorithm)
         assert jwk.Algorithm.hash_alg == RSAAlgorithm.SHA256
+        assert jwk.algorithm_name == "RS256"
 
     @crypto_required
     def test_should_load_key_ec_p256_from_dict(self):
@@ -87,6 +89,7 @@ class TestPyJWK:
         assert jwk.key_type == "EC"
         assert isinstance(jwk.Algorithm, ECAlgorithm)
         assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA256
+        assert jwk.algorithm_name == "ES256"
 
     @crypto_required
     def test_should_load_key_ec_p384_from_dict(self):
@@ -98,6 +101,7 @@ class TestPyJWK:
         assert jwk.key_type == "EC"
         assert isinstance(jwk.Algorithm, ECAlgorithm)
         assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA384
+        assert jwk.algorithm_name == "ES384"
 
     @crypto_required
     def test_should_load_key_ec_p521_from_dict(self):
@@ -109,6 +113,7 @@ class TestPyJWK:
         assert jwk.key_type == "EC"
         assert isinstance(jwk.Algorithm, ECAlgorithm)
         assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA512
+        assert jwk.algorithm_name == "ES512"
 
     @crypto_required
     def test_should_load_key_ec_secp256k1_from_dict(self):
@@ -120,6 +125,7 @@ class TestPyJWK:
         assert jwk.key_type == "EC"
         assert isinstance(jwk.Algorithm, ECAlgorithm)
         assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA256
+        assert jwk.algorithm_name == "ES256K"
 
     @crypto_required
     def test_should_load_key_hmac_from_dict(self):
@@ -131,6 +137,7 @@ class TestPyJWK:
         assert jwk.key_type == "oct"
         assert isinstance(jwk.Algorithm, HMACAlgorithm)
         assert jwk.Algorithm.hash_alg == HMACAlgorithm.SHA256
+        assert jwk.algorithm_name == "HS256"
 
     @crypto_required
     def test_should_load_key_hmac_without_alg_from_dict(self):
@@ -143,6 +150,7 @@ class TestPyJWK:
         assert jwk.key_type == "oct"
         assert isinstance(jwk.Algorithm, HMACAlgorithm)
         assert jwk.Algorithm.hash_alg == HMACAlgorithm.SHA256
+        assert jwk.algorithm_name == "HS256"
 
     @crypto_required
     def test_should_load_key_okp_without_alg_from_dict(self):
@@ -153,6 +161,7 @@ class TestPyJWK:
 
         assert jwk.key_type == "OKP"
         assert isinstance(jwk.Algorithm, OKPAlgorithm)
+        assert jwk.algorithm_name == "EdDSA"
 
     @crypto_required
     def test_from_dict_should_throw_exception_if_arg_is_invalid(self):
diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py
index 3385716..c764f09 100644
--- a/tests/test_api_jws.py
+++ b/tests/test_api_jws.py
@@ -4,6 +4,7 @@ from decimal import Decimal
 import pytest
 
 from jwt.algorithms import NoneAlgorithm, has_crypto
+from jwt.api_jwk import PyJWK
 from jwt.api_jws import PyJWS
 from jwt.exceptions import (
     DecodeError,
@@ -250,6 +251,59 @@ class TestJWS:
             ),
         }
 
+    def test_decodes_with_jwk(self, jws, payload):
+        jwk = PyJWK(
+            {
+                "kty": "oct",
+                "alg": "HS256",
+                "k": "c2VjcmV0",  # "secret"
+            }
+        )
+        example_jws = (
+            b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
+            b"aGVsbG8gd29ybGQ."
+            b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM"
+        )
+
+        decoded_payload = jws.decode(example_jws, jwk, algorithms=["HS256"])
+
+        assert decoded_payload == payload
+
+    def test_decodes_with_jwk_and_no_algorithm(self, jws, payload):
+        jwk = PyJWK(
+            {
+                "kty": "oct",
+                "alg": "HS256",
+                "k": "c2VjcmV0",  # "secret"
+            }
+        )
+        example_jws = (
+            b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
+            b"aGVsbG8gd29ybGQ."
+            b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM"
+        )
+
+        decoded_payload = jws.decode(example_jws, jwk)
+
+        assert decoded_payload == payload
+
+    def test_decodes_with_jwk_and_mismatched_algorithm(self, jws, payload):
+        jwk = PyJWK(
+            {
+                "kty": "oct",
+                "alg": "HS512",
+                "k": "c2VjcmV0",  # "secret"
+            }
+        )
+        example_jws = (
+            b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
+            b"aGVsbG8gd29ybGQ."
+            b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM"
+        )
+
+        with pytest.raises(InvalidAlgorithmError):
+            jws.decode(example_jws, jwk)
+
     # 'Control' Elliptic Curve jws created by another library.
     # Used to test for regressions that could affect both
     # encoding / decoding operations equally (causing tests
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 122dcb4..d8d0d6c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,6 @@
 import pytest
 
-from jwt.utils import force_bytes, from_base64url_uint, to_base64url_uint
+from jwt.utils import force_bytes, from_base64url_uint, is_ssh_key, to_base64url_uint
 
 
 @pytest.mark.parametrize(
@@ -37,3 +37,19 @@ def test_from_base64url_uint(inputval, expected):
 def test_force_bytes_raises_error_on_invalid_object():
     with pytest.raises(TypeError):
         force_bytes({})  # type: ignore[arg-type]
+
+
+@pytest.mark.parametrize(
+    "key_format",
+    (
+        b"ssh-ed25519",
+        b"ssh-rsa",
+        b"ssh-dss",
+        b"ecdsa-sha2-nistp256",
+        b"ecdsa-sha2-nistp384",
+        b"ecdsa-sha2-nistp521",
+    ),
+)
+def test_is_ssh_key(key_format):
+    assert is_ssh_key(key_format + b" any") is True
+    assert is_ssh_key(b"not a ssh key") is False