Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Ed448/EdDSA. #675

Merged
merged 2 commits into from Oct 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -16,6 +16,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>`__
--------------------------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/algorithms.rst
Expand Up @@ -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
----------------------------------
Expand Down
50 changes: 32 additions & 18 deletions jwt/algorithms.py
Expand Up @@ -22,6 +22,10 @@
EllipticCurvePrivateKey,
EllipticCurvePublicKey,
)
from cryptography.hazmat.primitives.asymmetric.ed448 import (
Ed448PrivateKey,
Ed448PublicKey,
)
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
Expand Down Expand Up @@ -93,7 +97,7 @@ def get_default_algorithms():
"PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256),
"PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384),
"PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512),
"EdDSA": Ed25519Algorithm(),
"EdDSA": OKPAlgorithm(),
}
)

Expand Down Expand Up @@ -534,9 +538,9 @@ def verify(self, msg, key, sig):
except InvalidSignature:
return False

class Ed25519Algorithm(Algorithm):
class OKPAlgorithm(Algorithm):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might need to consider this a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your time reviewing this PR.

I think Algorithm classes are not disclosed via jwt/init.py so it is not necessary for us to regard this change as a breaking change.

As you can see, there is no change on public class tests (e.g., jwt_test.py, jws_test.py, jwk_test.py) except for here where a test assertion checks an instance type of jwk.Algorithm returned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@auvipy Hi, could you review and merge it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my only concern is what happen to Ed25519Algorithm? can we use deprecation warning to it and introduce OKPAlgorithm at the same time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use deprecation warning to it and introduce OKPAlgorithm at the same time?

We don't need to use deprecation warning because Ed25519Algorithm is a private class and this change does not break backward-compat. All of the tests for public classes and methods are passed successfully.

"""
Performs signing and verification operations using Ed25519
Performs signing and verification operations using EdDSA

This class requires ``cryptography>=2.6`` to be installed.
"""
Expand All @@ -546,7 +550,10 @@ def __init__(self, **kwargs):

def prepare_key(self, key):

if isinstance(key, (Ed25519PrivateKey, Ed25519PublicKey)):
if isinstance(
key,
(Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey),
):
return key

if isinstance(key, (bytes, str)):
Expand All @@ -565,28 +572,30 @@ def prepare_key(self, key):

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
return key.sign(msg)

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.
Expand All @@ -595,21 +604,21 @@ def verify(self, msg, key, sig):

@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,
Expand All @@ -621,12 +630,13 @@ def to_jwk(key):
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,
}
)

Expand All @@ -648,7 +658,7 @@ def from_jwk(jwk):
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:
Expand All @@ -657,8 +667,12 @@ def from_jwk(jwk):

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
9 changes: 9 additions & 0 deletions 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"
}
8 changes: 8 additions & 0 deletions 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"
}