Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions jose/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,18 @@
from jose.backends.cryptography_backend import CryptographyECKey as ECKey # noqa: F401
except ImportError:
from jose.backends.ecdsa_backend import ECDSAECKey as ECKey # noqa: F401

try:
from jose.backends.nacl_backend import Ed25519Key # noqa: F401
except ImportError:
pass
else:
# Since PyNaCl is an optional dependency, we do not add EdDSA to the set
# of supported algorithms in the jose.constants module.
# As a result, when we successfully import Ed25519Key, we need to manually
# register that algorithm, but we cannot do it in the jose.constants module
# because that would create a circular import. Instead, we do it here.
# TODO: Refactor to use __init_subclass__ hook on jose.backends.base.Key
import jose.constants as j_c
j_c.ALGORITHMS.SUPPORTED = j_c.ALGORITHMS.SUPPORTED.union(j_c.ALGORITHMS.ED)
j_c.ALGORITHMS.ALL = j_c.ALGORITHMS.SUPPORTED.union([j_c.ALGORITHMS.NONE])
106 changes: 106 additions & 0 deletions jose/backends/nacl_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import base64

import six

from jose.backends.base import Key
from jose.constants import ALGORITHMS, USAGES
from jose.exceptions import JWKError

from nacl.exceptions import BadSignatureError
from nacl.signing import SigningKey, VerifyKey


class Ed25519Key(Key):
def __init__(self, key, algorithm, use=None):
if algorithm not in ALGORITHMS.ED:
raise JWKError('hash_alg: %s is not a valid Ed25519 hash algorithm' % algorithm)

# TODO: Validate Ed25519 hash algorithms
self._algorithm = algorithm

if isinstance(key, dict):
self._prepared_key = self._process_jwk(key)
return

if isinstance(key, (SigningKey, VerifyKey)):
self._prepared_key = key
return

if isinstance(key, six.string_types):
key = key.encode('utf-8') + b'=='

if isinstance(key, six.binary_type):
if use is None:
raise JWKError("The 'use' parameter is required when deserializing an Ed25519 key "
"from a string or bytes")

if use == USAGES.PUBLIC:
decoded_key_bytes = base64.urlsafe_b64decode(key)
self._prepared_key = VerifyKey(decoded_key_bytes)
elif use == USAGES.PRIVATE:
decoded_key_bytes = base64.urlsafe_b64decode(key)
self._prepared_key = SigningKey(decoded_key_bytes)
else:
raise JWKError("The 'use' parameter must either be 'public' or 'private', not %s" % use)
return

raise JWKError('Unable to parse an Ed25519_JWK from key: %s' % key)

def _process_jwk(self, jwk_dict):
if not jwk_dict.get('kty') == 'OKP':
raise JWKError("Incorrect key type. Expected: 'OKP', Received: %s" % jwk_dict.get('kty'))

if not jwk_dict.get('crv') == 'Ed25519':
raise JWKError("Incorrect key subtype. Expected 'Ed25519', Received %s" % jwk_dict.get('crv'))

if 'd' in jwk_dict:
# d indicates private key
d = jwk_dict.get('d').encode('utf-8') + b'=='
decoded_d_bytes = base64.urlsafe_b64decode(d)
return SigningKey(decoded_d_bytes)
else:
# no d indicates public key
x = jwk_dict.get('x').encode('utf-8') + b'=='
decoded_x_bytes = base64.urlsafe_b64decode(x)
return VerifyKey(decoded_x_bytes)

def sign(self, msg):
if isinstance(msg, six.string_types):
msg = msg.encode('utf-8')
return self._prepared_key.sign(msg)

def verify(self, msg, sig=None):
try:
self._prepared_key.verify(msg, sig)
return True
except BadSignatureError:
return False

def is_public(self):
return isinstance(self._prepared_key, VerifyKey)

def public_key(self):
if isinstance(self._prepared_key, VerifyKey):
return self
return self.__class__(self._prepared_key.verify_key, self._algorithm)

def to_pem(self, *args, **kwargs):
# Serializing Ed25519 keys is not yet supported anywhere in Python
# AFAICT, so instead of creating our own format, we simply prevent
# anybody from serializing to PEM.
raise NotImplementedError("Cannot serialize Ed25519 keys yet")

def to_dict(self):
public_key = self.public_key()

data = {
'alg': self._algorithm,
'kty': 'OKP',
'crv': 'Ed25519',
'x': base64.urlsafe_b64encode(bytes(public_key._prepared_key)).decode('utf-8'),
}

if not self.is_public():
data.update({'d': base64.urlsafe_b64encode(bytes(self._prepared_key)).decode('utf-8')})

