composer: Composer is not trusting locally installed CAs on macOS

On macOS, to add custom CAs to openssl (installed via homebrew), one should copy the .pem file to /usr/local/etc/openssl/certs and run /usr/local/opt/openssl/bin/c_rehash, which creates a symlink to the certificate under /usr/local/etc/openssl/certs (source).

At $WORK, we have an internal CA which is used to sign the SSL certificate used in our local composer repository (we use Artifactory). After installing our company’s CA to the Mac’s Keychain and also adding it to /usr/local/etc/openssl/certs and creating the appropriate symlink with c_rehash, every tool (openssl, curl, etc.) and language (PHP, Ruby, Python) recognize this CA, except for composer.

PHP itself does recognize said certificates:

$ php --version
PHP 5.6.30 (cli) (built: Mar 11 2017 09:56:18)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
$ php -r "file_get_contents('https://composer.intranet');"
$

However, composer does not recognize the certificates installed this way because it only reads the default bundle at /usr/local/etc/openssl/cert.pem (see the output from composer install --prefer-dist -vvv below).


My composer.json:

{
    "repositories": [
        {"type": "composer", "url": "https://composer.intranet"},
        {"packagist": false}
    ]
}

Output of composer diagnose:

Checking composer.json: WARNING
No license specified, it is recommended to do so. For closed-source software you may use "proprietary" as license.
Checking platform settings: OK
Checking git settings: OK
Checking http connectivity to packagist: OK
Checking https connectivity to packagist: OK
Checking github.com rate limit: OK
Checking disk free space: OK
Checking pubkeys:
Tags Public Key Fingerprint: 57815BA2 7E54DC31 7ECC7CC5 573090D0  87719BA6 8F3BB723 4E5D42D0 84A14642
Dev Public Key Fingerprint: 4AC45767 E5EC2265 2F0C1167 CBBB8A2B  0C708369 153E328C AD90147D AFE50952
OK
Checking composer version: WARNING
You are not running the latest stable version, run `composer self-update` to update (1.4.1 => 1.4.2)

When I run this command:

composer install --prefer-dist -vvv

I get the following output:

Reading ./composer.json
Loading config file ./composer.json
Checked CA file /usr/local/etc/openssl/cert.pem: valid
Executing command (/private/tmp/composer-test): git branch --no-color --no-abbrev -v
Executing command (/private/tmp/composer-test): git describe --exact-match --tags
Executing command (/private/tmp/composer-test): git log --pretty="%H" -n1 HEAD
Executing command (/private/tmp/composer-test): hg branch
Executing command (/private/tmp/composer-test): fossil branch list
Executing command (/private/tmp/composer-test): fossil tag list
Executing command (/private/tmp/composer-test): svn info --xml
Failed to initialize global composer: Composer could not find the config file: /Users/dserodio/.composer/composer.json
To initialize a project, please create a composer.json file as described in the https://getcomposer.org/ "Getting Started" section
Reading /private/tmp/composer-test/vendor/composer/installed.json
Running 1.4.1 (2017-03-10 09:29:45) with PHP 5.5.38 on Darwin / 15.6.0
Loading composer repositories with package information
Downloading https://composer.intranet/packages.json
Downloading https://composer.intranet/packages.json
Downloading https://composer.intranet/packages.json


  [Composer\Downloader\TransportException]
  The "https://composer.intranet/packages.json" file could not be downloaded: SSL operation failed with code 1. OpenSSL Error messages:
  error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
  Failed to enable crypto
  failed to open stream: operation failed


Exception trace:
 () at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Util/RemoteFilesystem.php:482
 Composer\Util\RemoteFilesystem->get() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Util/RemoteFilesystem.php:101
 Composer\Util\RemoteFilesystem->getContents() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Repository/ComposerRepository.php:661
 Composer\Repository\ComposerRepository->fetchFile() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Repository/ComposerRepository.php:479
 Composer\Repository\ComposerRepository->loadRootServerFile() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Repository/ComposerRepository.php:258
 Composer\Repository\ComposerRepository->hasProviders() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/DependencyResolver/Pool.php:99
 Composer\DependencyResolver\Pool->addRepository() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Installer.php:376
 Composer\Installer->doInstall() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Installer.php:223
 Composer\Installer->run() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Command/InstallCommand.php:119
 Composer\Command\InstallCommand->execute() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/vendor/symfony/console/Command/Command.php:267
 Symfony\Component\Console\Command\Command->run() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/vendor/symfony/console/Application.php:846
 Symfony\Component\Console\Application->doRunCommand() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/vendor/symfony/console/Application.php:191
 Symfony\Component\Console\Application->doRun() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Console/Application.php:227
 Composer\Console\Application->doRun() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/vendor/symfony/console/Application.php:122
 Symfony\Component\Console\Application->run() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/src/Composer/Console/Application.php:100
 Composer\Console\Application->run() at phar:///usr/local/Cellar/composer/1.4.1/libexec/composer.phar/bin/composer:54
 require() at /usr/local/Cellar/composer/1.4.1/libexec/composer.phar:24

