Матеріал розбирає підхід 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.

Базовий приклад, який ламається через цю проблему:
$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
-
Завантажити файл CA з сайту cURL.
cURL-проєкт веде актуальний список CA, витягнутий з кореневого сховища Mozilla:
- Сторінка опису: https://curl.se/docs/caextract.html
- Безпосередньо файл: https://curl.se/ca/cacert.pem
-
Перенести файл у директорію, доступну PHP і вебсерверу. У прикладі автора використовується шлях:
C:/php/cacert.pemВажлива деталь: шлях має бути абсолютним.
-
Прописати шлях до
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 в лапках можна використовувати прямі слеші
/або зворотні\, головне — коректний абсолютний шлях.
- Розкоментувати рядок
-
Перезапустити вебсервер (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= 0CURLOPT_SSL_VERIFYHOST= 0
Приклад, який автор прямо позначає як небезпечний і не рекомендується:
Проблема цього «рішення» в тому, що без перевірки сертифіката cURL прийме будь-який TLS сертифікат. Це означає:
- можна прозоро підмінити трафік (man-in-the-middle),
- PHP застосунок не знатиме, що замість оригінального сервера підключився інший,
- будь-які захищені дані (токени, логіни, персональні дані) можуть потрапити до сторонньої сторони.
Цей варіант може тимчасово «прибрати» помилку в логах, але фактично відключає головний сенс HTTPS — перевірку автентичності сервера.
З точки зору автора джерела, правильний шлях — або дати cURL доступ до нативного сховища сертифікатів (якщо версії дозволяють), або коректно налаштувати cacert.pem і підтримувати його в актуальному стані, а не знімати перевірку безпеки.
Джерело: Ayesh Karunaratne — PHP.Watch: статьи