Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove pycrypto/dome dependency on python-rsa #121

Merged
merged 17 commits into from
Apr 7, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ matrix:
- python: 2.7
env: TOXENV=py27-cryptography-only
- python: 2.7
env: TOXENV=py27-pycryptodome
env: TOXENV=py27-pycryptodome-norsa
- python: 2.7
env: TOXENV=py27-pycrypto
env: TOXENV=py27-pycrypto-norsa
- python: 2.7
env: TOXENV=py27-compatibility
# CPython 3.4
Expand All @@ -27,9 +27,9 @@ matrix:
- python: 3.4
env: TOXENV=py34-cryptography-only
- python: 3.4
env: TOXENV=py34-pycryptodome
env: TOXENV=py34-pycryptodome-norsa
- python: 3.4
env: TOXENV=py34-pycrypto
env: TOXENV=py34-pycrypto-norsa
- python: 3.4
env: TOXENV=py34-compatibility
# CPython 3.5
Expand All @@ -38,9 +38,9 @@ matrix:
- python: 3.5
env: TOXENV=py35-cryptography-only
- python: 3.5
env: TOXENV=py35-pycryptodome
env: TOXENV=py35-pycryptodome-norsa
- python: 3.5
env: TOXENV=py35-pycrypto
env: TOXENV=py35-pycrypto-norsa
- python: 3.5
env: TOXENV=py35-compatibility
# CPython 3.5
Expand All @@ -49,9 +49,9 @@ matrix:
- python: 3.5
env: TOXENV=py35-cryptography-only
- python: 3.5
env: TOXENV=py35-pycryptodome
env: TOXENV=py35-pycryptodome-norsa
- python: 3.5
env: TOXENV=py35-pycrypto
env: TOXENV=py35-pycrypto-norsa
- python: 3.5
env: TOXENV=py35-compatibility
# PyPy 5.3.1
Expand All @@ -60,9 +60,9 @@ matrix:
- python: pypy-5.3.1
env: TOXENV=pypy-cryptography-only
- python: pypy-5.3.1
env: TOXENV=pypy-pycryptodome
env: TOXENV=pypy-pycryptodome-norsa
- python: pypy-5.3.1
env: TOXENV=pypy-pycrypto
env: TOXENV=pypy-pycrypto-norsa
- python: pypy-5.3.1
env: TOXENV=pypy-compatibility
# matrix:
Expand Down
17 changes: 17 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ The crytography option is a good default.
$ pip install python-jose[pycryptodome]
$ pip install python-jose[pycrypto]

Due to complexities with setuptools, the ``python-rsa`` and ``python-ecdsa`` libraries are always installed.
If you use one of the custom backends and would like to clean up unneeded dependencies,
you can remove the following dependencies for each backend:

* ``cryptography``

* ``pip uninstall rsa ecdsa pyasn1``

* ``pycrypto`` or ``pycryptodome``

* ``pip uninstall rsa``

.. warning::

Uninstall carefully. Make sure that nothing else in your environment needs these
libraries before uninstalling them.


Usage
-----
Expand Down
83 changes: 83 additions & 0 deletions jose/backends/_asn1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""ASN1 encoding helpers for converting between PKCS1 and PKCS8.

