From 9b6fa7ab18702cdf6fab30093b2480f06ba50c55 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 24 Dec 2018 22:18:14 -0800 Subject: [PATCH 1/8] add ability to remove base crypto backend for tests commands_pre requires tox >= 3.4.0 --- tox.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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)" From 28dcc70df19046964d4964c4a8d6df20a754c4b5 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 24 Dec 2018 22:20:02 -0800 Subject: [PATCH 2/8] update Travis CI to run cryptography-only --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 From 92226735c475397c13e18ebfcf61d3442dad4824 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 24 Dec 2018 22:45:36 -0800 Subject: [PATCH 3/8] isolate ecdsa key class imports to allow for missing library --- jose/backends/cryptography_backend.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index 68047665..371ead7f 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -1,5 +1,9 @@ import six -import ecdsa + +try: + from ecdsa import SigningKey as EcdsaSigningKey, VerifyingKey as EcdsaVerifyingKey +except ImportError: + SigningKey = VerifyingKey = None from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der from jose.backends.base import Key @@ -37,7 +41,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') From 04ef8b580953892c73fef9e3139aa3eac7619ffb Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 25 Dec 2018 10:46:19 -0800 Subject: [PATCH 4/8] isolate asn1/der signature transformation to simplify testing --- jose/backends/cryptography_backend.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index 371ead7f..ee367ca8 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -94,19 +94,27 @@ def _process_jwk(self, jwk_dict): else: return public.public_key(self.cryptography_backend()) + def _der_to_asn1(self, der_signature): + """Convert signature from DER encoding to ASN1 encoding.""" + order = (2 ** self.prepared_key.curve.key_size) - 1 + return sigencode_string(*sigdecode_der(der_signature, order), order=order) + + def _asn1_to_der(self, asn1_signature): + """Convert signature from ASN1 encoding to DER encoding.""" + order = (2 ** self.prepared_key.curve.key_size) - 1 + return sigencode_der(*sigdecode_string(asn1_signature, order), order=order) + 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_asn1(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._asn1_to_der(sig) self.prepared_key.verify(signature, msg, ec.ECDSA(self.hash_alg())) return True except Exception: From cea6ae82f31c0fea863f2bc28a17ba31ef3e61cb Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 25 Dec 2018 10:57:55 -0800 Subject: [PATCH 5/8] add tests to verify der/asn1 conversions --- tests/algorithms/test_EC.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/algorithms/test_EC.py b/tests/algorithms/test_EC.py index 2f9b72a2..9fa6f0e6 100644 --- a/tests/algorithms/test_EC.py +++ b/tests/algorithms/test_EC.py @@ -31,6 +31,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" +) +ASN1_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 +63,20 @@ 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") +def test_cryptograhy_der_to_asn1(): + key = CryptographyECKey(private_key, ALGORITHMS.ES256) + assert key._der_to_asn1(DER_SIGNATURE) == ASN1_SIGNATURE + + +@pytest.mark.cryptography +@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available") +def test_cryptograhy_asn1_to_der(): + key = CryptographyECKey(private_key, ALGORITHMS.ES256) + assert key._asn1_to_der(ASN1_SIGNATURE) == DER_SIGNATURE + + class TestECAlgorithm: def test_key_from_pem(self): From e786a0e6ed9058fae97c6e3d33b87e790eeecdc1 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 25 Dec 2018 12:05:33 -0800 Subject: [PATCH 6/8] remove dependency of pyca/cryptography backend on python-ecdsa --- jose/backends/cryptography_backend.py | 32 ++++++++++++++++++++++----- tests/algorithms/test_EC.py | 25 ++++++++++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index ee367ca8..daf0b5b6 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -1,10 +1,13 @@ +from __future__ import division + +import math + import six try: from ecdsa import SigningKey as EcdsaSigningKey, VerifyingKey as EcdsaVerifyingKey except ImportError: - SigningKey = VerifyingKey = None -from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der + EcdsaSigningKey = EcdsaVerifyingKey = None from jose.backends.base import Key from jose.utils import base64_to_long, long_to_base64 @@ -15,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 @@ -94,15 +99,30 @@ 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 math.ceil(self.prepared_key.key_size / 8.0) + def _der_to_asn1(self, der_signature): """Convert signature from DER encoding to ASN1 encoding.""" - order = (2 ** self.prepared_key.curve.key_size) - 1 - return sigencode_string(*sigdecode_der(der_signature, order), order=order) + 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 _asn1_to_der(self, asn1_signature): """Convert signature from ASN1 encoding to DER encoding.""" - order = (2 ** self.prepared_key.curve.key_size) - 1 - return sigencode_der(*sigdecode_string(asn1_signature, order), order=order) + component_length = self._sig_component_length() + if len(asn1_signature) != int(2 * component_length): + raise ValueError("Invalid signature") + + r_bytes = asn1_signature[:component_length] + s_bytes = asn1_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: diff --git a/tests/algorithms/test_EC.py b/tests/algorithms/test_EC.py index 9fa6f0e6..afa6b295 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 @@ -63,6 +65,27 @@ 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_asn1(): From 93a3c0a2bef7c9fee6fddf8e3cb4cbc9abd94326 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 25 Dec 2018 12:17:59 -0800 Subject: [PATCH 7/8] JOSE ECDSA signature encoding is raw encoding, not ASN1 --- jose/backends/cryptography_backend.py | 18 +++++++++--------- tests/algorithms/test_EC.py | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index daf0b5b6..b97f68d2 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -106,20 +106,20 @@ def _sig_component_length(self): """ return math.ceil(self.prepared_key.key_size / 8.0) - def _der_to_asn1(self, der_signature): - """Convert signature from DER encoding to ASN1 encoding.""" + 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 _asn1_to_der(self, asn1_signature): - """Convert signature from ASN1 encoding to DER encoding.""" + def _raw_to_der(self, raw_signature): + """Convert signature from RAW encoding to DER encoding.""" component_length = self._sig_component_length() - if len(asn1_signature) != int(2 * component_length): + if len(raw_signature) != int(2 * component_length): raise ValueError("Invalid signature") - r_bytes = asn1_signature[:component_length] - s_bytes = asn1_signature[component_length:] + 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) @@ -130,11 +130,11 @@ def sign(self, msg): "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())) - return self._der_to_asn1(signature) + return self._der_to_raw(signature) def verify(self, msg, sig): try: - signature = self._asn1_to_der(sig) + 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 afa6b295..7f012afb 100644 --- a/tests/algorithms/test_EC.py +++ b/tests/algorithms/test_EC.py @@ -39,7 +39,7 @@ 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" ) -ASN1_SIGNATURE = ( +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" @@ -88,16 +88,16 @@ def test_cryptography_sig_component_length(algorithm, expected_length): @pytest.mark.cryptography @pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available") -def test_cryptograhy_der_to_asn1(): +def test_cryptograhy_der_to_raw(): key = CryptographyECKey(private_key, ALGORITHMS.ES256) - assert key._der_to_asn1(DER_SIGNATURE) == ASN1_SIGNATURE + 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_asn1_to_der(): +def test_cryptograhy_raw_to_der(): key = CryptographyECKey(private_key, ALGORITHMS.ES256) - assert key._asn1_to_der(ASN1_SIGNATURE) == DER_SIGNATURE + assert key._raw_to_der(RAW_SIGNATURE) == DER_SIGNATURE class TestECAlgorithm: From e2189cb766de250644716006f75da8035d3523ed Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 25 Dec 2018 12:18:38 -0800 Subject: [PATCH 8/8] math.ceil returns a float in Python 2 --- jose/backends/cryptography_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index b97f68d2..b9bdc0dc 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -104,7 +104,7 @@ def _sig_component_length(self): This is the number of bytes required to encode the maximum key value. """ - return math.ceil(self.prepared_key.key_size / 8.0) + 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."""