Skip to content

Commit e619b51

Browse files
committed
WL10770: Ensure all Session connections are secure by default
Option ssl-enable is replaced by ssl-mode with the following possible values: 1. required (default) 2. disabled 3. verify_ca (ssl-ca is required) 4. verify_identity (ssl-ca is required) All values are case-insensitive. Tests have been added for regression.
1 parent a95f7e2 commit e619b51

File tree

4 files changed

+164
-70
lines changed

4 files changed

+164
-70
lines changed

lib/mysqlx/__init__.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from .compat import STRING_TYPES, urlparse, unquote, parse_qsl
3030
from .connection import Session
31+
from .constants import SSLMode
3132
from .crud import Schema, Collection, Table, View
3233
from .dbdoc import DbDoc
3334
from .errors import (Error, Warning, InterfaceError, DatabaseError,
@@ -45,6 +46,7 @@
4546

4647
_SPLIT = re.compile(r',(?![^\(\)]*\))')
4748
_PRIORITY = re.compile(r'^\(address=(.+),priority=(\d+)\)$', re.VERBOSE)
49+
ssl_opts = ["ssl-cert", "ssl-ca", "ssl-key", "ssl-crl"]
4850

4951
def _parse_address_list(path):
5052
"""Parses a list of host, port pairs
@@ -114,8 +116,14 @@ def _parse_connection_uri(uri):
114116
else:
115117
settings.update(_parse_address_list(host))
116118

117-
for opt, val in dict(parse_qsl(query_str, True)).items():
118-
settings[opt] = unquote(val.strip("()")) or True
119+
for key, val in parse_qsl(query_str, True):
120+
opt = key.lower()
121+
if opt in settings:
122+
raise InterfaceError("Duplicate option '{0}'.".format(key))
123+
if opt in ssl_opts:
124+
settings[opt] = unquote(val.strip("()"))
125+
else:
126+
settings[opt] = val.lower()
119127
return settings
120128

121129
def _validate_settings(settings):
@@ -127,6 +135,33 @@ def _validate_settings(settings):
127135
Args:
128136
settings: dict containing connection settings.
129137
"""
138+
if "routers" in settings:
139+
for router in settings["routers"]:
140+
_validate_hosts(router)
141+
elif "host" in settings:
142+
_validate_hosts(settings)
143+
144+
if "ssl-mode" in settings and settings["ssl-mode"] not in SSLMode:
145+
raise InterfaceError("Invalid SSL Mode '{0}'."
146+
"".format(settings["ssl-mode"]))
147+
elif settings.get("ssl-mode") == SSLMode.DISABLED and \
148+
any(key in settings for key in ssl_opts):
149+
raise InterfaceError("SSL options used with ssl-mode 'disabled'.")
150+
151+
if "ssl-crl" in settings and not "ssl-ca" in settings:
152+
raise InterfaceError("CA Certificate not provided.")
153+
if "ssl-key" in settings and not "ssl-cert" in settings:
154+
raise InterfaceError("Client Certificate not provided.")
155+
156+
if not "ssl-ca" in settings and settings.get("ssl-mode") \
157+
in [SSLMode.VERIFY_IDENTITY, SSLMode.VERIFY_CA]:
158+
raise InterfaceError("Cannot verify Server without CA.")
159+
if "ssl-ca" in settings and settings.get("ssl-mode") \
160+
not in [SSLMode.VERIFY_IDENTITY, SSLMode.VERIFY_CA]:
161+
raise InterfaceError("Must verify Server if CA is provided.")
162+
163+
164+
def _validate_hosts(settings):
130165
if "priority" in settings and settings["priority"]:
131166
try:
132167
settings["priority"] = int(settings["priority"])
@@ -163,16 +198,14 @@ def _get_connection_settings(*args, **kwargs):
163198
settings.update(args[0])
164199
elif kwargs:
165200
settings.update(kwargs)
201+
for key, val in settings.items():
202+
if "_" in key:
203+
settings[key.replace("_", "-")] = settings.pop(key)
166204

167205
if not settings:
168206
raise InterfaceError("Settings not provided")
169207

170-
if "routers" in settings:
171-
for router in settings.get("routers"):
172-
_validate_settings(router)
173-
else:
174-
_validate_settings(settings)
175-
208+
_validate_settings(settings)
176209
return settings
177210

178211
def get_session(*args, **kwargs):

lib/mysqlx/connection.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,33 @@
3131

3232
import sys
3333
import socket
34+
import logging
3435

3536
from functools import wraps
3637

3738
from .authentication import MySQL41AuthPlugin
3839
from .errors import InterfaceError, OperationalError, ProgrammingError
3940
from .compat import PY3, STRING_TYPES, UNICODE_TYPES
4041
from .crud import Schema
42+
from .constants import SSLMode
4143
from .protocol import Protocol, MessageReaderWriter
4244
from .result import Result, RowResult, DocResult
4345
from .statement import SqlStatement, AddStatement
4446

4547

4648
_DROP_DATABASE_QUERY = "DROP DATABASE IF EXISTS `{0}`"
4749
_CREATE_DATABASE_QUERY = "CREATE DATABASE IF NOT EXISTS `{0}`"
48-
50+
_LOGGER = logging.getLogger("mysqlx")
4951

5052
class SocketStream(object):
5153
def __init__(self):
5254
self._socket = None
5355
self._is_ssl = False
56+
self._host = None
5457

5558
def connect(self, params):
5659
if isinstance(params, tuple):
60+
self._host = params[0]
5761
s_type = socket.AF_INET6 if ":" in params[0] else socket.AF_INET
5862
else:
5963
s_type = socket.AF_UNIX
@@ -84,39 +88,46 @@ def close(self):
8488
self._socket.close()
8589
self._socket = None
8690

87-
def set_ssl(self, ssl_opts={}):
91+
def set_ssl(self, ssl_mode, ssl_ca, ssl_crl, ssl_cert, ssl_key):
8892
if not SSL_AVAILABLE:
8993
self.close()
9094
raise RuntimeError("Python installation has no SSL support.")
9195

9296
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
9397
context.load_default_certs()
94-
if "ssl-ca" in ssl_opts:
98+
99+
if ssl_ca:
95100
try:
96-
context.load_verify_locations(ssl_opts["ssl-ca"])
101+
context.load_verify_locations(ssl_ca)
97102
context.verify_mode = ssl.CERT_REQUIRED
98-
except (IOError, ssl.SSLError):
103+
except (IOError, ssl.SSLError) as err:
99104
self.close()
100-
raise InterfaceError("Invalid CA certificate.")
101-
if "ssl-crl" in ssl_opts:
105+
raise InterfaceError("Invalid CA Certificate: {}".format(err))
106+
107+
if ssl_crl:
102108
try:
103-
context.load_verify_locations(ssl_opts["ssl-crl"])
104-
context.verify_flags = ssl.VERIFY_CRL_CHECK_CHAIN
105-
except (IOError, ssl.SSLError):
109+
context.load_verify_locations(ssl_crl)
110+
context.verify_flags = ssl.VERIFY_CRL_CHECK_LEAF
111+
except (IOError, ssl.SSLError) as err:
106112
self.close()
107-
raise InterfaceError("Invalid CRL.")
108-
if "ssl-cert" in ssl_opts:
113+
raise InterfaceError("Invalid CRL: {}".format(err))
114+
115+
if ssl_cert:
109116
try:
110-
context.load_cert_chain(ssl_opts["ssl-cert"],
111-
ssl_opts.get("ssl-key", None))
112-
except (IOError, ssl.SSLError):
117+
context.load_cert_chain(ssl_cert, ssl_key)
118+
except (IOError, ssl.SSLError) as err:
113119
self.close()
114-
raise InterfaceError("Invalid Client Certificate/Key.")
115-
elif "ssl-key" in ssl_opts:
116-
self.close()
117-
raise InterfaceError("Client Certificate not provided.")
120+
raise InterfaceError("Invalid Certificate/Key: {}".format(err))
118121

119122
self._socket = context.wrap_socket(self._socket)
123+
if ssl_mode == SSLMode.VERIFY_IDENTITY:
124+
try:
125+
hostname = socket.gethostbyaddr(self._host)
126+
ssl.match_hostname(self._socket.getpeercert(), hostname[0])
127+
except ssl.CertificateError as err:
128+
self.close()
129+
raise InterfaceError("Unable to verify server identity: {}"
130+
"".format(err))
120131
self._is_ssl = True
121132

122133

@@ -223,22 +234,29 @@ def connect(self):
223234
raise InterfaceError("Failed to connect to any of the routers.", 4001)
224235

225236
def _handle_capabilities(self):
237+
if self.settings.get("ssl-mode") == SSLMode.DISABLED:
238+
return
239+
if "socket" in self.settings:
240+
if self.settings.get("ssl-mode"):
241+
_LOGGER.warning("SSL not required when using Unix socket.")
242+
return
243+
226244
data = self.protocol.get_capabilites().capabilities
227245
if not (data[0]["name"].lower() == "tls" if data else False):
228-
if self.settings.get("ssl-enable", False):
229-
self.close()
230-
raise OperationalError("SSL not enabled at server.")
231-
return
246+
self.close()
247+
raise OperationalError("SSL not enabled at server.")
232248

233249
if sys.version_info < (2, 7, 9):
234-
if self.settings.get("ssl-enable", False):
235-
self.close()
236-
raise RuntimeError("The support for SSL is not available for "
237-
"this Python version.")
238-
return
250+
self.close()
251+
raise RuntimeError("The support for SSL is not available for "
252+
"this Python version.")
239253

240254
self.protocol.set_capabilities(tls=True)
241-
self.stream.set_ssl(self.settings)
255+
self.stream.set_ssl(self.settings.get("ssl-mode", SSLMode.REQUIRED),
256+
self.settings.get("ssl-ca"),
257+
self.settings.get("ssl-crl"),
258+
self.settings.get("ssl-cert"),
259+
self.settings.get("ssl-key"))
242260

243261
def _authenticate(self):
244262
plugin = MySQL41AuthPlugin(self._user, self._password)

lib/mysqlx/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def create_enum(name, fields, values=None):
4646
Algorithms = create_enum("Algorithms", ("MERGE", "TMPTABLE", "UNDEFINED"))
4747
Securities = create_enum("Securities", ("DEFINER", "INVOKER"))
4848
CheckOptions = create_enum("CheckOptions", ("CASCADED", "LOCAL"))
49+
SSLMode = create_enum("SSLMode",
50+
("REQUIRED", "DISABLED", "VERIFY_CA", "VERIFY_IDENTITY"),
51+
("required", "disabled", "verify_ca", "verify_identity"))
4952

5053

5154
__all__ = ["Algorithms", "Securities", "CheckOptions"]

0 commit comments

Comments
 (0)