Tidy up and general improvement of sAN handling code
* Move OpenSSL functions into a new TlsHelper class * Add error when sAN certificate cannot be verified due to CVE-2013-6420 * Throw exception if PHP >= 5.6 manages to use fallback code * Add support for wildcards in CN/sAN * Add tests for cert name validation * Check for backported security fix for CVE-2013-6420 using testcase from PHP tests. * Whitelist some disto PHP versions that have the CVE-2013-6420 fix backported.pull/4798/head
parent
7e2a015e9b
commit
304c268c3b
|
@ -258,7 +258,7 @@ class RemoteFilesystem
|
|||
// Emulate fingerprint validation on PHP < 5.6
|
||||
$params = stream_context_get_params($ctx);
|
||||
$expectedPeerFingerprint = $options['ssl']['peer_fingerprint'];
|
||||
$peerFingerprint = $this->getCertificateFingerprint($params['options']['ssl']['peer_certificate']);
|
||||
$peerFingerprint = TlsHelper::getCertificateFingerprint($params['options']['ssl']['peer_certificate']);
|
||||
|
||||
// Constant time compare??!
|
||||
if ($expectedPeerFingerprint !== $peerFingerprint) {
|
||||
|
@ -383,6 +383,7 @@ class RemoteFilesystem
|
|||
// 4. To prevent any attempt at being hoodwinked by switching the
|
||||
// certificate between steps 2 and 3 the fingerprint of the certificate
|
||||
// presented in step 3 is compared against the one recorded in step 2.
|
||||
if (TlsHelper::isOpensslParseSafe()) {
|
||||
$certDetails = $this->getCertificateCnAndFp($this->fileUrl, $options);
|
||||
|
||||
if ($certDetails) {
|
||||
|
@ -390,6 +391,12 @@ class RemoteFilesystem
|
|||
|
||||
$this->retry = true;
|
||||
}
|
||||
} else {
|
||||
$this->io->writeError(sprintf(
|
||||
'<error>Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.</error>',
|
||||
PHP_VERSION
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->retry) {
|
||||
|
@ -566,15 +573,25 @@ class RemoteFilesystem
|
|||
|
||||
$tlsOptions['ssl']['CN_match'] = $host;
|
||||
$tlsOptions['ssl']['SNI_server_name'] = $host;
|
||||
}
|
||||
|
||||
if (isset($this->peerCertificateMap[$this->getUrlAuthority($originUrl)])) {
|
||||
$urlAuthority = $this->getUrlAuthority($this->fileUrl);
|
||||
|
||||
if (isset($this->peerCertificateMap[$urlAuthority])) {
|
||||
// Handle subjectAltName on lesser PHP's.
|
||||
$certMap = $this->peerCertificateMap[$this->getUrlAuthority($originUrl)];
|
||||
$certMap = $this->peerCertificateMap[$urlAuthority];
|
||||
|
||||
if ($this->io->isDebug()) {
|
||||
$this->io->writeError(sprintf(
|
||||
'Using <info>%s</info> as CN for subjectAltName enabled host <info>%s</info>',
|
||||
$certMap['cn'],
|
||||
$urlAuthority
|
||||
));
|
||||
}
|
||||
|
||||
$tlsOptions['ssl']['CN_match'] = $certMap['cn'];
|
||||
$tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp'];
|
||||
}
|
||||
}
|
||||
|
||||
$headers = array();
|
||||
|
||||
|
@ -849,8 +866,20 @@ class RemoteFilesystem
|
|||
unset($source, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch certificate common name and fingerprint for validation of SAN.
|
||||
*
|
||||
* @todo Remove when PHP 5.6 is minimum supported version.
|
||||
*/
|
||||
private function getCertificateCnAndFp($url, $options)
|
||||
{
|
||||
if (PHP_VERSION_ID >= 50600) {
|
||||
throw new \BadMethodCallException(sprintf(
|
||||
'%s must not be used on PHP >= 5.6',
|
||||
__METHOD__
|
||||
));
|
||||
}
|
||||
|
||||
$context = StreamContextFactory::getContext($url, $options, array('options' => array(
|
||||
'ssl' => array(
|
||||
'capture_peer_cert' => true,
|
||||
|
@ -858,92 +887,30 @@ class RemoteFilesystem
|
|||
))
|
||||
));
|
||||
|
||||
// Ideally this would just use stream_socket_client() to avoid sending a
|
||||
// HTTP request but that does not capture the certificate.
|
||||
if (false === $handle = @fopen($url, 'rb', false, $context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close non authenticated connection without reading any content.
|
||||
fclose($handle);
|
||||
$handle = null;
|
||||
|
||||
$params = stream_context_get_params($context);
|
||||
|
||||
if (!empty($params['options']['ssl']['peer_certificate'])) {
|
||||
$peerCertificate = $params['options']['ssl']['peer_certificate'];
|
||||
|
||||
$fp = $this->getCertificateFingerprint($peerCertificate);
|
||||
$cert = openssl_x509_parse($peerCertificate, false);
|
||||
$commonName = $cert['subject']['commonName'];
|
||||
|
||||
$subjectAltName = preg_split('{\s*,\s*}', $cert['extensions']['subjectAltName']);
|
||||
$subjectAltName = array_filter(array_map(function ($name) {
|
||||
if (0 === strpos($name, 'DNS:')) {
|
||||
return substr($name, 4);
|
||||
}
|
||||
}, $subjectAltName));
|
||||
|
||||
if (in_array(parse_url($url, PHP_URL_HOST), $subjectAltName, true)) {
|
||||
if (TlsHelper::checkCertificateHost($peerCertificate, parse_url($url, PHP_URL_HOST), $commonName)) {
|
||||
return array(
|
||||
'cn' => $commonName,
|
||||
'fp' => $fp,
|
||||
'fp' => TlsHelper::getCertificateFingerprint($peerCertificate),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Support wildcards.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the certificate pin.
|
||||
*
|
||||
* By Kevin McArthur of StormTide Digital Studios Inc.
|
||||
* @KevinSMcArthur / https://github.com/StormTide
|
||||
*
|
||||
* See http://tools.ietf.org/html/draft-ietf-websec-key-pinning-02
|
||||
*
|
||||
* This method was adapted from Sslurp.
|
||||
* https://github.com/EvanDotPro/Sslurp
|
||||
*
|
||||
* (c) Evan Coury <me@evancoury.com>
|
||||
*
|
||||
* For the full copyright and license information, please see below:
|
||||
*
|
||||
* Copyright (c) 2013, Evan Coury
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
private function getCertificateFingerprint($certificate)
|
||||
{
|
||||
$pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate));
|
||||
$pubkeypem = $pubkeydetails['key'];
|
||||
//Convert PEM to DER before SHA1'ing
|
||||
$start = '-----BEGIN PUBLIC KEY-----';
|
||||
$end = '-----END PUBLIC KEY-----';
|
||||
$pemtrim = substr($pubkeypem, (strpos($pubkeypem, $start) + strlen($start)), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1));
|
||||
$der = base64_decode($pemtrim);
|
||||
|
||||
return sha1($der);
|
||||
}
|
||||
|
||||
private function getUrlAuthority($url)
|
||||
{
|
||||
$defaultPorts = array(
|
||||
|
@ -952,9 +919,18 @@ class RemoteFilesystem
|
|||
'https' => 443,
|
||||
);
|
||||
|
||||
$defaultPort = $defaultPorts[parse_url($this->fileUrl, PHP_URL_SCHEME)];
|
||||
$port = parse_url($this->fileUrl, PHP_URL_PORT) ?: $defaultPort;
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
|
||||
return parse_url($this->fileUrl, PHP_URL_HOST).':'.$port;
|
||||
if (!isset($defaultPorts[$scheme])) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Could not get default port for unknown scheme: %s',
|
||||
$scheme
|
||||
));
|
||||
}
|
||||
|
||||
$defaultPort = $defaultPorts[$scheme];
|
||||
$port = parse_url($url, PHP_URL_PORT) ?: $defaultPort;
|
||||
|
||||
return parse_url($url, PHP_URL_HOST).':'.$port;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Util;
|
||||
|
||||
use Symfony\Component\Process\PhpProcess;
|
||||
|
||||
/**
|
||||
* @author Chris Smith <chris@cs278.org>
|
||||
*/
|
||||
final class TlsHelper
|
||||
{
|
||||
private static $useOpensslParse;
|
||||
|
||||
/**
|
||||
* Match hostname against a certificate.
|
||||
*
|
||||
* @param mixed $certificate X.509 certificate
|
||||
* @param string $hostname Hostname in the URL
|
||||
* @param string $cn Set to the common name of the certificate iff match found
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function checkCertificateHost($certificate, $hostname, &$cn = null)
|
||||
{
|
||||
$names = self::getCertificateNames($certificate);
|
||||
|
||||
if (empty($names)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$combinedNames = array_merge($names['san'], array($names['cn']));
|
||||
$hostname = strtolower($hostname);
|
||||
|
||||
foreach ($combinedNames as $certName) {
|
||||
$matcher = self::certNameMatcher($certName);
|
||||
|
||||
if ($matcher && $matcher($hostname)) {
|
||||
$cn = $names['cn'];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract DNS names out of an X.509 certificate.
|
||||
*
|
||||
* @param mixed $certificate X.509 certificate
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public static function getCertificateNames($certificate)
|
||||
{
|
||||
if (is_array($certificate)) {
|
||||
$info = $certificate;
|
||||
} elseif (self::isOpensslParseSafe()) {
|
||||
$info = openssl_x509_parse($certificate, false);
|
||||
}
|
||||
|
||||
if (!isset($info['subject']['commonName'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commonName = strtolower($info['subject']['commonName']);
|
||||
$subjectAltNames = array();
|
||||
|
||||
if (isset($info['extensions']['subjectAltName'])) {
|
||||
$subjectAltNames = preg_split('{\s*,\s*}', $info['extensions']['subjectAltName']);
|
||||
$subjectAltNames = array_filter(array_map(function ($name) {
|
||||
if (0 === strpos($name, 'DNS:')) {
|
||||
return strtolower(ltrim(substr($name, 4)));
|
||||
}
|
||||
}, $subjectAltNames));
|
||||
$subjectAltNames = array_values($subjectAltNames);
|
||||
}
|
||||
|
||||
return array(
|
||||
'cn' => $commonName,
|
||||
'san' => $subjectAltNames,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the certificate pin.
|
||||
*
|
||||
* By Kevin McArthur of StormTide Digital Studios Inc.
|
||||
* @KevinSMcArthur / https://github.com/StormTide
|
||||
*
|
||||
* See http://tools.ietf.org/html/draft-ietf-websec-key-pinning-02
|
||||
*
|
||||
* This method was adapted from Sslurp.
|
||||
* https://github.com/EvanDotPro/Sslurp
|
||||
*
|
||||
* (c) Evan Coury <me@evancoury.com>
|
||||
*
|
||||
* For the full copyright and license information, please see below:
|
||||
*
|
||||
* Copyright (c) 2013, Evan Coury
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
public static function getCertificateFingerprint($certificate)
|
||||
{
|
||||
$pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate));
|
||||
$pubkeypem = $pubkeydetails['key'];
|
||||
//Convert PEM to DER before SHA1'ing
|
||||
$start = '-----BEGIN PUBLIC KEY-----';
|
||||
$end = '-----END PUBLIC KEY-----';
|
||||
$pemtrim = substr($pubkeypem, (strpos($pubkeypem, $start) + strlen($start)), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1));
|
||||
$der = base64_decode($pemtrim);
|
||||
|
||||
return sha1($der);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if it is safe to use the PHP function openssl_x509_parse().
|
||||
*
|
||||
* This checks if OpenSSL extensions is vulnerable to remote code execution
|
||||
* via the exploit documented as CVE-2013-6420.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isOpensslParseSafe()
|
||||
{
|
||||
if (null !== self::$useOpensslParse) {
|
||||
return self::$useOpensslParse;
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID >= 50600) {
|
||||
return self::$useOpensslParse = true;
|
||||
}
|
||||
|
||||
// Vulnerable:
|
||||
// PHP 5.3.0 - PHP 5.3.27
|
||||
// PHP 5.4.0 - PHP 5.4.22
|
||||
// PHP 5.5.0 - PHP 5.5.6
|
||||
if (
|
||||
(PHP_VERSION_ID < 50400 && PHP_VERSION_ID >= 50328)
|
||||
|| (PHP_VERSION_ID < 50500 && PHP_VERSION_ID >= 50423)
|
||||
|| (PHP_VERSION_ID < 50600 && PHP_VERSION_ID >= 50507)
|
||||
) {
|
||||
// This version of PHP has the fix for CVE-2013-6420 applied.
|
||||
return self::$useOpensslParse = true;
|
||||
}
|
||||
|
||||
if ('\\' === DIRECTORY_SEPARATOR) {
|
||||
// Windows is probably insecure in this case.
|
||||
return self::$useOpensslParse = false;
|
||||
}
|
||||
|
||||
$compareDistroVersionPrefix = function ($prefix, $fixedVersion) {
|
||||
$regex = '{^'.preg_quote($prefix).'([0-9]+)}$';
|
||||
|
||||
if (preg_match($regex, PHP_VERSION, $m)) {
|
||||
return ((int) $m[1]) >= $fixedVersion;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Hard coded list of PHP distributions with the fix backported.
|
||||
if (
|
||||
$compareDistroVersionPrefix('5.3.3-7+squeeze', 19) // Debian 6 (Squeeze)
|
||||
|| $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) // Debian 7 (Wheezy)
|
||||
|| $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9) // Ubuntu 12.04 (Precise)
|
||||
) {
|
||||
return self::$useOpensslParse = true;
|
||||
}
|
||||
|
||||
// This is where things get crazy, because distros backport security
|
||||
// fixes the chances are on NIX systems the fix has been applied but
|
||||
// it's not possible to verify that from the PHP version.
|
||||
//
|
||||
// To verify exec a new PHP process and run the issue testcase with
|
||||
// known safe input that replicates the bug.
|
||||
|
||||
// Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415
|
||||
$cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEVEUwTVRFeU9ERXhNemt6TlZvd2djTXhDekFKQmdOVkJBWVRBa1JGTVJ3d0dnWURWUVFJREJOTwpiM0prY21obGFXNHRWMlZ6ZEdaaGJHVnVNUkF3RGdZRFZRUUhEQWRMdzRQQ3RteHVNUlF3RWdZRFZRUUtEQXRUClpXdDBhVzl1UldsdWN6RWZNQjBHQTFVRUN3d1dUV0ZzYVdOcGIzVnpJRU5sY25RZ1UyVmpkR2x2YmpFaE1COEcKQTFVRUF3d1liV0ZzYVdOcGIzVnpMbk5sYTNScGIyNWxhVzV6TG1SbE1Tb3dLQVlKS29aSWh2Y05BUWtCRmh0egpkR1ZtWVc0dVpYTnpaWEpBYzJWcmRHbHZibVZwYm5NdVpHVXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRRERBZjNobDdKWTBYY0ZuaXlFSnBTU0RxbjBPcUJyNlFQNjV1c0pQUnQvOFBhRG9xQnUKd0VZVC9OYSs2ZnNnUGpDMHVLOURaZ1dnMnRIV1dvYW5TYmxBTW96NVBINlorUzRTSFJaN2UyZERJalBqZGhqaAowbUxnMlVNTzV5cDBWNzk3R2dzOWxOdDZKUmZIODFNTjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K';
|
||||
$script = <<<'EOT'
|
||||
|
||||
error_reporting(-1);
|
||||
$info = openssl_x509_parse(base64_decode('%s'));
|
||||
var_dump(PHP_VERSION, $info['issuer']['emailAddress'], $info['validFrom_time_t']);
|
||||
|
||||
EOT;
|
||||
$script = '<'."?php\n".sprintf($script, $cert);
|
||||
|
||||
try {
|
||||
$process = new PhpProcess($script);
|
||||
$process->mustRun();
|
||||
} catch (\Exception $e) {
|
||||
// In the case of any exceptions just accept it is not possible to
|
||||
// determine the safety of openssl_x509_parse and bail out.
|
||||
return self::$useOpensslParse = false;
|
||||
}
|
||||
|
||||
$output = preg_split('{\r?\n}', trim($process->getOutput()));
|
||||
$errorOutput = trim($process->getErrorOutput());
|
||||
|
||||
if (
|
||||
count($output) === 3
|
||||
&& $output[0] === sprintf('string(%d) "%s"', strlen(PHP_VERSION), PHP_VERSION)
|
||||
&& $output[1] === 'string(27) "stefan.esser@sektioneins.de"'
|
||||
&& $output[2] === 'int(-1)'
|
||||
&& preg_match('{openssl_x509_parse\(\): illegal length in timestamp in - on line \d+}', $errorOutput)
|
||||
) {
|
||||
// This PHP has the fix backported probably by a distro security team.
|
||||
return self::$useOpensslParse = true;
|
||||
}
|
||||
|
||||
return self::$useOpensslParse = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert certificate name into matching function.
|
||||
*
|
||||
* @param $certName CN/SAN
|
||||
*
|
||||
* @return callable|null
|
||||
*/
|
||||
private static function certNameMatcher($certName)
|
||||
{
|
||||
$wildcards = substr_count($certName, '*');
|
||||
|
||||
if (0 === $wildcards) {
|
||||
// Literal match.
|
||||
return function ($hostname) use ($certName) {
|
||||
return $hostname === $certName;
|
||||
};
|
||||
}
|
||||
|
||||
if (1 === $wildcards) {
|
||||
$components = explode('.', $certName);
|
||||
|
||||
if (3 > count($components)) {
|
||||
// Must have 3+ components
|
||||
return;
|
||||
}
|
||||
|
||||
$firstComponent = $components[0];
|
||||
|
||||
// Wildcard must be the last character.
|
||||
if ('*' !== $firstComponent[strlen($firstComponent) - 1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wildcardRegex = preg_quote($certName);
|
||||
$wildcardRegex = str_replace('\\*', '[a-z0-9-]+', $wildcardRegex);
|
||||
$wildcardRegex = "{^{$wildcardRegex}$}";
|
||||
|
||||
return function ($hostname) use ($wildcardRegex) {
|
||||
// var_dump($wildcardRegex);
|
||||
return 1 === preg_match($wildcardRegex, $hostname);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Test\Util;
|
||||
|
||||
use Composer\Util\TlsHelper;
|
||||
|
||||
class TlsHelperTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/** @dataProvider dataCheckCertificateHost */
|
||||
public function testCheckCertificateHost($expectedResult, $hostname, $certNames)
|
||||
{
|
||||
$certificate['subject']['commonName'] = $expectedCn = array_shift($certNames);
|
||||
$certificate['extensions']['subjectAltName'] = $certNames ? 'DNS:'.implode(',DNS:', $certNames) : '';
|
||||
|
||||
$result = TlsHelper::checkCertificateHost($certificate, $hostname, $foundCn);
|
||||
|
||||
if (true === $expectedResult) {
|
||||
$this->assertTrue($result);
|
||||
$this->assertSame($expectedCn, $foundCn);
|
||||
} else {
|
||||
$this->assertFalse($result);
|
||||
$this->assertNull($foundCn);
|
||||
}
|
||||
}
|
||||
|
||||
public function dataCheckCertificateHost()
|
||||
{
|
||||
return array(
|
||||
array(true, 'getcomposer.org', array('getcomposer.org')),
|
||||
array(true, 'getcomposer.org', array('getcomposer.org', 'packagist.org')),
|
||||
array(true, 'getcomposer.org', array('packagist.org', 'getcomposer.org')),
|
||||
array(true, 'foo.getcomposer.org', array('*.getcomposer.org')),
|
||||
array(false, 'xyz.foo.getcomposer.org', array('*.getcomposer.org')),
|
||||
array(true, 'foo.getcomposer.org', array('getcomposer.org', '*.getcomposer.org')),
|
||||
array(true, 'foo.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
|
||||
array(true, 'foo1.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
|
||||
array(true, 'foo2.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
|
||||
array(false, 'foo2.another.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
|
||||
array(false, 'test.example.net', array('**.example.net', '**.example.net')),
|
||||
array(false, 'test.example.net', array('t*t.example.net', 't*t.example.net')),
|
||||
array(false, 'xyz.example.org', array('*z.example.org', '*z.example.org')),
|
||||
array(false, 'foo.bar.example.com', array('foo.*.example.com', 'foo.*.example.com')),
|
||||
array(false, 'example.com', array('example.*', 'example.*')),
|
||||
array(true, 'localhost', array('localhost')),
|
||||
array(false, 'localhost', array('*')),
|
||||
array(false, 'localhost', array('local*')),
|
||||
array(false, 'example.net', array('*.net', '*.org', 'ex*.net')),
|
||||
array(true, 'example.net', array('*.net', '*.org', 'example.net')),
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCertificateNames()
|
||||
{
|
||||
$certificate['subject']['commonName'] = 'example.net';
|
||||
$certificate['extensions']['subjectAltName'] = 'DNS: example.com, IP: 127.0.0.1, DNS: getcomposer.org, Junk: blah, DNS: composer.example.org';
|
||||
|
||||
$names = TlsHelper::getCertificateNames($certificate);
|
||||
|
||||
$this->assertSame('example.net', $names['cn']);
|
||||
$this->assertSame(array(
|
||||
'example.com',
|
||||
'getcomposer.org',
|
||||
'composer.example.org',
|
||||
), $names['san']);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue