blob: 3818165a836fb1f7dc6ad009f544884b3017f704 [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 ]
Christian Heimesa871f692020-06-01 08:58:14 +0200317 # cmd.extend(["no-deprecated", "--api=1.1.0"])
Christian Heimes529525f2018-05-23 22:24:45 +0200318 env = os.environ.copy()
319 # set rpath
320 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400321 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400322 env['SYSTEM'] = self.system
323 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700324 # Old OpenSSL versions do not support parallel builds.
Steve Dowere5f41d22018-05-16 17:50:29 -0400325 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700326
Christian Heimes529525f2018-05-23 22:24:45 +0200327 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100328 self._subprocess_call(
329 ["make", "-j1", self.install_target],
330 cwd=self.build_dir
331 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200332 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200333 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700334 shutil.rmtree(self.build_dir)
335
Christian Heimes62d618c2020-05-15 18:48:25 +0200336 def _post_install(self):
337 pass
338
Christian Heimesd3b9f972017-09-06 18:59:22 -0700339 def install(self):
340 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200341 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700342 if not self.has_src:
343 self._download_src()
344 else:
345 log.debug("Already has src {}".format(self.src_file))
346 self._unpack_src()
347 self._build_src()
348 self._make_install()
349 else:
350 log.info("Already has installation {}".format(self.install_dir))
351 # validate installation
352 version = self.openssl_version
353 if self.version not in version:
354 raise ValueError(version)
355
356 def recompile_pymods(self):
357 log.warning("Using build from {}".format(self.build_dir))
358 # force a rebuild of all modules that use OpenSSL APIs
359 for fname in self.module_files:
360 os.utime(fname, None)
361 # remove all build artefacts
362 for root, dirs, files in os.walk('build'):
363 for filename in files:
364 if filename.startswith(self.module_libs):
365 os.unlink(os.path.join(root, filename))
366
367 # overwrite header and library search paths
368 env = os.environ.copy()
369 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
370 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
371 # set rpath
372 env["LD_RUN_PATH"] = self.lib_dir
373
374 log.info("Rebuilding Python modules")
375 cmd = [sys.executable, "setup.py", "build"]
376 self._subprocess_call(cmd, env=env)
377 self.check_imports()
378
379 def check_imports(self):
380 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
381 self._subprocess_call(cmd)
382
383 def check_pyssl(self):
384 version = self.pyssl_version
385 if self.version not in version:
386 raise ValueError(version)
387
388 def run_python_tests(self, tests, network=True):
389 if not tests:
390 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
391 elif sys.version_info < (3, 3):
392 cmd = [sys.executable, '-m', 'test.regrtest']
393 else:
394 cmd = [sys.executable, '-m', 'test', '-j0']
395 if network:
396 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
397 cmd.extend(['-w', '-r'])
398 cmd.extend(tests)
399 self._subprocess_call(cmd, stdout=None)
400
401
402class BuildOpenSSL(AbstractBuilder):
403 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200404 url_templates = (
405 "https://www.openssl.org/source/openssl-{v}.tar.gz",
406 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
407 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700408 src_template = "openssl-{}.tar.gz"
409 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100410 # only install software, skip docs
411 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700412
Christian Heimes62d618c2020-05-15 18:48:25 +0200413 def _post_install(self):
414 if self.version.startswith("3.0"):
415 self._post_install_300()
416
417 def _post_install_300(self):
418 # create ssl/ subdir with example configs
419 self._subprocess_call(
420 ["make", "-j1", "install_ssldirs"],
421 cwd=self.build_dir
422 )
423 # Install FIPS module
424 # https://wiki.openssl.org/index.php/OpenSSL_3.0#Completing_the_installation_of_the_FIPS_Module
425 fipsinstall_cnf = os.path.join(
426 self.install_dir, "ssl", "fipsinstall.cnf"
427 )
428 openssl_fips_cnf = os.path.join(
429 self.install_dir, "ssl", "openssl-fips.cnf"
430 )
431 fips_mod = os.path.join(self.lib_dir, "ossl-modules/fips.so")
432 self._subprocess_call(
433 [
434 self.openssl_cli, "fipsinstall",
435 "-out", fipsinstall_cnf,
436 "-module", fips_mod,
437 "-provider_name", "fips",
438 "-mac_name", "HMAC",
439 "-macopt", "digest:SHA256",
440 "-macopt", "hexkey:00",
441 "-section_name", "fips_sect"
442 ]
443 )
444 with open(openssl_fips_cnf, "w") as f:
445 f.write(OPENSSL_FIPS_CNF.format(self=self))
Christian Heimes938717f2020-05-15 22:32:25 +0200446 @property
447 def short_version(self):
448 """Short version for OpenSSL download URL"""
449 short_version = self.version.rstrip(string.ascii_letters)
450 if short_version.startswith("0.9"):
451 short_version = "0.9.x"
452 return short_version
Christian Heimes62d618c2020-05-15 18:48:25 +0200453
Christian Heimesd3b9f972017-09-06 18:59:22 -0700454
455class BuildLibreSSL(AbstractBuilder):
456 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200457 url_templates = (
458 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
459 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700460 src_template = "libressl-{}.tar.gz"
461 build_template = "libressl-{}"
462
463
464def configure_make():
465 if not os.path.isfile('Makefile'):
466 log.info('Running ./configure')
467 subprocess.check_call([
468 './configure', '--config-cache', '--quiet',
469 '--with-pydebug'
470 ])
471
472 log.info('Running make')
473 subprocess.check_call(['make', '--quiet'])
474
475
476def main():
477 args = parser.parse_args()
478 if not args.openssl and not args.libressl:
479 args.openssl = list(OPENSSL_RECENT_VERSIONS)
480 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
481 if not args.disable_ancient:
482 args.openssl.extend(OPENSSL_OLD_VERSIONS)
483 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
484
485 logging.basicConfig(
486 level=logging.DEBUG if args.debug else logging.INFO,
487 format="*** %(levelname)s %(message)s"
488 )
489
490 start = datetime.now()
491
Christian Heimesced9cb52018-01-16 21:02:26 +0100492 if args.steps in {'modules', 'tests'}:
493 for name in ['setup.py', 'Modules/_ssl.c']:
494 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
495 parser.error(
496 "Must be executed from CPython build dir"
497 )
498 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700499 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100500 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700501 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100502 # check for configure and run make
503 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700504
505 # download and register builder
506 builds = []
507
508 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100509 build = BuildOpenSSL(
510 version,
511 args
512 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700513 build.install()
514 builds.append(build)
515
516 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100517 build = BuildLibreSSL(
518 version,
519 args
520 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700521 build.install()
522 builds.append(build)
523
Christian Heimesced9cb52018-01-16 21:02:26 +0100524 if args.steps in {'modules', 'tests'}:
525 for build in builds:
526 try:
527 build.recompile_pymods()
528 build.check_pyssl()
529 if args.steps == 'tests':
530 build.run_python_tests(
531 tests=args.tests,
532 network=args.network,
533 )
534 except Exception as e:
535 log.exception("%s failed", build)
536 print("{} failed: {}".format(build, e), file=sys.stderr)
537 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700538
Christian Heimesced9cb52018-01-16 21:02:26 +0100539 log.info("\n{} finished in {}".format(
540 args.steps.capitalize(),
541 datetime.now() - start
542 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700543 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100544 if args.steps == 'tests':
545 if args.tests:
546 print('Executed Tests:', ' '.join(args.tests))
547 else:
548 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700549
550 print('OpenSSL / LibreSSL versions:')
551 for build in builds:
552 print(" * {0.library} {0.version}".format(build))
553
554
555if __name__ == "__main__":
556 main()