diff --git a/CHANGES.txt b/CHANGES.txt index f8f5bfc6f2..d3a26b60b4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 2.2.20 + * CQL shell should prefer newer TLS version by default (CASSANDRA-16695) * Fix Debian init start/stop (CASSANDRA-15770) * Remove ant targets list-jvm-dtests and ant list-jvm-upgrade-dtests (CASSANDRA-16519) * Fix centos packaging for arm64, >=4.0 rpm's now require python3 (CASSANDRA-16477) diff --git a/pylib/cqlshlib/sslhandling.py b/pylib/cqlshlib/sslhandling.py index 8765ffa31a..40f1b4a84e 100644 --- a/pylib/cqlshlib/sslhandling.py +++ b/pylib/cqlshlib/sslhandling.py @@ -47,6 +47,18 @@ def ssl_settings(host, config_file, env=os.environ): except ConfigParser.Error: return None + def get_best_tls_protocol(ssl_ver_str): + # newer python versions suggest to use PROTOCOL_TLS to negotiate the highest TLS version. + # older protocol versions have been deprecated: + # https://docs.python.org/2/library/ssl.html#ssl.PROTOCOL_TLS + # https://docs.python.org/3/library/ssl.html#ssl.PROTOCOL_TLS + if ssl_ver_str: + return getattr(ssl, "PROTOCOL_%s" % ssl_ver_str, None) + for protocol in ['PROTOCOL_TLS', 'PROTOCOL_TLSv1_2', 'PROTOCOL_TLSv1_1', 'PROTOCOL_TLSv1']: + if hasattr(ssl, protocol): + return getattr(ssl, protocol) + return None + ssl_validate = env.get('SSL_VALIDATE') if ssl_validate is None: ssl_validate = get_option('ssl', 'validate') @@ -55,13 +67,11 @@ def ssl_settings(host, config_file, env=os.environ): ssl_version_str = env.get('SSL_VERSION') if ssl_version_str is None: ssl_version_str = get_option('ssl', 'version') - if ssl_version_str is None: - ssl_version_str = "TLSv1" - ssl_version = getattr(ssl, "PROTOCOL_%s" % ssl_version_str, None) + ssl_version = get_best_tls_protocol(ssl_version_str) if ssl_version is None: - sys.exit("%s is not a valid SSL protocol, please use one of SSLv23, " - "TLSv1, TLSv1.1, or TLSv1.2" % (ssl_version_str,)) + sys.exit("%s is not a valid SSL protocol, please use one of " + "TLS, TLSv1_2, TLSv1_1, or TLSv1" % (ssl_version_str,)) ssl_certfile = env.get('SSL_CERTFILE') if ssl_certfile is None: diff --git a/pylib/cqlshlib/test/config/sslhandling.config b/pylib/cqlshlib/test/config/sslhandling.config new file mode 100644 index 0000000000..63f41c7a51 --- /dev/null +++ b/pylib/cqlshlib/test/config/sslhandling.config @@ -0,0 +1,2 @@ +[ssl] +version = TLSv1 \ No newline at end of file diff --git a/pylib/cqlshlib/test/config/sslhandling_invalid.config b/pylib/cqlshlib/test/config/sslhandling_invalid.config new file mode 100644 index 0000000000..90e061fe82 --- /dev/null +++ b/pylib/cqlshlib/test/config/sslhandling_invalid.config @@ -0,0 +1,2 @@ +[ssl] +version = invalid_ssl \ No newline at end of file diff --git a/pylib/cqlshlib/test/test_sslhandling.py b/pylib/cqlshlib/test/test_sslhandling.py new file mode 100644 index 0000000000..347fe2a114 --- /dev/null +++ b/pylib/cqlshlib/test/test_sslhandling.py @@ -0,0 +1,75 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cassandra.policies import SimpleConvictionPolicy +from cassandra.pool import Host +from cqlshlib.sslhandling import ssl_settings +from nose.tools import assert_raises + +import unittest +import os +import ssl + +class SslSettingsTest(unittest.TestCase): + + def setUp(self): + os.environ['SSL_VALIDATE'] = 'False' + self.config_file = 'test_config' + self.host = Host('10.0.0.1', SimpleConvictionPolicy, 9000) + + def tearDown(self): + del os.environ['SSL_VALIDATE'] + try: + del os.environ['SSL_VERSION'] + except KeyError: + pass + + def _test_ssl_version_from_env(self, version): + """ + Getting SSL version string from env variable SSL_VERSION. + """ + os.environ['SSL_VERSION'] = version + ssl_ret_val = ssl_settings(self.host, self.config_file) + assert ssl_ret_val is not None + assert ssl_ret_val.get('ssl_version') == getattr(ssl, 'PROTOCOL_%s' % version) + + def test_ssl_versions_from_env(self): + versions = ['TLS', 'TLSv1_1', 'TLSv1_2', 'TLSv1'] + for version in versions: + self._test_ssl_version_from_env(version) + + def test_invalid_ssl_versions_from_env(self): + msg = "invalid_ssl is not a valid SSL protocol, please use one of TLSv1, TLSv1_1, or TLSv1_2" + with assert_raises(SystemExit) as error: + self._test_ssl_version_from_env('invalid_ssl') + assert msg == error.exception.message + + def test_default_ssl_version(self): + ssl_ret_val = ssl_settings(self.host, self.config_file) + assert ssl_ret_val is not None + assert ssl_ret_val.get('ssl_version') == getattr(ssl, 'PROTOCOL_TLS') + + def test_ssl_version_config(self): + ssl_ret_val = ssl_settings(self.host, os.path.join('test', 'config', 'sslhandling.config')) + assert ssl_ret_val is not None + assert ssl_ret_val.get('ssl_version') == getattr(ssl, 'PROTOCOL_TLSv1') + + def test_invalid_ssl_version_config(self): + msg = "invalid_ssl is not a valid SSL protocol, please use one of TLSv1, TLSv1_1, or TLSv1_2" + with assert_raises(SystemExit) as error: + ssl_settings(self.host, os.path.join('test', 'config', 'sslhandling_invalid.config')) + assert msg in error.exception.message + \ No newline at end of file