Устранение неполадок OpenSSL: решение ошибки unable to get local issuer certificate
При установлении защищённых соединений с помощью OpenSSL (например, при выполнении сетевых запросов из PHP, использовании команд openssl
или curl
, либо при создании SSL/TLS-соединений в приложениях Node.js/Python) разработчики часто сталкиваются с ошибкой 20:unable to get local issuer certificate
. Это распространённая проблема, связанная с верификацией OpenSSL цепочки сертификатов и особенностями работы с доверенными центрами сертификации.
В целях безопасности OpenSSL по умолчанию требует, чтобы цепочка доверия до корневого центра сертификации (CA) была явно задана. Если такие доверенные корневые сертификаты не обнаружены или не указаны, OpenSSL не может подтвердить легитимность серверного сертификата и сообщает об ошибке.
В этой статье подробно объясняются причины появления ошибки и предлагаются способы её решения в среде ServBay, включая настройку хранилища доверенных CA для PHP, Python, Node.js и команды curl
.
Сообщение об ошибке 20:unable to get local issuer certificate
Описание проблемы
Когда OpenSSL пытается проверить SSL/TLS-сертификат удалённого сервера, он строит цепочку доверия — от сертификата сервера до доверенного корневого CA. Если OpenSSL не удаётся найти на локальном устройстве нужные промежуточные или корневые сертификаты, или если не указан путь к хранилищу доверия (CAFile или CAPath), процесс верификации завершится ошибкой 20:unable to get local issuer certificate
.
Проще говоря, OpenSSL «не знает», какому центру сертификации доверять, поэтому не может подтвердить подлинность сервера, к которому вы подключаетесь.
Версия OpenSSL и путь к CA-сертификатам в ServBay
ServBay — это интегрированная локальная среда веб-разработки с предустановленным пакетом OpenSSL, а также набором общедоступных корневых CA-сертификатов. Конкретная версия OpenSSL в ServBay зависит от типа процессора вашего устройства:
- ServBay для Apple Silicon (чипы серии M): используется OpenSSL версии 3.2.1.
- ServBay для Intel: используется OpenSSL версии 1.1.1u.
Поддерживаемый набор корневых сертификатов (cacert.pem
) и папка с отдельными сертификатами (certs
) расположены в каталоге установки пакетов ServBay. Вам нужно определить путь в зависимости от вашей сборки ServBay:
# Файл-бандл всех доверенных корневых сертификатов
cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
# Директория с отдельными сертификатами (обычно достаточно cacert.pem, но некоторые приложения требуют capath)
capath=/Applications/ServBay/package/common/openssl/3.2/certs
2
3
4
# Файл-бандл всех доверенных корневых сертификатов
cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
# Директория с отдельными сертификатами
capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
2
3
4
Ключ к устранению ошибки unable to get local issuer certificate
— явно указать OpenSSL путь к соответствующему cafile
или capath
, чтобы можно было найти доверенные CA-сертификаты.
Примеры решений
Ниже приведено, как можно задать хранилище доверенных CA в разных инструментах и языках программирования.
Пример 1: Тестирование соединения с помощью команды openssl
Если при прямом использовании команды openssl s_client
возникает ошибка:
openssl s_client -quiet -connect gmail.com:443
Можно увидеть такой фрагмент вывода с ошибкой verify error:num=20:unable to get local issuer certificate
:
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... другая информация о соединении ...
2
3
4
5
6
7
8
Решение:
Укажите путь к файлу CA с помощью параметра -CAfile
для используемой версии ServBay:
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/3.2/cacert.pem
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
Если соединение успешное и сертификат прошёл проверку, в выводе все значения verify return
будут равны 1
, и строки с verify error:num=20
не будет:
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... другая информация о соединении ...
2
3
4
5
6
7
Пример 2: OpenSSL в PHP
Во многих сетевых функциях PHP (например, file_get_contents
для HTTPS URL, stream_socket_client
, cURL и пр.) используется OpenSSL. Указать доверенное хранилище CA можно через php.ini
либо непосредственно в коде через опции stream context.
Способ А: Редактирование php.ini
(рекомендуется)
Это глобальное решение для всех PHP-скриптов. Откройте редактор для соответствующего файла php.ini
(доступен в панели управления ServBay), найдите секцию [openssl]
и добавьте или скорректируйте такие строки, выбрав путь согласно архитектуре устройства:
[openssl]
; Путь к файлу доверенных CA-сертификатов
openssl.cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
; Путь к каталогу CA-сертификатов (опционально, рекомендуется)
openssl.capath=/Applications/ServBay/package/common/openssl/3.2/certs
2
3
4
5
[openssl]
; Путь к файлу доверенных CA-сертификатов
openssl.cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
; Путь к каталогу CA-сертификатов (опционально, рекомендуется)
openssl.capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
2
3
4
5
После изменения php.ini
перезапустите PHP-сервис в ServBay (или всю среду) для применения настроек.
Способ Б: Указание параметров в коде (только для текущего соединения)
Если не хочется изменять глобальные настройки, можно явно указать параметры при создании SSL/TLS-контекста с помощью stream_context_create
:
<?php
// Пример: подключение к SMTP-серверу с SSL/TLS
$server = 'ssl0.ovh.net';
$port = 465;
// Выберите путь к CA-файлу согласно версии ServBay
// Для Apple Silicon:
$caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// Для Intel:
// $caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
$contextOptions = [
'ssl' => [
'verify_peer' => true, // Включить проверку сертификата сервера
'verify_peer_name' => true, // Проверять имя хоста в сертификате
'allow_self_signed' => false, // Не разрешать самоподписанные сертификаты, если только не доверяете им вручную
'cafile' => $caCertFile, // Путь к файлу CA
// 'capath' => '/Applications/ServBay/package/common/openssl/3.2/certs', // Опционально — директория CA
],
];
$context = stream_context_create($contextOptions);
// Установка SSL/TLS соединения с использованием контекста
$connection = @stream_socket_client(
"ssl://$server:$port",
$errno,
$errstr,
30, // Таймаут
STREAM_CLIENT_CONNECT,
$context // Передаем опции контекста
);
if ($connection) {
echo "Connection established to $server:$port\n";
// Пример: отправка команды EHLO
fwrite($connection, "EHLO servbay.demo\r\n"); // Используется демонстрационный домен от ServBay
while (!feof($connection)) {
echo fgets($connection);
}
fclose($connection);
} else {
echo "Failed to connect to $server:$port. Error: $errstr ($errno)\n";
}
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Пример 3: Использование OpenSSL в Python (модуль ssl)
Модуль ssl
в Python также позволяет явно задать путь к CA-файлу через SSLContext.
import ssl
import socket
server = 'ssl0.ovh.net'
port = 465
# Выберите путь к CA-файлу согласно версии ServBay
# Для Apple Silicon:
ca_cert_file = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem'
# Для Intel:
# ca_cert_file = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem'
# Создаем стандартный SSLContext и указываем CA-файл
context = ssl.create_default_context(cafile=ca_cert_file)
# Можно также указать директорию: context = ssl.create_default_context(capath='/Applications/ServBay/package/common/openssl/3.2/certs')
try:
# Стандартное подключение через socket
with socket.create_connection((server, port)) as sock:
# Оборачиваем socket в SSL-слой
with context.wrap_socket(sock, server_hostname=server) as ssock:
print(f"SSL connection established. Negotiated Protocol: {ssock.version()}")
# Пример: отправка EHLO
ssock.sendall(b"EHLO servbay.demo\r\n") # Демонстрационный домен ServBay
while True:
data = ssock.recv(4096)
if not data:
break
print(data.decode())
except Exception as e:
print(f"Failed to connect or SSL error: {e}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Пример 4: Использование OpenSSL в Node.js (модуль tls)
В Node.js модуль tls
применяется для SSL/TLS-подключений. В параметре опций можно задать свойство ca
, указав файл cacert.pem
.
const tls = require('tls');
const fs = require('fs');
const server = 'www.google.com'; // Демонстрационный пример с доверенным сайтом
const port = 443;
// Выберите путь к CA-файлу согласно версии ServBay
// Для Apple Silicon:
const caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// Для Intel:
// const caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
const options = {
host: server,
port: port,
// Загружаем содержимое CA-файла
ca: fs.readFileSync(caCertFile),
// Модуль tls по умолчанию проверяет hostname (checkServerIdentity),
// Если CA-файл корректно задан, а сертификат сервера действителен, ошибок быть не должно.
// Не отключайте checkServerIdentity без крайней необходимости (это критично для безопасности!).
// checkServerIdentity: () => { return null; } // <-- НЕ ИСПОЛЬЗУЙТЕ без понимания риска!
};
const socket = tls.connect(options, () => {
console.log('SSL connection established');
// Для HTTPS обычно требуется отправить HTTP-запрос, этот пример — просто демонстрация соединения
// socket.write('GET / HTTP/1.1\r\nHost: ' + server + '\r\n\r\n');
});
socket.on('data', (data) => {
console.log(data.toString());
});
socket.on('close', () => {
console.log('Connection closed');
});
socket.on('error', (error) => {
console.error('Error:', error.message); // Вывод информации об ошибке
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Замечание: В приведённом примере для Node.js строка с checkServerIdentity: () => { return null; }
удалена. Этот параметр полностью отключает верификацию имени хоста и крайне небезопасен. Ошибка OpenSSL unable to get local issuer certificate
связана с доверием к корневым CA, а не с именем хоста (verify_peer_name). Если правильно задан параметр ca
и действительный сертификат сервера, стандартная проверка Node.js должна проходить успешно. Если появляется ошибка hostname, это проблема самого сертификата, а не хранилища доверия.
Пример 5: Использование OpenSSL в команде curl
Команда curl
для HTTPS также базируется на OpenSSL (или другой SSL-библиотеке). Путь к CA-файлу задаётся параметром --cacert
.
# Пример доступа к HTTPS-сайту через curl с использованием CA-файла ServBay
# Выберите путь согласно вашей версии ServBay:
# Для Apple Silicon:
curl --cacert /Applications/ServBay/package/common/openssl/3.2/cacert.pem https://example.com
# Для Intel:
# curl --cacert /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem https://example.com
2
3
4
5
6
7
При корректно указанном пути к CA-файлу и действительном серверном сертификате команда отработает без ошибок сертификата.
Итоги
Ошибка 20:unable to get local issuer certificate
очень часто возникает при работе с OpenSSL при подключениях по SSL/TLS. Причина — необходимость явного указания доверенного хранилища сертификатов для проверки серверных сертификатов. В ServBay уже есть предустановленный файл cacert.pem
с наборами общеизвестных корневых CA.
Чтобы решить проблему, просто укажите путь к этому файлу в настройках вашей среды (через php.ini
, параметры SSL-контекста в коде, аргументы команд — например, -CAfile
для OpenSSL, --cacert
для curl). Обязательно убедитесь, что путь соответствует вашей архитектуре macOS (Apple Silicon или Intel) и версии OpenSSL в ServBay. Корректная настройка доверенных CA позволяет безопасно работать с любыми внешними сервисами в вашей локальной среде разработки.