summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomeguy123 <Someguy123@users.noreply.github.com>2020-05-24 02:58:27 +0100
committerGitHub <noreply@github.com>2020-05-24 07:58:27 +0600
commit75c849efcfaccfcd268d820245197f7abbd35825 (patch)
treee370e06cf18998dac74434b83c5eaac72a3e75a2
parent8556a1590532d58c540256ae1824688ec6c7abe2 (diff)
downloadpyjwt-75c849efcfaccfcd268d820245197f7abbd35825.tar.gz
Add support for Ed25519 / EdDSA, with unit tests (#455)
-rw-r--r--jwt/algorithms.py12
-rw-r--r--jwt/contrib/algorithms/py_ed25519.py71
-rw-r--r--tests/contrib/test_algorithms.py86
-rw-r--r--tests/keys/testkey_ed255193
-rw-r--r--tests/keys/testkey_ed25519.pub1
5 files changed, 173 insertions, 0 deletions
diff --git a/jwt/algorithms.py b/jwt/algorithms.py
index 293a470..5e65d9f 100644
--- a/jwt/algorithms.py
+++ b/jwt/algorithms.py
@@ -43,6 +43,7 @@ try:
has_crypto = True
except ImportError:
has_crypto = False
+ has_ed25519 = False
requires_cryptography = set(
[
@@ -56,6 +57,7 @@ requires_cryptography = set(
"PS256",
"PS384",
"PS512",
+ "EdDSA",
]
)
@@ -86,8 +88,18 @@ def get_default_algorithms():
"PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256),
"PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384),
"PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512),
+
}
)
+ # Older versions of the `cryptography` libraries may not have Ed25519 available.
+ # Needs a minimum of version 2.6
+ try:
+ from jwt.contrib.algorithms.py_ed25519 import Ed25519Algorithm
+ default_algorithms.update({
+ "EdDSA": Ed25519Algorithm(),
+ })
+ except ImportError:
+ pass
return default_algorithms
diff --git a/jwt/contrib/algorithms/py_ed25519.py b/jwt/contrib/algorithms/py_ed25519.py
new file mode 100644
index 0000000..6e761d5
--- /dev/null
+++ b/jwt/contrib/algorithms/py_ed25519.py
@@ -0,0 +1,71 @@
+"""
+Implementation of Ed25519 using ``cryptography`` (as of Version 2.6 released in February 2019)
+"""
+
+import cryptography.exceptions
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
+from cryptography.hazmat.primitives.serialization import load_pem_public_key, load_pem_private_key, load_ssh_public_key
+
+from jwt.algorithms import Algorithm
+from jwt.compat import string_types, text_type
+
+
+class Ed25519Algorithm(Algorithm):
+ """
+ Performs signing and verification operations using Ed25519
+
+ This class requires ``cryptography>=2.6`` to be installed.
+ """
+
+ def __init__(self, **kwargs):
+ pass
+
+ def prepare_key(self, key):
+
+ if isinstance(key, (Ed25519PrivateKey, Ed25519PublicKey)):
+ return key
+
+ if isinstance(key, string_types):
+ if isinstance(key, text_type):
+ key = key.encode("utf-8")
+ str_key = key.decode('utf-8')
+
+ if '-----BEGIN PUBLIC' in str_key:
+ return load_pem_public_key(key, backend=default_backend())
+ if '-----BEGIN PRIVATE' in str_key:
+ return load_pem_private_key(key, password=None, backend=default_backend())
+ if str_key[0:4] == 'ssh-':
+ return load_ssh_public_key(key, backend=default_backend())
+
+ raise TypeError("Expecting a PEM-formatted or OpenSSH key.")
+
+ def sign(self, msg, key):
+ """
+ Sign a message ``msg`` using the Ed25519 private key ``key``
+ :param str|bytes msg: Message to sign
+ :param Ed25519PrivateKey key: A :class:`.Ed25519PrivateKey` instance
+ :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``
+
+ :param str|bytes sig: Ed25519 signature to check ``msg`` against
+ :param str|bytes msg: Message to sign
+ :param Ed25519PrivateKey|Ed25519PublicKey key: A private or public Ed25519 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):
+ key = key.public_key()
+ key.verify(sig, msg)
+ return True # If no exception was raised, the signature is valid.
+ except cryptography.exceptions.InvalidSignature:
+ return False
diff --git a/tests/contrib/test_algorithms.py b/tests/contrib/test_algorithms.py
index 4a1550b..35825c0 100644
--- a/tests/contrib/test_algorithms.py
+++ b/tests/contrib/test_algorithms.py
@@ -1,4 +1,5 @@
import base64
+import warnings
import pytest
@@ -20,6 +21,13 @@ try:
except ImportError:
has_ecdsa = False
+try:
+ from jwt.contrib.algorithms.py_ed25519 import Ed25519Algorithm
+
+ has_ed25519 = True
+except ImportError as e:
+ has_ed25519 = False
+
@pytest.mark.skipif(
not has_pycrypto, reason="Not supported without PyCrypto library"
@@ -212,3 +220,81 @@ class TestEcdsaAlgorithms:
jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
assert jwt_pub_key_first == jwt_pub_key_second
+
+
+@pytest.mark.skipif(
+ not has_ed25519, reason="Not supported without cryptography>=2.6 library"
+)
+class TestEd25519Algorithms:
+ hello_world_sig = 'Qxa47mk/azzUgmY2StAOguAd4P7YBLpyCfU3JdbaiWnXM4o4WibXwmIHvNYgN3frtE2fcyd8OYEaOiD/KiwkCg=='
+ hello_world = force_bytes('Hello World!')
+
+ def test_ed25519_should_reject_non_string_key(self):
+ algo = Ed25519Algorithm()
+
+ with pytest.raises(TypeError):
+ algo.prepare_key(None)
+
+ with open(key_path("testkey_ed25519"), "r") as keyfile:
+ jwt_key = algo.prepare_key(keyfile.read())
+
+ with open(key_path("testkey_ed25519.pub"), "r") as keyfile:
+ jwt_pub_key = algo.prepare_key(keyfile.read())
+
+ def test_ed25519_should_accept_unicode_key(self):
+ algo = Ed25519Algorithm()
+
+ with open(key_path("testkey_ed25519"), "r") as ec_key:
+ algo.prepare_key(force_unicode(ec_key.read()))
+
+ def test_ed25519_sign_should_generate_correct_signature_value(self):
+ algo = Ed25519Algorithm()
+
+ jwt_message = self.hello_world
+
+ expected_sig = base64.b64decode(force_bytes(self.hello_world_sig))
+
+ with open(key_path("testkey_ed25519"), "r") as keyfile:
+ jwt_key = algo.prepare_key(keyfile.read())
+
+ with open(key_path("testkey_ed25519.pub"), "r") as keyfile:
+ jwt_pub_key = algo.prepare_key(keyfile.read())
+
+ algo.sign(jwt_message, jwt_key)
+ 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()
+
+ jwt_message = self.hello_world
+ jwt_sig = base64.b64decode(force_bytes(self.hello_world_sig))
+
+ jwt_sig += force_bytes("123") # Signature is now invalid
+
+ with open(key_path("testkey_ed25519.pub"), "r") as keyfile:
+ jwt_pub_key = algo.prepare_key(keyfile.read())
+
+ 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()
+
+ jwt_message = self.hello_world
+ jwt_sig = base64.b64decode(force_bytes(self.hello_world_sig))
+
+ with open(key_path("testkey_ed25519.pub"), "r") as keyfile:
+ jwt_pub_key = algo.prepare_key(keyfile.read())
+
+ result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
+ assert result
+
+ def test_ed25519_prepare_key_should_be_idempotent(self):
+ algo = Ed25519Algorithm()
+
+ with open(key_path("testkey_ed25519.pub"), "r") as keyfile:
+ jwt_pub_key_first = algo.prepare_key(keyfile.read())
+ jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
+
+ assert jwt_pub_key_first == jwt_pub_key_second
diff --git a/tests/keys/testkey_ed25519 b/tests/keys/testkey_ed25519
new file mode 100644
index 0000000..6863f17
--- /dev/null
+++ b/tests/keys/testkey_ed25519
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIBy9N4xfv/9qOiKrxwRKeGfO5ab6lSukKHbuC5vaJ1Mg
+-----END PRIVATE KEY-----
diff --git a/tests/keys/testkey_ed25519.pub b/tests/keys/testkey_ed25519.pub
new file mode 100644
index 0000000..13c80c7
--- /dev/null
+++ b/tests/keys/testkey_ed25519.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC4pK2dePGgctIAsh0H/tmUrLzx2Vc4Ltc8TN9nfuChG \ No newline at end of file