return data
16 changes: 16 additions & 0 deletions jose/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ class Algorithms(object):
ES256 = 'ES256'
ES384 = 'ES384'
ES512 = 'ES512'
# RFC8037 - https://tools.ietf.org/html/rfc8037
EdDSA = 'EdDSA'

HMAC = {HS256, HS384, HS512}
RSA = {RS256, RS384, RS512}
EC = {ES256, ES384, ES512}
ED = {EdDSA}

# This is modified in jose.backends.nacl_backend to register EdDSA
SUPPORTED = HMAC.union(RSA).union(EC)

ALL = SUPPORTED.union([NONE])
Expand All @@ -37,3 +41,15 @@ class Algorithms(object):


ALGORITHMS = Algorithms()


class Usages(object):
PUBLIC = 'public'
PRIVATE = 'private'

SUPPORTED = {PUBLIC, PRIVATE}

ALL = SUPPORTED


USAGES = Usages()
15 changes: 13 additions & 2 deletions jose/jwk.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
except ImportError:
pass

try:
from jose.backends import Ed25519Key # noqa: F401
except ImportError:
pass


def get_key(algorithm):
if algorithm in ALGORITHMS.KEYS:
Expand All @@ -31,6 +36,9 @@ def get_key(algorithm):
elif algorithm in ALGORITHMS.EC:
from jose.backends import ECKey # noqa: F811
return ECKey
elif algorithm in ALGORITHMS.ED:
from jose.backends import Ed25519Key # noqa: F811
return Ed25519Key
return None


Expand All @@ -42,7 +50,7 @@ def register_key(algorithm, key_class):
return True


