MDL-55404 core_upgrade: Add libcurl environment checks
authorCameron Ball <cameron@moodle.com>
Tue, 2 Aug 2016 08:31:25 +0000 (16:31 +0800)
committerCameron Ball <cameron@moodle.com>
Tue, 6 Sep 2016 03:34:46 +0000 (11:34 +0800)
This patch adds logic to test whether or not the
libcurl (used by PHP/cURL)  was compiled against a compatible
SSL/TLS library.

admin/environment.xml
lang/en/admin.php
lib/classes/upgrade/util.php [new file with mode: 0644]
lib/tests/environment_test.php
lib/tests/upgrade_util_test.php [new file with mode: 0644]
lib/upgradelib.php

index 7bef8fc..5775d1a 100644 (file)
           <ON_CHECK message="unoconvwarning" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_tls_libraries" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tlswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
 </COMPATIBILITY_MATRIX>
index 52f555b..f3419e1 100644 (file)
@@ -1086,6 +1086,7 @@ $string['timezoneisforcedto'] = 'Force all users to use';
 $string['timezonenotforced'] = 'Users can choose their own timezone';
 $string['timezonephpdefault'] = 'Default PHP timezone ({$a})';
 $string['timezoneserver'] = 'Server timezone ({$a})';
+$string['tlswarning'] = 'No PHP/cURL extension with TLSv1.2 support has been detected. Some services may not work. It is strongly recommentd that you upgrade your TLS libraries.';
 $string['tokenizerrecommended'] = 'Installing the optional PHP Tokenizer extension is recommended -- it improves Moodle Networking functionality.';
 $string['tools'] = 'Admin tools';
 $string['toolsmanage'] = 'Manage admin tools';
diff --git a/lib/classes/upgrade/util.php b/lib/classes/upgrade/util.php
new file mode 100644 (file)
index 0000000..c300901
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PayPal enrolment plugin utility class.
+ *
+ * @package    core
+ * @copyright  2016 Cameron Ball <cameron@cameron1729.xyz>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\upgrade;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Core upgrade utility class.
+ *
+ * @package   core
+ * @copyright 2016 Cameron Ball <cameron@cameron1729.xyz>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+final class util {
+
+    /**
+     * Gets the minimum version of a SSL/TLS library required for TLS 1.2 support.
+     *
+     * @param  string $sslflavour The SSL/TLS library
+     * @return string|false The version string if it exists. False otherwise
+     */
+    private static function get_min_ssl_lib_version_for_tls12($sslflavour) {
+        // Min versions for TLS 1.2.
+        $versionmatrix = [
+            'OpenSSL' => '1.0.1c',
+            'GnuTLS' => '1.7.1',
+            'NSS' => '3.15.1', // This number is usually followed by something like "Basic ECC".
+            'CyaSSL' => '1.1.0',
+            'wolfSSL' => '1.1.0',
+            'PolarSSL' => '1.2.0',
+            'WinSSL' => '*', // Does not specify a version but needs Windows >= 7.
+            'SecureTransport' => '*' // Does not specify a version but needs iOS >= 5.0 or OS X >= 10.8.0.
+        ];
+
+        return isset($versionmatrix[$sslflavour]) ? $versionmatrix[$sslflavour] : false;
+    }
+
+    /**
+     * Validates PHP/cURL extension for use with SSL/TLS.
+     *
+     * @param  array $curlinfo array of cURL information as returned by curl_version()
+     * @param  int   $zts 0 or 1 as defined by PHP_ZTS
+     * @return bool
+     */
+    public static function validate_php_curl_tls(array $curlinfo, $zts) {
+        if (empty($curlinfo['ssl_version'])) {
+            return false;
+        }
+
+        $flavour = explode('/', $curlinfo['ssl_version'])[0];
+        // In threadsafe mode the only valid choices are OpenSSL and GnuTLS.
+        if ($zts === 1 && $flavour != 'OpenSSL' && $flavour !== 'GnuTLS') {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Tests if the system is capable of using TLS 1.2 for requests.
+     *
+     * @param  array  $curlinfo array of cURL information as returned by curl_version()
+     * @param  string $uname server uname
+     * @return bool
+     */
+    public static function can_use_tls12(array $curlinfo, $uname) {
+        if ($curlinfo['version_number'] < 467456 || !defined('CURL_SSLVERSION_TLSv1_2')) {
+            return false;
+        }
+
+        $sslversion = explode('/', $curlinfo['ssl_version']);
+        // NSS has a space in the version number ðŸ˜¦.
+        $flavour = explode(' ', $sslversion[0])[0];
+        $version = count($sslversion) == 2 ? $sslversion[1] : null;
+
+        $minversion = self::get_min_ssl_lib_version_for_tls12($flavour);
+        if (!$minversion) {
+            return false;
+        }
+
+        // Special case (see $versionmatrix above).
+        if ($flavour == 'WinSSL') {
+            return $uname >= '6.1';
+        }
+
+        // Special case (see $versionmatrix above).
+        if ($flavour == 'SecureTransport') {
+            return $uname >= '10.8.0';
+        }
+
+        return $version >= $minversion;
+    }
+}
index e59fbdb..a660edb 100644 (file)
@@ -40,6 +40,8 @@ class core_environment_testcase extends advanced_testcase {
         require_once($CFG->libdir.'/environmentlib.php');
         list($envstatus, $environment_results) = check_moodle_environment(normalize_version($CFG->release), ENV_SELECT_RELEASE);
 
+        $sslmessages = ['ssl/tls configuration not supported', 'invalid ssl/tls configuration'];
+
         $this->assertNotEmpty($envstatus);
         foreach ($environment_results as $environment_result) {
             if ($environment_result->part === 'php_setting'
@@ -50,6 +52,14 @@ class core_environment_testcase extends advanced_testcase {
                 $this->markTestSkipped('OPCache extension is not necessary for unit testing.');
                 continue;
             }
+            if ($environment_result->part === 'custom_check'
+                and in_array($environment_result->info, $sslmessages)
+                and $environment_result->getLevel() === 'optional'
+                and $environment_result->getStatus() === false
+            ) {
+                $this->markTestSkipped('Up-to-date TLS libraries are not necessary for unit testing.');
+                continue;
+            }
             $this->assertTrue($environment_result->getStatus(), "Problem detected in environment ($environment_result->part:$environment_result->info), fix all warnings and errors!");
         }
     }
diff --git a/lib/tests/upgrade_util_test.php b/lib/tests/upgrade_util_test.php
new file mode 100644 (file)
index 0000000..7016ce1
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Upgrade utility class  tests.
+ *
+ * @package    core
+ * @copyright  2016 Cameron Ball <cameron@cameron1729.xyz>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Upgrade utility class tests.
+ *
+ * @package   core
+ * @copyright 2016 Cameron Ball <cameron@cameron1729.xyz>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class upgrade_util_testcase extends advanced_testcase {
+
+    /**
+     * A cURL version that supports TLS 1.2.
+     */
+    const VALID_CURL_VERSION = 467456;
+
+    /**
+     * A cURL version that does not support TLS 1.2.
+     */
+    const INVALID_CURL_VERSION = 467455;
+
+    /**
+     * The value of PHP_ZTS when thread safety is enabled.
+     */
+    const PHP_ZTS_ENABLED = 1;
+
+    /**
+     * The value of PHP_ZTS when thread safety is disabled.
+     */
+    const PHP_ZTS_DISABLED = 0;
+
+    /**
+     * Test PHP/cURL validation.
+     *
+     * @dataProvider validate_php_curl_tls_testcases()
+     * @param array $curlinfo server curl_version array
+     * @param int   $zts      0 or 1 as defined by PHP_ZTS
+     * @param bool  $expected expected result
+     */
+    public function test_validate_php_curl_tls($curlinfo, $zts, $expected) {
+        $expected === true && $this->assertTrue(\core\upgrade\util::validate_php_curl_tls($curlinfo, $zts));
+        $expected === false && $this->assertFalse(\core\upgrade\util::validate_php_curl_tls($curlinfo, $zts));
+    }
+
+    /**
+     * Test cases for validate_php_curl_tls test.
+     */
+    public function validate_php_curl_tls_testcases() {
+        $base = curl_version();
+
+        return [
+            'Not threadsafe - Valid SSL (GnuTLS)' => [
+                ['ssl_version' => 'GnuTLS/4.20'] + $base,
+                self::PHP_ZTS_DISABLED,
+                true
+            ],
+            'Not threadsafe - Valid SSL (OpenSSL)' => [
+                ['ssl_version' => 'OpenSSL'] + $base,
+                self::PHP_ZTS_DISABLED,
+                true
+            ],
+            'Not threadsafe - Valid SSL (WinSSL)' => [
+                ['ssl_version' => 'WinSSL'] + $base,
+                self::PHP_ZTS_DISABLED,
+                true
+            ],
+            'Not threadsafe - Invalid SSL' => [
+                ['ssl_version' => ''] + $base,
+                self::PHP_ZTS_DISABLED,
+                false
+            ],
+            'Threadsafe - Valid SSL (OpenSSL)' => [
+                ['ssl_version' => 'OpenSSL/1729'] + $base,
+                self::PHP_ZTS_ENABLED,
+                true
+            ],
+            'Threadsafe - Valid SSL (GnuTLS)' => [
+                ['ssl_version' => 'GnuTLS/3.14'] + $base,
+                self::PHP_ZTS_ENABLED,
+                true
+            ],
+            'Threadsafe - Invalid SSL' => [
+                ['ssl_version' => ''] + $base,
+                self::PHP_ZTS_ENABLED,
+                false
+            ],
+            'Threadsafe - Invalid SSL (but not empty)' => [
+                ['ssl_version' => 'Not GnuTLS or OpenSSL'] + $base,
+                self::PHP_ZTS_ENABLED,
+                false
+            ]
+        ];
+    }
+
+    /**
+     * Test various combinations of SSL/TLS libraries.
+     *
+     * @dataProvider can_use_tls12_testcases
+     * @param array $environment the server environment
+     * @param bool  $expected    expected result
+     */
+    public function test_can_use_tls12($environment, $expected) {
+        $curlinfo = $environment['curl_version'] + curl_version();
+
+        if ($curlinfo['version_number'] >= self::VALID_CURL_VERSION && !defined('CURL_SSLVERSION_TLSv1_2')) {
+            define('CURL_SSLVERSION_TLSv1_2', 6);
+        }
+
+        $expected === true && $this->assertTrue(\core\upgrade\util::can_use_tls12($curlinfo, $environment['uname']));
+        $expected === false && $this->assertFalse(\core\upgrade\util::can_use_tls12($curlinfo, $environment['uname']));
+    }
+
+    /**
+     * Test cases for the can_use_tls test.
+     *
+     * @return array of testcases
+     */
+    public function can_use_tls12_testcases() {
+        $versionmatrix = [
+            'OpenSSL'         => ['Older' => '0.9.8o',  'Min required' => '1.0.1c',           'Newer' => '1.0.1t'],
+            'GnuTLS'          => ['Older' => '1.5.0',   'Min requires' => '1.7.1',            'Newer' => '1.8.1'],
+            'NSS'             => ['Older' => '3.14.15', 'Min required' => '3.15.1 Basic ECC', 'Newer' => '3.17.2 Basic ECC'],
+            'CyaSSL'          => ['Older' => '0.9.9',   'Min required' => '1.1.0',            'Newer' => '1.2.0'],
+            'wolfSSL'         => ['Older' => '1.0.0',   'Min required' => '1.1.0',            'Newer' => '1.2.0'],
+            'WinSSL'          => ['Older' => '5.1',     'Min required' => '6.1',              'Newer' => '7.0'],
+            'SecureTransport' => ['Older' => '10.7.5',  'Min required' => '10.8.0',           'Newer' => '10.9.0']
+        ];
+
+        // This will generate an array of testcases from the matrix above.
+        // It generates one testcase for every version. If the version is too
+        // old or the cURL version (passed as an argument) is too old, the
+        // expected result of the testcase is false. Otherwise it is true.
+        //
+        // Each testcase is given a name like WinSSL/Valid env/Min required.
+        // The first part is the SSL/TLS library, the second part is whether
+        // or not the environment is valid (i.e., we are using a valid/invalid
+        // cURL version. The final part says which version of the SSL/TLS library
+        // is being used (i.e., Older, Min required or Newer).
+        $generatetestcases = function($curlversion) use ($versionmatrix) {
+            return array_reduce(array_keys($versionmatrix), function($carry, $sslflavour) use ($versionmatrix, $curlversion) {
+                return $carry + array_reduce(array_keys($versionmatrix[$sslflavour]), function($carry, $sslversion)
+                        use ($versionmatrix, $curlversion, $sslflavour) {
+                    $env = $curlversion == self::VALID_CURL_VERSION ? 'Valid' : 'Invalid';
+                    $exceptions = ['WinSSL', 'SecureTransport'];
+                    $versionsuffix = in_array($sslflavour, $exceptions) ? '' : '/' . $versionmatrix[$sslflavour][$sslversion];
+                    return $carry + [$sslflavour . '/' . $env. ' env/' . $sslversion => [[
+                        'curl_version' => [
+                            'ssl_version' => $sslflavour . $versionsuffix,
+                            'version_number' => $curlversion
+                        ],
+                        'uname' => in_array($sslflavour, $exceptions) ? $versionmatrix[$sslflavour][$sslversion] : php_uname('r')
+                    ], $sslversion != 'Older' && $curlversion != self::INVALID_CURL_VERSION]];
+                }, []);
+            }, []);
+        };
+
+        return $generatetestcases(self::VALID_CURL_VERSION) + $generatetestcases(self::INVALID_CURL_VERSION);
+    }
+}
index f8c95cd..f5c79f0 100644 (file)
@@ -2317,3 +2317,27 @@ function check_unoconv_version(environment_results $result) {
     }
     return null;
 }
+
+/**
+ * Checks for up-to-date TLS libraries.
+ *
+ * @param environment_results $result object to update, if relevant.
+ * @return environment_results|null updated results or null if unoconv path is not executable.
+ */
+function check_tls_libraries(environment_results $result) {
+    global $CFG;
+
+    if (!\core\upgrade\util::validate_php_curl_tls(curl_version(), PHP_ZTS)) {
+        $result->setInfo('invalid ssl/tls configuration');
+        $result->setStatus(false);
+        return $result;
+    }
+
+    if (!\core\upgrade\util::can_use_tls12(curl_version(), php_uname('r'))) {
+        $result->setInfo('ssl/tls configuration not supported');
+        $result->setStatus(false);
+        return $result;
+    }
+
+    return null;
+}