From a7817de4ab3b3a7f52088ea8b8f2e243d74ae9b8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 8 Jul 2015 11:30:09 +0000 Subject: [PATCH] Rewrite JWK.load, JWKRSA autowraps ComparableRSAKey. --- acme/jose/jwa_test.py | 23 ++++----- acme/jose/jwk.py | 105 +++++++++++++++++++++++++++++------------- acme/jose/jwk_test.py | 29 ++++++++++-- acme/other.py | 2 +- 4 files changed, 109 insertions(+), 50 deletions(-) diff --git a/acme/jose/jwa_test.py b/acme/jose/jwa_test.py index 91b864cf5..147038788 100644 --- a/acme/jose/jwa_test.py +++ b/acme/jose/jwa_test.py @@ -7,16 +7,9 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from acme.jose import errors +from acme.jose import jwk_test -RSA256_KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa256_key.pem')), - password=None, backend=default_backend()) -RSA512_KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) RSA1024_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( __name__, os.path.join('testdata', 'rsa1024_key.pem')), @@ -83,13 +76,13 @@ class JWARSTest(unittest.TestCase): def test_sign_no_private_part(self): from acme.jose.jwa import RS256 self.assertRaises( - errors.Error, RS256.sign, RSA512_KEY.public_key(), 'foo') + errors.Error, RS256.sign, jwk_test.RSA512_KEY.public_key(), 'foo') def test_sign_key_too_small(self): from acme.jose.jwa import RS256 from acme.jose.jwa import PS256 - self.assertRaises(errors.Error, RS256.sign, RSA256_KEY, 'foo') - self.assertRaises(errors.Error, PS256.sign, RSA256_KEY, 'foo') + self.assertRaises(errors.Error, RS256.sign, jwk_test.RSA256_KEY, 'foo') + self.assertRaises(errors.Error, PS256.sign, jwk_test.RSA256_KEY, 'foo') def test_rs(self): from acme.jose.jwa import RS256 @@ -99,9 +92,11 @@ class JWARSTest(unittest.TestCase): '\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99' '\xd2\xb9.>}\xfd' ) - self.assertEqual(RS256.sign(RSA512_KEY, 'foo'), sig) - self.assertTrue(RS256.verify(RSA512_KEY.public_key(), 'foo', sig)) - self.assertFalse(RS256.verify(RSA512_KEY.public_key(), 'foo', sig + '!')) + self.assertEqual(RS256.sign(jwk_test.RSA512_KEY, 'foo'), sig) + self.assertTrue(RS256.verify( + jwk_test.RSA512_KEY.public_key(), 'foo', sig)) + self.assertFalse(RS256.verify( + jwk_test.RSA512_KEY.public_key(), 'foo', sig + '!')) def test_ps(self): from acme.jose.jwa import PS256 diff --git a/acme/jose/jwk.py b/acme/jose/jwk.py index 376988ae2..9454c6f9e 100644 --- a/acme/jose/jwk.py +++ b/acme/jose/jwk.py @@ -1,9 +1,12 @@ """JSON Web Key.""" import abc import binascii +import logging +import cryptography.exceptions from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa from acme.jose import b64 @@ -12,16 +15,16 @@ from acme.jose import json_util from acme.jose import util +logger = logging.getLogger(__name__) + + class JWK(json_util.TypedJSONObjectWithFields): # pylint: disable=too-few-public-methods """JSON Web Key.""" type_field_name = 'kty' TYPES = {} - - @util.abstractclassmethod - def load(cls, string): # pragma: no cover - """Load key from normalized string form.""" - raise NotImplementedError() + cryptography_key_types = () + """Subclasses should override.""" @abc.abstractmethod def public_key(self): # pragma: no cover @@ -32,6 +35,63 @@ class JWK(json_util.TypedJSONObjectWithFields): """ raise NotImplementedError() + @classmethod + def _load_cryptography_key(cls, data, password=None, backend=None): + backend = default_backend() if backend is None else backend + exceptions = {} + + # private key? + for loader in (serialization.load_pem_private_key, + serialization.load_der_private_key): + try: + return loader(data, password, backend) + except (ValueError, TypeError, + cryptography.exceptions.UnsupportedAlgorithm) as error: + exceptions[loader] = error + + # public key? + for loader in (serialization.load_pem_public_key, + serialization.load_der_public_key): + try: + return loader(data, backend) + except (ValueError, + cryptography.exceptions.UnsupportedAlgorithm) as error: + exceptions[loader] = error + + # no luck + raise errors.Error("Unable to deserialize key: {0}".format(exceptions)) + + @classmethod + def load(cls, data, password=None, backend=None): + """Load serialized key as JWK. + + :param str data: Public or private key serialized as PEM or DER. + :param str password: Optional password. + :param backend: A `.PEMSerializationBackend` and + `.DERSerializationBackend` provider. + + :raises errors.Error: if unable to deserialize, or unsupported + JWK algorithm + + :returns: JWK of an appropriate type. + :rtype: `JWK` + + """ + try: + key = cls._load_cryptography_key(data, password, backend) + except errors.Error as error: + logger.debug("Loading symmetric key, assymentric failed: %s", error) + return JWKOct(key=data) + + if cls.typ is not NotImplemented and not isinstance( + key, cls.cryptography_key_types): + raise errors.Error("Unable to deserialize {0} into {1}".format( + key.__class__, cls.__class__)) + for jwk_cls in cls.TYPES.itervalues(): + if isinstance(key, jwk_cls.cryptography_key_types): + return jwk_cls(key=key) + raise errors.Error("Unsupported algorithm: {0}".format(key.__class__)) + @JWK.register class JWKES(JWK): # pragma: no cover @@ -42,6 +102,8 @@ class JWKES(JWK): # pragma: no cover """ typ = 'ES' + cryptography_key_types = ( + ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) def fields_to_partial_json(self): raise NotImplementedError() @@ -50,10 +112,6 @@ class JWKES(JWK): # pragma: no cover def fields_from_json(cls, jobj): raise NotImplementedError() - @classmethod - def load(cls, string): - raise NotImplementedError() - def public_key(self): raise NotImplementedError() @@ -75,10 +133,6 @@ class JWKOct(JWK): def fields_from_json(cls, jobj): return cls(key=jobj['k']) - @classmethod - def load(cls, string): - return cls(key=string) - def public_key(self): return self @@ -93,8 +147,15 @@ class JWKRSA(JWK): """ typ = 'RSA' + cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) __slots__ = ('key',) + def __init__(self, *args, **kwargs): + if 'key' in kwargs and not isinstance( + kwargs['key'], util.ComparableRSAKey): + kwargs['key'] = util.ComparableRSAKey(kwargs['key']) + super(JWKRSA, self).__init__(*args, **kwargs) + @classmethod def _encode_param(cls, data): def _leading_zeros(arg): @@ -112,24 +173,6 @@ class JWKRSA(JWK): except ValueError: # invalid literal for long() with base 16 raise errors.DeserializationError() - @classmethod - def load(cls, string): - """Load RSA key from string. - - :param str string: RSA key in string form. - - :returns: - :rtype: :class:`JWKRSA` - - """ - try: - key = serialization.load_pem_public_key( - string, backend=default_backend()) - except ValueError: # ValueError: Could not unserialize key data. - key = serialization.load_pem_private_key( - string, password=None, backend=default_backend()) - return cls(key=util.ComparableRSAKey(key)) - def public_key(self): return type(self)(key=self.key.public_key()) diff --git a/acme/jose/jwk_test.py b/acme/jose/jwk_test.py index f3745b251..06032f45b 100644 --- a/acme/jose/jwk_test.py +++ b/acme/jose/jwk_test.py @@ -10,14 +10,28 @@ from acme.jose import errors from acme.jose import util -RSA256_KEY = util.ComparableRSAKey(serialization.load_pem_private_key( +DSA_PEM = pkg_resources.resource_string( + 'letsencrypt.tests', os.path.join('testdata', 'dsa512_key.pem')) +RSA256_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( __name__, os.path.join('testdata', 'rsa256_key.pem')), - password=None, backend=default_backend())) -RSA512_KEY = util.ComparableRSAKey(serialization.load_pem_private_key( + password=None, backend=default_backend()) +RSA512_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( __name__, os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend())) + password=None, backend=default_backend()) + + +class JWKTest(unittest.TestCase): + """Tests for acme.jose.jwk.JWK.""" + + def test_load(self): + from acme.jose.jwk import JWK + self.assertRaises(errors.Error, JWK.load, DSA_PEM) + + def test_load_subclass_wrong_type(self): + from acme.jose.jwk import JWKRSA + self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM) class JWKOctTest(unittest.TestCase): @@ -49,6 +63,7 @@ class JWKOctTest(unittest.TestCase): class JWKRSATest(unittest.TestCase): """Tests for acme.jose.jwk.JWKRSA.""" + # pylint: disable=too-many-instance-attributes def setUp(self): from acme.jose.jwk import JWKRSA @@ -58,6 +73,8 @@ class JWKRSATest(unittest.TestCase): 'e': 'AQAB', 'n': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk', } + self.jwk256_comparable = JWKRSA(key=util.ComparableRSAKey( + RSA256_KEY.public_key())) self.jwk512 = JWKRSA(key=RSA512_KEY.public_key()) self.jwk512json = { 'kty': 'RSA', @@ -79,6 +96,10 @@ class JWKRSATest(unittest.TestCase): 'qi': 'oi45cEkbVoJjAbnQpFY87Q', }) + def test_init_comparable(self): + self.assertTrue(isinstance(self.jwk256.key, util.ComparableRSAKey)) + self.assertEqual(self.jwk256, self.jwk256_comparable) + def test_equals(self): self.assertEqual(self.jwk256, self.jwk256) self.assertEqual(self.jwk512, self.jwk512) diff --git a/acme/other.py b/acme/other.py index 5170d49cc..cf6425b7f 100644 --- a/acme/other.py +++ b/acme/other.py @@ -42,7 +42,7 @@ class Signature(jose.JSONObjectWithFields): :param key: Key used for signing. :type key: `cryptography.hazmat.primitives.assymetric.rsa.RSAPrivateKey` - wrapped in `.ComparableRSAKey`. + (optionally wrapped in `.ComparableRSAKey`). :param str nonce: Nonce to be used. If None, nonce of ``nonce_size`` will be randomly generated.