PYTHON-2508 Improve PyOpenSSL on Windows and macOS
This commit is contained in:
parent
6ff2883f82
commit
70b927a01d
15
README.rst
15
README.rst
@ -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]
|
||||
|
||||
|
||||
@ -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
|
||||
....................
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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:
|
||||
|
||||
5
setup.py
5
setup.py
@ -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'],
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user