Skip to content

Commit ff8a03b

Browse files
authored
Merge pull request #117 from mattsb42-aws/no-ecdsa
Remove pyca/cryptography backend's dependency on python-ecdsa
2 parents b91fe07 + e2189cb commit ff8a03b

File tree

4 files changed

+101
-14
lines changed

4 files changed

+101
-14
lines changed

.travis.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ matrix:
1414
- python: 2.7
1515
env: TOXENV=py27-base
1616
- python: 2.7
17-
env: TOXENV=py27-cryptography
17+
env: TOXENV=py27-cryptography-only
1818
- python: 2.7
1919
env: TOXENV=py27-pycryptodome
2020
- python: 2.7
@@ -25,7 +25,7 @@ matrix:
2525
- python: 3.4
2626
env: TOXENV=py34-base
2727
- python: 3.4
28-
env: TOXENV=py34-cryptography
28+
env: TOXENV=py34-cryptography-only
2929
- python: 3.4
3030
env: TOXENV=py34-pycryptodome
3131
- python: 3.4
@@ -36,7 +36,7 @@ matrix:
3636
- python: 3.5
3737
env: TOXENV=py35-base
3838
- python: 3.5
39-
env: TOXENV=py35-cryptography
39+
env: TOXENV=py35-cryptography-only
4040
- python: 3.5
4141
env: TOXENV=py35-pycryptodome
4242
- python: 3.5
@@ -47,7 +47,7 @@ matrix:
4747
- python: 3.5
4848
env: TOXENV=py35-base
4949
- python: 3.5
50-
env: TOXENV=py35-cryptography
50+
env: TOXENV=py35-cryptography-only
5151
- python: 3.5
5252
env: TOXENV=py35-pycryptodome
5353
- python: 3.5
@@ -58,7 +58,7 @@ matrix:
5858
- python: pypy-5.3.1
5959
env: TOXENV=pypy-base
6060
- python: pypy-5.3.1
61-
env: TOXENV=pypy-cryptography
61+
env: TOXENV=pypy-cryptography-only
6262
- python: pypy-5.3.1
6363
env: TOXENV=pypy-pycryptodome
6464
- python: pypy-5.3.1

jose/backends/cryptography_backend.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
from __future__ import division
2+
3+
import math
4+
15
import six
2-
import ecdsa
3-
from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der
6+
7+
try:
8+
from ecdsa import SigningKey as EcdsaSigningKey, VerifyingKey as EcdsaVerifyingKey
9+
except ImportError:
10+
EcdsaSigningKey = EcdsaVerifyingKey = None
411

512
from jose.backends.base import Key
613
from jose.utils import base64_to_long, long_to_base64
@@ -11,7 +18,9 @@
1118
from cryptography.hazmat.backends import default_backend
1219
from cryptography.hazmat.primitives import hashes, serialization
1320
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding
21+
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
1422
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
23+
from cryptography.utils import int_from_bytes, int_to_bytes
1524
from cryptography.x509 import load_pem_x509_certificate
1625

1726

@@ -37,7 +46,7 @@ def __init__(self, key, algorithm, cryptography_backend=default_backend):
3746
self.prepared_key = key
3847
return
3948

40-
if isinstance(key, (ecdsa.SigningKey, ecdsa.VerifyingKey)):
49+
if None not in (EcdsaSigningKey, EcdsaVerifyingKey) and isinstance(key, (EcdsaSigningKey, EcdsaVerifyingKey)):
4150
# convert to PEM and let cryptography below load it as PEM
4251
key = key.to_pem().decode('utf-8')
4352

@@ -90,19 +99,42 @@ def _process_jwk(self, jwk_dict):
9099
else:
91100
return public.public_key(self.cryptography_backend())
92101

102+
def _sig_component_length(self):
103+
"""Determine the correct serialization length for an encoded signature component.
104+
105+
This is the number of bytes required to encode the maximum key value.
106+
"""
107+
return int(math.ceil(self.prepared_key.key_size / 8.0))
108+
109+
def _der_to_raw(self, der_signature):
110+
"""Convert signature from DER encoding to RAW encoding."""
111+
r, s = decode_dss_signature(der_signature)
112+
component_length = self._sig_component_length()
113+
return int_to_bytes(r, component_length) + int_to_bytes(s, component_length)
114+
115+
def _raw_to_der(self, raw_signature):
116+
"""Convert signature from RAW encoding to DER encoding."""
117+
component_length = self._sig_component_length()
118+
if len(raw_signature) != int(2 * component_length):
119+
raise ValueError("Invalid signature")
120+
121+
r_bytes = raw_signature[:component_length]
122+
s_bytes = raw_signature[component_length:]
123+
r = int_from_bytes(r_bytes, "big")
124+
s = int_from_bytes(s_bytes, "big")
125+
return encode_dss_signature(r, s)
126+
93127
def sign(self, msg):
94128
if self.hash_alg.digest_size * 8 > self.prepared_key.curve.key_size:
95129
raise TypeError("this curve (%s) is too short "
96130
"for your digest (%d)" % (self.prepared_key.curve.name,
97131
8 * self.hash_alg.digest_size))
98132
signature = self.prepared_key.sign(msg, ec.ECDSA(self.hash_alg()))
99-
order = (2 ** self.prepared_key.curve.key_size) - 1
100-
return sigencode_string(*sigdecode_der(signature, order), order=order)
133+
return self._der_to_raw(signature)
101134

