Skip to content

Commit 5d2e03f

Browse files
authored
Merge pull request #129 from mpdavis/backend-explicit-tests
Merge in backend-explicit-tests
2 parents cc710e0 + 64cb29f commit 5d2e03f

19 files changed

+1124
-246
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,12 @@ docs/_build/
5555

5656
# PyBuilder
5757
target/
58+
59+
# PyCharm
60+
.idea/
61+
62+
# PyEnv
63+
.python-version
64+
65+
# PyTest
66+
.pytest_cache/

.travis.yml

Lines changed: 105 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,114 @@
22
# detail: https://blog.travis-ci.com/2017-06-21-trusty-updates-2017-Q2-launch
33
dist: precise
44
language: python
5-
python:
6-
- "2.7"
7-
- "3.4"
8-
- "3.5"
9-
- "3.6"
10-
- "pypy-5.3.1"
115
install:
126
- pip install -U setuptools && pip install -U tox codecov tox-travis
137
script:
148
- tox
159
after_success:
1610
- codecov
17-
# matrix:
18-
# include:
19-
# - python: 3.6
20-
# env:
21-
# - TOX_ENV=flake8
22-
# script: tox -e $TOX_ENV
11+
matrix:
12+
include:
13+
# CPython 2.7
14+
- python: 2.7
15+
env: TOXENV=py27-base
16+
- python: 2.7
17+
env: TOXENV=py27-cryptography-only
18+
- python: 2.7
19+
env: TOXENV=py27-pycryptodome-norsa
20+
- python: 2.7
21+
env: TOXENV=py27-pycrypto-norsa
22+
- python: 2.7
23+
env: TOXENV=py27-compatibility
24+
# CPython 3.4
25+
- python: 3.4
26+
env: TOXENV=py34-base
27+
- python: 3.4
28+
env: TOXENV=py34-cryptography-only
29+
- python: 3.4
30+
env: TOXENV=py34-pycryptodome-norsa
31+
- python: 3.4
32+
env: TOXENV=py34-pycrypto-norsa
33+
- python: 3.4
34+
env: TOXENV=py34-compatibility
35+
# CPython 3.5
36+
- python: 3.5
37+
env: TOXENV=py35-base
38+
- python: 3.5
39+
env: TOXENV=py35-cryptography-only
40+
- python: 3.5
41+
env: TOXENV=py35-pycryptodome-norsa
42+
- python: 3.5
43+
env: TOXENV=py35-pycrypto-norsa
44+
- python: 3.5
45+
env: TOXENV=py35-compatibility
46+
# CPython 3.5
47+
- python: 3.5
48+
env: TOXENV=py35-base
49+
- python: 3.5
50+
env: TOXENV=py35-cryptography-only
51+
- python: 3.5
52+
env: TOXENV=py35-pycryptodome-norsa
53+
- python: 3.5
54+
env: TOXENV=py35-pycrypto-norsa
55+
- python: 3.5
56+
env: TOXENV=py35-compatibility
57+
# CPython 3.6
58+
- python: 3.6
59+
env: TOXENV=py35-base
60+
- python: 3.6
61+
env: TOXENV=py35-cryptography-only
62+
- python: 3.6
63+
env: TOXENV=py35-pycryptodome-norsa
64+
- python: 3.6
65+
env: TOXENV=py35-pycrypto-norsa
66+
- python: 3.6
67+
env: TOXENV=py35-compatibility
68+
# CPython 3.7
69+
# xenial + sudo are currently needed to get 3.7
70+
# https://github.com/travis-ci/travis-ci/issues/9815
71+
- python: 3.7
72+
env: TOXENV=py35-base
73+
dist: xenial
74+
sudo: true
75+
- python: 3.7
76+
env: TOXENV=py35-cryptography-only
77+
dist: xenial
78+
sudo: true
79+
- python: 3.7
80+
env: TOXENV=py35-pycryptodome-norsa
81+
dist: xenial
82+
sudo: true
83+
- python: 3.7
84+
env: TOXENV=py35-pycrypto-norsa
85+
dist: xenial
86+
sudo: true
87+
- python: 3.7
88+
env: TOXENV=py35-compatibility
89+
dist: xenial
90+
sudo: true
91+
# PyPy 5.3.1
92+
- python: pypy-5.3.1
93+
env: TOXENV=pypy-base
94+
- python: pypy-5.3.1
95+
env: TOXENV=pypy-cryptography-only
96+
- python: pypy-5.3.1
97+
env: TOXENV=pypy-pycryptodome-norsa
98+
- python: pypy-5.3.1
99+
env: TOXENV=pypy-pycrypto-norsa
100+
- python: pypy-5.3.1
101+
env: TOXENV=pypy-compatibility
102+
# PyPy 3.5 (5.10.1?)
103+
- python: pypy3.5
104+
env: TOXENV=pypy-base
105+
- python: pypy3.5
106+
env: TOXENV=pypy-cryptography-only
107+
- python: pypy3.5
108+
env: TOXENV=pypy-pycryptodome-norsa
109+
- python: pypy3.5
110+
env: TOXENV=pypy-pycrypto-norsa
111+
- python: pypy3.5
112+
env: TOXENV=pypy-compatibility
113+
# Linting
114+
- python: 3.6
115+
env: TOX_ENV=flake8

