Skip to content

Commit 84f1a8c

Browse files
committed
PYTHON-2144 Handle the case where the peer omits the self-signed issuer cert
1 parent c04a433 commit 84f1a8c

File tree

5 files changed

+180
-9
lines changed

5 files changed

+180
-9
lines changed

pymongo/ocsp_support.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Support for requesting and verifying OCSP responses."""
1616

1717
import logging as _logging
18+
import re as _re
1819

1920
from datetime import datetime as _datetime
2021

@@ -39,6 +40,7 @@
3940
AuthorityInformationAccess as _AuthorityInformationAccess,
4041
ExtendedKeyUsage as _ExtendedKeyUsage,
4142
ExtensionNotFound as _ExtensionNotFound,
43+
load_pem_x509_certificate as _load_pem_x509_certificate,
4244
TLSFeature as _TLSFeature,
4345
TLSFeatureType as _TLSFeatureType)
4446
from cryptography.x509.oid import (
@@ -59,12 +61,39 @@
5961

6062
_LOGGER = _logging.getLogger(__name__)
6163

64+
_CERT_REGEX = _re.compile(
65+
b'-----BEGIN CERTIFICATE[^\r\n]+.+?-----END CERTIFICATE[^\r\n]+',
66+
_re.DOTALL)
6267

63-
def _get_issuer_cert(cert, chain):
68+
69+
def _load_trusted_ca_certs(cafile):
70+
"""Parse the tlsCAFile into a list of certificates."""
71+
with open(cafile, 'rb') as f:
72+
data = f.read()
73+
74+
# Load all the certs in the file.
75+
trusted_ca_certs = []
76+
backend = _default_backend()
77+
for cert_data in _re.findall(_CERT_REGEX, data):
78+
trusted_ca_certs.append(
79+
_load_pem_x509_certificate(cert_data, backend))
80+
return trusted_ca_certs
81+
82+
83+
def _get_issuer_cert(cert, chain, trusted_ca_certs):
6484
issuer_name = cert.issuer
6585
for candidate in chain:
6686
if candidate.subject == issuer_name:
6787
return candidate
88+
89+
# Depending on the server's TLS library, the peer's cert chain may not
90+
# include the self signed root CA. In this case we check the user
91+
# provided tlsCAFile (ssl_ca_certs) for the issuer.
92+
# Remove once we use the verified peer cert chain in PYTHON-2147.
93+
if trusted_ca_certs:
94+
for candidate in trusted_ca_certs:
95+
if candidate.subject == issuer_name:
96+
return candidate
6897
return None
6998

7099

@@ -232,11 +261,19 @@ def _verify_response(issuer, response):
232261
return 1
233262

234263

235-
def ocsp_callback(conn, ocsp_bytes, user_data):
264+
def _ocsp_callback(conn, ocsp_bytes, user_data):
236265
"""Callback for use with OpenSSL.SSL.Context.set_ocsp_client_callback."""
237-
cert = conn.get_peer_certificate().to_cryptography()
238-
chain = [cer.to_cryptography() for cer in conn.get_peer_cert_chain()]
239-
issuer = _get_issuer_cert(cert, chain)
266+
cert = conn.get_peer_certificate()
267+
if cert is None:
268+
_LOGGER.debug("No peer cert?")
269+
return 0
270+
cert = cert.to_cryptography()
271+
chain = conn.get_peer_cert_chain()
272+
if not chain:
273+
_LOGGER.debug("No peer cert chain?")
274+
return 0
275+
chain = [cer.to_cryptography() for cer in chain]
276+
issuer = _get_issuer_cert(cert, chain, user_data.trusted_ca_certs)
240277
must_staple = False
241278
# https://tools.ietf.org/html/rfc7633#section-4.2.3.1
242279
ext = _get_extension(cert, _TLSFeature)

pymongo/pyopenssl_context.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@
3232
CertificateError as _SICertificateError,
3333
VerificationError as _SIVerificationError)
3434

35+
from cryptography.hazmat.backends import default_backend as _default_backend
36+
3537
from bson.py3compat import _unicode
3638
from pymongo.errors import CertificateError as _CertificateError
3739
from pymongo.monotonic import time as _time
38-
from pymongo.ocsp_support import ocsp_callback as _ocsp_callback
40+
from pymongo.ocsp_support import (
41+
_load_trusted_ca_certs,
42+
_ocsp_callback)
3943
from pymongo.socket_checker import (
4044
_errno_from_exception, SocketChecker as _SocketChecker)
4145

@@ -133,23 +137,31 @@ def sendall(self, buf, flags=0):
133137
total_sent += sent
134138

135139

140+
class _CallbackData(object):
141+
"""Data class which is passed to the OCSP callback."""
142+
def __init__(self):
143+
self.trusted_ca_certs = None
144+
145+
136146
class SSLContext(object):
137147
"""A CPython compatible SSLContext implementation wrapping PyOpenSSL's
138148
context.
139149
"""
140150

141-
__slots__ = ('_protocol', '_ctx', '_check_hostname')
151+
__slots__ = ('_protocol', '_ctx', '_check_hostname', '_callback_data')
142152

143153
def __init__(self, protocol):
144154
self._protocol = protocol
145155
self._ctx = _SSL.Context(self._protocol)
146156
self._check_hostname = True
157+
self._callback_data = _CallbackData()
147158
# OCSP
148159
# XXX: Find a better place to do this someday, since this is client
149160
# side configuration and wrap_socket tries to support both client and
150161
# server side sockets.
151162
self._ctx.set_ocsp_client_callback(
152-
callback=_ocsp_callback, data=None)
163+
callback=_ocsp_callback, data=self._callback_data)
164+
153165

154166
@property
155167
def protocol(self):
@@ -229,6 +241,7 @@ def load_verify_locations(self, cafile=None, capath=None):
229241
ssl.CERT_NONE.
230242
"""
231243
self._ctx.load_verify_locations(cafile, capath)
244+
self._callback_data.trusted_ca_certs = _load_trusted_ca_certs(cafile)
232245

