Skip to content

Commit 9e27449

Browse files
committed
feat(jwa): add Ed25519 and Ed448 algorithms for JWS
#76
1 parent 730f5d5 commit 9e27449

File tree

5 files changed

+102
-4
lines changed

5 files changed

+102
-4
lines changed

src/joserfc/_rfc8037/jws_eddsa.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from cryptography.exceptions import InvalidSignature
22
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
33
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey, Ed448PrivateKey
4+
from ..errors import InvalidKeyTypeError
45
from .._rfc7515.model import JWSAlgModel
56
from .okp_key import OKPKey
67

@@ -9,15 +10,18 @@ class EdDSAAlgorithm(JWSAlgModel):
910
name = "EdDSA"
1011
description = "Edwards-curve Digital Signature Algorithm for JWS"
1112
key_type = "OKP"
13+
security_warning = "EdDSA is deprecated via RFC 9864"
1214

1315
def sign(self, msg: bytes, key: OKPKey) -> bytes:
1416
op_key = key.get_op_key("sign")
15-
assert isinstance(op_key, (Ed25519PrivateKey, Ed448PrivateKey))
17+
if not isinstance(op_key, (Ed25519PrivateKey, Ed448PrivateKey)):
18+
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires 'Ed25519' and 'Ed448' OKP key")
1619
return op_key.sign(msg)
1720

1821
def verify(self, msg: bytes, sig: bytes, key: OKPKey) -> bool:
1922
op_key = key.get_op_key("verify")
20-
assert isinstance(op_key, (Ed25519PublicKey, Ed448PublicKey))
23+
if not isinstance(op_key, (Ed25519PublicKey, Ed448PublicKey)):
24+
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires 'Ed25519' and 'Ed448' OKP key")
2125
try:
2226
op_key.verify(sig, msg)
2327
return True

src/joserfc/_rfc9864/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from .jws_eddsa import JWS_ALGORITHMS, Ed25519, Ed448
2+
3+
4+
__all__ = [
5+
"JWS_ALGORITHMS",
6+
"Ed25519",
7+
"Ed448",
8+
]

src/joserfc/_rfc9864/jws_eddsa.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import typing as t
2+
from cryptography.exceptions import InvalidSignature
3+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
4+
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey, Ed448PrivateKey
5+
from ..errors import InvalidKeyTypeError
6+
from .._rfc7515.model import JWSAlgModel
7+
from .._rfc8037.okp_key import OKPKey
8+
9+
10+
_private_key_mapping = {"Ed25519": Ed25519PrivateKey, "Ed448": Ed448PrivateKey}
11+
_public_key_mapping = {"Ed25519": Ed25519PublicKey, "Ed448": Ed448PublicKey}
12+
13+
14+
class EdDSAAlgorithm(JWSAlgModel):
15+
key_type = "OKP"
16+
17+
def __init__(self, curve: t.Literal["Ed25519", "Ed448"]):
18+
self.name = curve
19+
self.description = f"EdDSA using the {curve} parameter set"
20+
21+
def sign(self, msg: bytes, key: OKPKey) -> bytes:
22+
op_key = t.cast(t.Union[Ed25519PrivateKey, Ed448PrivateKey], key.get_op_key("sign"))
23+
private_key_cls = _private_key_mapping[self.name]
24+
if not isinstance(op_key, private_key_cls):
25+
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires '{self.name}' OKP key")
26+
return op_key.sign(msg)
27+
28+
def verify(self, msg: bytes, sig: bytes, key: OKPKey) -> bool:
29+
op_key = t.cast(t.Union[Ed25519PublicKey, Ed448PublicKey], key.get_op_key("verify"))
30+
public_key_cls = _public_key_mapping[self.name]
31+
if not isinstance(op_key, public_key_cls):
32+
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires '{self.name}' OKP key")
33+
try:
34+
op_key.verify(sig, msg)
35+
return True
36+
except InvalidSignature:
37+
return False
38+
39+
40+
Ed25519 = EdDSAAlgorithm("Ed25519")
41+
Ed448 = EdDSAAlgorithm("Ed448")
42+
43+
JWS_ALGORITHMS: list[EdDSAAlgorithm] = [Ed25519, Ed448]

