Repository: jpadilla/pyjwt

Issue:
Support from_jwk for Ed25519Algorithm
I'd like to use `get_signing_key_from_jwk()`  for JWT with alg=EdDSA.

But, at the moment, it seems that pyjwt does not support it because `Ed25519Algorithm` does not have `from_jwk()`.

Do you have a plan to support that?
If you don't have the plan, I can send a PR for that.

I'd appreciate it if you can tell me the plan to support EdDSA-based token.


Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c3850a5..5c46c97 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -23,6 +23,7 @@ Added
 - Add caching by default to PyJWKClient `#611 <https://github.com/jpadilla/pyjwt/pull/611>`__
 - Add missing exceptions.InvalidKeyError to jwt module __init__ imports `#620 <https://github.com/jpadilla/pyjwt/pull/620>`__
 - Add support for ES256K algorithm `#629 <https://github.com/jpadilla/pyjwt/pull/629>`__
+- Add `from_jwk()` to Ed25519Algorithm `#621 <https://github.com/jpadilla/pyjwt/pull/621>`__
 
 `v2.0.1 <https://github.com/jpadilla/pyjwt/compare/2.0.0...2.0.1>`__
 --------------------------------------------------------------------
diff --git a/jwt/algorithms.py b/jwt/algorithms.py
index 7784a94..50719be 100644
--- a/jwt/algorithms.py
+++ b/jwt/algorithms.py
@@ -586,3 +586,34 @@ if has_crypto:
                 return True  # If no exception was raised, the signature is valid.
             except cryptography.exceptions.InvalidSignature:
                 return False
+
+        @staticmethod
+        def from_jwk(jwk):
+            try:
+                if isinstance(jwk, str):
+                    obj = json.loads(jwk)
+                elif isinstance(jwk, dict):
+                    obj = jwk
+                else:
+                    raise ValueError
+            except ValueError:
+                raise InvalidKeyError("Key is not valid JSON")
+
+            if obj.get("kty") != "OKP":
+                raise InvalidKeyError("Not an Octet Key Pair")
+
+            curve = obj.get("crv")
+            if curve != "Ed25519":
+                raise InvalidKeyError(f"Invalid curve: {curve}")
+
+            if "x" not in obj:
+                raise InvalidKeyError('OKP should have "x" parameter')
+            x = base64url_decode(obj.get("x"))
+
+            try:
+                if "d" not in obj:
+                    return Ed25519PublicKey.from_public_bytes(x)
+                d = base64url_decode(obj.get("d"))
+                return Ed25519PrivateKey.from_private_bytes(d)
+            except ValueError as err:
+                raise InvalidKeyError("Invalid key parameter") from err


Test patch:
diff --git a/tests/keys/jwk_okp_key_Ed25519.json b/tests/keys/jwk_okp_key_Ed25519.json
new file mode 100644
index 0000000..6451fe9
--- /dev/null
+++ b/tests/keys/jwk_okp_key_Ed25519.json
@@ -0,0 +1,6 @@
+{
+  "kty":"OKP",
+  "crv":"Ed25519",
+  "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
+  "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+}
diff --git a/tests/keys/jwk_okp_pub_Ed25519.json b/tests/keys/jwk_okp_pub_Ed25519.json
new file mode 100644
index 0000000..4ffdb5f
--- /dev/null
+++ b/tests/keys/jwk_okp_pub_Ed25519.json
@@ -0,0 +1,5 @@
+{
+  "kty":"OKP",
+  "crv":"Ed25519",
+  "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+}
diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py
index 2341c1c..2144d48 100644
--- a/tests/test_algorithms.py
+++ b/tests/test_algorithms.py
@@ -733,3 +733,70 @@ class TestEd25519Algorithms:
             jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
 
         assert jwt_pub_key_first == jwt_pub_key_second
+
+    def test_ed25519_jwk_private_key_should_parse_and_verify(self):
+        algo = Ed25519Algorithm()
+
+        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.public_key(), signature)
+
+    def test_ed25519_jwk_public_key_should_parse_and_verify(self):
+        algo = Ed25519Algorithm()
+
+        with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile:
+            priv_key = algo.from_jwk(keyfile.read())
+
+        with open(key_path("jwk_okp_pub_Ed25519.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_ed25519_jwk_fails_on_invalid_json(self):
+        algo = Ed25519Algorithm()
+
+        with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile:
+            valid_pub = json.loads(keyfile.read())
+        with open(key_path("jwk_okp_key_Ed25519.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 "Ed25519"
+        v = valid_pub.copy()
+        v["crv"] = "P-256"
+        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)