README.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ The crytography option is a good default.
4242
$ pip install python-jose[pycryptodome]
4343
$ pip install python-jose[pycrypto]
4444

45+
Due to complexities with setuptools, the ``python-rsa`` and ``python-ecdsa`` libraries are always installed.
46+
If you use one of the custom backends and would like to clean up unneeded dependencies,
47+
you can remove the following dependencies for each backend:
48+
49+
* ``cryptography``
50+
51+
* ``pip uninstall rsa ecdsa pyasn1``
52+
53+
* ``pycrypto`` or ``pycryptodome``
54+
55+
* ``pip uninstall rsa``
56+
57+
.. warning::
58+
59+
Uninstall carefully. Make sure that nothing else in your environment needs these
60+
libraries before uninstalling them.
61+
4562

4663
Usage
4764
-----

jose/backends/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11

22
try:
3-
from jose.backends.pycrypto_backend import RSAKey # noqa: F401
3+
from jose.backends.cryptography_backend import CryptographyRSAKey as RSAKey # noqa: F401
44
except ImportError:
55
try:
6-
from jose.backends.cryptography_backend import CryptographyRSAKey as RSAKey # noqa: F401
6+
from jose.backends.pycrypto_backend import RSAKey # noqa: F401
77
except ImportError:
88
from jose.backends.rsa_backend import RSAKey # noqa: F401
99

jose/backends/_asn1.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""ASN1 encoding helpers for converting between PKCS1 and PKCS8.
2+
3+
Required by rsa_backend and pycrypto_backend but not cryptography_backend.
4+
"""
5+
from pyasn1.codec.der import decoder, encoder
6+
from pyasn1.type import namedtype, univ
7+
8+
RSA_ENCRYPTION_ASN1_OID = "1.2.840.113549.1.1.1"
9+
10+
11+
class RsaAlgorithmIdentifier(univ.Sequence):
12+
"""ASN1 structure for recording RSA PrivateKeyAlgorithm identifiers."""
13+
componentType = namedtype.NamedTypes(
14+
namedtype.NamedType("rsaEncryption", univ.ObjectIdentifier()),
15+
namedtype.NamedType("parameters", univ.Null())
16+
)
17+
18+
19+
class PKCS8PrivateKey(univ.Sequence):
20+
"""ASN1 structure for recording PKCS8 private keys."""
21+
componentType = namedtype.NamedTypes(
22+
namedtype.NamedType("version", univ.Integer()),
23+
namedtype.NamedType("privateKeyAlgorithm", RsaAlgorithmIdentifier()),
24+
namedtype.NamedType("privateKey", univ.OctetString())
25+
)
26+
27+
28+
class PublicKeyInfo(univ.Sequence):
29+
"""ASN1 structure for recording PKCS8 public keys."""
30+
componentType = namedtype.NamedTypes(
31+
namedtype.NamedType("algorithm", RsaAlgorithmIdentifier()),
32+
namedtype.NamedType("publicKey", univ.BitString())
33+
)
34+
35+
36+
def rsa_private_key_pkcs8_to_pkcs1(pkcs8_key):
37+
"""Convert a PKCS8-encoded RSA private key to PKCS1."""
38+
decoded_values = decoder.decode(pkcs8_key, asn1Spec=PKCS8PrivateKey())
39+
40+
try:
41+
decoded_key = decoded_values[0]
42+
except IndexError:
43+
raise ValueError("Invalid private key encoding")
44+
45+
return decoded_key["privateKey"]
46+
47+
48+
def rsa_private_key_pkcs1_to_pkcs8(pkcs1_key):
49+
"""Convert a PKCS1-encoded RSA private key to PKCS8."""
50+
algorithm = RsaAlgorithmIdentifier()
51+
algorithm["rsaEncryption"] = RSA_ENCRYPTION_ASN1_OID
52+
53+
pkcs8_key = PKCS8PrivateKey()
54+
pkcs8_key["version"] = 0
55+
pkcs8_key["privateKeyAlgorithm"] = algorithm
56+
pkcs8_key["privateKey"] = pkcs1_key
57+
58+
return encoder.encode(pkcs8_key)
59+
60+
61+
def rsa_public_key_pkcs1_to_pkcs8(pkcs1_key):
62+
"""Convert a PKCS1-encoded RSA private key to PKCS8."""
63+
algorithm = RsaAlgorithmIdentifier()
64+
algorithm["rsaEncryption"] = RSA_ENCRYPTION_ASN1_OID
65+
66+
pkcs8_key = PublicKeyInfo()
67+
pkcs8_key["algorithm"] = algorithm
68+
pkcs8_key["publicKey"] = univ.BitString.fromOctetString(pkcs1_key)
69+
70+
return encoder.encode(pkcs8_key)
71+
72+
73+
def rsa_public_key_pkcs8_to_pkcs1(pkcs8_key):
74+
"""Convert a PKCS8-encoded RSA private key to PKCS1."""
75+
decoded_values = decoder.decode(pkcs8_key, asn1Spec=PublicKeyInfo())
76+
77+
try:
78+
decoded_key = decoded_values[0]
79+
except IndexError:
80+
raise ValueError("Invalid public key encoding.")
81+
82+
return decoded_key["publicKey"].asOctets()

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:

0 commit comments

Comments
 (0)