From ea182896b0082211479ab93e2a0b717ae7ddb4e4 Mon Sep 17 00:00:00 2001
From: Daira Hopwood <daira@jacaranda.org>
Date: Mon, 1 Jun 2015 14:52:03 +0100
Subject: [PATCH] Add OpenSSL version check and tests.

Signed-off-by: Daira Hopwood <daira@jacaranda.org>
---
 src/allmydata/__init__.py          | 62 ++++++++++++++++++++++++++
 src/allmydata/_auto_deps.py        |  3 +-
 src/allmydata/test/test_version.py | 70 +++++++++++++++++++++++++++++-
 3 files changed, 133 insertions(+), 2 deletions(-)

diff --git a/src/allmydata/__init__.py b/src/allmydata/__init__.py
index 44f971f8..9883931d 100644
--- a/src/allmydata/__init__.py
+++ b/src/allmydata/__init__.py
@@ -400,9 +400,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 69a60815..eb469f6f 100644
--- a/src/allmydata/_auto_deps.py
+++ b/src/allmydata/_auto_deps.py
@@ -169,7 +169,8 @@ else:
 #   not *directly* depend on pyOpenSSL.
 #
 # * pyOpenSSL >= 0.13 is needed in order to avoid
-#   <https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2005>.
+#   <https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2005>, and also to check the
+#   version of OpenSSL that pyOpenSSL is using.
 #
 # * pyOpenSSL >= 0.14 is built on the 'cryptography' package which depends
 #   on 'cffi' (and indirectly several other packages). Unfortunately cffi
diff --git a/src/allmydata/test/test_version.py b/src/allmydata/test/test_version.py
index fc642777..9471016e 100644
--- a/src/allmydata/test/test_version.py
+++ b/src/allmydata/test/test_version.py
@@ -3,12 +3,27 @@ from pkg_resources import Requirement
 
 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):
         self._check_success("setuptools >= 0.6c6", {"setuptools": ("0.6", "", None)})
@@ -118,6 +133,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