Permalink
Browse files

SSL certificate verification PYTHON-466

  • Loading branch information...
1 parent 41e2f82 commit 48046b2efd1085b793fab0ea6f90ad1611410a69 @rozza rozza committed Feb 19, 2013
View
@@ -21,6 +21,12 @@
from pymongo.read_preferences import ReadPreference
from pymongo.errors import ConfigurationError
+HAS_SSL = True
+try:
+ import ssl
+except ImportError:
+ HAS_SSL = False
+
def raise_config_error(key, dummy):
"""Raise ConfigurationError with the given key name."""
@@ -63,6 +69,33 @@ def validate_positive_integer(option, value):
return val
+def validate_readable(option, value):
+ """Validates that 'value' is file-like and readable.
+ """
+ # First make sure its a string py3.3 open(True, 'r') succeeds
+ # Used in ssl cert checking due to poor ssl module error reporting
+ value = validate_basestring(option, value)
+ open(value, 'r').close()
+ return value
+
+
+def validate_cert_reqs(option, value):
+ """Validate the cert reqs are valid. It must be None or one of the three
+ values ``ssl.CERT_NONE``, ``ssl.CERT_OPTIONAL`` or ``ssl.CERT_REQUIRED``"""
+ if value is None:
+ return value
+ if HAS_SSL:
+ if value in (ssl.CERT_NONE, ssl.CERT_OPTIONAL, ssl.CERT_REQUIRED):
+ return value
+ raise ConfigurationError("The value of %s must be one of: "
+ "`ssl.CERT_NONE`, `ssl.CERT_OPTIONAL` or "
+ "`ssl.CERT_REQUIRED" % (option,))
+ else:
+ raise ConfigurationError("The value of %s is set but can't be "
+ "validated. The ssl module is not available"
+ % (option,))
+
+
def validate_positive_integer_or_none(option, value):
"""Validate that 'value' is a positive integer or None.
"""
@@ -180,6 +213,10 @@ def validate_auth_mechanism(option, value):
'connecttimeoutms': validate_timeout_or_none,
'sockettimeoutms': validate_timeout_or_none,
'ssl': validate_boolean,
+ 'ssl_keyfile': validate_readable,
+ 'ssl_certfile': validate_readable,
+ 'ssl_cert_reqs': validate_cert_reqs,
+ 'ssl_ca_certs': validate_readable,
'readpreference': validate_read_preference,
'read_preference': validate_read_preference,
'tag_sets': validate_tag_sets,
View
@@ -135,8 +135,28 @@ def __init__(self, host=None, port=None, max_pool_size=10,
until :meth:`end_request()`
- `slave_okay` or `slaveOk` (deprecated): Use `read_preference`
instead.
+ - `ssl_keyfile`: The private keyfile used to identify the local
+ connection against mongod. If included with the ``certfile` then
+ only the ``ssl_certfile`` is needed. Implies ``ssl=True``.
+ - `ssl_certfile`: The certificate file used to identify the local
+ connection against mongod. Implies ``ssl=True``.
+ - `ssl_cert_reqs`: The parameter cert_reqs specifies whether a
+ certificate is required from the other side of the connection,
+ and whether it will be validated if provided. It must be one of the
+ three values ``ssl.CERT_NONE`` (certificates ignored),
+ ``ssl.CERT_OPTIONAL`` (not required, but validated if provided), or
+ ``ssl.CERT_REQUIRED`` (required and validated). If the value of
+ this parameter is not ``ssl.CERT_NONE``, then the ``ssl_ca_certs``
+ parameter must point to a file of CA certificates.
+ Implies ``ssl=True``.
+ - `ssl_ca_certs`: The ca_certs file contains a set of concatenated
+ "certification authority" certificates, which are used to validate
+ certificates passed from the other end of the connection.
+ Implies ``ssl=True``.
.. seealso:: :meth:`end_request`
+ .. versionchanged:: 2.4.2+
+ Added addtional ssl options
.. versionchanged:: 2.3
Added support for failover between mongos seed list members.
.. versionchanged:: 2.2
View
@@ -16,6 +16,11 @@
from bson.errors import *
+try:
+ from ssl import CertificateError
+except ImportError:
+ from pymongo.ssl_match_hostname import CertificateError
+
class PyMongoError(Exception):
"""Base class for all PyMongo exceptions.
@@ -98,6 +103,7 @@ class InvalidURI(ConfigurationError):
.. versionadded:: 1.5
"""
+
class UnsupportedOption(ConfigurationError):
"""Exception for unsupported options.
View
@@ -26,6 +26,11 @@
OperationFailure,
TimeoutError)
+try:
+ from ssl import match_hostname
+except ImportError:
+ from pymongo.ssl_match_hostname import match_hostname
+
def _index_list(key_or_list, direction=None):
"""Helper to generate a list of (key, direction) pairs.
@@ -167,4 +172,3 @@ def shuffled(sequence):
out = list(sequence)
random.shuffle(out)
return out
-
View
@@ -49,6 +49,7 @@
message,
pool,
uri_parser)
+from pymongo.common import HAS_SSL
from pymongo.cursor_manager import CursorManager
from pymongo.errors import (AutoReconnect,
ConfigurationError,
@@ -60,6 +61,7 @@
EMPTY = b("")
+
def _partition_node(node):
"""Split a host:port string returned from mongod/s into
a (host, int(port)) pair needed for socket.connect().
@@ -163,11 +165,30 @@ def __init__(self, host=None, port=None, max_pool_size=10,
- `use_greenlets`: If ``True``, :meth:`start_request()` will ensure
that the current greenlet uses the same socket for all
operations until :meth:`end_request()`
+ - `ssl_keyfile`: The private keyfile used to identify the local
+ connection against mongod. If included with the ``certfile` then
+ only the ``ssl_certfile`` is needed. Implies ``ssl=True``.
+ - `ssl_certfile`: The certificate file used to identify the local
+ connection against mongod. Implies ``ssl=True``.
+ - `ssl_cert_reqs`: Specifies whether a certificate is required from
+ the other side of the connection, and whether it will be validated
+ if provided. It must be one of the three values ``ssl.CERT_NONE``
+ (certificates ignored), ``ssl.CERT_OPTIONAL``
+ (not required, but validated if provided), or ``ssl.CERT_REQUIRED``
+ (required and validated). If the value of this parameter is not
+ ``ssl.CERT_NONE``, then the ``ssl_ca_certs`` parameter must point
+ to a file of CA certificates. Implies ``ssl=True``.
+ - `ssl_ca_certs`: The ca_certs file contains a set of concatenated
+ "certification authority" certificates, which are used to validate
+ certificates passed from the other end of the connection.
+ Implies ``ssl=True``.
.. seealso:: :meth:`end_request`
.. mongodoc:: connections
+ .. versionchanged:: 2.4.2+
+ Added addtional ssl options
.. versionadded:: 2.4
"""
if host is None:
@@ -232,21 +253,47 @@ def __init__(self, host=None, port=None, max_pool_size=10,
self.__net_timeout = options.get('sockettimeoutms')
self.__conn_timeout = options.get('connecttimeoutms')
- self.__use_ssl = options.get('ssl', False)
- if self.__use_ssl and not pool.have_ssl:
+ self.__use_ssl = options.get('ssl', None)
+ self.__ssl_keyfile = options.get('ssl_keyfile', None)
+ self.__ssl_certfile = options.get('ssl_certfile', None)
+ self.__ssl_cert_reqs = options.get('ssl_cert_reqs', None)
+ self.__ssl_ca_certs = options.get('ssl_ca_certs', None)
+
+ if self.__use_ssl and not HAS_SSL:
raise ConfigurationError("The ssl module is not available. If you "
"are using a python version previous to "
"2.6 you must install the ssl package "
"from PyPI.")
+ ssl_kwarg_keys = [k for k in kwargs.keys() if k.startswith('ssl_')]
+ if self.__use_ssl == False and ssl_kwarg_keys:
+ raise ConfigurationError("ssl has not been enabled but the "
+ "following ssl parameters have been set: "
+ "%s. Please set `ssl=True` or remove."
+ % ', '.join(ssl_kwarg_keys))
+
+ if self.__ssl_cert_reqs and not self.__ssl_ca_certs:
+ raise ConfigurationError("If `ssl_cert_reqs` is not "
+ "`ssl.CERT_NONE` then you must "
+ "include `ssl_ca_certs` to be able "
+ "to validate the server.")
+
+ if ssl_kwarg_keys and self.__use_ssl is None:
+ # ssl options imply ssl = True
+ self.__use_ssl = True
+
self.__use_greenlets = options.get('use_greenlets', False)
self.__pool = pool_class(
None,
self.__max_pool_size,
self.__net_timeout,
self.__conn_timeout,
self.__use_ssl,
- use_greenlets=self.__use_greenlets)
+ use_greenlets=self.__use_greenlets,
+ ssl_keyfile=self.__ssl_keyfile,
+ ssl_certfile=self.__ssl_certfile,
+ ssl_cert_reqs=self.__ssl_cert_reqs,
+ ssl_ca_certs=self.__ssl_ca_certs)
self.__document_class = document_class
self.__tz_aware = common.validate_boolean('tz_aware', tz_aware)
@@ -31,14 +31,14 @@
Database(MongoReplicaSetClient([u'...', u'...']), u'test_database')
"""
+import atexit
import datetime
import socket
import struct
import threading
import time
import warnings
import weakref
-import atexit
from bson.py3compat import b
from pymongo import (auth,
@@ -58,6 +58,9 @@
InvalidDocument,
OperationFailure)
+if common.HAS_SSL:
+ import ssl
+
EMPTY = b("")
MAX_BSON_SIZE = 4 * 1024 * 1024
MAX_RETRY = 3
@@ -369,7 +372,26 @@ def __init__(self, hosts_or_uri=None, max_pool_size=10,
precedence.
- `port`: For compatibility with :class:`~mongo_client.MongoClient`.
The default port number to use for hosts.
-
+ - `ssl_keyfile`: The private keyfile used to identify the local
+ connection against mongod. If included with the ``certfile` then
+ only the ``ssl_certfile`` is needed. Implies ``ssl=True``.
+ - `ssl_certfile`: The certificate file used to identify the local
+ connection against mongod. Implies ``ssl=True``.
+ - `ssl_cert_reqs`: Specifies whether a certificate is required from
+ the other side of the connection, and whether it will be validated
+ if provided. It must be one of the three values ``ssl.CERT_NONE``
+ (certificates ignored), ``ssl.CERT_OPTIONAL``
+ (not required, but validated if provided), or ``ssl.CERT_REQUIRED``
+ (required and validated). If the value of this parameter is not
+ ``ssl.CERT_NONE``, then the ``ssl_ca_certs`` parameter must point
+ to a file of CA certificates. Implies ``ssl=True``.
+ - `ssl_ca_certs`: The ca_certs file contains a set of concatenated
+ "certification authority" certificates, which are used to validate
+ certificates passed from the other end of the connection.
+ Implies ``ssl=True``.
+
+ .. versionchanged:: 2.4.2+
+ Added addtional ssl options
.. versionadded:: 2.4
"""
self.__opts = {}
@@ -437,16 +459,37 @@ def __init__(self, hosts_or_uri=None, max_pool_size=10,
raise ConfigurationError("the replicaSet "
"keyword parameter is required.")
-
self.__net_timeout = self.__opts.get('sockettimeoutms')
self.__conn_timeout = self.__opts.get('connecttimeoutms')
- self.__use_ssl = self.__opts.get('ssl', False)
- if self.__use_ssl and not pool.have_ssl:
+ self.__use_ssl = self.__opts.get('ssl', None)
+ self.__ssl_keyfile = self.__opts.get('ssl_keyfile', None)
+ self.__ssl_certfile = self.__opts.get('ssl_certfile', None)
+ self.__ssl_cert_reqs = self.__opts.get('ssl_cert_reqs', None)
+ self.__ssl_ca_certs = self.__opts.get('ssl_ca_certs', None)
+
+ if self.__use_ssl and not common.HAS_SSL:
raise ConfigurationError("The ssl module is not available. If you "
"are using a python version previous to "
"2.6 you must install the ssl package "
"from PyPI.")
+ ssl_kwarg_keys = [k for k in kwargs.keys() if k.startswith('ssl_')]
+ if self.__use_ssl == False and ssl_kwarg_keys:
+ raise ConfigurationError("ssl has not been enabled but the "
+ "following ssl parameters have been set: "
+ "%s. Please set `ssl=True` or remove."
+ % ', '.join(ssl_kwarg_keys))
+
+ if self.__ssl_cert_reqs and not self.__ssl_ca_certs:
+ raise ConfigurationError("If `ssl_cert_reqs` is not "
+ "`ssl.CERT_NONE` then you must "
+ "include `ssl_ca_certs` to be able "
+ "to validate the server.")
+
+ if ssl_kwarg_keys and self.__use_ssl is None:
+ # ssl options imply ssl = True
+ self.__use_ssl = True
+
super(MongoReplicaSetClient, self).__init__(**self.__opts)
if self.slave_okay:
warnings.warn("slave_okay is deprecated. Please "
@@ -717,8 +760,16 @@ def __is_master(self, host):
Returns (response, connection_pool, ping_time in seconds).
"""
connection_pool = self.pool_class(
- host, self.__max_pool_size, self.__net_timeout, self.__conn_timeout,
- self.__use_ssl, use_greenlets=self.__use_greenlets)
+ host,
+ self.__max_pool_size,
+ self.__net_timeout,
+ self.__conn_timeout,
+ self.__use_ssl,
+ use_greenlets=self.__use_greenlets,
+ ssl_keyfile=self.__ssl_keyfile,
+ ssl_certfile=self.__ssl_certfile,
+ ssl_cert_reqs=self.__ssl_cert_reqs,
+ ssl_ca_certs=self.__ssl_ca_certs)
if self.in_request():
connection_pool.start_request()
Oops, something went wrong.

9 comments on commit 48046b2

You forgot to change MANIFEST.in to include *.pem files in release tarballs. test_ssl.py now fails in PyMongo 2.5:

IOError: [Errno 2] No such file or directory: '/tmp/pymongo-2.5/test/certificates/client.pem'
Member

behackett replied Mar 25, 2013

Thanks for pointing that out. Fixed in bd57514.

Member

behackett replied Mar 25, 2013

Oops, meant c0673df.

I now noticed that test_ssl.py fails in different way with Jython 2.7. Jython 2.5 has no ssl module. Jython 2.7 has ssl module without majority of attributes, so e.g. ssl.CERT_NONE triggers AttributeError. I suggest:

--- pymongo/common.py
+++ pymongo/common.py
@@ -24,7 +24,8 @@
 HAS_SSL = True
 try:
     import ssl
-except ImportError:
+    ssl.CERT_NONE
+except (AttributeError, ImportError):
     HAS_SSL = False


Member

behackett replied Mar 25, 2013

Is there a Jython 2.7 ticket for this problem? 2.7 is only beta 1 so I'm not too interested in working around it's bugs.

There is rather no such bug in Jython bug tracker, but you might see:
http://hg.python.org/jython/file/11776cd9765b/Lib/ssl.py
http://wiki.python.org/jython/SSLModule

Member

behackett replied Mar 25, 2013

Interesting. Looks like it was just added Friday. I'll take a look.

That page in HG web interface mentions last change in given branch, not particular file.
http://hg.python.org/jython/log/default/Lib/ssl.py

Please sign in to comment.