src/joserfc/jwa.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
RSAAlgorithm,
1717
ESAlgorithm,
1818
RSAPSSAlgorithm,
19-
JWS_ALGORITHMS as _JWS_ALGORITHMS,
19+
JWS_ALGORITHMS as RFC7518_JWS_ALGORITHMS,
2020
)
2121
from ._rfc7518.jwe_algs import (
2222
DirectAlgEncryption,
@@ -37,6 +37,7 @@
3737
)
3838
from ._rfc8037.jws_eddsa import EdDSA, EdDSAAlgorithm
3939
from ._rfc8812 import ES256K
40+
from ._rfc9864 import JWS_ALGORITHMS as RFC9864_JWS_ALGORITHMS
4041
from ._keys import KeySet
4142

4243
__all__ = [
@@ -74,9 +75,10 @@
7475
]
7576

7677
JWS_ALGORITHMS = [
77-
*_JWS_ALGORITHMS,
78+
*RFC7518_JWS_ALGORITHMS,
7879
EdDSA,
7980
ES256K,
81+
*RFC9864_JWS_ALGORITHMS,
8082
]
8183

8284

tests/jws/test_eddsa.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import typing as t
2+
from unittest import TestCase
3+
from joserfc import jwt
4+
from joserfc.jwk import OKPKey
5+
from joserfc.errors import InvalidKeyTypeError, BadSignatureError
6+
from tests.base import load_key
7+
8+
9+
class TestEdDSA(TestCase):
10+
x25519_key = t.cast(OKPKey, load_key("okp-x25519-alice.json"))
11+
ed25519_key = t.cast(OKPKey, load_key("okp-ed25519-private.json"))
12+
ed448_key = t.cast(OKPKey, load_key("okp-ed448-private.pem"))
13+
14+
def test_EdDSA(self):
15+
algorithms = ["EdDSA"]
16+
encoded_jwt = jwt.encode({"alg": "EdDSA"}, {}, self.ed25519_key, algorithms=algorithms)
17+
jwt.decode(encoded_jwt, self.ed25519_key, algorithms=algorithms)
18+
self.assertRaises(InvalidKeyTypeError, jwt.decode, encoded_jwt, self.x25519_key, algorithms=algorithms)
19+
self.assertRaises(InvalidKeyTypeError, jwt.encode, {"alg": "EdDSA"}, {}, self.x25519_key, algorithms=algorithms)
20+
21+
def test_Ed25519(self):
22+
algorithms = ["Ed25519"]
23+
encoded_jwt = jwt.encode({"alg": "Ed25519"}, {}, self.ed25519_key, algorithms=algorithms)
24+
jwt.decode(encoded_jwt, self.ed25519_key, algorithms=algorithms)
25+
self.assertRaises(
26+
InvalidKeyTypeError, jwt.encode, {"alg": "Ed25519"}, {}, self.ed448_key, algorithms=algorithms
27+
)
28+
self.assertRaises(InvalidKeyTypeError, jwt.decode, encoded_jwt, self.ed448_key, algorithms=algorithms)
29+
wrong_key = OKPKey.generate_key("Ed25519", private=False)
30+
self.assertRaises(BadSignatureError, jwt.decode, encoded_jwt, wrong_key, algorithms=algorithms)
31+
32+
def test_Ed448(self):
33+
algorithms = ["Ed448"]
34+
encoded_jwt = jwt.encode({"alg": "Ed448"}, {}, self.ed448_key, algorithms=algorithms)
35+
jwt.decode(encoded_jwt, self.ed448_key, algorithms=algorithms)
36+
self.assertRaises(
37+
InvalidKeyTypeError, jwt.encode, {"alg": "Ed448"}, {}, self.ed25519_key, algorithms=algorithms
38+
)
39+
self.assertRaises(InvalidKeyTypeError, jwt.decode, encoded_jwt, self.ed25519_key, algorithms=algorithms)
40+
wrong_key = OKPKey.generate_key("Ed448", private=False)
41+
self.assertRaises(BadSignatureError, jwt.decode, encoded_jwt, wrong_key, algorithms=algorithms)

0 commit comments

Comments
 (0)