Skip to content

Commit dd5d551

Browse files
authored
Merge pull request #121 from mattsb42-aws/asn-rsa
Remove pycrypto/dome dependency on python-rsa
2 parents 595dd71 + b169959 commit dd5d551

File tree

9 files changed

+387
-206
lines changed

9 files changed

+387
-206
lines changed

.travis.yml

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ matrix:
1616
- python: 2.7
1717
env: TOXENV=py27-cryptography-only
1818
- python: 2.7
19-
env: TOXENV=py27-pycryptodome
19+
env: TOXENV=py27-pycryptodome-norsa
2020
- python: 2.7
21-
env: TOXENV=py27-pycrypto
21+
env: TOXENV=py27-pycrypto-norsa
2222
- python: 2.7
2323
env: TOXENV=py27-compatibility
2424
# CPython 3.4
@@ -27,9 +27,9 @@ matrix:
2727
- python: 3.4
2828
env: TOXENV=py34-cryptography-only
2929
- python: 3.4
30-
env: TOXENV=py34-pycryptodome
30+
env: TOXENV=py34-pycryptodome-norsa
3131
- python: 3.4
32-
env: TOXENV=py34-pycrypto
32+
env: TOXENV=py34-pycrypto-norsa
3333
- python: 3.4
3434
env: TOXENV=py34-compatibility
3535
# CPython 3.5
@@ -38,9 +38,9 @@ matrix:
3838
- python: 3.5
3939
env: TOXENV=py35-cryptography-only
4040
- python: 3.5
41-
env: TOXENV=py35-pycryptodome
41+
env: TOXENV=py35-pycryptodome-norsa
4242
- python: 3.5
43-
env: TOXENV=py35-pycrypto
43+
env: TOXENV=py35-pycrypto-norsa
4444
- python: 3.5
4545
env: TOXENV=py35-compatibility
4646
# CPython 3.5
@@ -49,23 +49,67 @@ matrix:
4949
- python: 3.5
5050
env: TOXENV=py35-cryptography-only
5151
- python: 3.5
52-
env: TOXENV=py35-pycryptodome
52+
env: TOXENV=py35-pycryptodome-norsa
5353
- python: 3.5
54-
env: TOXENV=py35-pycrypto
54+
env: TOXENV=py35-pycrypto-norsa
5555
- python: 3.5
5656
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
5791
# PyPy 5.3.1
5892
- python: pypy-5.3.1
5993
env: TOXENV=pypy-base
6094
- python: pypy-5.3.1
6195
env: TOXENV=pypy-cryptography-only
6296
- python: pypy-5.3.1
63-
env: TOXENV=pypy-pycryptodome
97+
env: TOXENV=pypy-pycryptodome-norsa
6498
- python: pypy-5.3.1
65-
env: TOXENV=pypy-pycrypto
99+
env: TOXENV=pypy-pycrypto-norsa
66100
- python: pypy-5.3.1
67101
env: TOXENV=pypy-compatibility
68-
# matrix:
69-
# include:
70-
# - python: 3.6
71-
# env: TOX_ENV=flake8
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/_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/pycrypto_backend.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from base64 import b64encode
2+
13
import six
24

35
import Crypto.Hash.SHA256
@@ -9,7 +11,7 @@
911
from Crypto.Util.asn1 import DerSequence
1012

1113
from jose.backends.base import Key
12-
from jose.backends.rsa_backend import pem_to_spki
14+
from jose.backends._asn1 import rsa_public_key_pkcs8_to_pkcs1
1315
from jose.utils import base64_to_long, long_to_base64
1416
from jose.constants import ALGORITHMS
1517
from jose.exceptions import JWKError
@@ -25,6 +27,21 @@
2527
_RSAKey = RSA._RSAobj
2628

2729

