[2.7] bpo-33570: TLS 1.3 ciphers for OpenSSL 1.1.1 (GH-6976) (GH-8760) (GH-10607)

Change TLS 1.3 cipher suite settings for compatibility with OpenSSL
1.1.1-pre6 and newer. OpenSSL 1.1.1 will have TLS 1.3 cipers enabled by
default.

Also update multissltests to test with latest OpenSSL.

Signed-off-by: Christian Heimes <christian@python.org>.
(cherry picked from commit 3e630c541b)
Co-authored-by: Christian Heimes <christian@python.org>
This commit is contained in:
stratakis 2019-02-15 14:17:12 +01:00 committed by Victor Stinner
parent 826a8b7081
commit c49f63c176
4 changed files with 120 additions and 79 deletions

View File

@ -294,11 +294,6 @@ purposes.
3DES was dropped from the default cipher string. 3DES was dropped from the default cipher string.
.. versionchanged:: 2.7.15
TLS 1.3 cipher suites TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384,
and TLS_CHACHA20_POLY1305_SHA256 were added to the default cipher string.
.. function:: _https_verify_certificates(enable=True) .. function:: _https_verify_certificates(enable=True)
Specifies whether or not server certificates are verified when creating Specifies whether or not server certificates are verified when creating
@ -1179,6 +1174,9 @@ to speed up repeated connections from the same clients.
when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
give the currently selected cipher. give the currently selected cipher.
OpenSSL 1.1.1 has TLS 1.3 cipher suites enabled by default. The suites
cannot be disabled with :meth:`~SSLContext.set_ciphers`.
.. method:: SSLContext.set_alpn_protocols(protocols) .. method:: SSLContext.set_alpn_protocols(protocols)
Specify which protocols the socket should advertise during the SSL/TLS Specify which protocols the socket should advertise during the SSL/TLS

View File

@ -2796,17 +2796,22 @@ else:
sock.do_handshake() sock.do_handshake()
self.assertEqual(cm.exception.errno, errno.ENOTCONN) self.assertEqual(cm.exception.errno, errno.ENOTCONN)
def test_default_ciphers(self): def test_no_shared_ciphers(self):
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) server_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
try: server_context.load_cert_chain(SIGNED_CERTFILE)
# Force a set of weak ciphers on our client context client_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.set_ciphers("DES") client_context.verify_mode = ssl.CERT_REQUIRED
except ssl.SSLError: client_context.check_hostname = True
self.skipTest("no DES cipher available")
with ThreadedEchoServer(CERTFILE, # OpenSSL enables all TLS 1.3 ciphers, enforce TLS 1.2 for test
ssl_version=ssl.PROTOCOL_SSLv23, client_context.options |= ssl.OP_NO_TLSv1_3
chatty=False) as server: # Force different suites on client and master
with closing(context.wrap_socket(socket.socket())) as s: client_context.set_ciphers("AES128")
server_context.set_ciphers("AES256")
with ThreadedEchoServer(context=server_context) as server:
s = client_context.wrap_socket(
socket.socket(),
server_hostname="localhost")
with self.assertRaises(ssl.SSLError): with self.assertRaises(ssl.SSLError):
s.connect((HOST, server.port)) s.connect((HOST, server.port))
self.assertIn("no shared cipher", str(server.conn_errors[0])) self.assertIn("no shared cipher", str(server.conn_errors[0]))
@ -2839,9 +2844,9 @@ else:
with context.wrap_socket(socket.socket()) as s: with context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port)) s.connect((HOST, server.port))
self.assertIn(s.cipher()[0], [ self.assertIn(s.cipher()[0], [
'TLS13-AES-256-GCM-SHA384', 'TLS_AES_256_GCM_SHA384',
'TLS13-CHACHA20-POLY1305-SHA256', 'TLS_CHACHA20_POLY1305_SHA256',
'TLS13-AES-128-GCM-SHA256', 'TLS_AES_128_GCM_SHA256',
]) ])
@unittest.skipUnless(ssl.HAS_ECDH, "test requires ECDH-enabled OpenSSL") @unittest.skipUnless(ssl.HAS_ECDH, "test requires ECDH-enabled OpenSSL")

