PYTHON-2144 Handle the case where the peer omits the self-signed issuer cert
This commit is contained in:
parent
c04a43396c
commit
84f1a8c5f9
@ -15,6 +15,7 @@
|
||||
"""Support for requesting and verifying OCSP responses."""
|
||||
|
||||
import logging as _logging
|
||||
import re as _re
|
||||
|
||||
from datetime import datetime as _datetime
|
||||
|
||||
@ -39,6 +40,7 @@ from cryptography.x509 import (
|
||||
AuthorityInformationAccess as _AuthorityInformationAccess,
|
||||
ExtendedKeyUsage as _ExtendedKeyUsage,
|
||||
ExtensionNotFound as _ExtensionNotFound,
|
||||
load_pem_x509_certificate as _load_pem_x509_certificate,
|
||||
TLSFeature as _TLSFeature,
|
||||
TLSFeatureType as _TLSFeatureType)
|
||||
from cryptography.x509.oid import (
|
||||
@ -59,12 +61,39 @@ from requests.exceptions import RequestException as _RequestException
|
||||
|
||||
_LOGGER = _logging.getLogger(__name__)
|
||||
|
||||
_CERT_REGEX = _re.compile(
|
||||
b'-----BEGIN CERTIFICATE[^\r\n]+.+?-----END CERTIFICATE[^\r\n]+',
|
||||
_re.DOTALL)
|
||||
|
||||
def _get_issuer_cert(cert, chain):
|
||||
|
||||
def _load_trusted_ca_certs(cafile):
|
||||
"""Parse the tlsCAFile into a list of certificates."""
|
||||
with open(cafile, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
# Load all the certs in the file.
|
||||
trusted_ca_certs = []
|
||||
backend = _default_backend()
|
||||
for cert_data in _re.findall(_CERT_REGEX, data):
|
||||
trusted_ca_certs.append(
|
||||
_load_pem_x509_certificate(cert_data, backend))
|
||||
return trusted_ca_certs
|
||||
|
||||
|
||||
def _get_issuer_cert(cert, chain, trusted_ca_certs):
|
||||
issuer_name = cert.issuer
|
||||
for candidate in chain:
|
||||
if candidate.subject == issuer_name:
|
||||
return candidate
|
||||
|
||||
# Depending on the server's TLS library, the peer's cert chain may not
|
||||
# include the self signed root CA. In this case we check the user
|
||||
# provided tlsCAFile (ssl_ca_certs) for the issuer.
|
||||
# Remove once we use the verified peer cert chain in PYTHON-2147.
|
||||
if trusted_ca_certs:
|
||||
for candidate in trusted_ca_certs:
|
||||
if candidate.subject == issuer_name:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
@ -232,11 +261,19 @@ def _verify_response(issuer, response):
|
||||
return 1
|
||||
|
||||
|
||||
def ocsp_callback(conn, ocsp_bytes, user_data):
|
||||
def _ocsp_callback(conn, ocsp_bytes, user_data):
|
||||
"""Callback for use with OpenSSL.SSL.Context.set_ocsp_client_callback."""
|
||||
cert = conn.get_peer_certificate().to_cryptography()
|
||||
chain = [cer.to_cryptography() for cer in conn.get_peer_cert_chain()]
|
||||
issuer = _get_issuer_cert(cert, chain)
|
||||
cert = conn.get_peer_certificate()
|
||||
if cert is None:
|
||||
_LOGGER.debug("No peer cert?")
|
||||
return 0
|
||||
cert = cert.to_cryptography()
|
||||
chain = conn.get_peer_cert_chain()
|
||||
if not chain:
|
||||
_LOGGER.debug("No peer cert chain?")
|
||||
return 0
|
||||
chain = [cer.to_cryptography() for cer in chain]
|
||||
issuer = _get_issuer_cert(cert, chain, user_data.trusted_ca_certs)
|
||||
must_staple = False
|
||||
# https://tools.ietf.org/html/rfc7633#section-4.2.3.1
|
||||
ext = _get_extension(cert, _TLSFeature)
|
||||
|
||||
@ -32,10 +32,14 @@ from service_identity import (
|
||||
CertificateError as _SICertificateError,
|
||||
VerificationError as _SIVerificationError)
|
||||
|
||||
from cryptography.hazmat.backends import default_backend as _default_backend
|
||||
|
||||
from bson.py3compat import _unicode
|
||||
from pymongo.errors import CertificateError as _CertificateError
|
||||
from pymongo.monotonic import time as _time
|
||||
from pymongo.ocsp_support import ocsp_callback as _ocsp_callback
|
||||
from pymongo.ocsp_support import (
|
||||
_load_trusted_ca_certs,
|
||||
_ocsp_callback)
|
||||
from pymongo.socket_checker import (
|
||||
_errno_from_exception, SocketChecker as _SocketChecker)
|
||||
|
||||
@ -133,23 +137,31 @@ class _sslConn(_SSL.Connection):
|
||||
total_sent += sent
|
||||
|
||||
|
||||
class _CallbackData(object):
|
||||
"""Data class which is passed to the OCSP callback."""
|
||||
def __init__(self):
|
||||
self.trusted_ca_certs = None
|
||||
|
||||
|
||||
class SSLContext(object):
|
||||
"""A CPython compatible SSLContext implementation wrapping PyOpenSSL's
|
||||
context.
|
||||
"""
|
||||
|
||||
__slots__ = ('_protocol', '_ctx', '_check_hostname')
|
||||
__slots__ = ('_protocol', '_ctx', '_check_hostname', '_callback_data')
|
||||
|
||||
def __init__(self, protocol):
|
||||
self._protocol = protocol
|
||||
self._ctx = _SSL.Context(self._protocol)
|
||||
self._check_hostname = True
|
||||
self._callback_data = _CallbackData()
|
||||
# OCSP
|
||||
# XXX: Find a better place to do this someday, since this is client
|
||||
# side configuration and wrap_socket tries to support both client and
|
||||
# server side sockets.
|
||||
self._ctx.set_ocsp_client_callback(
|
||||
callback=_ocsp_callback, data=None)
|
||||
callback=_ocsp_callback, data=self._callback_data)
|
||||
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
@ -229,6 +241,7 @@ class SSLContext(object):
|
||||
ssl.CERT_NONE.
|
||||
"""
|
||||
self._ctx.load_verify_locations(cafile, capath)
|
||||
self._callback_data.trusted_ca_certs = _load_trusted_ca_certs(cafile)
|
||||
|
||||
def set_default_verify_paths(self):
|
||||
"""Specify that the platform provided CA certificates are to be used
|
||||
|
||||
82
test/certificates/trusted-ca.pem
Normal file
82
test/certificates/trusted-ca.pem
Normal file
@ -0,0 +1,82 @@
|
||||
# CA bundle file used to test tlsCAFile loading for OCSP.
|
||||
# Copied from the server:
|
||||
# https://github.com/mongodb/mongo/blob/r4.3.4/jstests/libs/trusted-ca.pem
|
||||
|
||||
# Autogenerated file, do not edit.
|
||||
# Generate using jstests/ssl/x509/mkcert.py --config jstests/ssl/x509/certs.yml trusted-ca.pem
|
||||
#
|
||||
# CA for alternate client/server certificate chain.
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDojCCAooCBG585gswDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxETAP
|
||||
BgNVBAgMCE5ldyBZb3JrMRYwFAYDVQQHDA1OZXcgWW9yayBDaXR5MRAwDgYDVQQK
|
||||
DAdNb25nb0RCMQ8wDQYDVQQLDAZLZXJuZWwxHzAdBgNVBAMMFlRydXN0ZWQgS2Vy
|
||||
bmVsIFRlc3QgQ0EwHhcNMTkwOTI1MjMyNzQxWhcNMzkwOTI3MjMyNzQxWjB8MQsw
|
||||
CQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxFjAUBgNVBAcMDU5ldyBZb3Jr
|
||||
IENpdHkxEDAOBgNVBAoMB01vbmdvREIxDzANBgNVBAsMBktlcm5lbDEfMB0GA1UE
|
||||
AwwWVHJ1c3RlZCBLZXJuZWwgVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBANlRxtpMeCGhkotkjHQqgqvO6O6hoRoAGGJlDaTVtqrjmC8nwySz
|
||||
1nAFndqUHttxS3A5j4enOabvffdOcV7+Z6vDQmREF6QZmQAk81pmazSc3wOnRiRs
|
||||
AhXjld7i+rhB50CW01oYzQB50rlBFu+ONKYj32nBjD+1YN4AZ2tuRlbxfx2uf8Bo
|
||||
Zowfr4n9nHVcWXBLFmaQLn+88WFO/wuwYUOn6Di1Bvtkvqum0or5QeAF0qkJxfhg
|
||||
3a4vBnomPdwEXCgAGLvHlB41CWG09EuAjrnE3HPPi5vII8pjY2dKKMomOEYmA+KJ
|
||||
AC1NlTWdN0TtsoaKnyhMMhLWs3eTyXL7kbkCAwEAAaMxMC8wDAYDVR0TBAUwAwEB
|
||||
/zAfBgNVHREEGDAWgglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsF
|
||||
AAOCAQEAQk56MO9xAhtO077COCqIYe6pYv3uzOplqjXpJ7Cph7GXwQqdFWfKls7B
|
||||
cLfF/fhIUZIu5itStEkY+AIwht4mBr1F5+hZUp9KZOed30/ewoBXAUgobLipJV66
|
||||
FKg8NRtmJbiZrrC00BSO+pKfQThU8k0zZjBmNmpjxnbKZZSFWUKtbhHV1vujver6
|
||||
SXZC7R6692vLwRBMoZxhgy/FkYRdiN0U9wpluKd63eo/O02Nt6OEMyeiyl+Z3JWi
|
||||
8g5iHNrBYGBbGSnDOnqV6tjEY3eq600JDWiodpA1OQheLi78pkc/VQZwof9dyBCm
|
||||
6BoCskTjip/UB+vIhdPFT9sgUdgDTg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZUcbaTHghoZKL
|
||||
ZIx0KoKrzujuoaEaABhiZQ2k1baq45gvJ8Mks9ZwBZ3alB7bcUtwOY+Hpzmm7333
|
||||
TnFe/merw0JkRBekGZkAJPNaZms0nN8Dp0YkbAIV45Xe4vq4QedAltNaGM0AedK5
|
||||
QRbvjjSmI99pwYw/tWDeAGdrbkZW8X8drn/AaGaMH6+J/Zx1XFlwSxZmkC5/vPFh
|
||||
Tv8LsGFDp+g4tQb7ZL6rptKK+UHgBdKpCcX4YN2uLwZ6Jj3cBFwoABi7x5QeNQlh
|
||||
tPRLgI65xNxzz4ubyCPKY2NnSijKJjhGJgPiiQAtTZU1nTdE7bKGip8oTDIS1rN3
|
||||
k8ly+5G5AgMBAAECggEAS7GjLKgT88reSzUTgubHquYf1fZwMak01RjTnsVdoboy
|
||||
aMJVwzPsjgo2yEptUQvuNcGmz54cg5vJaVlmPaspGveg6WGaRmswEo/MP4GK98Fo
|
||||
IFKkKM2CEHO74O14XLN/w8yFA02+IdtM3X/haEFE71VxXNmwawRXIBxN6Wp4j5Fb
|
||||
mPLKIspnWQ/Y/Fn799sCFAzX5mKkbCt1IEgKssgQQEm1UkvmCkcZE+mdO/ErYP8A
|
||||
COO0LpM+TK6WQY2LKiteeCCiosTZFb1GO7MkXrRP5uOBZKaW5kq1R0b6PcopJPCM
|
||||
OcYF0Zli6KB7oiQLdXgU2jCaxYOnuRb6RYh2l7NvAQKBgQD6CZ9TKOn/EUQtukyw
|
||||
pvYTyt1hoLXqYGcbRtLc1gcC+Z2BD28hd3eD/mEUv+g/8bq/OP4wYV9X+VRvR8xN
|
||||
MmfAG/sJeOCOClz1A1TyNeA+G0GZ25qWHyHQ2W4WlSG1CXQgxGzU6wo/t6wiVW5R
|
||||
O4jplFVEOXznf4vmVfBJK50R2QKBgQDegGxm23jF2N5sIYDZ14oxms8bbjPz8zH6
|
||||
tiIRYNGbSzI7J4KFGY2HiBwtf1yxS22HBL69Y1WrEzGm1vm4aZG/GUwBzI79QZAO
|
||||
+YFIGaIrdlv12Zm6lpJMmAWlOs9XFirC17oQEwOQFweOdQSt7F/+HMZOigdikRBV
|
||||
pK+8Kfay4QKBgQDarDevHwUmkg8yftA7Xomv3aenjkoK5KzH6jTX9kbDj1L0YG8s
|
||||
sbLQuVRmNUAFTH+qZUnJPh+IbQIvIHfIu+CI3u+55QFeuCl8DqHoAr5PEr9Ys/qK
|
||||
eEe2w7HIBj0oe1AYqDEWNUkNWLEuhdCpMowW3CeGN1DJlX7gvyAang4MYQKBgHwM
|
||||
aWNnFQxo/oiWnTnWm2tQfgszA7AMdF7s0E2UBwhnghfMzU3bkzZuwhbznQATp3rR
|
||||
QG5iRU7dop7717ni0akTN3cBTu8PcHuIy3UhJXLJyDdnG/gVHnepgew+v340E58R
|
||||
muB/WUsqK8JWp0c4M8R+0mjTN47ShaLZ8EgdtTbBAoGBAKOcpuDfFEMI+YJgn8zX
|
||||
h0nFT60LX6Lx+zcSDY9+6J6a4n5NhC+weYCDFOGlsLka1SwHcg1xanfrLVjpH7Ok
|
||||
HDJGLrSh1FP2Rq/oFxZ/OKCjonHLa8IulqD/AA+sqYRbysKNsT3Pi0554F2xFEqQ
|
||||
z/C84nlT1R2uTCWIxvrnpU2h
|
||||
-----END PRIVATE KEY-----
|
||||
# Pre Oct 2019 trusted-ca.pem
|
||||
# Transitional pending BUILD update.
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDpjCCAo6gAwIBAgIDAghHMA0GCSqGSIb3DQEBBQUAMHwxHzAdBgNVBAMTFlRy
|
||||
dXN0ZWQgS2VybmVsIFRlc3QgQ0ExDzANBgNVBAsTBktlcm5lbDEQMA4GA1UEChMH
|
||||
TW9uZ29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlv
|
||||
cmsxCzAJBgNVBAYTAlVTMB4XDTE2MDMzMTE0NTY1NVoXDTM2MDMzMTE0NTY1NVow
|
||||
fDEfMB0GA1UEAxMWVHJ1c3RlZCBLZXJuZWwgVGVzdCBDQTEPMA0GA1UECxMGS2Vy
|
||||
bmVsMRAwDgYDVQQKEwdNb25nb0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREw
|
||||
DwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUA
|
||||
A4IBDwAwggEKAoIBAQCePFHZTydC96SlSHSyu73vw//ddaE33kPllBB9DP2L7yRF
|
||||
6D/blFmno9fSM+Dfg64VfGV+0pCXPIZbpH29nzJu0DkvHzKiWK7P1zUj8rAHaX++
|
||||
d6k0yeTLFM9v+7YE9rHoANVn22aOyDvTgAyMmA0CLn+SmUy6WObwMIf9cZn97Znd
|
||||
lww7IeFNyK8sWtfsVN4yRBnjr7kKN2Qo0QmWeFa7jxVQptMJQrY8k1PcyVUOgOjQ
|
||||
ocJLbWLlm9k0/OMEQSwQHJ+d9weUbKjlZ9ExOrm4QuuA2tJhb38baTdAYw3Jui4f
|
||||
yD6iBAGD0Jkpc+3YaWv6CBmK8NEFkYJD/gn+lJ75AgMBAAGjMTAvMAwGA1UdEwQF
|
||||
MAMBAf8wHwYDVR0RBBgwFoIJbG9jYWxob3N0ggkxMjcuMC4wLjEwDQYJKoZIhvcN
|
||||
AQEFBQADggEBADYikjB6iwAUs6sglwkE4rOkeMkJdRCNwK/5LpFJTWrDjBvBQCdA
|
||||
Y5hlAVq8PfIYeh+wEuSvsEHXmx7W29X2+p4VuJ95/xBA6NLapwtzuiijRj2RBAOG
|
||||
1EGuyFQUPTL27DR3+tfayNykDclsVDNN8+l7nt56j8HojP74P5OMHtn+6HX5+mtF
|
||||
FfZMTy0mWguCsMOkZvjAskm6s4U5gEC8pYEoC0ZRbfUdyYsxZe/nrXIFguVlVPCB
|
||||
XnfB/0iG9t+VH5cUVj1LP9skXTW4kXfhQmljUuo+EVBNR6n2nfTnpoC65WeAgHV4
|
||||
V+s9mJsUv2x72KtKYypqEVT0gaJ1WIN9N1s=
|
||||
-----END CERTIFICATE-----
|
||||
@ -39,7 +39,9 @@ from test import (IntegrationTest,
|
||||
SkipTest,
|
||||
unittest,
|
||||
HAVE_IPADDRESS)
|
||||
from test.utils import remove_all_users, connected
|
||||
from test.utils import (remove_all_users,
|
||||
cat_files,
|
||||
connected)
|
||||
|
||||
_HAVE_PYOPENSSL = False
|
||||
try:
|
||||
@ -51,6 +53,11 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if _HAVE_PYOPENSSL:
|
||||
from pymongo.ocsp_support import _load_trusted_ca_certs
|
||||
else:
|
||||
_load_trusted_ca_certs = None
|
||||
|
||||
if HAVE_SSL:
|
||||
import ssl
|
||||
|
||||
@ -59,6 +66,7 @@ CERT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
CLIENT_PEM = os.path.join(CERT_PATH, 'client.pem')
|
||||
CLIENT_ENCRYPTED_PEM = os.path.join(CERT_PATH, 'password_protected.pem')
|
||||
CA_PEM = os.path.join(CERT_PATH, 'ca.pem')
|
||||
CA_BUNDLE_PEM = os.path.join(CERT_PATH, 'trusted-ca.pem')
|
||||
CRL_PEM = os.path.join(CERT_PATH, 'crl.pem')
|
||||
MONGODB_X509_USERNAME = (
|
||||
"C=US,ST=New York,L=New York City,O=MDB,OU=Drivers,CN=client")
|
||||
@ -157,6 +165,11 @@ class TestClientSSL(unittest.TestCase):
|
||||
def test_use_openssl_when_available(self):
|
||||
self.assertTrue(_ssl.IS_PYOPENSSL)
|
||||
|
||||
@unittest.skipUnless(_HAVE_PYOPENSSL, "Cannot test without PyOpenSSL")
|
||||
def test_load_trusted_ca_certs(self):
|
||||
trusted_ca_certs = _load_trusted_ca_certs(CA_BUNDLE_PEM)
|
||||
self.assertEqual(2, len(trusted_ca_certs))
|
||||
|
||||
|
||||
class TestSSL(IntegrationTest):
|
||||
|
||||
@ -644,6 +657,23 @@ class TestSSL(IntegrationTest):
|
||||
else:
|
||||
self.fail("Invalid certificate accepted.")
|
||||
|
||||
def test_connect_with_ca_bundle(self):
|
||||
def remove(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
temp_ca_bundle = os.path.join(CERT_PATH, 'trusted-ca-bundle.pem')
|
||||
self.addCleanup(remove, temp_ca_bundle)
|
||||
# Add the CA cert file to the bundle.
|
||||
cat_files(temp_ca_bundle, CA_BUNDLE_PEM, CA_PEM)
|
||||
with MongoClient('localhost',
|
||||
tls=True,
|
||||
tlsCertificateKeyFile=CLIENT_PEM,
|
||||
tlsCAFile=temp_ca_bundle) as client:
|
||||
self.assertTrue(client.admin.command('ismaster'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -20,6 +20,7 @@ import contextlib
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@ -880,3 +881,11 @@ def server_name_to_type(name):
|
||||
if name == 'PossiblePrimary':
|
||||
return SERVER_TYPE.Unknown
|
||||
return getattr(SERVER_TYPE, name)
|
||||
|
||||
|
||||
def cat_files(dest, *sources):
|
||||
"""Cat multiple files into dest."""
|
||||
with open(dest, 'wb') as fdst:
|
||||
for src in sources:
|
||||
with open(src, 'rb') as fsrc:
|
||||
shutil.copyfileobj(fsrc, fdst)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user