diff --git a/jose/backends/base.py b/jose/backends/base.py index 91a95781..37fc2ea3 100644 --- a/jose/backends/base.py +++ b/jose/backends/base.py @@ -16,3 +16,6 @@ def public_key(self): def to_pem(self): raise NotImplementedError() + + def to_dict(self): + raise NotImplementedError() diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index cbb638f5..5c35983e 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -3,7 +3,7 @@ from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der from jose.backends.base import Key -from jose.utils import base64_to_long +from jose.utils import base64_to_long, long_to_base64 from jose.constants import ALGORITHMS from jose.exceptions import JWKError @@ -68,19 +68,26 @@ def _process_jwk(self, jwk_dict): if not jwk_dict.get('kty') == 'EC': raise JWKError("Incorrect key type. Expected: 'EC', Recieved: %s" % jwk_dict.get('kty')) + if not all(k in jwk_dict for k in ['x', 'y', 'crv']): + raise JWKError('Mandatory parameters are missing') + x = base64_to_long(jwk_dict.get('x')) y = base64_to_long(jwk_dict.get('y')) - curve = { 'P-256': ec.SECP256R1, 'P-384': ec.SECP384R1, 'P-521': ec.SECP521R1, }[jwk_dict['crv']] - ec_pn = ec.EllipticCurvePublicNumbers(x, y, curve()) - verifying_key = ec_pn.public_key(self.cryptography_backend()) + public = ec.EllipticCurvePublicNumbers(x, y, curve()) - return verifying_key + if 'd' in jwk_dict: + d = base64_to_long(jwk_dict.get('d')) + private = ec.EllipticCurvePrivateNumbers(d, public) + + return private.private_key(self.cryptography_backend()) + else: + return public.public_key(self.cryptography_backend()) def sign(self, msg): if self.hash_alg.digest_size * 8 > self.prepared_key.curve.key_size: @@ -101,13 +108,16 @@ def verify(self, msg, sig): except: return False + def is_public(self): + return hasattr(self.prepared_key, 'public_bytes') + def public_key(self): - if hasattr(self.prepared_key, 'public_bytes'): + if self.is_public(): return self return self.__class__(self.prepared_key.public_key(), self._algorithm) def to_pem(self): - if hasattr(self.prepared_key, 'public_bytes'): + if self.is_public(): pem = self.prepared_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo @@ -120,6 +130,39 @@ def to_pem(self): ) return pem + def to_dict(self): + if not self.is_public(): + public_key = self.prepared_key.public_key() + else: + public_key = self.prepared_key + + crv = { + 'secp256r1': 'P-256', + 'secp384r1': 'P-384', + 'secp521r1': 'P-521', + }[self.prepared_key.curve.name] + + # Calculate the key size in bytes. Section 6.2.1.2 and 6.2.1.3 of + # RFC7518 prescribes that the 'x', 'y' and 'd' parameters of the curve + # points must be encoded as octed-strings of this length. + key_size = (self.prepared_key.curve.key_size + 7) // 8 + + data = { + 'alg': self._algorithm, + 'kty': 'EC', + 'crv': crv, + 'x': long_to_base64(public_key.public_numbers().x, size=key_size), + 'y': long_to_base64(public_key.public_numbers().y, size=key_size), + } + + if not self.is_public(): + data['d'] = long_to_base64( + self.prepared_key.private_numbers().private_value, + size=key_size + ) + + return data + class CryptographyRSAKey(Key): SHA256 = hashes.SHA256 @@ -170,17 +213,51 @@ def _process_jwk(self, jwk_dict): e = base64_to_long(jwk_dict.get('e', 256)) n = base64_to_long(jwk_dict.get('n')) - - verifying_key = rsa.RSAPublicNumbers(e, n).public_key(self.cryptography_backend()) - return verifying_key + public = rsa.RSAPublicNumbers(e, n) + + if 'd' not in jwk_dict: + return public.public_key(self.cryptography_backend()) + else: + # This is a private key. + d = base64_to_long(jwk_dict.get('d')) + + extra_params = ['p', 'q', 'dp', 'dq', 'qi'] + + if any(k in jwk_dict for k in extra_params): + # Precomputed private key parameters are available. + if not all(k in jwk_dict for k in extra_params): + # These values must be present when 'p' is according to + # Section 6.3.2 of RFC7518, so if they are not we raise + # an error. + raise JWKError('Precomputed private key parameters are incomplete.') + + p = base64_to_long(jwk_dict['p']) + q = base64_to_long(jwk_dict['q']) + dp = base64_to_long(jwk_dict['dp']) + dq = base64_to_long(jwk_dict['dq']) + qi = base64_to_long(jwk_dict['qi']) + else: + # The precomputed private key parameters are not available, + # so we use cryptography's API to fill them in. + p, q = rsa.rsa_recover_prime_factors(n, e, d) + dp = rsa.rsa_crt_dmp1(d, p) + dq = rsa.rsa_crt_dmq1(d, q) + qi = rsa.rsa_crt_iqmp(p, q) + + private = rsa.RSAPrivateNumbers(p, q, d, dp, dq, qi, public) + + return private.private_key(self.cryptography_backend()) def sign(self, msg): - signer = self.prepared_key.signer( - padding.PKCS1v15(), - self.hash_alg() - ) - signer.update(msg) - signature = signer.finalize() + try: + signer = self.prepared_key.signer( + padding.PKCS1v15(), + self.hash_alg() + ) + signer.update(msg) + signature = signer.finalize() + except Exception as e: + raise JWKError(e) return signature def verify(self, msg, sig): @@ -196,13 +273,16 @@ def verify(self, msg, sig): except InvalidSignature: return False + def is_public(self): + return hasattr(self.prepared_key, 'public_bytes') + def public_key(self): - if hasattr(self.prepared_key, 'public_bytes'): + if self.is_public(): return self return self.__class__(self.prepared_key.public_key(), self._algorithm) def to_pem(self): - if hasattr(self.prepared_key, 'public_bytes'): + if self.is_public(): return self.prepared_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo @@ -213,3 +293,28 @@ def to_pem(self): format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption() ) + + def to_dict(self): + if not self.is_public(): + public_key = self.prepared_key.public_key() + else: + public_key = self.prepared_key + + data = { + 'alg': self._algorithm, + 'kty': 'RSA', + 'n': long_to_base64(public_key.public_numbers().n), + 'e': long_to_base64(public_key.public_numbers().e), + } + + if not self.is_public(): + data.update({ + 'd': long_to_base64(self.prepared_key.private_numbers().d), + 'p': long_to_base64(self.prepared_key.private_numbers().p), + 'q': long_to_base64(self.prepared_key.private_numbers().q), + 'dp': long_to_base64(self.prepared_key.private_numbers().dmp1), + 'dq': long_to_base64(self.prepared_key.private_numbers().dmq1), + 'qi': long_to_base64(self.prepared_key.private_numbers().iqmp), + }) + + return data diff --git a/jose/backends/ecdsa_backend.py b/jose/backends/ecdsa_backend.py index 59a36fa2..91fb14a3 100644 --- a/jose/backends/ecdsa_backend.py +++ b/jose/backends/ecdsa_backend.py @@ -6,7 +6,7 @@ from jose.constants import ALGORITHMS from jose.exceptions import JWKError -from jose.utils import base64_to_long +from jose.utils import base64_to_long, long_to_base64 class ECDSAECKey(Key): @@ -72,16 +72,23 @@ def _process_jwk(self, jwk_dict): if not jwk_dict.get('kty') == 'EC': raise JWKError("Incorrect key type. Expected: 'EC', Recieved: %s" % jwk_dict.get('kty')) - x = base64_to_long(jwk_dict.get('x')) - y = base64_to_long(jwk_dict.get('y')) + if not all(k in jwk_dict for k in ['x', 'y', 'crv']): + raise JWKError('Mandatory parameters are missing') - if not ecdsa.ecdsa.point_is_valid(self.curve.generator, x, y): - raise JWKError("Point: %s, %s is not a valid point" % (x, y)) + if 'd' in jwk_dict: + # We are dealing with a private key; the secret exponent is enough + # to create an ecdsa key. + d = base64_to_long(jwk_dict.get('d')) + return ecdsa.keys.SigningKey.from_secret_exponent(d, self.curve) + else: + x = base64_to_long(jwk_dict.get('x')) + y = base64_to_long(jwk_dict.get('y')) - point = ecdsa.ellipticcurve.Point(self.curve.curve, x, y, self.curve.order) - verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point, self.curve) + if not ecdsa.ecdsa.point_is_valid(self.curve.generator, x, y): + raise JWKError("Point: %s, %s is not a valid point" % (x, y)) - return verifying_key + point = ecdsa.ellipticcurve.Point(self.curve.curve, x, y, self.curve.order) + return ecdsa.keys.VerifyingKey.from_public_point(point, self.curve) def sign(self, msg): return self.prepared_key.sign(msg, hashfunc=self.hash_alg, sigencode=ecdsa.util.sigencode_string) @@ -92,10 +99,46 @@ def verify(self, msg, sig): except: return False + def is_public(self): + return isinstance(self.prepared_key, ecdsa.VerifyingKey) + def public_key(self): - if isinstance(self.prepared_key, ecdsa.VerifyingKey): + if self.is_public(): return self return self.__class__(self.prepared_key.get_verifying_key(), self._algorithm) def to_pem(self): return self.prepared_key.to_pem() + + def to_dict(self): + if not self.is_public(): + public_key = self.prepared_key.get_verifying_key() + else: + public_key = self.prepared_key + + crv = { + ecdsa.curves.NIST256p: 'P-256', + ecdsa.curves.NIST384p: 'P-384', + ecdsa.curves.NIST521p: 'P-521', + }[self.prepared_key.curve] + + # Calculate the key size in bytes. Section 6.2.1.2 and 6.2.1.3 of + # RFC7518 prescribes that the 'x', 'y' and 'd' parameters of the curve + # points must be encoded as octed-strings of this length. + key_size = self.prepared_key.curve.baselen + + data = { + 'alg': self._algorithm, + 'kty': 'EC', + 'crv': crv, + 'x': long_to_base64(public_key.pubkey.point.x(), size=key_size), + 'y': long_to_base64(public_key.pubkey.point.y(), size=key_size), + } + + if not self.is_public(): + data['d'] = long_to_base64( + self.prepared_key.privkey.secret_multiplier, + size=key_size + ) + + return data diff --git a/jose/backends/pycrypto_backend.py b/jose/backends/pycrypto_backend.py index 87b64ee7..a888f3e2 100644 --- a/jose/backends/pycrypto_backend.py +++ b/jose/backends/pycrypto_backend.py @@ -9,7 +9,7 @@ from Crypto.Util.asn1 import DerSequence from jose.backends.base import Key -from jose.utils import base64_to_long +from jose.utils import base64_to_long, long_to_base64 from jose.constants import ALGORITHMS from jose.exceptions import JWKError from jose.utils import base64url_decode @@ -79,8 +79,35 @@ def _process_jwk(self, jwk_dict): e = base64_to_long(jwk_dict.get('e', 256)) n = base64_to_long(jwk_dict.get('n')) + params = (n, e) + + if 'd' in jwk_dict: + params += (base64_to_long(jwk_dict.get('d')),) + + extra_params = ['p', 'q', 'dp', 'dq', 'qi'] + + if any(k in jwk_dict for k in extra_params): + # Precomputed private key parameters are available. + if not all(k in jwk_dict for k in extra_params): + # These values must be present when 'p' is according to + # Section 6.3.2 of RFC7518, so if they are not we raise + # an error. + raise JWKError('Precomputed private key parameters are incomplete.') + + p = base64_to_long(jwk_dict.get('p')) + q = base64_to_long(jwk_dict.get('q')) + qi = base64_to_long(jwk_dict.get('qi')) + + # PyCrypto does not take the dp and dq as arguments, so we do + # not pass them. Furthermore, the parameter qi specified in + # the JWK is the inverse of q modulo p, whereas PyCrypto + # takes the inverse of p modulo q. We therefore switch the + # parameters to make the third parameter the inverse of the + # second parameter modulo the first parameter. + params += (q, p, qi) + + self.prepared_key = RSA.construct(params) - self.prepared_key = RSA.construct((n, e)) return self.prepared_key def _process_cert(self, key): @@ -105,8 +132,11 @@ def verify(self, msg, sig): except Exception as e: return False + def is_public(self): + return not self.prepared_key.has_private() + def public_key(self): - if not self.prepared_key.has_private(): + if self.is_public(): return self return self.__class__(self.prepared_key.publickey(), self._algorithm) @@ -121,3 +151,33 @@ def to_pem(self): if not pem.endswith(b'\n'): pem = pem + b'\n' return pem + + def to_dict(self): + data = { + 'alg': self._algorithm, + 'kty': 'RSA', + 'n': long_to_base64(self.prepared_key.n), + 'e': long_to_base64(self.prepared_key.e), + } + + if not self.is_public(): + # Section 6.3.2 of RFC7518 prescribes that when we include the + # optional parameters p and q, we must also include the values of + # dp and dq, which are not readily available from PyCrypto - so we + # calculate them. Moreover, PyCrypto stores the inverse of p + # modulo q rather than the inverse of q modulo p, so we switch + # p and q. As far as I can tell, this is OK - RFC7518 only + # asserts that p is the 'first factor', but does not specify + # what 'first' means in this case. + dp = self.prepared_key.d % (self.prepared_key.p - 1) + dq = self.prepared_key.d % (self.prepared_key.q - 1) + data.update({ + 'd': long_to_base64(self.prepared_key.d), + 'p': long_to_base64(self.prepared_key.q), + 'q': long_to_base64(self.prepared_key.p), + 'dp': long_to_base64(dq), + 'dq': long_to_base64(dp), + 'qi': long_to_base64(self.prepared_key.u), + }) + + return data diff --git a/jose/jwk.py b/jose/jwk.py index df807fff..03a318d2 100644 --- a/jose/jwk.py +++ b/jose/jwk.py @@ -5,7 +5,7 @@ from jose.constants import ALGORITHMS from jose.exceptions import JWKError -from jose.utils import base64url_decode +from jose.utils import base64url_decode, base64url_encode from jose.utils import constant_time_string_compare from jose.backends.base import Key @@ -90,6 +90,7 @@ class HMACKey(Key): def __init__(self, key, algorithm): if algorithm not in ALGORITHMS.HMAC: raise JWKError('hash_alg: %s is not a valid hash algorithm' % algorithm) + self._algorithm = algorithm self.hash_alg = get_algorithm_object(algorithm) if isinstance(key, dict): @@ -131,3 +132,10 @@ def sign(self, msg): def verify(self, msg, sig): return constant_time_string_compare(sig, self.sign(msg)) + + def to_dict(self): + return { + 'alg': self._algorithm, + 'kty': 'oct', + 'k': base64url_encode(self.prepared_key), + } diff --git a/jose/utils.py b/jose/utils.py index 29324424..2b98472c 100644 --- a/jose/utils.py +++ b/jose/utils.py @@ -5,12 +5,40 @@ import struct import sys -# Deal with integer compatibilities between Python 2 and 3. -# Using `from builtins import int` is not supported on AppEngine. if sys.version_info > (3,): + # Deal with integer compatibilities between Python 2 and 3. + # Using `from builtins import int` is not supported on AppEngine. long = int +# Piggyback of the backends implementation of the function that converts a long +# to a bytes stream. Some plumbing is necessary to have the signatures match. +try: + from Crypto.Util.number import long_to_bytes +except ImportError: + try: + from cryptography.utils import int_to_bytes as _long_to_bytes + + def long_to_bytes(n, blocksize=0): + return _long_to_bytes(n, blocksize or None) + + except ImportError: + from ecdsa.ecdsa import int_to_string as _long_to_bytes + + def long_to_bytes(n, blocksize=0): + ret = _long_to_bytes(n) + if blocksize == 0: + return ret + else: + assert len(ret) <= blocksize + padding = blocksize - len(ret) + return b'\x00' * padding + ret + + +def long_to_base64(data, size=0): + return base64.urlsafe_b64encode(long_to_bytes(data, size)).strip(b'=') + + def int_arr_to_long(arr): return long(''.join(["%02x" % byte for byte in arr]), 16) @@ -104,5 +132,3 @@ def constant_time_string_compare(a, b): result |= ord(x) ^ ord(y) return result == 0 - - diff --git a/tests/algorithms/test_EC.py b/tests/algorithms/test_EC.py index 8a12475a..02819bb4 100644 --- a/tests/algorithms/test_EC.py +++ b/tests/algorithms/test_EC.py @@ -9,47 +9,143 @@ import pytest private_key = """-----BEGIN EC PRIVATE KEY----- -MHQCAQEEIIAK499svJugZZfsTsgL2tc7kH/CpzQbkr4g55CEWQyPoAcGBSuBBAAK -oUQDQgAEsOnVqWVPfjte2nI0Ay3oTZVehCUtH66nJM8z6flUluHxhLG8ZTTCkJAZ -W6xQdXHfqGUy3Dx40NDhgTaM8xAdSw== +MHcCAQEEIOiSs10XnBlfykk5zsJRmzYybKdMlGniSJcssDvUcF6DoAoGCCqGSM49 +AwEHoUQDQgAE7gb4edKJ7ul9IgomCdcOebQTZ8qktqtBfRKboa71CfEKzBruUi+D +WkG0HJWIORlPbvXME+DRh6G/yVOKnTm88Q== -----END EC PRIVATE KEY-----""" class TestECAlgorithm: - def test_EC_key(self): + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_key_from_pem(self, Backend): + assert not Backend(private_key, ALGORITHMS.ES256).is_public() + + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_key_from_ecdsa(self, Backend): key = ecdsa.SigningKey.from_pem(private_key) - ECDSAECKey(key, ALGORITHMS.ES256) - CryptographyECKey(key, ALGORITHMS.ES256) + assert not Backend(key, ALGORITHMS.ES256).is_public() - ECDSAECKey(private_key, ALGORITHMS.ES256) - CryptographyECKey(private_key, ALGORITHMS.ES256) + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_to_pem(self, Backend): + key = Backend(private_key, ALGORITHMS.ES256) + assert not key.is_public() + assert key.to_pem().strip() == private_key.strip().encode('utf-8') - def test_string_secret(self): - key = 'secret' - with pytest.raises(JOSEError): - ECDSAECKey(key, ALGORITHMS.ES256) + public_pem = key.public_key().to_pem() + assert Backend(public_pem, ALGORITHMS.ES256).is_public() + + @pytest.mark.parametrize( + "Backend,ExceptionType", + [ + (ECDSAECKey, ecdsa.BadDigestError), + (CryptographyECKey, TypeError) + ] + ) + def test_key_too_short(self, Backend, ExceptionType): + priv_key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p).to_pem() + key = Backend(priv_key, ALGORITHMS.ES512) + with pytest.raises(ExceptionType): + key.sign(b'foo') + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_get_public_key(self, Backend): + key = Backend(private_key, ALGORITHMS.ES256) + pubkey = key.public_key() + pubkey2 = pubkey.public_key() + assert pubkey == pubkey2 + + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_string_secret(self, Backend): + key = 'secret' with pytest.raises(JOSEError): - CryptographyECKey(key, ALGORITHMS.ES256) + Backend(key, ALGORITHMS.ES256) - def test_object(self): + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_object(self, Backend): key = object() with pytest.raises(JOSEError): - ECDSAECKey(key, ALGORITHMS.ES256) + Backend(key, ALGORITHMS.ES256) - with pytest.raises(JOSEError): - CryptographyECKey(key, ALGORITHMS.ES256) + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_invalid_algorithm(self, Backend): + with pytest.raises(JWKError): + Backend(private_key, 'nonexistent') + + with pytest.raises(JWKError): + Backend({'kty': 'bla'}, ALGORITHMS.ES256) + + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_EC_jwk(self, Backend): + key = { + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv": "P-521", + "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", + "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1", + "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt", + } + + assert not Backend(key, ALGORITHMS.ES512).is_public() + + del key['d'] - def test_invalid_algorithm(self): + # We are now dealing with a public key. + assert Backend(key, ALGORITHMS.ES512).is_public() + + del key['x'] + + # This key is missing a required parameter. with pytest.raises(JWKError): - ECDSAECKey({'kty': 'bla'}, ALGORITHMS.ES256) + Backend(key, ALGORITHMS.ES512) - def test_verify(self): - key = ECDSAECKey(private_key, ALGORITHMS.ES256) + @pytest.mark.parametrize("Backend", [ECDSAECKey]) + def test_verify(self, Backend): + key = Backend(private_key, ALGORITHMS.ES256) msg = b'test' signature = key.sign(msg) public_key = key.public_key() assert public_key.verify(msg, signature) == True - assert public_key.verify(msg, b'not a signature') == False \ No newline at end of file + assert public_key.verify(msg, b'not a signature') == False + + def assert_parameters(self, as_dict, private): + assert isinstance(as_dict, dict) + + # Public parameters should always be there. + assert 'x' in as_dict + assert 'y' in as_dict + assert 'crv' in as_dict + + assert 'kty' in as_dict + assert as_dict['kty'] == 'EC' + + if private: + # Private parameters as well + assert 'd' in as_dict + + else: + # Private parameters should be absent + assert 'd' not in as_dict + + @pytest.mark.parametrize("Backend", [ECDSAECKey, CryptographyECKey]) + def test_to_dict(self, Backend): + key = Backend(private_key, ALGORITHMS.ES256) + self.assert_parameters(key.to_dict(), private=True) + self.assert_parameters(key.public_key().to_dict(), private=False) + + @pytest.mark.parametrize("BackendSign", [ECDSAECKey, CryptographyECKey]) + @pytest.mark.parametrize("BackendVerify", [ECDSAECKey, CryptographyECKey]) + def test_signing_parity(self, BackendSign, BackendVerify): + key_sign = BackendSign(private_key, ALGORITHMS.ES256) + key_verify = BackendVerify(private_key, ALGORITHMS.ES256).public_key() + + msg = b'test' + sig = key_sign.sign(msg) + + # valid signature + assert key_verify.verify(msg, sig) + + # invalid signature + assert not key_verify.verify(msg, b'n' * 64) diff --git a/tests/algorithms/test_HMAC.py b/tests/algorithms/test_HMAC.py index 314c7b3e..30e2714c 100644 --- a/tests/algorithms/test_HMAC.py +++ b/tests/algorithms/test_HMAC.py @@ -24,3 +24,18 @@ def test_RSA_key(self): key = "ssh-rsa" with pytest.raises(JOSEError): HMACKey(key, ALGORITHMS.HS256) + + def test_to_dict(self): + passphrase = 'The quick brown fox jumps over the lazy dog' + encoded = b'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw' + key = HMACKey(passphrase, ALGORITHMS.HS256) + + as_dict = key.to_dict() + assert 'alg' in as_dict + assert as_dict['alg'] == ALGORITHMS.HS256 + + assert 'kty' in as_dict + assert as_dict['kty'] == 'oct' + + assert 'k' in as_dict + assert as_dict['k'] == encoded diff --git a/tests/algorithms/test_RSA.py b/tests/algorithms/test_RSA.py index 5c222724..25582ff5 100644 --- a/tests/algorithms/test_RSA.py +++ b/tests/algorithms/test_RSA.py @@ -70,34 +70,15 @@ class TestRSAAlgorithm: - def test_RSA_key(self): - RSAKey(private_key, ALGORITHMS.RS256) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_RSA_key(self, Backend): + assert not Backend(private_key, ALGORITHMS.RS256).is_public() - def test_RSA_key_instance(self): + def test_pycrypto_RSA_key_instance(self): key = RSA.construct((long(26057131595212989515105618545799160306093557851986992545257129318694524535510983041068168825614868056510242030438003863929818932202262132630250203397069801217463517914103389095129323580576852108653940669240896817348477800490303630912852266209307160550655497615975529276169196271699168537716821419779900117025818140018436554173242441334827711966499484119233207097432165756707507563413323850255548329534279691658369466534587631102538061857114141268972476680597988266772849780811214198186940677291891818952682545840788356616771009013059992237747149380197028452160324144544057074406611859615973035412993832273216732343819), long(65537))) RSAKey(key, ALGORITHMS.RS256) - def test_string_secret(self): - key = 'secret' - with pytest.raises(JOSEError): - RSAKey(key, ALGORITHMS.RS256) - - def test_object(self): - key = object() - with pytest.raises(JOSEError): - RSAKey(key, ALGORITHMS.RS256) - - def test_bad_cert(self): - key = '-----BEGIN CERTIFICATE-----' - with pytest.raises(JOSEError): - RSAKey(key, ALGORITHMS.RS256) - - -class TestRSACryptography: - def test_RSA_key(self): - CryptographyRSAKey(private_key, ALGORITHMS.RS256) - - def test_RSA_key_instance(self): + def test_cryptography_RSA_key_instance(self): from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa @@ -107,81 +88,151 @@ def test_RSA_key_instance(self): ).public_key(default_backend()) pubkey = CryptographyRSAKey(key, ALGORITHMS.RS256) + assert pubkey.is_public() + pem = pubkey.to_pem() assert pem.startswith(b'-----BEGIN PUBLIC KEY-----') - def test_invalid_algorithm(self): + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_string_secret(self, Backend): + key = 'secret' + with pytest.raises(JOSEError): + Backend(key, ALGORITHMS.RS256) + + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_object(self, Backend): + key = object() + with pytest.raises(JOSEError): + Backend(key, ALGORITHMS.RS256) + + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_bad_cert(self, Backend): + key = '-----BEGIN CERTIFICATE-----' + with pytest.raises(JOSEError): + Backend(key, ALGORITHMS.RS256) + + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_invalid_algorithm(self, Backend): with pytest.raises(JWKError): - CryptographyRSAKey(private_key, ALGORITHMS.ES256) + Backend(private_key, ALGORITHMS.ES256) with pytest.raises(JWKError): - CryptographyRSAKey({'kty': 'bla'}, ALGORITHMS.RS256) + Backend({'kty': 'bla'}, ALGORITHMS.RS256) - def test_RSA_jwk(self): - d = { + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_RSA_jwk(self, Backend): + key = { "kty": "RSA", "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", "e": "AQAB", } - CryptographyRSAKey(d, ALGORITHMS.RS256) + assert Backend(key, ALGORITHMS.RS256).is_public() - def test_string_secret(self): - key = 'secret' - with pytest.raises(JOSEError): - CryptographyRSAKey(key, ALGORITHMS.RS256) + key = { + "kty": "RSA", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", + "e": "AQAB", + "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ", + "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k", + "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc", + "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX 59ehik", + "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8", + "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4" + } + assert not Backend(key, ALGORITHMS.RS256).is_public() - def test_object(self): - key = object() - with pytest.raises(JOSEError): - CryptographyRSAKey(key, ALGORITHMS.RS256) + del key['p'] - def test_bad_cert(self): - key = '-----BEGIN CERTIFICATE-----' - with pytest.raises(JOSEError): - CryptographyRSAKey(key, ALGORITHMS.RS256) + # Some but not all extra parameters are present + with pytest.raises(JWKError): + Backend(key, ALGORITHMS.RS256) - def test_get_public_key(self): - key = CryptographyRSAKey(private_key, ALGORITHMS.RS256) - public_key = key.public_key() - public_key2 = public_key.public_key() - assert public_key == public_key2 + del key['q'] + del key['dp'] + del key['dq'] + del key['qi'] + + # None of the extra parameters are present, but 'key' is still private. + assert not Backend(key, ALGORITHMS.RS256).is_public() + + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_string_secret(self, Backend): + key = 'secret' + with pytest.raises(JOSEError): + Backend(key, ALGORITHMS.RS256) - key = RSAKey(private_key, ALGORITHMS.RS256) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_get_public_key(self, Backend): + key = Backend(private_key, ALGORITHMS.RS256) public_key = key.public_key() public_key2 = public_key.public_key() + assert public_key.is_public() + assert public_key2.is_public() assert public_key == public_key2 - def test_to_pem(self): - key = CryptographyRSAKey(private_key, ALGORITHMS.RS256) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_to_pem(self, Backend): + key = Backend(private_key, ALGORITHMS.RS256) assert key.to_pem().strip() == private_key.strip() - key = RSAKey(private_key, ALGORITHMS.RS256) - assert key.to_pem().strip() == private_key.strip() - - def test_signing_parity(self): - key1 = RSAKey(private_key, ALGORITHMS.RS256) - vkey1 = key1.public_key() - key2 = CryptographyRSAKey(private_key, ALGORITHMS.RS256) - vkey2 = key2.public_key() + def assert_parameters(self, as_dict, private): + assert isinstance(as_dict, dict) + + # Public parameters should always be there. + assert 'n' in as_dict + assert 'e' in as_dict + + if private: + # Private parameters as well + assert 'd' in as_dict + assert 'p' in as_dict + assert 'q' in as_dict + assert 'dp' in as_dict + assert 'dq' in as_dict + assert 'qi' in as_dict + else: + # Private parameters should be absent + assert 'd' not in as_dict + assert 'p' not in as_dict + assert 'q' not in as_dict + assert 'dp' not in as_dict + assert 'dq' not in as_dict + assert 'qi' not in as_dict + + def assert_roundtrip(self, key, Backend): + assert Backend( + key.to_dict(), + ALGORITHMS.RS256 + ).to_dict() == key.to_dict() + + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_to_dict(self, Backend): + key = Backend(private_key, ALGORITHMS.RS256) + self.assert_parameters(key.to_dict(), private=True) + self.assert_parameters(key.public_key().to_dict(), private=False) + self.assert_roundtrip(key, Backend) + self.assert_roundtrip(key.public_key(), Backend) + + @pytest.mark.parametrize("BackendSign", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("BackendVerify", [RSAKey, CryptographyRSAKey]) + def test_signing_parity(self, BackendSign, BackendVerify): + key_sign = BackendSign(private_key, ALGORITHMS.RS256) + key_verify = BackendVerify(private_key, ALGORITHMS.RS256).public_key() msg = b'test' - sig1 = key1.sign(msg) - sig2 = key2.sign(msg) + sig = key_sign.sign(msg) - assert vkey1.verify(msg, sig1) - assert vkey1.verify(msg, sig2) - assert vkey2.verify(msg, sig1) - assert vkey2.verify(msg, sig2) + # valid signature + assert key_verify.verify(msg, sig) # invalid signature - assert not vkey2.verify(msg, b'n' * 64) + assert not key_verify.verify(msg, b'n' * 64) - def test_pycrypto_invalid_signature(self): + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + def test_pycrypto_unencoded_cleartext(self, Backend): + key = Backend(private_key, ALGORITHMS.RS256) - key = RSAKey(private_key, ALGORITHMS.RS256) - msg = b'test' - signature = key.sign(msg) - public_key = key.public_key() - - assert public_key.verify(msg, signature) == True - assert public_key.verify(msg, 1) == False + with pytest.raises(JWKError): + key.sign(True) diff --git a/tests/algorithms/test_cryptography_EC.py b/tests/algorithms/test_cryptography_EC.py deleted file mode 100644 index 37262e42..00000000 --- a/tests/algorithms/test_cryptography_EC.py +++ /dev/null @@ -1,77 +0,0 @@ - -from jose.constants import ALGORITHMS -from jose.exceptions import JOSEError, JWKError -from jose.backends.cryptography_backend import CryptographyECKey -from jose.backends.ecdsa_backend import ECDSAECKey - -import ecdsa -import pytest - -private_key = b"""-----BEGIN EC PRIVATE KEY----- -MHQCAQEEIIAK499svJugZZfsTsgL2tc7kH/CpzQbkr4g55CEWQyPoAcGBSuBBAAK -oUQDQgAEsOnVqWVPfjte2nI0Ay3oTZVehCUtH66nJM8z6flUluHxhLG8ZTTCkJAZ -W6xQdXHfqGUy3Dx40NDhgTaM8xAdSw== ------END EC PRIVATE KEY-----""" - - -class TestCryptographyECAlgorithm: - - def test_EC_key(self): - key = ecdsa.SigningKey.from_pem(private_key) - k = CryptographyECKey(key, ALGORITHMS.ES256) - - assert k.to_pem().strip() == private_key.strip() - public_pem = k.public_key().to_pem() - public_key = CryptographyECKey(public_pem, ALGORITHMS.ES256) - - def test_invalid_algorithm(self): - with pytest.raises(JWKError): - CryptographyECKey(private_key, 'nonexistent') - - with pytest.raises(JWKError): - CryptographyECKey({'kty': 'bla'}, ALGORITHMS.ES256) - - def test_key_too_short(self): - priv_key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p).to_pem() - key = CryptographyECKey(priv_key, ALGORITHMS.ES512) - with pytest.raises(TypeError): - key.sign('foo') - - def test_get_public_key(self): - key = CryptographyECKey(private_key, ALGORITHMS.ES256) - pubkey = key.public_key() - pubkey2 = pubkey.public_key() - assert pubkey == pubkey2 - - def test_string_secret(self): - key = 'secret' - with pytest.raises(JOSEError): - CryptographyECKey(key, ALGORITHMS.ES256) - - def test_object(self): - key = object() - with pytest.raises(JOSEError): - CryptographyECKey(key, ALGORITHMS.ES256) - - def test_cryptography_EC_key(self): - key = ecdsa.SigningKey.from_pem(private_key) - CryptographyECKey(key, ALGORITHMS.ES256) - - def test_signing_parity(self): - key1 = ECDSAECKey(private_key, ALGORITHMS.ES256) - public_key = key1.public_key().to_pem() - vkey1 = ECDSAECKey(public_key, ALGORITHMS.ES256) - key2 = CryptographyECKey(private_key, ALGORITHMS.ES256) - vkey2 = CryptographyECKey(public_key, ALGORITHMS.ES256) - - msg = b'test' - sig1 = key1.sign(msg) - sig2 = key2.sign(msg) - - assert vkey1.verify(msg, sig1) - assert vkey1.verify(msg, sig2) - assert vkey2.verify(msg, sig1) - assert vkey2.verify(msg, sig2) - - # invalid signature - assert not vkey2.verify(msg, b'n' * 64) diff --git a/tests/test_utils.py b/tests/test_utils.py index 48554de7..71ced1f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,3 +10,7 @@ def test_total_seconds(self): td = timedelta(seconds=5) assert utils.timedelta_total_seconds(td) == 5 + + def test_long_to_base64(self): + assert utils.long_to_base64(0xDEADBEEF) == b'3q2-7w' + assert utils.long_to_base64(0xCAFED00D, size=10) == b'AAAAAAAAyv7QDQ'