summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAjitomi, Daisuke <ajitomi@gmail.com>2021-10-03 14:23:56 +0900
committerGitHub <noreply@github.com>2021-10-03 11:23:56 +0600
commite7a6c022f3f2e5ba329cbadd242c788014926a7e (patch)
treee2856bc61e1ace6ba39cd1d1173dc4b60cbd19fb
parent19ce9c5ec7947428d35aaffd302eb2629210a697 (diff)
downloadpyjwt-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.rst2
-rw-r--r--docs/algorithms.rst2
-rw-r--r--jwt/algorithms.py50
-rw-r--r--tests/keys/jwk_okp_key_Ed448.json9
-rw-r--r--tests/keys/jwk_okp_pub_Ed448.json8
-rw-r--r--tests/test_algorithms.py173
-rw-r--r--tests/test_api_jwk.py9
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):