Merge pull request #4805 from alcohol/capath
Add capath configuration capability and refactor cafile resolvingpull/4124/head
commit
7d7b3ccb2a
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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\": \"...\"}.",
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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, '-', '_'));
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
Loading…
Reference in New Issue