def construct(key_data, algorithm=None):
def construct(key_data, algorithm=None, use=None):
"""
Construct a Key object for the given algorithm with the given
key_data.
Expand All @@ -58,7 +66,10 @@ def construct(key_data, algorithm=None):
key_class = get_key(algorithm)
if not key_class:
raise JWKError('Unable to find a algorithm for key: %s' % key_data)
return key_class(key_data, algorithm)
if use is None:
return key_class(key_data, algorithm)
else:
return key_class(key_data, algorithm, use)


def get_algorithm_object(algorithm):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ six
rsa
ecdsa
pyasn1
pynacl
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ def _cryptography_version():
'cryptography': [_cryptography_version()],
'pycrypto': ['pycrypto >=2.6.0, <2.7.0'] + pyasn1,
'pycryptodome': ['pycryptodome >=3.3.1, <4.0.0'] + pyasn1,
'ed25519': ['pynacl'],
}
legacy_backend_requires = ['ecdsa <1.0', 'rsa'] + pyasn1
install_requires = ['six <2.0']
install_requires = ['six <2.0', 'pynacl']

# TODO: work this into the extras selection instead.
install_requires += legacy_backend_requires
Expand Down
165 changes: 165 additions & 0 deletions tests/algorithms/test_Ed25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@

import base64

from jose.backends.nacl_backend import Ed25519Key
from jose.constants import ALGORITHMS, USAGES
from jose.exceptions import JWKError

from nacl.signing import SigningKey, VerifyKey

import pytest


SIGNING_KEY = "npAVhmIfq2byvIzcmgS5cguKCv2Nw8Seqa1Fku00LoE="
signing_key_bytes = base64.urlsafe_b64decode(SIGNING_KEY.encode('utf-8'))
nacl_signing_key = SigningKey(signing_key_bytes)
nacl_verify_key = nacl_signing_key.verify_key
VERIFY_KEY = base64.urlsafe_b64encode(bytes(nacl_verify_key))


class TestEd25519Algorithm:

@pytest.mark.parametrize("alg", ALGORITHMS.ED)
@pytest.mark.parametrize("use", USAGES.ALL)
def test_Ed25519_key(self, alg, use):
assert Ed25519Key(SIGNING_KEY, algorithm=alg, use=use)._prepared_key
assert Ed25519Key(SIGNING_KEY.encode('utf-8'), algorithm=alg, use=use)._prepared_key
# With Ed25519, there is no difference between seeds for private and public keys, and ALL
# 256-bit values are valid seeds, so since we have already tested with private key seeds,
# we do not need to also test for public key seeds

@pytest.mark.parametrize("alg", ALGORITHMS.ED)
def test_Ed25519_signing_key(self, alg):
assert Ed25519Key(nacl_signing_key, algorithm=alg)._prepared_key
assert not Ed25519Key(nacl_signing_key, algorithm=alg).is_public()

@pytest.mark.parametrize("alg", ALGORITHMS.ED)
def test_Ed25519_verify_key(self, alg):
assert Ed25519Key(nacl_verify_key, algorithm=alg)._prepared_key
assert Ed25519Key(nacl_verify_key, algorithm=alg).is_public()

def test_Ed25519_key_unknown_object(self):
with pytest.raises(JWKError):
Ed25519Key(object(), algorithm=ALGORITHMS.EdDSA)

@pytest.mark.parametrize("use", USAGES.ALL)
def test_Ed25519_key_bad_alg(self, use):
with pytest.raises(JWKError):
Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.ES256, use=use)

@pytest.mark.parametrize("alg", ALGORITHMS.ED)
def test_Ed25519_key_bad_use(self, alg):
with pytest.raises(JWKError):
Ed25519Key(SIGNING_KEY, algorithm=alg)

with pytest.raises(JWKError):
Ed25519Key(SIGNING_KEY, algorithm=alg, use=None)

with pytest.raises(JWKError):
Ed25519Key(SIGNING_KEY, algorithm=alg, use='bad_usage')

def test_get_verify_key(self):
signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE)

assert not signing_key.is_public()

verify_key = signing_key.public_key() # public_key is part of the Key API
verify_key2 = verify_key.public_key()

assert verify_key.is_public()
assert verify_key is verify_key2

def test_to_pem(self):
signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE)

with pytest.raises(NotImplementedError):
signing_key.to_pem()

def test_verify_key_to_pem(self):
signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE)
verify_key = signing_key.public_key()

with pytest.raises(NotImplementedError):
verify_key.to_pem()

def assert_parameters(self, as_dict, private):
assert isinstance(as_dict, dict)

# Public parameters should always be there
assert 'x' in as_dict

if private:
# Private parameters as well
assert 'd' in as_dict
else:
# Private parameters should be absent
assert 'd' not in as_dict

def assert_roundtrip(self, key, use):
assert Ed25519Key(key.to_dict(), ALGORITHMS.EdDSA, use=use).to_dict() == key.to_dict()

def test_signing_key_to_dict(self):
key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE)

self.assert_parameters(key.to_dict(), private=True)
self.assert_roundtrip(key, use=USAGES.PRIVATE)

def test_verify_key_to_dict(self):
key = Ed25519Key(VERIFY_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PUBLIC)

self.assert_parameters(key.to_dict(), private=False)
self.assert_roundtrip(key, use=USAGES.PUBLIC)

def test_verify_key_from_bad_dict(self):
key = Ed25519Key(VERIFY_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PUBLIC)
key_dict = key.to_dict()

bad_key_dict = key_dict.copy()
bad_key_dict['kty'] = "SOMETHING_ELSE"

with pytest.raises(JWKError):
Ed25519Key(bad_key_dict, algorithm=ALGORITHMS.EdDSA)

bad_key_dict = key_dict.copy()
bad_key_dict['crv'] = "SOMETHING_ELSE"

with pytest.raises(JWKError):
Ed25519Key(bad_key_dict, algorithm=ALGORITHMS.EdDSA)

def test_signing_bytes_parity(self):
signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE)
verify_key = signing_key.public_key()

msg = b'test'
smsg = signing_key.sign(msg) # -> signature + cleartext message
bad_smsg_message = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.message)])
bad_smsg_signature = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.signature)])
bad_smsg = bad_smsg_signature + bad_smsg_message

assert verify_key.verify(smsg)
assert verify_key.verify(smsg.signature + smsg.message)
assert verify_key.verify(smsg.message, smsg.signature)

assert not verify_key.verify(smsg.message, bad_smsg_signature)
assert not verify_key.verify(bad_smsg_message, smsg.signature)
assert not verify_key.verify(bad_smsg_message, bad_smsg_signature)
assert not verify_key.verify(bad_smsg)

def test_signing_string_parity(self):
signing_key = Ed25519Key(SIGNING_KEY, algorithm=ALGORITHMS.EdDSA, use=USAGES.PRIVATE)
verify_key = signing_key.public_key()

msg = 'test'
smsg = signing_key.sign(msg) # -> signature + cleartext message
bad_smsg_message = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.message)])
bad_smsg_signature = bytes([(b + 1) if b < 255 else 0 for b in bytearray(smsg.signature)])
bad_smsg = bad_smsg_signature + bad_smsg_message

assert verify_key.verify(smsg)
assert verify_key.verify(smsg.signature + smsg.message)
assert verify_key.verify(smsg.message, smsg.signature)

assert not verify_key.verify(smsg.message, bad_smsg_signature)
assert not verify_key.verify(bad_smsg_message, smsg.signature)
assert not verify_key.verify(bad_smsg_message, bad_smsg_signature)
assert not verify_key.verify(bad_smsg)
Loading