Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PYTHON-2798 Workaround windows cert issue with SSL_CERT_FILE #670

Merged
merged 1 commit into from Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 8 additions & 6 deletions .evergreen/run-tests.sh
Expand Up @@ -146,13 +146,15 @@ if [ -n "$TEST_ENCRYPTION" ]; then
. $DRIVERS_TOOLS/.evergreen/csfle/set-temp-creds.sh

# Start the mock KMS servers.
if [ "$OS" != "Windows_NT" ]; then
pushd ${DRIVERS_TOOLS}/.evergreen/csfle
python -u lib/kms_http_server.py --ca_file ../x509gen/ca.pem --cert_file ../x509gen/expired.pem --port 8000 &
python -u lib/kms_http_server.py --ca_file ../x509gen/ca.pem --cert_file ../x509gen/wrong-host.pem --port 8001 &
trap 'kill $(jobs -p)' EXIT HUP
popd
if [ "$OS" = "Windows_NT" ]; then
# Remove after BUILD-13574.
python -m pip install certifi
fi
pushd ${DRIVERS_TOOLS}/.evergreen/csfle
python -u lib/kms_http_server.py --ca_file ../x509gen/ca.pem --cert_file ../x509gen/expired.pem --port 8000 &
python -u lib/kms_http_server.py --ca_file ../x509gen/ca.pem --cert_file ../x509gen/wrong-host.pem --port 8001 &
trap 'kill $(jobs -p)' EXIT HUP
popd
fi

if [ -z "$DATA_LAKE" ]; then
Expand Down
24 changes: 23 additions & 1 deletion test/__init__.py
Expand Up @@ -49,7 +49,7 @@
from pymongo import common, message
from pymongo.common import partition_node
from pymongo.server_api import ServerApi
from pymongo.ssl_support import HAVE_SSL, validate_cert_reqs
from pymongo.ssl_support import HAVE_SSL, _ssl
from pymongo.uri_parser import parse_uri
from test.version import Version

Expand Down Expand Up @@ -898,6 +898,10 @@ def setUpClass(cls):
else:
cls.credentials = {}

def patch_system_certs(self, ca_certs):
patcher = SystemCertsPatcher(ca_certs)
self.addCleanup(patcher.disable)


# Use assertRaisesRegex if available, otherwise use Python 2.7's
# deprecated assertRaisesRegexp, with a 'p'.
Expand Down Expand Up @@ -1043,3 +1047,21 @@ def clear_warning_registry():
for name, module in list(sys.modules.items()):
if hasattr(module, "__warningregistry__"):
setattr(module, "__warningregistry__", {})


class SystemCertsPatcher(object):
def __init__(self, ca_certs):
if (ssl.OPENSSL_VERSION.lower().startswith('libressl') and
sys.platform == 'darwin' and not _ssl.IS_PYOPENSSL):
raise SkipTest(
"LibreSSL on OSX doesn't support setting CA certificates "
"using SSL_CERT_FILE environment variable.")
self.original_certs = os.environ.get('SSL_CERT_FILE')
# Tell OpenSSL where CA certificates live.
os.environ['SSL_CERT_FILE'] = ca_certs

def disable(self):
if self.original_certs is None:
os.environ.pop('SSL_CERT_FILE')
else:
os.environ['SSL_CERT_FILE'] = self.original_certs
48 changes: 28 additions & 20 deletions test/test_encryption.py
Expand Up @@ -19,7 +19,6 @@
import os
import traceback
import socket
import ssl
import sys
import textwrap
import uuid
Expand Down Expand Up @@ -50,10 +49,14 @@
WriteError)
from pymongo.mongo_client import MongoClient
from pymongo.operations import InsertOne
from pymongo.ssl_support import _ssl
from pymongo.write_concern import WriteConcern
from test.test_ssl import CA_PEM

from test import unittest, IntegrationTest, PyMongoTestCase, client_context
from test import (unittest,
client_context,
IntegrationTest,
PyMongoTestCase,
SystemCertsPatcher)
from test.utils import (TestCreator,
camel_to_snake_args,
OvertCommandListener,
Expand All @@ -62,7 +65,26 @@
rs_or_single_client,
wait_until)
from test.utils_spec_runner import SpecRunner
from test.test_ssl import CA_PEM

