From 3cd9ceec33ced359decbad75b413ad668ae6332c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@users.noreply.github.com>
Date: Fri, 11 Sep 2026 22:29:12 -0400
Subject: [PATCH] fix: reject inconsistent OKP private keys

---
 jwt/algorithms.py        | 14 ++++++++++++--
 tests/test_algorithms.py | 29 +++++++++++++++++++++++++++++
 2 files changed, 41 insertions(+), 2 deletions(-)

Index: pyjwt-2.12.1/jwt/algorithms.py
===================================================================
--- pyjwt-2.12.1.orig/jwt/algorithms.py
+++ pyjwt-2.12.1/jwt/algorithms.py
@@ -1039,8 +1039,18 @@ if has_crypto:
                         return Ed25519PublicKey.from_public_bytes(x)
                     return Ed448PublicKey.from_public_bytes(x)
                 d = base64url_decode(obj.get("d"))
+                private_key: Ed25519PrivateKey | Ed448PrivateKey
                 if curve == "Ed25519":
-                    return Ed25519PrivateKey.from_private_bytes(d)
-                return Ed448PrivateKey.from_private_bytes(d)
+                    private_key = Ed25519PrivateKey.from_private_bytes(d)
+                else:
+                    private_key = Ed448PrivateKey.from_private_bytes(d)
+                if (
+                    private_key.public_key().public_bytes(
+                        encoding=Encoding.Raw, format=PublicFormat.Raw
+                    )
+                    != x
+                ):
+                    raise InvalidKeyError("Public key does not match private key")
+                return private_key
             except ValueError as err:
                 raise InvalidKeyError("Invalid key parameter") from err
Index: pyjwt-2.12.1/tests/test_algorithms.py
===================================================================
--- pyjwt-2.12.1.orig/tests/test_algorithms.py
+++ pyjwt-2.12.1/tests/test_algorithms.py
@@ -942,6 +942,35 @@ class TestAlgorithmsRFC7520:
 
 @crypto_required
 class TestOKPAlgorithms:
+    @pytest.mark.parametrize("curve", ("Ed25519", "Ed448"))
+    @pytest.mark.parametrize("as_dict", (False, True))
+    def test_okp_jwk_should_reject_inconsistent_private_key(
+        self, curve: str, as_dict: bool
+    ) -> None:
+        with open(key_path(f"jwk_okp_key_{curve}.json")) as keyfile:
+            jwk = json.load(keyfile)
+
+        d = base64url_decode(jwk["d"])
+        jwk["d"] = (
+            base64.urlsafe_b64encode(bytes([d[0] ^ 1]) + d[1:]).rstrip(b"=").decode()
+        )
+
+        with pytest.raises(InvalidKeyError):
+            OKPAlgorithm.from_jwk(jwk if as_dict else json.dumps(jwk))
+
+    @pytest.mark.parametrize("curve", ("Ed25519", "Ed448"))
+    @pytest.mark.parametrize("length", (0, 1, 56, 58))
+    def test_okp_jwk_should_reject_private_key_with_invalid_public_length(
+        self, curve: str, length: int
+    ) -> None:
+        with open(key_path(f"jwk_okp_key_{curve}.json")) as keyfile:
+            jwk = json.load(keyfile)
+
+        jwk["x"] = base64.urlsafe_b64encode(b"\x00" * length).rstrip(b"=").decode()
+
+        with pytest.raises(InvalidKeyError):
+            OKPAlgorithm.from_jwk(jwk)
+
     hello_world_sig = b"Qxa47mk/azzUgmY2StAOguAd4P7YBLpyCfU3JdbaiWnXM4o4WibXwmIHvNYgN3frtE2fcyd8OYEaOiD/KiwkCg=="
     hello_world_sig_pem = b"9ueQE7PT8uudHIQb2zZZ7tB7k1X3jeTnIfOVvGCINZejrqQbru1EXPeuMlGcQEZrGkLVcfMmr99W/+byxfppAg=="
     hello_world = b"Hello World!"
