[#3866]: "certbot certificates" checks validity with OpenSSL (#4155)

* cert signature validation for certificates subcommand + a test

* refactoring validation + adding in a check for making sure that the private key matches the certificate

* adding testing certs

* assertIsNone(x) -> assertEqual(None,x) to unbreak the py2.6 tests

* modifying test_verifu_renewable_cert_failure to hopefully appease python 3 test timeouts

* updating cryptography to be >=1.2 so that we can use verify

* removing unused, old testing certificate

* adding better error handling/logging

* adding test for IOError

* switching to a 2048 bit rsa key
This commit is contained in:
yomna 2017-05-31 15:04:41 -07:00 committed by Brad Warren
parent 001d90e106
commit d7f9859c3f
10 changed files with 301 additions and 4 deletions

View file

@ -6,6 +6,7 @@ import pytz
import traceback
import zope.component
from certbot import crypto_util
from certbot import errors
from certbot import interfaces
from certbot import ocsp
@ -73,6 +74,7 @@ def certificates(config):
for renewal_file in storage.renewal_conf_files(config):
try:
renewal_candidate = storage.RenewableCert(renewal_file, config)
crypto_util.verify_renewable_cert(renewal_candidate)
parsed_certs.append(renewal_candidate)
except Exception as e: # pylint: disable=broad-except
logger.warning("Renewal configuration file %s produced an "

View file

@ -12,6 +12,8 @@ import OpenSSL
import pyrfc3339
import six
import zope.component
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from acme import crypto_util as acme_crypto_util
from acme import jose
@ -206,6 +208,95 @@ def valid_privkey(privkey):
return False
def verify_renewable_cert(renewable_cert):
"""For checking that your certs were not corrupted on disk.
Several things are checked:
1. Signature verification for the cert.
2. That fullchain matches cert and chain when concatenated.
3. Check that the private key matches the certificate.
:param `.storage.RenewableCert` renewable_cert: cert to verify
:raises errors.Error: If verification fails.
"""
verify_renewable_cert_sig(renewable_cert)
verify_fullchain(renewable_cert)
verify_cert_matches_priv_key(renewable_cert)
def verify_renewable_cert_sig(renewable_cert):
""" Verifies the signature of a `.storage.RenewableCert` object.
:param `.storage.RenewableCert` renewable_cert: cert to verify
:raises errors.Error: If signature verification fails.
"""
try:
with open(renewable_cert.chain, 'rb') as chain:
chain, _ = pyopenssl_load_certificate(chain.read())
with open(renewable_cert.cert, 'rb') as cert:
cert = x509.load_pem_x509_certificate(cert.read(), default_backend())
hash_name = cert.signature_hash_algorithm.name
OpenSSL.crypto.verify(chain, cert.signature, cert.tbs_certificate_bytes, hash_name)
except (IOError, ValueError, OpenSSL.crypto.Error) as e:
error_str = "verifying the signature of the cert located at {0} has failed. \
Details: {1}".format(renewable_cert.cert, e)
logger.exception(error_str)
raise errors.Error(error_str)
def verify_cert_matches_priv_key(renewable_cert):
""" Verifies that the private key and cert match.
:param `.storage.RenewableCert` renewable_cert: cert to verify
:raises errors.Error: If they don't match.
"""
try:
with open(renewable_cert.cert) as cert:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.read())
with open(renewable_cert.privkey) as privkey:
privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey.read())
context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
context.use_privatekey(privkey)
context.use_certificate(cert)
context.check_privatekey()
except (IOError, OpenSSL.SSL.Error) as e:
error_str = "verifying the cert located at {0} matches the \
private key located at {1} has failed. \
Details: {2}".format(renewable_cert.cert,
renewable_cert.privkey, e)
logger.exception(error_str)
raise errors.Error(error_str)
def verify_fullchain(renewable_cert):
""" Verifies that fullchain is indeed cert concatenated with chain.
:param `.storage.RenewableCert` renewable_cert: cert to verify
:raises errors.Error: If cert and chain do not combine to fullchain.
"""
try:
with open(renewable_cert.chain) as chain:
chain = chain.read()
with open(renewable_cert.cert) as cert:
cert = cert.read()
with open(renewable_cert.fullchain) as fullchain:
fullchain = fullchain.read()
if (cert + chain) != fullchain:
error_str = "fullchain does not match cert + chain for {0}!"
error_str = error_str.format(renewable_cert.lineagename)
raise errors.Error(error_str)
except IOError as e:
error_str = "reading one of cert, chain, or fullchain has failed: {0}".format(e)
logger.exception(error_str)
raise errors.Error(error_str)
except errors.Error as e:
raise e
def pyopenssl_load_certificate(data):
"""Load PEM/DER certificate.
@ -340,6 +431,7 @@ def _notAfterBefore(cert_path, method):
:rtype: :class:`datetime.datetime`
"""
# pylint: disable=redefined-outer-name
with open(cert_path) as f:
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
f.read())

