Merge remote-tracking branch 'cs278/san-support'
commit
1ea810d40b
|
@ -33,6 +33,7 @@ class RemoteFilesystem
|
||||||
private $progress;
|
private $progress;
|
||||||
private $lastProgress;
|
private $lastProgress;
|
||||||
private $options = array();
|
private $options = array();
|
||||||
|
private $peerCertificateMap = array();
|
||||||
private $disableTls = false;
|
private $disableTls = false;
|
||||||
private $retryAuthFailure;
|
private $retryAuthFailure;
|
||||||
private $lastHeaders;
|
private $lastHeaders;
|
||||||
|
@ -264,6 +265,18 @@ class RemoteFilesystem
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
$result = file_get_contents($fileUrl, false, $ctx);
|
$result = file_get_contents($fileUrl, false, $ctx);
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 50600 && !empty($options['ssl']['peer_fingerprint'])) {
|
||||||
|
// Emulate fingerprint validation on PHP < 5.6
|
||||||
|
$params = stream_context_get_params($ctx);
|
||||||
|
$expectedPeerFingerprint = $options['ssl']['peer_fingerprint'];
|
||||||
|
$peerFingerprint = TlsHelper::getCertificateFingerprint($params['options']['ssl']['peer_certificate']);
|
||||||
|
|
||||||
|
// Constant time compare??!
|
||||||
|
if ($expectedPeerFingerprint !== $peerFingerprint) {
|
||||||
|
throw new TransportException('Peer fingerprint did not match');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
if ($e instanceof TransportException && !empty($http_response_header[0])) {
|
if ($e instanceof TransportException && !empty($http_response_header[0])) {
|
||||||
$e->setHeaders($http_response_header);
|
$e->setHeaders($http_response_header);
|
||||||
|
@ -370,6 +383,39 @@ class RemoteFilesystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (false === $result && false !== strpos($errorMessage, 'Peer certificate') && PHP_VERSION_ID < 50600) {
|
||||||
|
// Certificate name error, PHP doesn't support subjectAltName on PHP < 5.6
|
||||||
|
// The procedure to handle sAN for older PHP's is:
|
||||||
|
//
|
||||||
|
// 1. Open socket to remote server and fetch certificate (disabling peer
|
||||||
|
// validation because PHP errors without giving up the certificate.)
|
||||||
|
//
|
||||||
|
// 2. Verifying the domain in the URL against the names in the sAN field.
|
||||||
|
// If there is a match record the authority [host/port], certificate
|
||||||
|
// common name, and certificate fingerprint.
|
||||||
|
//
|
||||||
|
// 3. Retry the original request but changing the CN_match parameter to
|
||||||
|
// the common name extracted from the certificate in step 2.
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
$this->peerCertificateMap[$this->getUrlAuthority($this->fileUrl)] = $certDetails;
|
||||||
|
|
||||||
|
$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) {
|
if ($this->retry) {
|
||||||
$this->retry = false;
|
$this->retry = false;
|
||||||
|
|
||||||
|
@ -531,12 +577,8 @@ class RemoteFilesystem
|
||||||
$tlsOptions = array();
|
$tlsOptions = array();
|
||||||
|
|
||||||
// Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN
|
// Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN
|
||||||
if ($this->disableTls === false && PHP_VERSION_ID < 50600) {
|
if ($this->disableTls === false && PHP_VERSION_ID < 50600 && !stream_is_local($this->fileUrl)) {
|
||||||
if (!preg_match('{^https?://}', $this->fileUrl)) {
|
|
||||||
$host = $originUrl;
|
|
||||||
} else {
|
|
||||||
$host = parse_url($this->fileUrl, PHP_URL_HOST);
|
$host = parse_url($this->fileUrl, PHP_URL_HOST);
|
||||||
}
|
|
||||||
|
|
||||||
if (PHP_VERSION_ID >= 50304) {
|
if (PHP_VERSION_ID >= 50304) {
|
||||||
// Must manually follow when setting CN_match because this causes all
|
// Must manually follow when setting CN_match because this causes all
|
||||||
|
@ -555,6 +597,24 @@ class RemoteFilesystem
|
||||||
|
|
||||||
$tlsOptions['ssl']['CN_match'] = $host;
|
$tlsOptions['ssl']['CN_match'] = $host;
|
||||||
$tlsOptions['ssl']['SNI_server_name'] = $host;
|
$tlsOptions['ssl']['SNI_server_name'] = $host;
|
||||||
|
|
||||||
|
$urlAuthority = $this->getUrlAuthority($this->fileUrl);
|
||||||
|
|
||||||
|
if (isset($this->peerCertificateMap[$urlAuthority])) {
|
||||||
|
// Handle subjectAltName on lesser PHP's.
|
||||||
|
$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();
|
$headers = array();
|
||||||
|
@ -702,6 +762,7 @@ class RemoteFilesystem
|
||||||
'verify_peer' => true,
|
'verify_peer' => true,
|
||||||
'verify_depth' => 7,
|
'verify_depth' => 7,
|
||||||
'SNI_enabled' => true,
|
'SNI_enabled' => true,
|
||||||
|
'capture_peer_cert' => true,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -883,4 +944,72 @@ class RemoteFilesystem
|
||||||
|
|
||||||
unset($source, $target);
|
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,
|
||||||
|
'verify_peer' => false, // Yes this is fucking insane! But PHP is lame.
|
||||||
|
))
|
||||||
|
));
|
||||||
|
|
||||||
|
// 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'];
|
||||||
|
|
||||||
|
if (TlsHelper::checkCertificateHost($peerCertificate, parse_url($url, PHP_URL_HOST), $commonName)) {
|
||||||
|
return array(
|
||||||
|
'cn' => $commonName,
|
||||||
|
'fp' => TlsHelper::getCertificateFingerprint($peerCertificate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUrlAuthority($url)
|
||||||
|
{
|
||||||
|
$defaultPorts = array(
|
||||||
|
'ftp' => 21,
|
||||||
|
'http' => 80,
|
||||||
|
'https' => 443,
|
||||||
|
);
|
||||||
|
|
||||||
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||||
|
|
||||||
|
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', 18) // 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
|
||||||
|
// changes in https://github.com/php/php-src/commit/76a7fd893b7d6101300cc656058704a73254d593
|
||||||
|
$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 (?:ASN1 data type for|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) {
|
||||||
|
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