install [--prefer-source] [--prefer-dist] [--dry-run] [--dev] [--no-dev] [--no-custom-installers] [--no-autoloader] [--no-scripts] [--no-progress] [--no-suggest] [-v|vv|vvv|--verbose] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--ignore-platform-reqs] [--] [<packages>]...

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 1
  • Comments: 28 (9 by maintainers)

Commits related to this issue

Most upvoted comments

Mac OS X: Look for the location of your cafile by: php -r 'var_dump(openssl_get_cert_locations());' Then set composer to use this (with the example in my case): composer config --global cafile '<location of cafile>' Example: composer config --global cafile '/usr/local/etc/openssl/cert.pem'

Windows: https://support.microsoft.com/en-us/help/931125/how-to-get-a-root-certificate-update-for-windows

Linux Debian: apt-get install --only-upgrade ca-certificates

RedHat / CentOS: yum update ca-certificates

Ref: https://typo3.org/article/certificate-issue-with-composer/

I agree with @brianlmoon the problem is that composer provides only file or directory and not both. I’m now in the same situation and everything on my mac works nice with the certificates, everything that uses openssl. Except for composer.

Imho composer should have an option/setting to disable the ca-bundle and use the defaults of PHP and OpenSSL.

I’l try to explain it better.

If you specify both cafile and capath, than (like @brianlmoon already mentioned) first it will try to use the cafile and than fallback to the certs in capath.

In the current case composer provides only one of them and forces the usage of only one of them.

This leads to the case that if you provide a cafile it will work for everything which is signed by an official authority. If you provide capath with your custom authority certificate, than only this will be used and not the official CAs. This will make your custom domain working (which is signed with a cert file from capath) but will fail on packagist.org because it is signed by an authority from cafile which was not provided to the context.

This is not the intended behaviour of OpenSSL and breaks composer if you want to use satis hosted on a domain signed by your companies CA and Packagist in parallel.

Still an issue in Composer 1.9.0.

Setting openssl.cafile and openssl.capath in php.ini instead of relying on defaults “fixes” this, but it shouldn’t be required. PHP works fine with the defaults, but Composer breaks this with unnecessary stream options.

@alcohol Maybe this script will help me be more clear.

<?php

function fetch_repos($opts) {
    $context = stream_context_create($opts);

    echo "Fetching Private Repository: ";
    $result = @file_get_contents("https://composer.dealnews.net/packages.json", false, $context);
    if($result !== false) {
        echo "OK\n";
    } else {
        echo "Failed\n";
    }

    echo "Fetching Packagist Repository: ";
    $result = @file_get_contents("https://packagist.org/packages.json", false, $context);
    if($result !== false) {
        echo "OK\n";
    } else {
        echo "Failed\n";
    }

}

echo "Using capath and cafile in context\n";
$opts = array(
    'ssl' => array(
        'cafile' => "/usr/local/etc/openssl/cert.pem",
        'capath' => "/usr/local/etc/openssl/certs"
    ),
);
fetch_repos($opts);
echo "\n";


echo "Using only capath in context\n";
$opts = array(
    'ssl' => array(
        'capath' => "/usr/local/etc/openssl/certs"
    ),
);
fetch_repos($opts);
echo "\n";


echo "Using only cafile in context\n";
$opts = array(
    'ssl' => array(
        'cafile' => "/usr/local/etc/openssl/cert.pem"
    ),
);
fetch_repos($opts);
echo "\n";

output:

$ php ~/tmp/test.php 
Using capath and cafile in context
Fetching Private Repository: OK
Fetching Packagist Repository: OK

Using only capath in context
Fetching Private Repository: OK
Fetching Packagist Repository: Failed

Using only cafile in context
Fetching Private Repository: Failed
Fetching Packagist Repository: OK

It only proves my case even more. There is something wrong with your certs. I simply cannot reproduce this scenario.