try:
import certifi
HAVE_CERTIFI = True
except ImportError:
HAVE_CERTIFI = False

patcher = None


def setUpModule():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a standard interface expected by unittest?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, why is this needed if we can just call self.patch_system_certs wherever required?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. https://docs.python.org/3/library/unittest.html#setupmodule-and-teardownmodule

It was easier to implement this for the entire module than it was to try do it via setUpClass() or setUp().

if sys.platform == 'win32' and HAVE_CERTIFI:
# Remove after BUILD-13574.
global patcher
patcher = SystemCertsPatcher(certifi.where())


def tearDownModule():
if patcher:
patcher.disable()


def get_client_opts(client):
Expand Down Expand Up @@ -1629,25 +1651,11 @@ def test_bypassAutoEncryption(self):

# https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#kms-tls-tests
class TestKmsTLSProse(EncryptionIntegrationTest):
@unittest.skipIf(sys.platform == 'win32',
"Can't test system ca certs on Windows")
@unittest.skipIf(ssl.OPENSSL_VERSION.lower().startswith('libressl') and
sys.platform == 'darwin' and not _ssl.IS_PYOPENSSL,
"LibreSSL on OSX doesn't support setting CA certificates "
"using SSL_CERT_FILE environment variable.")
@unittest.skipUnless(any(AWS_CREDS.values()),
'AWS environment credentials are not set')
def setUp(self):
self.original_certs = os.environ.get('SSL_CERT_FILE')
def restore_certs():
if self.original_certs is None:
os.environ.pop('SSL_CERT_FILE')
else:
os.environ['SSL_CERT_FILE'] = self.original_certs
# Tell OpenSSL where CA certificates live.
os.environ['SSL_CERT_FILE'] = CA_PEM
self.addCleanup(restore_certs)

super(TestKmsTLSProse, self).setUp()
self.patch_system_certs(CA_PEM)
self.client_encrypted = ClientEncryption(
{'aws': AWS_CREDS}, 'keyvault.datakeys', self.client, OPTS)
self.addCleanup(self.client_encrypted.close)
Expand Down
53 changes: 20 additions & 33 deletions test/test_ssl.py
Expand Up @@ -449,45 +449,32 @@ def test_validation_with_system_ca_certs(self):
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
# --sslWeakCertificateValidation
#
if sys.platform == "win32":
raise SkipTest("Can't test system ca certs on Windows.")

if (ssl.OPENSSL_VERSION.lower().startswith('libressl') and
sys.platform == 'darwin' and not _ssl.IS_PYOPENSSL):
raise SkipTest(
"LibreSSL on OSX doesn't support setting CA certificates "
"using SSL_CERT_FILE environment variable.")

# Tell OpenSSL where CA certificates live.
os.environ['SSL_CERT_FILE'] = CA_PEM
try:
with self.assertRaises(ConnectionFailure):
# Server cert is verified but hostname matching fails
connected(MongoClient('server',
ssl=True,
serverSelectionTimeoutMS=100,
**self.credentials))

# Server cert is verified. Disable hostname matching.
self.patch_system_certs(CA_PEM)
with self.assertRaises(ConnectionFailure):
# Server cert is verified but hostname matching fails
connected(MongoClient('server',
ssl=True,
ssl_match_hostname=False,
serverSelectionTimeoutMS=100,
**self.credentials))

# Server cert and hostname are verified.
connected(MongoClient('localhost',
ssl=True,
serverSelectionTimeoutMS=100,
**self.credentials))
# Server cert is verified. Disable hostname matching.
connected(MongoClient('server',
ssl=True,
ssl_match_hostname=False,
serverSelectionTimeoutMS=100,
**self.credentials))

# Server cert and hostname are verified.
connected(
MongoClient(
'mongodb://localhost/?ssl=true&serverSelectionTimeoutMS=100',
**self.credentials))
finally:
os.environ.pop('SSL_CERT_FILE')
# Server cert and hostname are verified.
connected(MongoClient('localhost',
ssl=True,
serverSelectionTimeoutMS=100,
**self.credentials))

# Server cert and hostname are verified.
connected(
MongoClient(
'mongodb://localhost/?ssl=true&serverSelectionTimeoutMS=100',
**self.credentials))

def test_system_certs_config_error(self):
ctx = get_ssl_context(
Expand Down