Required by rsa_backend and pycrypto_backend but not cryptography_backend.
"""
from pyasn1.codec.der import decoder, encoder
from pyasn1.error import PyAsn1Error
from pyasn1.type import namedtype, univ

RSA_ENCRYPTION_ASN1_OID = "1.2.840.113549.1.1.1"


class RsaAlgorithmIdentifier(univ.Sequence):
"""ASN1 structure for recording RSA PrivateKeyAlgorithm identifiers."""
componentType = namedtype.NamedTypes(
namedtype.NamedType("rsaEncryption", univ.ObjectIdentifier()),
namedtype.NamedType("parameters", univ.Null())
)


class PKCS8PrivateKey(univ.Sequence):
"""ASN1 structure for recording PKCS8 private keys."""
componentType = namedtype.NamedTypes(
namedtype.NamedType("version", univ.Integer()),
namedtype.NamedType("privateKeyAlgorithm", RsaAlgorithmIdentifier()),
namedtype.NamedType("privateKey", univ.OctetString())
)


class PublicKeyInfo(univ.Sequence):
"""ASN1 structure for recording PKCS8 public keys."""
componentType = namedtype.NamedTypes(
namedtype.NamedType("algorithm", RsaAlgorithmIdentifier()),
namedtype.NamedType("publicKey", univ.BitString())
)


def rsa_private_key_pkcs8_to_pkcs1(pkcs8_key):
"""Convert a PKCS8-encoded RSA private key to PKCS1."""
decoded_values = decoder.decode(pkcs8_key, asn1Spec=PKCS8PrivateKey())

try:
decoded_key = decoded_values[0]
except IndexError:
raise ValueError("Invalid private key encoding")

return decoded_key["privateKey"]


def rsa_private_key_pkcs1_to_pkcs8(pkcs1_key):
"""Convert a PKCS1-encoded RSA private key to PKCS8."""
algorithm = RsaAlgorithmIdentifier()
algorithm["rsaEncryption"] = RSA_ENCRYPTION_ASN1_OID

pkcs8_key = PKCS8PrivateKey()
pkcs8_key["version"] = 0
pkcs8_key["privateKeyAlgorithm"] = algorithm
pkcs8_key["privateKey"] = pkcs1_key

return encoder.encode(pkcs8_key)


def rsa_public_key_pkcs1_to_pkcs8(pkcs1_key):
"""Convert a PKCS1-encoded RSA private key to PKCS8."""
algorithm = RsaAlgorithmIdentifier()
algorithm["rsaEncryption"] = RSA_ENCRYPTION_ASN1_OID

pkcs8_key = PublicKeyInfo()
pkcs8_key["algorithm"] = algorithm
pkcs8_key["publicKey"] = univ.BitString.fromOctetString(pkcs1_key)

return encoder.encode(pkcs8_key)


def rsa_public_key_pkcs8_to_pkcs1(pkcs8_key):
"""Convert a PKCS8-encoded RSA private key to PKCS1."""
decoded_values = decoder.decode(pkcs8_key, asn1Spec=PublicKeyInfo())

try:
decoded_key = decoded_values[0]
except IndexError:
raise ValueError("Invalid public key encoding.")

return decoded_key["publicKey"].asOctets()
11 changes: 7 additions & 4 deletions jose/backends/pycrypto_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import Crypto.Hash.SHA384
import Crypto.Hash.SHA512

from Crypto.IO import PEM
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Util.asn1 import DerSequence

from jose.backends.base import Key
from jose.backends.rsa_backend import pem_to_spki
from jose.backends._asn1 import rsa_public_key_pkcs8_to_pkcs1
from jose.utils import base64_to_long, long_to_base64
from jose.constants import ALGORITHMS
from jose.exceptions import JWKError
Expand Down Expand Up @@ -152,11 +153,13 @@ def to_pem(self, pem_format='PKCS8'):
raise ValueError("Invalid pem format specified: %r" % (pem_format,))

if self.is_public():
pem = self.prepared_key.exportKey('PEM', pkcs=1)
# PyCrypto/dome always export public keys as PKCS8
if pkcs == 8:
pem = pem_to_spki(pem, fmt='PKCS8')
pem = self.prepared_key.exportKey('PEM')
else:
pem = pem_to_spki(pem, fmt='PKCS1')
pkcs8_der = self.prepared_key.exportKey('DER')
pkcs1_der = rsa_public_key_pkcs8_to_pkcs1(pkcs8_der)
pem = PEM.encode(pkcs1_der, 'RSA PUBLIC KEY').encode('utf-8')
return pem
else:
pem = self.prepared_key.exportKey('PEM', pkcs=pkcs)
Expand Down
70 changes: 10 additions & 60 deletions jose/backends/rsa_backend.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import binascii

import six
from pyasn1.codec.der import decoder, encoder
from pyasn1.error import PyAsn1Error
from pyasn1.type import namedtype, univ

import rsa as pyrsa
import rsa.pem as pyrsa_pem
from rsa.asn1 import OpenSSLPubKey, AsnPubKey, PubKeyHeader

from jose.backends.base import Key
from jose.backends._asn1 import (
rsa_private_key_pkcs1_to_pkcs8,
rsa_private_key_pkcs8_to_pkcs1,
rsa_public_key_pkcs1_to_pkcs8,
)
from jose.constants import ALGORITHMS
from jose.exceptions import JWKError
from jose.utils import base64_to_long, long_to_base64
Expand Down Expand Up @@ -114,48 +116,6 @@ def _legacy_private_key_pkcs8_to_pkcs1(pkcs8_key):
return pkcs8_key[len(LEGACY_INVALID_PKCS8_RSA_HEADER):]


class PKCS8RsaPrivateKeyAlgorithm(univ.Sequence):
"""ASN1 structure for recording RSA PrivateKeyAlgorithm identifiers."""
componentType = namedtype.NamedTypes(
namedtype.NamedType("rsaEncryption", univ.ObjectIdentifier()),
namedtype.NamedType("parameters", univ.Null())
)


class PKCS8PrivateKey(univ.Sequence):
"""ASN1 structure for recording PKCS8 private keys."""
componentType = namedtype.NamedTypes(
namedtype.NamedType("version", univ.Integer()),
namedtype.NamedType("privateKeyAlgorithm", PKCS8RsaPrivateKeyAlgorithm()),
namedtype.NamedType("privateKey", univ.OctetString())
)


def _private_key_pkcs8_to_pkcs1(pkcs8_key):
"""Convert a PKCS8-encoded RSA private key to PKCS1."""
decoded_values = decoder.decode(pkcs8_key, asn1Spec=PKCS8PrivateKey())

try:
decoded_key = decoded_values[0]
except IndexError:
raise ValueError("Invalid private key encoding")

return decoded_key["privateKey"]


def _private_key_pkcs1_to_pkcs8(pkcs1_key):
"""Convert a PKCS1-encoded RSA private key to PKCS8."""
algorithm = PKCS8RsaPrivateKeyAlgorithm()
algorithm["rsaEncryption"] = RSA_ENCRYPTION_ASN1_OID

pkcs8_key = PKCS8PrivateKey()
pkcs8_key["version"] = 0
pkcs8_key["privateKeyAlgorithm"] = algorithm
pkcs8_key["privateKey"] = pkcs1_key

return encoder.encode(pkcs8_key)


class RSAKey(Key):
SHA256 = 'SHA-256'
SHA384 = 'SHA-384'
Expand Down Expand Up @@ -196,7 +156,7 @@ def __init__(self, key, algorithm):
try:
der = pyrsa_pem.load_pem(key, b'PRIVATE KEY')
try:
pkcs1_key = _private_key_pkcs8_to_pkcs1(der)
pkcs1_key = rsa_private_key_pkcs8_to_pkcs1(der)
except PyAsn1Error:
# If the key was encoded using the old, invalid,
# encoding then pyasn1 will throw an error attempting
Expand Down Expand Up @@ -259,27 +219,17 @@ def to_pem(self, pem_format='PKCS8'):
if isinstance(self._prepared_key, pyrsa.PrivateKey):
der = self._prepared_key.save_pkcs1(format='DER')
if pem_format == 'PKCS8':
pkcs8_der = _private_key_pkcs1_to_pkcs8(der)
pkcs8_der = rsa_private_key_pkcs1_to_pkcs8(der)
pem = pyrsa_pem.save_pem(pkcs8_der, pem_marker='PRIVATE KEY')
elif pem_format == 'PKCS1':
pem = pyrsa_pem.save_pem(der, pem_marker='RSA PRIVATE KEY')
else:
raise ValueError("Invalid pem format specified: %r" % (pem_format,))
else:
if pem_format == 'PKCS8':
asn_key = AsnPubKey()
asn_key.setComponentByName('modulus', self._prepared_key.n)
asn_key.setComponentByName('publicExponent', self._prepared_key.e)
der = encoder.encode(asn_key)

header = PubKeyHeader()
header['oid'] = univ.ObjectIdentifier(RSA_ENCRYPTION_ASN1_OID)
pub_key = OpenSSLPubKey()
pub_key['header'] = header
pub_key['key'] = univ.BitString.fromOctetString(der)

der = encoder.encode(pub_key)
pem = pyrsa_pem.save_pem(der, pem_marker='PUBLIC KEY')
pkcs1_der = self._prepared_key.save_pkcs1(format="DER")
pkcs8_der = rsa_public_key_pkcs1_to_pkcs8(pkcs1_der)
pem = pyrsa_pem.save_pem(pkcs8_der, pem_marker='PUBLIC KEY')
elif pem_format == 'PKCS1':
der = self._prepared_key.save_pkcs1(format='DER')
pem = pyrsa_pem.save_pem(der, pem_marker='RSA PUBLIC KEY')
Expand Down
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ def get_packages(package):
]


pyasn1 = ['pyasn1']
extras_require = {
'cryptography': ['cryptography'],
'pycrypto': ['pycrypto >=2.6.0, <2.7.0'],
'pycryptodome': ['pycryptodome >=3.3.1, <4.0.0'],
'pycrypto': ['pycrypto >=2.6.0, <2.7.0'] + pyasn1,
'pycryptodome': ['pycryptodome >=3.3.1, <4.0.0'] + pyasn1,
}
legacy_backend_requires = ['ecdsa <1.0', 'rsa', 'pyasn1']
legacy_backend_requires = ['ecdsa <1.0', 'rsa'] + pyasn1
install_requires = ['six <2.0', 'future <1.0']

# TODO: work this into the extras selection instead.
Expand Down
Loading