Як на Windows виправити помилку «SSL certificate problem: unable to get local issuer certificate» у PHP cURL

Матеріал розбирає підхід Ayesh Karunaratne до налаштування PHP cURL на Windows, коли HTTPS запити падають із помилкою перевірки сертифіката.

Симптом: HTTPS запити через cURL повертають помилку SSL

Типова ситуація на Windows: простий HTTPS запит через PHP cURL повертає false, а curl_error() містить текст:

SSL certificate problem: unable to get local issuer certificate

Причина в тому, що cURL повинен мати доступ до списку довірених кореневих сертифікатів (Certificate Authorities, CA). На Linux/BSD/macOS libcurl зазвичай використовує системне сховище сертифікатів за замовчуванням. На Windows такого єдиного файлу зі всіма кореневими сертифікатами немає, і PHP сам окремий список CA не постачає.

У результаті cURL на Windows часто не може знайти файл CA, тому й виникає помилка unable to get local issuer certificate.

How to fix PHP Curl HTTPS Certificate Authority issues on Windows

Базовий приклад, який ламається через цю проблему:

$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

Підхід 1 — використати нативне сховище сертифікатів Windows

Автор вважає основним рішенням використання нативних сертифікатів операційної системи. Починаючи з cURL 7.71, зʼявилась опція, яка дозволяє libcurl читати кореневі сертифікати з системного сховища навіть на Windows.

У PHP це робиться через опцію CURLOPT_SSL_OPTIONS з бітом CURLSSLOPT_NATIVE_CA. Важливі умови:

  • PHP зібрано з cURL версії 7.71 або новішої.
  • Використовується PHP 8.2 або новіший, де константа CURLSSLOPT_NATIVE_CA доступна.

Мінімальний приклад використання нативного CA:

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

У цьому варіанті код просто вмикає CURLSSLOPT_NATIVE_CA</code і покладається на те, що версії 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);

Тут важливі моменти:

  • defined('CURLSSLOPT_NATIVE_CA') — перевіряє, чи PHP взагалі знає про цю константу (тобто чи є підтримка в збірці).
  • version_compare(curl_version()['version'], '7.71', '>=') — гарантує, що підключена бібліотека cURL не старша за 7.71.
  • Якщо обидві умови виконані, вмикається використання системного сховища сертифікатів.

Переваги цього підходу:

  • Не потрібно вручну завантажувати та оновлювати файл з кореневими сертифікатами.
  • PHP cURL використовує той самий набір довірених CA, що й сама Windows.
  • Менше ризику забути оновити файл cacert.pem і раптово отримати помилки з окремими сайтами.

Обмеження:

  • Працює лише на PHP 8.2+ з cURL 7.71+.
  • Потрібно мати збірку PHP, де cURL зібраний з підтримкою цієї опції.

Підхід 2 — власний файл cacert.pem і налаштування php.ini

Якщо ви працюєте з PHP версії до 8.2 або з cURL старішим за 7.71, константа CURLSSLOPT_NATIVE_CA буде недоступна. У такому випадку автор пропонує класичне рішення: власний файл з кореневими сертифікатами і явне налаштування cURL використовувати його.

Кроки налаштування cacert.pem

  1. Завантажити файл CA з сайту cURL.

    cURL-проєкт веде актуальний список CA, витягнутий з кореневого сховища Mozilla:

  2. Перенести файл у директорію, доступну PHP і вебсерверу. У прикладі автора використовується шлях:

    C:/php/cacert.pem

    Важлива деталь: шлях має бути абсолютним.

  3. Прописати шлях до cacert.pem у php.ini через директиву curl.cainfo.

    [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"

    Ключові моменти:

    • Розкоментувати рядок curl.cainfo та вказати абсолютний шлях до файлу.
    • У Windows в лапках можна використовувати прямі слеші / або зворотні \, головне — коректний абсолютний шлях.
  4. Перезапустити вебсервер (Apache, Nginx з PHP-FPM під Windows, вбудований PHP сервер тощо), щоб php.ini перечитався.

Після цього всі cURL запити в цьому PHP середовищі будуть використовувати вказаний cacert.pem за замовчуванням.

Підтримка актуальності cacert.pem

Слабке місце цього підходу — файл cacert.pem треба регулярно оновлювати. За описом автора, список CA, який формує cURL на основі Mozilla, оновлюється приблизно 4-5 разів на рік.

Наслідки, якщо не оновлювати файл:

  • Нові довірені CA не потраплять у ваш локальний список, і деякі сайти з «новішими» ланцюжками сертифікатів можуть раптово почати падати.
  • Старі, скомпрометовані або відкликані сертифікати можуть довше лишатися в локальному файлі, хоча в системі або в браузерах вже видалені.

Автор не дає конкретної інструкції з автоматизації, але з опису зрозуміло, що адміністратору варто передбачити періодичне оновлення файлу з офіційного URL.

Якщо немає доступу до php.ini

У деяких середовищах змінити php.ini неможливо (хостинг, обмежені права тощо). Тоді шлях до cacert.pem можна задати безпосередньо в коді для кожного cURL запиту через опцію CURLOPT_CAINFO.

Приклад з джерела:

Приклад коду в оригіналі

Тут все аналогічно варіанту з php.ini, але шлях визначається в коді. Це корисно, якщо:

  • ви хочете використовувати окремий набір CA для конкретного застосунку;
  • нескладно змінити конфігурацію лише в цьому проєкті, а php.ini спільний для багатьох.

Окремо згадується, що на PHP 8.2+ з cURL 7.77 підтримується ще одна опція — CURLOPT_CAINFO_BLOB, яка дозволяє передати в cURL сам вміст файлу cacert.pem як рядок. Це зручно, коли сертифікати зберігаються не як файл на диску (наприклад, в базі даних чи вбудовані в конфіг).

Чого робити не можна — вимикати перевірку сертифікатів

Автор окремо підкреслює поширену, але небезпечну «пораду» з форумів та блогів: повністю вимкнути перевірку TLS сертифіката в cURL.

Це роблять через опції:

  • CURLOPT_SSL_VERIFYPEER = 0
  • CURLOPT_SSL_VERIFYHOST = 0

Приклад, який автор прямо позначає як небезпечний і не рекомендується:

Приклад коду в оригіналі

Проблема цього «рішення» в тому, що без перевірки сертифіката cURL прийме будь-який TLS сертифікат. Це означає:

  • можна прозоро підмінити трафік (man-in-the-middle),
  • PHP застосунок не знатиме, що замість оригінального сервера підключився інший,
  • будь-які захищені дані (токени, логіни, персональні дані) можуть потрапити до сторонньої сторони.

Цей варіант може тимчасово «прибрати» помилку в логах, але фактично відключає головний сенс HTTPS — перевірку автентичності сервера.

З точки зору автора джерела, правильний шлях — або дати cURL доступ до нативного сховища сертифікатів (якщо версії дозволяють), або коректно налаштувати cacert.pem і підтримувати його в актуальному стані, а не знімати перевірку безпеки.


Джерело: Ayesh Karunaratne — PHP.Watch: статьи

Пошук

Закрити