1
0
Fork 0

Merge pull request #4805 from alcohol/capath

Add capath configuration capability and refactor cafile resolving
pull/4124/head
Jordi Boggiano 2016-01-22 13:46:05 +00:00
commit 7d7b3ccb2a
7 changed files with 124 additions and 76 deletions

View File

@ -55,9 +55,15 @@ php_openssl extension in php.ini.
## cafile ## cafile
A way to set the path to the openssl CA file. In PHP 5.6+ you should rather Location of Certificate Authority file on local filesystem. In PHP 5.6+ you
set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should
detect your system CA file automatically. be able to detect your system CA file automatically.
## capath
If cafile is not specified or if the certificate is not found there, the
directory pointed to by capath is searched for a suitable certificate.
capath must be a correctly hashed certificate directory.
## http-basic ## http-basic

View File

@ -149,6 +149,10 @@
"type": "string", "type": "string",
"description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically." "description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically."
}, },
"capath": {
"type": "string",
"description": "If cafile is not specified or if the certificate is not found there, the directory pointed to by capath is searched for a suitable certificate. capath must be a correctly hashed certificate directory."
},
"http-basic": { "http-basic": {
"type": "object", "type": "object",
"description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.", "description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",

View File

@ -333,6 +333,10 @@ EOT
function ($val) { return file_exists($val) && is_readable($val); }, function ($val) { return file_exists($val) && is_readable($val); },
function ($val) { return $val === 'null' ? null : $val; } function ($val) { return $val === 'null' ? null : $val; }
), ),
'capath' => array(
function ($val) { return is_dir($val) && is_readable($val); },
function ($val) { return $val === 'null' ? null : $val; }
),
'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
); );
$multiConfigValues = array( $multiConfigValues = array(

View File

@ -47,6 +47,7 @@ class Config
'github-domains' => array('github.com'), 'github-domains' => array('github.com'),
'disable-tls' => false, 'disable-tls' => false,
'cafile' => null, 'cafile' => null,
'capath' => null,
'github-expose-hostname' => true, 'github-expose-hostname' => true,
'gitlab-domains' => array('gitlab.com'), 'gitlab-domains' => array('gitlab.com'),
'store-auths' => 'prompt', 'store-auths' => 'prompt',
@ -179,6 +180,7 @@ class Config
case 'cache-repo-dir': case 'cache-repo-dir':
case 'cache-vcs-dir': case 'cache-vcs-dir':
case 'cafile': case 'cafile':
case 'capath':
// convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config
$env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_'));

View File

@ -590,9 +590,12 @@ class Factory
$remoteFilesystemOptions = array(); $remoteFilesystemOptions = array();
if ($disableTls === false) { if ($disableTls === false) {
if ($config && $config->get('cafile')) { if ($config && $config->get('cafile')) {
$remoteFilesystemOptions = array('ssl' => array('cafile' => $config->get('cafile'))); $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile');
} }
$remoteFilesystemOptions = array_merge_recursive($remoteFilesystemOptions, $options); if ($config && $config->get('capath')) {
$remoteFilesystemOptions['ssl']['capath'] = $config->get('capath');
}
$remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options);
} }
try { try {
$remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);

View File

@ -54,15 +54,7 @@ class RemoteFilesystem
// Setup TLS options // Setup TLS options
// The cafile option can be set via config.json // The cafile option can be set via config.json
if ($disableTls === false) { if ($disableTls === false) {
$this->options = $this->getTlsDefaults(); $this->options = $this->getTlsDefaults($options);
if (isset($options['ssl']['cafile'])
&& (
!is_readable($options['ssl']['cafile'])
|| !$this->validateCaFile($options['ssl']['cafile'])
)
) {
throw new TransportException('The configured cafile was not valid or could not be read.');
}
} else { } else {
$this->disableTls = true; $this->disableTls = true;
} }
@ -575,7 +567,12 @@ class RemoteFilesystem
return $options; return $options;
} }
private function getTlsDefaults() /**
* @param array $options
*
* @return array
*/
private function getTlsDefaults(array $options)
{ {
$ciphers = implode(':', array( $ciphers = implode(':', array(
'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256',
@ -622,7 +619,7 @@ class RemoteFilesystem
* *
* cafile or capath can be overridden by passing in those options to constructor. * cafile or capath can be overridden by passing in those options to constructor.
*/ */
$options = array( $defaults = array(
'ssl' => array( 'ssl' => array(
'ciphers' => $ciphers, 'ciphers' => $ciphers,
'verify_peer' => true, 'verify_peer' => true,
@ -631,80 +628,86 @@ class RemoteFilesystem
) )
); );
if (isset($options['ssl'])) {
$defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']);
}
/** /**
* Attempt to find a local cafile or throw an exception if none pre-set * Attempt to find a local cafile or throw an exception if none pre-set
* The user may go download one if this occurs. * The user may go download one if this occurs.
*/ */
if (!isset($this->options['ssl']['cafile'])) { if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) {
$result = $this->getSystemCaRootBundlePath(); $result = $this->getSystemCaRootBundlePath();
if ($result) {
if (preg_match('{^phar://}', $result)) {
$targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem';
// use stream_copy_to_stream instead of copy if (preg_match('{^phar://}', $result)) {
// to work around https://bugs.php.net/bug.php?id=64634 $hash = hash_file('sha256', $result);
$source = fopen($result, 'r'); $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem';
$target = fopen($targetPath, 'w+');
stream_copy_to_stream($source, $target);
fclose($source);
fclose($target);
unset($source, $target);
$options['ssl']['cafile'] = $targetPath; if (!file_exists($targetPath) || $hash !== hash_file('sha256', $targetPath)) {
} else { $this->streamCopy($result, $targetPath);
if (is_dir($result)) { chmod($targetPath, 0666);
$options['ssl']['capath'] = $result;
} elseif ($result) {
$options['ssl']['cafile'] = $result;
}
} }
$defaults['ssl']['cafile'] = $targetPath;
} elseif (is_dir($result)) {
$defaults['ssl']['capath'] = $result;
} else { } else {
throw new TransportException('A valid cafile could not be located automatically.'); $defaults['ssl']['cafile'] = $result;
} }
} }
if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !$this->validateCaFile($defaults['ssl']['cafile']))) {
throw new TransportException('The configured cafile was not valid or could not be read.');
}
if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) {
throw new TransportException('The configured capath was not valid or could not be read.');
}
/** /**
* Disable TLS compression to prevent CRIME attacks where supported. * Disable TLS compression to prevent CRIME attacks where supported.
*/ */
if (PHP_VERSION_ID >= 50413) { if (PHP_VERSION_ID >= 50413) {
$options['ssl']['disable_compression'] = true; $defaults['ssl']['disable_compression'] = true;
} }
return $options; return $defaults;
} }
/** /**
* This method was adapted from Sslurp. * This method was adapted from Sslurp.
* https://github.com/EvanDotPro/Sslurp * https://github.com/EvanDotPro/Sslurp
* *
* (c) Evan Coury <me@evancoury.com> * (c) Evan Coury <me@evancoury.com>
* *
* For the full copyright and license information, please see below: * For the full copyright and license information, please see below:
* *
* Copyright (c) 2013, Evan Coury * Copyright (c) 2013, Evan Coury
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without modification, * Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met: * are permitted provided that the following conditions are met:
* *
* * Redistributions of source code must retain the above copyright notice, * * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. * this list of conditions and the following disclaimer.
* *
* * Redistributions in binary form must reproduce the above copyright notice, * * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation * this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution. * and/or other materials provided with the distribution.
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * 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 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 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 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ *
* @return string
*/
private function getSystemCaRootBundlePath() private function getSystemCaRootBundlePath()
{ {
static $caPath = null; static $caPath = null;
@ -721,6 +724,11 @@ class RemoteFilesystem
return $caPath = $envCertFile; return $caPath = $envCertFile;
} }
$configured = ini_get('openssl.cafile');
if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) {
return $caPath = $configured;
}
$caBundlePaths = array( $caBundlePaths = array(
'/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package)
'/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package) '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package)
@ -732,14 +740,8 @@ class RemoteFilesystem
'/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat? '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
'/etc/ssl/cert.pem', // OpenBSD '/etc/ssl/cert.pem', // OpenBSD
'/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x
__DIR__.'/../../../res/cacert.pem', // Bundled with Composer
); );
$configured = ini_get('openssl.cafile');
if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) {
return $caPath = $configured;
}
foreach ($caBundlePaths as $caBundle) { foreach ($caBundlePaths as $caBundle) {
if (@is_readable($caBundle) && $this->validateCaFile($caBundle)) { if (@is_readable($caBundle) && $this->validateCaFile($caBundle)) {
return $caPath = $caBundle; return $caPath = $caBundle;
@ -753,9 +755,14 @@ class RemoteFilesystem
} }
} }
return $caPath = false; return $caPath = __DIR__.'/../../../res/cacert.pem'; // Bundled with Composer, last resort
} }
/**
* @param string $filename
*
* @return bool
*/
private function validateCaFile($filename) private function validateCaFile($filename)
{ {
if ($this->io->isDebug()) { if ($this->io->isDebug()) {
@ -775,4 +782,22 @@ class RemoteFilesystem
return (bool) openssl_x509_parse($contents); return (bool) openssl_x509_parse($contents);
} }
/**
* Uses stream_copy_to_stream instead of copy to work around https://bugs.php.net/bug.php?id=64634
*
* @param string $source
* @param string $target
*/
private function streamCopy($source, $target)
{
$source = fopen($source, 'r');
$target = fopen($target, 'w+');
stream_copy_to_stream($source, $target);
fclose($source);
fclose($target);
unset($source, $target);
}
} }

View File

@ -64,6 +64,10 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
->with('cafile') ->with('cafile')
->will($this->returnValue(null)); ->will($this->returnValue(null));
$config->expects($this->at(2)) $config->expects($this->at(2))
->method('get')
->with('capath')
->will($this->returnValue(null));
$config->expects($this->at(3))
->method('get') ->method('get')
->with('vendor-dir') ->with('vendor-dir')
->will($this->returnValue($this->testDir)); ->will($this->returnValue($this->testDir));