diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index d373072170ac..377513068dcc 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -42,12 +42,12 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $rfs = new RemoteFilesystem($this->getIO()); - $latest = trim($rfs->getContents('getcomposer.org', 'http://getcomposer.org/version', false)); + $latest = trim($rfs->getContents('getcomposer.org', 'https://getcomposer.org/version', false)); if (Composer::VERSION !== $latest) { $output->writeln(sprintf("Updating to version %s.", $latest)); - $remoteFilename = 'http://getcomposer.org/composer.phar'; + $remoteFilename = 'https://getcomposer.org/composer.phar'; $localFilename = $_SERVER['argv'][0]; $tempFilename = basename($localFilename, '.phar').'-temp.phar'; diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 39e95fd0378f..324e36657216 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -22,13 +22,13 @@ class Config 'vendor-dir' => 'vendor', 'bin-dir' => '{$vendor-dir}/bin', 'notify-on-install' => true, - 'github-protocols' => array('git', 'https', 'http'), + 'github-protocols' => array('https', 'git'), ); public static $defaultRepositories = array( 'packagist' => array( 'type' => 'composer', - 'url' => 'http://packagist.org', + 'url' => 'https://packagist.org', ) ); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 51c98dec05c4..31229ee88b28 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -39,7 +39,7 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config) { if (!preg_match('{^\w+://}', $repoConfig['url'])) { // assume http as the default protocol - $repoConfig['url'] = 'http://'.$repoConfig['url']; + $repoConfig['url'] = 'https://'.$repoConfig['url']; } $repoConfig['url'] = rtrim($repoConfig['url'], '/'); if (function_exists('filter_var') && version_compare(PHP_VERSION, '5.3.3', '>=') && !filter_var($repoConfig['url'], FILTER_VALIDATE_URL)) { diff --git a/src/Composer/Util/BadCryptoException.php b/src/Composer/Util/BadCryptoException.php new file mode 100644 index 000000000000..72cf683b9cbb --- /dev/null +++ b/src/Composer/Util/BadCryptoException.php @@ -0,0 +1,20 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * @author Evan Coury + */ +class BadCryptoException extends \Exception +{ +} diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index e82313033d17..d158e948a6ab 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -90,9 +90,15 @@ protected function get($originUrl, $fileUrl, $fileName = null, $progress = true) $this->result = null; $this->originUrl = $originUrl; $this->fileUrl = $fileUrl; + $this->fileUrlParts = parse_url($fileUrl); $this->fileName = $fileName; $this->progress = $progress; $this->lastProgress = null; + $this->ssl = ('https' === substr($fileUrl, 0, 5)); + + if (!extension_loaded('openssl') && $this->ssl) { + throw new \RuntimeException('You must enable the openssl extension in your php.ini to load information from '.$fileUrl); + } $options = $this->getOptionsForUrl($originUrl); $ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet'))); @@ -104,11 +110,22 @@ protected function get($originUrl, $fileUrl, $fileName = null, $progress = true) $errorMessage = null; set_error_handler(function ($code, $msg) use (&$errorMessage) { $errorMessage = preg_replace('{^file_get_contents\(.+?\): }', '', $msg); + if ($this->ssl && strpos($errorMessage, 'SSL') !== false || strpos($errorMessage, 'enable crypto') !== false) { + throw new BadCryptoException($errorMessage, $code); + } if (!ini_get('allow_url_fopen')) { $errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')'; } }); - $result = file_get_contents($fileUrl, false, $ctx); + try { + $result = file_get_contents($fileUrl, false, $ctx); + } catch (BadCryptoException $e) { + // SSL failed -- let's prompt the user about the certificate + $port = isset($this->fileUrlParts['port']) ? $this->fileUrlParts['port'] : 443; + $sslHelper = new SslHelper($this->io); + $sslHelper->verifySslCertificateFromServer($this->fileUrlParts['host'], $port, $fileUrl); + $result = file_get_contents($fileUrl, false, $ctx); + } restore_error_handler(); // fix for 5.4.0 https://bugs.php.net/bug.php?id=61336 @@ -241,6 +258,18 @@ protected function getOptionsForUrl($originUrl) $options['http']['header'] .= "Authorization: Basic $authStr\r\n"; } + if (!$this->ssl) return $options; + + //if (getenv('COMPOSER_INSECURE_SSL_NOVERIFY') !== false) return $options; + + $options['ssl'] = array( + 'verify_peer' => true, + 'allow_self_signed' => false, + 'cafile' => SslHelper::initCaBundleFile(), + // PHP improperly handles CN_match, so it's handled manually in Composer\Util\SslHelper + //'CN_match' => true, + ); + return $options; } } diff --git a/src/Composer/Util/SslHelper.php b/src/Composer/Util/SslHelper.php new file mode 100644 index 000000000000..411bfd73a0a0 --- /dev/null +++ b/src/Composer/Util/SslHelper.php @@ -0,0 +1,147 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Factory; +use Composer\IO\IOInterface; + +/** + * @author Evan Coury + */ +class SslHelper +{ + private $io; + + public function __construct(IOInterface $io) + { + $this->io = $io; + $this->config = Factory::createConfig(); + } + + public function verifySslCertificateFromServer($hostname, $port = 443, $url = false) + { + $chain = $this->fetchCertificateChain($hostname, $port); + $cert = $chain[0]; + $certInfo = openssl_x509_parse($cert); + $commonName = $certInfo['subject']['CN']; + $caCert = $chain[count($chain)-1]; + $caCertInfo = openssl_x509_parse($caCert); + + // Check if the certificate has expired + $expired = false; + if ($certInfo['validTo_time_t'] < time()) { + $expired = true; + if (!$this->io->askConfirmation("WARNING! The SSL certificate for {$hostname} has expired.\n\nAre you sure you wish to continue with the expired SSL certificate? [y/N] ", false)) { + throw new BadCryptoException('Encountered expired SSL certificate; exiting on user command.'); + } + } + $expires = date('Y-m-d', $certInfo['validTo_time_t']) . (($expired) ? ' (WARNING: Certificate has expired!)' : ''); + + // Proper CN_Match handling -- PHP's is broken + $alternativeNames = explode(', ', str_replace('DNS:', '', $certInfo['extensions']['subjectAltName'])); + $validDomain = true; + if ($hostname != $commonName && !in_array($hostname, $alternativeNames)) { + $validDomain = false; + if (!$this->io->askConfirmation("WARNING! The SSL certificate at {$hostname} is NOT VALID for the requested hostname. " . + "This could be an indication of a man-in-the-middle attack " . + "or a misconfigured server. It is strongly recommended that you DO NOT continue.\n\n" . + "Are you sure you want to continue with the invalid SSL certificate? [y/N] ", false)) { + throw new BadCryptoException('Encountered invalid SSL certificate; exiting on user command.'); + } + } + + // Get a readable Certificate Authority name + if (isset($caCertInfo['subject']['O'], $caCertInfo['subject']['CN'])) { + $caName = "{$caCertInfo['subject']['O']} » {$caCertInfo['subject']['CN']}"; + } elseif (isset($caCertInfo['issuer']['O'], $caCertInfo['issuer']['CN'])) { + $caName = "{$caCertInfo['issuer']['O']} » {$caCertInfo['issuer']['CN']}"; + } else { + $caName = $caCertInfo['name']; + } + + // Get the certificate fingerprint + openssl_x509_export($cert, $certString); + $fingerprint = $this->getSha1Fingerprint($certString); + + $this->io->write("\n #############################################################"); + $this->io->write(' # WARNING: Composer is trying to connect to a secure server #'); + $this->io->write(' # with an untrusted public key. Please review carefully. #'); + $this->io->write(" #############################################################\n"); + + if ($url) $this->io->write(" Requested URL: {$url}"); + $this->io->write(" Hostname: {$hostname}"); + $this->io->write(' Common Name: ' . $commonName . + ((count($alternativeNames) > 0) ? ' (' . implode(', ', $alternativeNames) . ')' : '') . ''); // show alt names + $this->io->write(" Valid Until: {$expires}"); + $this->io->write(" Fingerprint: {$fingerprint}"); + $this->io->write(" Authority: {$caName}"); + + $this->io->write("\nTo verify the certificate fingerprint, go to https://{$hostname}/ in your web browser and view the certificate details."); + + if ($this->io->askConfirmation("\nAre you sure you trust the Certificate Authority listed above and have verified the certificate fingerprint? [y/N] ", false)) { + openssl_x509_export($caCert, $caCertString); + file_put_contents(static::initCaBundleFile(), $caCertString, FILE_APPEND); + $this->io->write(" Added {$caName} as a trustworthy Certificate Authority."); + return true; + } + + throw new BadCryptoException('Encountered untrusted SSL certificate; exiting on user command.'); + } + + protected function getSha1Fingerprint($certificate) + { + $certificate = str_replace('-----BEGIN CERTIFICATE-----', '', $certificate); + $certificate = str_replace('-----END CERTIFICATE-----', '', $certificate); + $certificate = base64_decode($certificate); + $fingerprint = strtoupper(sha1($certificate)); + $fingerprint = str_split($fingerprint, 2); + return implode(':', $fingerprint); + } + + protected function fetchCertificateChain($hostname, $port) + { + $context = stream_context_create(array('ssl' => array('capture_peer_cert_chain' => true))); + $socket = stream_socket_client("ssl://{$hostname}:{$port}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context); + $params = stream_context_get_params($socket); + return $params['options']['ssl']['peer_certificate_chain']; + } + + public static function initCaBundleFile() + { + $config = Factory::createConfig(); + $caBundleFile = $config->get('home') . '/trusted-ca-bundle.crt'; + + if (file_exists($caBundleFile)) { + return $caBundleFile; + } + touch($caBundleFile); + + // Improve usability for some linux users by automatically + // importing the distro's CA bundle if it's found. + $caBundleLocations = array( + '/etc/pki/tls/certs/ca-bundle.crt', + '/usr/share/ssl/certs/ca-bubdle.crt', + ); + + foreach ($caBundleLocations as $caBundle) { + if (file_exists($caBundle) && is_readable($caBundle)) { + $trustedBundle = file_get_contents($caBundle); + break; + } + } + + if (isset($trustedBundle)) file_put_contents($caBundleFile, $trustedBundle); + + return $caBundleFile; + } +}