Skip to content

Commit 48046b2

Browse files
committed
SSL certificate verification PYTHON-466
1 parent 41e2f82 commit 48046b2

14 files changed

+652
-45
lines changed

pymongo/common.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
from pymongo.read_preferences import ReadPreference
2222
from pymongo.errors import ConfigurationError
2323

24+
HAS_SSL = True
25+
try:
26+
import ssl
27+
except ImportError:
28+
HAS_SSL = False
29+
2430

2531
def raise_config_error(key, dummy):
2632
"""Raise ConfigurationError with the given key name."""
@@ -63,6 +69,33 @@ def validate_positive_integer(option, value):
6369
return val
6470

6571

72+
def validate_readable(option, value):
73+
"""Validates that 'value' is file-like and readable.
74+
"""
75+
# First make sure its a string py3.3 open(True, 'r') succeeds
76+
# Used in ssl cert checking due to poor ssl module error reporting
77+
value = validate_basestring(option, value)
78+
open(value, 'r').close()
79+
return value
80+
81+
82+
def validate_cert_reqs(option, value):
83+
"""Validate the cert reqs are valid. It must be None or one of the three
84+
values ``ssl.CERT_NONE``, ``ssl.CERT_OPTIONAL`` or ``ssl.CERT_REQUIRED``"""
85+
if value is None:
86+
return value
87+
if HAS_SSL:
88+
if value in (ssl.CERT_NONE, ssl.CERT_OPTIONAL, ssl.CERT_REQUIRED):
89+
return value
90+
raise ConfigurationError("The value of %s must be one of: "
91+
"`ssl.CERT_NONE`, `ssl.CERT_OPTIONAL` or "
92+
"`ssl.CERT_REQUIRED" % (option,))
93+
else:
94+
raise ConfigurationError("The value of %s is set but can't be "
95+
"validated. The ssl module is not available"
96+
% (option,))
97+
98+
6699
def validate_positive_integer_or_none(option, value):
67100
"""Validate that 'value' is a positive integer or None.
68101
"""
@@ -180,6 +213,10 @@ def validate_auth_mechanism(option, value):
180213
'connecttimeoutms': validate_timeout_or_none,
181214
'sockettimeoutms': validate_timeout_or_none,
182215
'ssl': validate_boolean,
216+
'ssl_keyfile': validate_readable,
217+
'ssl_certfile': validate_readable,
218+
'ssl_cert_reqs': validate_cert_reqs,
219+
'ssl_ca_certs': validate_readable,
183220
'readpreference': validate_read_preference,
184221
'read_preference': validate_read_preference,
185222
'tag_sets': validate_tag_sets,