102135
def verify(self, msg, sig):
103-
order = (2 ** self.prepared_key.curve.key_size) - 1
104-
signature = sigencode_der(*sigdecode_string(sig, order), order=order)
105136
try:
137+
signature = self._raw_to_der(sig)
106138
self.prepared_key.verify(signature, msg, ec.ECDSA(self.hash_alg()))
107139
return True
108140
except Exception:

tests/algorithms/test_EC.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
try:
1313
from jose.backends.cryptography_backend import CryptographyECKey
14+
from cryptography.hazmat.primitives.asymmetric import ec as CryptographyEc
15+
from cryptography.hazmat.backends import default_backend as CryptographyBackend
1416
except ImportError:
15-
CryptographyECKey = None
17+
CryptographyECKey = CryptographyEc = CryptographyBackend = None
1618

1719
import pytest
1820

@@ -31,6 +33,18 @@
3133
-----END EC PRIVATE KEY-----
3234
"""
3335

36+
# ES256 signatures generated to test conversion logic
37+
DER_SIGNATURE = (
38+
b"0F\x02!\x00\x89yG\x81W\x01\x11\x9b0\x08\xa4\xd0\xe3g([\x07\xb5\x01\xb3"
39+
b"\x9d\xdf \xd1\xbc\xedK\x01\x87:}\xf2\x02!\x00\xb2shTA\x00\x1a\x13~\xba"
40+
b"J\xdb\xeem\x12\x1e\xfeMO\x04\xb2[\x86A\xbd\xc6hu\x953X\x1e"
41+
)
42+
RAW_SIGNATURE = (
43+
b"\x89yG\x81W\x01\x11\x9b0\x08\xa4\xd0\xe3g([\x07\xb5\x01\xb3\x9d\xdf "
44+
b"\xd1\xbc\xedK\x01\x87:}\xf2\xb2shTA\x00\x1a\x13~\xbaJ\xdb\xeem\x12\x1e"
45+
b"\xfeMO\x04\xb2[\x86A\xbd\xc6hu\x953X\x1e"
46+
)
47+
3448

3549
def _backend_exception_types():
3650
"""Build the backend exception types based on available backends."""
@@ -51,6 +65,41 @@ def test_key_from_ecdsa():
5165
assert not ECKey(key, ALGORITHMS.ES256).is_public()
5266

5367

68+
@pytest.mark.cryptography
69+
@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available")
70+
@pytest.mark.parametrize("algorithm, expected_length", (
71+
(ALGORITHMS.ES256, 32),
72+
(ALGORITHMS.ES384, 48),
73+
(ALGORITHMS.ES512, 66)
74+
))
75+
def test_cryptography_sig_component_length(algorithm, expected_length):
76+
# Put mapping inside here to avoid more complex handling for test runs that do not have pyca/cryptography
77+
mapping = {
78+
ALGORITHMS.ES256: CryptographyEc.SECP256R1,
79+
ALGORITHMS.ES384: CryptographyEc.SECP384R1,
80+
ALGORITHMS.ES512: CryptographyEc.SECP521R1,
81+
}
82+
key = CryptographyECKey(
83+
CryptographyEc.generate_private_key(mapping[algorithm](), backend=CryptographyBackend()),
84+
algorithm
85+
)
86+
assert key._sig_component_length() == expected_length
87+
88+
89+
@pytest.mark.cryptography
90+
@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available")
91+
def test_cryptograhy_der_to_raw():
92+
key = CryptographyECKey(private_key, ALGORITHMS.ES256)
93+
assert key._der_to_raw(DER_SIGNATURE) == RAW_SIGNATURE
94+
95+
96+
@pytest.mark.cryptography
97+
@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available")
98+
def test_cryptograhy_raw_to_der():
99+
key = CryptographyECKey(private_key, ALGORITHMS.ES256)
100+
assert key._raw_to_der(RAW_SIGNATURE) == DER_SIGNATURE
101+
102+
54103
class TestECAlgorithm:
55104

56105
def test_key_from_pem(self):

tox.ini

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
[tox]
2-
envlist = py{27,34,35,36,py}-{base,cryptography,pycryptodome,pycrypto,compatibility},flake8
2+
minversion = 3.4.0
3+
envlist =
4+
py{27,34,35,36,py}-{base,cryptography-only,pycryptodome,pycrypto,compatibility},
5+
flake8
36
skip_missing_interpreters = True
47

58
[testenv:basecommand]
@@ -19,6 +22,9 @@ deps =
1922
pytest-cov
2023
pytest-runner
2124
compatibility: {[testenv:compatibility]deps}
25+
commands_pre =
26+
# Remove the python-rsa backend
27+
only: pip uninstall -y ecdsa rsa
2228
commands =
2329
# Test the python-rsa backend
2430
base: {[testenv:basecommand]commands} -m "not (cryptography or pycryptodome or pycrypto or backend_compatibility)"

0 commit comments

Comments
 (0)