diff --git a/.travis.yml b/.travis.yml index 39c2aba9..7dc5fb2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ matrix: - python: 2.7 env: TOXENV=py27-base - python: 2.7 - env: TOXENV=py27-cryptography + env: TOXENV=py27-cryptography-only - python: 2.7 env: TOXENV=py27-pycryptodome - python: 2.7 @@ -25,7 +25,7 @@ matrix: - python: 3.4 env: TOXENV=py34-base - python: 3.4 - env: TOXENV=py34-cryptography + env: TOXENV=py34-cryptography-only - python: 3.4 env: TOXENV=py34-pycryptodome - python: 3.4 @@ -36,7 +36,7 @@ matrix: - python: 3.5 env: TOXENV=py35-base - python: 3.5 - env: TOXENV=py35-cryptography + env: TOXENV=py35-cryptography-only - python: 3.5 env: TOXENV=py35-pycryptodome - python: 3.5 @@ -47,7 +47,7 @@ matrix: - python: 3.5 env: TOXENV=py35-base - python: 3.5 - env: TOXENV=py35-cryptography + env: TOXENV=py35-cryptography-only - python: 3.5 env: TOXENV=py35-pycryptodome - python: 3.5 @@ -58,7 +58,7 @@ matrix: - python: pypy-5.3.1 env: TOXENV=pypy-base - python: pypy-5.3.1 - env: TOXENV=pypy-cryptography + env: TOXENV=pypy-cryptography-only - python: pypy-5.3.1 env: TOXENV=pypy-pycryptodome - python: pypy-5.3.1 diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index 68047665..b9bdc0dc 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -1,6 +1,13 @@ +from __future__ import division + +import math + import six -import ecdsa -from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der + +try: + from ecdsa import SigningKey as EcdsaSigningKey, VerifyingKey as EcdsaVerifyingKey +except ImportError: + EcdsaSigningKey = EcdsaVerifyingKey = None from jose.backends.base import Key from jose.utils import base64_to_long, long_to_base64 @@ -11,7 +18,9 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key +from cryptography.utils import int_from_bytes, int_to_bytes from cryptography.x509 import load_pem_x509_certificate @@ -37,7 +46,7 @@ def __init__(self, key, algorithm, cryptography_backend=default_backend): self.prepared_key = key return - if isinstance(key, (ecdsa.SigningKey, ecdsa.VerifyingKey)): + if None not in (EcdsaSigningKey, EcdsaVerifyingKey) and isinstance(key, (EcdsaSigningKey, EcdsaVerifyingKey)): # convert to PEM and let cryptography below load it as PEM key = key.to_pem().decode('utf-8') @@ -90,19 +99,42 @@ def _process_jwk(self, jwk_dict): else: return public.public_key(self.cryptography_backend()) + def _sig_component_length(self): + """Determine the correct serialization length for an encoded signature component. + + This is the number of bytes required to encode the maximum key value. + """ + return int(math.ceil(self.prepared_key.key_size / 8.0)) + + def _der_to_raw(self, der_signature): + """Convert signature from DER encoding to RAW encoding.""" + r, s = decode_dss_signature(der_signature) + component_length = self._sig_component_length() + return int_to_bytes(r, component_length) + int_to_bytes(s, component_length) + + def _raw_to_der(self, raw_signature): + """Convert signature from RAW encoding to DER encoding.""" + component_length = self._sig_component_length() + if len(raw_signature) != int(2 * component_length): + raise ValueError("Invalid signature") + + r_bytes = raw_signature[:component_length] + s_bytes = raw_signature[component_length:] + r = int_from_bytes(r_bytes, "big") + s = int_from_bytes(s_bytes, "big") + return encode_dss_signature(r, s) + def sign(self, msg): if self.hash_alg.digest_size * 8 > self.prepared_key.curve.key_size: raise TypeError("this curve (%s) is too short " "for your digest (%d)" % (self.prepared_key.curve.name, 8 * self.hash_alg.digest_size)) signature = self.prepared_key.sign(msg, ec.ECDSA(self.hash_alg())) - order = (2 ** self.prepared_key.curve.key_size) - 1 - return sigencode_string(*sigdecode_der(signature, order), order=order) + return self._der_to_raw(signature) def verify(self, msg, sig): - order = (2 ** self.prepared_key.curve.key_size) - 1 - signature = sigencode_der(*sigdecode_string(sig, order), order=order) try: + signature = self._raw_to_der(sig) self.prepared_key.verify(signature, msg, ec.ECDSA(self.hash_alg())) return True except Exception: diff --git a/tests/algorithms/test_EC.py b/tests/algorithms/test_EC.py index 2f9b72a2..7f012afb 100644 --- a/tests/algorithms/test_EC.py +++ b/tests/algorithms/test_EC.py @@ -11,8 +11,10 @@ try: from jose.backends.cryptography_backend import CryptographyECKey + from cryptography.hazmat.primitives.asymmetric import ec as CryptographyEc + from cryptography.hazmat.backends import default_backend as CryptographyBackend except ImportError: - CryptographyECKey = None + CryptographyECKey = CryptographyEc = CryptographyBackend = None import pytest @@ -31,6 +33,18 @@ -----END EC PRIVATE KEY----- """ +# ES256 signatures generated to test conversion logic +DER_SIGNATURE = ( + b"0F\x02!\x00\x89yG\x81W\x01\x11\x9b0\x08\xa4\xd0\xe3g([\x07\xb5\x01\xb3" + b"\x9d\xdf \xd1\xbc\xedK\x01\x87:}\xf2\x02!\x00\xb2shTA\x00\x1a\x13~\xba" + b"J\xdb\xeem\x12\x1e\xfeMO\x04\xb2[\x86A\xbd\xc6hu\x953X\x1e" +) +RAW_SIGNATURE = ( + b"\x89yG\x81W\x01\x11\x9b0\x08\xa4\xd0\xe3g([\x07\xb5\x01\xb3\x9d\xdf " + b"\xd1\xbc\xedK\x01\x87:}\xf2\xb2shTA\x00\x1a\x13~\xbaJ\xdb\xeem\x12\x1e" + b"\xfeMO\x04\xb2[\x86A\xbd\xc6hu\x953X\x1e" +) + def _backend_exception_types(): """Build the backend exception types based on available backends.""" @@ -51,6 +65,41 @@ def test_key_from_ecdsa(): assert not ECKey(key, ALGORITHMS.ES256).is_public() +@pytest.mark.cryptography +@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available") +@pytest.mark.parametrize("algorithm, expected_length", ( + (ALGORITHMS.ES256, 32), + (ALGORITHMS.ES384, 48), + (ALGORITHMS.ES512, 66) +)) +def test_cryptography_sig_component_length(algorithm, expected_length): + # Put mapping inside here to avoid more complex handling for test runs that do not have pyca/cryptography + mapping = { + ALGORITHMS.ES256: CryptographyEc.SECP256R1, + ALGORITHMS.ES384: CryptographyEc.SECP384R1, + ALGORITHMS.ES512: CryptographyEc.SECP521R1, + } + key = CryptographyECKey( + CryptographyEc.generate_private_key(mapping[algorithm](), backend=CryptographyBackend()), + algorithm + ) + assert key._sig_component_length() == expected_length + + +@pytest.mark.cryptography +@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available") +def test_cryptograhy_der_to_raw(): + key = CryptographyECKey(private_key, ALGORITHMS.ES256) + assert key._der_to_raw(DER_SIGNATURE) == RAW_SIGNATURE + + +@pytest.mark.cryptography +@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available") +def test_cryptograhy_raw_to_der(): + key = CryptographyECKey(private_key, ALGORITHMS.ES256) + assert key._raw_to_der(RAW_SIGNATURE) == DER_SIGNATURE + + class TestECAlgorithm: def test_key_from_pem(self): diff --git a/tox.ini b/tox.ini index 605869d3..14bca5bc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,8 @@ [tox] -envlist = py{27,34,35,36,py}-{base,cryptography,pycryptodome,pycrypto,compatibility},flake8 +minversion = 3.4.0 +envlist = + py{27,34,35,36,py}-{base,cryptography-only,pycryptodome,pycrypto,compatibility}, + flake8 skip_missing_interpreters = True [testenv:basecommand] @@ -19,6 +22,9 @@ deps = pytest-cov pytest-runner compatibility: {[testenv:compatibility]deps} +commands_pre = + # Remove the python-rsa backend + only: pip uninstall -y ecdsa rsa commands = # Test the python-rsa backend base: {[testenv:basecommand]commands} -m "not (cryptography or pycryptodome or pycrypto or backend_compatibility)"