233246
def set_default_verify_paths(self):
234247
"""Specify that the platform provided CA certificates are to be used

test/certificates/trusted-ca.pem

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# CA bundle file used to test tlsCAFile loading for OCSP.
2+
# Copied from the server:
3+
# https://github.com/mongodb/mongo/blob/r4.3.4/jstests/libs/trusted-ca.pem
4+
5+
# Autogenerated file, do not edit.
6+
# Generate using jstests/ssl/x509/mkcert.py --config jstests/ssl/x509/certs.yml trusted-ca.pem
7+
#
8+
# CA for alternate client/server certificate chain.
9+
-----BEGIN CERTIFICATE-----
10+
MIIDojCCAooCBG585gswDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxETAP
11+
BgNVBAgMCE5ldyBZb3JrMRYwFAYDVQQHDA1OZXcgWW9yayBDaXR5MRAwDgYDVQQK
12+
DAdNb25nb0RCMQ8wDQYDVQQLDAZLZXJuZWwxHzAdBgNVBAMMFlRydXN0ZWQgS2Vy
13+
bmVsIFRlc3QgQ0EwHhcNMTkwOTI1MjMyNzQxWhcNMzkwOTI3MjMyNzQxWjB8MQsw
14+
CQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxFjAUBgNVBAcMDU5ldyBZb3Jr
15+
IENpdHkxEDAOBgNVBAoMB01vbmdvREIxDzANBgNVBAsMBktlcm5lbDEfMB0GA1UE
16+
AwwWVHJ1c3RlZCBLZXJuZWwgVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
17+
ADCCAQoCggEBANlRxtpMeCGhkotkjHQqgqvO6O6hoRoAGGJlDaTVtqrjmC8nwySz
18+
1nAFndqUHttxS3A5j4enOabvffdOcV7+Z6vDQmREF6QZmQAk81pmazSc3wOnRiRs
19+
AhXjld7i+rhB50CW01oYzQB50rlBFu+ONKYj32nBjD+1YN4AZ2tuRlbxfx2uf8Bo
20+
Zowfr4n9nHVcWXBLFmaQLn+88WFO/wuwYUOn6Di1Bvtkvqum0or5QeAF0qkJxfhg
21+
3a4vBnomPdwEXCgAGLvHlB41CWG09EuAjrnE3HPPi5vII8pjY2dKKMomOEYmA+KJ
22+
AC1NlTWdN0TtsoaKnyhMMhLWs3eTyXL7kbkCAwEAAaMxMC8wDAYDVR0TBAUwAwEB
23+
/zAfBgNVHREEGDAWgglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsF
24+
AAOCAQEAQk56MO9xAhtO077COCqIYe6pYv3uzOplqjXpJ7Cph7GXwQqdFWfKls7B
25+
cLfF/fhIUZIu5itStEkY+AIwht4mBr1F5+hZUp9KZOed30/ewoBXAUgobLipJV66
26+
FKg8NRtmJbiZrrC00BSO+pKfQThU8k0zZjBmNmpjxnbKZZSFWUKtbhHV1vujver6
27+
SXZC7R6692vLwRBMoZxhgy/FkYRdiN0U9wpluKd63eo/O02Nt6OEMyeiyl+Z3JWi
28+
8g5iHNrBYGBbGSnDOnqV6tjEY3eq600JDWiodpA1OQheLi78pkc/VQZwof9dyBCm
29+
6BoCskTjip/UB+vIhdPFT9sgUdgDTg==
30+
-----END CERTIFICATE-----
31+
-----BEGIN PRIVATE KEY-----
32+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZUcbaTHghoZKL
33+
ZIx0KoKrzujuoaEaABhiZQ2k1baq45gvJ8Mks9ZwBZ3alB7bcUtwOY+Hpzmm7333
34+
TnFe/merw0JkRBekGZkAJPNaZms0nN8Dp0YkbAIV45Xe4vq4QedAltNaGM0AedK5
35+
QRbvjjSmI99pwYw/tWDeAGdrbkZW8X8drn/AaGaMH6+J/Zx1XFlwSxZmkC5/vPFh
36+
Tv8LsGFDp+g4tQb7ZL6rptKK+UHgBdKpCcX4YN2uLwZ6Jj3cBFwoABi7x5QeNQlh
37+
tPRLgI65xNxzz4ubyCPKY2NnSijKJjhGJgPiiQAtTZU1nTdE7bKGip8oTDIS1rN3
38+
k8ly+5G5AgMBAAECggEAS7GjLKgT88reSzUTgubHquYf1fZwMak01RjTnsVdoboy
39+
aMJVwzPsjgo2yEptUQvuNcGmz54cg5vJaVlmPaspGveg6WGaRmswEo/MP4GK98Fo
40+
IFKkKM2CEHO74O14XLN/w8yFA02+IdtM3X/haEFE71VxXNmwawRXIBxN6Wp4j5Fb
41+
mPLKIspnWQ/Y/Fn799sCFAzX5mKkbCt1IEgKssgQQEm1UkvmCkcZE+mdO/ErYP8A
42+
COO0LpM+TK6WQY2LKiteeCCiosTZFb1GO7MkXrRP5uOBZKaW5kq1R0b6PcopJPCM
43+
OcYF0Zli6KB7oiQLdXgU2jCaxYOnuRb6RYh2l7NvAQKBgQD6CZ9TKOn/EUQtukyw
44+
pvYTyt1hoLXqYGcbRtLc1gcC+Z2BD28hd3eD/mEUv+g/8bq/OP4wYV9X+VRvR8xN
45+
MmfAG/sJeOCOClz1A1TyNeA+G0GZ25qWHyHQ2W4WlSG1CXQgxGzU6wo/t6wiVW5R
46+
O4jplFVEOXznf4vmVfBJK50R2QKBgQDegGxm23jF2N5sIYDZ14oxms8bbjPz8zH6
47+
tiIRYNGbSzI7J4KFGY2HiBwtf1yxS22HBL69Y1WrEzGm1vm4aZG/GUwBzI79QZAO
48+
+YFIGaIrdlv12Zm6lpJMmAWlOs9XFirC17oQEwOQFweOdQSt7F/+HMZOigdikRBV
49+
pK+8Kfay4QKBgQDarDevHwUmkg8yftA7Xomv3aenjkoK5KzH6jTX9kbDj1L0YG8s
50+
sbLQuVRmNUAFTH+qZUnJPh+IbQIvIHfIu+CI3u+55QFeuCl8DqHoAr5PEr9Ys/qK
51+
eEe2w7HIBj0oe1AYqDEWNUkNWLEuhdCpMowW3CeGN1DJlX7gvyAang4MYQKBgHwM
52+
aWNnFQxo/oiWnTnWm2tQfgszA7AMdF7s0E2UBwhnghfMzU3bkzZuwhbznQATp3rR
53+
QG5iRU7dop7717ni0akTN3cBTu8PcHuIy3UhJXLJyDdnG/gVHnepgew+v340E58R
54+
muB/WUsqK8JWp0c4M8R+0mjTN47ShaLZ8EgdtTbBAoGBAKOcpuDfFEMI+YJgn8zX
55+
h0nFT60LX6Lx+zcSDY9+6J6a4n5NhC+weYCDFOGlsLka1SwHcg1xanfrLVjpH7Ok
56+
HDJGLrSh1FP2Rq/oFxZ/OKCjonHLa8IulqD/AA+sqYRbysKNsT3Pi0554F2xFEqQ
57+
z/C84nlT1R2uTCWIxvrnpU2h
58+
-----END PRIVATE KEY-----
59+
# Pre Oct 2019 trusted-ca.pem
60+
# Transitional pending BUILD update.
61+
-----BEGIN CERTIFICATE-----
62+
MIIDpjCCAo6gAwIBAgIDAghHMA0GCSqGSIb3DQEBBQUAMHwxHzAdBgNVBAMTFlRy
63+
dXN0ZWQgS2VybmVsIFRlc3QgQ0ExDzANBgNVBAsTBktlcm5lbDEQMA4GA1UEChMH
64+
TW9uZ29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlv
65+
cmsxCzAJBgNVBAYTAlVTMB4XDTE2MDMzMTE0NTY1NVoXDTM2MDMzMTE0NTY1NVow
66+
fDEfMB0GA1UEAxMWVHJ1c3RlZCBLZXJuZWwgVGVzdCBDQTEPMA0GA1UECxMGS2Vy
67+
bmVsMRAwDgYDVQQKEwdNb25nb0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREw
68+
DwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUA
69+
A4IBDwAwggEKAoIBAQCePFHZTydC96SlSHSyu73vw//ddaE33kPllBB9DP2L7yRF
70+
6D/blFmno9fSM+Dfg64VfGV+0pCXPIZbpH29nzJu0DkvHzKiWK7P1zUj8rAHaX++
71+
d6k0yeTLFM9v+7YE9rHoANVn22aOyDvTgAyMmA0CLn+SmUy6WObwMIf9cZn97Znd
72+
lww7IeFNyK8sWtfsVN4yRBnjr7kKN2Qo0QmWeFa7jxVQptMJQrY8k1PcyVUOgOjQ
73+
ocJLbWLlm9k0/OMEQSwQHJ+d9weUbKjlZ9ExOrm4QuuA2tJhb38baTdAYw3Jui4f
74+
yD6iBAGD0Jkpc+3YaWv6CBmK8NEFkYJD/gn+lJ75AgMBAAGjMTAvMAwGA1UdEwQF
75+
MAMBAf8wHwYDVR0RBBgwFoIJbG9jYWxob3N0ggkxMjcuMC4wLjEwDQYJKoZIhvcN
76+
AQEFBQADggEBADYikjB6iwAUs6sglwkE4rOkeMkJdRCNwK/5LpFJTWrDjBvBQCdA
77+
Y5hlAVq8PfIYeh+wEuSvsEHXmx7W29X2+p4VuJ95/xBA6NLapwtzuiijRj2RBAOG
78+
1EGuyFQUPTL27DR3+tfayNykDclsVDNN8+l7nt56j8HojP74P5OMHtn+6HX5+mtF
79+
FfZMTy0mWguCsMOkZvjAskm6s4U5gEC8pYEoC0ZRbfUdyYsxZe/nrXIFguVlVPCB
80+
XnfB/0iG9t+VH5cUVj1LP9skXTW4kXfhQmljUuo+EVBNR6n2nfTnpoC65WeAgHV4
81+
V+s9mJsUv2x72KtKYypqEVT0gaJ1WIN9N1s=
82+
-----END CERTIFICATE-----

test/test_ssl.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
SkipTest,
4040
unittest,
4141
HAVE_IPADDRESS)
42-
from test.utils import remove_all_users, connected
42+
from test.utils import (remove_all_users,
43+
cat_files,
44+
connected)
4345

4446
_HAVE_PYOPENSSL = False
4547
try:
@@ -51,6 +53,11 @@
5153
except ImportError:
5254
pass
5355

56+
if _HAVE_PYOPENSSL:
57+
from pymongo.ocsp_support import _load_trusted_ca_certs
58+
else:
59+
_load_trusted_ca_certs = None
60+
5461
if HAVE_SSL:
5562
import ssl
5663

@@ -59,6 +66,7 @@
5966
CLIENT_PEM = os.path.join(CERT_PATH, 'client.pem')
6067
CLIENT_ENCRYPTED_PEM = os.path.join(CERT_PATH, 'password_protected.pem')
6168
CA_PEM = os.path.join(CERT_PATH, 'ca.pem')
69+
CA_BUNDLE_PEM = os.path.join(CERT_PATH, 'trusted-ca.pem')
6270
CRL_PEM = os.path.join(CERT_PATH, 'crl.pem')
6371
MONGODB_X509_USERNAME = (
6472
"C=US,ST=New York,L=New York City,O=MDB,OU=Drivers,CN=client")
@@ -157,6 +165,11 @@ def test_config_ssl(self):
157165
def test_use_openssl_when_available(self):
158166
self.assertTrue(_ssl.IS_PYOPENSSL)
159167

168+
@unittest.skipUnless(_HAVE_PYOPENSSL, "Cannot test without PyOpenSSL")
169+
def test_load_trusted_ca_certs(self):
170+
trusted_ca_certs = _load_trusted_ca_certs(CA_BUNDLE_PEM)
171+
self.assertEqual(2, len(trusted_ca_certs))
172+
160173

161174
class TestSSL(IntegrationTest):
162175

@@ -644,6 +657,23 @@ def test_mongodb_x509_auth(self):
644657
else:
645658
self.fail("Invalid certificate accepted.")
646659

660+
def test_connect_with_ca_bundle(self):
661+
def remove(path):
662+
try:
663+
os.remove(path)
664+
except OSError:
665+
pass
666+
667+
temp_ca_bundle = os.path.join(CERT_PATH, 'trusted-ca-bundle.pem')
668+
self.addCleanup(remove, temp_ca_bundle)
669+
# Add the CA cert file to the bundle.
670+
cat_files(temp_ca_bundle, CA_BUNDLE_PEM, CA_PEM)
671+
with MongoClient('localhost',
672+
tls=True,
673+
tlsCertificateKeyFile=CLIENT_PEM,
674+
tlsCAFile=temp_ca_bundle) as client:
675+
self.assertTrue(client.admin.command('ismaster'))
676+
647677

648678
if __name__ == "__main__":
649679
unittest.main()

test/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import functools
2121
import os
2222
import re
23+
import shutil
2324
import sys
2425
import threading
2526
import time
@@ -880,3 +881,11 @@ def server_name_to_type(name):
880881
if name == 'PossiblePrimary':
881882
return SERVER_TYPE.Unknown
882883
return getattr(SERVER_TYPE, name)
884+
885+
886+
def cat_files(dest, *sources):
887+
"""Cat multiple files into dest."""
888+
with open(dest, 'wb') as fdst:
889+
for src in sources:
890+
with open(src, 'rb') as fsrc:
891+
shutil.copyfileobj(fsrc, fdst)

0 commit comments

Comments
 (0)