30+
def _der_to_pem(der_key, marker):
31+
"""
32+
Perform a simple DER to PEM conversion.
33+
"""
34+
pem_key_chunks = [('-----BEGIN %s-----' % marker).encode('utf-8')]
35+
36+
# Limit base64 output lines to 64 characters by limiting input lines to 48 characters.
37+
for chunk_start in range(0, len(der_key), 48):
38+
pem_key_chunks.append(b64encode(der_key[chunk_start:chunk_start + 48]))
39+
40+
pem_key_chunks.append(('-----END %s-----' % marker).encode('utf-8'))
41+
42+
return b'\n'.join(pem_key_chunks)
43+
44+
2845
class RSAKey(Key):
2946
"""
3047
Performs signing and verification operations using
@@ -132,7 +149,7 @@ def sign(self, msg):
132149
def verify(self, msg, sig):
133150
try:
134151
return PKCS1_v1_5.new(self.prepared_key).verify(self.hash_alg.new(msg), sig)
135-
except Exception as e:
152+
except Exception:
136153
return False
137154

138155
def is_public(self):
@@ -152,11 +169,13 @@ def to_pem(self, pem_format='PKCS8'):
152169
raise ValueError("Invalid pem format specified: %r" % (pem_format,))
153170

154171
if self.is_public():
155-
pem = self.prepared_key.exportKey('PEM', pkcs=1)
172+
# PyCrypto/dome always export public keys as PKCS8
156173
if pkcs == 8:
157-
pem = pem_to_spki(pem, fmt='PKCS8')
174+
pem = self.prepared_key.exportKey('PEM')
158175
else:
159-
pem = pem_to_spki(pem, fmt='PKCS1')
176+
pkcs8_der = self.prepared_key.exportKey('DER')
177+
pkcs1_der = rsa_public_key_pkcs8_to_pkcs1(pkcs8_der)
178+
pem = _der_to_pem(pkcs1_der, 'RSA PUBLIC KEY')
160179
return pem
161180
else:
162181
pem = self.prepared_key.exportKey('PEM', pkcs=pkcs)

jose/backends/rsa_backend.py

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import binascii
22

33
import six
4-
from pyasn1.codec.der import decoder, encoder
54
from pyasn1.error import PyAsn1Error
6-
from pyasn1.type import namedtype, univ
75

86
import rsa as pyrsa
97
import rsa.pem as pyrsa_pem
10-
from rsa.asn1 import OpenSSLPubKey, AsnPubKey, PubKeyHeader
118

129
from jose.backends.base import Key
10+
from jose.backends._asn1 import (
11+
rsa_private_key_pkcs1_to_pkcs8,
12+
rsa_private_key_pkcs8_to_pkcs1,
13+
rsa_public_key_pkcs1_to_pkcs8,
14+
)
1315
from jose.constants import ALGORITHMS
1416
from jose.exceptions import JWKError
1517
from jose.utils import base64_to_long, long_to_base64
@@ -114,48 +116,6 @@ def _legacy_private_key_pkcs8_to_pkcs1(pkcs8_key):
114116
return pkcs8_key[len(LEGACY_INVALID_PKCS8_RSA_HEADER):]
115117

116118

117-
class PKCS8RsaPrivateKeyAlgorithm(univ.Sequence):
118-
"""ASN1 structure for recording RSA PrivateKeyAlgorithm identifiers."""
119-
componentType = namedtype.NamedTypes(
120-
namedtype.NamedType("rsaEncryption", univ.ObjectIdentifier()),
121-
namedtype.NamedType("parameters", univ.Null())
122-
)
123-
124-
125-
class PKCS8PrivateKey(univ.Sequence):
126-
"""ASN1 structure for recording PKCS8 private keys."""
127-
componentType = namedtype.NamedTypes(
128-
namedtype.NamedType("version", univ.Integer()),
129-
namedtype.NamedType("privateKeyAlgorithm", PKCS8RsaPrivateKeyAlgorithm()),
130-
namedtype.NamedType("privateKey", univ.OctetString())
131-
)
132-
133-
134-
def _private_key_pkcs8_to_pkcs1(pkcs8_key):
135-
"""Convert a PKCS8-encoded RSA private key to PKCS1."""
136-
decoded_values = decoder.decode(pkcs8_key, asn1Spec=PKCS8PrivateKey())
137-
138-
try:
139-
decoded_key = decoded_values[0]
140-
except IndexError:
141-
raise ValueError("Invalid private key encoding")
142-
143-
return decoded_key["privateKey"]
144-
145-
146-
def _private_key_pkcs1_to_pkcs8(pkcs1_key):
147-
"""Convert a PKCS1-encoded RSA private key to PKCS8."""
148-
algorithm = PKCS8RsaPrivateKeyAlgorithm()
149-
algorithm["rsaEncryption"] = RSA_ENCRYPTION_ASN1_OID
150-
151-
pkcs8_key = PKCS8PrivateKey()
152-
pkcs8_key["version"] = 0
153-
pkcs8_key["privateKeyAlgorithm"] = algorithm
154-
pkcs8_key["privateKey"] = pkcs1_key
155-
156-
return encoder.encode(pkcs8_key)
157-
158-
159119
class RSAKey(Key):
160120
SHA256 = 'SHA-256'
161121
SHA384 = 'SHA-384'
@@ -196,7 +156,7 @@ def __init__(self, key, algorithm):
196156
try:
197157
der = pyrsa_pem.load_pem(key, b'PRIVATE KEY')
198158
try:
199-
pkcs1_key = _private_key_pkcs8_to_pkcs1(der)
159+
pkcs1_key = rsa_private_key_pkcs8_to_pkcs1(der)
200160
except PyAsn1Error:
201161
# If the key was encoded using the old, invalid,
202162
# encoding then pyasn1 will throw an error attempting
@@ -259,27 +219,17 @@ def to_pem(self, pem_format='PKCS8'):
259219
if isinstance(self._prepared_key, pyrsa.PrivateKey):
260220
der = self._prepared_key.save_pkcs1(format='DER')
261221
if pem_format == 'PKCS8':
262-
pkcs8_der = _private_key_pkcs1_to_pkcs8(der)
222+
pkcs8_der = rsa_private_key_pkcs1_to_pkcs8(der)
263223
pem = pyrsa_pem.save_pem(pkcs8_der, pem_marker='PRIVATE KEY')
264224
elif pem_format == 'PKCS1':
265225
pem = pyrsa_pem.save_pem(der, pem_marker='RSA PRIVATE KEY')
266226
else:
267227
raise ValueError("Invalid pem format specified: %r" % (pem_format,))
268228
else:
269229
if pem_format == 'PKCS8':
270-
asn_key = AsnPubKey()
271-
asn_key.setComponentByName('modulus', self._prepared_key.n)
272-
asn_key.setComponentByName('publicExponent', self._prepared_key.e)
273-
der = encoder.encode(asn_key)
274-
275-
header = PubKeyHeader()
276-
header['oid'] = univ.ObjectIdentifier(RSA_ENCRYPTION_ASN1_OID)
277-
pub_key = OpenSSLPubKey()
278-
pub_key['header'] = header
279-
pub_key['key'] = univ.BitString.fromOctetString(der)
280-
281-
der = encoder.encode(pub_key)
282-
pem = pyrsa_pem.save_pem(der, pem_marker='PUBLIC KEY')
230+
pkcs1_der = self._prepared_key.save_pkcs1(format="DER")
231+
pkcs8_der = rsa_public_key_pkcs1_to_pkcs8(pkcs1_der)
232+
pem = pyrsa_pem.save_pem(pkcs8_der, pem_marker='PUBLIC KEY')
283233
elif pem_format == 'PKCS1':
284234
der = self._prepared_key.save_pkcs1(format='DER')
285235
pem = pyrsa_pem.save_pem(der, pem_marker='RSA PUBLIC KEY')

0 commit comments

Comments
 (0)