blob: dc47c5ce12cfa10ed2ae9dbd73f4c755f7202c1b [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 Heimesa54fc682021-03-30 02:00:34 +020049 "1.1.1k",
Christian Heimesdcf65812021-04-23 14:19:21 +020050 "3.0.0-alpha15"
Christian Heimesd3b9f972017-09-06 18:59:22 -070051]
52
53LIBRESSL_OLD_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070054]
55
56LIBRESSL_RECENT_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070057]
58
59# store files in ../multissl
Christian Heimesced9cb52018-01-16 21:02:26 +010060HERE = os.path.dirname(os.path.abspath(__file__))
61PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
62MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
63
Christian Heimesd3b9f972017-09-06 18:59:22 -070064
65parser = argparse.ArgumentParser(
66 prog='multissl',
67 description=(
68 "Run CPython tests with multiple OpenSSL and LibreSSL "
69 "versions."
70 )
71)
72parser.add_argument(
73 '--debug',
74 action='store_true',
Christian Heimes529525f2018-05-23 22:24:45 +020075 help="Enable debug logging",
Christian Heimesd3b9f972017-09-06 18:59:22 -070076)
77parser.add_argument(
78 '--disable-ancient',
79 action='store_true',
Christian Heimes6e8cda92020-05-16 03:33:05 +020080 help="Don't test OpenSSL and LibreSSL versions without upstream support",
Christian Heimesd3b9f972017-09-06 18:59:22 -070081)
82parser.add_argument(
83 '--openssl',
84 nargs='+',
85 default=(),
86 help=(
87 "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
88 "OpenSSL and LibreSSL versions are given."
89 ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
90)
91parser.add_argument(
92 '--libressl',
93 nargs='+',
94 default=(),
95 help=(
96 "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
97 "OpenSSL and LibreSSL versions are given."
98 ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
99)
100parser.add_argument(
101 '--tests',
102 nargs='*',
103 default=(),
104 help="Python tests to run, defaults to all SSL related tests.",
105)
106parser.add_argument(
107 '--base-directory',
108 default=MULTISSL_DIR,
109 help="Base directory for OpenSSL / LibreSSL sources and builds."
110)
111parser.add_argument(
112 '--no-network',
113 action='store_false',
114 dest='network',
115 help="Disable network tests."
116)
117parser.add_argument(
Christian Heimesced9cb52018-01-16 21:02:26 +0100118 '--steps',
119 choices=['library', 'modules', 'tests'],
120 default='tests',
121 help=(
122 "Which steps to perform. 'library' downloads and compiles OpenSSL "
123 "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
124 "all and runs the test suite."
125 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700126)
Steve Dowere5f41d22018-05-16 17:50:29 -0400127parser.add_argument(
128 '--system',
129 default='',
130 help="Override the automatic system type detection."
131)
Christian Heimes529525f2018-05-23 22:24:45 +0200132parser.add_argument(
133 '--force',
134 action='store_true',
135 dest='force',
136 help="Force build and installation."
137)
138parser.add_argument(
139 '--keep-sources',
140 action='store_true',
141 dest='keep_sources',
142 help="Keep original sources for debugging."
143)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700144
Christian Heimes62d618c2020-05-15 18:48:25 +0200145OPENSSL_FIPS_CNF = """\
146openssl_conf = openssl_init
147
148.include {self.install_dir}/ssl/fipsinstall.cnf
149# .include {self.install_dir}/ssl/openssl.cnf
150
151[openssl_init]
152providers = provider_sect
153
154[provider_sect]
155fips = fips_sect
156default = default_sect
157
158[default_sect]
159activate = 1
160"""
161
Christian Heimesd3b9f972017-09-06 18:59:22 -0700162
163class AbstractBuilder(object):
164 library = None
Christian Heimes938717f2020-05-15 22:32:25 +0200165 url_templates = None
Christian Heimesd3b9f972017-09-06 18:59:22 -0700166 src_template = None
167 build_template = None
Christian Heimes32eba612021-03-19 10:29:25 +0100168 depend_target = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100169 install_target = 'install'
Christian Heimes32eba612021-03-19 10:29:25 +0100170 jobs = os.cpu_count()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700171
172 module_files = ("Modules/_ssl.c",
173 "Modules/_hashopenssl.c")
174 module_libs = ("_ssl", "_hashlib")
175
Christian Heimesced9cb52018-01-16 21:02:26 +0100176 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700177 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100178 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700179 # installation directory
180 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100181 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700182 )
183 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100184 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700185 self.src_file = os.path.join(
186 self.src_dir, self.src_template.format(version))
187 # build directory (removed after install)
188 self.build_dir = os.path.join(
189 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400190 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700191
192 def __str__(self):
193 return "<{0.__class__.__name__} for {0.version}>".format(self)
194
195 def __eq__(self, other):
196 if not isinstance(other, AbstractBuilder):
197 return NotImplemented
198 return (
199 self.library == other.library
200 and self.version == other.version
201 )
202
203 def __hash__(self):
204 return hash((self.library, self.version))
205
206 @property
Christian Heimes938717f2020-05-15 22:32:25 +0200207 def short_version(self):
208 """Short version for OpenSSL download URL"""
209 return None
210
211 @property
Christian Heimesd3b9f972017-09-06 18:59:22 -0700212 def openssl_cli(self):
213 """openssl CLI binary"""
214 return os.path.join(self.install_dir, "bin", "openssl")
215
216 @property
217 def openssl_version(self):
218 """output of 'bin/openssl version'"""
219 cmd = [self.openssl_cli, "version"]
220 return self._subprocess_output(cmd)
221
222 @property
223 def pyssl_version(self):
224 """Value of ssl.OPENSSL_VERSION"""
225 cmd = [
226 sys.executable,
227 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
228 ]
229 return self._subprocess_output(cmd)
230
231 @property
232 def include_dir(self):
233 return os.path.join(self.install_dir, "include")
234
235 @property
236 def lib_dir(self):
237 return os.path.join(self.install_dir, "lib")
238
239 @property
240 def has_openssl(self):
241 return os.path.isfile(self.openssl_cli)
242
243 @property
244 def has_src(self):
245 return os.path.isfile(self.src_file)
246
247 def _subprocess_call(self, cmd, env=None, **kwargs):
248 log.debug("Call '{}'".format(" ".join(cmd)))
249 return subprocess.check_call(cmd, env=env, **kwargs)
250
251 def _subprocess_output(self, cmd, env=None, **kwargs):
252 log.debug("Call '{}'".format(" ".join(cmd)))
253 if env is None:
254 env = os.environ.copy()
255 env["LD_LIBRARY_PATH"] = self.lib_dir
256 out = subprocess.check_output(cmd, env=env, **kwargs)
257 return out.strip().decode("utf-8")
258
259 def _download_src(self):
260 """Download sources"""
261 src_dir = os.path.dirname(self.src_file)
262 if not os.path.isdir(src_dir):
263 os.makedirs(src_dir)
Christian Heimes938717f2020-05-15 22:32:25 +0200264 data = None
265 for url_template in self.url_templates:
266 url = url_template.format(v=self.version, s=self.short_version)
267 log.info("Downloading from {}".format(url))
268 try:
269 req = urlopen(url)
270 # KISS, read all, write all
271 data = req.read()
272 except HTTPError as e:
273 log.error(
274 "Download from {} has from failed: {}".format(url, e)
275 )
276 else:
277 log.info("Successfully downloaded from {}".format(url))
278 break
279 if data is None:
280 raise ValueError("All download URLs have failed")
Christian Heimesd3b9f972017-09-06 18:59:22 -0700281 log.info("Storing {}".format(self.src_file))
282 with open(self.src_file, "wb") as f:
283 f.write(data)
284
285 def _unpack_src(self):
286 """Unpack tar.gz bundle"""
287 # cleanup
288 if os.path.isdir(self.build_dir):
289 shutil.rmtree(self.build_dir)
290 os.makedirs(self.build_dir)
291
292 tf = tarfile.open(self.src_file)
293 name = self.build_template.format(self.version)
294 base = name + '/'
295 # force extraction into build dir
296 members = tf.getmembers()
297 for member in list(members):
298 if member.name == name:
299 members.remove(member)
300 elif not member.name.startswith(base):
301 raise ValueError(member.name, base)
302 member.name = member.name[len(base):].lstrip('/')
303 log.info("Unpacking files to {}".format(self.build_dir))
304 tf.extractall(self.build_dir, members)
305
306 def _build_src(self):
307 """Now build openssl"""
308 log.info("Running build in {}".format(self.build_dir))
309 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200310 cmd = [
311 "./config",
312 "shared", "--debug",
313 "--prefix={}".format(self.install_dir)
314 ]
Christian Heimesa871f692020-06-01 08:58:14 +0200315 # cmd.extend(["no-deprecated", "--api=1.1.0"])
Christian Heimes529525f2018-05-23 22:24:45 +0200316 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 Heimes32eba612021-03-19 10:29:25 +0100322 if self.depend_target:
323 self._subprocess_call(
324 ["make", "-j1", self.depend_target], cwd=cwd, env=env
325 )
326 self._subprocess_call(["make", f"-j{self.jobs}"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700327
Christian Heimes529525f2018-05-23 22:24:45 +0200328 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100329 self._subprocess_call(
330 ["make", "-j1", self.install_target],
331 cwd=self.build_dir
332 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200333 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200334 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700335 shutil.rmtree(self.build_dir)
336
Christian Heimes62d618c2020-05-15 18:48:25 +0200337 def _post_install(self):
338 pass
339
Christian Heimesd3b9f972017-09-06 18:59:22 -0700340 def install(self):
341 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200342 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700343 if not self.has_src:
344 self._download_src()
345 else:
346 log.debug("Already has src {}".format(self.src_file))
347 self._unpack_src()
348 self._build_src()
349 self._make_install()
350 else:
351 log.info("Already has installation {}".format(self.install_dir))
352 # validate installation
353 version = self.openssl_version
354 if self.version not in version:
355 raise ValueError(version)
356
357 def recompile_pymods(self):
358 log.warning("Using build from {}".format(self.build_dir))
359 # force a rebuild of all modules that use OpenSSL APIs
360 for fname in self.module_files:
361 os.utime(fname, None)
362 # remove all build artefacts
363 for root, dirs, files in os.walk('build'):
364 for filename in files:
365 if filename.startswith(self.module_libs):
366 os.unlink(os.path.join(root, filename))
367
368 # overwrite header and library search paths
369 env = os.environ.copy()
370 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
371 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
372 # set rpath
373 env["LD_RUN_PATH"] = self.lib_dir
374
375 log.info("Rebuilding Python modules")
376 cmd = [sys.executable, "setup.py", "build"]
377 self._subprocess_call(cmd, env=env)
378 self.check_imports()
379
380 def check_imports(self):
381 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
382 self._subprocess_call(cmd)
383
384 def check_pyssl(self):
385 version = self.pyssl_version
386 if self.version not in version:
387 raise ValueError(version)
388
389 def run_python_tests(self, tests, network=True):
390 if not tests:
391 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
392 elif sys.version_info < (3, 3):
393 cmd = [sys.executable, '-m', 'test.regrtest']
394 else:
395 cmd = [sys.executable, '-m', 'test', '-j0']
396 if network:
397 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
398 cmd.extend(['-w', '-r'])
399 cmd.extend(tests)
400 self._subprocess_call(cmd, stdout=None)
401
402
403class BuildOpenSSL(AbstractBuilder):
404 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200405 url_templates = (
406 "https://www.openssl.org/source/openssl-{v}.tar.gz",
407 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
408 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700409 src_template = "openssl-{}.tar.gz"
410 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100411 # only install software, skip docs
412 install_target = 'install_sw'
Christian Heimes32eba612021-03-19 10:29:25 +0100413 depend_target = 'depend'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700414
Christian Heimes62d618c2020-05-15 18:48:25 +0200415 def _post_install(self):
416 if self.version.startswith("3.0"):
417 self._post_install_300()
418
419 def _post_install_300(self):
420 # create ssl/ subdir with example configs
421 self._subprocess_call(
422 ["make", "-j1", "install_ssldirs"],
423 cwd=self.build_dir
424 )
425 # Install FIPS module
426 # https://wiki.openssl.org/index.php/OpenSSL_3.0#Completing_the_installation_of_the_FIPS_Module
427 fipsinstall_cnf = os.path.join(
428 self.install_dir, "ssl", "fipsinstall.cnf"
429 )
430 openssl_fips_cnf = os.path.join(
431 self.install_dir, "ssl", "openssl-fips.cnf"
432 )
433 fips_mod = os.path.join(self.lib_dir, "ossl-modules/fips.so")
434 self._subprocess_call(
435 [
436 self.openssl_cli, "fipsinstall",
437 "-out", fipsinstall_cnf,
438 "-module", fips_mod,
Christian Heimes32eba612021-03-19 10:29:25 +0100439 # "-provider_name", "fips",
440 # "-mac_name", "HMAC",
441 # "-macopt", "digest:SHA256",
442 # "-macopt", "hexkey:00",
443 # "-section_name", "fips_sect"
Christian Heimes62d618c2020-05-15 18:48:25 +0200444 ]
445 )
446 with open(openssl_fips_cnf, "w") as f:
447 f.write(OPENSSL_FIPS_CNF.format(self=self))
Christian Heimes938717f2020-05-15 22:32:25 +0200448 @property
449 def short_version(self):
450 """Short version for OpenSSL download URL"""
451 short_version = self.version.rstrip(string.ascii_letters)
452 if short_version.startswith("0.9"):
453 short_version = "0.9.x"
454 return short_version
Christian Heimes62d618c2020-05-15 18:48:25 +0200455
Christian Heimesd3b9f972017-09-06 18:59:22 -0700456
457class BuildLibreSSL(AbstractBuilder):
458 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200459 url_templates = (
460 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
461 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700462 src_template = "libressl-{}.tar.gz"
463 build_template = "libressl-{}"
464
465
466def configure_make():
467 if not os.path.isfile('Makefile'):
468 log.info('Running ./configure')
469 subprocess.check_call([
470 './configure', '--config-cache', '--quiet',
471 '--with-pydebug'
472 ])
473
474 log.info('Running make')
475 subprocess.check_call(['make', '--quiet'])
476
477
478def main():
479 args = parser.parse_args()
480 if not args.openssl and not args.libressl:
481 args.openssl = list(OPENSSL_RECENT_VERSIONS)
482 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
483 if not args.disable_ancient:
484 args.openssl.extend(OPENSSL_OLD_VERSIONS)
485 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
486
487 logging.basicConfig(
488 level=logging.DEBUG if args.debug else logging.INFO,
489 format="*** %(levelname)s %(message)s"
490 )
491
492 start = datetime.now()
493
Christian Heimesced9cb52018-01-16 21:02:26 +0100494 if args.steps in {'modules', 'tests'}:
495 for name in ['setup.py', 'Modules/_ssl.c']:
496 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
497 parser.error(
498 "Must be executed from CPython build dir"
499 )
500 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700501 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100502 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700503 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100504 # check for configure and run make
505 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700506
507 # download and register builder
508 builds = []
509
510 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100511 build = BuildOpenSSL(
512 version,
513 args
514 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700515 build.install()
516 builds.append(build)
517
518 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100519 build = BuildLibreSSL(
520 version,
521 args
522 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700523 build.install()
524 builds.append(build)
525
Christian Heimesced9cb52018-01-16 21:02:26 +0100526 if args.steps in {'modules', 'tests'}:
527 for build in builds:
528 try:
529 build.recompile_pymods()
530 build.check_pyssl()
531 if args.steps == 'tests':
532 build.run_python_tests(
533 tests=args.tests,
534 network=args.network,
535 )
536 except Exception as e:
537 log.exception("%s failed", build)
538 print("{} failed: {}".format(build, e), file=sys.stderr)
539 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700540
Christian Heimesced9cb52018-01-16 21:02:26 +0100541 log.info("\n{} finished in {}".format(
542 args.steps.capitalize(),
543 datetime.now() - start
544 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700545 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100546 if args.steps == 'tests':
547 if args.tests:
548 print('Executed Tests:', ' '.join(args.tests))
549 else:
550 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700551
552 print('OpenSSL / LibreSSL versions:')
553 for build in builds:
554 print(" * {0.library} {0.version}".format(build))
555
556
557if __name__ == "__main__":
558 main()