From ab3759fff5e9d301b039ce94e871df820414ba61 Mon Sep 17 00:00:00 2001 From: Cameron Ball Date: Tue, 2 Aug 2016 16:31:25 +0800 Subject: [PATCH] MDL-55404 core_upgrade: Add libcurl environment checks 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 | 5 + lang/en/admin.php | 1 + lib/classes/upgrade/util.php | 115 ++++++++++++++++++++ lib/tests/environment_test.php | 10 ++ lib/tests/upgrade_util_test.php | 183 ++++++++++++++++++++++++++++++++ lib/upgradelib.php | 24 +++++ 6 files changed, 338 insertions(+) create mode 100644 lib/classes/upgrade/util.php create mode 100644 lib/tests/upgrade_util_test.php diff --git a/admin/environment.xml b/admin/environment.xml index 7bef8fc2743..5775d1a59a7 100644 --- a/admin/environment.xml +++ b/admin/environment.xml @@ -1846,6 +1846,11 @@ + + + + + diff --git a/lang/en/admin.php b/lang/en/admin.php index 52f555bfb2b..f3419e19d25 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -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 index 00000000000..c3009010825 --- /dev/null +++ b/lib/classes/upgrade/util.php @@ -0,0 +1,115 @@ +. + +/** + * PayPal enrolment plugin utility class. + * + * @package core + * @copyright 2016 Cameron Ball + * @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 + * @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; + } +} diff --git a/lib/tests/environment_test.php b/lib/tests/environment_test.php index e59fbdbe3cd..a660edb0a62 100644 --- a/lib/tests/environment_test.php +++ b/lib/tests/environment_test.php @@ -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 index 00000000000..7016ce13365 --- /dev/null +++ b/lib/tests/upgrade_util_test.php @@ -0,0 +1,183 @@ +. + +/** + * Upgrade utility class tests. + * + * @package core + * @copyright 2016 Cameron Ball + * @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 + * @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); + } +} diff --git a/lib/upgradelib.php b/lib/upgradelib.php index f8c95cd64fb..f5c79f0f486 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -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; +}