1#!./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 33 from urllib.error import HTTPError 34except ImportError: 35 from urllib2 import urlopen, HTTPError 36import shutil 37import string 38import subprocess 39import sys 40import tarfile 41 42 43log = logging.getLogger("multissl") 44 45OPENSSL_OLD_VERSIONS = [ 46 "1.0.2u", 47 "1.1.0l", 48] 49 50OPENSSL_RECENT_VERSIONS = [ 51 "1.1.1g", 52 # "3.0.0-alpha2" 53] 54 55LIBRESSL_OLD_VERSIONS = [ 56 "2.9.2", 57] 58 59LIBRESSL_RECENT_VERSIONS = [ 60 "3.1.0", 61] 62 63# store files in ../multissl 64HERE = 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 68 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', 79 help="Enable debug logging", 80) 81parser.add_argument( 82 '--disable-ancient', 83 action='store_true', 84 help="Don't test OpenSSL and LibreSSL versions without upstream support", 85) 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( 122 '--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 ) 130) 131parser.add_argument( 132 '--system', 133 default='', 134 help="Override the automatic system type detection." 135) 136parser.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) 148 149OPENSSL_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 166 167class AbstractBuilder(object): 168 library = None 169 url_templates = None 170 src_template = None 171 build_template = None 172 install_target = 'install' 173 174 module_files = ("Modules/_ssl.c", 175 "Modules/_hashopenssl.c") 176 module_libs = ("_ssl", "_hashlib") 177 178 def __init__(self, version, args): 179 self.version = version 180 self.args = args 181 # installation directory 182 self.install_dir = os.path.join( 183 os.path.join(args.base_directory, self.library.lower()), version 184 ) 185 # source file 186 self.src_dir = os.path.join(args.base_directory, 'src') 187 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)) 192 self.system = args.system 193 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 209 def short_version(self): 210 """Short version for OpenSSL download URL""" 211 return None 212 213 @property 214 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) 266 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") 283 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 312 cmd = [ 313 "./config", 314 "shared", "--debug", 315 "--prefix={}".format(self.install_dir) 316 ] 317 # cmd.extend(["no-deprecated", "--api=1.1.0"]) 318 env = os.environ.copy() 319 # set rpath 320 env["LD_RUN_PATH"] = self.lib_dir 321 if self.system: 322 env['SYSTEM'] = self.system 323 self._subprocess_call(cmd, cwd=cwd, env=env) 324 # Old OpenSSL versions do not support parallel builds. 325 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env) 326 327 def _make_install(self): 328 self._subprocess_call( 329 ["make", "-j1", self.install_target], 330 cwd=self.build_dir 331 ) 332 self._post_install() 333 if not self.args.keep_sources: 334 shutil.rmtree(self.build_dir) 335 336 def _post_install(self): 337 pass 338 339 def install(self): 340 log.info(self.openssl_cli) 341 if not self.has_openssl or self.args.force: 342 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" 404 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 ) 408 src_template = "openssl-{}.tar.gz" 409 build_template = "openssl-{}" 410 # only install software, skip docs 411 install_target = 'install_sw' 412 413 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)) 446 @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 453 454 455class BuildLibreSSL(AbstractBuilder): 456 library = "LibreSSL" 457 url_templates = ( 458 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz", 459 ) 460 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 492 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): 499 parser.error( 500 "Must be executed with ./python from CPython build dir" 501 ) 502 # check for configure and run make 503 configure_make() 504 505 # download and register builder 506 builds = [] 507 508 for version in args.openssl: 509 build = BuildOpenSSL( 510 version, 511 args 512 ) 513 build.install() 514 builds.append(build) 515 516 for version in args.libressl: 517 build = BuildLibreSSL( 518 version, 519 args 520 ) 521 build.install() 522 builds.append(build) 523 524 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) 538 539 log.info("\n{} finished in {}".format( 540 args.steps.capitalize(), 541 datetime.now() - start 542 )) 543 print('Python: ', sys.version) 544 if args.steps == 'tests': 545 if args.tests: 546 print('Executed Tests:', ' '.join(args.tests)) 547 else: 548 print('Executed all SSL tests.') 549 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() 557