blob: 0e37ec1bba93b2f7d799b94cbe072b553868c44e [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 Heimesd3b9f972017-09-06 18:59:22 -070046]
47
48OPENSSL_RECENT_VERSIONS = [
Christian Heimes62d618c2020-05-15 18:48:25 +020049 "1.0.2u",
Christian Heimes58ab1342019-09-11 18:45:52 +020050 "1.1.0l",
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 Heimesd3b9f972017-09-06 18:59:22 -070056]
57
58LIBRESSL_RECENT_VERSIONS = [
Christian Heimes58ab1342019-09-11 18:45:52 +020059 "2.9.2",
Christian Heimesd3b9f972017-09-06 18:59:22 -070060]
61
62# store files in ../multissl
Christian Heimesced9cb52018-01-16 21:02:26 +010063HERE = os.path.dirname(os.path.abspath(__file__))
64PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
65MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
66
Christian Heimesd3b9f972017-09-06 18:59:22 -070067
68parser = argparse.ArgumentParser(
69 prog='multissl',
70 description=(
71 "Run CPython tests with multiple OpenSSL and LibreSSL "
72 "versions."
73 )
74)
75parser.add_argument(
76 '--debug',
77 action='store_true',
Christian Heimes529525f2018-05-23 22:24:45 +020078 help="Enable debug logging",
Christian Heimesd3b9f972017-09-06 18:59:22 -070079)
80parser.add_argument(
81 '--disable-ancient',
82 action='store_true',
83 help="Don't test OpenSSL < 1.0.2 and LibreSSL < 2.5.3.",
84)
85parser.add_argument(
86 '--openssl',
87 nargs='+',
88 default=(),
89 help=(
90 "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
91 "OpenSSL and LibreSSL versions are given."
92 ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
93)
94parser.add_argument(
95 '--libressl',
96 nargs='+',
97 default=(),
98 help=(
99 "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
100 "OpenSSL and LibreSSL versions are given."
101 ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
102)
103parser.add_argument(
104 '--tests',
105 nargs='*',
106 default=(),
107 help="Python tests to run, defaults to all SSL related tests.",
108)
109parser.add_argument(
110 '--base-directory',
111 default=MULTISSL_DIR,
112 help="Base directory for OpenSSL / LibreSSL sources and builds."
113)
114parser.add_argument(
115 '--no-network',
116 action='store_false',
117 dest='network',
118 help="Disable network tests."
119)
120parser.add_argument(
Christian Heimesced9cb52018-01-16 21:02:26 +0100121 '--steps',
122 choices=['library', 'modules', 'tests'],
123 default='tests',
124 help=(
125 "Which steps to perform. 'library' downloads and compiles OpenSSL "
126 "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
127 "all and runs the test suite."
128 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700129)
Steve Dowere5f41d22018-05-16 17:50:29 -0400130parser.add_argument(
131 '--system',
132 default='',
133 help="Override the automatic system type detection."
134)
Christian Heimes529525f2018-05-23 22:24:45 +0200135parser.add_argument(
136 '--force',
137 action='store_true',
138 dest='force',
139 help="Force build and installation."
140)
141parser.add_argument(
142 '--keep-sources',
143 action='store_true',
144 dest='keep_sources',
145 help="Keep original sources for debugging."
146)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700147
Christian Heimes62d618c2020-05-15 18:48:25 +0200148OPENSSL_FIPS_CNF = """\
149openssl_conf = openssl_init
150
151.include {self.install_dir}/ssl/fipsinstall.cnf
152# .include {self.install_dir}/ssl/openssl.cnf
153
154[openssl_init]
155providers = provider_sect
156
157[provider_sect]
158fips = fips_sect
159default = default_sect
160
161[default_sect]
162activate = 1
163"""
164
Christian Heimesd3b9f972017-09-06 18:59:22 -0700165
166class AbstractBuilder(object):
167 library = None
Christian Heimes938717f2020-05-15 22:32:25 +0200168 url_templates = None
Christian Heimesd3b9f972017-09-06 18:59:22 -0700169 src_template = None
170 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100171 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700172
173 module_files = ("Modules/_ssl.c",
174 "Modules/_hashopenssl.c")
175 module_libs = ("_ssl", "_hashlib")
176
Christian Heimesced9cb52018-01-16 21:02:26 +0100177 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700178 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100179 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700180 # installation directory
181 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100182 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700183 )
184 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100185 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700186 self.src_file = os.path.join(
187 self.src_dir, self.src_template.format(version))
188 # build directory (removed after install)
189 self.build_dir = os.path.join(
190 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400191 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700192
193 def __str__(self):
194 return "<{0.__class__.__name__} for {0.version}>".format(self)
195
196 def __eq__(self, other):
197 if not isinstance(other, AbstractBuilder):
198 return NotImplemented
199 return (
200 self.library == other.library
201 and self.version == other.version
202 )
203
204 def __hash__(self):
205 return hash((self.library, self.version))
206
207 @property
Christian Heimes938717f2020-05-15 22:32:25 +0200208 def short_version(self):
209 """Short version for OpenSSL download URL"""
210 return None
211
212 @property
Christian Heimesd3b9f972017-09-06 18:59:22 -0700213 def openssl_cli(self):
214 """openssl CLI binary"""
215 return os.path.join(self.install_dir, "bin", "openssl")
216
217 @property
218 def openssl_version(self):
219 """output of 'bin/openssl version'"""
220 cmd = [self.openssl_cli, "version"]
221 return self._subprocess_output(cmd)
222
223 @property
224 def pyssl_version(self):
225 """Value of ssl.OPENSSL_VERSION"""
226 cmd = [
227 sys.executable,
228 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
229 ]
230 return self._subprocess_output(cmd)
231
232 @property
233 def include_dir(self):
234 return os.path.join(self.install_dir, "include")
235
236 @property
237 def lib_dir(self):
238 return os.path.join(self.install_dir, "lib")
239
240 @property
241 def has_openssl(self):
242 return os.path.isfile(self.openssl_cli)
243
244 @property
245 def has_src(self):
246 return os.path.isfile(self.src_file)
247
248 def _subprocess_call(self, cmd, env=None, **kwargs):
249 log.debug("Call '{}'".format(" ".join(cmd)))
250 return subprocess.check_call(cmd, env=env, **kwargs)
251
252 def _subprocess_output(self, cmd, env=None, **kwargs):
253 log.debug("Call '{}'".format(" ".join(cmd)))
254 if env is None:
255 env = os.environ.copy()
256 env["LD_LIBRARY_PATH"] = self.lib_dir
257 out = subprocess.check_output(cmd, env=env, **kwargs)
258 return out.strip().decode("utf-8")
259
260 def _download_src(self):
261 """Download sources"""
262 src_dir = os.path.dirname(self.src_file)
263 if not os.path.isdir(src_dir):
264 os.makedirs(src_dir)
Christian Heimes938717f2020-05-15 22:32:25 +0200265 data = None
266 for url_template in self.url_templates:
267 url = url_template.format(v=self.version, s=self.short_version)
268 log.info("Downloading from {}".format(url))
269 try:
270 req = urlopen(url)
271 # KISS, read all, write all
272 data = req.read()
273 except HTTPError as e:
274 log.error(
275 "Download from {} has from failed: {}".format(url, e)
276 )
277 else:
278 log.info("Successfully downloaded from {}".format(url))
279 break
280 if data is None:
281 raise ValueError("All download URLs have failed")
Christian Heimesd3b9f972017-09-06 18:59:22 -0700282 log.info("Storing {}".format(self.src_file))
283 with open(self.src_file, "wb") as f:
284 f.write(data)
285
286 def _unpack_src(self):
287 """Unpack tar.gz bundle"""
288 # cleanup
289 if os.path.isdir(self.build_dir):
290 shutil.rmtree(self.build_dir)
291 os.makedirs(self.build_dir)
292
293 tf = tarfile.open(self.src_file)
294 name = self.build_template.format(self.version)
295 base = name + '/'
296 # force extraction into build dir
297 members = tf.getmembers()
298 for member in list(members):
299 if member.name == name:
300 members.remove(member)
301 elif not member.name.startswith(base):
302 raise ValueError(member.name, base)
303 member.name = member.name[len(base):].lstrip('/')
304 log.info("Unpacking files to {}".format(self.build_dir))
305 tf.extractall(self.build_dir, members)
306
307 def _build_src(self):
308 """Now build openssl"""
309 log.info("Running build in {}".format(self.build_dir))
310 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200311 cmd = [
312 "./config",
313 "shared", "--debug",
314 "--prefix={}".format(self.install_dir)
315 ]
316 env = os.environ.copy()
317 # set rpath
318 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400319 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400320 env['SYSTEM'] = self.system
321 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700322 # Old OpenSSL versions do not support parallel builds.
Steve Dowere5f41d22018-05-16 17:50:29 -0400323 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700324
Christian Heimes529525f2018-05-23 22:24:45 +0200325 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100326 self._subprocess_call(
327 ["make", "-j1", self.install_target],
328 cwd=self.build_dir
329 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200330 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200331 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700332 shutil.rmtree(self.build_dir)
333
Christian Heimes62d618c2020-05-15 18:48:25 +0200334 def _post_install(self):
335 pass
336
Christian Heimesd3b9f972017-09-06 18:59:22 -0700337 def install(self):
338 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200339 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700340 if not self.has_src:
341 self._download_src()
342 else:
343 log.debug("Already has src {}".format(self.src_file))
344 self._unpack_src()
345 self._build_src()
346 self._make_install()
347 else:
348 log.info("Already has installation {}".format(self.install_dir))
349 # validate installation
350 version = self.openssl_version
351 if self.version not in version:
352 raise ValueError(version)
353
354 def recompile_pymods(self):
355 log.warning("Using build from {}".format(self.build_dir))
356 # force a rebuild of all modules that use OpenSSL APIs
357 for fname in self.module_files:
358 os.utime(fname, None)
359 # remove all build artefacts
360 for root, dirs, files in os.walk('build'):
361 for filename in files:
362 if filename.startswith(self.module_libs):
363 os.unlink(os.path.join(root, filename))
364
365 # overwrite header and library search paths
366 env = os.environ.copy()
367 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
368 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
369 # set rpath
370 env["LD_RUN_PATH"] = self.lib_dir
371
372 log.info("Rebuilding Python modules")
373 cmd = [sys.executable, "setup.py", "build"]
374 self._subprocess_call(cmd, env=env)
375 self.check_imports()
376
377 def check_imports(self):
378 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
379 self._subprocess_call(cmd)
380
381 def check_pyssl(self):
382 version = self.pyssl_version
383 if self.version not in version:
384 raise ValueError(version)
385
386 def run_python_tests(self, tests, network=True):
387 if not tests:
388 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
389 elif sys.version_info < (3, 3):
390 cmd = [sys.executable, '-m', 'test.regrtest']
391 else:
392 cmd = [sys.executable, '-m', 'test', '-j0']
393 if network:
394 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
395 cmd.extend(['-w', '-r'])
396 cmd.extend(tests)
397 self._subprocess_call(cmd, stdout=None)
398
399
400class BuildOpenSSL(AbstractBuilder):
401 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200402 url_templates = (
403 "https://www.openssl.org/source/openssl-{v}.tar.gz",
404 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
405 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700406 src_template = "openssl-{}.tar.gz"
407 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100408 # only install software, skip docs
409 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700410
Christian Heimes62d618c2020-05-15 18:48:25 +0200411 def _post_install(self):
412 if self.version.startswith("3.0"):
413 self._post_install_300()
414
415 def _post_install_300(self):
416 # create ssl/ subdir with example configs
417 self._subprocess_call(
418 ["make", "-j1", "install_ssldirs"],
419 cwd=self.build_dir
420 )
421 # Install FIPS module
422 # https://wiki.openssl.org/index.php/OpenSSL_3.0#Completing_the_installation_of_the_FIPS_Module
423 fipsinstall_cnf = os.path.join(
424 self.install_dir, "ssl", "fipsinstall.cnf"
425 )
426 openssl_fips_cnf = os.path.join(
427 self.install_dir, "ssl", "openssl-fips.cnf"
428 )
429 fips_mod = os.path.join(self.lib_dir, "ossl-modules/fips.so")
430 self._subprocess_call(
431 [
432 self.openssl_cli, "fipsinstall",
433 "-out", fipsinstall_cnf,
434 "-module", fips_mod,
435 "-provider_name", "fips",
436 "-mac_name", "HMAC",
437 "-macopt", "digest:SHA256",
438 "-macopt", "hexkey:00",
439 "-section_name", "fips_sect"
440 ]
441 )
442 with open(openssl_fips_cnf, "w") as f:
443 f.write(OPENSSL_FIPS_CNF.format(self=self))
Christian Heimes938717f2020-05-15 22:32:25 +0200444 @property
445 def short_version(self):
446 """Short version for OpenSSL download URL"""
447 short_version = self.version.rstrip(string.ascii_letters)
448 if short_version.startswith("0.9"):
449 short_version = "0.9.x"
450 return short_version
Christian Heimes62d618c2020-05-15 18:48:25 +0200451
Christian Heimesd3b9f972017-09-06 18:59:22 -0700452
453class BuildLibreSSL(AbstractBuilder):
454 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200455 url_templates = (
456 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
457 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700458 src_template = "libressl-{}.tar.gz"
459 build_template = "libressl-{}"
460
461
462def configure_make():
463 if not os.path.isfile('Makefile'):
464 log.info('Running ./configure')
465 subprocess.check_call([
466 './configure', '--config-cache', '--quiet',
467 '--with-pydebug'
468 ])
469
470 log.info('Running make')
471 subprocess.check_call(['make', '--quiet'])
472
473
474def main():
475 args = parser.parse_args()
476 if not args.openssl and not args.libressl:
477 args.openssl = list(OPENSSL_RECENT_VERSIONS)
478 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
479 if not args.disable_ancient:
480 args.openssl.extend(OPENSSL_OLD_VERSIONS)
481 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
482
483 logging.basicConfig(
484 level=logging.DEBUG if args.debug else logging.INFO,
485 format="*** %(levelname)s %(message)s"
486 )
487
488 start = datetime.now()
489
Christian Heimesced9cb52018-01-16 21:02:26 +0100490 if args.steps in {'modules', 'tests'}:
491 for name in ['setup.py', 'Modules/_ssl.c']:
492 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
493 parser.error(
494 "Must be executed from CPython build dir"
495 )
496 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700497 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100498 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700499 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100500 # check for configure and run make
501 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700502
503 # download and register builder
504 builds = []
505
506 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100507 build = BuildOpenSSL(
508 version,
509 args
510 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700511 build.install()
512 builds.append(build)
513
514 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100515 build = BuildLibreSSL(
516 version,
517 args
518 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700519 build.install()
520 builds.append(build)
521
Christian Heimesced9cb52018-01-16 21:02:26 +0100522 if args.steps in {'modules', 'tests'}:
523 for build in builds:
524 try:
525 build.recompile_pymods()
526 build.check_pyssl()
527 if args.steps == 'tests':
528 build.run_python_tests(
529 tests=args.tests,
530 network=args.network,
531 )
532 except Exception as e:
533 log.exception("%s failed", build)
534 print("{} failed: {}".format(build, e), file=sys.stderr)
535 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700536
Christian Heimesced9cb52018-01-16 21:02:26 +0100537 log.info("\n{} finished in {}".format(
538 args.steps.capitalize(),
539 datetime.now() - start
540 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700541 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100542 if args.steps == 'tests':
543 if args.tests:
544 print('Executed Tests:', ' '.join(args.tests))
545 else:
546 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700547
548 print('OpenSSL / LibreSSL versions:')
549 for build in builds:
550 print(" * {0.library} {0.version}".format(build))
551
552
553if __name__ == "__main__":
554 main()