blob: 7afa1eea2efb20125df0ba4599ee982e72b439b3 [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 Heimes32eba612021-03-19 10:29:25 +010051 "1.1.1j",
52 # "3.0.0-alpha12"
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 Heimes32eba612021-03-19 10:29:25 +010060 "3.2.4",
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 Heimes32eba612021-03-19 10:29:25 +0100172 depend_target = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100173 install_target = 'install'
Christian Heimes32eba612021-03-19 10:29:25 +0100174 jobs = os.cpu_count()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700175
176 module_files = ("Modules/_ssl.c",
177 "Modules/_hashopenssl.c")
178 module_libs = ("_ssl", "_hashlib")
179
Christian Heimesced9cb52018-01-16 21:02:26 +0100180 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700181 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100182 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700183 # installation directory
184 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100185 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700186 )
187 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100188 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700189 self.src_file = os.path.join(
190 self.src_dir, self.src_template.format(version))
191 # build directory (removed after install)
192 self.build_dir = os.path.join(
193 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400194 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700195
196 def __str__(self):
197 return "<{0.__class__.__name__} for {0.version}>".format(self)
198
199 def __eq__(self, other):
200 if not isinstance(other, AbstractBuilder):
201 return NotImplemented
202 return (
203 self.library == other.library
204 and self.version == other.version
205 )
206
207 def __hash__(self):
208 return hash((self.library, self.version))
209
210 @property
Christian Heimes938717f2020-05-15 22:32:25 +0200211 def short_version(self):
212 """Short version for OpenSSL download URL"""
213 return None
214
215 @property
Christian Heimesd3b9f972017-09-06 18:59:22 -0700216 def openssl_cli(self):
217 """openssl CLI binary"""
218 return os.path.join(self.install_dir, "bin", "openssl")
219
220 @property
221 def openssl_version(self):
222 """output of 'bin/openssl version'"""
223 cmd = [self.openssl_cli, "version"]
224 return self._subprocess_output(cmd)
225
226 @property
227 def pyssl_version(self):
228 """Value of ssl.OPENSSL_VERSION"""
229 cmd = [
230 sys.executable,
231 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
232 ]
233 return self._subprocess_output(cmd)
234
235 @property
236 def include_dir(self):
237 return os.path.join(self.install_dir, "include")
238
239 @property
240 def lib_dir(self):
241 return os.path.join(self.install_dir, "lib")
242
243 @property
244 def has_openssl(self):
245 return os.path.isfile(self.openssl_cli)
246
247 @property
248 def has_src(self):
249 return os.path.isfile(self.src_file)
250
251 def _subprocess_call(self, cmd, env=None, **kwargs):
252 log.debug("Call '{}'".format(" ".join(cmd)))
253 return subprocess.check_call(cmd, env=env, **kwargs)
254
255 def _subprocess_output(self, cmd, env=None, **kwargs):
256 log.debug("Call '{}'".format(" ".join(cmd)))
257 if env is None:
258 env = os.environ.copy()
259 env["LD_LIBRARY_PATH"] = self.lib_dir
260 out = subprocess.check_output(cmd, env=env, **kwargs)
261 return out.strip().decode("utf-8")
262
263 def _download_src(self):
264 """Download sources"""
265 src_dir = os.path.dirname(self.src_file)
266 if not os.path.isdir(src_dir):
267 os.makedirs(src_dir)
Christian Heimes938717f2020-05-15 22:32:25 +0200268 data = None
269 for url_template in self.url_templates:
270 url = url_template.format(v=self.version, s=self.short_version)
271 log.info("Downloading from {}".format(url))
272 try:
273 req = urlopen(url)
274 # KISS, read all, write all
275 data = req.read()
276 except HTTPError as e:
277 log.error(
278 "Download from {} has from failed: {}".format(url, e)
279 )
280 else:
281 log.info("Successfully downloaded from {}".format(url))
282 break
283 if data is None:
284 raise ValueError("All download URLs have failed")
Christian Heimesd3b9f972017-09-06 18:59:22 -0700285 log.info("Storing {}".format(self.src_file))
286 with open(self.src_file, "wb") as f:
287 f.write(data)
288
289 def _unpack_src(self):
290 """Unpack tar.gz bundle"""
291 # cleanup
292 if os.path.isdir(self.build_dir):
293 shutil.rmtree(self.build_dir)
294 os.makedirs(self.build_dir)
295
296 tf = tarfile.open(self.src_file)
297 name = self.build_template.format(self.version)
298 base = name + '/'
299 # force extraction into build dir
300 members = tf.getmembers()
301 for member in list(members):
302 if member.name == name:
303 members.remove(member)
304 elif not member.name.startswith(base):
305 raise ValueError(member.name, base)
306 member.name = member.name[len(base):].lstrip('/')
307 log.info("Unpacking files to {}".format(self.build_dir))
308 tf.extractall(self.build_dir, members)
309
310 def _build_src(self):
311 """Now build openssl"""
312 log.info("Running build in {}".format(self.build_dir))
313 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200314 cmd = [
315 "./config",
316 "shared", "--debug",
317 "--prefix={}".format(self.install_dir)
318 ]
Christian Heimesa871f692020-06-01 08:58:14 +0200319 # cmd.extend(["no-deprecated", "--api=1.1.0"])
Christian Heimes529525f2018-05-23 22:24:45 +0200320 env = os.environ.copy()
321 # set rpath
322 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400323 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400324 env['SYSTEM'] = self.system
325 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimes32eba612021-03-19 10:29:25 +0100326 if self.depend_target:
327 self._subprocess_call(
328 ["make", "-j1", self.depend_target], cwd=cwd, env=env
329 )
330 self._subprocess_call(["make", f"-j{self.jobs}"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700331
Christian Heimes529525f2018-05-23 22:24:45 +0200332 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100333 self._subprocess_call(
334 ["make", "-j1", self.install_target],
335 cwd=self.build_dir
336 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200337 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200338 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700339 shutil.rmtree(self.build_dir)
340
Christian Heimes62d618c2020-05-15 18:48:25 +0200341 def _post_install(self):
342 pass
343
Christian Heimesd3b9f972017-09-06 18:59:22 -0700344 def install(self):
345 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200346 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700347 if not self.has_src:
348 self._download_src()
349 else:
350 log.debug("Already has src {}".format(self.src_file))
351 self._unpack_src()
352 self._build_src()
353 self._make_install()
354 else:
355 log.info("Already has installation {}".format(self.install_dir))
356 # validate installation
357 version = self.openssl_version
358 if self.version not in version:
359 raise ValueError(version)
360
361 def recompile_pymods(self):
362 log.warning("Using build from {}".format(self.build_dir))
363 # force a rebuild of all modules that use OpenSSL APIs
364 for fname in self.module_files:
365 os.utime(fname, None)
366 # remove all build artefacts
367 for root, dirs, files in os.walk('build'):
368 for filename in files:
369 if filename.startswith(self.module_libs):
370 os.unlink(os.path.join(root, filename))
371
372 # overwrite header and library search paths
373 env = os.environ.copy()
374 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
375 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
376 # set rpath
377 env["LD_RUN_PATH"] = self.lib_dir
378
379 log.info("Rebuilding Python modules")
380 cmd = [sys.executable, "setup.py", "build"]
381 self._subprocess_call(cmd, env=env)
382 self.check_imports()
383
384 def check_imports(self):
385 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
386 self._subprocess_call(cmd)
387
388 def check_pyssl(self):
389 version = self.pyssl_version
390 if self.version not in version:
391 raise ValueError(version)
392
393 def run_python_tests(self, tests, network=True):
394 if not tests:
395 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
396 elif sys.version_info < (3, 3):
397 cmd = [sys.executable, '-m', 'test.regrtest']
398 else:
399 cmd = [sys.executable, '-m', 'test', '-j0']
400 if network:
401 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
402 cmd.extend(['-w', '-r'])
403 cmd.extend(tests)
404 self._subprocess_call(cmd, stdout=None)
405
406
407class BuildOpenSSL(AbstractBuilder):
408 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200409 url_templates = (
410 "https://www.openssl.org/source/openssl-{v}.tar.gz",
411 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
412 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700413 src_template = "openssl-{}.tar.gz"
414 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100415 # only install software, skip docs
416 install_target = 'install_sw'
Christian Heimes32eba612021-03-19 10:29:25 +0100417 depend_target = 'depend'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700418
Christian Heimes62d618c2020-05-15 18:48:25 +0200419 def _post_install(self):
420 if self.version.startswith("3.0"):
421 self._post_install_300()
422
423 def _post_install_300(self):
424 # create ssl/ subdir with example configs
425 self._subprocess_call(
426 ["make", "-j1", "install_ssldirs"],
427 cwd=self.build_dir
428 )
429 # Install FIPS module
430 # https://wiki.openssl.org/index.php/OpenSSL_3.0#Completing_the_installation_of_the_FIPS_Module
431 fipsinstall_cnf = os.path.join(
432 self.install_dir, "ssl", "fipsinstall.cnf"
433 )
434 openssl_fips_cnf = os.path.join(
435 self.install_dir, "ssl", "openssl-fips.cnf"
436 )
437 fips_mod = os.path.join(self.lib_dir, "ossl-modules/fips.so")
438 self._subprocess_call(
439 [
440 self.openssl_cli, "fipsinstall",
441 "-out", fipsinstall_cnf,
442 "-module", fips_mod,
Christian Heimes32eba612021-03-19 10:29:25 +0100443 # "-provider_name", "fips",
444 # "-mac_name", "HMAC",
445 # "-macopt", "digest:SHA256",
446 # "-macopt", "hexkey:00",
447 # "-section_name", "fips_sect"
Christian Heimes62d618c2020-05-15 18:48:25 +0200448 ]
449 )
450 with open(openssl_fips_cnf, "w") as f:
451 f.write(OPENSSL_FIPS_CNF.format(self=self))
Christian Heimes938717f2020-05-15 22:32:25 +0200452 @property
453 def short_version(self):
454 """Short version for OpenSSL download URL"""
455 short_version = self.version.rstrip(string.ascii_letters)
456 if short_version.startswith("0.9"):
457 short_version = "0.9.x"
458 return short_version
Christian Heimes62d618c2020-05-15 18:48:25 +0200459
Christian Heimesd3b9f972017-09-06 18:59:22 -0700460
461class BuildLibreSSL(AbstractBuilder):
462 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200463 url_templates = (
464 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
465 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700466 src_template = "libressl-{}.tar.gz"
467 build_template = "libressl-{}"
468
469
470def configure_make():
471 if not os.path.isfile('Makefile'):
472 log.info('Running ./configure')
473 subprocess.check_call([
474 './configure', '--config-cache', '--quiet',
475 '--with-pydebug'
476 ])
477
478 log.info('Running make')
479 subprocess.check_call(['make', '--quiet'])
480
481
482def main():
483 args = parser.parse_args()
484 if not args.openssl and not args.libressl:
485 args.openssl = list(OPENSSL_RECENT_VERSIONS)
486 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
487 if not args.disable_ancient:
488 args.openssl.extend(OPENSSL_OLD_VERSIONS)
489 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
490
491 logging.basicConfig(
492 level=logging.DEBUG if args.debug else logging.INFO,
493 format="*** %(levelname)s %(message)s"
494 )
495
496 start = datetime.now()
497
Christian Heimesced9cb52018-01-16 21:02:26 +0100498 if args.steps in {'modules', 'tests'}:
499 for name in ['setup.py', 'Modules/_ssl.c']:
500 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
501 parser.error(
502 "Must be executed from CPython build dir"
503 )
504 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700505 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100506 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700507 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100508 # check for configure and run make
509 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700510
511 # download and register builder
512 builds = []
513
514 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100515 build = BuildOpenSSL(
516 version,
517 args
518 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700519 build.install()
520 builds.append(build)
521
522 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100523 build = BuildLibreSSL(
524 version,
525 args
526 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700527 build.install()
528 builds.append(build)
529
Christian Heimesced9cb52018-01-16 21:02:26 +0100530 if args.steps in {'modules', 'tests'}:
531 for build in builds:
532 try:
533 build.recompile_pymods()
534 build.check_pyssl()
535 if args.steps == 'tests':
536 build.run_python_tests(
537 tests=args.tests,
538 network=args.network,
539 )
540 except Exception as e:
541 log.exception("%s failed", build)
542 print("{} failed: {}".format(build, e), file=sys.stderr)
543 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700544
Christian Heimesced9cb52018-01-16 21:02:26 +0100545 log.info("\n{} finished in {}".format(
546 args.steps.capitalize(),
547 datetime.now() - start
548 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700549 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100550 if args.steps == 'tests':
551 if args.tests:
552 print('Executed Tests:', ' '.join(args.tests))
553 else:
554 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700555
556 print('OpenSSL / LibreSSL versions:')
557 for build in builds:
558 print(" * {0.library} {0.version}".format(build))
559
560
561if __name__ == "__main__":
562 main()