blob: 7aa28bd2157fb38bf44572eb8b4ff95a7823a994 [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
33except ImportError:
34 from urllib2 import urlopen
35import subprocess
36import shutil
37import sys
38import tarfile
39
40
41log = logging.getLogger("multissl")
42
43OPENSSL_OLD_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070044]
45
46OPENSSL_RECENT_VERSIONS = [
Christian Heimes62d618c2020-05-15 18:48:25 +020047 "1.0.2u",
Christian Heimes58ab1342019-09-11 18:45:52 +020048 "1.1.0l",
Christian Heimes62d618c2020-05-15 18:48:25 +020049 "1.1.1g",
50 # "3.0.0-alpha2"
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 Heimes58ab1342019-09-11 18:45:52 +020057 "2.9.2",
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',
81 help="Don't test OpenSSL < 1.0.2 and LibreSSL < 2.5.3.",
82)
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
166 url_template = None
167 src_template = None
168 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100169 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700170
171 module_files = ("Modules/_ssl.c",
172 "Modules/_hashopenssl.c")
173 module_libs = ("_ssl", "_hashlib")
174
Christian Heimesced9cb52018-01-16 21:02:26 +0100175 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700176 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100177 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700178 # installation directory
179 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100180 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700181 )
182 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100183 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700184 self.src_file = os.path.join(
185 self.src_dir, self.src_template.format(version))
186 # build directory (removed after install)
187 self.build_dir = os.path.join(
188 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400189 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700190
191 def __str__(self):
192 return "<{0.__class__.__name__} for {0.version}>".format(self)
193
194 def __eq__(self, other):
195 if not isinstance(other, AbstractBuilder):
196 return NotImplemented
197 return (
198 self.library == other.library
199 and self.version == other.version
200 )
201
202 def __hash__(self):
203 return hash((self.library, self.version))
204
205 @property
206 def openssl_cli(self):
207 """openssl CLI binary"""
208 return os.path.join(self.install_dir, "bin", "openssl")
209
210 @property
211 def openssl_version(self):
212 """output of 'bin/openssl version'"""
213 cmd = [self.openssl_cli, "version"]
214 return self._subprocess_output(cmd)
215
216 @property
217 def pyssl_version(self):
218 """Value of ssl.OPENSSL_VERSION"""
219 cmd = [
220 sys.executable,
221 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
222 ]
223 return self._subprocess_output(cmd)
224
225 @property
226 def include_dir(self):
227 return os.path.join(self.install_dir, "include")
228
229 @property
230 def lib_dir(self):
231 return os.path.join(self.install_dir, "lib")
232
233 @property
234 def has_openssl(self):
235 return os.path.isfile(self.openssl_cli)
236
237 @property
238 def has_src(self):
239 return os.path.isfile(self.src_file)
240
241 def _subprocess_call(self, cmd, env=None, **kwargs):
242 log.debug("Call '{}'".format(" ".join(cmd)))
243 return subprocess.check_call(cmd, env=env, **kwargs)
244
245 def _subprocess_output(self, cmd, env=None, **kwargs):
246 log.debug("Call '{}'".format(" ".join(cmd)))
247 if env is None:
248 env = os.environ.copy()
249 env["LD_LIBRARY_PATH"] = self.lib_dir
250 out = subprocess.check_output(cmd, env=env, **kwargs)
251 return out.strip().decode("utf-8")
252
253 def _download_src(self):
254 """Download sources"""
255 src_dir = os.path.dirname(self.src_file)
256 if not os.path.isdir(src_dir):
257 os.makedirs(src_dir)
258 url = self.url_template.format(self.version)
259 log.info("Downloading from {}".format(url))
260 req = urlopen(url)
261 # KISS, read all, write all
262 data = req.read()
263 log.info("Storing {}".format(self.src_file))
264 with open(self.src_file, "wb") as f:
265 f.write(data)
266
267 def _unpack_src(self):
268 """Unpack tar.gz bundle"""
269 # cleanup
270 if os.path.isdir(self.build_dir):
271 shutil.rmtree(self.build_dir)
272 os.makedirs(self.build_dir)
273
274 tf = tarfile.open(self.src_file)
275 name = self.build_template.format(self.version)
276 base = name + '/'
277 # force extraction into build dir
278 members = tf.getmembers()
279 for member in list(members):
280 if member.name == name:
281 members.remove(member)
282 elif not member.name.startswith(base):
283 raise ValueError(member.name, base)
284 member.name = member.name[len(base):].lstrip('/')
285 log.info("Unpacking files to {}".format(self.build_dir))
286 tf.extractall(self.build_dir, members)
287
288 def _build_src(self):
289 """Now build openssl"""
290 log.info("Running build in {}".format(self.build_dir))
291 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200292 cmd = [
293 "./config",
294 "shared", "--debug",
295 "--prefix={}".format(self.install_dir)
296 ]
297 env = os.environ.copy()
298 # set rpath
299 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400300 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400301 env['SYSTEM'] = self.system
302 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700303 # Old OpenSSL versions do not support parallel builds.
Steve Dowere5f41d22018-05-16 17:50:29 -0400304 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700305
Christian Heimes529525f2018-05-23 22:24:45 +0200306 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100307 self._subprocess_call(
308 ["make", "-j1", self.install_target],
309 cwd=self.build_dir
310 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200311 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200312 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700313 shutil.rmtree(self.build_dir)
314
Christian Heimes62d618c2020-05-15 18:48:25 +0200315 def _post_install(self):
316 pass
317
Christian Heimesd3b9f972017-09-06 18:59:22 -0700318 def install(self):
319 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200320 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700321 if not self.has_src:
322 self._download_src()
323 else:
324 log.debug("Already has src {}".format(self.src_file))
325 self._unpack_src()
326 self._build_src()
327 self._make_install()
328 else:
329 log.info("Already has installation {}".format(self.install_dir))
330 # validate installation
331 version = self.openssl_version
332 if self.version not in version:
333 raise ValueError(version)
334
335 def recompile_pymods(self):
336 log.warning("Using build from {}".format(self.build_dir))
337 # force a rebuild of all modules that use OpenSSL APIs
338 for fname in self.module_files:
339 os.utime(fname, None)
340 # remove all build artefacts
341 for root, dirs, files in os.walk('build'):
342 for filename in files:
343 if filename.startswith(self.module_libs):
344 os.unlink(os.path.join(root, filename))
345
346 # overwrite header and library search paths
347 env = os.environ.copy()
348 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
349 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
350 # set rpath
351 env["LD_RUN_PATH"] = self.lib_dir
352
353 log.info("Rebuilding Python modules")
354 cmd = [sys.executable, "setup.py", "build"]
355 self._subprocess_call(cmd, env=env)
356 self.check_imports()
357
358 def check_imports(self):
359 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
360 self._subprocess_call(cmd)
361
362 def check_pyssl(self):
363 version = self.pyssl_version
364 if self.version not in version:
365 raise ValueError(version)
366
367 def run_python_tests(self, tests, network=True):
368 if not tests:
369 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
370 elif sys.version_info < (3, 3):
371 cmd = [sys.executable, '-m', 'test.regrtest']
372 else:
373 cmd = [sys.executable, '-m', 'test', '-j0']
374 if network:
375 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
376 cmd.extend(['-w', '-r'])
377 cmd.extend(tests)
378 self._subprocess_call(cmd, stdout=None)
379
380
381class BuildOpenSSL(AbstractBuilder):
382 library = "OpenSSL"
383 url_template = "https://www.openssl.org/source/openssl-{}.tar.gz"
384 src_template = "openssl-{}.tar.gz"
385 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100386 # only install software, skip docs
387 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700388
Christian Heimes62d618c2020-05-15 18:48:25 +0200389 def _post_install(self):
390 if self.version.startswith("3.0"):
391 self._post_install_300()
392
393 def _post_install_300(self):
394 # create ssl/ subdir with example configs
395 self._subprocess_call(
396 ["make", "-j1", "install_ssldirs"],
397 cwd=self.build_dir
398 )
399 # Install FIPS module
400 # https://wiki.openssl.org/index.php/OpenSSL_3.0#Completing_the_installation_of_the_FIPS_Module
401 fipsinstall_cnf = os.path.join(
402 self.install_dir, "ssl", "fipsinstall.cnf"
403 )
404 openssl_fips_cnf = os.path.join(
405 self.install_dir, "ssl", "openssl-fips.cnf"
406 )
407 fips_mod = os.path.join(self.lib_dir, "ossl-modules/fips.so")
408 self._subprocess_call(
409 [
410 self.openssl_cli, "fipsinstall",
411 "-out", fipsinstall_cnf,
412 "-module", fips_mod,
413 "-provider_name", "fips",
414 "-mac_name", "HMAC",
415 "-macopt", "digest:SHA256",
416 "-macopt", "hexkey:00",
417 "-section_name", "fips_sect"
418 ]
419 )
420 with open(openssl_fips_cnf, "w") as f:
421 f.write(OPENSSL_FIPS_CNF.format(self=self))
422
Christian Heimesd3b9f972017-09-06 18:59:22 -0700423
424class BuildLibreSSL(AbstractBuilder):
425 library = "LibreSSL"
426 url_template = (
427 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{}.tar.gz")
428 src_template = "libressl-{}.tar.gz"
429 build_template = "libressl-{}"
430
431
432def configure_make():
433 if not os.path.isfile('Makefile'):
434 log.info('Running ./configure')
435 subprocess.check_call([
436 './configure', '--config-cache', '--quiet',
437 '--with-pydebug'
438 ])
439
440 log.info('Running make')
441 subprocess.check_call(['make', '--quiet'])
442
443
444def main():
445 args = parser.parse_args()
446 if not args.openssl and not args.libressl:
447 args.openssl = list(OPENSSL_RECENT_VERSIONS)
448 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
449 if not args.disable_ancient:
450 args.openssl.extend(OPENSSL_OLD_VERSIONS)
451 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
452
453 logging.basicConfig(
454 level=logging.DEBUG if args.debug else logging.INFO,
455 format="*** %(levelname)s %(message)s"
456 )
457
458 start = datetime.now()
459
Christian Heimesced9cb52018-01-16 21:02:26 +0100460 if args.steps in {'modules', 'tests'}:
461 for name in ['setup.py', 'Modules/_ssl.c']:
462 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
463 parser.error(
464 "Must be executed from CPython build dir"
465 )
466 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700467 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100468 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700469 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100470 # check for configure and run make
471 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700472
473 # download and register builder
474 builds = []
475
476 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100477 build = BuildOpenSSL(
478 version,
479 args
480 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700481 build.install()
482 builds.append(build)
483
484 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100485 build = BuildLibreSSL(
486 version,
487 args
488 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700489 build.install()
490 builds.append(build)
491
Christian Heimesced9cb52018-01-16 21:02:26 +0100492 if args.steps in {'modules', 'tests'}:
493 for build in builds:
494 try:
495 build.recompile_pymods()
496 build.check_pyssl()
497 if args.steps == 'tests':
498 build.run_python_tests(
499 tests=args.tests,
500 network=args.network,
501 )
502 except Exception as e:
503 log.exception("%s failed", build)
504 print("{} failed: {}".format(build, e), file=sys.stderr)
505 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700506
Christian Heimesced9cb52018-01-16 21:02:26 +0100507 log.info("\n{} finished in {}".format(
508 args.steps.capitalize(),
509 datetime.now() - start
510 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700511 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100512 if args.steps == 'tests':
513 if args.tests:
514 print('Executed Tests:', ' '.join(args.tests))
515 else:
516 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700517
518 print('OpenSSL / LibreSSL versions:')
519 for build in builds:
520 print(" * {0.library} {0.version}".format(build))
521
522
523if __name__ == "__main__":
524 main()