View File

@ -0,0 +1,3 @@
Change TLS 1.3 cipher suite settings for compatibility with OpenSSL
1.1.1-pre6 and newer. OpenSSL 1.1.1 will have TLS 1.3 cipers enabled by
default.

View File

@ -41,15 +41,14 @@ import tarfile
log = logging.getLogger("multissl") log = logging.getLogger("multissl")
OPENSSL_OLD_VERSIONS = [ OPENSSL_OLD_VERSIONS = [
"0.9.8zc",
"0.9.8zh",
"1.0.1u", "1.0.1u",
"1.0.2o",
] ]
OPENSSL_RECENT_VERSIONS = [ OPENSSL_RECENT_VERSIONS = [
"1.0.2", "1.0.2p",
"1.0.2l", "1.1.0i",
"1.1.0f", # "1.1.1",
] ]
LIBRESSL_OLD_VERSIONS = [ LIBRESSL_OLD_VERSIONS = [
@ -59,13 +58,15 @@ LIBRESSL_OLD_VERSIONS = [
LIBRESSL_RECENT_VERSIONS = [ LIBRESSL_RECENT_VERSIONS = [
"2.5.5", "2.5.5",
"2.6.4", "2.6.5",
"2.7.1", "2.7.4",
] ]
# store files in ../multissl # store files in ../multissl
HERE = os.path.abspath(os.getcwd()) HERE = os.path.dirname(os.path.abspath(__file__))
MULTISSL_DIR = os.path.abspath(os.path.join(HERE, '..', 'multissl')) PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog='multissl', prog='multissl',
@ -77,7 +78,7 @@ parser = argparse.ArgumentParser(
parser.add_argument( parser.add_argument(
'--debug', '--debug',
action='store_true', action='store_true',
help="Enable debug mode", help="Enable debug logging",
) )
parser.add_argument( parser.add_argument(
'--disable-ancient', '--disable-ancient',
@ -120,32 +121,49 @@ parser.add_argument(
help="Disable network tests." help="Disable network tests."
) )
parser.add_argument( parser.add_argument(
'--compile-only', '--steps',
action='store_true', choices=['library', 'modules', 'tests'],
help="Don't run tests, only compile _ssl.c and _hashopenssl.c." default='tests',
help=(
"Which steps to perform. 'library' downloads and compiles OpenSSL "
"or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
"all and runs the test suite."
)
) )
parser.add_argument(
'--force',
action='store_true',
dest='force',
help="Force build and installation."
)
parser.add_argument(
'--keep-sources',
action='store_true',
dest='keep_sources',
help="Keep original sources for debugging."
)
class AbstractBuilder(object): class AbstractBuilder(object):
library = None library = None
url_template = None url_template = None
src_template = None src_template = None
build_template = None build_template = None
install_target = 'install'
module_files = ("Modules/_ssl.c", module_files = ("Modules/_ssl.c",
"Modules/_hashopenssl.c") "Modules/_hashopenssl.c")
module_libs = ("_ssl", "_hashlib") module_libs = ("_ssl", "_hashlib")
def __init__(self, version, compile_args=(), def __init__(self, version, args):
basedir=MULTISSL_DIR):
self.version = version self.version = version
self.compile_args = compile_args self.args = args
# installation directory # installation directory
self.install_dir = os.path.join( self.install_dir = os.path.join(
os.path.join(basedir, self.library.lower()), version os.path.join(args.base_directory, self.library.lower()), version
) )
# source file # source file
self.src_dir = os.path.join(basedir, 'src') self.src_dir = os.path.join(args.base_directory, 'src')
self.src_file = os.path.join( self.src_file = os.path.join(
self.src_dir, self.src_template.format(version)) self.src_dir, self.src_template.format(version))
# build directory (removed after install) # build directory (removed after install)
@ -252,21 +270,29 @@ class AbstractBuilder(object):
def _build_src(self): def _build_src(self):
"""Now build openssl""" """Now build openssl"""
log.info("Running build in {}".format(self.build_dir)) log.info("Running build in {}".format(self.build_dir))
cwd = self.build_dir cmd = [
cmd = ["./config", "shared", "--prefix={}".format(self.install_dir)] "./config",
cmd.extend(self.compile_args) "shared", "--debug",
"--prefix={}".format(self.install_dir)
]
env = os.environ.copy()
# set rpath
env["LD_RUN_PATH"] = self.lib_dir
self._subprocess_call(cmd, cwd=cwd) self._subprocess_call(cmd, cwd=cwd)
# Old OpenSSL versions do not support parallel builds. # Old OpenSSL versions do not support parallel builds.
self._subprocess_call(["make", "-j1"], cwd=cwd) self._subprocess_call(["make", "-j1"], cwd=cwd)
def _make_install(self, remove=True): def _make_install(self):
self._subprocess_call(["make", "-j1", "install"], cwd=self.build_dir) self._subprocess_call(
if remove: ["make", "-j1", self.install_target],
cwd=self.build_dir
)
if not self.args.keep_sources:
shutil.rmtree(self.build_dir) shutil.rmtree(self.build_dir)
def install(self): def install(self):
log.info(self.openssl_cli) log.info(self.openssl_cli)
if not self.has_openssl: if not self.has_openssl or self.args.force:
if not self.has_src: if not self.has_src:
self._download_src() self._download_src()
else: else:
@ -332,6 +358,8 @@ class BuildOpenSSL(AbstractBuilder):
url_template = "https://www.openssl.org/source/openssl-{}.tar.gz" url_template = "https://www.openssl.org/source/openssl-{}.tar.gz"
src_template = "openssl-{}.tar.gz" src_template = "openssl-{}.tar.gz"
build_template = "openssl-{}" build_template = "openssl-{}"
# only install software, skip docs
install_target = 'install_sw'
class BuildLibreSSL(AbstractBuilder): class BuildLibreSSL(AbstractBuilder):
@ -370,10 +398,11 @@ def main():
start = datetime.now() start = datetime.now()
for name in ['python', 'setup.py', 'Modules/_ssl.c']: if args.steps in {'modules', 'tests'}:
if not os.path.isfile(name): for name in ['setup.py', 'Modules/_ssl.c']:
if not os.path.isfile(os.path.join(PYTHONROOT, name)):
parser.error( parser.error(
"Must be executed from CPython build dir" "Must be executed with ./python from CPython build dir"
) )
if not os.path.samefile('python', sys.executable): if not os.path.samefile('python', sys.executable):
parser.error( parser.error(
@ -387,20 +416,27 @@ def main():
builds = [] builds = []
for version in args.openssl: for version in args.openssl:
build = BuildOpenSSL(version) build = BuildOpenSSL(
version,
args
)
build.install() build.install()
builds.append(build) builds.append(build)
for version in args.libressl: for version in args.libressl:
build = BuildLibreSSL(version) build = BuildLibreSSL(
version,
args
)
build.install() build.install()
builds.append(build) builds.append(build)
if args.steps in {'modules', 'tests'}:
for build in builds: for build in builds:
try: try:
build.recompile_pymods() build.recompile_pymods()
build.check_pyssl() build.check_pyssl()
if not args.compile_only: if args.steps == 'tests':
build.run_python_tests( build.run_python_tests(
tests=args.tests, tests=args.tests,
network=args.network, network=args.network,
@ -410,14 +446,13 @@ def main():
print("{} failed: {}".format(build, e), file=sys.stderr) print("{} failed: {}".format(build, e), file=sys.stderr)
sys.exit(2) sys.exit(2)
print("\n{} finished in {}".format( log.info("\n{} finished in {}".format(
"Tests" if not args.compile_only else "Builds", args.steps.capitalize(),
datetime.now() - start datetime.now() - start
)) ))
print('Python: ', sys.version) print('Python: ', sys.version)
if args.compile_only: if args.steps == 'tests':
print('Build only') if args.tests:
elif args.tests:
print('Executed Tests:', ' '.join(args.tests)) print('Executed Tests:', ' '.join(args.tests))
else: else:
print('Executed all SSL tests.') print('Executed all SSL tests.')