PYTHON-2144 Handle the case where the peer omits the self-signed issuer cert

This commit is contained in:
Shane Harvey 2020-02-27 13:36:16 -08:00
parent c04a43396c
commit 84f1a8c5f9
5 changed files with 180 additions and 9 deletions

View File

@ -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)

View File

@ -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

View 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-----

View File

@ -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()

View File

@ -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)