diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e67381fb..be10c2ba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Added - Add missing exceptions.InvalidKeyError to jwt module __init__ imports `#620 `__ - Add support for ES256K algorithm `#629 `__ - Add `from_jwk()` to Ed25519Algorithm `#621 `__ +- Add `to_jwk()` to Ed25519Algorithm `#643 `__ `v2.0.1 `__ -------------------------------------------------------------------- diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 50719bea..42648ce3 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -37,6 +37,10 @@ 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, @@ -587,6 +591,45 @@ def verify(self, msg, key, sig): 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: diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 2144d484..0ece6d88 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -800,3 +800,28 @@ def test_ed25519_jwk_fails_on_invalid_json(self): 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"})