View file

@ -149,12 +149,14 @@ class CertificatesTest(BaseCertManagerTest):
self.assertFalse(mock_utility.notification.called)
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
@mock.patch('certbot.crypto_util.verify_renewable_cert')
@mock.patch('certbot.cert_manager.logger')
@test_util.patch_get_utility()
@mock.patch("certbot.storage.RenewableCert")
@mock.patch('certbot.cert_manager._report_human_readable')
def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
mock_utility, mock_logger):
mock_utility, mock_logger, mock_verifier):
mock_verifier.return_value = None
mock_report.return_value = ""
self._certificates(self.cli_config)
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member

View file

@ -14,11 +14,14 @@ import certbot.tests.util as test_util
RSA256_KEY = test_util.load_vector('rsa256_key.pem')
RSA256_KEY_PATH = test_util.vector_path('rsa256_key.pem')
RSA512_KEY = test_util.load_vector('rsa512_key.pem')
RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem')
CERT_PATH = test_util.vector_path('cert.pem')
CERT = test_util.load_vector('cert.pem')
SAN_CERT = test_util.load_vector('cert-san.pem')
SS_CERT_PATH = test_util.vector_path('self_signed_cert.pem')
SS_CERT = test_util.load_vector('self_signed_cert.pem')
class InitSaveKeyTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.init_save_key."""
@ -194,6 +197,101 @@ class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods
OpenSSL.crypto.FILETYPE_PEM, make_key(1024))
class VerifyCertSetup(unittest.TestCase):
"""Refactoring for verification tests."""
def setUp(self):
super(VerifyCertSetup, self).setUp()
self.renewable_cert = mock.MagicMock()
self.renewable_cert.cert = SS_CERT_PATH
self.renewable_cert.chain = SS_CERT_PATH
self.renewable_cert.privkey = RSA2048_KEY_PATH
self.renewable_cert.fullchain = test_util.vector_path('self_signed_fullchain.pem')
self.bad_renewable_cert = mock.MagicMock()
self.bad_renewable_cert.chain = SS_CERT_PATH
self.bad_renewable_cert.cert = SS_CERT_PATH
self.bad_renewable_cert.fullchain = SS_CERT_PATH
class VerifyRenewableCertTest(VerifyCertSetup):
"""Tests for certbot.crypto_util.verify_renewable_cert."""
def setUp(self):
super(VerifyRenewableCertTest, self).setUp()
def _call(self, renewable_cert):
from certbot.crypto_util import verify_renewable_cert
return verify_renewable_cert(renewable_cert)
def test_verify_renewable_cert(self):
self.assertEqual(None, self._call(self.renewable_cert))
@mock.patch('certbot.crypto_util.verify_renewable_cert_sig', side_effect=errors.Error(""))
def test_verify_renewable_cert_failure(self, unused_verify_renewable_cert_sign):
self.assertRaises(errors.Error, self._call, self.bad_renewable_cert)
class VerifyRenewableCertSigTest(VerifyCertSetup):
"""Tests for certbot.crypto_util.verify_renewable_cert."""
def setUp(self):
super(VerifyRenewableCertSigTest, self).setUp()
def _call(self, renewable_cert):
from certbot.crypto_util import verify_renewable_cert_sig
return verify_renewable_cert_sig(renewable_cert)
def test_cert_sig_match(self):
self.assertEqual(None, self._call(self.renewable_cert))
def test_cert_sig_mismatch(self):
self.bad_renewable_cert.cert = test_util.vector_path('self_signed_cert_bad.pem')
self.assertRaises(errors.Error, self._call, self.bad_renewable_cert)
class VerifyFullchainTest(VerifyCertSetup):
"""Tests for certbot.crypto_util.verify_fullchain."""
def setUp(self):
super(VerifyFullchainTest, self).setUp()
def _call(self, renewable_cert):
from certbot.crypto_util import verify_fullchain
return verify_fullchain(renewable_cert)
def test_fullchain_matches(self):
self.assertEqual(None, self._call(self.renewable_cert))
def test_fullchain_mismatch(self):
self.assertRaises(errors.Error, self._call, self.bad_renewable_cert)
def test_fullchain_ioerror(self):
self.bad_renewable_cert.chain = "dog"
self.assertRaises(errors.Error, self._call, self.bad_renewable_cert)
class VerifyCertMatchesPrivKeyTest(VerifyCertSetup):
"""Tests for certbot.crypto_util.verify_cert_matches_priv_key."""
def setUp(self):
super(VerifyCertMatchesPrivKeyTest, self).setUp()
def _call(self, renewable_cert):
from certbot.crypto_util import verify_cert_matches_priv_key
return verify_cert_matches_priv_key(renewable_cert)
def test_cert_priv_key_match(self):
self.assertEqual(None, self._call(self.renewable_cert))
def test_cert_priv_key_mismatch(self):
self.bad_renewable_cert.privkey = RSA256_KEY_PATH
self.bad_renewable_cert.cert = SS_CERT_PATH
self.assertRaises(errors.Error, self._call, self.bad_renewable_cert)
class ValidPrivkeyTest(unittest.TestCase):
"""Tests for certbot.crypto_util.valid_privkey."""

28
certbot/tests/testdata/rsa2048_key.pem vendored Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDm1WIecnHjL4Fs
JvxDP27GyeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop
+D7s+oh0apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3
DaNokGn7r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf
5QU5pFx60a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDV
MJ3mIB8FOW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf
3nCJFk1fAgMBAAECggEAJkhbVntagfgd+cbZbXm2sIdKQGlwXk92/Zxd3tZMcuNY
rU+/C2bJ5uTEm+0R/V9f3FXlsCagGde2t7ExFnJScSRAGCuFRxudMMI/wNvUvnpR
O9vN3HxrRo2rZqBkqHIZCR0d2Bxs/0cvGqTLZgsVWKV4xM07TThcE7DtvsNGegRn
WFxfsRcRypkIvZoba1HagvCituRBEa07R7mQp8kRhP9ZeRq3bZws9qBmqzj1cylG
q8QA4Foq7sK8P78bpIhrcOFBDAr+Vr1ZGY6u01J0w13MUtl6iIx4VCjQKt4NkzsK
dj2q+GAMwhReR2ZS42o8LiyGpwusj+dKIFfFekgK2QKBgQD4wwmRDgvt85brQTNF
Tkhui0eToz5oXt8mVDb58nwkpojFQOv87ZyNsEqm7S0t/3RtEViVio2aymTMsrz4
21vRq46dvhINQ3DoMok6xIchEOEgMeonOilkURWtrMjD/Kn297Asv7zOqI5BCNiP
3FFcRqf+CaqbhnOgMkcI5z6b7QKBgQDtjM1otFFHyS7ctyLRuMeFyxWUSbWHvi8U
xjUW256c6wpQ2DBLSVB61VQjfrSjkZ5DJVFGnbw42HxSDafL11mzTbY1vDbgtgLK
YiuVHG7OYZJTLaZoM68BseX4xHN8FztnvvP1ttuk5oFb+vD8q6ODZSEawRd3PvtX
D7RtNouc+wKBgQDiwBWGTUF+gt18T5BGilbnvLlf0Btg06mgrH74UpnqZoqhEs6J
XKWpWZqSkfruxL4BdSBEH2l4QSiklgA+7uTBOBnlm42k3WaboQUJtn5eG5651AXV
/+Qe9vJFvwu56iObZKcIAzY9QdN5YHDWoULgU99pZrJG1cWrrmilqvOc+QKBgQCB
iOslslY0N+926eJxzDn4qkJtJzh2+e1AfcjLWx0F4mEwroK/Ow5IvPVxmZE1NJ3B
baMBR9gwg1RfhhS+4gKG9NRsPuMJ7BZfd+LeH7AImEorU1RPtAc1fGW0HqP+wchi
DU2I6pqhNBTMLG2myo2Sg93mce6y1sRFuEmh2EGPawKBgQC3uUEdjQekXaxXfYHi
1Dk3Ht1a9t8XxwoCVRqicE7lqlwDtS2y9lHAeUP7JNy8ZGNjx8srRZpkYVMztugo
Ecw26UA7FbNqJP5OPkGjfiFqtOq70h9vlfLdiAPmoqyOx//RkgiNXt9m5xcDzzdB
7EtBK59KSiQkB8fHtooy7Ipiiw==
-----END PRIVATE KEY-----

View file

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV
BAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD
RUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC
Q0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G
yeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0
apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7
r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6
0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F
OW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f
AgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME
GDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
SIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh
vds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv
xzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D
HEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X
YXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ
5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN
-----END CERTIFICATE-----

View file

@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICYzCCAg2gAwIBAgIJAPvqv4TcAtuFMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
VQQGEwJDQTEQMA4GA1UECAwHT250YXJpbzEQMA4GA1UEBwwHVG9yb250bzEMMAoG
A1UECgwDRUZGMRYwFAYDVQQLDA1UZWNoIFByb2plY3RzMQ4wDAYDVQQDDAVZb21u
YTEjMCEGCSqGSIb3DQEJARYUeW9tbmEubmFzc2VyQGVmZi5vcmcwHhcNMTcwMzI0
MjIzMjUxWhcNNDgwMTI0MjIzMjUxWjCBjDELMAkGA1UEBhMCQ0ExEDAOBgNVBAgM
B09udGFyaW8xEDAOBgNVBAcMB1Rvcm9udG8xDDAKBgNVBAoMA0VGRjEWMBQGA1UE
CwwNVGVjaCBQcm9qZWN0czEOMAwGA1UEAwwFWW9tbmExIzAhBgkqhkiG9w0BCQEW
FHlvbW5hLm5hc3NlckBlZmYub3JnMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1
c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvO
jm0c+pVE6K+EdE/twuUCAwEAAaNQME4wHQYDVR0OBBYEFCbPyO1L15Sy5CUDWCSP
BMB01ZeKMB8GA1UdIwQYMBaAFCbPyO1L15Sy5CUDWCSPBMB01ZeKMAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQELBQADQQAeWDdcrJOolFHr3m8TrlDJ/Ca4SfJya2jb
K1wahbX83sC42834HbDOQASGBhoLYDhC1cMPbKDDjMbR9rjYuf7T
-----END CERTIFICATE-----

View file

@ -0,0 +1,40 @@
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV
BAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD
RUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC
Q0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G
yeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0
apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7
r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6
0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F
OW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f
AgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME
GDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
SIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh
vds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv
xzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D
HEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X
YXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ
5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV
BAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD
RUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC
Q0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G
yeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0
apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7
r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6
0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F
OW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f
AgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME
GDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
SIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh
vds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv
xzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D
HEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X
YXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ
5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN
-----END CERTIFICATE-----

View file

@ -41,7 +41,7 @@ install_requires = [
# in which we added 2.6 support (see #2243), so we relax the requirement.
'ConfigArgParse>=0.9.3',
'configobj',
'cryptography>=0.7', # load_pem_x509_certificate
'cryptography>=1.2', # load_pem_x509_certificate
'mock',
'parsedatetime>=1.3', # Calendar.parseDT
'PyOpenSSL',

View file

@ -79,7 +79,7 @@ setenv =
# https://github.com/shazow/urllib3/pull/930
deps =
py{26,27}-oldest: cffi<=1.7
py{26,27}-oldest: cryptography==0.8
py{26,27}-oldest: cryptography==1.2
py{26,27}-oldest: configargparse==0.10.0
py{26,27}-oldest: PyOpenSSL==0.13
py{26,27}-oldest: requests<=2.11.1