blob: 24d70ac6e4015e2a43f492017b047148d587f8a8 [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 Heimes3c586ca2021-04-26 10:54:12 +020036import re
Christian Heimesd3b9f972017-09-06 18:59:22 -070037import shutil
Christian Heimes938717f2020-05-15 22:32:25 +020038import string
39import subprocess
Christian Heimesd3b9f972017-09-06 18:59:22 -070040import sys
41import tarfile
42
43
44log = logging.getLogger("multissl")
45
46OPENSSL_OLD_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070047]
48
49OPENSSL_RECENT_VERSIONS = [
Christian Heimesa54fc682021-03-30 02:00:34 +020050 "1.1.1k",
Christian Heimesdcf65812021-04-23 14:19:21 +020051 "3.0.0-alpha15"
Christian Heimesd3b9f972017-09-06 18:59:22 -070052]
53
54LIBRESSL_OLD_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070055]
56
57LIBRESSL_RECENT_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070058]
59
60# store files in ../multissl
Christian Heimesced9cb52018-01-16 21:02:26 +010061HERE = os.path.dirname(os.path.abspath(__file__))
62PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
63MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
64
Christian Heimesd3b9f972017-09-06 18:59:22 -070065
66parser = argparse.ArgumentParser(
67 prog='multissl',
68 description=(
69 "Run CPython tests with multiple OpenSSL and LibreSSL "
70 "versions."
71 )
72)
73parser.add_argument(
74 '--debug',
75 action='store_true',
Christian Heimes529525f2018-05-23 22:24:45 +020076 help="Enable debug logging",
Christian Heimesd3b9f972017-09-06 18:59:22 -070077)
78parser.add_argument(
79 '--disable-ancient',
80 action='store_true',
Christian Heimes6e8cda92020-05-16 03:33:05 +020081 help="Don't test OpenSSL and LibreSSL versions without upstream support",
Christian Heimesd3b9f972017-09-06 18:59:22 -070082)
83parser.add_argument(
84 '--openssl',
85 nargs='+',
86 default=(),
87 help=(
88 "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
89 "OpenSSL and LibreSSL versions are given."
90 ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
91)
92parser.add_argument(
93 '--libressl',
94 nargs='+',
95 default=(),
96 help=(
97 "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
98 "OpenSSL and LibreSSL versions are given."
99 ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
100)
101parser.add_argument(
102 '--tests',
103 nargs='*',
104 default=(),
105 help="Python tests to run, defaults to all SSL related tests.",
106)
107parser.add_argument(
108 '--base-directory',
109 default=MULTISSL_DIR,
110 help="Base directory for OpenSSL / LibreSSL sources and builds."
111)
112parser.add_argument(
113 '--no-network',
114 action='store_false',
115 dest='network',
116 help="Disable network tests."
117)
118parser.add_argument(
Christian Heimesced9cb52018-01-16 21:02:26 +0100119 '--steps',
120 choices=['library', 'modules', 'tests'],
121 default='tests',
122 help=(
123 "Which steps to perform. 'library' downloads and compiles OpenSSL "
124 "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
125 "all and runs the test suite."
126 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700127)
Steve Dowere5f41d22018-05-16 17:50:29 -0400128parser.add_argument(
129 '--system',
130 default='',
131 help="Override the automatic system type detection."
132)
Christian Heimes529525f2018-05-23 22:24:45 +0200133parser.add_argument(
134 '--force',
135 action='store_true',
136 dest='force',
137 help="Force build and installation."
138)
139parser.add_argument(
140 '--keep-sources',
141 action='store_true',
142 dest='keep_sources',
143 help="Keep original sources for debugging."
144)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700145
Christian Heimes62d618c2020-05-15 18:48:25 +0200146OPENSSL_FIPS_CNF = """\
147openssl_conf = openssl_init
148
149.include {self.install_dir}/ssl/fipsinstall.cnf
150# .include {self.install_dir}/ssl/openssl.cnf
151
152[openssl_init]
153providers = provider_sect
154
155[provider_sect]
156fips = fips_sect
157default = default_sect
158
159[default_sect]
160activate = 1
161"""
162
Christian Heimesd3b9f972017-09-06 18:59:22 -0700163
164class AbstractBuilder(object):
165 library = None
Christian Heimes938717f2020-05-15 22:32:25 +0200166 url_templates = None
Christian Heimesd3b9f972017-09-06 18:59:22 -0700167 src_template = None
168 build_template = None
Christian Heimes32eba612021-03-19 10:29:25 +0100169 depend_target = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100170 install_target = 'install'
Christian Heimes32eba612021-03-19 10:29:25 +0100171 jobs = os.cpu_count()
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 ]
Christian Heimesa871f692020-06-01 08:58:14 +0200316 # cmd.extend(["no-deprecated", "--api=1.1.0"])
Christian Heimes529525f2018-05-23 22:24:45 +0200317 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 Heimes32eba612021-03-19 10:29:25 +0100323 if self.depend_target:
324 self._subprocess_call(
325 ["make", "-j1", self.depend_target], cwd=cwd, env=env
326 )
327 self._subprocess_call(["make", f"-j{self.jobs}"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700328
Christian Heimes529525f2018-05-23 22:24:45 +0200329 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100330 self._subprocess_call(
331 ["make", "-j1", self.install_target],
332 cwd=self.build_dir
333 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200334 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200335 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700336 shutil.rmtree(self.build_dir)
337
Christian Heimes62d618c2020-05-15 18:48:25 +0200338 def _post_install(self):
339 pass
340
Christian Heimesd3b9f972017-09-06 18:59:22 -0700341 def install(self):
342 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200343 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700344 if not self.has_src:
345 self._download_src()
346 else:
347 log.debug("Already has src {}".format(self.src_file))
348 self._unpack_src()
349 self._build_src()
350 self._make_install()
351 else:
352 log.info("Already has installation {}".format(self.install_dir))
353 # validate installation
354 version = self.openssl_version
355 if self.version not in version:
356 raise ValueError(version)
357
358 def recompile_pymods(self):
359 log.warning("Using build from {}".format(self.build_dir))
360 # force a rebuild of all modules that use OpenSSL APIs
361 for fname in self.module_files:
362 os.utime(fname, None)
363 # remove all build artefacts
364 for root, dirs, files in os.walk('build'):
365 for filename in files:
366 if filename.startswith(self.module_libs):
367 os.unlink(os.path.join(root, filename))
368
369 # overwrite header and library search paths
370 env = os.environ.copy()
371 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
372 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
373 # set rpath
374 env["LD_RUN_PATH"] = self.lib_dir
375
376 log.info("Rebuilding Python modules")
377 cmd = [sys.executable, "setup.py", "build"]
378 self._subprocess_call(cmd, env=env)
379 self.check_imports()
380
381 def check_imports(self):
382 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
383 self._subprocess_call(cmd)
384
385 def check_pyssl(self):
386 version = self.pyssl_version
387 if self.version not in version:
388 raise ValueError(version)
389
390 def run_python_tests(self, tests, network=True):
391 if not tests:
392 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
393 elif sys.version_info < (3, 3):
394 cmd = [sys.executable, '-m', 'test.regrtest']
395 else:
396 cmd = [sys.executable, '-m', 'test', '-j0']
397 if network:
398 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
399 cmd.extend(['-w', '-r'])
400 cmd.extend(tests)
401 self._subprocess_call(cmd, stdout=None)
402
403
404class BuildOpenSSL(AbstractBuilder):
405 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200406 url_templates = (
407 "https://www.openssl.org/source/openssl-{v}.tar.gz",
408 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
409 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700410 src_template = "openssl-{}.tar.gz"
411 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100412 # only install software, skip docs
413 install_target = 'install_sw'
Christian Heimes32eba612021-03-19 10:29:25 +0100414 depend_target = 'depend'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700415
Christian Heimes62d618c2020-05-15 18:48:25 +0200416 def _post_install(self):
417 if self.version.startswith("3.0"):
418 self._post_install_300()
419
420 def _post_install_300(self):
421 # create ssl/ subdir with example configs
422 self._subprocess_call(
423 ["make", "-j1", "install_ssldirs"],
424 cwd=self.build_dir
425 )
426 # Install FIPS module
427 # https://wiki.openssl.org/index.php/OpenSSL_3.0#Completing_the_installation_of_the_FIPS_Module
428 fipsinstall_cnf = os.path.join(
429 self.install_dir, "ssl", "fipsinstall.cnf"
430 )
431 openssl_fips_cnf = os.path.join(
432 self.install_dir, "ssl", "openssl-fips.cnf"
433 )
434 fips_mod = os.path.join(self.lib_dir, "ossl-modules/fips.so")
435 self._subprocess_call(
436 [
437 self.openssl_cli, "fipsinstall",
438 "-out", fipsinstall_cnf,
439 "-module", fips_mod,
Christian Heimes32eba612021-03-19 10:29:25 +0100440 # "-provider_name", "fips",
441 # "-mac_name", "HMAC",
442 # "-macopt", "digest:SHA256",
443 # "-macopt", "hexkey:00",
444 # "-section_name", "fips_sect"
Christian Heimes62d618c2020-05-15 18:48:25 +0200445 ]
446 )
447 with open(openssl_fips_cnf, "w") as f:
448 f.write(OPENSSL_FIPS_CNF.format(self=self))
Christian Heimes938717f2020-05-15 22:32:25 +0200449 @property
450 def short_version(self):
451 """Short version for OpenSSL download URL"""
Christian Heimes3c586ca2021-04-26 10:54:12 +0200452 mo = re.search(r"^(\d+)\.(\d+)\.(\d+)", self.version)
453 parsed = tuple(int(m) for m in mo.groups())
454 if parsed < (1, 0, 0):
455 return "0.9.x"
456 if parsed >= (3, 0, 0):
457 # OpenSSL 3.0.0 -> /old/3.0/
458 parsed = parsed[:2]
459 return ".".join(str(i) for i in parsed)
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()