PYTHON-2508 Improve PyOpenSSL on Windows and macOS

This commit is contained in:
Bernie Hackett 2021-01-27 16:28:12 -08:00
parent 6ff2883f82
commit 70b927a01d
7 changed files with 77 additions and 96 deletions

View File

@ -110,19 +110,12 @@ Support for mongodb+srv:// URIs requires `dnspython
$ python -m pip install pymongo[srv]
TLS / SSL support may require `ipaddress
<https://pypi.python.org/pypi/ipaddress>`_ and `certifi
<https://pypi.python.org/pypi/certifi>`_ or `wincertstore
<https://pypi.python.org/pypi/wincertstore>`_ depending on the Python
version in use. The necessary dependencies can be installed along with
PyMongo::
$ python -m pip install pymongo[tls]
OCSP (Online Certificate Status Protocol) requires `PyOpenSSL
<https://pypi.org/project/pyOpenSSL/>`_, `requests
<https://pypi.org/project/requests/>`_ and `service_identity
<https://pypi.org/project/service_identity/>`_::
<https://pypi.org/project/requests/>`_, `service_identity
<https://pypi.org/project/service_identity/>`_ and may
require `certifi
<https://pypi.python.org/pypi/certifi>`_::
$ python -m pip install pymongo[ocsp]

View File

@ -63,6 +63,8 @@ Breaking Changes in 4.0
:meth:`~pymongo.collection.Collection.find`,
:meth:`~pymongo.collection.Collection.find_one`, and
:meth:`~pymongo.cursor.Cursor`.
- The "tls" install extra is no longer necessary or supported and will be
ignored by pip.
Notable improvements
....................

View File

@ -6,16 +6,6 @@ configuration options supported by PyMongo. See `the server documentation
<http://docs.mongodb.org/manual/tutorial/configure-ssl/>`_ to configure
MongoDB.
Dependencies
............
For connections using TLS/SSL, PyMongo may require third party dependencies as
determined by your version of Python. With PyMongo 3.3+, you can install
PyMongo 3.3+ and any TLS/SSL-related dependencies using the following pip
command::
$ python -m pip install pymongo[tls]
.. warning:: Industry best practices recommend, and some regulations require,
the use of TLS 1.1 or newer. Though no application changes are required for
PyMongo to make use of the newest protocols, some operating systems or

View File

@ -18,14 +18,15 @@ context.
import socket as _socket
import ssl as _stdlibssl
import time
import sys as _sys
import time as _time
from errno import EINTR as _EINTR
# service_identity requires this for py27, so it should always be available
from ipaddress import ip_address as _ip_address
from OpenSSL import SSL as _SSL
from cryptography.x509 import load_der_x509_certificate as _load_der_x509_certificate
from OpenSSL import crypto as _crypto, SSL as _SSL
from service_identity.pyopenssl import (
verify_hostname as _verify_hostname,
verify_ip_address as _verify_ip_address)
@ -33,7 +34,9 @@ from service_identity import (
CertificateError as _SICertificateError,
VerificationError as _SIVerificationError)
from pymongo.errors import CertificateError as _CertificateError
from pymongo.errors import (
CertificateError as _CertificateError,
ConfigurationError as _ConfigurationError)
from pymongo.ocsp_support import (
_load_trusted_ca_certs,
_ocsp_callback)
@ -41,6 +44,12 @@ from pymongo.ocsp_cache import _OCSPCache
from pymongo.socket_checker import (
_errno_from_exception, SocketChecker as _SocketChecker)
try:
import certifi
_HAVE_CERTIFI = True
except ImportError:
_HAVE_CERTIFI = False
PROTOCOL_SSLv23 = _SSL.SSLv23_METHOD
# Always available
OP_NO_SSLv2 = _SSL.OP_NO_SSLv2
@ -98,14 +107,14 @@ class _sslConn(_SSL.Connection):
def _call(self, call, *args, **kwargs):
timeout = self.gettimeout()
if timeout:
start = time.monotonic()
start = _time.monotonic()
while True:
try:
return call(*args, **kwargs)
except _RETRY_ERRORS:
self.socket_checker.select(
self, True, True, timeout)
if timeout and time.monotonic() - start > timeout:
if timeout and _time.monotonic() - start > timeout:
raise _socket.timeout("timed out")
continue
@ -272,6 +281,44 @@ class SSLContext(object):
self._ctx.load_verify_locations(cafile, capath)
self._callback_data.trusted_ca_certs = _load_trusted_ca_certs(cafile)
def _load_certifi(self):
"""Attempt to load CA certs from certifi."""
if _HAVE_CERTIFI:
self.load_verify_locations(certifi.where())
else:
raise _ConfigurationError(
"tlsAllowInvalidCertificates is False but no system "
"CA certificates could be loaded. Please install the "
"certifi package, or provide a path to a CA file using "
"the tlsCAFile option")
def _load_wincerts(self, store):
"""Attempt to load CA certs from Windows trust store."""
cert_store = self._ctx.get_cert_store()
oid = _stdlibssl.Purpose.SERVER_AUTH.oid
for cert, encoding, trust in _stdlibssl.enum_certificates(store):
if encoding == "x509_asn":
if trust is True or oid in trust:
cert_store.add_cert(
_crypto.X509.from_cryptography(
_load_der_x509_certificate(cert)))
def load_default_certs(self):
"""A PyOpenSSL version of load_default_certs from CPython."""
# PyOpenSSL is incapable of loading CA certs from Windows, and mostly
# incapable on macOS.
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
if _sys.platform == "win32":
try:
for storename in ('CA', 'ROOT'):
self._load_wincerts(storename)
except PermissionError:
# Fall back to certifi
self._load_certifi()
elif _sys.platform == "darwin":
self._load_certifi()
self._ctx.set_default_verify_paths()
def set_default_verify_paths(self):
"""Specify that the platform provided CA certificates are to be used
for verification purposes."""

