Browse Source

Merge pull request #4805 from alcohol/capath

Add capath configuration capability and refactor cafile resolving
Jordi Boggiano 9 years ago
parent
commit
7d7b3ccb2a

+ 9 - 3
doc/06-config.md

@@ -55,9 +55,15 @@ php_openssl extension in php.ini.
 
 ## cafile
 
-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.
+Location of Certificate Authority file on local filesystem. 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
+
+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
 

+ 4 - 0
res/composer-schema.json

@@ -149,6 +149,10 @@
                     "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."
                 },
+                "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": {
                     "type": "object",
                     "description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",

+ 4 - 0
src/Composer/Command/ConfigCommand.php

@@ -333,6 +333,10 @@ EOT
                 function ($val) { return file_exists($val) && is_readable($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),
         );
         $multiConfigValues = array(

+ 2 - 0
src/Composer/Config.php

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

+ 5 - 2
src/Composer/Factory.php

@@ -590,9 +590,12 @@ class Factory
         $remoteFilesystemOptions = array();
         if ($disableTls === false) {
             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 {
             $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);

+ 98 - 73
src/Composer/Util/RemoteFilesystem.php

@@ -54,15 +54,7 @@ class RemoteFilesystem
         // Setup TLS options
         // The cafile option can be set via config.json
         if ($disableTls === false) {
-            $this->options = $this->getTlsDefaults();
-            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.');
-            }
+            $this->options = $this->getTlsDefaults($options);
         } else {
             $this->disableTls = true;
         }
@@ -575,7 +567,12 @@ class RemoteFilesystem
         return $options;
     }
 
-    private function getTlsDefaults()
+    /**
+     * @param array $options
+     *
+     * @return array
+     */
+    private function getTlsDefaults(array $options)
     {
         $ciphers = implode(':', array(
             'ECDHE-RSA-AES128-GCM-SHA256',
@@ -622,7 +619,7 @@ class RemoteFilesystem
          *
          * cafile or capath can be overridden by passing in those options to constructor.
          */
-        $options = array(
+        $defaults = array(
             'ssl' => array(
                 'ciphers' => $ciphers,
                 '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
          * 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();
-            if ($result) {
-                if (preg_match('{^phar://}', $result)) {
-                    $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem';
-
-                    // use stream_copy_to_stream instead of copy
-                    // to work around https://bugs.php.net/bug.php?id=64634
-                    $source = fopen($result, 'r');
-                    $target = fopen($targetPath, 'w+');
-                    stream_copy_to_stream($source, $target);
-                    fclose($source);
-                    fclose($target);
-                    unset($source, $target);
-
-                    $options['ssl']['cafile'] = $targetPath;
-                } else {
-                    if (is_dir($result)) {
-                        $options['ssl']['capath'] = $result;
-                    } elseif ($result) {
-                        $options['ssl']['cafile'] = $result;
-                    }
+
+            if (preg_match('{^phar://}', $result)) {
+                $hash = hash_file('sha256', $result);
+                $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem';
+
+                if (!file_exists($targetPath) || $hash !== hash_file('sha256', $targetPath)) {
+                    $this->streamCopy($result, $targetPath);
+                    chmod($targetPath, 0666);
                 }
+
+                $defaults['ssl']['cafile'] = $targetPath;
+            } elseif (is_dir($result)) {
+                $defaults['ssl']['capath'] = $result;
             } 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.
          */
         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.
-    * 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.
-    */
+     * 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.
+     *
+     * @return string
+     */
     private function getSystemCaRootBundlePath()
     {
         static $caPath = null;
@@ -721,6 +724,11 @@ class RemoteFilesystem
             return $caPath = $envCertFile;
         }
 
+        $configured = ini_get('openssl.cafile');
+        if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) {
+            return $caPath = $configured;
+        }
+
         $caBundlePaths = array(
             '/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)
@@ -732,14 +740,8 @@ class RemoteFilesystem
             '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
             '/etc/ssl/cert.pem', // OpenBSD
             '/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) {
             if (@is_readable($caBundle) && $this->validateCaFile($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)
     {
         if ($this->io->isDebug()) {
@@ -775,4 +782,22 @@ class RemoteFilesystem
 
         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);
+    }
 }

+ 4 - 0
tests/Composer/Test/Downloader/ZipDownloaderTest.php

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