pymongo/connection.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,28 @@ def __init__(self, host=None, port=None, max_pool_size=10,
135135
until :meth:`end_request()`
136136
- `slave_okay` or `slaveOk` (deprecated): Use `read_preference`
137137
instead.
138+
- `ssl_keyfile`: The private keyfile used to identify the local
139+
connection against mongod. If included with the ``certfile` then
140+
only the ``ssl_certfile`` is needed. Implies ``ssl=True``.
141+
- `ssl_certfile`: The certificate file used to identify the local
142+
connection against mongod. Implies ``ssl=True``.
143+
- `ssl_cert_reqs`: The parameter cert_reqs specifies whether a
144+
certificate is required from the other side of the connection,
145+
and whether it will be validated if provided. It must be one of the
146+
three values ``ssl.CERT_NONE`` (certificates ignored),
147+
``ssl.CERT_OPTIONAL`` (not required, but validated if provided), or
148+
``ssl.CERT_REQUIRED`` (required and validated). If the value of
149+
this parameter is not ``ssl.CERT_NONE``, then the ``ssl_ca_certs``
150+
parameter must point to a file of CA certificates.
151+
Implies ``ssl=True``.
152+
- `ssl_ca_certs`: The ca_certs file contains a set of concatenated
153+
"certification authority" certificates, which are used to validate
154+
certificates passed from the other end of the connection.
155+
Implies ``ssl=True``.
138156
139157
.. seealso:: :meth:`end_request`
158+
.. versionchanged:: 2.4.2+
159+
Added addtional ssl options
140160
.. versionchanged:: 2.3
141161
Added support for failover between mongos seed list members.
142162
.. versionchanged:: 2.2

pymongo/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
from bson.errors import *
1818

19+
try:
20+
from ssl import CertificateError
21+
except ImportError:
22+
from pymongo.ssl_match_hostname import CertificateError
23+
1924

2025
class PyMongoError(Exception):
2126
"""Base class for all PyMongo exceptions.
@@ -98,6 +103,7 @@ class InvalidURI(ConfigurationError):
98103
.. versionadded:: 1.5
99104
"""
100105

106+
101107
class UnsupportedOption(ConfigurationError):
102108
"""Exception for unsupported options.
103109

pymongo/helpers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
OperationFailure,
2727
TimeoutError)
2828

29+
try:
30+
from ssl import match_hostname
31+
except ImportError:
32+
from pymongo.ssl_match_hostname import match_hostname
33+
2934

3035
def _index_list(key_or_list, direction=None):
3136
"""Helper to generate a list of (key, direction) pairs.
@@ -167,4 +172,3 @@ def shuffled(sequence):
167172
out = list(sequence)
168173
random.shuffle(out)
169174
return out
170-

pymongo/mongo_client.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
message,
5050
pool,
5151
uri_parser)
52+
from pymongo.common import HAS_SSL
5253
from pymongo.cursor_manager import CursorManager
5354
from pymongo.errors import (AutoReconnect,
5455
ConfigurationError,
@@ -60,6 +61,7 @@
6061

6162
EMPTY = b("")
6263

64+
6365
def _partition_node(node):
6466
"""Split a host:port string returned from mongod/s into
6567
a (host, int(port)) pair needed for socket.connect().
@@ -163,11 +165,30 @@ def __init__(self, host=None, port=None, max_pool_size=10,
163165
- `use_greenlets`: If ``True``, :meth:`start_request()` will ensure
164166
that the current greenlet uses the same socket for all
165167
operations until :meth:`end_request()`
168+
- `ssl_keyfile`: The private keyfile used to identify the local
169+
connection against mongod. If included with the ``certfile` then
170+
only the ``ssl_certfile`` is needed. Implies ``ssl=True``.
171+
- `ssl_certfile`: The certificate file used to identify the local
172+
connection against mongod. Implies ``ssl=True``.
173+
- `ssl_cert_reqs`: Specifies whether a certificate is required from
174+
the other side of the connection, and whether it will be validated
175+
if provided. It must be one of the three values ``ssl.CERT_NONE``
176+
(certificates ignored), ``ssl.CERT_OPTIONAL``
177+
(not required, but validated if provided), or ``ssl.CERT_REQUIRED``
178+
(required and validated). If the value of this parameter is not
179+
``ssl.CERT_NONE``, then the ``ssl_ca_certs`` parameter must point
180+
to a file of CA certificates. Implies ``ssl=True``.
181+
- `ssl_ca_certs`: The ca_certs file contains a set of concatenated
182+
"certification authority" certificates, which are used to validate
183+
certificates passed from the other end of the connection.
184+
Implies ``ssl=True``.
166185
167186
.. seealso:: :meth:`end_request`
168187
169188
.. mongodoc:: connections
170189
190+
.. versionchanged:: 2.4.2+
191+
Added addtional ssl options
171192
.. versionadded:: 2.4
172193
"""
173194
if host is None:
@@ -232,21 +253,47 @@ def __init__(self, host=None, port=None, max_pool_size=10,
232253

233254
self.__net_timeout = options.get('sockettimeoutms')
234255
self.__conn_timeout = options.get('connecttimeoutms')
235-
self.__use_ssl = options.get('ssl', False)
236-
if self.__use_ssl and not pool.have_ssl:
256+
self.__use_ssl = options.get('ssl', None)
257+
self.__ssl_keyfile = options.get('ssl_keyfile', None)
258+
self.__ssl_certfile = options.get('ssl_certfile', None)
259+
self.__ssl_cert_reqs = options.get('ssl_cert_reqs', None)
260+
self.__ssl_ca_certs = options.get('ssl_ca_certs', None)
261+
262+
if self.__use_ssl and not HAS_SSL:
237263
raise ConfigurationError("The ssl module is not available. If you "
238264
"are using a python version previous to "
239265
"2.6 you must install the ssl package "
240266
"from PyPI.")
241267

268+
ssl_kwarg_keys = [k for k in kwargs.keys() if k.startswith('ssl_')]
269+
if self.__use_ssl == False and ssl_kwarg_keys:
270+
raise ConfigurationError("ssl has not been enabled but the "
271+
"following ssl parameters have been set: "
272+
"%s. Please set `ssl=True` or remove."
273+
% ', '.join(ssl_kwarg_keys))
274+
275+
if self.__ssl_cert_reqs and not self.__ssl_ca_certs:
276+
raise ConfigurationError("If `ssl_cert_reqs` is not "
277+
"`ssl.CERT_NONE` then you must "
278+
"include `ssl_ca_certs` to be able "
279+
"to validate the server.")
280+
281+
if ssl_kwarg_keys and self.__use_ssl is None:
282+
# ssl options imply ssl = True
283+
self.__use_ssl = True
284+
242285
self.__use_greenlets = options.get('use_greenlets', False)
243286
self.__pool = pool_class(
244287
None,
245288
self.__max_pool_size,
246289
self.__net_timeout,
247290
self.__conn_timeout,
248291
self.__use_ssl,
249-
use_greenlets=self.__use_greenlets)
292+
use_greenlets=self.__use_greenlets,
293+
ssl_keyfile=self.__ssl_keyfile,
294+
ssl_certfile=self.__ssl_certfile,
295+
ssl_cert_reqs=self.__ssl_cert_reqs,
296+
ssl_ca_certs=self.__ssl_ca_certs)
250297

251298
self.__document_class = document_class
252299
self.__tz_aware = common.validate_boolean('tz_aware', tz_aware)

pymongo/mongo_replica_set_client.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@
3131
Database(MongoReplicaSetClient([u'...', u'...']), u'test_database')
3232
"""
3333

34+
import atexit
3435
import datetime
3536
import socket
3637
import struct
3738
import threading
3839
import time
3940
import warnings
4041
import weakref
41-
import atexit
4242

4343
from bson.py3compat import b
4444
from pymongo import (auth,
@@ -58,6 +58,9 @@
5858
InvalidDocument,
5959
OperationFailure)
6060

61+
if common.HAS_SSL:
62+
import ssl
63+
6164
EMPTY = b("")
6265
MAX_BSON_SIZE = 4 * 1024 * 1024
6366
MAX_RETRY = 3
@@ -369,7 +372,26 @@ def __init__(self, hosts_or_uri=None, max_pool_size=10,
369372
precedence.
370373
- `port`: For compatibility with :class:`~mongo_client.MongoClient`.
371374
The default port number to use for hosts.
372-
375+
- `ssl_keyfile`: The private keyfile used to identify the local
376+
connection against mongod. If included with the ``certfile` then
377+
only the ``ssl_certfile`` is needed. Implies ``ssl=True``.
378+
- `ssl_certfile`: The certificate file used to identify the local
379+
connection against mongod. Implies ``ssl=True``.
380+
- `ssl_cert_reqs`: Specifies whether a certificate is required from
381+
the other side of the connection, and whether it will be validated
382+
if provided. It must be one of the three values ``ssl.CERT_NONE``
383+
(certificates ignored), ``ssl.CERT_OPTIONAL``
384+
(not required, but validated if provided), or ``ssl.CERT_REQUIRED``
385+
(required and validated). If the value of this parameter is not
386+
``ssl.CERT_NONE``, then the ``ssl_ca_certs`` parameter must point
387+
to a file of CA certificates. Implies ``ssl=True``.
388+
- `ssl_ca_certs`: The ca_certs file contains a set of concatenated
389+
"certification authority" certificates, which are used to validate
390+
certificates passed from the other end of the connection.
391+
Implies ``ssl=True``.
392+
393+
.. versionchanged:: 2.4.2+
394+
Added addtional ssl options
373395
.. versionadded:: 2.4
374396
"""
375397
self.__opts = {}
@@ -437,16 +459,37 @@ def __init__(self, hosts_or_uri=None, max_pool_size=10,
437459
raise ConfigurationError("the replicaSet "
438460
"keyword parameter is required.")
439461

440-
441462
self.__net_timeout = self.__opts.get('sockettimeoutms')
442463
self.__conn_timeout = self.__opts.get('connecttimeoutms')
443-
self.__use_ssl = self.__opts.get('ssl', False)
444-
if self.__use_ssl and not pool.have_ssl:
464+
self.__use_ssl = self.__opts.get('ssl', None)
465+
self.__ssl_keyfile = self.__opts.get('ssl_keyfile', None)
466+
self.__ssl_certfile = self.__opts.get('ssl_certfile', None)
467+
self.__ssl_cert_reqs = self.__opts.get('ssl_cert_reqs', None)
468+
self.__ssl_ca_certs = self.__opts.get('ssl_ca_certs', None)
469+
470+
if self.__use_ssl and not common.HAS_SSL:
445471
raise ConfigurationError("The ssl module is not available. If you "
446472
"are using a python version previous to "
447473
"2.6 you must install the ssl package "
448474
"from PyPI.")
449475

476+
ssl_kwarg_keys = [k for k in kwargs.keys() if k.startswith('ssl_')]
477+
if self.__use_ssl == False and ssl_kwarg_keys:
478+
raise ConfigurationError("ssl has not been enabled but the "
479+
"following ssl parameters have been set: "
480+
"%s. Please set `ssl=True` or remove."
481+
% ', '.join(ssl_kwarg_keys))
482+
483+
if self.__ssl_cert_reqs and not self.__ssl_ca_certs:
484+
raise ConfigurationError("If `ssl_cert_reqs` is not "
485+
"`ssl.CERT_NONE` then you must "
486+
"include `ssl_ca_certs` to be able "
487+
"to validate the server.")
488+
489+
if ssl_kwarg_keys and self.__use_ssl is None:
490+
# ssl options imply ssl = True
491+
self.__use_ssl = True
492+
450493
super(MongoReplicaSetClient, self).__init__(**self.__opts)
451494
if self.slave_okay:
452495
warnings.warn("slave_okay is deprecated. Please "
@@ -717,8 +760,16 @@ def __is_master(self, host):
717760
Returns (response, connection_pool, ping_time in seconds).
718761
"""
719762
connection_pool = self.pool_class(
720-
host, self.__max_pool_size, self.__net_timeout, self.__conn_timeout,
721-
self.__use_ssl, use_greenlets=self.__use_greenlets)
763+
host,
764+
self.__max_pool_size,
765+
self.__net_timeout,
766+
self.__conn_timeout,
767+
self.__use_ssl,
768+
use_greenlets=self.__use_greenlets,
769+
ssl_keyfile=self.__ssl_keyfile,
770+
ssl_certfile=self.__ssl_certfile,
771+
ssl_cert_reqs=self.__ssl_cert_reqs,
772+
ssl_ca_certs=self.__ssl_ca_certs)
722773

723774
if self.in_request():
724775
connection_pool.start_request()

0 commit comments

Comments
 (0)