View File

@ -14,9 +14,7 @@
"""Support for SSL in PyMongo."""
import atexit
import sys
import threading
from pymongo.errors import ConfigurationError
@ -30,22 +28,6 @@ except ImportError:
except ImportError:
HAVE_SSL = False
HAVE_CERTIFI = False
try:
import certifi
HAVE_CERTIFI = True
except ImportError:
pass
HAVE_WINCERTSTORE = False
try:
from wincertstore import CertFile
HAVE_WINCERTSTORE = True
except ImportError:
pass
_WINCERTSLOCK = threading.Lock()
_WINCERTS = None
if HAVE_SSL:
# Note: The validate* functions below deal with users passing
@ -82,17 +64,6 @@ if HAVE_SSL:
return CERT_NONE
return CERT_REQUIRED
def _load_wincerts():
"""Set _WINCERTS to an instance of wincertstore.Certfile."""
global _WINCERTS
certfile = CertFile()
certfile.addstore("CA")
certfile.addstore("ROOT")
atexit.register(certfile.close)
_WINCERTS = certfile
def get_ssl_context(*args):
"""Create and return an SSLContext object."""
(certfile,
@ -138,30 +109,7 @@ if HAVE_SSL:
if ca_certs is not None:
ctx.load_verify_locations(ca_certs)
elif cert_reqs != CERT_NONE:
# CPython ssl module only, doesn't exist in PyOpenSSL
if hasattr(ctx, "load_default_certs"):
ctx.load_default_certs()
# Always useless on Windows.
elif (sys.platform != "win32" and
hasattr(ctx, "set_default_verify_paths")):
ctx.set_default_verify_paths()
# This is needed with PyOpenSSL on Windows
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
elif sys.platform == "win32" and HAVE_WINCERTSTORE:
with _WINCERTSLOCK:
if _WINCERTS is None:
_load_wincerts()
ctx.load_verify_locations(_WINCERTS.name)
# This is necessary with PyOpenSSL on macOS when homebrew isn't
# installed.
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
elif HAVE_CERTIFI:
ctx.load_verify_locations(certifi.where())
else:
raise ConfigurationError(
"`ssl_cert_reqs` is not ssl.CERT_NONE and no system "
"CA certificates could be loaded. `ssl_ca_certs` is "
"required.")
ctx.load_default_certs()
ctx.verify_mode = verify_mode
return ctx
else:

View File

@ -275,6 +275,11 @@ ext_modules = [Extension('bson._cbson',
# in set_default_verify_paths we should really avoid.
# service_identity 18.1.0 introduced support for IP addr matching.
pyopenssl_reqs = ["pyopenssl>=17.2.0", "requests<3.0.0", "service_identity>=18.1.0"]
if sys.platform in ('win32', 'darwin'):
# Fallback to certifi on Windows if we can't load CA certs from the system
# store and just use certifi on macOS.
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
pyopenssl_reqs.append('certifi')
extras_require = {
'encryption': ['pymongocrypt<2.0.0'],

View File

@ -336,25 +336,21 @@ class TestSSL(IntegrationTest):
#
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
# Python > 2.7.9. If SSLContext doesn't have load_default_certs
# it also doesn't have check_hostname.
ctx = get_ssl_context(
None, None, None, None, ssl.CERT_NONE, None, False, True)
if hasattr(ctx, 'load_default_certs'):
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(
None, None, None, None, ssl.CERT_NONE, None, True, True)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(
None, None, None, None, ssl.CERT_REQUIRED, None, False, True)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(
None, None, None, None, ssl.CERT_REQUIRED, None, True, True)
if _PY37PLUS or _HAVE_PYOPENSSL:
self.assertTrue(ctx.check_hostname)
else:
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(
None, None, None, None, ssl.CERT_NONE, None, True, True)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(
None, None, None, None, ssl.CERT_REQUIRED, None, False, True)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(
None, None, None, None, ssl.CERT_REQUIRED, None, True, True)
if _PY37PLUS:
self.assertTrue(ctx.check_hostname)
else:
self.assertFalse(ctx.check_hostname)
response = self.client.admin.command('ismaster')