Repository: jpadilla/pyjwt

Issue:
Support to_jwk for Ed25519Algorithm
after [Support of form_jwk()](https://github.com/jpadilla/pyjwt/issues/621) it would be nice to implement
to_jwk() for Ed25519Algorithm



Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e67381f..624b676 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -18,6 +18,7 @@ Fixed
 - Remove padding from JWK test data. `#628 <https://github.com/jpadilla/pyjwt/pull/628>`__
 - Make `kty` mandatory in JWK to be compliant with RFC7517. `#624 <https://github.com/jpadilla/pyjwt/pull/624>`__
 - Allow JWK without `alg` to be compliant with RFC7517. `#624 <https://github.com/jpadilla/pyjwt/pull/624>`__
+- Allow to verify with private key on ECAlgorithm, as well as on Ed25519Algorithm. `#645 <https://github.com/jpadilla/pyjwt/pull/645>`__
 
 Added
 ~~~~~
@@ -26,6 +27,7 @@ Added
 - 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>`__
+- Add `to_jwk()` to Ed25519Algorithm `#643 <https://github.com/jpadilla/pyjwt/pull/643>`__
 
 `v2.0.1 <https://github.com/jpadilla/pyjwt/compare/2.0.0...2.0.1>`__
 --------------------------------------------------------------------
diff --git a/docs/algorithms.rst b/docs/algorithms.rst
index 10402f0..7192252 100644
--- a/docs/algorithms.rst
+++ b/docs/algorithms.rst
@@ -57,3 +57,15 @@ of allowed algorithms:
 
 In the above case, if the JWT has any value for its alg header other than
 HS512 or HS256, the claim will be rejected with an ``InvalidAlgorithmError``.
+
+.. 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\*).
diff --git a/docs/api.rst b/docs/api.rst
index 266082a..2553359 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -28,9 +28,18 @@ API Reference
 
     :param list algorithms: allowed algorithms, e.g. ``["ES256"]``
 
-        .. note:: It is highly recommended to specify the expected ``algorithms``.
-
-        .. note:: It is insecure to mix symmetric and asymmetric algorithms because they require different kinds of keys.
+        .. 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
 
diff --git a/docs/usage.rst b/docs/usage.rst
index e578764..c5372af 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -262,7 +262,7 @@ Issued At Claim (iat)
 Requiring Presence of Claims
 ----------------------------
 
-If you wish to require one or more claims to be present in the claimset, you can set the ``require`` paramenter to include these claims.
+If you wish to require one or more claims to be present in the claimset, you can set the ``require`` parameter to include these claims.
 
 .. code-block:: pycon
 
diff --git a/jwt/algorithms.py b/jwt/algorithms.py
index 50719be..cee66a4 100644
--- a/jwt/algorithms.py
+++ b/jwt/algorithms.py
@@ -37,6 +37,10 @@ try:
         rsa_recover_prime_factors,
     )
     from cryptography.hazmat.primitives.serialization import (
+        Encoding,
+        NoEncryption,
+        PrivateFormat,
+        PublicFormat,
         load_pem_private_key,
         load_pem_public_key,
         load_ssh_public_key,
@@ -427,6 +431,8 @@ if has_crypto:
                 return False
 
             try:
+                if isinstance(key, EllipticCurvePrivateKey):
+                    key = key.public_key()
                 key.verify(der_sig, msg, ec.ECDSA(self.hash_alg()))
                 return True
             except InvalidSignature:
@@ -587,6 +593,45 @@ if has_crypto:
             except cryptography.exceptions.InvalidSignature:
                 return False
 
+        @staticmethod
+        def to_jwk(key):
+            if isinstance(key, Ed25519PublicKey):
+                x = key.public_bytes(
+                    encoding=Encoding.Raw,
+                    format=PublicFormat.Raw,
+                )
+
+                return json.dumps(
+                    {
+                        "x": base64url_encode(force_bytes(x)).decode(),
+                        "kty": "OKP",
+                        "crv": "Ed25519",
+                    }
+                )
+
+            if isinstance(key, Ed25519PrivateKey):
+                d = key.private_bytes(
+                    encoding=Encoding.Raw,
+                    format=PrivateFormat.Raw,
+                    encryption_algorithm=NoEncryption(),
+                )
+
+                x = key.public_key().public_bytes(
+                    encoding=Encoding.Raw,
+                    format=PublicFormat.Raw,
+                )
+
+                return json.dumps(
+                    {
+                        "x": base64url_encode(force_bytes(x)).decode(),
+                        "d": base64url_encode(force_bytes(d)).decode(),
+                        "kty": "OKP",
+                        "crv": "Ed25519",
+                    }
+                )
+
+            raise InvalidKeyError("Not a public or private key")
+
         @staticmethod
         def from_jwk(jwk):
             try:


Test patch:
diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py
index 2144d48..417f91d 100644
--- a/tests/test_algorithms.py
+++ b/tests/test_algorithms.py
@@ -658,6 +658,13 @@ class TestAlgorithmsRFC7520:
         result = algo.verify(signing_input, key, signature)
         assert result
 
+        # private key can also be used.
+        with open(key_path("jwk_ec_key_P-521.json")) as keyfile:
+            private_key = algo.from_jwk(keyfile.read())
+
+        result = algo.verify(signing_input, private_key, signature)
+        assert result
+
 
 @crypto_required
 class TestEd25519Algorithms:
@@ -800,3 +807,28 @@ class TestEd25519Algorithms:
         v["d"] = "123"
         with pytest.raises(InvalidKeyError):
             algo.from_jwk(v)
+
+    def test_ed25519_to_jwk_works_with_from_jwk(self):
+        algo = Ed25519Algorithm()
+
+        with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile:
+            priv_key_1 = algo.from_jwk(keyfile.read())
+
+        with open(key_path("jwk_okp_pub_Ed25519.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)
+
+    def test_ed25519_to_jwk_raises_exception_on_invalid_key(self):
+        algo = Ed25519Algorithm()
+
+        with pytest.raises(InvalidKeyError):
+            algo.to_jwk({"not": "a valid key"})