diff options
author | Ajitomi, Daisuke <ajitomi@gmail.com> | 2021-10-03 14:23:56 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-03 11:23:56 +0600 |
commit | e7a6c022f3f2e5ba329cbadd242c788014926a7e (patch) | |
tree | e2856bc61e1ace6ba39cd1d1173dc4b60cbd19fb | |
parent | 19ce9c5ec7947428d35aaffd302eb2629210a697 (diff) | |
download | pyjwt-e7a6c022f3f2e5ba329cbadd242c788014926a7e.tar.gz |
Add support for Ed448/EdDSA. (#675)
* Add support for Ed448/EdDSA.
* Add test for verification using EdDSA private key.
-rw-r--r-- | CHANGELOG.rst | 2 | ||||
-rw-r--r-- | docs/algorithms.rst | 2 | ||||
-rw-r--r-- | jwt/algorithms.py | 50 | ||||
-rw-r--r-- | tests/keys/jwk_okp_key_Ed448.json | 9 | ||||
-rw-r--r-- | tests/keys/jwk_okp_pub_Ed448.json | 8 | ||||
-rw-r--r-- | tests/test_algorithms.py | 173 | ||||
-rw-r--r-- | tests/test_api_jwk.py | 9 |
7 files changed, 198 insertions, 55 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8630a27..87bafcb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,8 @@ Fixed Added ~~~~~ +- Add support for Ed448/EdDSA. `#675 <https://github.com/jpadilla/pyjwt/pull/675>`__ + `v2.1.0 <https://github.com/jpadilla/pyjwt/compare/2.0.1...2.1.0>`__ -------------------------------------------------------------------- diff --git a/docs/algorithms.rst b/docs/algorithms.rst index 7192252..0fe0761 100644 --- a/docs/algorithms.rst +++ b/docs/algorithms.rst @@ -17,7 +17,7 @@ This library currently supports: * PS256 - RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256 * PS384 - RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384 * PS512 - RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512 -* EdDSA - Ed25519 signature using SHA-512. Provides 128-bit security +* EdDSA - Both Ed25519 signature using SHA-512 and Ed448 signature using SHA-3 are supported. Ed25519 and Ed448 provide 128-bit and 224-bit security respectively. Asymmetric (Public-key) Algorithms ---------------------------------- diff --git a/jwt/algorithms.py b/jwt/algorithms.py index cee66a4..2f73835 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -22,6 +22,10 @@ try: EllipticCurvePrivateKey, EllipticCurvePublicKey, ) + from cryptography.hazmat.primitives.asymmetric.ed448 import ( + Ed448PrivateKey, + Ed448PublicKey, + ) from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, @@ -93,7 +97,7 @@ def get_default_algorithms(): "PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256), "PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384), "PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512), - "EdDSA": Ed25519Algorithm(), + "EdDSA": OKPAlgorithm(), } ) @@ -534,9 +538,9 @@ if has_crypto: except InvalidSignature: return False - class Ed25519Algorithm(Algorithm): + class OKPAlgorithm(Algorithm): """ - Performs signing and verification operations using Ed25519 + Performs signing and verification operations using EdDSA This class requires ``cryptography>=2.6`` to be installed. """ @@ -546,7 +550,10 @@ if has_crypto: def prepare_key(self, key): - if isinstance(key, (Ed25519PrivateKey, Ed25519PublicKey)): + if isinstance( + key, + (Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey), + ): return key if isinstance(key, (bytes, str)): @@ -565,9 +572,10 @@ if has_crypto: def sign(self, msg, key): """ - Sign a message ``msg`` using the Ed25519 private key ``key`` + Sign a message ``msg`` using the EdDSA private key ``key`` :param str|bytes msg: Message to sign - :param Ed25519PrivateKey key: A :class:`.Ed25519PrivateKey` instance + :param Ed25519PrivateKey}Ed448PrivateKey key: A :class:`.Ed25519PrivateKey` + or :class:`.Ed448PrivateKey` iinstance :return bytes signature: The signature, as bytes """ msg = bytes(msg, "utf-8") if type(msg) is not bytes else msg @@ -575,18 +583,19 @@ if has_crypto: def verify(self, msg, key, sig): """ - Verify a given ``msg`` against a signature ``sig`` using the Ed25519 key ``key`` + Verify a given ``msg`` against a signature ``sig`` using the EdDSA key ``key`` - :param str|bytes sig: Ed25519 signature to check ``msg`` against + :param str|bytes sig: EdDSA signature to check ``msg`` against :param str|bytes msg: Message to sign - :param Ed25519PrivateKey|Ed25519PublicKey key: A private or public Ed25519 key instance + :param Ed25519PrivateKey|Ed25519PublicKey|Ed448PrivateKey|Ed448PublicKey key: + A private or public EdDSA key instance :return bool verified: True if signature is valid, False if not. """ try: msg = bytes(msg, "utf-8") if type(msg) is not bytes else msg sig = bytes(sig, "utf-8") if type(sig) is not bytes else sig - if isinstance(key, Ed25519PrivateKey): + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): key = key.public_key() key.verify(sig, msg) return True # If no exception was raised, the signature is valid. @@ -595,21 +604,21 @@ if has_crypto: @staticmethod def to_jwk(key): - if isinstance(key, Ed25519PublicKey): + if isinstance(key, (Ed25519PublicKey, Ed448PublicKey)): x = key.public_bytes( encoding=Encoding.Raw, format=PublicFormat.Raw, ) - + crv = "Ed25519" if isinstance(key, Ed25519PublicKey) else "Ed448" return json.dumps( { "x": base64url_encode(force_bytes(x)).decode(), "kty": "OKP", - "crv": "Ed25519", + "crv": crv, } ) - if isinstance(key, Ed25519PrivateKey): + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): d = key.private_bytes( encoding=Encoding.Raw, format=PrivateFormat.Raw, @@ -621,12 +630,13 @@ if has_crypto: format=PublicFormat.Raw, ) + crv = "Ed25519" if isinstance(key, Ed25519PrivateKey) else "Ed448" return json.dumps( { "x": base64url_encode(force_bytes(x)).decode(), "d": base64url_encode(force_bytes(d)).decode(), "kty": "OKP", - "crv": "Ed25519", + "crv": crv, } ) @@ -648,7 +658,7 @@ if has_crypto: raise InvalidKeyError("Not an Octet Key Pair") curve = obj.get("crv") - if curve != "Ed25519": + if curve != "Ed25519" and curve != "Ed448": raise InvalidKeyError(f"Invalid curve: {curve}") if "x" not in obj: @@ -657,8 +667,12 @@ if has_crypto: try: if "d" not in obj: - return Ed25519PublicKey.from_public_bytes(x) + if curve == "Ed25519": + return Ed25519PublicKey.from_public_bytes(x) + return Ed448PublicKey.from_public_bytes(x) d = base64url_decode(obj.get("d")) - return Ed25519PrivateKey.from_private_bytes(d) + if curve == "Ed25519": + return Ed25519PrivateKey.from_private_bytes(d) + return Ed448PrivateKey.from_private_bytes(d) except ValueError as err: raise InvalidKeyError("Invalid key parameter") from err diff --git a/tests/keys/jwk_okp_key_Ed448.json b/tests/keys/jwk_okp_key_Ed448.json new file mode 100644 index 0000000..02c44d0 --- /dev/null +++ b/tests/keys/jwk_okp_key_Ed448.json @@ -0,0 +1,9 @@ +{ + "kty": "OKP", + "kid": "sig_ed448_01", + "crv": "Ed448", + "use": "sig", + "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", + "d": "Zh5xx0r_0tq39xj-8jGuCwAA6wsDim2ME7cX_iXzqDRgPN8lsZZHu60AO7m31Fa4NtHO07eU63q8", + "alg": "EdDSA" +} diff --git a/tests/keys/jwk_okp_pub_Ed448.json b/tests/keys/jwk_okp_pub_Ed448.json new file mode 100644 index 0000000..9ce0a10 --- /dev/null +++ b/tests/keys/jwk_okp_pub_Ed448.json @@ -0,0 +1,8 @@ +{ + "kty": "OKP", + "kid": "sig_ed448_01", + "crv": "Ed448", + "use": "sig", + "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", + "alg": "EdDSA" +} diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 417f91d..b6a73fc 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -11,12 +11,7 @@ from .keys import load_ec_pub_key_p_521, load_hmac_key, load_rsa_pub_key from .utils import crypto_required, key_path if has_crypto: - from jwt.algorithms import ( - ECAlgorithm, - Ed25519Algorithm, - RSAAlgorithm, - RSAPSSAlgorithm, - ) + from jwt.algorithms import ECAlgorithm, OKPAlgorithm, RSAAlgorithm, RSAPSSAlgorithm class TestAlgorithms: @@ -667,12 +662,12 @@ class TestAlgorithmsRFC7520: @crypto_required -class TestEd25519Algorithms: +class TestOKPAlgorithms: hello_world_sig = b"Qxa47mk/azzUgmY2StAOguAd4P7YBLpyCfU3JdbaiWnXM4o4WibXwmIHvNYgN3frtE2fcyd8OYEaOiD/KiwkCg==" hello_world = b"Hello World!" - def test_ed25519_should_reject_non_string_key(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_should_reject_non_string_key(self): + algo = OKPAlgorithm() with pytest.raises(TypeError): algo.prepare_key(None) @@ -683,14 +678,14 @@ class TestEd25519Algorithms: with open(key_path("testkey_ed25519.pub")) as keyfile: algo.prepare_key(keyfile.read()) - def test_ed25519_should_accept_unicode_key(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_should_accept_unicode_key(self): + algo = OKPAlgorithm() with open(key_path("testkey_ed25519")) as ec_key: algo.prepare_key(ec_key.read()) - def test_ed25519_sign_should_generate_correct_signature_value(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_sign_should_generate_correct_signature_value(self): + algo = OKPAlgorithm() jwt_message = self.hello_world @@ -706,8 +701,8 @@ class TestEd25519Algorithms: result = algo.verify(jwt_message, jwt_pub_key, expected_sig) assert result - def test_ed25519_verify_should_return_false_if_signature_invalid(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_verify_should_return_false_if_signature_invalid(self): + algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(self.hello_world_sig) @@ -720,8 +715,8 @@ class TestEd25519Algorithms: result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert not result - def test_ed25519_verify_should_return_true_if_signature_valid(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_verify_should_return_true_if_signature_valid(self): + algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(self.hello_world_sig) @@ -732,8 +727,8 @@ class TestEd25519Algorithms: result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert result - def test_ed25519_prepare_key_should_be_idempotent(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_prepare_key_should_be_idempotent(self): + algo = OKPAlgorithm() with open(key_path("testkey_ed25519.pub")) as keyfile: jwt_pub_key_first = algo.prepare_key(keyfile.read()) @@ -741,8 +736,8 @@ class TestEd25519Algorithms: assert jwt_pub_key_first == jwt_pub_key_second - def test_ed25519_jwk_private_key_should_parse_and_verify(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_jwk_private_key_should_parse_and_verify(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: key = algo.from_jwk(keyfile.read()) @@ -750,8 +745,19 @@ class TestEd25519Algorithms: 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() + def test_okp_ed25519_jwk_private_key_should_parse_and_verify_with_private_key_as_is( + self, + ): + algo = OKPAlgorithm() + + 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, signature) + + def test_okp_ed25519_jwk_public_key_should_parse_and_verify(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: priv_key = algo.from_jwk(keyfile.read()) @@ -762,8 +768,8 @@ class TestEd25519Algorithms: 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() + def test_okp_ed25519_jwk_fails_on_invalid_json(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile: valid_pub = json.loads(keyfile.read()) @@ -790,6 +796,12 @@ class TestEd25519Algorithms: with pytest.raises(InvalidKeyError): algo.from_jwk(v) + # Invalid crv, "Ed448" + v = valid_pub.copy() + v["crv"] = "Ed448" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + # Missing x v = valid_pub.copy() del v["x"] @@ -808,8 +820,8 @@ class TestEd25519Algorithms: with pytest.raises(InvalidKeyError): algo.from_jwk(v) - def test_ed25519_to_jwk_works_with_from_jwk(self): - algo = Ed25519Algorithm() + def test_okp_ed25519_to_jwk_works_with_from_jwk(self): + algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: priv_key_1 = algo.from_jwk(keyfile.read()) @@ -827,8 +839,111 @@ class TestEd25519Algorithms: 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() + def test_okp_to_jwk_raises_exception_on_invalid_key(self): + algo = OKPAlgorithm() with pytest.raises(InvalidKeyError): algo.to_jwk({"not": "a valid key"}) + + def test_okp_ed448_jwk_private_key_should_parse_and_verify(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.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_okp_ed448_jwk_private_key_should_parse_and_verify_with_private_key_as_is( + self, + ): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + key = algo.from_jwk(keyfile.read()) + + signature = algo.sign(b"Hello World!", key) + assert algo.verify(b"Hello World!", key, signature) + + def test_okp_ed448_jwk_public_key_should_parse_and_verify(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + priv_key = algo.from_jwk(keyfile.read()) + + with open(key_path("jwk_okp_pub_Ed448.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_okp_ed448_jwk_fails_on_invalid_json(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_pub_Ed448.json")) as keyfile: + valid_pub = json.loads(keyfile.read()) + with open(key_path("jwk_okp_key_Ed448.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 "Ed448" + v = valid_pub.copy() + v["crv"] = "P-256" + with pytest.raises(InvalidKeyError): + algo.from_jwk(v) + + # Invalid crv, "Ed25519" + v = valid_pub.copy() + v["crv"] = "Ed25519" + 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) + + def test_okp_ed448_to_jwk_works_with_from_jwk(self): + algo = OKPAlgorithm() + + with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: + priv_key_1 = algo.from_jwk(keyfile.read()) + + with open(key_path("jwk_okp_pub_Ed448.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) diff --git a/tests/test_api_jwk.py b/tests/test_api_jwk.py index e0787f4..53c0547 100644 --- a/tests/test_api_jwk.py +++ b/tests/test_api_jwk.py @@ -9,12 +9,7 @@ from jwt.exceptions import InvalidKeyError, PyJWKError from .utils import crypto_required, key_path if has_crypto: - from jwt.algorithms import ( - ECAlgorithm, - Ed25519Algorithm, - HMACAlgorithm, - RSAAlgorithm, - ) + from jwt.algorithms import ECAlgorithm, HMACAlgorithm, OKPAlgorithm, RSAAlgorithm class TestPyJWK: @@ -166,7 +161,7 @@ class TestPyJWK: jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "OKP" - assert isinstance(jwk.Algorithm, Ed25519Algorithm) + assert isinstance(jwk.Algorithm, OKPAlgorithm) @crypto_required def test_from_dict_should_throw_exception_if_arg_is_invalid(self): |