blob: 12af98d12c45dbb6309fac694959ab4a3af15fb8 [file] [log] [blame]
Christian Heimesd3b9f972017-09-06 18:59:22 -07001#!./python
2"""Run Python tests against multiple installations of OpenSSL and LibreSSL
3
4The script
5
6 (1) downloads OpenSSL / LibreSSL tar bundle
7 (2) extracts it to ./src
8 (3) compiles OpenSSL / LibreSSL
9 (4) installs OpenSSL / LibreSSL into ../multissl/$LIB/$VERSION/
10 (5) forces a recompilation of Python modules using the
11 header and library files from ../multissl/$LIB/$VERSION/
12 (6) runs Python's test suite
13
14The script must be run with Python's build directory as current working
15directory.
16
17The script uses LD_RUN_PATH, LD_LIBRARY_PATH, CPPFLAGS and LDFLAGS to bend
18search paths for header files and shared libraries. It's known to work on
19Linux with GCC and clang.
20
21Please keep this script compatible with Python 2.7, and 3.4 to 3.7.
22
23(c) 2013-2017 Christian Heimes <christian@python.org>
24"""
25from __future__ import print_function
26
27import argparse
28from datetime import datetime
29import logging
30import os
31try:
32 from urllib.request import urlopen
Christian Heimes938717f2020-05-15 22:32:25 +020033 from urllib.error import HTTPError
Christian Heimesd3b9f972017-09-06 18:59:22 -070034except ImportError:
Christian Heimes938717f2020-05-15 22:32:25 +020035 from urllib2 import urlopen, HTTPError
Christian Heimesd3b9f972017-09-06 18:59:22 -070036import shutil
Christian Heimes938717f2020-05-15 22:32:25 +020037import string
38import subprocess
Christian Heimesd3b9f972017-09-06 18:59:22 -070039import sys
40import tarfile
41
42
43log = logging.getLogger("multissl")
44
45OPENSSL_OLD_VERSIONS = [
Christian Heimes6e8cda92020-05-16 03:33:05 +020046 "1.0.2u",
47 "1.1.0l",
Christian Heimesd3b9f972017-09-06 18:59:22 -070048]
49
50OPENSSL_RECENT_VERSIONS = [
Christian Heimes62d618c2020-05-15 18:48:25 +020051 "1.1.1g",
52 # "3.0.0-alpha2"
Christian Heimesd3b9f972017-09-06 18:59:22 -070053]
54
55LIBRESSL_OLD_VERSIONS = [
Christian Heimes6e8cda92020-05-16 03:33:05 +020056 "2.9.2",
Christian Heimesd3b9f972017-09-06 18:59:22 -070057]
58
59LIBRESSL_RECENT_VERSIONS = [
Christian Heimes6e8cda92020-05-16 03:33:05 +020060 "3.1.0",
Christian Heimesd3b9f972017-09-06 18:59:22 -070061]
62
63# store files in ../multissl
Christian Heimesced9cb52018-01-16 21:02:26 +010064HERE = os.path.dirname(os.path.abspath(__file__))
65PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
66MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
67
Christian Heimesd3b9f972017-09-06 18:59:22 -070068
69parser = argparse.ArgumentParser(
70 prog='multissl',
71 description=(
72 "Run CPython tests with multiple OpenSSL and LibreSSL "
73 "versions."
74 )
75)
76parser.add_argument(
77 '--debug',
78 action='store_true',
Christian Heimes529525f2018-05-23 22:24:45 +020079 help="Enable debug logging",
Christian Heimesd3b9f972017-09-06 18:59:22 -070080)
81parser.add_argument(
82 '--disable-ancient',
83 action='store_true',
Christian Heimes6e8cda92020-05-16 03:33:05 +020084 help="Don't test OpenSSL and LibreSSL versions without upstream support",
Christian Heimesd3b9f972017-09-06 18:59:22 -070085)
86parser.add_argument(
87 '--openssl',
88 nargs='+',
89 default=(),
90 help=(
91 "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
92 "OpenSSL and LibreSSL versions are given."
93 ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
94)
95parser.add_argument(
96 '--libressl',
97 nargs='+',
98 default=(),
99 help=(
100 "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
101 "OpenSSL and LibreSSL versions are given."
102 ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
103)
104parser.add_argument(
105 '--tests',
106 nargs='*',
107 default=(),
108 help="Python tests to run, defaults to all SSL related tests.",
109)
110parser.add_argument(
111 '--base-directory',
112 default=MULTISSL_DIR,
113 help="Base directory for OpenSSL / LibreSSL sources and builds."
114)
115parser.add_argument(
116 '--no-network',
117 action='store_false',
118 dest='network',
119 help="Disable network tests."
120)
121parser.add_argument(
Christian Heimesced9cb52018-01-16 21:02:26 +0100122 '--steps',
123 choices=['library', 'modules', 'tests'],
124 default='tests',
125 help=(
126 "Which steps to perform. 'library' downloads and compiles OpenSSL "
127 "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
128 "all and runs the test suite."
129 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700130)
Steve Dowere5f41d22018-05-16 17:50:29 -0400131parser.add_argument(
132 '--system',
133 default='',
134 help="Override the automatic system type detection."
135)
Christian Heimes529525f2018-05-23 22:24:45 +0200136parser.add_argument(
137 '--force',
138 action='store_true',
139 dest='force',
140 help="Force build and installation."
141)
142parser.add_argument(
143 '--keep-sources',
144 action='store_true',
145 dest='keep_sources',
146 help="Keep original sources for debugging."
147)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700148
Christian Heimes62d618c2020-05-15 18:48:25 +0200149OPENSSL_FIPS_CNF = """\
150openssl_conf = openssl_init
151
152.include {self.install_dir}/ssl/fipsinstall.cnf
153# .include {self.install_dir}/ssl/openssl.cnf
154
155[openssl_init]
156providers = provider_sect
157
158[provider_sect]
159fips = fips_sect
160default = default_sect
161
162[default_sect]
163activate = 1
164"""
165
Christian Heimesd3b9f972017-09-06 18:59:22 -0700166
167class AbstractBuilder(object):
168 library = None
Christian Heimes938717f2020-05-15 22:32:25 +0200169 url_templates = None
Christian Heimesd3b9f972017-09-06 18:59:22 -0700170 src_template = None
171 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100172 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700173
174 module_files = ("Modules/_ssl.c",
175 "Modules/_hashopenssl.c")
176 module_libs = ("_ssl", "_hashlib")
177
Christian Heimesced9cb52018-01-16 21:02:26 +0100178 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700179 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100180 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700181 # installation directory
182 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100183 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700184 )
185 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100186 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700187 self.src_file = os.path.join(
188 self.src_dir, self.src_template.format(version))
189 # build directory (removed after install)
190 self.build_dir = os.path.join(
191 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400192 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700193
194 def __str__(self):
195 return "<{0.__class__.__name__} for {0.version}>".format(self)
196
197 def __eq__(self, other):
198 if not isinstance(other, AbstractBuilder):
199 return NotImplemented
200 return (
201 self.library == other.library
202 and self.version == other.version
203 )
204
205 def __hash__(self):
206 return hash((self.library, self.version))
207
208 @property
Christian Heimes938717f2020-05-15 22:32:25 +0200209 def short_version(self):
210 """Short version for OpenSSL download URL"""
211 return None
212
213 @property
Christian Heimesd3b9f972017-09-06 18:59:22 -0700214 def openssl_cli(self):
215 """openssl CLI binary"""
216 return os.path.join(self.install_dir, "bin", "openssl")
217
218 @property
219 def openssl_version(self):
220 """output of 'bin/openssl version'"""
221 cmd = [self.openssl_cli, "version"]
222 return self._subprocess_output(cmd)
223
224 @property
225 def pyssl_version(self):
226 """Value of ssl.OPENSSL_VERSION"""
227 cmd = [
228 sys.executable,
229 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
230 ]
231 return self._subprocess_output(cmd)
232
233 @property
234 def include_dir(self):
235 return os.path.join(self.install_dir, "include")
236
237 @property
238 def lib_dir(self):
239 return os.path.join(self.install_dir, "lib")
240
241 @property
242 def has_openssl(self):
243 return os.path.isfile(self.openssl_cli)
244
245 @property
246 def has_src(self):
247 return os.path.isfile(self.src_file)
248
249 def _subprocess_call(self, cmd, env=None, **kwargs):
250 log.debug("Call '{}'".format(" ".join(cmd)))
251 return subprocess.check_call(cmd, env=env, **kwargs)
252
253 def _subprocess_output(self, cmd, env=None, **kwargs):
254 log.debug("Call '{}'".format(" ".join(cmd)))
255 if env is None:
256 env = os.environ.copy()
257 env["LD_LIBRARY_PATH"] = self.lib_dir
258 out = subprocess.check_output(cmd, env=env, **kwargs)
259 return out.strip().decode("utf-8")
260
261 def _download_src(self):
262 """Download sources"""
263 src_dir = os.path.dirname(self.src_file)
264 if not os.path.isdir(src_dir):
265 os.makedirs(src_dir)
Christian Heimes938717f2020-05-15 22:32:25 +0200266 data = None
267 for url_template in self.url_templates:
268 url = url_template.format(v=self.version, s=self.short_version)
269 log.info("Downloading from {}".format(url))
270 try:
271 req = urlopen(url)
272 # KISS, read all, write all
273 data = req.read()
274 except HTTPError as e:
275 log.error(
276 "Download from {} has from failed: {}".format(url, e)
277 )
278 else:
279 log.info("Successfully downloaded from {}".format(url))
280 break
281 if data is None:
282 raise ValueError("All download URLs have failed")
Christian Heimesd3b9f972017-09-06 18:59:22 -0700283 log.info("Storing {}".format(self.src_file))
284 with open(self.src_file, "wb") as f:
285 f.write(data)
286
287 def _unpack_src(self):
288 """Unpack tar.gz bundle"""
289 # cleanup
290 if os.path.isdir(self.build_dir):
291 shutil.rmtree(self.build_dir)
292 os.makedirs(self.build_dir)
293
294 tf = tarfile.open(self.src_file)
295 name = self.build_template.format(self.version)
296 base = name + '/'
297 # force extraction into build dir
298 members = tf.getmembers()
299 for member in list(members):
300 if member.name == name:
301 members.remove(member)
302 elif not member.name.startswith(base):
303 raise ValueError(member.name, base)
304 member.name = member.name[len(base):].lstrip('/')
305 log.info("Unpacking files to {}".format(self.build_dir))
306 tf.extractall(self.build_dir, members)
307
308 def _build_src(self):
309 """Now build openssl"""
310 log.info("Running build in {}".format(self.build_dir))
311 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200312 cmd = [
313 "./config",
314 "shared", "--debug",
315 "--prefix={}".format(self.install_dir)
316 ]
317 env = os.environ.copy()
318 # set rpath
319 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400320 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400321 env['SYSTEM'] = self.system
322 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700323 # Old OpenSSL versions do not support parallel builds.
Steve Dowere5f41d22018-05-16 17:50:29 -0400324 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700325
Christian Heimes529525f2018-05-23 22:24:45 +0200326 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100327 self._subprocess_call(
328 ["make", "-j1", self.install_target],
329 cwd=self.build_dir
330 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200331 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200332 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700333 shutil.rmtree(self.build_dir)
334
Christian Heimes62d618c2020-05-15 18:48:25 +0200335 def _post_install(self):
336 pass
337
Christian Heimesd3b9f972017-09-06 18:59:22 -0700338 def install(self):
339 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200340 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700341 if not self.has_src:
342 self._download_src()
343 else:
344 log.debug("Already has src {}".format(self.src_file))
345 self._unpack_src()
346 self._build_src()
347 self._make_install()
348 else:
349 log.info("Already has installation {}".format(self.install_dir))
350 # validate installation
351 version = self.openssl_version
352 if self.version not in version:
353 raise ValueError(version)
354
355 def recompile_pymods(self):
356 log.warning("Using build from {}".format(self.build_dir))
357 # force a rebuild of all modules that use OpenSSL APIs
358 for fname in self.module_files:
359 os.utime(fname, None)
360 # remove all build artefacts
361 for root, dirs, files in os.walk('build'):
362 for filename in files:
363 if filename.startswith(self.module_libs):
364 os.unlink(os.path.join(root, filename))
365
366 # overwrite header and library search paths
367 env = os.environ.copy()
368 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
369 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
370 # set rpath
371 env["LD_RUN_PATH"] = self.lib_dir
372
373 log.info("Rebuilding Python modules")
374 cmd = [sys.executable, "setup.py", "build"]
375 self._subprocess_call(cmd, env=env)
376 self.check_imports()
377
378 def check_imports(self):
379 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
380 self._subprocess_call(cmd)
381
382 def check_pyssl(self):
383 version = self.pyssl_version
384 if self.version not in version:
385 raise ValueError(version)
386
387 def run_python_tests(self, tests, network=True):
388 if not tests:
389 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
390 elif sys.version_info < (3, 3):
391 cmd = [sys.executable, '-m', 'test.regrtest']
392 else:
393 cmd = [sys.executable, '-m', 'test', '-j0']
394 if network:
395 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
396 cmd.extend(['-w', '-r'])
397 cmd.extend(tests)
398 self._subprocess_call(cmd, stdout=None)
399
400
401class BuildOpenSSL(AbstractBuilder):
402 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200403 url_templates = (
404 "https://www.openssl.org/source/openssl-{v}.tar.gz",
405 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
406 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700407 src_template = "openssl-{}.tar.gz"
408 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100409 # only install software, skip docs
410 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700411
Christian Heimes62d618c2020-05-15 18:48:25 +0200412 def _post_install(self):
413 if self.version.startswith("3.0"):
414 self._post_install_300()
415
416 def _post_install_300(self):
417 # create ssl/ subdir with example configs
418 self._subprocess_call(
419 ["make", "-j1", "install_ssldirs"],
420 cwd=self.build_dir
421 )
422 # Install FIPS module
423 # https://wiki.openssl.org/index.php/OpenSSL_3.0#Completing_the_installation_of_the_FIPS_Module
424 fipsinstall_cnf = os.path.join(
425 self.install_dir, "ssl", "fipsinstall.cnf"
426 )
427 openssl_fips_cnf = os.path.join(
428 self.install_dir, "ssl", "openssl-fips.cnf"
429 )
430 fips_mod = os.path.join(self.lib_dir, "ossl-modules/fips.so")
431 self._subprocess_call(
432 [
433 self.openssl_cli, "fipsinstall",
434 "-out", fipsinstall_cnf,
435 "-module", fips_mod,
436 "-provider_name", "fips",
437 "-mac_name", "HMAC",
438 "-macopt", "digest:SHA256",
439 "-macopt", "hexkey:00",
440 "-section_name", "fips_sect"
441 ]
442 )
443 with open(openssl_fips_cnf, "w") as f:
444 f.write(OPENSSL_FIPS_CNF.format(self=self))
Christian Heimes938717f2020-05-15 22:32:25 +0200445 @property
446 def short_version(self):
447 """Short version for OpenSSL download URL"""
448 short_version = self.version.rstrip(string.ascii_letters)
449 if short_version.startswith("0.9"):
450 short_version = "0.9.x"
451 return short_version
Christian Heimes62d618c2020-05-15 18:48:25 +0200452
Christian Heimesd3b9f972017-09-06 18:59:22 -0700453
454class BuildLibreSSL(AbstractBuilder):
455 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200456 url_templates = (
457 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
458 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700459 src_template = "libressl-{}.tar.gz"
460 build_template = "libressl-{}"
461
462
463def configure_make():
464 if not os.path.isfile('Makefile'):
465 log.info('Running ./configure')
466 subprocess.check_call([
467 './configure', '--config-cache', '--quiet',
468 '--with-pydebug'
469 ])
470
471 log.info('Running make')
472 subprocess.check_call(['make', '--quiet'])
473
474
475def main():
476 args = parser.parse_args()
477 if not args.openssl and not args.libressl:
478 args.openssl = list(OPENSSL_RECENT_VERSIONS)
479 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
480 if not args.disable_ancient:
481 args.openssl.extend(OPENSSL_OLD_VERSIONS)
482 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
483
484 logging.basicConfig(
485 level=logging.DEBUG if args.debug else logging.INFO,
486 format="*** %(levelname)s %(message)s"
487 )
488
489 start = datetime.now()
490
Christian Heimesced9cb52018-01-16 21:02:26 +0100491 if args.steps in {'modules', 'tests'}:
492 for name in ['setup.py', 'Modules/_ssl.c']:
493 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
494 parser.error(
495 "Must be executed from CPython build dir"
496 )
497 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700498 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100499 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700500 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100501 # check for configure and run make
502 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700503
504 # download and register builder
505 builds = []
506
507 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100508 build = BuildOpenSSL(
509 version,
510 args
511 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700512 build.install()
513 builds.append(build)
514
515 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100516 build = BuildLibreSSL(
517 version,
518 args
519 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700520 build.install()
521 builds.append(build)
522
Christian Heimesced9cb52018-01-16 21:02:26 +0100523 if args.steps in {'modules', 'tests'}:
524 for build in builds:
525 try:
526 build.recompile_pymods()
527 build.check_pyssl()
528 if args.steps == 'tests':
529 build.run_python_tests(
530 tests=args.tests,
531 network=args.network,
532 )
533 except Exception as e:
534 log.exception("%s failed", build)
535 print("{} failed: {}".format(build, e), file=sys.stderr)
536 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700537
Christian Heimesced9cb52018-01-16 21:02:26 +0100538 log.info("\n{} finished in {}".format(
539 args.steps.capitalize(),
540 datetime.now() - start
541 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700542 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100543 if args.steps == 'tests':
544 if args.tests:
545 print('Executed Tests:', ' '.join(args.tests))
546 else:
547 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700548
549 print('OpenSSL / LibreSSL versions:')
550 for build in builds:
551 print(" * {0.library} {0.version}".format(build))
552
553
554if __name__ == "__main__":
555 main()