From b0e3f8312318a45ebeca8179dda1962caf94f4ed Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Fri, 11 Apr 2014 17:17:06 +0100 Subject: [PATCH] Add OpenSSL version check and tests. Signed-off-by: Daira Hopwood --- src/allmydata/__init__.py | 62 ++++++++++++++++++++++++++ src/allmydata/_auto_deps.py | 17 ++++---- src/allmydata/test/test_version.py | 70 +++++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 9 deletions(-) diff --git a/src/allmydata/__init__.py b/src/allmydata/__init__.py index 4bafed47..94ad355c 100644 --- a/src/allmydata/__init__.py +++ b/src/allmydata/__init__.py @@ -386,9 +386,71 @@ def check_all_requirements(): except (ImportError, PackagingError), e: errors.append("%s: %s" % (e.__class__.__name__, e)) + try: + from OpenSSL import SSL + check_openssl_version(SSL) + except PackagingError, e: + errors.append("%s: %s" % (e.__class__.__name__, e)) + except Exception, e: + errors.append("Unable to check OpenSSL version due to %s: %s" % (e.__class__.__name__, e)) + if errors: raise PackagingError(get_error_string(errors, debug=True)) +MONTHS = {'Jan': 1, 'Feb':2, 'Mar':3, 'Apr':4, 'May':5, 'Jun':6, 'Jul':7, 'Aug':8, 'Sep':9, 'Oct':10, 'Nov':11, 'Dec':12} + +def parse_build_date(build_date): + day = int(build_date[0]) + month = MONTHS[build_date[1]] + year = int(build_date[2]) + return (year, month, day) + +def check_openssl_version(SSL): + openssl_version = SSL.SSLeay_version(SSL.SSLEAY_VERSION) + split_version = openssl_version.split(' ') + + if len(split_version) < 2 or split_version[0] != 'OpenSSL': + raise PackagingError("could not understand OpenSSL version string %s" % (openssl_version,)) + + try: + components = split_version[1].split('.') + numeric_components = map(int, components[:2]) + if len(components) > 2: + m = re.match(r'[0-9]*', components[2]) + numeric_components += [int(m.group(0))] + + if ((numeric_components == [0, 9, 8] and components[2] >= '8y') or + (numeric_components == [1, 0, 0] and components[2] >= '0l') or + (numeric_components == [1, 0, 1] and components[2] >= '1g') or + (numeric_components >= [1, 0, 2])): + return + + if numeric_components == [1, 0, 1] and components[2] >= '1d': + # Unfortunately, Debian and Ubuntu patched the Heartbleed bug without bumping + # the version number or providing any other way to detect the patch status. + # (BAD! STOP DOING THIS!) So we allow versions 1.0.1d through 1.0.1f provided + # they were compiled on or after 6 April 2014. + if len(split_version) >= 5 and parse_build_date(split_version[2:5]) >= (2014, 4, 6): + return + + # We also allow those versions if compiled with -DOPENSSL_NO_HEARTBEATS. + try: + openssl_cflags = SSL.SSLeay_version(SSL.SSLEAY_CFLAGS) + except Exception, e: + raise PackagingError("refusing to use %s which may be vulnerable to security bugs.\n" + "Unable to check compilation flags due to %s: %s\n" + "Please upgrade to OpenSSL 1.0.1g or later." + % (openssl_version, e.__class__.__name__, e)) + else: + if '-DOPENSSL_NO_HEARTBEATS' in openssl_cflags.split(' '): + return + except Exception, e: + pass + + raise PackagingError("refusing to use %s which may be vulnerable to security bugs.\n" + "Please upgrade to OpenSSL 1.0.1g or later." % (openssl_version,)) + + check_all_requirements() diff --git a/src/allmydata/_auto_deps.py b/src/allmydata/_auto_deps.py index b7be3212..5c0794ff 100644 --- a/src/allmydata/_auto_deps.py +++ b/src/allmydata/_auto_deps.py @@ -38,17 +38,18 @@ install_requires = [ # * foolscap < 0.6.3 is incompatible with Twisted-11.1.0 and newer. Since # current Twisted is 12.0, any build which needs twisted will grab a # version that requires foolscap>=0.6.3 - # * pyOpenSSL is required by foolscap for it (foolscap) to provide secure + # + "foolscap >= 0.6.3", + + # * pyOpenSSL is required by foolscap for foolscap to provide secure # connections. Foolscap doesn't reliably declare this dependency in a # machine-readable way, so we need to declare a dependency on pyOpenSSL - # ourselves. Tahoe-LAFS doesn't *really* depend directly on pyOpenSSL, - # so if something changes in the relationship between foolscap and - # pyOpenSSL, such as foolscap requiring a specific version of - # pyOpenSSL, or foolscap switching from pyOpenSSL to a different crypto - # library, we need to update this declaration here. + # ourselves. + # * To check for OpenSSL security vulnerabilities such as Heartbleed, + # we need to know the version of OpenSSL that pyOpenSSL is using. The + # necessary API to do this requires pyOpenSSL >= 0.13. # - "foolscap >= 0.6.3", - "pyOpenSSL", + "pyOpenSSL >= 0.13", "Nevow >= 0.6.0", diff --git a/src/allmydata/test/test_version.py b/src/allmydata/test/test_version.py index 296db062..5e30d2a0 100644 --- a/src/allmydata/test/test_version.py +++ b/src/allmydata/test/test_version.py @@ -1,12 +1,27 @@ from twisted.trial import unittest -from allmydata import check_requirement, cross_check, PackagingError +from allmydata import check_requirement, cross_check, check_openssl_version, parse_build_date, PackagingError from allmydata.util.verlib import NormalizedVersion as V, \ IrrationalVersionError, \ suggest_normalized_version as suggest +class MockSSL(object): + SSLEAY_VERSION = 0 + SSLEAY_CFLAGS = 2 + + def __init__(self, version, compiled_without_heartbeats=False): + self.opts = { + self.SSLEAY_VERSION: version, + self.SSLEAY_CFLAGS: compiled_without_heartbeats and 'compiler: gcc -DOPENSSL_NO_HEARTBEATS' + or 'compiler: gcc', + } + + def SSLeay_version(self, which): + return self.opts[which] + + class CheckRequirement(unittest.TestCase): def test_check_requirement(self): check_requirement("setuptools >= 0.6c6", {"setuptools": ("0.6", "", None)}) @@ -106,6 +121,59 @@ class CheckRequirement(unittest.TestCase): self.failUnlessEqual(len(res), 1) self.failUnlessIn("but version '2.0'", res[0]) + def test_check_openssl_version(self): + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("NotOpenSSL")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL a.b.c")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.1.x")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 0.9")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 0.9.0")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 0.9.8")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 0.9.8", True)) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 0.9.8x")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.0")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.0", True)) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.0k")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.1")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.1e 11 Feb 2013")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.1e 5 Apr 2014")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.1e 7 Abc 2014")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.1e invalid_date")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.1e 7 Apr")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 0.10")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 0.10.0")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.0")) + self.failUnlessRaises(PackagingError, check_openssl_version, MockSSL("OpenSSL 1.0.0 1 Jan 2000")) + + check_openssl_version(MockSSL("OpenSSL 0.9.8y")) + check_openssl_version(MockSSL("OpenSSL 0.9.8z")) + check_openssl_version(MockSSL("OpenSSL 1.0.0l")) + check_openssl_version(MockSSL("OpenSSL 1.0.0m")) + check_openssl_version(MockSSL("OpenSSL 1.0.1", True)) + check_openssl_version(MockSSL("OpenSSL 1.0.1e 11 Feb 2013", True)) + check_openssl_version(MockSSL("OpenSSL 1.0.1 7 Apr 2014")) + check_openssl_version(MockSSL("OpenSSL 1.0.1e 7 Apr 2014")) + check_openssl_version(MockSSL("OpenSSL 1.0.1g 1 Mar 2014")) + check_openssl_version(MockSSL("OpenSSL 1.0.1h 1 Jan 2015")) + check_openssl_version(MockSSL("OpenSSL 1.0.1zzz")) + check_openssl_version(MockSSL("OpenSSL 1.0.2")) + check_openssl_version(MockSSL("OpenSSL 1.0.2a")) + check_openssl_version(MockSSL("OpenSSL 1.0.10a")) + check_openssl_version(MockSSL("OpenSSL 1.1")) + check_openssl_version(MockSSL("OpenSSL 1.1.0")) + check_openssl_version(MockSSL("OpenSSL 1.1.0a")) + check_openssl_version(MockSSL("OpenSSL 1.10")) + check_openssl_version(MockSSL("OpenSSL 1.10.10a")) + check_openssl_version(MockSSL("OpenSSL 2")) + check_openssl_version(MockSSL("OpenSSL 2.0.0 31 Dec 2020")) + check_openssl_version(MockSSL("OpenSSL 10.0.0 31 Dec 2099")) + + self.failUnlessEqual(parse_build_date(['1', 'Jan', '2000']), (2000, 1, 1)) + self.failUnlessEqual(parse_build_date(['5', 'Apr', '2014']), (2014, 4, 5)) + self.failUnlessEqual(parse_build_date(['7', 'Apr', '2014']), (2014, 4, 7)) + self.failUnlessRaises(Exception, parse_build_date, []) + self.failUnlessRaises(Exception, parse_build_date, ['1', 'Abc' '2000']) + # based on https://bitbucket.org/tarek/distutilsversion/src/17df9a7d96ef/test_verlib.py -- 2.45.2