Как решить проблему PHP Curl с центром сертификации HTTPS в Windows

Источник: «How to fix PHP Curl HTTPS Certificate Authority issues on Windows»
В Windows запросы HTTPS, выполняемые с помощью расширения Curl, могут не выполняться, поскольку у Curl нет списка корневых сертификатов для проверки сертификатов сервера. Рассмотрим безопасные и эффективные решения, а также обратим внимание на плохие советы, которые могут сделать PHP-приложения небезопасными.

Успешный HTTPS-запрос включает в проверку HTTP-клиентом предоставленного сервером TLS-сертификата по списку известных и доверенных корневых сертификатов. Расширение PHP Curl не отличается от других; расширение Curl использует libcurl для выполнения HTTPS-запроса, а libcurl, в свою очередь, использует библиотеку TLS, такую как OpenSSL, для проверки запроса.

Расширение Curl требует наличия файла, содержащего все доверенные корневые сертификаты для завершения проверки HTTPS, и PHP предоставляет его в виде директивы в файле php.ini.

В Linux, BSD и macOS libcurl может по умолчанию использовать корневые сертификаты системы, но в Windows это невозможно, поскольку Windows не поставляется с одним файлом, содержащим все корневые сертификаты системы.

В этой статье рассматриваются два возможных подхода к успешному выполнению HTTPS-запросов с помощью расширения Curl, а также то, что не следует делать, чтобы HTTPS-запросы оставались небезопасными.

Почему это не работает

$ch = curl_init('https://php.watch');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch); // false

curl_error($ch);
// SSL certificate problem: unable to get local issuer certificate

Если вызовы curl_exec завершаются с ответом false, а curl_error указывает на проблему SSL certificate problem: unable to get local issuer certificate error, это означает, что Curl не был предоставлен файл, содержащий корневые сертификаты, или он не смог его обнаружить.

Эта ошибка редко встречается в системах Linux, BSD и macOS, но довольно распространена в Windows, так как нет специального файла для получения корневых сертификатов, а PHP не поставляет список корневых сертификатов сам по себе.

Решение состоит в том, чтобы предоставить файл с актуальными корневыми сертификатами или, в идеале, позволить Curl разобрать собственное хранилище корневых сертификатов, предоставляемое базовой операционной системой.

Используйте системные центры сертификации

В Curl 7.71 и более поздних версиях можно установить опцию для запроса Curl на использование собственных (системных) корневых сертификатов. Это работает даже в Windows, где Curl анализирует системные корневые сертификаты и использует их.

Когда опция CURLOPT_SSL_OPTIONS имеет значение CURLSSLOPT_NATIVE_CA или битовую маску, содержащую эти биты, Curl пытается использовать нативное хранилище корневых сертификатов, в зависимости от возможностей и версий базовой библиотеки TLS.

Это рекомендуемое исправление, если расширение Curl собрано с Curl 7.71 или более поздней версией и PHP 8.2 или более поздней версией.

 $ch = curl_init('https://php.watch');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
curl_exec($ch);

Обратите внимание, что в приведённом выше фрагменте не проверяется версия Curl и версия PHP, а предполагается, что требования к версии PHP и Curl выполнены. Далее приведён пример условного добавления опции Curl:

$ch = curl_init('https://php.watch');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if (defined('CURLSSLOPT_NATIVE_CA')
&& version_compare(curl_version()['version'], '7.71', '>=')) {
curl_setopt($ch, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
}

curl_exec($ch);

Загрузите и сохраните файл cacert.pem.

Для приложений, работающих на PHP версии старше 8.2 (где константа CURLSSLOPT_NATIVE_CA недоступна), или когда версия Curl старше 7.71, рекомендуемым альтернативным решением является загрузка файла корневого сертификата, совместимого с Curl, и настройка PHP или запроса Curl на его использование.

Проект Curl поддерживает актуальный список сертификатов. См. CA Certificates extracted from Mozilla.

  1. Загрузите файл cacert.pem

  2. Переместите файл в каталог, доступный PHP и веб-серверу. Например, в C:/php/cacert.pem.

  3. Отредактируйте файл php.ini и измените запись curl.cainfo так, чтобы она указывала на абсолютный путь к файлу cacert.pem.

    [curl]
    ; A default value for the CURLOPT_CAINFO option. This is required to be an
    ; absolute path.
    -;curl.cainfo =
    +curl.cainfo = "C:/php/cacert.pem"
  4. При необходимости перезапустите веб-сервер (например, Apache), чтобы перезагрузить INI-файл.

Недостатком этого подхода является необходимость регулярного обновления файла cacert.pem. Файл cacert.pem, предоставляемый, например, проектом Curl, извлекается из корневого хранилища, поддерживаемого Mozilla. В среднем этот список и сам файл обновляются 4-5 раз в год. Чтобы обеспечить совместимость с последним списком корневых сертификатов, регулярно обновляйте локальную копию этого файла.

Если модификация INI-файла невозможна, укажите абсолютный путь к файлу cacert.pem в запросе Curl:

 $ch = curl_init('https://php.watch');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_CAINFO, 'C:/php/cacert.pem');
curl_exec($ch);

На PHP 8.2+ с Curl 7.77 с помощью опции CURLOPT_CAINFO_BLOB можно получить строку, содержащую содержимое cacert.pem.

НЕ отключайте проверку сертификатов

На форумах и в статьях в Интернете часто встречается неправильный совет — отключить проверку сертификатов. Это проблема безопасности, поскольку без проверки сертификата Curl с радостью примет любой сертификат TLS, включая потенциально перехваченное или изменённое содержимое.

В следующем примере показано такое часто предлагаемое решение, которое небезопасно и не рекомендуется.

$ch = curl_init('https://php.watch');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

// Отключение валидации сертификатов.
// НЕ ДЕЛАЙТЕ ЭТОГО!!!
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

curl_exec($ch);

Дополнительные материалы

Предыдущая Статья

Красивый перенос текста с CSS свойством text-wrap

Следующая Статья

Классы кэша в Laravel