Skip to content

Commit 370e165

Browse files
authored
PYTHON-3003 Add kms_tls_options to configure options for KMS provider connections (#784)
1 parent c404150 commit 370e165

File tree

5 files changed

+133
-28
lines changed

5 files changed

+133
-28
lines changed

pymongo/common.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
validate_zlib_compression_level)
3131
from pymongo.driver_info import DriverInfo
3232
from pymongo.server_api import ServerApi
33-
from pymongo.encryption_options import validate_auto_encryption_opts_or_none
3433
from pymongo.errors import ConfigurationError
3534
from pymongo.monitoring import _validate_event_listeners
3635
from pymongo.read_concern import ReadConcern
@@ -582,6 +581,18 @@ def validate_tzinfo(dummy, value):
582581
return value
583582

584583

584+
def validate_auto_encryption_opts_or_none(option, value):
585+
"""Validate the driver keyword arg."""
586+
if value is None:
587+
return value
588+
from pymongo.encryption_options import AutoEncryptionOpts
589+
if not isinstance(value, AutoEncryptionOpts):
590+
raise TypeError("%s must be an instance of AutoEncryptionOpts" % (
591+
option,))
592+
593+
return value
594+
595+
585596
# Dictionary where keys are the names of public URI options, and values
586597
# are lists of aliases for that option.
587598
URI_OPTIONS_ALIAS_MAP = {

pymongo/encryption.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
EncryptionError,
4747
InvalidOperation,
4848
ServerSelectionTimeoutError)
49+
from pymongo.encryption_options import AutoEncryptionOpts
4950
from pymongo.mongo_client import MongoClient
5051
from pymongo.pool import _configured_socket, PoolOptions
5152
from pymongo.read_concern import ReadConcern
@@ -106,20 +107,23 @@ def kms_request(self, kms_context):
106107
"""
107108
endpoint = kms_context.endpoint
108109
message = kms_context.message
109-
host, port = parse_host(endpoint, _HTTPS_PORT)
110-
# Enable strict certificate verification, OCSP, match hostname, and
111-
# SNI using the system default CA certificates.
112-
ctx = get_ssl_context(
113-
None, # certfile
114-
None, # passphrase
115-
None, # ca_certs
116-
None, # crlfile
117-
False, # allow_invalid_certificates
118-
False, # allow_invalid_hostnames
119-
False) # disable_ocsp_endpoint_check
110+
provider = kms_context.kms_provider
111+
ctx = self.opts._kms_ssl_contexts.get(provider)
112+
if not ctx:
113+
# Enable strict certificate verification, OCSP, match hostname, and
114+
# SNI using the system default CA certificates.
115+
ctx = get_ssl_context(
116+
None, # certfile
117+
None, # passphrase
118+
None, # ca_certs
119+
None, # crlfile
120+
False, # allow_invalid_certificates
121+
False, # allow_invalid_hostnames
122+
False) # disable_ocsp_endpoint_check
120123
opts = PoolOptions(connect_timeout=_KMS_CONNECT_TIMEOUT,
121124
socket_timeout=_KMS_CONNECT_TIMEOUT,
122125
ssl_context=ctx)
126+
host, port = parse_host(endpoint, _HTTPS_PORT)
123127
conn = _configured_socket((host, port), opts)
124128
try:
125129
conn.sendall(message)
@@ -359,7 +363,7 @@ class ClientEncryption(object):
359363
"""Explicit client-side field level encryption."""
360364

361365
def __init__(self, kms_providers, key_vault_namespace, key_vault_client,
362-
codec_options):
366+
codec_options, kms_tls_options=None):
363367
"""Explicit client-side field level encryption.
364368
365369
The ClientEncryption class encapsulates explicit operations on a key
@@ -411,6 +415,16 @@ def __init__(self, kms_providers, key_vault_namespace, key_vault_client,
411415
should be the same CodecOptions instance configured on the
412416
MongoClient, Database, or Collection used to access application
413417
data.
418+
- `kms_tls_options` (optional): A map of KMS provider names to TLS
419+
options to use when creating secure connections to KMS providers.
420+
Accepts the same TLS options as
421+
:class:`pymongo.mongo_client.MongoClient`. For example, to
422+
override the system default CA file::
423+
424+
kms_tls_options={'kmip': {'tlsCAFile': certifi.where()}}
425+
426+
.. versionchanged:: 4.0
427+
Added the `kms_tls_options` parameter.
414428
415429
.. versionadded:: 3.9
416430
"""
@@ -432,7 +446,9 @@ def __init__(self, kms_providers, key_vault_namespace, key_vault_client,
432446
db, coll = key_vault_namespace.split('.', 1)
433447
key_vault_coll = key_vault_client[db][coll]
434448

435-
self._io_callbacks = _EncryptionIO(None, key_vault_coll, None, None)
449+
opts = AutoEncryptionOpts(kms_providers, key_vault_namespace,
450+
kms_tls_options=kms_tls_options)
451+
self._io_callbacks = _EncryptionIO(None, key_vault_coll, None, opts)
436452
self._encryption = ExplicitEncrypter(
437453
self._io_callbacks, MongoCryptOptions(kms_providers, None))
438454

pymongo/encryption_options.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
_HAVE_PYMONGOCRYPT = False
2424

2525
from pymongo.errors import ConfigurationError
26+
from pymongo.uri_parser import _parse_kms_tls_options
2627

2728

2829
class AutoEncryptionOpts(object):
@@ -35,7 +36,8 @@ def __init__(self, kms_providers, key_vault_namespace,
3536
mongocryptd_uri='mongodb://localhost:27020',
3637
mongocryptd_bypass_spawn=False,
3738
mongocryptd_spawn_path='mongocryptd',
38-
mongocryptd_spawn_args=None):
39+
mongocryptd_spawn_args=None,
40+
kms_tls_options=None):
3941
"""Options to configure automatic client-side field level encryption.
4042
4143
Automatic client-side field level encryption requires MongoDB 4.2
@@ -118,6 +120,16 @@ def __init__(self, kms_providers, key_vault_namespace,
118120
``['--idleShutdownTimeoutSecs=60']``. If the list does not include
119121
the ``idleShutdownTimeoutSecs`` option then
120122
``'--idleShutdownTimeoutSecs=60'`` will be added.
123+
- `kms_tls_options` (optional): A map of KMS provider names to TLS
124+
options to use when creating secure connections to KMS providers.
125+
Accepts the same TLS options as
126+
:class:`pymongo.mongo_client.MongoClient`. For example, to
127+
override the system default CA file::
128+
129+
kms_tls_options={'kmip': {'tlsCAFile': certifi.where()}}
130+
131+
.. versionchanged:: 4.0
132+
Added the `kms_tls_options` parameter.
121133
122134
.. versionadded:: 3.9
123135
"""
@@ -142,14 +154,5 @@ def __init__(self, kms_providers, key_vault_namespace,
142154
if not any('idleShutdownTimeoutSecs' in s
143155
for s in self._mongocryptd_spawn_args):
144156
self._mongocryptd_spawn_args.append('--idleShutdownTimeoutSecs=60')
145-
146-
147-
def validate_auto_encryption_opts_or_none(option, value):
148-
"""Validate the driver keyword arg."""
149-
if value is None:
150-
return value
151-
if not isinstance(value, AutoEncryptionOpts):
152-
raise TypeError("%s must be an instance of AutoEncryptionOpts" % (
153-
option,))
154-
155-
return value
157+
# Maps KMS provider name to a SSLContext.
158+
self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options)

pymongo/uri_parser.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from urllib.parse import unquote_plus
2222

23+
from pymongo.client_options import _parse_ssl_options
2324
from pymongo.common import (
2425
SRV_SERVICE_NAME,
2526
get_validated_options, INTERNAL_URI_OPTION_NAME_MAP,
@@ -569,6 +570,39 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False,
569570
}
570571

571572

573+
def _parse_kms_tls_options(kms_tls_options):
574+
"""Parse KMS TLS connection options."""
575+
if not kms_tls_options:
576+
return {}
577+
if not isinstance(kms_tls_options, dict):
578+
raise TypeError('kms_tls_options must be a dict')
579+
contexts = {}
580+
for provider, opts in kms_tls_options.items():
581+
if not isinstance(opts, dict):
582+
raise TypeError(f'kms_tls_options["{provider}"] must be a dict')
583+
opts.setdefault('tls', True)
584+
opts = _CaseInsensitiveDictionary(opts)
585+
opts = _handle_security_options(opts)
586+
opts = _normalize_options(opts)
587+
opts = validate_options(opts)
588+
ssl_context, allow_invalid_hostnames = _parse_ssl_options(opts)
589+
if ssl_context is None:
590+
raise ConfigurationError('TLS is required for KMS providers')
591+
if allow_invalid_hostnames:
592+
raise ConfigurationError('Insecure TLS options prohibited')
593+
594+
for n in ['tlsInsecure',
595+
'tlsAllowInvalidCertificates',
596+
'tlsAllowInvalidHostnames',
597+
'tlsDisableOCSPEndpointCheck',
598+
'tlsDisableCertificateRevocationCheck']:
599+
if n in opts:
600+
raise ConfigurationError(
601+
f'Insecure TLS options prohibited: {n}')
602+
contexts[provider] = ssl_context
603+
return contexts
604+
605+
572606
if __name__ == '__main__':
573607
import pprint
574608
import sys

test/test_encryption.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import base64
1818
import copy
1919
import os
20+
import ssl
2021
import traceback
2122
import socket
2223
import sys
@@ -50,9 +51,8 @@
5051
from pymongo.mongo_client import MongoClient
5152
from pymongo.operations import InsertOne
5253
from pymongo.write_concern import WriteConcern
53-
from test.test_ssl import CA_PEM
5454

55-
from test import (unittest,
55+
from test import (unittest, CA_PEM, CLIENT_PEM,
5656
client_context,
5757
IntegrationTest,
5858
PyMongoTestCase)
@@ -92,6 +92,7 @@ def test_init(self):
9292
self.assertEqual(opts._mongocryptd_spawn_path, 'mongocryptd')
9393
self.assertEqual(
9494
opts._mongocryptd_spawn_args, ['--idleShutdownTimeoutSecs=60'])
95+
self.assertEqual(opts._kms_ssl_contexts, {})
9596

9697
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
9798
def test_init_spawn_args(self):
@@ -116,6 +117,46 @@ def test_init_spawn_args(self):
116117
opts._mongocryptd_spawn_args,
117118
['--quiet', '--port=27020', '--idleShutdownTimeoutSecs=60'])
118119

120+
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
121+
def test_init_kms_tls_options(self):
122+
# Error cases:
123+
with self.assertRaisesRegex(
124+
TypeError, r'kms_tls_options\["kmip"\] must be a dict'):
125+
AutoEncryptionOpts({}, 'k.d', kms_tls_options={'kmip': 1})
126+
for tls_opts in [
127+
{'kmip': {'tls': True, 'tlsInsecure': True}},
128+
{'kmip': {'tls': True, 'tlsAllowInvalidCertificates': True}},
129+
{'kmip': {'tls': True, 'tlsAllowInvalidHostnames': True}},
130+
{'kmip': {'tls': True, 'tlsDisableOCSPEndpointCheck': True}}]:
131+
with self.assertRaisesRegex(
132+
ConfigurationError, 'Insecure TLS options prohibited'):
133+
opts = AutoEncryptionOpts({}, 'k.d', kms_tls_options=tls_opts)
134+
with self.assertRaises(FileNotFoundError):
135+
AutoEncryptionOpts({}, 'k.d', kms_tls_options={
136+
'kmip': {'tlsCAFile': 'does-not-exist'}})
137+
# Success cases:
138+
for tls_opts in [None, {}]:
139+
opts = AutoEncryptionOpts({}, 'k.d', kms_tls_options=tls_opts)
140+
self.assertEqual(opts._kms_ssl_contexts, {})
141+
opts = AutoEncryptionOpts(
142+
{}, 'k.d', kms_tls_options={'kmip': {'tls': True}, 'aws': {}})
143+
ctx = opts._kms_ssl_contexts['kmip']
144+
# On < 3.7 we check hostnames manually.
145+
if sys.version_info[:2] >= (3, 7):
146+
self.assertEqual(ctx.check_hostname, True)
147+
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
148+
ctx = opts._kms_ssl_contexts['aws']
149+
if sys.version_info[:2] >= (3, 7):
150+
self.assertEqual(ctx.check_hostname, True)
151+
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
152+
opts = AutoEncryptionOpts(
153+
{}, 'k.d', kms_tls_options={'kmip': {
154+
'tlsCAFile': CA_PEM, 'tlsCertificateKeyFile': CLIENT_PEM}})
155+
ctx = opts._kms_ssl_contexts['kmip']
156+
if sys.version_info[:2] >= (3, 7):
157+
self.assertEqual(ctx.check_hostname, True)
158+
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
159+
119160

120161
class TestClientOptions(PyMongoTestCase):
121162
def test_default(self):

0 commit comments

Comments
 (0)