mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 19:02:52 +02:00
Drop M2Crypto
This commit is contained in:
@@ -44,20 +44,14 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# dpkg-dev: dpkg-architecture binary necessary to compile M2Crypto, c.f.
|
|
||||||
# #276, https://github.com/martinpaljak/M2Crypto/issues/62,
|
|
||||||
# M2Crypto setup.py:add_multiarch_paths
|
|
||||||
|
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
git-core \
|
git-core \
|
||||||
python \
|
python \
|
||||||
python-dev \
|
python-dev \
|
||||||
"$virtualenv" \
|
"$virtualenv" \
|
||||||
gcc \
|
gcc \
|
||||||
swig \
|
|
||||||
dialog \
|
dialog \
|
||||||
libaugeas0 \
|
libaugeas0 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
dpkg-dev \
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ yum install -y \
|
|||||||
python-virtualenv \
|
python-virtualenv \
|
||||||
python-devel \
|
python-devel \
|
||||||
gcc \
|
gcc \
|
||||||
swig \
|
|
||||||
dialog \
|
dialog \
|
||||||
augeas-libs \
|
augeas-libs \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
brew install augeas swig
|
brew install augeas
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ import mock
|
|||||||
# http://docs.readthedocs.org/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules
|
# http://docs.readthedocs.org/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules
|
||||||
# c.f. #262
|
# c.f. #262
|
||||||
sys.modules.update(
|
sys.modules.update(
|
||||||
(mod_name, mock.MagicMock()) for mod_name in ['augeas', 'M2Crypto'])
|
(mod_name, mock.MagicMock()) for mod_name in ['augeas'])
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ are provided mainly for the :ref:`developers <hacking>` reference.
|
|||||||
In general:
|
In general:
|
||||||
|
|
||||||
* ``sudo`` is required as a suggested way of running privileged process
|
* ``sudo`` is required as a suggested way of running privileged process
|
||||||
* `SWIG`_ is required for compiling `M2Crypto`_
|
|
||||||
* `Augeas`_ is required for the Python bindings
|
* `Augeas`_ is required for the Python bindings
|
||||||
|
|
||||||
|
|
||||||
@@ -102,14 +101,6 @@ Centos 7
|
|||||||
|
|
||||||
sudo ./bootstrap/centos.sh
|
sudo ./bootstrap/centos.sh
|
||||||
|
|
||||||
For installation run this modified command (note the trailing
|
|
||||||
backslash):
|
|
||||||
|
|
||||||
.. code-block:: shell
|
|
||||||
|
|
||||||
SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \
|
|
||||||
./venv/bin/pip install -r requirements.txt .
|
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
@@ -127,13 +118,6 @@ Installation
|
|||||||
your operating system and are **not supported** by the
|
your operating system and are **not supported** by the
|
||||||
Let's Encrypt team!
|
Let's Encrypt team!
|
||||||
|
|
||||||
.. note:: If your operating system uses SWIG 3.0.5+, you will need to
|
|
||||||
run ``pip install -r requirements-swig-3.0.5.txt -r
|
|
||||||
requirements.txt .`` instead. Known affected systems:
|
|
||||||
|
|
||||||
* Fedora 22
|
|
||||||
* some versions of Mac OS X
|
|
||||||
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
@@ -152,6 +136,4 @@ The ``letsencrypt`` commandline tool has a builtin help:
|
|||||||
|
|
||||||
|
|
||||||
.. _Augeas: http://augeas.net/
|
.. _Augeas: http://augeas.net/
|
||||||
.. _M2Crypto: https://github.com/M2Crypto/M2Crypto
|
|
||||||
.. _SWIG: http://www.swig.org/
|
|
||||||
.. _Virtualenv: https://virtualenv.pypa.io
|
.. _Virtualenv: https://virtualenv.pypa.io
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ class DVSNI(AnnotatedChallenge):
|
|||||||
"""
|
"""
|
||||||
response = challenges.DVSNIResponse(s=s)
|
response = challenges.DVSNIResponse(s=s)
|
||||||
cert_pem = crypto_util.make_ss_cert(self.key.pem, [
|
cert_pem = crypto_util.make_ss_cert(self.key.pem, [
|
||||||
self.nonce_domain, self.domain, response.z_domain(self.challb)])
|
# TODO: setting nonce_domain first will cause "Error:
|
||||||
|
# [('asn1 encoding routines', 'ASN1_mbstring_ncopy',
|
||||||
|
# 'string too long')]" (get_subject().CN = domains[0])
|
||||||
|
self.domain, self.nonce_domain, response.z_domain(self.challb)])
|
||||||
return cert_pem, response
|
return cert_pem, response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
import M2Crypto
|
|
||||||
import OpenSSL.crypto
|
import OpenSSL.crypto
|
||||||
import zope.component
|
import zope.component
|
||||||
|
|
||||||
@@ -435,8 +434,10 @@ def validate_key_csr(privkey, csr=None):
|
|||||||
|
|
||||||
if csr:
|
if csr:
|
||||||
if csr.form == "der":
|
if csr.form == "der":
|
||||||
csr_obj = M2Crypto.X509.load_request_der_string(csr.data)
|
csr_obj = OpenSSL.crypto.load_certificate_request(
|
||||||
csr = le_util.CSR(csr.file, csr_obj.as_pem(), "der")
|
OpenSSL.crypto.FILETYPE_ASN1, csr.data)
|
||||||
|
csr = le_util.CSR(csr.file, OpenSSL.crypto.dump_certificate(
|
||||||
|
OpenSSL.crypto.FILETYPE_PEM, csr_obj), "pem")
|
||||||
|
|
||||||
# If CSR is provided, it must be readable and valid.
|
# If CSR is provided, it must be readable and valid.
|
||||||
if csr.data and not crypto_util.valid_csr(csr.data):
|
if csr.data and not crypto_util.valid_csr(csr.data):
|
||||||
|
|||||||
+96
-62
@@ -4,13 +4,13 @@
|
|||||||
is capable of handling the signatures.
|
is capable of handling the signatures.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
|
|
||||||
import M2Crypto
|
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
|
|
||||||
|
from letsencrypt import errors
|
||||||
from letsencrypt import le_util
|
from letsencrypt import le_util
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ def init_save_csr(privkey, names, path, csrname="csr-letsencrypt.pem"):
|
|||||||
def make_csr(key_str, domains):
|
def make_csr(key_str, domains):
|
||||||
"""Generate a CSR.
|
"""Generate a CSR.
|
||||||
|
|
||||||
:param str key_str: RSA key.
|
:param str key_str: PEM-encoded RSA key.
|
||||||
:param list domains: Domains included in the certificate.
|
:param list domains: Domains included in the certificate.
|
||||||
|
|
||||||
.. todo:: Detect duplicates in `domains`? Using a set doesn't
|
.. todo:: Detect duplicates in `domains`? Using a set doesn't
|
||||||
@@ -97,25 +97,23 @@ def make_csr(key_str, domains):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
assert domains, "Must provide one or more hostnames for the CSR."
|
assert domains, "Must provide one or more hostnames for the CSR."
|
||||||
rsa_key = M2Crypto.RSA.load_key_string(key_str)
|
pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_str)
|
||||||
pubkey = M2Crypto.EVP.PKey()
|
req = OpenSSL.crypto.X509Req()
|
||||||
pubkey.assign_rsa(rsa_key)
|
req.get_subject().CN = domains[0]
|
||||||
|
# TODO: what to put into req.get_subject()?
|
||||||
csr = M2Crypto.X509.Request()
|
# TODO: put SAN if len(domains) > 1
|
||||||
csr.set_pubkey(pubkey)
|
req.add_extensions([
|
||||||
# TODO: what to put into csr.get_subject()?
|
OpenSSL.crypto.X509Extension(
|
||||||
|
"subjectAltName",
|
||||||
extstack = M2Crypto.X509.X509_Extension_Stack()
|
critical=False,
|
||||||
ext = M2Crypto.X509.new_extension(
|
value=", ".join("DNS:%s" % d for d in domains)
|
||||||
"subjectAltName", ", ".join("DNS:%s" % d for d in domains))
|
),
|
||||||
|
])
|
||||||
extstack.push(ext)
|
req.set_pubkey(pkey)
|
||||||
csr.add_extensions(extstack)
|
req.sign(pkey, "sha256")
|
||||||
csr.sign(pubkey, "sha256")
|
return tuple(OpenSSL.crypto.dump_certificate_request(method, req)
|
||||||
assert csr.verify(pubkey)
|
for method in (OpenSSL.crypto.FILETYPE_PEM,
|
||||||
pubkey2 = csr.get_pubkey()
|
OpenSSL.crypto.FILETYPE_ASN1))
|
||||||
assert csr.verify(pubkey2)
|
|
||||||
return csr.as_pem(), csr.as_der()
|
|
||||||
|
|
||||||
|
|
||||||
# WARNING: the csr and private key file are possible attack vectors for TOCTOU
|
# WARNING: the csr and private key file are possible attack vectors for TOCTOU
|
||||||
@@ -135,9 +133,11 @@ def valid_csr(csr):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
csr_obj = M2Crypto.X509.load_request_string(csr)
|
req = OpenSSL.crypto.load_certificate_request(
|
||||||
return bool(csr_obj.verify(csr_obj.get_pubkey()))
|
OpenSSL.crypto.FILETYPE_PEM, csr)
|
||||||
except M2Crypto.X509.X509Error:
|
return req.verify(req.get_pubkey())
|
||||||
|
except OpenSSL.crypto.Error as error:
|
||||||
|
logger.debug(error, exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -145,15 +145,20 @@ def csr_matches_pubkey(csr, privkey):
|
|||||||
"""Does private key correspond to the subject public key in the CSR?
|
"""Does private key correspond to the subject public key in the CSR?
|
||||||
|
|
||||||
:param str csr: CSR in PEM.
|
:param str csr: CSR in PEM.
|
||||||
:param str privkey: Private key file contents
|
:param str privkey: Private key file contents (PEM)
|
||||||
|
|
||||||
:returns: Correspondence of private key to CSR subject public key.
|
:returns: Correspondence of private key to CSR subject public key.
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
csr_obj = M2Crypto.X509.load_request_string(csr)
|
req = OpenSSL.crypto.load_certificate_request(
|
||||||
privkey_obj = M2Crypto.RSA.load_key_string(privkey)
|
OpenSSL.crypto.FILETYPE_PEM, csr)
|
||||||
return csr_obj.get_pubkey().get_rsa().pub() == privkey_obj.pub()
|
pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey)
|
||||||
|
try:
|
||||||
|
return req.verify(pkey)
|
||||||
|
except OpenSSL.crypto.Error as error:
|
||||||
|
logger.debug(error, exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def make_key(bits):
|
def make_key(bits):
|
||||||
@@ -174,18 +179,39 @@ def make_key(bits):
|
|||||||
def valid_privkey(privkey):
|
def valid_privkey(privkey):
|
||||||
"""Is valid RSA private key?
|
"""Is valid RSA private key?
|
||||||
|
|
||||||
:param str privkey: Private key file contents
|
:param str privkey: Private key file contents in PEM
|
||||||
|
|
||||||
:returns: Validity of private key.
|
:returns: Validity of private key.
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return bool(M2Crypto.RSA.load_key_string(privkey).check_key())
|
return OpenSSL.crypto.load_privatekey(
|
||||||
except M2Crypto.RSA.RSAError:
|
OpenSSL.crypto.FILETYPE_PEM, privkey).check()
|
||||||
|
except (TypeError, OpenSSL.crypto.Error):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _pyopenssl_load(data, method, types=(
|
||||||
|
OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)):
|
||||||
|
openssl_errors = []
|
||||||
|
for filetype in types:
|
||||||
|
try:
|
||||||
|
return method(filetype, data), filetype
|
||||||
|
except OpenSSL.crypto.Error as error: # TODO: anything else?
|
||||||
|
openssl_errors.append(error)
|
||||||
|
raise errors.Error("Unable to load: {0}".format(",".join(
|
||||||
|
str(error) for error in openssl_errors)))
|
||||||
|
|
||||||
|
def pyopenssl_load_certificate(data):
|
||||||
|
"""Load PEM/DER certificate.
|
||||||
|
|
||||||
|
:raises errors.Error:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return _pyopenssl_load(data, OpenSSL.crypto.load_certificate)
|
||||||
|
|
||||||
|
|
||||||
def make_ss_cert(key_str, domains, not_before=None,
|
def make_ss_cert(key_str, domains, not_before=None,
|
||||||
validity=(7 * 24 * 60 * 60)):
|
validity=(7 * 24 * 60 * 60)):
|
||||||
"""Returns new self-signed cert in PEM form.
|
"""Returns new self-signed cert in PEM form.
|
||||||
@@ -193,44 +219,36 @@ def make_ss_cert(key_str, domains, not_before=None,
|
|||||||
Uses key_str and contains all domains.
|
Uses key_str and contains all domains.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert domains, "Must provide one or more hostnames for the CSR."
|
assert domains, "Must provide one or more hostnames for the cert."
|
||||||
|
pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_str)
|
||||||
rsa_key = M2Crypto.RSA.load_key_string(key_str)
|
cert = OpenSSL.crypto.X509()
|
||||||
pubkey = M2Crypto.EVP.PKey()
|
|
||||||
pubkey.assign_rsa(rsa_key)
|
|
||||||
|
|
||||||
cert = M2Crypto.X509.X509()
|
|
||||||
cert.set_pubkey(pubkey)
|
|
||||||
cert.set_serial_number(1337)
|
cert.set_serial_number(1337)
|
||||||
cert.set_version(2)
|
cert.set_version(2)
|
||||||
|
|
||||||
current_ts = long(time.time() if not_before is None else not_before)
|
extensions = [
|
||||||
current = M2Crypto.ASN1.ASN1_UTCTIME()
|
OpenSSL.crypto.X509Extension(
|
||||||
current.set_time(current_ts)
|
"basicConstraints", True, 'CA:TRUE, pathlen:0'),
|
||||||
expire = M2Crypto.ASN1.ASN1_UTCTIME()
|
]
|
||||||
expire.set_time(current_ts + validity)
|
|
||||||
cert.set_not_before(current)
|
|
||||||
cert.set_not_after(expire)
|
|
||||||
|
|
||||||
subject = cert.get_subject()
|
cert.get_subject().CN = domains[0]
|
||||||
subject.C = "US"
|
# TODO: what to put into cert.get_subject()?
|
||||||
subject.ST = "Michigan"
|
|
||||||
subject.L = "Ann Arbor"
|
|
||||||
subject.O = "University of Michigan and the EFF"
|
|
||||||
subject.CN = domains[0]
|
|
||||||
cert.set_issuer(cert.get_subject())
|
cert.set_issuer(cert.get_subject())
|
||||||
|
|
||||||
if len(domains) > 1:
|
if len(domains) > 1:
|
||||||
cert.add_ext(M2Crypto.X509.new_extension(
|
extensions.append(OpenSSL.crypto.X509Extension(
|
||||||
"basicConstraints", "CA:FALSE"))
|
"subjectAltName",
|
||||||
cert.add_ext(M2Crypto.X509.new_extension(
|
critical=False,
|
||||||
"subjectAltName", ", ".join(["DNS:%s" % d for d in domains])))
|
value=", ".join("DNS:%s" % d for d in domains)
|
||||||
|
))
|
||||||
|
|
||||||
cert.sign(pubkey, "sha256")
|
cert.add_extensions(extensions)
|
||||||
assert cert.verify(pubkey)
|
|
||||||
assert cert.verify()
|
cert.gmtime_adj_notBefore(0 if not_before is None else not_before)
|
||||||
# print check_purpose(,0
|
cert.gmtime_adj_notAfter(validity)
|
||||||
return cert.as_pem()
|
|
||||||
|
cert.set_pubkey(pkey)
|
||||||
|
cert.sign(pkey, "sha256")
|
||||||
|
return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
|
||||||
|
|
||||||
|
|
||||||
def _pyopenssl_cert_or_req_san(cert_or_req):
|
def _pyopenssl_cert_or_req_san(cert_or_req):
|
||||||
@@ -308,3 +326,19 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM):
|
|||||||
"""
|
"""
|
||||||
return _get_sans_from_cert_or_req(
|
return _get_sans_from_cert_or_req(
|
||||||
csr, OpenSSL.crypto.load_certificate_request, typ)
|
csr, OpenSSL.crypto.load_certificate_request, typ)
|
||||||
|
|
||||||
|
|
||||||
|
def asn1_generalizedtime_to_dt(timestamp):
|
||||||
|
"""Convert ASN.1 GENERALIZEDTIME to datetime.
|
||||||
|
|
||||||
|
Useful for deserialization of `OpenSSL.crypto.X509.get_notAfter` and
|
||||||
|
`OpenSSL.crypto.X509.get_notAfter` outputs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: there are two other ts formats
|
||||||
|
return datetime.datetime.strptime(timestamp, '%Y%m%d%H%M%SZ')
|
||||||
|
|
||||||
|
|
||||||
|
def pyopenssl_x509_name_as_text(x509name):
|
||||||
|
"""Convert `OpenSSL.crypto.X509Name to text."""
|
||||||
|
return "/".join("{0}={1}" for key, value in x509name.get_components())
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ def separate_list_input(input_):
|
|||||||
"""
|
"""
|
||||||
no_commas = input_.replace(",", " ")
|
no_commas = input_.replace(",", " ")
|
||||||
# Each string is naturally unicode, this causes problems with M2Crypto SANs
|
# Each string is naturally unicode, this causes problems with M2Crypto SANs
|
||||||
|
# TODO: check if above is still true when M2Crypto is gone ^
|
||||||
return [str(string) for string in no_commas.split()]
|
return [str(string) for string in no_commas.split()]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Proof of Possession Identifier Validation Challenge."""
|
"""Proof of Possession Identifier Validation Challenge."""
|
||||||
import M2Crypto
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
import zope.component
|
import zope.component
|
||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
@@ -43,15 +45,21 @@ class ProofOfPossession(object): # pylint: disable=too-few-public-methods
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
for cert, key, _ in self.installer.get_all_certs_keys():
|
for cert, key, _ in self.installer.get_all_certs_keys():
|
||||||
pkey = M2Crypto.X509.load_cert(cert).get_pubkey()
|
with open(cert) as cert_file:
|
||||||
|
cert_data = cert_file.read()
|
||||||
try:
|
try:
|
||||||
rsa_pkey = pkey.get_rsa()
|
cert_obj = x509.load_pem_x509_certificate(
|
||||||
|
cert_data, default_backend())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warn("Only RSA supported at this time")
|
try:
|
||||||
continue
|
cert_obj = x509.load_der_x509_certificate(
|
||||||
pem_cert_key = rsa_pkey.as_pem()
|
cert_data, default_backend())
|
||||||
cert_key = achall.alg.kty.load(pem_cert_key)
|
except ValueError:
|
||||||
# TODO: If JWKES.load raises other exceptions, they should be caught here
|
logger.warn("Certificate is neither PER nor DER: %s", cert)
|
||||||
|
|
||||||
|
# TODO: only RSA is supported
|
||||||
|
cert_key = achall.alg.kty(key=jose.ComparableRSAKey(
|
||||||
|
cert_obj.public_key()))
|
||||||
if cert_key == achall.hints.jwk:
|
if cert_key == achall.hints.jwk:
|
||||||
return self._gen_response(achall, key)
|
return self._gen_response(achall, key)
|
||||||
|
|
||||||
|
|||||||
+41
-24
@@ -13,11 +13,11 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import M2Crypto
|
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
|
|
||||||
from acme.jose import util as jose_util
|
from acme.jose import util as jose_util
|
||||||
|
|
||||||
|
from letsencrypt import crypto_util
|
||||||
from letsencrypt import errors
|
from letsencrypt import errors
|
||||||
from letsencrypt import le_util
|
from letsencrypt import le_util
|
||||||
from letsencrypt import network
|
from letsencrypt import network
|
||||||
@@ -196,10 +196,15 @@ class Revoker(object):
|
|||||||
|
|
||||||
for (cert_path, _, path) in self.installer.get_all_certs_keys():
|
for (cert_path, _, path) in self.installer.get_all_certs_keys():
|
||||||
try:
|
try:
|
||||||
cert_sha1 = M2Crypto.X509.load_cert(
|
with open(cert_path) as cert_file:
|
||||||
cert_path).get_fingerprint(md="sha1")
|
cert_data = cert_file.read()
|
||||||
except (IOError, M2Crypto.X509.X509Error):
|
except IOError:
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
|
cert_obj, _ = crypto_util.pyopenssl_load_certificate(cert_data)
|
||||||
|
except errors.Error:
|
||||||
|
continue
|
||||||
|
cert_sha1 = cert_obj.digest("sha1")
|
||||||
if cert_sha1 in csha1_vhlist:
|
if cert_sha1 in csha1_vhlist:
|
||||||
csha1_vhlist[cert_sha1].append(path)
|
csha1_vhlist[cert_sha1].append(path)
|
||||||
else:
|
else:
|
||||||
@@ -246,7 +251,6 @@ class Revoker(object):
|
|||||||
"""
|
"""
|
||||||
# XXX | pylint: disable=unused-variable
|
# XXX | pylint: disable=unused-variable
|
||||||
|
|
||||||
# These will both have to change in the future away from M2Crypto
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
certificate = jose_util.ComparableX509(cert._cert)
|
certificate = jose_util.ComparableX509(cert._cert)
|
||||||
try:
|
try:
|
||||||
@@ -373,8 +377,8 @@ class Revoker(object):
|
|||||||
class Cert(object):
|
class Cert(object):
|
||||||
"""Cert object used for Revocation convenience.
|
"""Cert object used for Revocation convenience.
|
||||||
|
|
||||||
:ivar _cert: M2Crypto X509 cert
|
:ivar _cert: Certificate
|
||||||
:type _cert: :class:`M2Crypto.X509`
|
:type _cert: :class:`OpenSSL.crypto.X509`
|
||||||
|
|
||||||
:ivar int idx: convenience index used for listing
|
:ivar int idx: convenience index used for listing
|
||||||
:ivar orig: (`str` path - original certificate, `str` status)
|
:ivar orig: (`str` path - original certificate, `str` status)
|
||||||
@@ -402,8 +406,16 @@ class Cert(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self._cert = M2Crypto.X509.load_cert(cert_path)
|
with open(cert_path) as cert_file:
|
||||||
except (IOError, M2Crypto.X509.X509Error):
|
cert_data = cert_file.read()
|
||||||
|
except IOError:
|
||||||
|
raise errors.RevokerError(
|
||||||
|
"Error loading certificate: %s" % cert_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._cert = OpenSSL.crypto.load_certificate(
|
||||||
|
OpenSSL.crypto.FILETYPE_PEM, cert_data)
|
||||||
|
except OpenSSL.crypto.Error:
|
||||||
raise errors.RevokerError(
|
raise errors.RevokerError(
|
||||||
"Error loading certificate: %s" % cert_path)
|
"Error loading certificate: %s" % cert_path)
|
||||||
|
|
||||||
@@ -451,8 +463,11 @@ class Cert(object):
|
|||||||
if not os.path.isfile(orig):
|
if not os.path.isfile(orig):
|
||||||
status = Cert.DELETED_MSG
|
status = Cert.DELETED_MSG
|
||||||
else:
|
else:
|
||||||
o_cert = M2Crypto.X509.load_cert(orig)
|
with open(orig) as orig_file:
|
||||||
if self.get_fingerprint() != o_cert.get_fingerprint(md="sha1"):
|
orig_data = orig_file.read()
|
||||||
|
o_cert = OpenSSL.crypto.load_certificate(
|
||||||
|
OpenSSL.crypto.FILETYPE_PEM, orig_data)
|
||||||
|
if self.get_fingerprint() != o_cert.digest("sha1"):
|
||||||
status = Cert.CHANGED_MSG
|
status = Cert.CHANGED_MSG
|
||||||
|
|
||||||
# Verify original key path
|
# Verify original key path
|
||||||
@@ -472,47 +487,49 @@ class Cert(object):
|
|||||||
self.backup_path = backup
|
self.backup_path = backup
|
||||||
self.backup_key_path = backup_key
|
self.backup_key_path = backup_key
|
||||||
|
|
||||||
# M2Crypto is eventually going to be replaced, hence the reason for _cert
|
|
||||||
def get_cn(self):
|
def get_cn(self):
|
||||||
"""Get common name."""
|
"""Get common name."""
|
||||||
return self._cert.get_subject().CN
|
return self._cert.get_subject().CN
|
||||||
|
|
||||||
def get_fingerprint(self):
|
def get_fingerprint(self):
|
||||||
"""Get SHA1 fingerprint."""
|
"""Get SHA1 fingerprint."""
|
||||||
return self._cert.get_fingerprint(md="sha1")
|
return self._cert.digest("sha1")
|
||||||
|
|
||||||
def get_not_before(self):
|
def get_not_before(self):
|
||||||
"""Get not_valid_before field."""
|
"""Get not_valid_before field."""
|
||||||
return self._cert.get_not_before().get_datetime()
|
return crypto_util.asn1_generalizedtime_to_dt(
|
||||||
|
self._cert.get_notBefore())
|
||||||
|
|
||||||
def get_not_after(self):
|
def get_not_after(self):
|
||||||
"""Get not_valid_after field."""
|
"""Get not_valid_after field."""
|
||||||
return self._cert.get_not_after().get_datetime()
|
return crypto_util.asn1_generalizedtime_to_dt(
|
||||||
|
self._cert.get_notAfter())
|
||||||
|
|
||||||
def get_der(self):
|
def get_der(self):
|
||||||
"""Get certificate in der format."""
|
"""Get certificate in der format."""
|
||||||
return self._cert.as_der()
|
return OpenSSL.crypto.dump_certificate(
|
||||||
|
OpenSSL.crypto.FILETYPE_ASN1, self._cert)
|
||||||
|
|
||||||
def get_pub_key(self):
|
def get_pub_key(self):
|
||||||
"""Get public key size.
|
"""Get public key size.
|
||||||
|
|
||||||
.. todo:: M2Crypto doesn't support ECC, this will have to be updated
|
.. todo:: Support for ECC
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return "RSA " + str(self._cert.get_pubkey().size() * 8)
|
return "RSA {0}".format(self._cert.get_pubkey().bits)
|
||||||
|
|
||||||
def get_san(self):
|
def get_san(self):
|
||||||
"""Get subject alternative name if available."""
|
"""Get subject alternative name if available."""
|
||||||
try:
|
# pylint: disable=protected-access
|
||||||
return self._cert.get_ext("subjectAltName").get_value()
|
return ", ".join(crypto_util._pyopenssl_cert_or_req_san(self._cert))
|
||||||
except LookupError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
text = [
|
text = [
|
||||||
"Subject: %s" % self._cert.get_subject().as_text(),
|
"Subject: %s" % crypto_util.pyopenssl_x509_name_as_text(
|
||||||
|
self._cert.get_subject()),
|
||||||
"SAN: %s" % self.get_san(),
|
"SAN: %s" % self.get_san(),
|
||||||
"Issuer: %s" % self._cert.get_issuer().as_text(),
|
"Issuer: %s" % crypto_util.pyopenssl_x509_name_as_text(
|
||||||
|
self._cert.get_issuer()),
|
||||||
"Public Key: %s" % self.get_pub_key(),
|
"Public Key: %s" % self.get_pub_key(),
|
||||||
"Not Before: %s" % str(self.get_not_before()),
|
"Not Before: %s" % str(self.get_not_before()),
|
||||||
"Not After: %s" % str(self.get_not_after()),
|
"Not After: %s" % str(self.get_not_after()),
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"""Tests for letsencrypt.achallenges."""
|
"""Tests for letsencrypt.achallenges."""
|
||||||
import os
|
import os
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
import re
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import M2Crypto
|
import OpenSSL
|
||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
|
|
||||||
|
from letsencrypt import crypto_util
|
||||||
from letsencrypt import le_util
|
from letsencrypt import le_util
|
||||||
from letsencrypt.tests import acme_util
|
from letsencrypt.tests import acme_util
|
||||||
|
|
||||||
@@ -31,15 +32,13 @@ class DVSNITest(unittest.TestCase):
|
|||||||
def test_gen_cert_and_response(self):
|
def test_gen_cert_and_response(self):
|
||||||
cert_pem, _ = self.achall.gen_cert_and_response(s=self.response.s)
|
cert_pem, _ = self.achall.gen_cert_and_response(s=self.response.s)
|
||||||
|
|
||||||
cert = M2Crypto.X509.load_cert_string(cert_pem)
|
cert = OpenSSL.crypto.load_certificate(
|
||||||
self.assertEqual(cert.get_subject().CN, self.chall.nonce_domain)
|
OpenSSL.crypto.FILETYPE_PEM, cert_pem)
|
||||||
|
self.assertEqual(cert.get_subject().CN, "example.com")
|
||||||
sans = cert.get_ext("subjectAltName").get_value()
|
# pylint: disable=protected-access
|
||||||
self.assertEqual(
|
self.assertEqual(crypto_util._pyopenssl_cert_or_req_san(cert), [
|
||||||
set([self.chall.nonce_domain, "example.com",
|
"example.com", self.chall.nonce_domain,
|
||||||
self.response.z_domain(self.chall)]),
|
self.response.z_domain(self.chall)])
|
||||||
set(re.findall(r"DNS:([^, $]*)", sans)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import M2Crypto
|
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
@@ -146,7 +145,8 @@ class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
|||||||
def test_it(self): # pylint: disable=no-self-use
|
def test_it(self): # pylint: disable=no-self-use
|
||||||
from letsencrypt.crypto_util import make_key
|
from letsencrypt.crypto_util import make_key
|
||||||
# Do not test larger keys as it takes too long.
|
# Do not test larger keys as it takes too long.
|
||||||
M2Crypto.RSA.load_key_string(make_key(1024))
|
OpenSSL.crypto.load_privatekey(
|
||||||
|
OpenSSL.crypto.FILETYPE_PEM, make_key(1024))
|
||||||
|
|
||||||
|
|
||||||
class ValidPrivkeyTest(unittest.TestCase):
|
class ValidPrivkeyTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Tests for letsencrypt.proof_of_possession."""
|
"""Tests for letsencrypt.proof_of_possession."""
|
||||||
import os
|
import os
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
@@ -18,9 +19,7 @@ from letsencrypt.display import util as display_util
|
|||||||
|
|
||||||
BASE_PACKAGE = "letsencrypt.tests"
|
BASE_PACKAGE = "letsencrypt.tests"
|
||||||
CERT0_PATH = pkg_resources.resource_filename(
|
CERT0_PATH = pkg_resources.resource_filename(
|
||||||
BASE_PACKAGE, os.path.join("testdata", "cert.pem"))
|
"acme.jose", os.path.join("testdata", "cert.der"))
|
||||||
CERT1_PATH = pkg_resources.resource_filename(
|
|
||||||
BASE_PACKAGE, os.path.join("testdata", "cert-san.pem"))
|
|
||||||
CERT2_PATH = pkg_resources.resource_filename(
|
CERT2_PATH = pkg_resources.resource_filename(
|
||||||
BASE_PACKAGE, os.path.join("testdata", "dsa_cert.pem"))
|
BASE_PACKAGE, os.path.join("testdata", "dsa_cert.pem"))
|
||||||
CERT2_KEY_PATH = pkg_resources.resource_filename(
|
CERT2_KEY_PATH = pkg_resources.resource_filename(
|
||||||
@@ -38,7 +37,8 @@ with open(CERT3_KEY_PATH) as cert3_file:
|
|||||||
class ProofOfPossessionTest(unittest.TestCase):
|
class ProofOfPossessionTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.installer = mock.MagicMock()
|
self.installer = mock.MagicMock()
|
||||||
certs = [CERT0_PATH, CERT1_PATH, CERT2_PATH, CERT3_PATH]
|
self.cert1_path = tempfile.mkstemp()[1]
|
||||||
|
certs = [CERT0_PATH, self.cert1_path, CERT2_PATH, CERT3_PATH]
|
||||||
keys = [None, None, CERT2_KEY_PATH, CERT3_KEY_PATH]
|
keys = [None, None, CERT2_KEY_PATH, CERT3_KEY_PATH]
|
||||||
self.installer.get_all_certs_keys.return_value = zip(
|
self.installer.get_all_certs_keys.return_value = zip(
|
||||||
certs, keys, 4 * [None])
|
certs, keys, 4 * [None])
|
||||||
@@ -56,6 +56,9 @@ class ProofOfPossessionTest(unittest.TestCase):
|
|||||||
self.achall = achallenges.ProofOfPossession(
|
self.achall = achallenges.ProofOfPossession(
|
||||||
challb=challb, domain="example.com")
|
challb=challb, domain="example.com")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.remove(self.cert1_path)
|
||||||
|
|
||||||
def test_perform_bad_challenge(self):
|
def test_perform_bad_challenge(self):
|
||||||
hints = challenges.ProofOfPossession.Hints(
|
hints = challenges.ProofOfPossession.Hints(
|
||||||
jwk=jose.jwk.JWKOct(key="foo"), cert_fingerprints=(),
|
jwk=jose.jwk.JWKOct(key="foo"), cert_fingerprints=(),
|
||||||
|
|||||||
@@ -267,18 +267,28 @@ class RevokerInstallerTest(RevokerBase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sha_vh[cert.get_fingerprint()], self.installs[i])
|
sha_vh[cert.get_fingerprint()], self.installs[i])
|
||||||
|
|
||||||
@mock.patch("letsencrypt.revoker.M2Crypto.X509.load_cert")
|
@mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_certificate")
|
||||||
def test_get_installed_load_failure(self, mock_m2):
|
def test_get_installed_load_failure(self, mock_load_certificate):
|
||||||
mock_installer = mock.MagicMock()
|
mock_installer = mock.MagicMock()
|
||||||
mock_installer.get_all_certs_keys.return_value = self.certs_keys
|
mock_installer.get_all_certs_keys.return_value = self.certs_keys
|
||||||
|
|
||||||
mock_m2.side_effect = IOError
|
mock_load_certificate.side_effect = OpenSSL.crypto.Error
|
||||||
|
|
||||||
revoker = self._get_revoker(mock_installer)
|
revoker = self._get_revoker(mock_installer)
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
self.assertEqual(revoker._get_installed_locations(), {})
|
self.assertEqual(revoker._get_installed_locations(), {})
|
||||||
|
|
||||||
|
def test_get_installed_load_failure_open(self):
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
mock_installer = mock.MagicMock()
|
||||||
|
mock_installer.get_all_certs_keys.return_value = [(
|
||||||
|
os.path.join(tmp, 'missing'), None, None)]
|
||||||
|
revoker = self._get_revoker(mock_installer)
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.assertEqual(revoker._get_installed_locations(), {})
|
||||||
|
os.rmdir(tmp)
|
||||||
|
|
||||||
|
|
||||||
class RevokerClassMethodsTest(RevokerBase):
|
class RevokerClassMethodsTest(RevokerBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -334,6 +344,13 @@ class CertTest(unittest.TestCase):
|
|||||||
from letsencrypt.revoker import Cert
|
from letsencrypt.revoker import Cert
|
||||||
self.assertRaises(errors.RevokerError, Cert, self.key_path)
|
self.assertRaises(errors.RevokerError, Cert, self.key_path)
|
||||||
|
|
||||||
|
def test_failed_load_open(self):
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
from letsencrypt.revoker import Cert
|
||||||
|
self.assertRaises(
|
||||||
|
errors.RevokerError, Cert, os.path.join(tmp, 'missing'))
|
||||||
|
os.rmdir(tmp)
|
||||||
|
|
||||||
def test_no_row(self):
|
def test_no_row(self):
|
||||||
self.assertEqual(self.certs[0].get_row(), None)
|
self.assertEqual(self.certs[0].get_row(), None)
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
# Support swig 3.0.5+
|
|
||||||
# https://github.com/M2Crypto/M2Crypto/issues/24
|
|
||||||
# https://github.com/M2Crypto/M2Crypto/pull/30
|
|
||||||
git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto
|
|
||||||
|
|
||||||
# This requirements file will fail on Travis CI 12.04 LTS Ubuntu build
|
|
||||||
# machine under TOX_ENV=py26 with very confusing error (full tracback
|
|
||||||
# at https://api.travis-ci.org/jobs/66529698/log.txt?deansi=true):
|
|
||||||
|
|
||||||
#Traceback (most recent call last):
|
|
||||||
# File "setup.py", line 133, in <module>
|
|
||||||
# include_package_data=True,
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/distutils/core.py", line 152, in setup
|
|
||||||
# dist.run_commands()
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 975, in run_commands
|
|
||||||
# self.run_command(cmd)
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 995, in run_command
|
|
||||||
# cmd_obj.run()
|
|
||||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 142, in run
|
|
||||||
# self.with_project_on_sys_path(self.run_tests)
|
|
||||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 122, in with_project_on_sys_path
|
|
||||||
# func()
|
|
||||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 163, in run_tests
|
|
||||||
# testRunner=self._resolve_as_ep(self.test_runner),
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 816, in __init__
|
|
||||||
# self.parseArgs(argv)
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 843, in parseArgs
|
|
||||||
# self.createTests()
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 849, in createTests
|
|
||||||
# self.module)
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 613, in loadTestsFromNames
|
|
||||||
# suites = [self.loadTestsFromName(name, module) for name in names]
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 587, in loadTestsFromName
|
|
||||||
# return self.loadTestsFromModule(obj)
|
|
||||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 37, in loadTestsFromModule
|
|
||||||
# tests.append(self.loadTestsFromName(submodule))
|
|
||||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 584, in loadTestsFromName
|
|
||||||
# parent, obj = obj, getattr(obj, part)
|
|
||||||
#AttributeError: 'module' object has no attribute 'continuity_auth'
|
|
||||||
|
|
||||||
# the above error happens because letsencrypt.continuity_auth cannot import M2Crypto:
|
|
||||||
|
|
||||||
#>>> import M2Crypto
|
|
||||||
#Traceback (most recent call last):
|
|
||||||
# File "<stdin>", line 1, in <module>
|
|
||||||
# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/__init__.py", line 22, in <module>
|
|
||||||
# import m2crypto
|
|
||||||
# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 26, in <module>
|
|
||||||
# _m2crypto = swig_import_helper()
|
|
||||||
# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 22, in swig_import_helper
|
|
||||||
# _mod = imp.load_module('_m2crypto', fp, pathname, description)
|
|
||||||
#ImportError: /root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/_m2crypto.so: undefined symbol: SSLv2_method
|
|
||||||
|
|
||||||
# For more info see:
|
|
||||||
|
|
||||||
# - https://github.com/martinpaljak/M2Crypto/commit/84977c532c2444c5487db57146d81bb68dd5431d
|
|
||||||
# - http://stackoverflow.com/questions/10547332/install-m2crypto-on-a-virtualenv-without-system-packages
|
|
||||||
# - http://stackoverflow.com/questions/8206546/undefined-symbol-sslv2-method
|
|
||||||
|
|
||||||
# In short: Python has been built without SSLv2 support, and
|
|
||||||
# github.com/M2Crypto/M2Crypto version doesn't contain necessary
|
|
||||||
# patch, but it's the only one that has a patch for newer versions of
|
|
||||||
# swig...
|
|
||||||
|
|
||||||
# Problem seems not exists on Python 2.7. It's unlikely that the
|
|
||||||
# target distribution has swig 3.0.5+ and doesn't have Python 2.7, so
|
|
||||||
# this file should only be used in conjuction with Python 2.6.
|
|
||||||
@@ -53,7 +53,7 @@ letsencrypt_install_requires = [
|
|||||||
'argparse',
|
'argparse',
|
||||||
'ConfigArgParse',
|
'ConfigArgParse',
|
||||||
'configobj',
|
'configobj',
|
||||||
'M2Crypto',
|
#'cryptography>=0.7', # load_pem_x509_certificate, version pin mismatch
|
||||||
'mock',
|
'mock',
|
||||||
'parsedatetime',
|
'parsedatetime',
|
||||||
'psutil>=2.1.0', # net_connections introduced in 2.1.0
|
'psutil>=2.1.0', # net_connections introduced in 2.1.0
|
||||||
@@ -64,7 +64,6 @@ letsencrypt_install_requires = [
|
|||||||
'pytz',
|
'pytz',
|
||||||
'zope.component',
|
'zope.component',
|
||||||
'zope.interface',
|
'zope.interface',
|
||||||
'M2Crypto',
|
|
||||||
]
|
]
|
||||||
letsencrypt_apache_install_requires = [
|
letsencrypt_apache_install_requires = [
|
||||||
#'acme',
|
#'acme',
|
||||||
@@ -103,9 +102,6 @@ install_requires = [
|
|||||||
'werkzeug',
|
'werkzeug',
|
||||||
'zope.component',
|
'zope.component',
|
||||||
'zope.interface',
|
'zope.interface',
|
||||||
# order of items in install_requires DOES matter and M2Crypto has
|
|
||||||
# to go last, see #152
|
|
||||||
'M2Crypto',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
assert set(install_requires) == set.union(*(set(ireq) for ireq in (
|
assert set(install_requires) == set.union(*(set(ireq) for ireq in (
|
||||||
|
|||||||
Reference in New Issue
Block a user