Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 97 · От первого результата к API

OpenSSL: подписи, ключи, сертификаты и защищённые файлы

Проверить накладную, сохранить ключ, прочитать сертификат и обменяться защищённым документом.

Поставщик прислал накладную: можно ли принять её байты?

В invoice.txt написано Заказ A1: 1200 RUB. Вместе с файлом пришла подпись — отдельный короткий набор байтов, созданный закрытым ключом поставщика. У нас уже есть его открытый ключ, полученный по доверенному каналу. Нужно увидеть конкретное решение: Принять накладную A1: подпись поставщика верна.

Проверка именно этой накладной
  1. invoice.txt: Заказ A1: 1200 RUB + перевод строки
  2. invoice.sig.b64: подпись этих точных байтов
  3. publisher-public.pem: заранее доверенный открытый ключ
  4. openssl_verify сопоставляет документ, подпись, ключ и SHA-256
  5. Результат 1 → принять; любой другой результат → остановиться

Ключ — данные для криптографической операции. У пары ключей закрытая часть позволяет подписывать, открытая — проверять. Аналогия с печатью помогает понять роли, но у файла нет видимого оттиска: проверяются все байты математическим алгоритмом. Подпись не прячет текст. Если злоумышленник заменит и документ, и ключ, одной успешной проверки будет недостаточно: доверие к открытой части устанавливают заранее.

В архиве примера есть все три входных файла. $invoice = сохраняет содержимое в переменную; file_get_contents(...) читает файл. __DIR__ — папка скрипта, точка соединяет её с именем. Base64 — запись двоичных байтов печатными символами; base64_decode(..., true) возвращает исходную подпись и строго проверяет алфавит. В нашем fixture она исправна. Вызов openssl_verify получает документ, подпись, открытый ключ и SHA-256 в этом порядке. === 1 требует именно успешный код, а ? ... : ... выбирает текст решения.

Проверить реальную подпись файла invoice.txtСамостоятельный пример
<?php
declare(strict_types=1);

$invoice = file_get_contents(__DIR__ . '/invoice.txt');
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
$publicKey = file_get_contents(__DIR__ . '/publisher-public.pem');
$result = openssl_verify($invoice, $signature, $publicKey, OPENSSL_ALGO_SHA256);
echo $result === 1 ? "Принять накладную A1: подпись поставщика верна\n" : "Не принимать накладную\n";
Ожидаемый результат
Принять накладную A1: подпись поставщика верна

Все входные файлы уже подготовлены. Внешний сервер не нужен. Учебный открытый ключ намеренно поставляется с примером; это модель предварительной доверенной настройки, а не доказательство личности произвольного отправителя.

Измените 1200 на 9000 в invoice.txt, сохранив прежнюю подпись: программа откажется принимать файл. Даже другой перевод строки меняет подписанные байты. Поэтому подписывают согласованный формат данных и не «исправляют» его после подписи. Как создать подпись — openssl_sign(); подробные три исхода проверки — openssl_verify().

PHP Manual: book.openssl PHP Manual: ref.openssl

Как выбрать задачу: подпись, шифрование, пароль или TLS

ПотребностьПодход и видимый результат
Убедиться, что накладную подписал владелец известного ключаsign/verify: принять исходный файл и отклонить изменённый. Текст остаётся читаемым.
Скрыть сообщение и обнаружить изменениеШифр AEAD, например AES-GCM: получатель получает исходное сообщение либо отказ. Нужно хранить nonce/IV, tag и согласованное AAD вместе с шифротекстом; ключ хранится отдельно.
Сопоставить файл с известным отпечаткомdigest: одинаковые байты дают одинаковый хеш. Хеш рядом с файлом сам по себе не удостоверяет отправителя.
Проверить пароль входаОбычно password_hash/password_verify. Обратимое шифрование пароля не нужно для входа.
Защитить HTTP-сеанс с серверомTLS настраивают в HTTP-клиенте/потоке с проверкой сертификата и имени. Успешный openssl_encrypt не проверяет сеть и не доказывает работоспособность HTTPS.
Передать сертификат/подпись по формату партнёраX.509, CSR, PKCS#7/CMS, PKCS#12 — разные структуры. Сначала согласуйте формат, назначение и доверенные CA; одинаковое расширение .pem не означает одинаковый объект.

OpenSSL даёт низкоуровневые строительные блоки и поддержку существующих протоколов. Для нового приложения выбирайте поддерживаемую библиотеку готового протокола; правила обмена ключами, повторы сообщений и хранение секретов не возникают автоматически из вызова функции. В этой главе каждое сочетание ограничено показанной локальной задачей.

Подготовить окружение: один CLI-процесс, одна конфигурация, рабочая операция

Если скопированный пример не запускается, сначала выясните, какой PHP его выполняет. CLI — запуск php example.php из терминала. Веб-сервер может использовать другой исполняемый файл и другой php.ini. Наличие openssl.exe в PATH само по себе не включает расширение PHP.

Узнать сборку, в которой выполняется примерСамостоятельный пример
<?php
declare(strict_types=1);

if (!extension_loaded('openssl')) { throw new RuntimeException('Расширение OpenSSL не загружено'); }
echo 'PHP: ', PHP_VERSION, "\n";
echo 'OpenSSL: ', OPENSSL_VERSION_TEXT, "\n";
echo function_exists('openssl_password_hash') ? "Argon2 backend: доступен\n" : "Argon2 backend: не собран\n";
echo "Следующий шаг: открыть ключ и выполнить подпись\n";
Ожидаемый результат
PHP: 8.5.11
OpenSSL: OpenSSL 3.5.8 25 Aug 2026
Argon2 backend: доступен
Следующий шаг: открыть ключ и выполнить подпись

Вывод фиксирует реально проверенную сборку учебника. На другой машине версии и доступность Argon2 могут отличаться; это повод проверить совместимость, а не подставить ожидаемую строку.

СредаЧто требуется
Сборка UnixПодключение --with-openssl; с PHP 7.4 путь нельзя передать как --with-openssl=DIR: используют PKG_CONFIG_PATH либо OPENSSL_LIBS/OPENSSL_CFLAGS. Нужен рабочий системный источник случайности; обычно /dev/urandom или /dev/random.
Готовая Windows-сборкаЗагрузите соответствующее расширение в php.ini, если оно поставляется динамически; DLL библиотеки должны быть доступны этой сборке через PATH. Старые поставки использовали libeay32.dll, OpenSSL 1.1+ — libcrypto-*.dll. Не копируйте случайные DLL в Windows System32.
Ключи/CSR/выпуск сертификатаНужен корректный openssl.cnf. В Windows-поставке пример обычно находится в extras/ssl. Его нельзя разрешать изменять непривилегированным пользователям.
Список шифров потоковОпция сборки --with-system-ciphers выбирает системный список вместо встроенного. Это не гарантирует успешного TLS-соединения с конкретным сервером.

Команды php --ini и php -m помогают найти конфигурацию и расширения именно CLI. В учебных архивах локальный openssl.cnf передаётся через options[config], поэтому пример не зависит от случайного системного файла. Сначала откройте README, распакуйте архив в отдельную папку и запустите php example.php из неё; файлы ввода уже включены.

Версия PHP из официальной таблицыДиапазон OpenSSL
PHP7.0≥0.9.8 и<1.2
PHP7.1–8.0≥1.0.1 и<3.0
PHP8.1–8.3≥1.0.2 и<4.0
PHP8.4+≥1.1.1 и<4.0

Это исторические требования совместимости, а не рекомендация ставить устаревшую библиотеку. Для рабочего сервиса выбирают поддерживаемую версию. В Windows PHP 8.2 по умолчанию использует OpenSSL 3; старые алгоритмы могут требовать legacy provider. Отказ unsupported после обновления разбирают по формату/алгоритму; включение legacy.dll глобально не является универсальным исправлением.

PHP Manual: openssl.setup PHP Manual: openssl.requirements PHP Manual: openssl.installation

Не путать openssl.cnf, php.ini и доверенный CA

openssl.cnf задаёт параметры библиотеки и генерации ключей/запросов. php.ini управляет расширением PHP. CA-файл содержит доверенные сертификаты издателей. Это три разные сущности: копирование CA в openssl.cnf не исправит ошибку ключевой конфигурации.

Поиск openssl.cnf
  1. OPENSSL_CONF: полный путь к файлу
  2. Затем историческая переменная SSLEAY_CONF
  3. Затем default certificate area, выбранная при сборке OpenSSL
  4. options[config] у конкретной функции может переопределить путь

В Windows с PHP 7.4 типичные default-пути — C:\Program Files\Common Files\SSL\openssl.cnf для x64 и C:\Program Files (x86)\Common Files\SSL\openssl.cnf для x86. Раньше использовался C:\usr\local\ssl\openssl.cnf. Это ориентиры сборки, не обещание существования файла на вашем компьютере.

Создать ключ по локальной конфигурации и подписать документСамостоятельный пример
<?php
declare(strict_types=1);

$config = __DIR__ . '/openssl.cnf';
if (!is_readable($config)) { throw new RuntimeException('Не найден локальный openssl.cnf'); }
$key = openssl_pkey_new(['config' => $config, 'private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_bits' => 2048]);
if ($key === false || !openssl_sign('Поставка A7', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Ключ не готов к подписи'); }
$public = openssl_pkey_get_details($key)['key'];
echo openssl_verify('Поставка A7', $signature, $public, OPENSSL_ALGO_SHA256) === 1 ? "Локальная конфигурация работает: A7 подписан и проверен\n" : "Проверка не пройдена\n";
Ожидаемый результат
Локальная конфигурация работает: A7 подписан и проверен

Проверена настоящая генерация и подпись, не только наличие расширения или lint. Этот результат ничего не утверждает о сетевом TLS или настройках веб-сервера.

Директива php.iniТочное назначение и доступность
openssl.cafileDefault пустая строка, INI_PERDIR. Файл доверенных CA для verify_peer при проверке удалённой стороны.
openssl.capathDefault пустая строка, INI_PERDIR. Правильно хешированный каталог сертификатов; поиск используется, если cafile не задан или нужный сертификат там не найден.
openssl.libctxDefault custom, INI_PERDIR. Отдельный контекст OpenSSL для каждого worker/потока улучшает изоляцию от других библиотек. Значение default выбирает глобальный контекст OpenSSL. Поддержка/ограничения зависят от сборки.

INI_PERDIR означает, что это не произвольные настройки для ini_set в любом скрипте: место задания сверяют с поддерживаемым SAPI. Для TLS отдельно задаются stream context verify_peer, verify_peer_name, cafile/capath; отключать проверки из-за ошибки доверия нельзя. openssl_get_cert_locations() показывает ориентиры путей, openssl_x509_checkpurpose() выполняет локальную проверку цепочки. Настоящее соединение требует отдельного runtime-теста.

PHP Manual: openssl.configuration

Назначение сертификата выбирается одним значением

Сертификат склада может разрешать TLS-сервер, но не запрос от TLS-клиента. purpose — одно назначение, а битовая маска используется в других API. Соединять X509_PURPOSE_* через| нельзя. Сравнение серверной и клиентской ролей уже выполнено в openssl_x509_checkpurpose().

КонстантаДля какой проверки
X509_PURPOSE_SSL_CLIENTСертификат TLS-клиента.
X509_PURPOSE_SSL_SERVERСертификат TLS-сервера.
X509_PURPOSE_NS_SSL_SERVERИсторическое серверное назначение Netscape SSL.
X509_PURPOSE_SMIME_SIGNПодписание S/MIME.
X509_PURPOSE_SMIME_ENCRYPTШифрование S/MIME для получателя.
X509_PURPOSE_CRL_SIGNПодпись списка отзыва сертификатов CRL.
X509_PURPOSE_ANYЛюбое назначение; не отключает все остальные проверки и не проверяет hostname.
X509_PURPOSE_OCSP_HELPERПомощник проверки OCSP-ответчика; с PHP 8.4.
X509_PURPOSE_TIMESTAMP_SIGNПодписант доверенной метки времени; с PHP 8.4.

PHP Manual: openssl.constants PHP Manual: openssl.purpose-check

Padding для RSA выбирается под конкретную операцию

В примере кода ячейки обе стороны согласовали OAEP/SHA-256. В подписи PSS используется другая схема. Нельзя взять подходящее название для одного API и автоматически перенести его в любую RSA-функцию.

КонстантаНазначение и предел
OPENSSL_PKCS1_PADDINGPKCS#1 v1.5; исторический default ряда RSA-функций. Для расшифровки учитывайте padding-oracle и provider implicit rejection.
OPENSSL_PKCS1_OAEP_PADDINGOAEP для public_encrypt/private_decrypt; с PHP 8.5 digest_algo позволяет явно задать хеш OAEP.
OPENSSL_PKCS1_PSS_PADDINGPSS для подписи/проверки RSA с PHP 8.5, не формат шифрования документа.
OPENSSL_NO_PADDINGСырой RSA без схемы подготовки. Нельзя безопасно шифровать произвольные пользовательские байты напрямую.
OPENSSL_SSLV23_PADDINGИсторически перечислен Manual, отсутствует в нашей OpenSSL 3-сборке; перед использованием старого кода проверяйте defined.

PHP Manual: openssl.padding

Тип ключа определяет допустимое действие

Один ключевой объект необязательно умеет всё. В учебнике RSA применяется к подписи и короткому OAEP-блоку, EC — к ECDSA/ECDH с совместимыми настройками. Для современных Curve25519/448 назначения разделены прямо в типах.

КонстантаМеханизм
OPENSSL_KEYTYPE_RSARSA: операции подписи/шифрования в согласованной схеме.
OPENSSL_KEYTYPE_DSADSA: подпись; нужен совместимый получатель и политика протокола.
OPENSSL_KEYTYPE_DHDH: согласование секрета, не подпись.
OPENSSL_KEYTYPE_ECЭллиптические кривые: соответствующие ECDSA/ECDH-операции.
OPENSSL_KEYTYPE_X25519
OPENSSL_KEYTYPE_X448
Согласование секрета; с PHP 8.4 при OpenSSL 3+.
OPENSSL_KEYTYPE_ED25519
OPENSSL_KEYTYPE_ED448
EdDSA-подписи; с PHP 8.4 при OpenSSL 3+. В PHP 8.5 алгоритм 0 у sign/verify позволяет операции, не использующие внешний хеш.

PHP Manual: openssl.key-types

PKCS7: что меняют флаги контейнера

Побитовое| объединяет независимые параметры, например BINARY|DETACHED. Флаг может изменить формат либо отключить проверку: эти два последствия нельзя считать одинаковыми. Ни NOVERIFY, ни NOSIGS не нужны для наших успешно проверенных лабораторных подписей.

ФлагЭффект
PKCS7_TEXTПри подписи/шифровании добавляет text/plain; проверка/расшифровка убирает эти заголовки и отказывает для иного MIME-типа.
PKCS7_BINARYНе канонизирует строки CRLF; подпись относится к выбранным исходным байтам. Для S/MIME detached согласуйте представление строк.
PKCS7_NOINTERNНе искать сертификат подписанта внутри сообщения; предоставьте его через untrusted_certificates_filename. Это не делает сертификат доверенным.
PKCS7_NOVERIFYОтключает проверку сертификата подписанта. Оставляет опасную для обычного приёма область непроверенного доверия.
PKCS7_NOCERTSНе включать сертификат подписанта при подписи; получатель должен получить его отдельно.
PKCS7_NOATTRНе добавлять обычные подписанные атрибуты, включая время и список алгоритмов. Выбирается только согласованным форматом.
PKCS7_DETACHEDОтделённая подпись; в S/MIME — multipart/signed. У PKCS7 это default, у cms_sign фактический default flags=0, значит attached. Оговорка в таблице CMS Manual, называющая default detached, не соответствует сигнатуре.
PKCS7_NOSIGSОтключить проверку подписей. Не используйте это для принятия документа.
PKCS7_NOCHAINНе использовать сертификаты из сообщения как промежуточные при построении цепочки.
PKCS7_NOOLDMIMETYPEС PHP 8.3: Content-Type application/pkcs7-mime вместо старого application/x-pkcs7-mime.
PKCS7_NOSMIMECAPС PHP 8.5: не включать SMIMECapabilities в подпись.
PKCS7_CRLFEOLС PHP 8.5: CRLF для строк вывода.
PKCS7_NOCRLС PHP 8.5: флаг управления CRL; таблица PHP Manual описывает невключение CRL в структуру. При проверке PKCS7 OpenSSL игнорирует внутренние CRL, если задано это значение; учитывайте конкретную операцию.
PKCS7_NO_DUAL_CONTENTС PHP 8.5: запрещает двойное содержимое при проверке, когда одновременно заданы внешние данные и вложенные. Это защита от неоднозначного выбора контента, а не универсальный способ сжать документ.

PHP Manual: openssl.pkcs7.flags

CMS: что меняют флаги контейнера

Побитовое| объединяет независимые параметры, например BINARY|DETACHED. Флаг может изменить формат либо отключить проверку: эти два последствия нельзя считать одинаковыми. Ни NOVERIFY, ни NOSIGS не нужны для наших успешно проверенных лабораторных подписей.

ФлагЭффект
OPENSSL_CMS_TEXTПри подписи/шифровании добавляет text/plain; проверка/расшифровка убирает эти заголовки и отказывает для иного MIME-типа.
OPENSSL_CMS_BINARYНе канонизирует строки CRLF; подпись относится к выбранным исходным байтам. Для S/MIME detached согласуйте представление строк.
OPENSSL_CMS_NOINTERNНе искать сертификат подписанта внутри сообщения; предоставьте его через untrusted_certificates_filename. Это не делает сертификат доверенным.
OPENSSL_CMS_NOVERIFYОтключает проверку сертификата подписанта. Оставляет опасную для обычного приёма область непроверенного доверия.
OPENSSL_CMS_NOCERTSНе включать сертификат подписанта при подписи; получатель должен получить его отдельно.
OPENSSL_CMS_NOATTRНе добавлять обычные подписанные атрибуты, включая время и список алгоритмов. Выбирается только согласованным форматом.
OPENSSL_CMS_DETACHEDОтделённая подпись; в S/MIME — multipart/signed. У PKCS7 это default, у cms_sign фактический default flags=0, значит attached. Оговорка в таблице CMS Manual, называющая default detached, не соответствует сигнатуре.
OPENSSL_CMS_NOSIGSОтключить проверку подписей. Не используйте это для принятия документа.
OPENSSL_CMS_OLDMIMETYPEС PHP 8.3: S/MIME Content-Type application/x-pkcs7-mime вместо современного application/pkcs7-mime.

PHP Manual: openssl.cms.flags

Идентификатор алгоритма — ещё не рекомендация его применять

Наличие константы в PHP не гарантирует доступность алгоритма у активного provider. Для современной подписи примеры выбирают SHA-256; старые MD/RC2/DES перечислены для полноты чтения существующих программ.

КонстантаСмысл
OPENSSL_ALGO_SHA1SHA-1 — исторический default sign/verify; явно выбирайте согласованный современный алгоритм.
OPENSSL_ALGO_SHA224
OPENSSL_ALGO_SHA256
OPENSSL_ALGO_SHA384
OPENSSL_ALGO_SHA512
Идентификаторы семейства SHA-2 для API, принимающих целочисленный algorithm.
OPENSSL_ALGO_RMD160RIPEMD-160 для совместимости.
OPENSSL_ALGO_MD5
OPENSSL_ALGO_MD4
Устаревшие дайджесты: не выбирайте для новых цифровых подписей.
OPENSSL_ALGO_DSS1Исторический DSA/SHA-1 идентификатор из Manual, отсутствует в нашей сборке.
OPENSSL_ALGO_MD2Условная историческая константа: нужна поддержка MD2 при сборке PHP/OpenSSL; Manual указывает -DHAVE_OPENSSL_MD2_H и enable-md2. В текущей лаборатории отсутствует.
OPENSSL_CIPHER_AES_128_CBC
OPENSSL_CIPHER_AES_192_CBC
OPENSSL_CIPHER_AES_256_CBC
Целочисленные идентификаторы AES-CBC для PKCS7/CMS. CBC не является аутентифицированным шифрованием.
OPENSSL_CIPHER_RC2_40
OPENSSL_CIPHER_RC2_64
OPENSSL_CIPHER_RC2_128
OPENSSL_CIPHER_DES
OPENSSL_CIPHER_3DES
Исторические контейнерные шифры, могут требовать legacy provider. Не используются в новых примерах.
OPENSSL_DEFAULT_STREAM_CIPHERSСтрока списка шифров потоков. Не формат cipher_algo для AES-функции и не гарантия настройки конкретного TLS-соединения.

PHP Manual: openssl.signature-algos PHP Manual: openssl.ciphers

Представление данных и версия библиотеки: отдельные настройки

У строкового encrypt есть режим Base64 и сырые байты; у CMS есть DER/PEM/S/MIME. Это разные уровни представления. Точное восстановление байтов и проверка тега важнее того, насколько строка похожа на случайную.

КонстантаДействие
OPENSSL_RAW_DATAencrypt/decrypt работают с сырыми байтами вместо встроенного Base64.
OPENSSL_ZERO_PADDINGОтключает стандартное блочное дополнение. Название не означает«добавить нули»: длина должна быть кратна размеру блока, иначе отказ.
OPENSSL_DONT_ZERO_PAD_KEYЗапретить дополнение короткого ключа нулями; это не KDF и не проверка пароля.
OPENSSL_ENCODING_SMIME
OPENSSL_ENCODING_DER
OPENSSL_ENCODING_PEM
Одна кодировка CMS: почтовая оболочка, бинарная DER или текстовая PEM.
OPENSSL_VERSION_TEXTСтрока версии подключённой библиотеки, показанная в диагностике выше.
OPENSSL_VERSION_NUMBERЧисловая версия OpenSSL; доступность возможностей дополнительно проверяют по API/provider.
OPENSSL_TLSEXT_SERVER_NAMEПоддержка SNI в сборке; нужна OpenSSL≥0.9.8j. SNI передаёт имя сервера при рукопожатии, но не равняется проверке имени в сертификате.

PHP Manual: openssl.constants.other PHP Manual: openssl.constversion PHP Manual: openssl.constsni

OpenSSLAsymmetricKey: ключ в памяти и PEM-файл — разные вещи

Подписывающий сервис читает закрытый PEM один раз и использует полученный объект в нескольких операциях. PEM — текстовая оболочка для бинарной структуры, с BEGIN/END и строками Base64. OpenSSLAsymmetricKey — непрозрачный объект в памяти PHP: открытых свойств и методов у него нет. Получить его можно через pkey_new/get_private/get_public. Создавать собственный наследник или доставать «секретное свойство» не требуется.

Жизнь ключа издателя
  1. publisher-private.pem → openssl_pkey_get_private
  2. Объект $key передан openssl_sign для накладной A1
  3. openssl_pkey_get_details → только выбранная открытая часть для получателя
  4. pkey_export → отдельно сохранённый PEM; объект сам ничего не пишет
  5. Все ссылки отпущены → PHP освобождает объект
Получить рабочий объект, затем подписать номер накладнойСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
if ($key === false) { throw new RuntimeException('Не удалось открыть ключ издателя'); }
if (!openssl_sign('A1', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
$public = openssl_pkey_get_details($key)['key'];
echo openssl_verify('A1', $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "Ключ издателя готов: A1 подписан\n" : "Проверка не пройдена\n";
Ожидаемый результат
Ключ издателя готов: A1 подписан

Объект не печатается: видимый результат — подписанный и проверенный номер. final запрещает наследование. Этот класс заменил ресурс OpenSSL key в PHP 8.0; is_resource для нового PHP не является проверкой успеха.

Что передать функцииОткуда взять
Объект OpenSSLAsymmetricKeyРезультат pkey_new, get_private либо get_public.
PEM-строкуСодержимое файла ключа, прочитанное file_get_contents. Обычный путь без file:// не является PEM-строкой.
file:// + полный путьOpenSSL сам читает PEM из указанного файла; public reader принимает и сертификат с публичной частью.
[закрытый PEM или file://путь, пароль]Форма пары для функций, принимающих зашифрованный закрытый ключ; у get_private есть отдельный passphrase.
OpenSSLCertificateСертификат содержит открытый ключ; закрытой части в сертификате нет.

В учебных архивах ключи специально опубликованы. Настоящие закрытые ключи хранят вне доступной через веб папки, ограничивают доступ и не пишут в журналы. unset убирает PHP-ссылку, но не обещает криптографическое обнуление всех копий байтов. Для сервиса важны и права на файл, и политика резервных копий.

PHP Manual: class.opensslasymmetrickey PHP Manual: openssl.certparams PHP Manual: openssl.resources

Скрыть заметку курьеру и получить отказ при подмене

Нужно сохранить заметку «Встреча: склад Север, 10:00» так, чтобы её мог прочитать обладатель секретного ключа. Выберем AES-256-GCM: этот режим скрывает содержимое и проверяет целостность. AEAD означает шифрование с аутентификацией и дополнительными данными; здесь аутентификация означает проверку знания общего ключа, а не установление личности пользователя.

Как заметка проходит AES-GCM
  1. Ключ: 32 случайных байта, остаётся секретным
  2. IV/nonce: 12 новых случайных байтов для этого шифрования
  3. Текст заметки → ciphertext + отдельный tag
  4. Получатель использует те же key, IV, tag и AAD
  5. Успех → исходная заметка; изменение → false, текст не используется

IV — начальное значение, для GCM его также называют nonce. Для одного ключа оно не должно повторяться; не фиксируйте нули и не вычисляйте его только из времени. IV и tag передают вместе с шифротекстом, они не обязаны быть секретными. В первом примере ключ новый при каждом запуске, IV создаётся отдельно. Для большого числа сообщений долгоживущего ключа правила уникальности nonce и ротации задаёт готовый протокол.

random_bytes(32) получает 32 криптографически случайных байта: число256 в AES-256 измеряется в битах. OPENSSL_RAW_DATA просит вернуть бинарные байты, а не Base64. Шестой аргумент $tag — выходная переменная: функция сама заполнит её. Мы не придумываем tag и не создаём новый IV при расшифровке.

Зашифровать заметку и получить её исходный текстСамостоятельный пример
<?php
declare(strict_types=1);

$key = random_bytes(32);
$iv = random_bytes(12);
$encrypted = openssl_encrypt('Встреча: склад Север, 10:00', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($encrypted === false) { throw new RuntimeException('Сообщение не зашифровано'); }
$plain = openssl_decrypt($encrypted, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) { throw new RuntimeException('Сообщение не прошло проверку'); }
echo $plain, "\n";
Ожидаемый результат
Встреча: склад Север, 10:00

Шифротекст не выводится: при правильной случайности его байты меняются. Проверяем полезный результат — полученный исходный текст. false проверяется строго, поскольку пустая строка тоже может быть корректным сообщением.

Параметр PHP называется passphrase, но внутри openssl_encrypt нет превращения пользовательского пароля в стойкий ключ: слишком короткое значение дополняется нулями, слишком длинное обрезается. Здесь передан ключ точного размера. Для парольного файла нужен отдельный KDF с солью и согласованными параметрами либо готовая библиотека формата.

Запрос на сертификат: что подготовить для удостоверяющего центра

У сервиса уже есть ключ. Чтобы другой участник получил удостоверенное соответствие имени и открытого ключа, сервис готовит CSR — запрос на выпуск сертификата. В запросе находятся имя субъекта, открытая часть и подпись запроса. Закрытый ключ центру не передают. CSR сам по себе не является сертификатом и не подтверждает право на указанное имя.

Заявка catalog.example.test
  1. Локальный закрытый ключ подписывает CSR
  2. CSR содержит открытый ключ + запрошенное имя catalog.example.test
  3. Центр проверяет право на имя и свою политику выпуска
  4. После разрешения центр создаёт X.509-сертификат
  5. Получатель проверяет цепочку к известному CA и нужное назначение

OpenSSLCertificateSigningRequest — final непрозрачный класс без публичных методов/свойств, заменивший ресурс OpenSSL X.509 CSR в PHP 8.0. Объект получают из csr_new; его читают функциями csr_get_subject/get_public_key и экспортируют в PEM. Для DNS-имён современного TLS нужен SAN (subjectAltName); один commonName не заменяет SAN.

Форма аргумента CSRСодержание
ОбъектOpenSSLCertificateSigningRequest от csr_new.
PEM-строкаТекст BEGIN CERTIFICATE REQUEST из экспорта.
file://полный/путьАдрес PEM-файла запроса; обычное имя файла без file:// не означает чтение.

PHP Manual: class.opensslcertificatesigningrequest PHP Manual: openssl.certparams

OpenSSLCertificate: прочитать удостоверение и проверить доверие — два шага

Сертификат X.509 связывает открытый ключ с именем и другими полями, а издатель подписывает эту структуру своим ключом. Владелец нашего файла — catalog.example.test, издатель — PHP Manual Lab CA. Нужны разные проверки: разбор полей, подпись издателя, путь к заранее доверенному CA, срок, назначение и имя ожидаемого сервера.

Границы операций
  1. x509_read: из PEM получен объект
  2. x509_parse: прочитаны владелец/издатель/даты/SAN
  3. x509_verify: проверена подпись известным ключом издателя
  4. x509_checkpurpose: локальная цепочка и назначение относительно выбранного CA
  5. Настоящий TLS-клиент дополнительно проверяет peer name и сам handshake

OpenSSLCertificate — final непрозрачный класс без публичных методов и свойств, вместо ресурса OpenSSL X.509 с PHP 8.0. Объект получают чтением или выпуском. В нашей лаборатории CA передаётся только отдельным проверкам: ни Windows, ни браузеру доверенный центр не устанавливается.

Открыть сертификат и показать, кому он выданСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = openssl_x509_read(file_get_contents(__DIR__ . '/publisher-cert.pem'));
if ($certificate === false) { throw new RuntimeException('Сертификат не прочитан'); }
$info = openssl_x509_parse($certificate);
echo 'Владелец: ', $info['subject']['CN'], "\n";
echo 'Издатель: ', $info['issuer']['CN'], "\n";
Ожидаемый результат
Владелец: catalog.example.test
Издатель: PHP Manual Lab CA

Это чтение полей сертификата: сам факт успешного разбора ещё не говорит о доверии, сроке, назначении или доступности TLS-сервера.

ca_info — массив путей к доверенным CA-файлам/каталогам. Каталог должен иметь хешированную структуру OpenSSL. Промежуточный сертификат из untrusted-файла помогает построить цепочку, но от этого не становится её доверенным корнем.

PHP Manual: class.opensslcertificate PHP Manual: openssl.cert.verification PHP Manual: openssl.certparams

Передать короткое значение RSA: почему большой файл требует другого подхода

Открытый ключ получателя позволяет зашифровать короткое значение; соответствующий закрытый ключ позволяет его восстановить. RSA не обрабатывает произвольный файл целиком. Padding — согласованная схема подготовки блока перед RSA. В примерах PHP 8.5 явно выбран OAEP с SHA-256; отправитель и получатель должны согласовать оба параметра.

Для RSA2048 и OAEP/SHA-256 предел исходного блока составляет 190 байт:256−2×32−2. Это байты, а не русские буквы. Не режьте большой документ на самостоятельные RSA-блоки: используйте предусмотренный протоколом контейнер или библиотеку гибридного шифрования.

Шифрование открытым ключом не доказывает автора: открытый ключ доступен каждому. Операции private_encrypt/public_decrypt ниже сохранены для понимания старого формата; для современной подписи нужен sign/verify, где алгоритм подписи и хеширование заданы целиком.

Один документ, два получателя: что находится в цифровом конверте

Передача складского задания
  1. Данные шифруются одним случайным симметричным ключом
  2. Для каждого адресата RSA шифрует свою копию этого ключа
  3. Отправляются ciphertext + IV + зашифрованный ключ нужного адресата
  4. Адресат открывает свою копию ключа и восстанавливает документ

seal/open — старый интерфейс такого конверта. Он не возвращает отдельный тег аутентификации. Ни AES-CBC, ни успешное раскрытие сами по себе не подтверждают неизменность и автора. Примеры показывают механику совместимости в лаборатории; для реального обмена используйте согласованный защищённый протокол/контейнер с проверкой отправителя.

Согласование секрета: стороны обмениваются открытыми значениями

Алиса иБоб хотят получить общий секрет, не отправляя его напрямую. Каждый хранит свой закрытый ключ и получает открытую часть другого. DH работает в конечной группе, ECDH — на эллиптической кривой. Параметры сторон должны совпадать; для DH архив использует стандартную ffdhe2048 из RFC7919, а не придуманные небольшие числа.

Общий результат ещё не является полноценным защищённым соединением. Протокол должен удостоверить собеседника, применить согласованную KDF для ключей нужного назначения и учесть повторные сообщения. Для веб-клиента эту работу делает TLS. Пример ниже показывает согласование, без заявления оготовом сетевом протоколе.

Если протокол требует Ed25519 или X25519

Похожие числа в именах не делают ключи взаимозаменяемыми. Ed25519 используется для подписи, X25519 — для согласования секрета. Получатель должен поддерживать выбранный протокол. Сначала покажем подпись записи об остатке, затем отдельно согласование; в этих примерах нужен PHP 8.5 с OpenSSL 3.

Подписать изменение остатка алгоритмом Ed25519Самостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_new(['config' => __DIR__ . '/openssl.cnf', 'private_key_type' => OPENSSL_KEYTYPE_ED25519]);
if ($key === false || !openssl_sign('A1: остаток 4', $signature, $key, 0)) { throw new RuntimeException('Ed25519-подпись не создана'); }
$public = openssl_pkey_get_details($key)['key'];
echo openssl_verify('A1: остаток 4', $signature, $public, 0) === 1
    ? "Ed25519: остаток A1 подтверждён\n" : "Изменение отклонено\n";
Ожидаемый результат
Ed25519: остаток A1 подтверждён

Тип ключа задаёт Ed25519. algorithm=0 означает отсутствие отдельного внешнего дайджеста; нельзя механически подставлять SHA-256 из RSA-примера. Подпись проверена настоящим вызовом.

Согласовать секрет двумя ключами X25519Самостоятельный пример
<?php
declare(strict_types=1);

$options = ['config' => __DIR__ . '/openssl.cnf', 'private_key_type' => OPENSSL_KEYTYPE_X25519];
$alice = openssl_pkey_new($options); $bob = openssl_pkey_new($options);
if ($alice === false || $bob === false) { throw new RuntimeException('Ключи не созданы'); }
$atAlice = openssl_pkey_derive(openssl_pkey_get_details($bob)['key'], $alice);
$atBob = openssl_pkey_derive(openssl_pkey_get_details($alice)['key'], $bob);
if ($atAlice === false || $atBob === false) { throw new RuntimeException('Согласование не выполнено'); }
echo hash_equals($atAlice, $atBob) ? "X25519: стороны согласовали общий секрет\n" : "Согласование нарушено\n";
Ожидаемый результат
X25519: стороны согласовали общий секрет

Здесь другая операция и другой тип ключа. Согласование всё ещё требует аутентификации собеседника и протокольной KDF; успешное совпадение не заявлено как готовый TLS.

PKCS#12: перенести сертификат вместе с закрытым ключом

Одного сертификата мало, чтобы новый сервис мог подписывать документы: нужна соответствующая закрытая часть. PKCS#12, часто с расширением .p12 или .pfx, объединяет ключ, сертификат и дополнительные сертификаты. Пароль защищает контейнер при хранении и переносе. Получивший контейнер и пароль сможет использовать закрытый ключ.

В этих архивах пароль lab-password и ключи намеренно открыты. В рабочем переносе контейнер и пароль передают по согласованным защищённым каналам, файл не кладут в публичный каталог. Чтение контейнера не означает автоматического доверия всей вложенной цепочке.

SPKAC: старый запрос с открытым ключом и проверочной строкой

При регистрации ключа сервер может выдать challenge — одноразовую проверочную строку. В старом формате SPKAC клиент подписывает структуру с этой строкой и своим открытым ключом. Это отличается от обычного PEM SubjectPublicKeyInfo: здесь есть challenge и подпись. Функции spki_* обслуживают этот исторический формат.

Успешная подпись структуры не доказывает имя пользователя. Сервер отдельно сверяет challenge с ожидаемым значением своей регистрации, сроком и фактом однократного использования. Устаревший HTML keygen из старых примеров не является современным способом регистрации; для выдачи сертификата обычно применяют CSR и установленный процесс удостоверения.

Пароль входа: сохранить проверочное значение, а не расшифровываемый пароль

Для входа в кабинет пароль не требуется восстанавливать. При регистрации сохраняют специальный медленный хеш, при входе проверяют введённую строку. Argon2 требует памяти и вычислений, затрудняя массовый перебор. Эти функции появились в PHP 8.4, если OpenSSL собран с поддержкой Argon2; они не заменяют защиту сессии, ограничение попыток и HTTPS.

Обычный код приложения чаще использует password_hash/password_verify. Вариант openssl_password_* нужен, когда сознательно выбран backend OpenSSL. Название алгоритма здесь передаётся первым аргументом строкой; не переносите порядок аргументов из обычного password_hash.

От накладной к подписанному файлу: S/MIME, DER и PEM

Ранее подпись хранилась отдельной бинарной строкой. В обмене документами часто требуется стандартный контейнер: в нём передаются подпись, сведения об алгоритмах и сертификаты. PKCS7 и его развитие CMS описывают такие структуры. S/MIME добавляет почтовую MIME-оболочку; DER — бинарная запись структуры; PEM — её текстовая Base64-оболочка с BEGIN/END.

Файл лабораторииЧто в нёмЧем прочитать
invoice.signed.emlПодписанная накладная в S/MIME, подпись отделена внутри MIME-сообщения.openssl_pkcs7_verify
invoice.cms.pemCMS SignedData с вложенной накладной в PEM.openssl_cms_verify с OPENSSL_ENCODING_PEM
invoice.detached.derТолько отделённая CMS-подпись в DER; исходная накладная нужна отдельно.openssl_cms_verify с DE Т ACHED, sigfile и DER
certificates.p7b.pemPEM PKCS7, из которого можно извлечь сертификаты.openssl_pkcs7_read, затем отдельно проверять доверие

Расширение .p7b или .p7m не определяет кодировку однозначно. Ошибка no content type часто означает, что DER отдали S/MIME-парсеру. Здесь выбран явный формат, а не отключена проверка подписи. Файлы остаются локальными; ни один пример не отправляет почту.

Подпись может быть attached: документ внутри контейнера, либо detached: документ хранится отдельно. BINARY сохраняет исходные байты без канонизации строк CRLF. Текстовый режим нужен только если обе стороны согласовали правила преобразования; иначе подпись относится уже к другим байтам.

Законченный локальный обмен: подписать, адресовать складу, проверить после открытия

Требование выросло: склад должен прочитать накладную, убедиться в её неизменности и увидеть сертификат подписанта. Сначала издатель создаёт подписанный CMS в PEM. Затем этот файл шифруется для склада. Склад открывает внешний контейнер, проверяет внутреннюю подпись относительно известного CA и только после этого использует текст.

Четыре разных действия
  1. invoice.txt → подпись ключом издателя → signed.pem
  2. signed.pem → шифрование для сертификата склада → delivery.pem
  3. delivery.pem → закрытый ключ склада → received.pem
  4. received.pem → подпись и Lab CA → verified.txt
Пройти всю цепочку на накладной A1Самостоятельный пример
<?php
declare(strict_types=1);

$publisher = file_get_contents(__DIR__ . '/publisher-cert.pem');
$publisherKey = file_get_contents(__DIR__ . '/publisher-private.pem');
$warehouse = file_get_contents(__DIR__ . '/recipient-cert.pem');
$warehouseKey = file_get_contents(__DIR__ . '/recipient-private.pem');
if (!openssl_cms_sign(__DIR__ . '/invoice.txt', __DIR__ . '/signed.pem', $publisher, $publisherKey, [], OPENSSL_CMS_BINARY, OPENSSL_ENCODING_PEM)) { throw new RuntimeException('Подпись не создана'); }
if (!openssl_cms_encrypt(__DIR__ . '/signed.pem', __DIR__ . '/delivery.pem', $warehouse, [], OPENSSL_CMS_BINARY, OPENSSL_ENCODING_PEM, OPENSSL_CIPHER_AES_256_CBC)) { throw new RuntimeException('Шифрование не выполнено'); }
if (!openssl_cms_decrypt(__DIR__ . '/delivery.pem', __DIR__ . '/received.pem', $warehouse, $warehouseKey, OPENSSL_ENCODING_PEM)) { throw new RuntimeException('Контейнер не открыт'); }
if (!openssl_cms_verify(__DIR__ . '/received.pem', OPENSSL_CMS_BINARY, __DIR__ . '/signer.pem', [__DIR__ . '/lab-ca.pem'], null, __DIR__ . '/verified.txt', null, null, OPENSSL_ENCODING_PEM)) { throw new RuntimeException('Подпись не принята'); }
$signer = file_get_contents(__DIR__ . '/signer.pem');
if (openssl_x509_fingerprint($signer, 'sha256') !== openssl_x509_fingerprint($publisher, 'sha256')) { throw new RuntimeException('Другой подписант'); }
echo 'Склад принял от ', openssl_x509_parse($signer)['subject']['CN'], ': ', trim(file_get_contents(__DIR__ . '/verified.txt')), "\n";
Ожидаемый результат
Склад принял от catalog.example.test: Заказ A1: 1200 RUB

Каждый механизм уже разобран отдельно выше. Дополнительная сверка сертификата ограничивает подписанта конкретным известным издателем; в реальном процессе эта политика должна учитывать перевыпуск сертификатов. Все операции выполнены над локальными файлами, почта/TLS здесь не тестировались.

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

Разобрать отказ по наблюдаемому этапу

Сначала зафиксируйте, что не произошло: не прочитан ключ, не разобран контейнер, не подошла подпись, не построилась цепочка или не расшифровались данные. Эти отказы требуют разных исправлений. Одна строка из очереди OpenSSL без результата конкретного вызова не показывает весь путь.

Реальный вопросПричина для проверки и следующий шаг
DER получает ошибку no content type у S/MIME-парсераВопрос сравнивает PHP с CLI -inform der. Современный собственный разбор выбирает CMS encoding DER и сохраняет проверку доверия; shell-код ответа и NOVERIFY не перенесены. openssl_cms_verify(), openssl_pkcs7_verify()
Своя подпись создана, но сертификат не признан довереннымИз вопроса выделены разные проверки: подпись, назначение и цепочка доверия. Наша лаборатория выпускает листовой сертификат CA с emailProtection и передаёт отдельный доверенный CA в verify. Совет ответа отключить проверку не принят. openssl_pkcs7_verify(), openssl_x509_checkpurpose()
GCM работает в одном запуске и отказывает в отдельном расшифрованииВопрос теряет исходный IV/tag. Собственный JSON-пример сохраняет ciphertext, IV и tag, фиксирует AAD и проверяет длину тега; ключ хранится отдельно. openssl_encrypt(), openssl_decrypt()
PHP не получает отдельный тег из сообщения другого языкаПостановка содержит IV и объединённые данные. Объясняем, что Base64 не определяет структуру пакета: получатель обязан знать границы IV/ciphertext/tag. Наш полностью локальный пример использует явные JSON-поля; межъязыковой запуск JavaScript не заявлен. openssl_decrypt()
Старый PKCS12 перестал открываться после PHP 5.6 → PHP 8.2 на WindowsВопрос прочитан целиком: unsupported и чтение cert/pkey после неуспеха. Отделяем неверный пароль от доступности старых алгоритмов OpenSSL 3. Собственные PKCS12 примеры проверяют bool и демонстрируют современный экспорт/повторное чтение. Откат PHP из ответа не преподаётся как решение. openssl_pkcs12_read(), openssl_pkcs12_export()
Ключ создаётся, но очередь содержит сообщения конфигурацииПрочитаны вопрос и приведённая конфигурация. Возвращаемое значение операции отделено от очереди ошибок; используем явный локальный config и проверяем реальную подпись. Исторические сообщения NCONF не выдаются за гарантированное поведение PHP 8.5; исходники optional config чтения сверены. openssl_pkey_new(), openssl_error_string()
Числовой результат проверки принимают за bool, а старую ошибку — за причинуВопрос содержит ECDSA-подпись и PEM. Наше объяснение не диагностирует непроверенные чужие ключи: последовательно декодируем формат, читаем ключ, сравниваем verify строго с 1 и отдельно обрабатываем 0/ошибку. Ни чужие ключи, ни код ответа не исполнялись. openssl_verify(), openssl_error_string()

Страницы Stack Overflow использованы как постановки практических проблем. Нормативные правила сверены с PHP Manual, исходниками PHP/OpenSSL и RFC; примеры написаны заново, чужие фрагменты не запускались.

Уточнения документации, которые влияют на правильный вызов

МестоЧто использовать в проверенной сборке
CSR optionsconfig_section_name, а не опечатку config_section_section.
cms_verifycertificates и content — выходные пути; sigfile — вход отделённой подписи. pk7 пишется в ветке указанного файла подписантов.
cms_readНесмотря на имя input_filename, аргумент содержит PEM-данные, а не путь.
CMS DETACHEDФактический default cms_sign — flags=0, attached. Детач явно задаётся константой.
dh_compute_keyВ PHP 8.5.11 принимает DH и отвергает EC; ECDH работает через pkey_derive.
x509_check_private_keyСопоставляет публичные компоненты, но обёртка PHP 8.5.11 отвергает явно переданный public-only объект.
PKCS7 NO_DUAL_CONTENT/NOCRLЭффект зависит от проверяющей операции; первый запрещает неоднозначное одновременное вложенное/внешнее содержимое, второй исключает внутренние CRL из проверки.

Эти уточнения не скрывают исходную оговорку Manual: она приведена рядом с наблюдением и ограничена проверенной версией. Первичные источники: Уточнены роли CMS verify файлов, cms_read принимает содержимое, dh_compute_key ограничен DH, x509_check_private_key сначала требует private key, Правильное имя options config_section_name и обработка необязательных параметров config, Границы RSA-блока, OAEP, запрет прямого сырого RSA, риски PKCS1 v1, Разделены содержимое, подпись, поиск подписанта и цепочка; выходной контент может записываться до окончательного результата, Первичная реализация NO_DUAL_CONTENT и NOCRL уточняет сокращённую таблицу Manual, Публичные стандартные параметры ffdhe2048, g=2; никаких придуманных небольших DH-групп.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

openssl_signСнабдить выгрузку подписью, чтобы получатель заметил изменение и проверил известный ключ поставщика.

PHP 8.2+ · openssl

openssl_sign( string $data, string &$signature, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0): bool

Снабдить выгрузку подписью, чтобы получатель заметил изменение и проверил известный ключ поставщика.

Четыре аргумента первого вызова: байты накладной; переменная для новой подписи; закрытый PEM-ключ; алгоритм SHA-256. Амперсанд в справочной сигнатуре означает выходной аргумент, в самом вызове его писать не нужно. if с ! прекращает обработку при false; throw сообщает отказ.

Подписать накладную и проверить её известным ключомСамостоятельный пример
<?php
declare(strict_types=1);

$invoice = file_get_contents(__DIR__ . '/invoice.txt');
$privateKey = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_sign($invoice, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
    throw new RuntimeException('Не удалось подписать накладную');
}
$publicKey = file_get_contents(__DIR__ . '/publisher-public.pem');
echo openssl_verify($invoice, $signature, $publicKey, OPENSSL_ALGO_SHA256) === 1
    ? "Накладная подписана: Заказ A1: 1200 RUB\n" : "Проверка не пройдена\n";
Ожидаемый результат
Накладная подписана: Заказ A1: 1200 RUB

Функция записывает подпись в переменную $signature; её не нужно создавать заранее. Возвращаемое bool говорит об успехе самой операции. Текст накладной остаётся исходным. Закрытый fixture-ключ опубликован только для воспроизводимого урока.

ПараметрДоговор
dataТочные байты подписываемой строки; кодировка и переводы строк входят в эти байты.
signatureВыход по ссылке: сырая двоичная подпись. Base64 для файла/JSON применяет приложение.
private_keyОбъект OpenSSLAsymmetricKey, закрытый PEM, путь file:// или пара [ключ, пароль]; допустимые формы подробно разобраны в разделе ключей.
algorithmКонстанта дайджеста либо имя из openssl_get_md_methods; default — SHA-1, в примерах явно SHA-256. Значение 0 для алгоритмов без внешнего дайджеста доступно со сборкой OpenSSL 3+.
paddingС PHP 8.5: 0, OPENSSL_PKCS1_PADDING либо OPENSSL_PKCS1_PSS_PADDING. Это настройка RSA, она должна совпадать с проверкой.

Результат и отказ. true означает, что подпись записана в signature; false — операция не выполнена. Подписанный документ при этом не шифруется.

Сохранить RSA-PSS-подпись и проверить её после чтенияСамостоятельный пример
<?php
declare(strict_types=1);

$invoice = file_get_contents(__DIR__ . '/invoice.txt');
$privateKey = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_sign($invoice, $signature, $privateKey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING)) {
    throw new RuntimeException('Подпись не создана');
}
if (file_put_contents(__DIR__ . '/invoice.pss.b64', base64_encode($signature)) === false) {
    throw new RuntimeException('Подпись не сохранена');
}
$stored = base64_decode(file_get_contents(__DIR__ . '/invoice.pss.b64'), true);
$publicKey = file_get_contents(__DIR__ . '/publisher-public.pem');
echo openssl_verify($invoice, $stored, $publicKey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING) === 1
    ? "invoice.pss.b64: сохранённая подпись подтверждает накладную A1\n" : "Подпись файла отклонена\n";
Ожидаемый результат
invoice.pss.b64: сохранённая подпись подтверждает накладную A1

PHP 8.5+ добавляет пятый аргумент padding. Обе стороны выбрали RSA-PSS и SHA-256. В файле лежит Base64, а openssl_verify получает декодированные байты. Сама PSS-подпись при повторном запуске может отличаться; проверяем её действие, не придумываем постоянную подпись.

Границы применения. Не подменяйте подпись private_encrypt: подпись имеет свой стандартизованный алгоритм и формат. Ключ не выводят в лог. Успешная локальная подпись не делает сертификат доверенным и не обеспечивает защиту от повторной отправки накладной.

Версии и совместимость. С PHP 8.0 вместо ресурсов принимаются объекты OpenSSL. Пятый параметр есть только с PHP 8.5; первый пример совместим с PHP 8.2+.

Следующий шаг: openssl_verify(), openssl_pkey_get_private().

openssl_verifyРешить, принять ли файл от владельца заранее известного открытого ключа.

PHP 8.2+ · openssl

openssl_verify( string $data, string $signature, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0): int|false

Решить, принять ли файл от владельца заранее известного открытого ключа.

Первый аргумент — исходные байты документа, второй — двоичная подпись, третий — открытый ключ, четвёртый — согласованный SHA-256. Результат проверяем через === 1, не через обычный if($result).

Проверить подпись поставщика по трём готовым файламСамостоятельный пример
<?php
declare(strict_types=1);

$invoice = file_get_contents(__DIR__ . '/invoice.txt');
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
$publicKey = file_get_contents(__DIR__ . '/publisher-public.pem');
$result = openssl_verify($invoice, $signature, $publicKey, OPENSSL_ALGO_SHA256);
echo $result === 1 ? "Принять накладную A1: подпись поставщика верна\n" : "Не принимать накладную\n";
Ожидаемый результат
Принять накладную A1: подпись поставщика верна

Возврат сравнивается строго с 1. Если подпись записана Base64, сначала декодируйте её. Открытый ключ должен быть получен независимо от недоверенного сообщения.

ПараметрДоговор
dataБайты, которые подписывались; изменение пробела/перевода строки тоже изменяет сообщение.
signatureСырая подпись от openssl_sign или совместимой реализации; не её Base64-строка.
public_keyСоответствующая открытая часть пары: объект, PEM или поддерживаемая форма ключа/сертификата. Проверка подписи не устанавливает доверие к происхождению этого ключа.
algorithmКонстанта либо строковое имя дайджеста; default SHA-1. Для алгоритмов без внешнего дайджеста 0 требует OpenSSL 3+.
paddingС PHP 8.5: 0/PKCS1/PSS; выбранные подписью параметры должны совпадать.

Результат и отказ. 1 — подпись подходит; 0 — не подходит; -1 либо false — ошибка проверки. Только 1 разрешает дальнейшую обработку.

Отделить исправную накладную от подменённой суммыСамостоятельный пример
<?php
declare(strict_types=1);

$invoice = file_get_contents(__DIR__ . '/invoice.txt');
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
$key = file_get_contents(__DIR__ . '/publisher-public.pem');
foreach (['Исходник' => $invoice, 'Сумма 9000' => str_replace('1200', '9000', $invoice)] as $label => $data) {
    $result = openssl_verify($data, $signature, $key, OPENSSL_ALGO_SHA256);
    if ($result === 1) { echo $label, ": принять\n"; }
    elseif ($result === 0) { echo $label, ": отклонить, подпись не подходит\n"; }
    else { throw new RuntimeException('Проверка подписи не выполнена'); }
}
Ожидаемый результат
Исходник: принять
Сумма 9000: отклонить, подпись не подходит

Та же подпись проверена по двум наборам байтов. 0 — математически неподходящая подпись; false/-1 — ошибка самой проверки. Не превращайте любой truthy результат в разрешение: -1 тоже truthy.

Границы применения. После успешной подписи отдельно проверяют содержание накладной, полномочия отправителя, номер/срок и повторы. Доверие сертификату и его назначение — отдельные проверки X.509.

Версии и совместимость. OpenSSL-объекты принимаются с PHP 8.0; padding добавлен в PHP 8.5.

Следующий шаг: openssl_sign(), openssl_error_string(), openssl_x509_checkpurpose().

openssl_pkey_newСоздать новую ключевую пару для подписи документов отдельного издателя.

PHP 8.2+ · openssl

openssl_pkey_new(?array $options = null): OpenSSLAsymmetricKey|false

Создать новую ключевую пару для подписи документов отдельного издателя.

options — массив настроек: ключи строковые, => связывает имя с выбранным значением. openssl_pkey_new возвращает объект пары; при === false работу прекращаем. Открытую часть извлекаем отдельно, закрытую не выводим.

Создать пару для нового издателя и подписать его документСамостоятельный пример
<?php
declare(strict_types=1);

$options = ['config' => __DIR__ . '/openssl.cnf', 'private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_bits' => 2048];

$key = openssl_pkey_new($options);
if ($key === false) { throw new RuntimeException('Ключ не создан'); }
if (!openssl_sign('Накладная A2', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
$public = openssl_pkey_get_details($key)['key'];
echo openssl_verify('Накладная A2', $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "Новая пара RSA: накладная A2 проверена\n" : "Проверка не пройдена\n";
Ожидаемый результат
Новая пара RSA: накладная A2 проверена

private_key_type выбирает RSA, private_key_bits — размер в битах. Настройки читаются из локального openssl.cnf, входящего в архив. Ключ создаётся заново при каждом запуске; нужный для следующих запусков ключ отдельно экспортируют.

ПараметрДоговор
optionsМассив или null; общие настройки/алгоритм-специфичные формы ниже. Нужен доступный корректный openssl.cnf; явный config делает учебный запуск переносимым.

Результат и отказ. OpenSSLAsymmetricKey при успехе; false при отказе. Сам вызов не записывает файлы и не выпускает сертификат.

Режим optionsПолный набор специальных полей
Общиеprivate_key_bits, private_key_type, curve_name, config; остальные общие CSR-настройки не являются настройками генератора ключа.
rsan (модуль, обязателен), d (закрытая степень, обязательна), e (открытая степень), p/q (простые), dmp1/dmq1 (d mod p−1/q−1), iqmp (q⁻¹ mod p). Значения — бинарные строки чисел, не десятичный текст.
dsap, q, g — параметры группы; priv_key/pub_key — закрытая/открытая компоненты. Все поля этой формы необязательны; библиотека определяет допустимые комбинации.
dhp, g — общий модуль и генератор; priv_key/pub_key — локальные компоненты. Для обмена стороны используют одинаковые параметры.
eccurve_name; p,a,b,seed,generator,g_x,g_y,cofactor,order — параметры кривой; x,y — открытая точка, d — закрытое число. Пользовательские параметры поддержаны с PHP 8.3.
x25519/ed25519/x448/ed448С PHP 8.4: вложенный массив priv_key/pub_key. Имена обозначают разные назначения: X — согласование секрета, Ed — подпись. Доступность зависит от OpenSSL.
Создать EC-ключ для согласованной подписи с SHA-256Самостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_new(['config' => __DIR__ . '/openssl.cnf', 'private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
if ($key === false || !openssl_sign('Остаток A1: 4', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('EC-подпись не создана'); }
$details = openssl_pkey_get_details($key);
echo 'Кривая издателя: ', $details['ec']['curve_name'], "\n";
echo openssl_verify('Остаток A1: 4', $signature, $details['key'], OPENSSL_ALGO_SHA256) === 1
    ? "Изменение остатка подтверждено\n" : "Проверка не пройдена\n";
Ожидаемый результат
Кривая издателя: prime256v1
Изменение остатка подтверждено

Кривая — именованный набор математических параметров, выбранный совместимым протоколом. Не заменяйте имя на произвольное без соглашения с получателем. Доступность имён показывает openssl_get_curve_names.

Границы применения. Ручной импорт математических параметров нужен для совместимости существующего формата, а не для изобретения своей кривой или простых чисел. Для повседневной генерации используйте поддерживаемый тип/именованную кривую.

Версии и совместимость. curve_name с PHP 7.1; объект вместо ресурса с PHP 8.0; собственные EC-параметры с 8.3; X25519/Ed25519/X448/Ed448 с 8.4.

Следующий шаг: openssl_pkey_get_details(), openssl_pkey_export(), openssl_get_curve_names().

openssl_pkey_get_publicПодготовить известный открытый ключ к проверке документов.

PHP 8.2+ · openssl

openssl_pkey_get_public(OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key): OpenSSLAsymmetricKey|false

Подготовить известный открытый ключ к проверке документов.

Первый аргумент — содержимое или явно обозначенный адрес PEM. Результат сохраняем в $key и сравниваем с false; успешный объект используем в следующей операции.

Подготовить открытый ключ и проверить накладнуюСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_public(file_get_contents(__DIR__ . '/publisher-public.pem'));

if ($key === false) { throw new RuntimeException('Открытый ключ не прочитан'); }
$data = file_get_contents(__DIR__ . '/invoice.txt');
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify($data, $signature, $key, OPENSSL_ALGO_SHA256) === 1
    ? "A1: проверено ключом из publisher-public.pem\n" : "Накладная отклонена\n";
Ожидаемый результат
A1: проверено ключом из publisher-public.pem

Объект можно повторно передавать проверяющим функциям. Наличие корректного PEM ещё не устанавливает личность владельца.

ПараметрДоговор
public_keyОбъект открытого ключа, PEM-строка либо file://путь к PEM-ключу/сертификату; OpenSSLCertificate также представляет открытую часть.

Результат и отказ. OpenSSLAsymmetricKey при успехе; false при невозможности разобрать ключ или открыть защищённую часть.

Извлечь открытую часть из сертификата издателяСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_public('file://' . __DIR__ . '/publisher-cert.pem');

if ($key === false) { throw new RuntimeException('Сертификат не дал открытый ключ'); }
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify(file_get_contents(__DIR__ . '/invoice.txt'), $signature, $key, OPENSSL_ALGO_SHA256) === 1
    ? "A1: подпись соответствует ключу сертификата\n" : "Накладная отклонена\n";
Ожидаемый результат
A1: подпись соответствует ключу сертификата

file:// отличает адрес PEM от его содержимого. Здесь извлечён ключ и проверена подпись; цепочка доверия, срок и назначение сертификата ещё не проверялись.

Границы применения. Не выводите весь key details для диагностики: там могут оказаться секретные компоненты. Извлечение публичной части сертификата не равно проверке его доверия.

Версии и совместимость. С PHP 8.0 возвращается объект вместо ресурса.

Следующий шаг: openssl_verify(), openssl_pkey_get_details().

openssl_get_publickeyПодготовить известный открытый ключ к проверке документов.

PHP 8.2+ · openssl

openssl_get_publickey(OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key): OpenSSLAsymmetricKey|false

Подготовить известный открытый ключ к проверке документов.

Это алиас openssl_pkey_get_public() с тем же поведением. Первый аргумент — содержимое или явно обозначенный адрес PEM. Результат сохраняем в $key и сравниваем с false; успешный объект используем в следующей операции.

Подготовить открытый ключ и проверить накладнуюСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_get_publickey(file_get_contents(__DIR__ . '/publisher-public.pem'));

if ($key === false) { throw new RuntimeException('Открытый ключ не прочитан'); }
$data = file_get_contents(__DIR__ . '/invoice.txt');
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify($data, $signature, $key, OPENSSL_ALGO_SHA256) === 1
    ? "A1: проверено ключом из publisher-public.pem\n" : "Накладная отклонена\n";
Ожидаемый результат
A1: проверено ключом из publisher-public.pem

Объект можно повторно передавать проверяющим функциям. Наличие корректного PEM ещё не устанавливает личность владельца.

ПараметрДоговор
public_keyОбъект открытого ключа, PEM-строка либо file://путь к PEM-ключу/сертификату; OpenSSLCertificate также представляет открытую часть.

Результат и отказ. OpenSSLAsymmetricKey при успехе; false при невозможности разобрать ключ или открыть защищённую часть.

Извлечь открытую часть из сертификата издателяСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_get_publickey('file://' . __DIR__ . '/publisher-cert.pem');

if ($key === false) { throw new RuntimeException('Сертификат не дал открытый ключ'); }
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify(file_get_contents(__DIR__ . '/invoice.txt'), $signature, $key, OPENSSL_ALGO_SHA256) === 1
    ? "A1: подпись соответствует ключу сертификата\n" : "Накладная отклонена\n";
Ожидаемый результат
A1: подпись соответствует ключу сертификата

file:// отличает адрес PEM от его содержимого. Здесь извлечён ключ и проверена подпись; цепочка доверия, срок и назначение сертификата ещё не проверялись.

Границы применения. Не выводите весь key details для диагностики: там могут оказаться секретные компоненты. Извлечение публичной части сертификата не равно проверке его доверия.

Версии и совместимость. С PHP 8.0 возвращается объект вместо ресурса. Алиас сохранён для чтения существующего кода.

Следующий шаг: openssl_verify(), openssl_pkey_get_details().

openssl_pkey_get_privateОткрыть сохранённый закрытый ключ для подписи.

PHP 8.2+ · openssl

openssl_pkey_get_private(OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, ?string $passphrase = null): OpenSSLAsymmetricKey|false

Открыть сохранённый закрытый ключ для подписи.

Первый аргумент — содержимое или явно обозначенный адрес PEM. Результат сохраняем в $key и сравниваем с false; успешный объект используем в следующей операции.

Прочитать ключ издателя и подписать следующий документСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));

if ($key === false) { throw new RuntimeException('Ключ издателя не прочитан'); }
if (!openssl_sign('Накладная A3', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
$public = file_get_contents(__DIR__ . '/publisher-public.pem');
echo openssl_verify('Накладная A3', $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "A3: ключ прочитан, документ подписан\n" : "Проверка не пройдена\n";
Ожидаемый результат
A3: ключ прочитан, документ подписан

Прочитан уже существующий ключ: генерация новой пары для каждого документа помешала бы получателю использовать прежний доверенный открытый ключ.

ПараметрДоговор
private_keyЗакрытый PEM, file://путь, объект или поддерживаемая форма ключа. Обычная строка пути без file:// не читается автоматически.
passphraseПароль защищённого PEM; null допустим с PHP 8.0, для незашифрованного ключа пароль не нужен.

Результат и отказ. OpenSSLAsymmetricKey при успехе; false при невозможности разобрать ключ или открыть защищённую часть.

Открыть защищённый PEM только подходящим паролемСамостоятельный пример
<?php
declare(strict_types=1);

$original = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkey_export($original, $protected, 'lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Экспорт не выполнен'); }
$wrong = openssl_pkey_get_private($protected, 'wrong-password');
$key = openssl_pkey_get_private($protected, 'lab-password');

echo $wrong === false ? "Чужой пароль: ключ не открыт\n" : "Неожиданный результат\n";
if ($key === false || !openssl_sign('A4', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo "Верный пароль: A4 подписан\n";
Ожидаемый результат
Чужой пароль: ключ не открыт
Верный пароль: A4 подписан

Пароль защищает PEM на хранении. После успешного чтения объект уже пригоден к подписи; передавать пароль каждой операции не нужно. lab-password — известный учебный пароль, не шаблон рабочего секрета.

Границы применения. Не выводите весь key details для диагностики: там могут оказаться секретные компоненты. Извлечение публичной части сертификата не равно проверке его доверия.

Версии и совместимость. С PHP 8.0 возвращается объект вместо ресурса.

Следующий шаг: openssl_sign(), openssl_pkey_get_details().

openssl_get_privatekeyОткрыть сохранённый закрытый ключ для подписи.

PHP 8.2+ · openssl

openssl_get_privatekey(OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, ?string $passphrase = null): OpenSSLAsymmetricKey|false

Открыть сохранённый закрытый ключ для подписи.

Это алиас openssl_pkey_get_private() с тем же поведением. Первый аргумент — содержимое или явно обозначенный адрес PEM. Результат сохраняем в $key и сравниваем с false; успешный объект используем в следующей операции.

Прочитать ключ издателя и подписать следующий документСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_get_privatekey(file_get_contents(__DIR__ . '/publisher-private.pem'));

if ($key === false) { throw new RuntimeException('Ключ издателя не прочитан'); }
if (!openssl_sign('Накладная A3', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
$public = file_get_contents(__DIR__ . '/publisher-public.pem');
echo openssl_verify('Накладная A3', $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "A3: ключ прочитан, документ подписан\n" : "Проверка не пройдена\n";
Ожидаемый результат
A3: ключ прочитан, документ подписан

Прочитан уже существующий ключ: генерация новой пары для каждого документа помешала бы получателю использовать прежний доверенный открытый ключ.

ПараметрДоговор
private_keyЗакрытый PEM, file://путь, объект или поддерживаемая форма ключа. Обычная строка пути без file:// не читается автоматически.
passphraseПароль защищённого PEM; null допустим с PHP 8.0, для незашифрованного ключа пароль не нужен.

Результат и отказ. OpenSSLAsymmetricKey при успехе; false при невозможности разобрать ключ или открыть защищённую часть.

Открыть защищённый PEM только подходящим паролемСамостоятельный пример
<?php
declare(strict_types=1);

$original = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkey_export($original, $protected, 'lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Экспорт не выполнен'); }
$wrong = openssl_get_privatekey($protected, 'wrong-password');
$key = openssl_get_privatekey($protected, 'lab-password');

echo $wrong === false ? "Чужой пароль: ключ не открыт\n" : "Неожиданный результат\n";
if ($key === false || !openssl_sign('A4', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo "Верный пароль: A4 подписан\n";
Ожидаемый результат
Чужой пароль: ключ не открыт
Верный пароль: A4 подписан

Пароль защищает PEM на хранении. После успешного чтения объект уже пригоден к подписи; передавать пароль каждой операции не нужно. lab-password — известный учебный пароль, не шаблон рабочего секрета.

Границы применения. Не выводите весь key details для диагностики: там могут оказаться секретные компоненты. Извлечение публичной части сертификата не равно проверке его доверия.

Версии и совместимость. С PHP 8.0 возвращается объект вместо ресурса. Алиас сохранён для чтения существующего кода.

Следующий шаг: openssl_sign(), openssl_pkey_get_details().

openssl_pkey_get_detailsПолучить открытую часть и параметры ключа для настройки получателя.

PHP 8.2+ · openssl

openssl_pkey_get_details(OpenSSLAsymmetricKey $key): array|false

Получить открытую часть и параметры ключа для настройки получателя.

Аргумент key — объект ключа, а результат — массив. Квадратные скобки [key], [bits], [type] читают конкретные поля; публичный PEM находится в key.

Сформировать карточку открытого ключа без секретных компонентСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
$details = openssl_pkey_get_details($key);
if ($details === false) { throw new RuntimeException('Параметры ключа не прочитаны'); }
echo 'Назначение: издатель каталога', "\n", 'Тип: ', $details['type'] === OPENSSL_KEYTYPE_RSA ? 'RSA' : 'другой', "\n";
echo 'Размер: ', $details['bits'], " бит\n";
echo strtok($details['key'], "\n"), "\n";
Ожидаемый результат
Назначение: издатель каталога
Тип: RSA
Размер: 2048 бит
-----BEGIN PUBLIC KEY-----

Показаны только отобранные несекретные поля. key — публичный PEM даже тогда, когда входной объект содержит закрытую часть. Длина/тип — проверка согласованного формата, не доказательство доверия.

ПараметрДоговор
keyОбъект OpenSSLAsymmetricKey, полученный созданием или чтением ключа.

Результат и отказ. Массив с bits, key, type либо false. type — OPENSSL_KEYTYPE_* или −1 для неизвестного типа. Набор дополнительных полей зависит от типа и наличия секретных частей.

Поля результатаЧто содержат
bits, key, typeРазмер в битах; публичный PEM; OPENSSL_KEYTYPE_* либо −1. Это общая часть результата.
rsan/e — открытые компоненты; d/p/q/dmp1/dmq1/iqmp — закрытые компоненты, если доступны. В отличие от options генератора, это описание уже существующего ключа.
dsap/q/g — параметры; pub_key и при наличии priv_key — компоненты ключа.
dhp/g — параметры, pub_key/priv_key — компоненты. Состав зависит от доступных данных ключа.
eccurve_name/curve_oid — идентификаторы кривой; x/y — открытая точка; d — закрытая компонента, если доступна.
x25519, ed25519, x448, ed448С PHP8.4: вложенные pub_key/priv_key соответствующего типа; закрытая компонента может отсутствовать у публичного объекта.
Сохранить public.pem для получателя и проверить накладнуюСамостоятельный пример
<?php
declare(strict_types=1);

$private = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
$details = openssl_pkey_get_details($private);
if ($details === false || file_put_contents(__DIR__ . '/public.pem', $details['key']) === false) { throw new RuntimeException('Публичный файл не сохранён'); }
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify(file_get_contents(__DIR__ . '/invoice.txt'), $signature, file_get_contents(__DIR__ . '/public.pem'), OPENSSL_ALGO_SHA256) === 1
    ? "public.pem: получатель проверил накладную A1\n" : "Накладная отклонена\n";
Ожидаемый результат
public.pem: получатель проверил накладную A1

В файл передана только key, не весь массив details и не экспорт закрытого ключа. Передачу public.pem по доверенному каналу организует приложение.

Границы применения. Дополнительные rsa/dsa/dh/ec/x25519/ed25519/x448/ed448 содержат математические компоненты; priv_key/d и другие закрытые поля нельзя публиковать. Не все перечисленные компоненты присутствуют всегда.

Версии и совместимость. Объект вместо ресурса с PHP 8.0; четыре Curve25519/448 поля добавлены в 8.4.

Следующий шаг: openssl_pkey_new(), openssl_pkey_get_public().

openssl_pkey_exportПолучить PEM-представление закрытого ключа для защищённого хранения.

PHP 8.2+ · openssl

openssl_pkey_export( OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $key, string &$output, ?string $passphrase = null, ?array $options = null): bool

Получить PEM-представление закрытого ключа для защищённого хранения.

key задаёт закрытую часть; output — выходная переменная по ссылке; passphrase защищает PEM; options выбирает локальный openssl.cnf. Проверяем bool до использования результата.

Экспортировать защищённый ключ и прочитать его сноваСамостоятельный пример
<?php
declare(strict_types=1);

$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkey_export($private, $pem, 'lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Ключ не экспортирован'); }

echo strtok($pem, "\n"), "\n";
$loaded = openssl_pkey_get_private($pem, 'lab-password');
if ($loaded === false || !openssl_sign('A5', $signature, $loaded, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Экспорт не удалось использовать'); }
echo "A5 подписан повторно прочитанным ключом\n";
Ожидаемый результат
-----BEGIN ENCRYPTED PRIVATE KEY-----
A5 подписан повторно прочитанным ключом

Видна оболочка защищённого PEM, но секретные байты не выводятся. Успешное повторное чтение проверено реальной подписью. Зашифрованная оболочка не заменяет права доступа к файлам.

ПараметрДоговор
keyЗакрытый ключ в допустимой объектной/PEM/file://форме; для защищённого входа возможна пара [ключ, пароль].
outputВыход по ссылке: PEM-строка. Вызов не создаёт файл сам.
passphraseНеобязательный пароль экспортируемого закрытого PEM; null оставляет его без такой защиты.
optionsНастройки экспорта/конфигурации как у openssl_csr_new; локальный config приложен к примеру.

Результат и отказ. true при успешном экспорте, false при отказе.

Изменить пароль хранения, сохранив пару ключейСамостоятельный пример
<?php
declare(strict_types=1);

$private = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
if (!openssl_pkey_export($private, $pem, 'lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Ключ не экспортирован'); }

$protectedKey = openssl_pkey_get_private($pem, 'lab-password');
if (!openssl_pkey_export($protectedKey, $rotated, 'new-lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Повторный экспорт не удался'); }

$loaded = openssl_pkey_get_private($rotated, 'new-lab-password');
if ($loaded === false || !openssl_sign('A6', $signature, $loaded, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Новый пароль не открыл ключ'); }
echo openssl_verify('A6', $signature, file_get_contents(__DIR__ . '/publisher-public.pem'), OPENSSL_ALGO_SHA256) === 1
    ? "Пароль хранения изменён; прежний открытый ключ проверяет A6\n" : "Ключи не соответствуют\n";
Ожидаемый результат
Пароль хранения изменён; прежний открытый ключ проверяет A6

Меняется пароль оболочки, а не математическая пара. Для настоящей ротации ключевой пары нужно создать новый ключ и согласованно обновить доверие у получателей. В примере используются отдельные файлы/переменные, рабочий ключ не перезаписывается.

Границы применения. Экспорт закрытого ключа отличается от передачи публичного details[key]. Используйте отдельную учебную папку; реальные ключи сохраняют вне web-root и проверяют права. Пароль из исходника примера не годится для рабочего хранилища.

Версии и совместимость. С PHP 8.0 key принимает объекты OpenSSL вместо прежних ресурсов.

Следующий шаг: openssl_pkey_get_private(), openssl_pkey_get_details().

openssl_pkey_export_to_fileСохранить закрытый ключ в отдельный PEM-файл для следующего запуска.

PHP 8.2+ · openssl

openssl_pkey_export_to_file( OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $key, string $output_filename, ?string $passphrase = null, ?array $options = null): bool

Сохранить закрытый ключ в отдельный PEM-файл для следующего запуска.

key задаёт закрытую часть; output_filename — путь создаваемого файла; passphrase защищает PEM; options выбирает локальный openssl.cnf. Проверяем bool до использования результата.

Экспортировать защищённый ключ и прочитать его сноваСамостоятельный пример
<?php
declare(strict_types=1);

$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkey_export_to_file($private, __DIR__ . '/protected.pem', 'lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Ключ не экспортирован'); }
$pem = file_get_contents(__DIR__ . '/protected.pem');

echo strtok($pem, "\n"), "\n";
$loaded = openssl_pkey_get_private($pem, 'lab-password');
if ($loaded === false || !openssl_sign('A5', $signature, $loaded, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Экспорт не удалось использовать'); }
echo "A5 подписан повторно прочитанным ключом\n";
Ожидаемый результат
-----BEGIN ENCRYPTED PRIVATE KEY-----
A5 подписан повторно прочитанным ключом

Видна оболочка защищённого PEM, но секретные байты не выводятся. Успешное повторное чтение проверено реальной подписью. Зашифрованная оболочка не заменяет права доступа к файлам.

ПараметрДоговор
keyЗакрытый ключ в допустимой объектной/PEM/file://форме; для защищённого входа возможна пара [ключ, пароль].
output_filenameПуть результата; операция пишет именно в этот файл.
passphraseНеобязательный пароль экспортируемого закрытого PEM; null оставляет его без такой защиты.
optionsНастройки экспорта/конфигурации как у openssl_csr_new; локальный config приложен к примеру.

Результат и отказ. true при успешном экспорте, false при отказе.

Изменить пароль хранения, сохранив пару ключейСамостоятельный пример
<?php
declare(strict_types=1);

$private = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
if (!openssl_pkey_export_to_file($private, __DIR__ . '/protected.pem', 'lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Ключ не экспортирован'); }
$pem = file_get_contents(__DIR__ . '/protected.pem');

$protectedKey = openssl_pkey_get_private($pem, 'lab-password');
if (!openssl_pkey_export_to_file($protectedKey, __DIR__ . '/rotated.pem', 'new-lab-password', ['config' => __DIR__ . '/openssl.cnf'])) { throw new RuntimeException('Повторный экспорт не удался'); }
$rotated = file_get_contents(__DIR__ . '/rotated.pem');

$loaded = openssl_pkey_get_private($rotated, 'new-lab-password');
if ($loaded === false || !openssl_sign('A6', $signature, $loaded, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Новый пароль не открыл ключ'); }
echo openssl_verify('A6', $signature, file_get_contents(__DIR__ . '/publisher-public.pem'), OPENSSL_ALGO_SHA256) === 1
    ? "Пароль хранения изменён; прежний открытый ключ проверяет A6\n" : "Ключи не соответствуют\n";
Ожидаемый результат
Пароль хранения изменён; прежний открытый ключ проверяет A6

Меняется пароль оболочки, а не математическая пара. Для настоящей ротации ключевой пары нужно создать новый ключ и согласованно обновить доверие у получателей. В примере используются отдельные файлы/переменные, рабочий ключ не перезаписывается.

Границы применения. Экспорт закрытого ключа отличается от передачи публичного details[key]. Используйте отдельную учебную папку; реальные ключи сохраняют вне web-root и проверяют права. Пароль из исходника примера не годится для рабочего хранилища.

Версии и совместимость. С PHP 8.0 key принимает объекты OpenSSL вместо прежних ресурсов.

Следующий шаг: openssl_pkey_get_private(), openssl_pkey_get_details().

openssl_pkey_freeПеренести старый код с ресурсами OpenSSL на автоматическое время жизни объектов PHP 8.

Устарело с PHP 8.0 · openssl

openssl_pkey_free(OpenSSLAsymmetricKey $key): void

Перенести старый код с ресурсами OpenSSL на автоматическое время жизни объектов PHP 8.

Старый вызов получает один объект key. В PHP 8 результат не нужен: функция ничего не делает. Обработчик E_DEPRECATED в первом примере нужен только для наблюдения миграционной проблемы.

Увидеть, почему старый free не запрещает новую подписьСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
set_error_handler(static function (int $level): bool {
    if ($level === E_DEPRECATED) { echo "Устаревший вызов: объект освобождается автоматически\n"; return true; }
    return false;
});
try { openssl_pkey_free($key); } finally { restore_error_handler(); }
if (!openssl_sign('A7', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo "После free объект всё ещё подписывает A7\n";
Ожидаемый результат
Устаревший вызов: объект освобождается автоматически
После free объект всё ещё подписывает A7

Это воспроизведение для миграции старого кода, не рекомендуемый жизненный цикл. Предупреждение перехвачено и объяснено. Начиная с PHP 8 вызов не закрывает объект и не отзывает ключ.

ПараметрДоговор
keyOpenSSLAsymmetricKey; до PHP 8 это был ресурс ключа.

Результат и отказ. void; с PHP 8.0 нет эффекта, функция устарела.

Окончить работу локальной функции без ручного freeСамостоятельный пример
<?php
declare(strict_types=1);

function signInvoice(string $text): string {
    $key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
    if ($key === false || !openssl_sign($text, $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
    return $signature;
}
$signature = signInvoice('A8');
echo openssl_verify('A8', $signature, file_get_contents(__DIR__ . '/publisher-public.pem'), OPENSSL_ALGO_SHA256) === 1
    ? "Функция завершилась; подпись A8 доступна получателю\n" : "Подпись не подходит\n";
Ожидаемый результат
Функция завершилась; подпись A8 доступна получателю

Локальная ссылка на key исчезает при выходе, а возвращённые байты подписи остаются. Если объект хранится ещё где-то, другой владелец продолжает им пользоваться. PHP управляет временем жизни ссылок автоматически.

Границы применения. Ни free, ни unset не отзывают опубликованный сертификат и не стирают приватный ключ с диска. Для удаления последней PHP-ссылки обычно достаточно области видимости; управление секретами требует отдельной политики.

Версии и совместимость. Оба имени оставлены ради полного API и чтения старого кода. Новый код не должен полагаться на их действие.

Следующий шаг: openssl_pkey_get_private().

openssl_free_keyПеренести старый код с ресурсами OpenSSL на автоматическое время жизни объектов PHP 8.

Устарело с PHP 8.0 · openssl

openssl_free_key(OpenSSLAsymmetricKey $key): void

Перенести старый код с ресурсами OpenSSL на автоматическое время жизни объектов PHP 8.

Старый вызов получает один объект key. В PHP 8 результат не нужен: функция ничего не делает. Обработчик E_DEPRECATED в первом примере нужен только для наблюдения миграционной проблемы.

Увидеть, почему старый free не запрещает новую подписьСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
set_error_handler(static function (int $level): bool {
    if ($level === E_DEPRECATED) { echo "Устаревший вызов: объект освобождается автоматически\n"; return true; }
    return false;
});
try { openssl_free_key($key); } finally { restore_error_handler(); }
if (!openssl_sign('A7', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo "После free объект всё ещё подписывает A7\n";
Ожидаемый результат
Устаревший вызов: объект освобождается автоматически
После free объект всё ещё подписывает A7

Это воспроизведение для миграции старого кода, не рекомендуемый жизненный цикл. Предупреждение перехвачено и объяснено. Начиная с PHP 8 вызов не закрывает объект и не отзывает ключ.

ПараметрДоговор
keyOpenSSLAsymmetricKey; до PHP 8 это был ресурс ключа.

Результат и отказ. void; с PHP 8.0 нет эффекта, функция устарела.

Окончить работу локальной функции без ручного freeСамостоятельный пример
<?php
declare(strict_types=1);

function signInvoice(string $text): string {
    $key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
    if ($key === false || !openssl_sign($text, $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
    return $signature;
}
$signature = signInvoice('A8');
echo openssl_verify('A8', $signature, file_get_contents(__DIR__ . '/publisher-public.pem'), OPENSSL_ALGO_SHA256) === 1
    ? "Функция завершилась; подпись A8 доступна получателю\n" : "Подпись не подходит\n";
Ожидаемый результат
Функция завершилась; подпись A8 доступна получателю

Локальная ссылка на key исчезает при выходе, а возвращённые байты подписи остаются. Если объект хранится ещё где-то, другой владелец продолжает им пользоваться. PHP управляет временем жизни ссылок автоматически.

Границы применения. Ни free, ни unset не отзывают опубликованный сертификат и не стирают приватный ключ с диска. Для удаления последней PHP-ссылки обычно достаточно области видимости; управление секретами требует отдельной политики.

Версии и совместимость. Оба имени оставлены ради полного API и чтения старого кода. Новый код не должен полагаться на их действие.

Следующий шаг: openssl_pkey_get_private().

openssl_encryptСкрыть содержимое сообщения и получить данные для проверки целостности у получателя.

PHP 8.2+ · openssl

openssl_encrypt( string $data, string $cipher_algo, string $passphrase, int $options = 0, string $iv = "", string &$tag = null, string $aad = "", int $tag_length = 16): string|false

Скрыть содержимое сообщения и получить данные для проверки целостности у получателя.

Первый вызов получает текст, aes-256-gcm,32-байтный ключ, RAW_DATA,12-байтный IV и выходную переменную tag. Первые пять значений мы передаём, шестое библиотека создаёт. Текст результата после обратной операции виден ниже.

Сохранить смысл заметки при шифровании и расшифровкеСамостоятельный пример
<?php
declare(strict_types=1);

$key = random_bytes(32);
$iv = random_bytes(12);
$encrypted = openssl_encrypt('Встреча: склад Север, 10:00', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($encrypted === false) { throw new RuntimeException('Сообщение не зашифровано'); }
$plain = openssl_decrypt($encrypted, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) { throw new RuntimeException('Сообщение не прошло проверку'); }
echo $plain, "\n";
Ожидаемый результат
Встреча: склад Север, 10:00

Вызов encrypt возвращает ciphertext, отдельно заполняя tag. Текст и параметры связываются одной операцией; получатель должен получить все необходимые несекретные части.

ПараметрДоговор
dataИсходная строка любых байтов; в decrypt — шифротекст, сырой при RAW_DATA либо Base64 без него.
cipher_algoИмя шифра из get_cipher_methods. Выбор должен соответствовать протоколу, доступность сама по себе не делает режим подходящим.
passphraseИменно ключевые байты. KDF не выполняется; неподходящая длина обычно дополняется NUL/обрезается. В примерах AES-256 получает ровно 32 байта.
optionsПобитовое объединение OPENSSL_RAW_DATA, OPENSSL_ZERO_PADDING, OPENSSL_DONT_ZERO_PAD_KEY. RAW_DATA определяет представление ciphertext; ZERO_PADDING отключает стандартное заполнение блока, а не «добавляет нули».
ivСогласованное начальное значение. Короткое дополняется NUL с Warning, длинное обрезается с Warning; приложение проверяет длину заранее.
tagAEAD-метка. В encrypt — выход по ссылке; в decrypt — обязательная для выбранного GCM-контракта входная строка байтов.
aadДополнительные аутентифицируемые байты: видимы, но защищены от изменения. Обе стороны передают точно одинаковые значения.
tag_lengthДлина создаваемой метки, default16. Для GCM PHP допускает 4…16; в учебном контракте всегда 16, уменьшение требует отдельного протокольного решения.

Результат и отказ. Шифротекст строкой либо false. Без RAW_DATA строка уже Base64. Неизвестный алгоритм и неподходящий IV вызывают Warning; не используйте частичный результат после отказа.

Сохранить зашифрованную заметку в JSON и прочитать её обратноСамостоятельный пример
<?php
declare(strict_types=1);

$key = random_bytes(32);
$iv = random_bytes(12);
$aad = 'delivery:A1';
$encrypted = openssl_encrypt('Курьер: вход со двора', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, $aad, 16);
if ($encrypted === false) { throw new RuntimeException('Шифрование не выполнено'); }
$package = ['iv' => base64_encode($iv), 'tag' => base64_encode($tag), 'ciphertext' => base64_encode($encrypted)];
if (file_put_contents(__DIR__ . '/delivery.json', json_encode($package, JSON_THROW_ON_ERROR)) === false) { throw new RuntimeException('Файл не записан'); }
$stored = json_decode(file_get_contents(__DIR__ . '/delivery.json'), true, flags: JSON_THROW_ON_ERROR);
$plain = openssl_decrypt(base64_decode($stored['ciphertext'], true), 'aes-256-gcm', $key, OPENSSL_RAW_DATA, base64_decode($stored['iv'], true), base64_decode($stored['tag'], true), $aad);
if ($plain === false) { throw new RuntimeException('Файл не прошёл проверку'); }
echo 'delivery.json → ', $plain, "\n";
echo "Назначение A1 проверено вместе с текстом\n";
Ожидаемый результат
delivery.json → Курьер: вход со двора
Назначение A1 проверено вместе с текстом

Base64 позволяет хранить двоичные части в JSON без порчи UTF-8. key в JSON не записан: в этой локальной демонстрации он живёт в памяти. AAD приходит из ожидаемого назначения доставки; слепо доверять любому AAD из чужого файла нельзя. Для чужого пакета сначала проверяют его структуру/длины, как в следующей карточке.

Границы применения. CBC и CTR сами по себе не подтверждают целостность. Не заменяйте GCM на них только ради исчезновения ошибки tag. Числа флагов разных семейств OpenSSL невзаимозаменяемы; пишите имена. Наличие шифрования не заменяет TLS, проверку пользователя и управление ключами.

Версии и совместимость. Параметры tag/aad/tag_length доступны с PHP 7.1.

Следующий шаг: openssl_decrypt(), openssl_cipher_key_length(), openssl_cipher_iv_length().

openssl_decryptПолучить исходное сообщение только после успешной проверки согласованного AEAD-пакета.

PHP 8.2+ · openssl

openssl_decrypt( string $data, string $cipher_algo, string $passphrase, int $options = 0, string $iv = "", ?string $tag = null, string $aad = ""): string|false

Получить исходное сообщение только после успешной проверки согласованного AEAD-пакета.

message.json хранит Base64-поля. После декодирования проверяем размеры key/IV/tag, затем вызываем decrypt с заранее ожидаемым delivery:A1. Возврат сравнивается строго с false: пустой корректный текст разрешён.

Открыть готовую зашифрованную заметку из архиваСамостоятельный пример
<?php
declare(strict_types=1);

$message = json_decode(file_get_contents(__DIR__ . '/message.json'), true, flags: JSON_THROW_ON_ERROR);
$key = base64_decode(file_get_contents(__DIR__ . '/message-key.b64'), true);
$iv = base64_decode($message['iv'], true);
$tag = base64_decode($message['tag'], true);
$encrypted = base64_decode($message['ciphertext'], true);

if ($key === false || strlen($key) !== 32 || $iv === false || strlen($iv) !== 12 || $tag === false || strlen($tag) !== 16 || $encrypted === false) {
    throw new UnexpectedValueException('Неверный формат учебного пакета');
}
$plain = openssl_decrypt($encrypted, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, 'delivery:A1');
if ($plain === false) { throw new RuntimeException('Заметка отклонена'); }
echo $plain, "\n";
Ожидаемый результат
Встреча: склад Север, 10:00

message.json и message-key.b64 созданы отдельно и входят в архив. Мы используем сохранённые IV/tag, а не генерируем новые при чтении. Это известная структура fixture; для произвольного JSON дополнительно проверяют обязательные поля и типы до base64_decode.

ПараметрДоговор
dataИсходная строка любых байтов; в decrypt — шифротекст, сырой при RAW_DATA либо Base64 без него.
cipher_algoИмя шифра из get_cipher_methods. Выбор должен соответствовать протоколу, доступность сама по себе не делает режим подходящим.
passphraseИменно ключевые байты. KDF не выполняется; неподходящая длина обычно дополняется NUL/обрезается. В примерах AES-256 получает ровно 32 байта.
optionsПобитовое объединение OPENSSL_RAW_DATA, OPENSSL_ZERO_PADDING, OPENSSL_DONT_ZERO_PAD_KEY. RAW_DATA определяет представление ciphertext; ZERO_PADDING отключает стандартное заполнение блока, а не «добавляет нули».
ivСогласованное начальное значение. Короткое дополняется NUL с Warning, длинное обрезается с Warning; приложение проверяет длину заранее.
tagAEAD-метка. В encrypt — выход по ссылке; в decrypt — обязательная для выбранного GCM-контракта входная строка байтов.
aadДополнительные аутентифицируемые байты: видимы, но защищены от изменения. Обе стороны передают точно одинаковые значения.

Результат и отказ. Исходная строка либо false. При неправильном tag GCM возвращает false; выдавать данные дальше нельзя. Неизвестный алгоритм/ошибочный IV могут дополнительно породить Warning.

Отклонить повреждённую заметку и укороченный tagСамостоятельный пример
<?php
declare(strict_types=1);

$message = json_decode(file_get_contents(__DIR__ . '/message.json'), true, flags: JSON_THROW_ON_ERROR);
$key = base64_decode(file_get_contents(__DIR__ . '/message-key.b64'), true);
$iv = base64_decode($message['iv'], true);
$tag = base64_decode($message['tag'], true);
$encrypted = base64_decode($message['ciphertext'], true);

if ($key === false || $iv === false || $tag === false || $encrypted === false) { throw new RuntimeException('Повреждён Base64'); }
$changed = $encrypted;
$changed[0] = chr(ord($changed[0]) ^ 1);
foreach (['Исходник' => [$encrypted, $tag], 'Подмена' => [$changed, $tag], 'Короткий tag' => [$encrypted, substr($tag, 0, 8)]] as $label => [$bytes, $candidateTag]) {
    if (strlen($key) !== 32 || strlen($iv) !== 12 || strlen($candidateTag) !== 16) {
        echo $label, ": отклонить формат до расшифровки\n"; continue;
    }
    $plain = openssl_decrypt($bytes, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $candidateTag, 'delivery:A1');
    echo $label, ': ', $plain === false ? 'отклонить, целостность не подтверждена' : $plain, "\n";
}
Ожидаемый результат
Исходник: Встреча: склад Север, 10:00
Подмена: отклонить, целостность не подтверждена
Короткий tag: отклонить формат до расшифровки

Операция XOR ^1 меняет один бит первого байта. Длина tag проверяется самим приложением: PHP не требует исходную длину и может проверить только переданный префикс, поэтому пропуск такой проверки ослабил бы наш контракт.

Границы применения. Метод не проверяет соответствие длины tag исходной длине шифрования. Согласуйте точный размер и проверьте его до вызова. Не используйте неподтверждённые байты; не объединяйте «ошибка расшифровки» с «пустой документ».

Версии и совместимость. tag/aad с PHP 7.1; tag принимает null с PHP 8.1, но это не делает tag необязательным для GCM.

Следующий шаг: openssl_encrypt(), openssl_error_string().

openssl_cipher_key_lengthУзнать требуемый размер ключ до генерации случайных байтов.

PHP 8.2+ · openssl

openssl_cipher_key_length(string $cipher_algo): int|false

Узнать требуемый размер ключ до генерации случайных байтов.

Единственный аргумент — строковое имя шифра. Результат — длина в байтах; false проверяют до передачи числа в random_bytes.

Подобрать размер ключ для AES-256-GCMСамостоятельный пример
<?php
declare(strict_types=1);

$length = openssl_cipher_key_length('aes-256-gcm');
if ($length === false) { throw new RuntimeException('Шифр неизвестен'); }
echo 'Для aes-256-gcm ключ: ', $length, " байт\n";
Ожидаемый результат
Для aes-256-gcm ключ: 32 байт

Размер относится к байтам PHP-строки. key_length и iv_length отвечают на разные вопросы; 256 в имени AES — число бит ключа.

ПараметрДоговор
cipher_algoИмя шифра из openssl_get_cipher_methods.

Результат и отказ. Целая длина или false. Для неизвестного имени также возникает E_WARNING.

Использовать найденный размер в доставке заметкиСамостоятельный пример
<?php
declare(strict_types=1);

$size = openssl_cipher_key_length('aes-256-gcm');
if ($size === false) { throw new RuntimeException('Шифр недоступен'); }
$key = random_bytes($size);
$iv = random_bytes(12);
$encrypted = openssl_encrypt('Встреча: склад Север, 10:00', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($encrypted === false) { throw new RuntimeException('Сообщение не зашифровано'); }
$plain = openssl_decrypt($encrypted, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) { throw new RuntimeException('Сообщение не прошло проверку'); }
echo $plain, "\n";
Ожидаемый результат
Встреча: склад Север, 10:00

Вычисленный размер используется непосредственно при генерации. Нельзя брать размер IV как размер ключа или наоборот; каждый принадлежит выбранному параметру.

Границы применения. Размер сам по себе не гарантирует случайность и правильное управление ключом/nonce. Для GCM nonce не повторяют с одним ключом; длину tag этот метод не сообщает.

Следующий шаг: openssl_encrypt(), openssl_get_cipher_methods().

openssl_cipher_iv_lengthУзнать требуемый размер IV до генерации случайных байтов.

PHP 5.3.3+ · openssl

openssl_cipher_iv_length(string $cipher_algo): int|false

Узнать требуемый размер IV до генерации случайных байтов.

Единственный аргумент — строковое имя шифра. Результат — длина в байтах; false проверяют до передачи числа в random_bytes.

Подобрать размер IV для AES-256-GCMСамостоятельный пример
<?php
declare(strict_types=1);

$length = openssl_cipher_iv_length('aes-256-gcm');
if ($length === false) { throw new RuntimeException('Шифр неизвестен'); }
echo 'Для aes-256-gcm IV: ', $length, " байт\n";
Ожидаемый результат
Для aes-256-gcm IV: 12 байт

Размер относится к байтам PHP-строки. key_length и iv_length отвечают на разные вопросы; 256 в имени AES — число бит ключа.

ПараметрДоговор
cipher_algoИмя шифра из openssl_get_cipher_methods.

Результат и отказ. Целая длина или false. Для неизвестного имени также возникает E_WARNING.

Использовать найденный размер в доставке заметкиСамостоятельный пример
<?php
declare(strict_types=1);

$size = openssl_cipher_iv_length('aes-256-gcm');
if ($size === false) { throw new RuntimeException('Шифр недоступен'); }
$key = random_bytes(32);
$iv = random_bytes($size);
$encrypted = openssl_encrypt('Встреча: склад Север, 10:00', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($encrypted === false) { throw new RuntimeException('Сообщение не зашифровано'); }
$plain = openssl_decrypt($encrypted, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) { throw new RuntimeException('Сообщение не прошло проверку'); }
echo $plain, "\n";
Ожидаемый результат
Встреча: склад Север, 10:00

Вычисленный размер используется непосредственно при генерации. Нельзя брать размер IV как размер ключа или наоборот; каждый принадлежит выбранному параметру.

Границы применения. Размер сам по себе не гарантирует случайность и правильное управление ключом/nonce. Для GCM nonce не повторяют с одним ключом; длину tag этот метод не сообщает.

Следующий шаг: openssl_encrypt(), openssl_get_cipher_methods().

openssl_digestПосчитать отпечаток файла и заметить изменение при сверке с известным манифестом.

PHP 8.2+ · openssl

openssl_digest(string $data, string $digest_algo, bool $binary = false): string|false

Посчитать отпечаток файла и заметить изменение при сверке с известным манифестом.

data содержит байты файла, sha256 выбирает дайджест. Без третьего аргумента возвращается шестнадцатеричная строка; true запрашивает бинарный результат.

Получить SHA-256 накладной для манифеста файловСамостоятельный пример
<?php
declare(strict_types=1);

$bytes = file_get_contents(__DIR__ . '/invoice.txt');
$digest = openssl_digest($bytes, 'sha256');
if ($digest === false) { throw new RuntimeException('Хеш не вычислен'); }
echo 'invoice.txt: ', $digest, "\n";
Ожидаемый результат
invoice.txt: 77a66c9361fd30e59e1eb1f0311f0a26944659b4cfbc994593426081d320f185

Это отпечаток конкретных байтов файла, включая завершающий перевод строки. Его можно включить в доверенный манифест выгрузки; сам по себе хеш не является цифровой подписью.

ПараметрДоговор
dataСтрока входных байтов, не имя файла. Файл сначала читается приложением.
digest_algoИмя алгоритма, например sha256; возможные имена сообщает get_md_methods.
binaryfalse по умолчанию: hex-строка; true: сырые байты дайджеста.

Результат и отказ. Строка дайджеста или false. Неизвестный алгоритм также вызывает E_WARNING.

Сравнить полученную накладную с известным манифестомСамостоятельный пример
<?php
declare(strict_types=1);

$expected = trim(file_get_contents(__DIR__ . '/manifest.sha256'));
$invoice = file_get_contents(__DIR__ . '/invoice.txt');
foreach (['Исходник' => $invoice, 'Подмена' => str_replace('1200', '9000', $invoice)] as $name => $data) {
    $actual = openssl_digest($data, 'sha256', true);
    if ($actual === false) { throw new RuntimeException('Хеш не вычислен'); }
    echo $name, ': ', hash_equals($expected, bin2hex($actual)) ? 'байты соответствуют манифесту' : 'файл изменён', "\n";
}
Ожидаемый результат
Исходник: байты соответствуют манифесту
Подмена: файл изменён

true возвращает двоичные байты. bin2hex превращает их в запись для сравнения с текстовым манифестом. Манифест должен быть получен по независимому доверенному каналу; возможность изменить одновременно файл и его хеш разрушает такую проверку.

Границы применения. Обычный хеш не скрывает данные и не подтверждает отправителя; его нельзя использовать как быстрый хеш пароля. Для подписи нужен sign/verify, для аутентифицируемого общего секрета — соответствующий MAC готового протокола.

Следующий шаг: openssl_get_md_methods(), openssl_sign().

openssl_get_cipher_methodsПеред обработкой файла сопоставить согласованные шифры с возможностями установленной библиотеки.

PHP 8.2+ · openssl

openssl_get_cipher_methods(bool $aliases = false): array

Перед обработкой файла сопоставить согласованные шифры с возможностями установленной библиотеки.

Вызов без аргумента возвращает массив строк. in_array(..., true) проверяет точное присутствие нужного имени. Не сортируем весь список ради выбора случайного первого значения.

Показать доступные алгоритмы согласованного форматаСамостоятельный пример
<?php
declare(strict_types=1);

$available = openssl_get_cipher_methods();
foreach (['aes-128-gcm', 'aes-256-gcm'] as $name) {
    echo $name, in_array($name, $available, true) ? ": доступен\n" : ": отсутствует\n";
}
Ожидаемый результат
aes-128-gcm: доступен
aes-256-gcm: доступен

Мы показываем только два алгоритма конкретного договора, а не случайно выбираем первый из длинного списка. Вывод относится к проверенной сборке; фактический список зависит от OpenSSL и провайдеров.

ПараметрДоговор
aliasesfalse по умолчанию: основные имена; true добавляет алиасы алгоритмов.

Результат и отказ. Массив доступных строковых имён.

Отказать неизвестному алгоритму до обработки накладнойСамостоятельный пример
<?php
declare(strict_types=1);

$available = openssl_get_cipher_methods(true);
foreach (['aes-256-gcm', 'partner-unknown-v9'] as $requested) {
    if (!in_array($requested, ['aes-256-gcm'], true) || !in_array($requested, $available, true)) {
        echo $requested, ": формат не согласован, файл не обрабатываем\n"; continue;
    }
    echo $requested, ": можно обработать накладную\n";
}
Ожидаемый результат
aes-256-gcm: можно обработать накладную
partner-unknown-v9: формат не согласован, файл не обрабатываем

Первый список — политика приложения, второй — возможности библиотеки. aliases=true включает альтернативные имена, но не расширяет автоматически разрешённую политику. Удалённый отправитель не должен выбирать произвольный алгоритм лишь потому, что он есть в OpenSSL.

Границы применения. Наличие алгоритма не подтверждает его пригодность для нового протокола. Список меняется при другой сборке/провайдере; нельзя превращать его полный дамп в постоянный ожидаемый результат.

Версии и совместимость. Для шифров OpenSSL 1.1.1+ возвращает нижний регистр; старые версии включали верхние/нижние варианты.

Следующий шаг: openssl_encrypt().

openssl_get_md_methodsПеред обработкой файла сопоставить согласованные дайджесты с возможностями установленной библиотеки.

PHP 8.2+ · openssl

openssl_get_md_methods(bool $aliases = false): array

Перед обработкой файла сопоставить согласованные дайджесты с возможностями установленной библиотеки.

Вызов без аргумента возвращает массив строк. in_array(..., true) проверяет точное присутствие нужного имени. Не сортируем весь список ради выбора случайного первого значения.

Показать доступные алгоритмы согласованного форматаСамостоятельный пример
<?php
declare(strict_types=1);

$available = openssl_get_md_methods();
foreach (['sha256', 'sha512'] as $name) {
    echo $name, in_array($name, $available, true) ? ": доступен\n" : ": отсутствует\n";
}
Ожидаемый результат
sha256: доступен
sha512: доступен

Мы показываем только два алгоритма конкретного договора, а не случайно выбираем первый из длинного списка. Вывод относится к проверенной сборке; фактический список зависит от OpenSSL и провайдеров.

ПараметрДоговор
aliasesfalse по умолчанию: основные имена; true добавляет алиасы алгоритмов.

Результат и отказ. Массив доступных строковых имён.

Отказать неизвестному алгоритму до обработки накладнойСамостоятельный пример
<?php
declare(strict_types=1);

$available = openssl_get_md_methods(true);
foreach (['sha256', 'partner-unknown-v9'] as $requested) {
    if (!in_array($requested, ['sha256'], true) || !in_array($requested, $available, true)) {
        echo $requested, ": формат не согласован, файл не обрабатываем\n"; continue;
    }
    echo $requested, ": можно обработать накладную\n";
}
Ожидаемый результат
sha256: можно обработать накладную
partner-unknown-v9: формат не согласован, файл не обрабатываем

Первый список — политика приложения, второй — возможности библиотеки. aliases=true включает альтернативные имена, но не расширяет автоматически разрешённую политику. Удалённый отправитель не должен выбирать произвольный алгоритм лишь потому, что он есть в OpenSSL.

Границы применения. Наличие алгоритма не подтверждает его пригодность для нового протокола. Список меняется при другой сборке/провайдере; нельзя превращать его полный дамп в постоянный ожидаемый результат.

Следующий шаг: openssl_digest().

openssl_get_curve_namesПроверить, может ли сервер создать EC-ключ формата партнёра.

PHP 7.1+ · openssl

openssl_get_curve_names(): array|false

Проверить, может ли сервер создать EC-ключ формата партнёра.

У функции нет аргументов. Возвращённый массив содержит имена curve_name для pkey_new; false означает отказ получения списка.

Проверить поддержку согласованных кривых P-256/P-384Самостоятельный пример
<?php
declare(strict_types=1);

$names = openssl_get_curve_names();
if ($names === false) { throw new RuntimeException('Список кривых недоступен'); }
foreach (['prime256v1' => 'P-256', 'secp384r1' => 'P-384'] as $name => $label) {
    echo $label, in_array($name, $names, true) ? ": доступна\n" : ": отсутствует\n";
}
Ожидаемый результат
P-256: доступна
P-384: доступна

Читаемые обозначения P-256/P-384 сопоставлены с именами OpenSSL. Это проверка возможностей среды; конкретную кривую заранее задаёт протокол.

ПараметрДоговор

Результат и отказ. Массив имён либо false.

Приблизительная таблица из официального раздела сохранена как справка; она не заменяет выбор актуального протокола.

Симметричная оценка, битRSA/DSA, битECC, бит
801024160
1122048224
1283072256
1927680384
25615360512
Создать ключ только после проверки имени кривойСамостоятельный пример
<?php
declare(strict_types=1);

$curve = 'prime256v1';
$names = openssl_get_curve_names();
if ($names === false || !in_array($curve, $names, true)) { throw new RuntimeException('Кривая протокола недоступна'); }
$key = openssl_pkey_new(['config' => __DIR__ . '/openssl.cnf', 'private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => $curve]);
if ($key === false || !openssl_sign('Остаток A1: 4', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo openssl_verify('Остаток A1: 4', $signature, openssl_pkey_get_details($key)['key'], OPENSSL_ALGO_SHA256) === 1
    ? "P-256: изменение остатка A1 подтверждено\n" : "Подпись отклонена\n";
Ожидаемый результат
P-256: изменение остатка A1 подтверждено

Отсутствие кривой не заменяется молча другим именем. Успех списка ещё не гарантирует успех генерации: оба результата проверены.

Границы применения. Длина ключа разных семейств не сравнивается напрямую. Официальная таблица ниже даёт приблизительные соответствия, а не рекомендацию использовать исторические короткие ключи; PHP Manual указывает ориентир ECC не менее 256 бит.

Следующий шаг: openssl_pkey_new().

openssl_random_pseudo_bytesПолучить непредсказуемые байты для ключа или непрозрачного идентификатора.

PHP 8.2+ · openssl

openssl_random_pseudo_bytes(int $length, bool &$strong_result = null): string

Получить непредсказуемые байты для ключа или непрозрачного идентификатора.

length задаётся в байтах; необязательная переменная strong_result заполняется по ссылке. В вызове амперсанд не ставят. bin2hex нужен только для печатного представления; для AES передают исходные 32 байта.

Связать непредсказуемый токен с учебной накладнойСамостоятельный пример
<?php
declare(strict_types=1);

$token = bin2hex(openssl_random_pseudo_bytes(32, $strong));
if (!$strong) { throw new RuntimeException('Источник случайности не подходит'); }
$documents = [$token => 'invoice.txt'];
$requestedToken = $token;
echo file_get_contents(__DIR__ . '/' . $documents[$requestedToken]);
Ожидаемый результат
Заказ A1: 1200 RUB

random создаёт байты, bin2hex переводит их в удобный печатный токен. Массив связывает этот непрозрачный идентификатор с разрешённым файлом; строка токена не становится произвольным путём. Это локальная модель поиска, не проверенный HTTP-сервис.

ПараметрДоговор
lengthПоложительное целое число байтов до 2147483647 включительно. Не запрашивайте огромный объём ради токена.
strong_resultНеобязательный bool по ссылке: использован ли криптографически стойкий источник; с PHP 8 параметр допускает null.

Результат и отказ. Бинарная строка. При отказе с PHP 7.4 выбрасывается Exception вместо прежнего false; недопустимая длина — ошибка аргумента.

Случайный ключ защищает заметку; секрет не показываетсяСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_random_pseudo_bytes(32, $strong);
if (!$strong) { throw new RuntimeException('Источник случайности не подходит'); }
$iv = random_bytes(12);
$encrypted = openssl_encrypt('Курьер: заказ A1 готов', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($encrypted === false) { throw new RuntimeException('Заметка не зашифрована'); }
$plain = openssl_decrypt($encrypted, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) { throw new RuntimeException('Заметка отклонена'); }
echo $plain, "\n";
Ожидаемый результат
Курьер: заказ A1 готов

Секретные случайные байты применены как ключ точной длины. Они не должны совпадать между запусками, поэтому результат урока — успешно восстановленное сообщение, а не фиктивная постоянная случайная строка.

Границы применения. Для обычного нового PHP-кода доступен random_bytes из Core. Ни токен, ни случайный ключ сами не задают срок действия, права доступа, хранение и отзыв; токен не выводят в публичный журнал.

Версии и совместимость. С PHP 7.4 отказ — исключение; strong_result nullable с PHP 8.0.

Следующий шаг: openssl_encrypt().

openssl_error_stringСвязать диагностику OpenSSL с той попыткой, которая действительно отказала.

PHP 8.2+ · openssl

openssl_error_string(): string|false

Связать диагностику OpenSSL с той попыткой, которая действительно отказала.

Вызов без аргументов извлекает одно сообщение из очереди. while повторяет вызов до false; присваивание в скобках сохраняет текущую строку. Очистку делают осознанно на границе операции, не вместо обработки отказа.

Сохранить причину отказа разбора ключа в отдельную диагностикуСамостоятельный пример
<?php
declare(strict_types=1);

while (openssl_error_string() !== false) {}
$key = openssl_pkey_get_private('это не PEM-ключ');
if ($key === false) {
    $messages = [];
    while (($message = openssl_error_string()) !== false) { $messages[] = $message; }
    if ($messages === []) { throw new RuntimeException('Ожидалась диагностика OpenSSL'); }
    file_put_contents(__DIR__ . '/openssl-errors.log', implode("\n", $messages));
    echo "Ключ не прочитан: проверьте PEM; подробности в openssl-errors.log\n";
}
Ожидаемый результат
Ключ не прочитан: проверьте PEM; подробности в openssl-errors.log

Полный реальный текст ошибки записан в учебный файл. Его формулировка зависит от OpenSSL, поэтому не закреплена как постоянный бизнес-код. Перед отдельной попыткой очередь очищена, чтобы не приписать ей старую ошибку.

ПараметрДоговор

Результат и отказ. Строка очередного сообщения или false, когда очередь пуста. Последнее прочитанное сообщение относится к самой недавней ошибке очереди.

После ошибочного PEM выполнить независимую правильную попыткуСамостоятельный пример
<?php
declare(strict_types=1);

while (openssl_error_string() !== false) {}
foreach (['Ошибочный файл' => 'not a PEM', 'Исправный файл' => file_get_contents(__DIR__ . '/publisher-private.pem')] as $label => $pem) {
    $key = openssl_pkey_get_private($pem);
    echo $label, ': ', $key === false ? 'подпись не выполняем' : 'ключ готов к подписи', "\n";
    while (openssl_error_string() !== false) {}
}
Ожидаемый результат
Ошибочный файл: подпись не выполняем
Исправный файл: ключ готов к подписи

Решение принимает прямой результат операции; наличие старых сообщений в очереди не превращает успешный ключ в отказ. Очередь читают до строгого false. PHP Warning и исключения — отдельные каналы, error_string не перехватывает их.

Границы применения. Не все ошибки протокола обязаны давать строку: например, неподходящий AEAD-tag может просто вернуть false. Проверяйте возврат функции. Не показывайте пользователю пути, секретные данные и полный серверный журнал.

Следующий шаг: openssl_pkey_get_private(), openssl_decrypt().

openssl_pbkdf2Преобразовать пароль в ключ согласованного формата, вместо прямой передачи короткого пароля в AES.

PHP 5.5+ · openssl

openssl_pbkdf2( string $password, string $salt, int $key_length, int $iterations, string $digest_algo = "sha1"): string|false

Преобразовать пароль в ключ согласованного формата, вместо прямой передачи короткого пароля в AES.

Пять аргументов: пароль,16 случайных байтов соли,32 байта результата,600000 итераций,sha256. Работа повторяется намеренно: это увеличивает цену массового перебора, но не делает слабый пароль сильным автоматически.

Получить одинаковый ключ из пароля и сохранённой солиСамостоятельный пример
<?php
declare(strict_types=1);

$salt = random_bytes(16);
$key = openssl_pbkdf2('учебный пароль', $salt, 32, 600000, 'sha256');
$again = openssl_pbkdf2('учебный пароль', $salt, 32, 600000, 'sha256');
if ($key === false || $again === false) { throw new RuntimeException('KDF не выполнен'); }
echo hash_equals($key, $again) ? "Пароль и соль восстановили ключ файла\n" : "Ключи различаются\n";
Ожидаемый результат
Пароль и соль восстановили ключ файла

Соль — случайные несекретные байты; её сохраняют вместе с зашифрованным файлом. Для повторного получения ключа нужны точно те же пароль, соль, размер, число итераций и дайджест. PBKDF2 не хранит эти параметры внутри возвращённых байтов.

ПараметрДоговор
passwordИсходные байты пароля; согласуйте кодировку и не нормализуйте незаметно по-разному на двух сторонах.
saltСлучайная криптографическая соль, официальный ориентир минимум 128 бит/16 байт. Её сохраняют для восстановления ключа.
key_lengthДлина бинарного результата в байтах, например 32 для AES-256.
iterationsПоложительная стоимость вычисления, согласованная форматом. PHP Manual приводит ориентиры OWASP2023:600000 для HMAC-SHA256 и 210000 для HMAC-SHA512; актуальную стоимость проверяют для приложения.
digest_algoИмя дайджеста; default sha1. В новых согласованных примерах явно sha256 либо sha512.

Результат и отказ. Сырая бинарная строка или false.

Открыть парольную заметку и отказать другому паролюСамостоятельный пример
<?php
declare(strict_types=1);

$salt = random_bytes(16); $iv = random_bytes(12);
$key = openssl_pbkdf2('учебный пароль', $salt, 32, 600000, 'sha256');
if ($key === false) { throw new RuntimeException('KDF не выполнен'); }
$encrypted = openssl_encrypt('Заметка к A1: звонок за час', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($encrypted === false) { throw new RuntimeException('Шифрование не выполнено'); }
foreach (['Верный пароль' => 'учебный пароль', 'Другой пароль' => 'ошибочный пароль'] as $label => $password) {
    $candidate = openssl_pbkdf2($password, $salt, 32, 600000, 'sha256');
    if ($candidate === false) { throw new RuntimeException('KDF не выполнен'); }
    $plain = openssl_decrypt($encrypted, 'aes-256-gcm', $candidate, OPENSSL_RAW_DATA, $iv, $tag);
    echo $label, ': ', $plain === false ? 'заметка не открыта' : $plain, "\n";
}
Ожидаемый результат
Верный пароль: Заметка к A1: звонок за час
Другой пароль: заметка не открыта

Этот локальный пример показывает две роли: PBKDF2 получает ключ, GCM подтверждает целостность заметки. Это не готовый переносимый файловый протокол. Для рабочего формата выбирают библиотеку, ограничивают входные параметры KDF и хранят версию/соль/стоимость/IV/tag по договору.

Границы применения. KDF не шифрует данные и не создаёт самодостаточную запись пароля. Для проверки паролей входа обычно нужен password_hash/password_verify. Не берите стоимость/длину из непроверенного файла без верхних границ: это вычислительная нагрузка.

Следующий шаг: openssl_encrypt(), openssl_get_md_methods().

openssl_csr_newПодготовить запрос на сертификат с собственной открытой частью и заявленным именем.

PHP 8.2+ · openssl

openssl_csr_new( array $distinguished_names, ?OpenSSLAsymmetricKey &$private_key, ?array $options = null, ?array $extra_attributes = null): OpenSSLCertificateSigningRequest|bool

Подготовить запрос на сертификат с собственной открытой частью и заявленным именем.

Первый массив задаёт поля имени; $key — существующий объект либо переменная null по ссылке; третий массив выбирает config и SHA-256. Успех проверяется через instanceof, потому что редкий возврат true ещё не является подписанным CSR.

Создать запрос на имя catalog.example.testСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
$csr = openssl_csr_new(['commonName' => 'catalog.example.test'], $key, ['config' => __DIR__ . '/openssl.cnf', 'digest_alg' => 'sha256']);
if (!$csr instanceof OpenSSLCertificateSigningRequest) { throw new RuntimeException('Подписанный CSR не создан'); }
echo 'Запрошен сертификат для: ', openssl_csr_get_subject($csr)['CN'], "\n";
Ожидаемый результат
Запрошен сертификат для: catalog.example.test

commonName — заявленное имя, не доказательство владения доменом. Конфигурация добавляет SAN для того же учебного имени. Существующий закрытый ключ остаётся у заявителя; CSR содержит его открытую часть.

ПараметрДоговор
distinguished_namesАссоциативный массив DN: commonName/CN, organizationName/O, countryName/C, stateOrProvinceName/ST, localityName/L, organizationalUnitName/OU, emailAddress и поддерживаемые OID. Значения — строки, с PHP 8.4 также массивы строк для повторяющегося поля.
private_keyOpenSSLAsymmetricKey либо переменная null по ссылке. При null генерируется новый ключ согласно options и записывается в переменную; закрытая часть подписывает запрос.
optionsПереопределения openssl.cnf по таблице ниже. Нужна корректная конфигурация.
extra_attributesНеобязательный массив дополнительных CSR-атрибутов: ключи преобразуются в OID. С PHP 8.4 действительно изменяются атрибуты CSR, до этого ошибочно менялся DN.

Результат и отказ. Объект CSR при полном успехе; true если запрос создан, но подписание не удалось; false при отказе. Проверка только !== false недостаточна для получения готового объекта.

Настройка / аналог в openssl.cnfНазначение
digest_alg / default_mdАлгоритм подписи, например sha256; специальная строка "null" нужна алгоритмам без внешнего дайджеста.
x509_extensionsИмя секции расширений выпускаемого сертификата.
req_extensionsИмя секции расширений запроса CSR, например subjectAltName.
private_key_bits / default_bitsРазмер создаваемого ключа в битах, если ключ ещё не передан.
private_key_typeRSA по умолчанию; известные типы RSA/DSA/DH/EC и современные типы зависят от операции. Тип для согласования секрета не обязательно умеет подписывать CSR.
encrypt_keyШифровать экспортируемый ключ при наличии passphrase; не обещает автоматическую запись файла.
encrypt_key_cipherКонстанта шифра для защищённого экспорта.
curve_nameИмя EC-кривой из get_curve_names; поддержано с PHP 7.1.
configПуть к локальному openssl.cnf.
config_section_nameИмя секции настроек вместо req; точное имя проверено в PHP 8.5.11 source. В текущем тексте PHP Manual встречается опечатка config_section_section.
Создать ключ вместе с CSR и сохранить принадлежность заявкиСамостоятельный пример
<?php
declare(strict_types=1);

$key = null;
$csr = openssl_csr_new(['commonName' => 'catalog.example.test', 'organizationalUnitName' => ['Warehouse', 'Exports']], $key,
    ['config' => __DIR__ . '/openssl.cnf', 'config_section_name' => 'req', 'private_key_bits' => 2048, 'digest_alg' => 'sha256'],
    ['unstructuredName' => 'catalog-import']);
if (!$csr instanceof OpenSSLCertificateSigningRequest) { throw new RuntimeException('CSR не создан'); }
$subject = openssl_csr_get_subject($csr, false);
echo 'Подразделения: ', implode(', ', $subject['organizationalUnitName']), "\n";
if (!openssl_sign('Накладная заявки', $signature, $key, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Ключ заявки недоступен'); }
echo openssl_verify('Накладная заявки', $signature, openssl_csr_get_public_key($csr), OPENSSL_ALGO_SHA256) === 1
    ? "Новый ключ соответствует открытой части CSR\n" : "Ключ не соответствует CSR\n";
Ожидаемый результат
Подразделения: Warehouse, Exports
Новый ключ соответствует открытой части CSR

null передан в переменной по ссылке: функция создаёт ключ и заполняет $key. СPHP8.4 одно поле DN может содержать несколько значений. extra_attributes — атрибуты самого CSR, не дополнительные поля DN; здесь указан маркер catalog-import, а не секрет.

Границы применения. Ни заявленное имя, ни собственная подпись CSR не доказывают право на домен. Центр должен проверить заявку и разрешённые расширения; автоматическая подпись любого входного CSR не является готовым CA-сервисом.

Версии и совместимость. Объекты вместо ресурсов с PHP 8.0; массивы значений DN и исправленные extra_attributes с 8.4. config_section_name сверено с php-src/php-8.5.11/ext/openssl/openssl_backend_common.c.

Следующий шаг: openssl_csr_export(), openssl_csr_sign(), openssl_csr_get_subject().

openssl_csr_exportПередать запрос на сертификат в PEM-формате через строку.

PHP 8.2+ · openssl

openssl_csr_export(OpenSSLCertificateSigningRequest|string $csr, string &$output, bool $no_text = true): bool

Передать запрос на сертификат в PEM-формате через строку.

csr задаёт готовый запрос; output получает PEM по ссылке. no_text=true оставляет только PEM, false добавляет описание для человека.

Подготовить PEM-заявку и проверить её имяСамостоятельный пример
<?php
declare(strict_types=1);

$csr = file_get_contents(__DIR__ . '/publisher.csr.pem');
if (!openssl_csr_export($csr, $pem)) { throw new RuntimeException('CSR не экспортирован'); }

echo strtok($pem, "\n"), "\n";
echo 'Заявка: ', openssl_csr_get_subject($pem)['CN'], "\n";
Ожидаемый результат
-----BEGIN CERTIFICATE REQUEST-----
Заявка: catalog.example.test

Показаны тип PEM и фактически прочитанное из результата имя. Длинная Base64-часть вносит мало пользы в экранный вывод; полный созданный PEM доступен в переменной/учебном файле.

ПараметрДоговор
csrОбъект CSR, PEM-строка или file://путь.
outputВыходная PEM-строка по ссылке.
no_texttrue по умолчанию: без дополнительного описания; false: с описанием.

Результат и отказ. true при успехе; false при отказе.

Добавить читаемое описание, не потеряв сам запросСамостоятельный пример
<?php
declare(strict_types=1);

$csr = file_get_contents(__DIR__ . '/publisher.csr.pem');
if (!openssl_csr_export($csr, $verbose, false)) { throw new RuntimeException('Подробный экспорт не выполнен'); }

$subject = openssl_csr_get_subject($verbose, false);
if ($subject === false) { throw new RuntimeException('Экспорт перестал быть читаемым CSR'); }
echo 'Читаемое описание добавлено: ', str_contains($verbose, 'Certificate Request:') ? 'да' : 'нет', "\n";
echo 'Имя из сохранённого CSR: ', $subject['commonName'], "\n";
Ожидаемый результат
Читаемое описание добавлено: да
Имя из сохранённого CSR: catalog.example.test

Третий аргумент false включает дополнительный текст перед PEM. Партнёру нужен согласованный формат: если он ожидает только PEM, используйте default true. Это текстовое описание запроса, не выдача сертификата.

Границы применения. Закрытый ключ в CSR не экспортируется. Полученный файл ещё не сертификат; выпускающий центр должен выполнить свою проверку заявки.

Версии и совместимость. CSR-объект принимается с PHP 8.0 вместо ресурса.

Следующий шаг: openssl_csr_get_subject(), openssl_csr_sign().

openssl_csr_export_to_fileПередать запрос на сертификат в PEM-формате через файл.

PHP 8.2+ · openssl

openssl_csr_export_to_file(OpenSSLCertificateSigningRequest|string $csr, string $output_filename, bool $no_text = true): bool

Передать запрос на сертификат в PEM-формате через файл.

csr задаёт готовый запрос; output_filename задаёт путь результата. no_text=true оставляет только PEM, false добавляет описание для человека.

Подготовить PEM-заявку и проверить её имяСамостоятельный пример
<?php
declare(strict_types=1);

$csr = file_get_contents(__DIR__ . '/publisher.csr.pem');
if (!openssl_csr_export_to_file($csr, __DIR__ . '/request.pem')) { throw new RuntimeException('CSR не экспортирован'); }
$pem = file_get_contents(__DIR__ . '/request.pem');

echo strtok($pem, "\n"), "\n";
echo 'Заявка: ', openssl_csr_get_subject($pem)['CN'], "\n";
Ожидаемый результат
-----BEGIN CERTIFICATE REQUEST-----
Заявка: catalog.example.test

Показаны тип PEM и фактически прочитанное из результата имя. Длинная Base64-часть вносит мало пользы в экранный вывод; полный созданный PEM доступен в переменной/учебном файле.

ПараметрДоговор
csrОбъект CSR, PEM-строка или file://путь.
output_filenameИмя записываемого PEM-файла.
no_texttrue по умолчанию: без дополнительного описания; false: с описанием.

Результат и отказ. true при успехе; false при отказе.

Добавить читаемое описание, не потеряв сам запросСамостоятельный пример
<?php
declare(strict_types=1);

$csr = file_get_contents(__DIR__ . '/publisher.csr.pem');
if (!openssl_csr_export_to_file($csr, __DIR__ . '/request-readable.pem', false)) { throw new RuntimeException('Подробный экспорт не выполнен'); }
$verbose = file_get_contents(__DIR__ . '/request-readable.pem');

$subject = openssl_csr_get_subject($verbose, false);
if ($subject === false) { throw new RuntimeException('Экспорт перестал быть читаемым CSR'); }
echo 'Читаемое описание добавлено: ', str_contains($verbose, 'Certificate Request:') ? 'да' : 'нет', "\n";
echo 'Имя из сохранённого CSR: ', $subject['commonName'], "\n";
Ожидаемый результат
Читаемое описание добавлено: да
Имя из сохранённого CSR: catalog.example.test

Третий аргумент false включает дополнительный текст перед PEM. Партнёру нужен согласованный формат: если он ожидает только PEM, используйте default true. Это текстовое описание запроса, не выдача сертификата.

Границы применения. Закрытый ключ в CSR не экспортируется. Полученный файл ещё не сертификат; выпускающий центр должен выполнить свою проверку заявки.

Версии и совместимость. CSR-объект принимается с PHP 8.0 вместо ресурса.

Следующий шаг: openssl_csr_get_subject(), openssl_csr_sign().

openssl_csr_get_subjectПрочитать заявленное имя и организацию перед обработкой CSR.

PHP 8.2+ · openssl

openssl_csr_get_subject(OpenSSLCertificateSigningRequest|string $csr, bool $short_names = true): array|false

Прочитать заявленное имя и организацию перед обработкой CSR.

Первый аргумент — CSR. Необязательный false меняет имена ключей массива: CN превращается в commonName. Наличие поля проверяют по договору конкретного центра.

Показать имя из входной заявкиСамостоятельный пример
<?php
declare(strict_types=1);

$subject = openssl_csr_get_subject(file_get_contents(__DIR__ . '/publisher.csr.pem'));
if ($subject === false) { throw new RuntimeException('Заявка не разобрана'); }
echo 'Имя: ', $subject['CN'], "\n", 'Организация: ', $subject['O'], "\n";
Ожидаемый результат
Имя: catalog.example.test
Организация: PHP Manual Lab

При default short_names=true используются CN/O. Поля прочитаны из содержимого заявки; это заявление заявителя, а не подтверждённая внешним центром личность.

ПараметрДоговор
csrОбъект, PEM-строка либо file://путь CSR.
short_namestrue: короткие ключи CN/O/C; false: commonName/organizationName/countryName.

Результат и отказ. Ассоциативный массив DN или false при отказе.

Отправить неподходящую заявку на ручную проверкуСамостоятельный пример
<?php
declare(strict_types=1);

foreach (['publisher.csr.pem', 'recipient.csr.pem'] as $file) {
    $subject = openssl_csr_get_subject(file_get_contents(__DIR__ . '/' . $file), false);
    if ($subject === false) { throw new RuntimeException('CSR не прочитан'); }
    $name = $subject['commonName'] ?? '';
    echo $name, ': ', $name === 'catalog.example.test' ? 'передать в очередь проверки каталога' : 'проверить назначение вручную', "\n";
}
Ожидаемый результат
catalog.example.test: передать в очередь проверки каталога
warehouse.example.test: проверить назначение вручную

false даёт длинные имена ключей. Сравнение commonName используется только для маршрутизации заявки, не для подтверждения домена и не для полноценной проверки TLS.

Границы применения. Это извлечение данных, не проверка права на имя. DN и атрибуты/расширения CSR — разные части запроса.

Версии и совместимость. С PHP 8.0 принимает объект CSR вместо ресурса.

Следующий шаг: openssl_csr_get_public_key(), openssl_csr_new().

openssl_csr_get_public_keyИзвлечь из заявки открытую часть для сопоставления с документами заявителя.

PHP 8.2+ · openssl

openssl_csr_get_public_key(OpenSSLCertificateSigningRequest|string $csr, bool $short_names = true): OpenSSLAsymmetricKey|false

Извлечь из заявки открытую часть для сопоставления с документами заявителя.

Передайте CSR. Возвращённый объект открытого ключа подходит для verify; его публичный PEM извлекается через get_details[key].

Проверить, что накладная подписана ключом заявкиСамостоятельный пример
<?php
declare(strict_types=1);

$public = openssl_csr_get_public_key(file_get_contents(__DIR__ . '/publisher.csr.pem'));
if ($public === false) { throw new RuntimeException('Открытый ключ CSR не извлечён'); }
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify(file_get_contents(__DIR__ . '/invoice.txt'), $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "Накладная A1 соответствует ключу заявки\n" : "Ключ заявки не подтверждает A1\n";
Ожидаемый результат
Накладная A1 соответствует ключу заявки

Ключ CSR использован в реальной операции. Само извлечение открытой части не превращает запрос в доверенный сертификат.

ПараметрДоговор
csrCSR в объектной, PEM либо file://форме.
short_namesАргумент игнорируется; изменение true/false не меняет объект ключа.

Результат и отказ. OpenSSLAsymmetricKey или false.

Подготовить отдельный открытый PEM из запросаСамостоятельный пример
<?php
declare(strict_types=1);

$public = openssl_csr_get_public_key('file://' . __DIR__ . '/publisher.csr.pem', false);
if ($public === false) { throw new RuntimeException('Ключ не извлечён'); }
$details = openssl_pkey_get_details($public);
if ($details === false || file_put_contents(__DIR__ . '/request-public.pem', $details['key']) === false) { throw new RuntimeException('Публичный файл не записан'); }
echo strtok(file_get_contents(__DIR__ . '/request-public.pem'), "\n"), "\n";
echo "request-public.pem содержит открытую часть заявки каталога\n";
Ожидаемый результат
-----BEGIN PUBLIC KEY-----
request-public.pem содержит открытую часть заявки каталога

short_names сохранён в сигнатуре для совместимости, но функция его игнорирует: результат — объект, не DN-массив. Вывод показывает созданный тип файла, а не секретные компоненты.

Границы применения. Извлечение не устанавливает доверие к CSR. Закрытую часть этим способом получить нельзя.

Версии и совместимость. Объекты CSR/ключа вместо ресурсов с PHP 8.0.

Следующий шаг: openssl_csr_get_subject(), openssl_verify().

openssl_csr_signПревратить разрешённую заявку в сертификат с заданным издателем, сроком и серийным номером.

PHP 8.2+ · openssl

openssl_csr_sign( OpenSSLCertificateSigningRequest|string $csr, OpenSSLCertificate|string|null $ca_certificate, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, int $days, ?array $options = null, int $serial = 0, ?string $serial_hex = null): OpenSSLCertificate|false

Превратить разрешённую заявку в сертификат с заданным издателем, сроком и серийным номером.

csr — запрос; ca_certificate — сертификат центра либо null для самоподписи; private_key — соответствующий закрытый ключ подписанта; days — срок в днях. options выбирает расширения, serial/serial_hex — номер выпуска.

Создать самостоятельный учебный сертификат без доверенного центраСамостоятельный пример
<?php
declare(strict_types=1);

$csr = file_get_contents(__DIR__ . '/publisher.csr.pem');
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
$certificate = openssl_csr_sign($csr, null, $private, 30, ['config' => __DIR__ . '/openssl.cnf', 'digest_alg' => 'sha256', 'x509_extensions' => 'publisher'], 1100);
if ($certificate === false) { throw new RuntimeException('Сертификат не создан'); }
$info = openssl_x509_parse($certificate);
echo 'Владелец: ', $info['subject']['CN'], "\n", 'Издатель: ', $info['issuer']['CN'], "\n";
echo "Самоподпись не добавляет доверие в браузере\n";
Ожидаемый результат
Владелец: catalog.example.test
Издатель: catalog.example.test
Самоподпись не добавляет доверие в браузере

null вместо CA означает самоподпись.30 задаёт срок в днях; это не настройка браузера и не реальный TLS-тест. Имена берутся из фактически выпущенного сертификата.

ПараметрДоговор
csrОбъект CSR, экспортированный PEM или file://путь.
ca_certificateСертификат издателя; null создаёт самоподписанный сертификат.
private_keyЗакрытый ключ центра, а при самоподписи — ключ заявителя; поддерживаются объект/PEM/file:// и парольная пара.
daysСрок действия выпускаемого сертификата в днях.
optionsНастройки подписи/конфигурации как у csr_new; особенно digest_alg и x509_extensions.
serialЦелый серийный номер, default0. Реальный центр обеспечивает корректную уникальность номеров по своей политике.
serial_hexС PHP 8.4: шестнадцатеричный номер; при не-null имеет приоритет над serial.

Результат и отказ. OpenSSLCertificate либо false.

Выпустить сертификат локальным CA и прочитать серийный номерСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = openssl_csr_sign(file_get_contents(__DIR__ . '/publisher.csr.pem'), 'file://' . __DIR__ . '/lab-ca.pem',
    'file://' . __DIR__ . '/lab-ca-private.pem', 30,
    ['config' => __DIR__ . '/openssl.cnf', 'digest_alg' => 'sha256', 'x509_extensions' => 'publisher'], 7, 'ABCD1001');
if ($certificate === false || !openssl_x509_export_to_file($certificate, __DIR__ . '/issued.pem')) { throw new RuntimeException('Выпуск не завершён'); }
$info = openssl_x509_parse(file_get_contents(__DIR__ . '/issued.pem'));
echo 'Издатель: ', $info['issuer']['CN'], "\n", 'Серийный номер: ', $info['serialNumberHex'], "\n";
Ожидаемый результат
Издатель: PHP Manual Lab CA
Серийный номер: ABCD1001

serial_hex с PHP 8.4 имеет приоритет над целым serial=7. Центр выбирает расширения publisher из своего локального договора. В учебном наборе все ключи публичны, системное доверие к CA не устанавливается.

Границы применения. Функция не проверяет право клиента на имя и не организует инфраструктуру CA. Не подписывайте произвольные публичные заявки с закрытым ключом центра. Для реального HTTPS отдельно нужны доверенная цепочка, имя/SAN, срок, конфигурация сервера и настоящий handshake.

Версии и совместимость. Объекты с PHP 8.0; serial_hex добавлен в PHP 8.4.

Следующий шаг: openssl_x509_parse(), openssl_x509_checkpurpose().

openssl_x509_readПревратить PEM-сертификат в объект для чтения полей и дальнейших проверок.

PHP 8.2+ · openssl

openssl_x509_read(OpenSSLCertificate|string $certificate): OpenSSLCertificate|false

Превратить PEM-сертификат в объект для чтения полей и дальнейших проверок.

Передайте содержимое PEM либо строку file:// с адресом. Результат сохранится в $certificate; строгое === false обнаруживает отказ. Названия полей читаются отдельной parse-функцией.

Открыть сертификат и показать, кому он выданСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = openssl_x509_read(file_get_contents(__DIR__ . '/publisher-cert.pem'));
if ($certificate === false) { throw new RuntimeException('Сертификат не прочитан'); }
$info = openssl_x509_parse($certificate);
echo 'Владелец: ', $info['subject']['CN'], "\n";
echo 'Издатель: ', $info['issuer']['CN'], "\n";
Ожидаемый результат
Владелец: catalog.example.test
Издатель: PHP Manual Lab CA

Это чтение полей сертификата: сам факт успешного разбора ещё не говорит о доверии, сроке, назначении или доступности TLS-сервера.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.

Результат и отказ. OpenSSLCertificate или false. Если сертификат невозможно получить, также возникает E_WARNING.

Использовать прочитанный сертификат при проверке накладнойСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = openssl_x509_read('file://' . __DIR__ . '/publisher-cert.pem');
if ($certificate === false) { throw new RuntimeException('Сертификат не прочитан'); }
$public = openssl_pkey_get_public($certificate);
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify(file_get_contents(__DIR__ . '/invoice.txt'), $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "A1: подпись соответствует открытому ключу прочитанного сертификата\n" : "Накладная отклонена\n";
Ожидаемый результат
A1: подпись соответствует открытому ключу прочитанного сертификата

Объект сертификата передаётся другой функции. Доверие к владельцу сертификата устанавливают отдельно; подпись накладной и подпись самого сертификата — разные проверки.

Границы применения. Разобрать сертификат не значит доверять его имени. Произвольный самоподписанный сертификат тоже может быть корректным X.509.

Версии и совместимость. С PHP 8.0 принимает/возвращает объект вместо ресурса.

Следующий шаг: openssl_x509_parse(), openssl_x509_checkpurpose().

openssl_x509_parseПоказать имя, издателя, назначение и срок сертификата перед диагностикой подключения.

PHP 8.2+ · openssl

openssl_x509_parse(OpenSSLCertificate|string $certificate, bool $short_names = true): array|false

Показать имя, издателя, назначение и срок сертификата перед диагностикой подключения.

certificate — сертификат, short_names управляет именами DN-ключей: true даёт CN, false — commonName. Метки validFrom_time_t/validTo_time_t в нашей сборке — Unix-секунды.

Составить читаемую карточку сертификатаСамостоятельный пример
<?php
declare(strict_types=1);

$info = openssl_x509_parse(file_get_contents(__DIR__ . '/publisher-cert.pem'));
if ($info === false) { throw new RuntimeException('Сертификат не разобран'); }
echo 'Кому: ', $info['subject']['CN'], "\n";
echo 'Кем: ', $info['issuer']['CN'], "\n";
echo 'SAN: ', $info['extensions']['subjectAltName'], "\n";
echo 'Номер: ', $info['serialNumber'], "\n";
Ожидаемый результат
Кому: catalog.example.test
Кем: PHP Manual Lab CA
SAN: DNS:catalog.example.test, email:publisher@example.test
Номер: 1001

Карточка использует конкретные поля нашей проверенной сборки; PHP Manual намеренно не закрепляет всю структуру массива как неизменный контракт. SAN и commonName показаны отдельно.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.
short_namestrue по умолчанию: короткие DN-имена; false: длинные имена.

Результат и отказ. Массив описания либо false. Содержит сведения о subject/issuer, назначениях и датах; полный формат массива намеренно не гарантирован документацией.

Проверить две даты относительно срока конкретного сертификатаСамостоятельный пример
<?php
declare(strict_types=1);

$info = openssl_x509_parse(file_get_contents(__DIR__ . '/publisher-cert.pem'), false);
if ($info === false) { throw new RuntimeException('Сертификат не разобран'); }
$inside = $info['validFrom_time_t'] + 60;
$after = $info['validTo_time_t'] + 1;
foreach (['Минута после выпуска' => $inside, 'После срока' => $after] as $label => $time) {
    $valid = $time >= $info['validFrom_time_t'] && $time <= $info['validTo_time_t'];
    echo $info['subject']['commonName'], ' / ', $label, ': ', $valid ? 'срок соблюдён' : 'срок нарушен', "\n";
}
Ожидаемый результат
catalog.example.test / Минута после выпуска: срок соблюдён
catalog.example.test / После срока: срок нарушен

Использованы две явно вычисленные учебные даты, поэтому результат не зависит от текущего дня. Это проверка интервала для объяснения; она не заменяет стандартное построение цепочки/проверку TLS.

Границы применения. Поля без проверки подписи могут быть заявлением недоверенного издателя. Нельзя проверять hostname простым равенством CN: правила TLS используют SAN, wildcard и тип имени по протоколу.

Версии и совместимость. Объекты с PHP 8.0; с PHP 8.4 запрещён UTCTime без секунд для любой версии OpenSSL, ранее OpenSSL 3.3+ уже не принимал такой формат.

Следующий шаг: openssl_x509_checkpurpose(), openssl_x509_verify().

openssl_x509_exportПодготовить сертификат в PEM для передачи строкой.

PHP 8.2+ · openssl

openssl_x509_export(OpenSSLCertificate|string $certificate, string &$output, bool $no_text = true): bool

Подготовить сертификат в PEM для передачи строкой.

Первый аргумент — сертификат; второй получает строку по ссылке. Необязательный no_text по умолчанию true; false добавляет описание.

Экспортировать сертификат и прочитать его имяСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = openssl_x509_read(file_get_contents(__DIR__ . '/publisher-cert.pem'));
if (!openssl_x509_export($certificate, $pem)) { throw new RuntimeException('Экспорт не выполнен'); }

echo strtok($pem, "\n"), "\n";
echo 'В копии: ', openssl_x509_parse($pem)['subject']['CN'], "\n";
Ожидаемый результат
-----BEGIN CERTIFICATE-----
В копии: catalog.example.test

Показаны тип и имя из настоящего экспортированного результата. Это публичный сертификат; закрытого ключа в нём нет.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.
outputВыходная строка PEM по ссылке.
no_texttrue: только PEM; false: также читаемая информация.

Результат и отказ. true при успехе, false при отказе.

Получить описание сертификата вместе с PEMСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
if (!openssl_x509_export($certificate, $pem, false)) { throw new RuntimeException('Экспорт не выполнен'); }

echo str_contains($pem, 'Certificate:') ? "Описание сертификата включено\n" : "Описания нет\n";
echo 'Издатель из PEM: ', openssl_x509_parse($pem)['issuer']['CN'], "\n";
Ожидаемый результат
Описание сертификата включено
Издатель из PEM: PHP Manual Lab CA

no_text=false добавляет человеческий текст. PEM внутри остаётся читаемым; потребителю, ожидающему только PEM-блок, передают вариант с default true.

Границы применения. Экспорт не продлевает срок и не меняет подписанные поля сертификата. Для изменения имени/назначения нужен новый выпуск, а не редактирование PEM вручную.

Версии и совместимость. OpenSSLCertificate вместо ресурса с PHP 8.0.

Следующий шаг: openssl_x509_read(), openssl_x509_parse().

openssl_x509_export_to_fileПодготовить сертификат в PEM для отдельного файла.

PHP 8.2+ · openssl

openssl_x509_export_to_file(OpenSSLCertificate|string $certificate, string $output_filename, bool $no_text = true): bool

Подготовить сертификат в PEM для отдельного файла.

Первый аргумент — сертификат; второй задаёт путь записываемого файла. Необязательный no_text по умолчанию true; false добавляет описание.

Экспортировать сертификат и прочитать его имяСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = openssl_x509_read(file_get_contents(__DIR__ . '/publisher-cert.pem'));
if (!openssl_x509_export_to_file($certificate, __DIR__ . '/copy-cert.pem')) { throw new RuntimeException('Экспорт не выполнен'); }
$pem = file_get_contents(__DIR__ . '/copy-cert.pem');

echo strtok($pem, "\n"), "\n";
echo 'В копии: ', openssl_x509_parse($pem)['subject']['CN'], "\n";
Ожидаемый результат
-----BEGIN CERTIFICATE-----
В копии: catalog.example.test

Показаны тип и имя из настоящего экспортированного результата. Это публичный сертификат; закрытого ключа в нём нет.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.
output_filenameПуть PEM-результата.
no_texttrue: только PEM; false: также читаемая информация.

Результат и отказ. true при успехе, false при отказе.

Получить описание сертификата вместе с PEMСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
if (!openssl_x509_export_to_file($certificate, __DIR__ . '/copy-cert.pem', false)) { throw new RuntimeException('Экспорт не выполнен'); }
$pem = file_get_contents(__DIR__ . '/copy-cert.pem');

echo str_contains($pem, 'Certificate:') ? "Описание сертификата включено\n" : "Описания нет\n";
echo 'Издатель из PEM: ', openssl_x509_parse($pem)['issuer']['CN'], "\n";
Ожидаемый результат
Описание сертификата включено
Издатель из PEM: PHP Manual Lab CA

no_text=false добавляет человеческий текст. PEM внутри остаётся читаемым; потребителю, ожидающему только PEM-блок, передают вариант с default true.

Границы применения. Экспорт не продлевает срок и не меняет подписанные поля сертификата. Для изменения имени/назначения нужен новый выпуск, а не редактирование PEM вручную.

Версии и совместимость. OpenSSLCertificate вместо ресурса с PHP 8.0.

Следующий шаг: openssl_x509_read(), openssl_x509_parse().

openssl_x509_fingerprintСверить конкретный сертификат с отпечатком, полученным по доверенному каналу.

PHP 5.6+ · openssl

openssl_x509_fingerprint(OpenSSLCertificate|string $certificate, string $digest_algo = "sha1", bool $binary = false): string|false

Сверить конкретный сертификат с отпечатком, полученным по доверенному каналу.

Второй аргумент явно sha256 вместо default sha1. Третий true возвращает сырые байты; false даёт строчный hex. Сравнивать нужно одинаковое представление.

Получить отпечаток конкретного сертификата для сверкиСамостоятельный пример
<?php
declare(strict_types=1);

$fingerprint = openssl_x509_fingerprint(file_get_contents(__DIR__ . '/publisher-cert.pem'), 'sha256');
if ($fingerprint === false) { throw new RuntimeException('Отпечаток не получен'); }
echo 'catalog.example.test / SHA-256: ', $fingerprint, "\n";
Ожидаемый результат
catalog.example.test / SHA-256: 8561deb722fe9151cdd14e2240af490d82b8a83d5e5c7f8faa69e4aab287775b

Отпечаток относится к структуре сертификата, а не к пробелам/перено сам его PEM-оболочки. При новом выпуске сертификата он изменится; это требует согласованного обновления ожидаемого значения.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.
digest_algoИмя дайджеста из get_md_methods; default sha1, в примерах sha256.
binaryfalse: строчный hex; true: бинарный отпечаток.

Результат и отказ. Строка или false при отказе.

Сопоставить сертификат с заранее известным отпечаткомСамостоятельный пример
<?php
declare(strict_types=1);

$expected = trim(file_get_contents(__DIR__ . '/certificate.sha256'));
foreach (['publisher-cert.pem', 'recipient-cert.pem'] as $file) {
    $raw = openssl_x509_fingerprint(file_get_contents(__DIR__ . '/' . $file), 'sha256', true);
    if ($raw === false) { throw new RuntimeException('Отпечаток не вычислен'); }
    echo $file, ': ', hash_equals($expected, bin2hex($raw)) ? 'известный сертификат' : 'другой сертификат, остановиться', "\n";
}
Ожидаемый результат
publisher-cert.pem: известный сертификат
recipient-cert.pem: другой сертификат, остановиться

Ожидаемое значение хранится отдельно от входного сертификата. Такое закрепление требует плана обновления и не является полной заменой всех требований TLS.

Границы применения. Сверка с отпечатком, присланным вместе с недоверенным сертификатом, не устанавливает доверие. Сертификат и отдельный открытый ключ имеют разные отпечатки.

Версии и совместимость. Объект сертификата принимается с PHP 8.0.

Следующий шаг: openssl_x509_parse(), openssl_digest().

openssl_x509_check_private_keyВыявить несоответствие ключа и сертификата в настройках сервиса.

PHP 8.2+ · openssl

openssl_x509_check_private_key(OpenSSLCertificate|string $certificate, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key): bool

Выявить несоответствие ключа и сертификата в настройках сервиса.

Передайте сертификат и успешно прочитанный закрытый ключ. Метод сопоставляет ключевые параметры; отдельная операция подписи показывает, что данным ключом действительно можно подписать документ.

Проверить соответствие настроенного ключа сертификатуСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
foreach (['publisher-private.pem', 'recipient-private.pem'] as $file) {
    $key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/' . $file));
    if ($key === false) { throw new RuntimeException('Закрытый ключ не прочитан'); }
    echo $file, ': ', openssl_x509_check_private_key($certificate, $key) ? 'пара совпала' : 'ключ от другого сертификата', "\n";
}
Ожидаемый результат
publisher-private.pem: пара совпала
recipient-private.pem: ключ от другого сертификата

Так можно обнаружить ошибочно подобранные файлы перед запуском сервиса. Прочитать закрытый ключ и сопоставить его открытые параметры — отдельные действия.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.
private_keyЗакрытый ключ в поддерживаемой объектной/PEM/file:// форме. Наша PHP 8.5.11 отвергает явно переданный объект с одной открытой частью до вызова OpenSSL.

Результат и отказ. true при соответствии публичных компонент, иначе false.

Обнаружить, что вместо закрытого ключа передана открытая частьСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$publicOnly = openssl_pkey_get_public(file_get_contents(__DIR__ . '/publisher-public.pem'));
$publicWarning = false;
set_error_handler(static function (int $level, string $message) use (&$publicWarning): bool {
    if ($level === E_WARNING && str_contains($message, 'is a public key')) {
        $publicWarning = true; return true;
    }
    return false;
});
try { $result = openssl_x509_check_private_key($certificate, $publicOnly); }
finally { restore_error_handler(); }
if ($result !== false || !$publicWarning) { throw new RuntimeException('Поведение сборки изменилось'); }
echo "Открытый объект: PHP отказал до сопоставления\n";
echo "Для подписи нужен успешно прочитанный закрытый ключ\n";
Ожидаемый результат
Открытый объект: PHP отказал до сопоставления
Для подписи нужен успешно прочитанный закрытый ключ

В PHP 8.5.11 обёртка требует закрытый ключ и выдаёт E_WARNING для открытого объекта. Ожидаемое предупреждение обработано явно; любое другое предупреждение остаётся видимым. Оговорка Manual о сопоставлении открытых компонентов не означает, что этот конкретный вызов обязан пройти.

Границы применения. PHP Manual отдельно предупреждает: сопоставление публичных компонентов само по себе не доказывает наличие всех закрытых данных. Не подменяйте этим проверку возможности подписи. Функция также не проверяет доверие к издателю, срок и имя сервера.

Версии и совместимость. OpenSSL-объекты вместо ресурсов с PHP 8.0. Уточнение поведения публичного объекта проверено реальным запуском на PHP 8.5.11; в php-src обёртка запрашивает private key перед X509_check_private_key.

Следующий шаг: openssl_pkey_get_private(), openssl_sign().

openssl_x509_verifyПроверить, каким известным ключом подписан сертификат.

PHP 7.4+ · openssl

openssl_x509_verify(OpenSSLCertificate|string $certificate, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key): int

Проверить, каким известным ключом подписан сертификат.

Первый аргумент — проверяемый сертификат, второй — открытый ключ предполагаемого издателя. Только строгий результат === 1 разрешает считать подпись подходящей.

Проверить подпись сертификата открытым ключом лабораторного CAСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$issuer = openssl_pkey_get_public(file_get_contents(__DIR__ . '/lab-ca.pem'));
$result = openssl_x509_verify($certificate, $issuer);
echo $result === 1 ? "Сертификат каталога подписан ключом PHP Manual Lab CA\n" : "Подпись сертификата не подтверждена\n";
Ожидаемый результат
Сертификат каталога подписан ключом PHP Manual Lab CA

Проверена именно подпись внутри сертификата. Это не построение полной цепочки и не подтверждение сетевого соединения.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.
public_keyОткрытая часть издателя в объектной/PEM/поддерживаемой форме.

Результат и отказ. 1 — подпись подходит;0 — не подходит;−1 — ошибка. Обычный if ошибочно счёл бы−1 успехом.

Отклонить неверно выбранный ключ издателяСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
foreach (['Ключ CA' => 'lab-ca-public.pem', 'Ключ самого сайта' => 'publisher-public.pem'] as $label => $file) {
    $result = openssl_x509_verify($certificate, file_get_contents(__DIR__ . '/' . $file));
    echo $label, ': ', $result === 1 ? 'подпись подтверждена' : 'сертификат не принимаем', "\n";
}
Ожидаемый результат
Ключ CA: подпись подтверждена
Ключ самого сайта: сертификат не принимаем

Владелец сертификата и его издатель могут иметь разные ключи. Любое значение кроме 1 означает, что дальнейшая обработка не разрешена; подробная диагностика различает 0 и−1.

Границы применения. Не проверяет полный набор требований цепочки доверия, срок/назначение/hostname сам по себе. Для сети используйте штатную проверку TLS-клиента.

Версии и совместимость. OpenSSL-объекты с PHP 8.0.

Следующий шаг: openssl_x509_checkpurpose(), openssl_error_string().

openssl_x509_checkpurposeПроверить, допускает ли доверенная цепочка использование сертификата для нужной роли.

PHP 8.2+ · openssl

openssl_x509_checkpurpose( OpenSSLCertificate|string $certificate, int $purpose, array $ca_info = [], ?string $untrusted_certificates_file = null): bool|int

Проверить, допускает ли доверенная цепочка использование сертификата для нужной роли.

Три основных аргумента: сертификат; ровно одна purpose-константа; массив путей доверенных CA. Только === true означает успех, поскольку−1 тоже truthy.

Проверить назначение сертификата относительно своего учебного CAСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$result = openssl_x509_checkpurpose($certificate, X509_PURPOSE_SSL_SERVER, [__DIR__ . '/lab-ca.pem']);
if ($result === -1) { throw new RuntimeException('Проверка назначения не выполнена'); }
echo $result === true ? "Цепочка к Lab CA и назначение TLS-сервера приняты\n" : "Сертификат не подходит\n";
Ожидаемый результат
Цепочка к Lab CA и назначение TLS-сервера приняты

Доверенный CA выбран явным локальным файлом, системное хранилище не меняется. Функция не получила имя ожидаемого хоста и не выполняла TLS-рукопожатие; эти проверки здесь не заявлены.

ПараметрДоговор
certificateOpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа.
purposeОдна X509_PURPOSE_*: SSL_CLIENT, SSL_SERVER, историческая NS_SSL_SERVER, SMIME_SIGN, SMIME_ENCRYPT, CRL_SIGN, ANY; дополнительные OCSP_HELPER/TIMESTAMP_SIGN зависят от runtime. Это не битовая маска.
ca_infoМассив доверенных CA-файлов либо правильно хешированных каталогов OpenSSL. Пустой массив использует стандартные настройки доверия.
untrusted_certificates_fileНеобязательный PEM-файл промежуточных сертификатов для построения цепочки. Содержимое помогает поиску, но не становится доверенным корнем.

Результат и отказ. true — подходит; false — не подходит;−1 — ошибка проверки.

Различить разрешённый сервер и неподходящее клиентское назначениеСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
foreach (['Сервер' => X509_PURPOSE_SSL_SERVER, 'Клиент' => X509_PURPOSE_SSL_CLIENT] as $label => $purpose) {
    $result = openssl_x509_checkpurpose($certificate, $purpose, [__DIR__ . '/lab-ca.pem']);
    if ($result === -1) { throw new RuntimeException('Не удалось проверить назначение'); }
    echo $label, ': ', $result === true ? 'назначение разрешено' : 'назначение не разрешено', "\n";
}
Ожидаемый результат
Сервер: назначение разрешено
Клиент: назначение не разрешено

Учебный сертификат содержит serverAuth/emailProtection, но не clientAuth. Назначение — одно значение, константы нельзя смешивать побитовым OR. Проверка времени использует реальное текущее время: fixture выдан на 10 лет, после его срока набор следует перевыпустить.

Границы применения. Не заменяет hostname/SAN-проверку ожидаемого сервера и настоящий TLS-handshake. Политика отзывов/доверия зависит от окружения и протокола. Нельзя заменить CA Info сертификатом отправителя только ради успеха проверки.

Версии и совместимость. Объекты и nullable untrusted_certificates_file с PHP 8.0.

Следующий шаг: openssl_get_cert_locations(), openssl_x509_verify().

openssl_x509_freeУбрать из старого кода предположение о ручном закрытии сертификата-ресурса.

Устарело с PHP 8.0 · openssl

openssl_x509_free(OpenSSLCertificate $certificate): void

Убрать из старого кода предположение о ручном закрытии сертификата-ресурса.

Функция принимает объект, но в PHP 8 не делает ничего. Первый пример намеренно ловит уведомление о deprecated; второй показывает обычную область видимости.

Наблюдать старый free без ошибочного ожидания закрытия объектаСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = openssl_x509_read(file_get_contents(__DIR__ . '/publisher-cert.pem'));
set_error_handler(static function (int $level): bool {
    if ($level === E_DEPRECATED) { echo "Устарело: сертификат освобождается автоматически\n"; return true; }
    return false;
});
try { openssl_x509_free($certificate); } finally { restore_error_handler(); }
echo 'После free: ', openssl_x509_parse($certificate)['subject']['CN'], "\n";
Ожидаемый результат
Устарело: сертификат освобождается автоматически
После free: catalog.example.test

Демонстрация нужна для миграции PHP 7-кода: функция больше не закрывает объект. Она также не отзывает сертификат.

ПараметрДоговор
certificateOpenSSLCertificate; до PHP 8 — ресурс X.509.

Результат и отказ. void. С PHP 8.0 функция устарела и не имеет эффекта.

Вернуть имя из функции без ручного освобожденияСамостоятельный пример
<?php
declare(strict_types=1);

function certificateOwner(string $pem): string {
    $certificate = openssl_x509_read($pem);
    if ($certificate === false) { throw new RuntimeException('Сертификат не прочитан'); }
    return openssl_x509_parse($certificate)['subject']['CN'];
}
echo 'Настроен сертификат: ', certificateOwner(file_get_contents(__DIR__ . '/publisher-cert.pem')), "\n";
Ожидаемый результат
Настроен сертификат: catalog.example.test

Функция возвращает нужную строку, локальная ссылка на объект перестаёт быть нужна. Дополнительный free в новом PHP не требуется.

Границы применения. Управление PHP-ссылками не равно отзыву сертификата или удалению PEM с диска.

Следующий шаг: openssl_x509_read().

openssl_get_cert_locationsНайти, откуда PHP/OpenSSL берёт сведения о расположении доверенных CA.

PHP 5.6+ · openssl

openssl_get_cert_locations(): array

Найти, откуда PHP/OpenSSL берёт сведения о расположении доверенных CA.

Функция без аргументов возвращает массив. default_cert_file_env/default_cert_dir_env — имена переменных среды; getenv читает их значения.

Узнать имена настроек поиска CA в этой сборкеСамостоятельный пример
<?php
declare(strict_types=1);

$locations = openssl_get_cert_locations();
echo 'Переменная для CA-файла: ', $locations['default_cert_file_env'], "\n";
echo 'Переменная для CA-каталога: ', $locations['default_cert_dir_env'], "\n";
echo "Настройки PHP: openssl.cafile и openssl.capath\n";
Ожидаемый результат
Переменная для CA-файла: SSL_CERT_FILE
Переменная для CA-каталога: SSL_CERT_DIR
Настройки PHP: openssl.cafile и openssl.capath

Метод показывает конфигурационные ориентиры. Имя переменной среды отличается от её значения, а прописанный путь — от реально существующего файла. Полный массив в вашей среде покажет default_cert_file/dir и ini_cafile/capath.

ПараметрДоговор

Результат и отказ. Массив расположений: default_cert_file, default_cert_file_env, default_cert_dir, default_cert_dir_env, default_private_dir, default_default_cert_area, ini_cafile, ini_capath.

Найти явно выбранный локальный CA и применить егоСамостоятельный пример
<?php
declare(strict_types=1);

$locations = openssl_get_cert_locations();
$variable = $locations['default_cert_file_env'];
$previous = getenv($variable);
putenv($variable . '=' . __DIR__ . '/lab-ca.pem');
try {
    $path = getenv($variable);
    if ($path === false || !is_file($path)) { throw new RuntimeException('CA-файл не найден'); }
    $result = openssl_x509_checkpurpose(file_get_contents(__DIR__ . '/publisher-cert.pem'), X509_PURPOSE_SSL_SERVER, [$path]);
    echo basename($path), ': ', $result === true ? 'цепочка сертификата каталога подтверждена' : 'цепочка отклонена', "\n";
} finally {
    putenv($previous === false ? $variable : $variable . '=' . $previous);
}
Ожидаемый результат
lab-ca.pem: цепочка сертификата каталога подтверждена

Переменная изменяется только в процессе примера и затем восстанавливается. Для наглядности путь передан checkpurpose явно: этот запуск не доказывает, что другой процесс/веб-сервер уже подхватил ту же среду, и не выполняет TLS.

Границы применения. Наличие строки пути не гарантирует существование, правильный формат и доверенность её содержимого. Каталог CA должен быть правильно хеширован. CLI и веб-сервер могут иметь разные php.ini и переменные среды.

Следующий шаг: openssl_x509_checkpurpose().

openssl_public_encryptПолучить RSA-блок для получателя с известным открытым ключом.

PHP 8.2+ · openssl

openssl_public_encrypt( string $data, string &$encrypted_data, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key, int $padding = OPENSSL_PKCS1_PADDING, ?string $digest_algo = null): bool

Получить RSA-блок для получателя с известным открытым ключом.

Аргументы идут в порядке: входные байты, выходная переменная по ссылке, ключ, padding, digest_algo. Результат вызова bool, а само сообщение находится во втором аргументе.

Восстановить короткое сообщение для владельца ключаСамостоятельный пример
<?php
declare(strict_types=1);

$public = file_get_contents(__DIR__ . '/publisher-public.pem');
if (!openssl_public_encrypt('Код ячейки: B17', $ciphertext, $public, OPENSSL_PKCS1_OAEP_PADDING, 'sha256')) { throw new RuntimeException('Сообщение не зашифровано'); }
file_put_contents(__DIR__ . '/cell.b64', base64_encode($ciphertext));
if (!openssl_private_decrypt($ciphertext, $text, file_get_contents(__DIR__ . '/publisher-private.pem'), OPENSSL_PKCS1_OAEP_PADDING, 'sha256')) { throw new RuntimeException('Проверка не пройдена'); }
echo $text, "\n";
Ожидаемый результат
Код ячейки: B17

Бинарный блок хранится в Base64 только ради переносимого текстового файла. Base64 не добавляет шифрование. Закрытый ключ опубликован лишь для воспроизводимости лаборатории.

ПараметрДоговор
dataИсходные байты, помещающиеся в один RSA-блок с выбранным padding.
encrypted_dataВыходная строка по ссылке; использовать только после успеха.
public_keyОткрытый ключ адресата, а не отправителя.
paddingПо умолчанию PKCS1 v1.5; явно согласуйте OAEP для нового RSA-протокола. NO_PADDING — сырой примитив, не безопасный режим для пользовательских данных. SSLV23_PADDING упомянут исторически, отсутствует в актуальном OpenSSL 3.
digest_algoС PHP 8.5: алгоритм OAEP либо null для библиотечного default. Здесь обе стороны используют sha256.

Результат и отказ. true при успехе, false при отказе. Проверяйте результат перед чтением выходной переменной.

Остановить неподходящую операцию до использования данныхСамостоятельный пример
<?php
declare(strict_types=1);

$public = file_get_contents(__DIR__ . '/publisher-public.pem');
$longDocument = str_repeat('Строка накладной A1. ', 20);
$bytes = intdiv(openssl_pkey_get_details(openssl_pkey_get_public($public))['bits'], 8);
$limit = $bytes - 2 * 32 - 2;
if (strlen($longDocument) > $limit) {
    echo 'Документ длиннее ', $limit, " байт: выбираем контейнер CMS\n";
} else {
    openssl_public_encrypt($longDocument, $ciphertext, $public, OPENSSL_PKCS1_OAEP_PADDING, 'sha256');
}
Ожидаемый результат
Документ длиннее 190 байт: выбираем контейнер CMS

Отказ не превращается в пустое успешное сообщение. Для сетевого сервиса не возвращайте подробности внутренних ошибок расшифрования клиенту; единое поведение задаёт протокол.

Границы применения. RSA сам не удостоверяет отправителя и не задаёт формат пакета. У PKCS1 v1.5 есть риски padding oracle; поведение implicit rejection зависит от OpenSSL/provider, поэтому один bool не заменяет целый безопасный протокол.

Версии и совместимость. OpenSSL-объекты с PHP 8.0, digest_algo с PHP 8.5; примеры требуют PHP 8.5.

Следующий шаг: openssl_cms_encrypt(), openssl_sign().

openssl_private_decryptПолучить исходное короткое сообщение, адресованное владельцу ключа.

PHP 8.2+ · openssl

openssl_private_decrypt( string $data, string &$decrypted_data, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, int $padding = OPENSSL_PKCS1_PADDING, ?string $digest_algo = null): bool

Получить исходное короткое сообщение, адресованное владельцу ключа.

Аргументы идут в порядке: входные байты, выходная переменная по ссылке, ключ, padding, digest_algo. Результат вызова bool, а само сообщение находится во втором аргументе.

Восстановить короткое сообщение для владельца ключаСамостоятельный пример
<?php
declare(strict_types=1);

$ciphertext = base64_decode(file_get_contents(__DIR__ . '/rsa-message.b64'), true);
$key = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_private_decrypt($ciphertext, $text, $key, OPENSSL_PKCS1_OAEP_PADDING, 'sha256')) { throw new RuntimeException('Сообщение не открыто'); }
echo $text, "\n";
Ожидаемый результат
Код ячейки: B17

Бинарный блок хранится в Base64 только ради переносимого текстового файла. Base64 не добавляет шифрование. Закрытый ключ опубликован лишь для воспроизводимости лаборатории.

ПараметрДоговор
dataЗашифрованный бинарный блок.
decrypted_dataВыходная строка по ссылке; использовать только после успеха.
private_keyСоответствующий закрытый ключ.
paddingПо умолчанию PKCS1 v1.5; явно согласуйте OAEP для нового RSA-протокола. NO_PADDING — сырой примитив, не безопасный режим для пользовательских данных. SSLV23_PADDING упомянут исторически, отсутствует в актуальном OpenSSL 3.
digest_algoС PHP 8.5: алгоритм OAEP либо null для библиотечного default. Здесь обе стороны используют sha256.

Результат и отказ. true при успехе, false при отказе. Проверяйте результат перед чтением выходной переменной.

Остановить неподходящую операцию до использования данныхСамостоятельный пример
<?php
declare(strict_types=1);

$ciphertext = base64_decode(file_get_contents(__DIR__ . '/rsa-message.b64'), true);
foreach (['recipient-private.pem', 'publisher-private.pem'] as $file) {
    $ok = openssl_private_decrypt($ciphertext, $text, file_get_contents(__DIR__ . '/' . $file), OPENSSL_PKCS1_OAEP_PADDING, 'sha256');
    echo $file, ': ', $ok ? $text : 'отказ, данные не использовать', "\n";
}
Ожидаемый результат
recipient-private.pem: отказ, данные не использовать
publisher-private.pem: Код ячейки: B17

Отказ не превращается в пустое успешное сообщение. Для сетевого сервиса не возвращайте подробности внутренних ошибок расшифрования клиенту; единое поведение задаёт протокол.

Границы применения. RSA сам не удостоверяет отправителя и не задаёт формат пакета. У PKCS1 v1.5 есть риски padding oracle; поведение implicit rejection зависит от OpenSSL/provider, поэтому один bool не заменяет целый безопасный протокол.

Версии и совместимость. OpenSSL-объекты с PHP 8.0, digest_algo с PHP 8.5; примеры требуют PHP 8.5.

Следующий шаг: openssl_cms_encrypt(), openssl_sign().

openssl_private_encryptОбслужить старый формат, где RSA-операция выполняется закрытым ключом.

PHP 8.2+ · openssl

openssl_private_encrypt( string $data, string &$encrypted_data, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, int $padding = OPENSSL_PKCS1_PADDING): bool

Обслужить старый формат, где RSA-операция выполняется закрытым ключом.

data — короткий исходный блок; второй аргумент получает результат по ссылке; третий выбирает ключ нужной стороны. В четвёртом аргументе поддержаны PKCS1_PADDING и NO_PADDING.

Прочитать короткую запись старого RSA-форматаСамостоятельный пример
<?php
declare(strict_types=1);

$public = file_get_contents(__DIR__ . '/publisher-public.pem');
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_private_encrypt('legacy-record:A1', $block, $private, OPENSSL_PKCS1_PADDING)) { throw new RuntimeException('Старый блок не создан'); }

if (!openssl_public_decrypt($block, $record, $public, OPENSSL_PKCS1_PADDING)) { throw new RuntimeException('Старый блок не прочитан'); }
echo $record, "\n";
Ожидаемый результат
legacy-record:A1

Это совместимость со старым специально заданным форматом. Любой обладатель открытого ключа прочтёт запись: конфиденциальности здесь нет.

ПараметрДоговор
dataКороткая исходная строка старого протокола.
encrypted_dataРезультат по ссылке.
private_keyЗакрытый ключ автора блока.
paddingPKCS1_PADDING по умолчанию; NO_PADDING оставлен для низкоуровневых форматов, не для проектирования собственной подписи.

Результат и отказ. true при успехе, false при отказе.

Заменить старый формат стандартной операцией подписиСамостоятельный пример
<?php
declare(strict_types=1);

$record = 'legacy-record:A1';
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_sign($record, $signature, $private, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
$public = file_get_contents(__DIR__ . '/publisher-public.pem');
echo openssl_verify($record, $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "A1: современная подпись проверена\n" : "A1: отказ\n";
Ожидаемый результат
A1: современная подпись проверена

Переход меняет формат подписи и требует обновить обе стороны. Нельзя просто поменять имя функции и сохранить прежние блоки.

Границы применения. В Manual операция связана с подписью данных или хеша, но это не готовая схема подписи с современными правилами кодирования. Для новых задач используйте openssl_sign/verify, а для секретности — encrypt/decrypt соответствующего протокола.

Версии и совместимость. Объектные ключи принимаются с PHP 8.0.

Следующий шаг: openssl_sign(), openssl_verify().

openssl_public_decryptОбслужить старый формат, где RSA-операция читается открытым ключом.

PHP 8.2+ · openssl

openssl_public_decrypt( string $data, string &$decrypted_data, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key, int $padding = OPENSSL_PKCS1_PADDING): bool

Обслужить старый формат, где RSA-операция читается открытым ключом.

data — короткий исходный блок; второй аргумент получает результат по ссылке; третий выбирает ключ нужной стороны. В четвёртом аргументе поддержаны PKCS1_PADDING и NO_PADDING.

Прочитать короткую запись старого RSA-форматаСамостоятельный пример
<?php
declare(strict_types=1);

$public = file_get_contents(__DIR__ . '/publisher-public.pem');
$block = base64_decode(file_get_contents(__DIR__ . '/legacy-rsa.b64'), true);

if (!openssl_public_decrypt($block, $record, $public, OPENSSL_PKCS1_PADDING)) { throw new RuntimeException('Старый блок не прочитан'); }
echo $record, "\n";
Ожидаемый результат
legacy-record:A1

Это совместимость со старым специально заданным форматом. Любой обладатель открытого ключа прочтёт запись: конфиденциальности здесь нет.

ПараметрДоговор
dataБинарный старый RSA-блок.
decrypted_dataРезультат по ссылке.
public_keyОткрытый ключ автора блока.
paddingPKCS1_PADDING по умолчанию; NO_PADDING оставлен для низкоуровневых форматов, не для проектирования собственной подписи.

Результат и отказ. true при успехе, false при отказе.

Заменить старый формат стандартной операцией подписиСамостоятельный пример
<?php
declare(strict_types=1);

$record = 'legacy-record:A1';
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_sign($record, $signature, $private, OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
$public = file_get_contents(__DIR__ . '/publisher-public.pem');
echo openssl_verify($record, $signature, $public, OPENSSL_ALGO_SHA256) === 1
    ? "A1: современная подпись проверена\n" : "A1: отказ\n";
Ожидаемый результат
A1: современная подпись проверена

Переход меняет формат подписи и требует обновить обе стороны. Нельзя просто поменять имя функции и сохранить прежние блоки.

Границы применения. В Manual операция связана с подписью данных или хеша, но это не готовая схема подписи с современными правилами кодирования. Для новых задач используйте openssl_sign/verify, а для секретности — encrypt/decrypt соответствующего протокола.

Версии и совместимость. Объектные ключи принимаются с PHP 8.0.

Следующий шаг: openssl_sign(), openssl_verify().

openssl_sealПодготовить одинаковый документ нескольким владельцам ключей.

PHP 8.2+ · openssl

openssl_seal( string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo, string &$iv = null): int|false

Подготовить одинаковый документ нескольким владельцам ключей.

Конверт состоит из данных, IV и индивидуального encrypted_key. seal возвращает длину ciphertext, не его строку; данные и ключи поступают по ссылке.

Передать складское задание одному получателюСамостоятельный пример
<?php
declare(strict_types=1);

$public = openssl_pkey_get_public(file_get_contents(__DIR__ . '/recipient-public.pem'));
if (openssl_seal('Склад: принять A1', $sealed, $keys, [$public], 'aes-256-cbc', $iv) === false) { throw new RuntimeException('Конверт не создан'); }
if (!openssl_open($sealed, $text, $keys[0], file_get_contents(__DIR__ . '/recipient-private.pem'), 'aes-256-cbc', $iv)) { throw new RuntimeException('Конверт не открыт'); }
echo $text, "\n";
Ожидаемый результат
Склад: принять A1

keys[0] соответствует первому ключу в массиве получателей; в готовом пакете лаборатории склад — второй адресат с индексом 1. IV передаётся вместе с данными.

ПараметрДоговор
dataИсходная строка документа.
sealed_dataЗашифрованная строка по ссылке.
encrypted_keysМассив зашифрованных ключей по ссылке; порядок соответствует public_key.
public_keyМассив объектов открытых ключей адресатов.
cipher_algoОбязательное имя поддерживаемого алгоритма.
ivВыходной IV по ссылке. Генерируется случайно: переданное начальное значение будет перезаписано.

Результат и отказ. Длина зашифрованных данных как int при успехе, false при ошибке; проверять строго !== false.

Показать документ обоим адресатам их собственными ключамиСамостоятельный пример
<?php
declare(strict_types=1);

$names = ['publisher', 'recipient'];
$public = array_map(static fn(string $name) => openssl_pkey_get_public(file_get_contents(__DIR__ . '/' . $name . '-public.pem')), $names);
if (openssl_seal('Склад: принять A1', $sealed, $keys, $public, 'aes-256-cbc', $iv) === false) { throw new RuntimeException('Конверт не создан'); }
foreach ($names as $index => $name) {
    $private = file_get_contents(__DIR__ . '/' . $name . '-private.pem');
    if (!openssl_open($sealed, $text, $keys[$index], $private, 'aes-256-cbc', $iv)) { throw new RuntimeException('Конверт не открыт'); }
    echo $name, ': ', $text, "\n";
}
Ожидаемый результат
publisher: Склад: принять A1
recipient: Склад: принять A1

Получатели используют один ciphertext, но разные encrypted_keys. Имена и индексы должны быть частью согласованного формата, чтобы копии ключей не перепутались.

Границы применения. Нельзя выбрать GCM только заменой имени: у этого интерфейса нет параметров тега. Сама схема не аутентифицирует отправителя. Старый default RC4 небезопасен.

Версии и совместимость. С PHP 8.0 cipher_algo обязателен; раньше default RC4. Ключи объектные с 8.0; выходной iv у seal nullable с 8.0.

Следующий шаг: openssl_cms_encrypt(), openssl_cms_sign().

openssl_openВосстановить документ своим ключом конверта.

PHP 8.2+ · openssl

openssl_open( string $data, string &$output, string $encrypted_key, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, string $cipher_algo, ?string $iv = null): bool

Восстановить документ своим ключом конверта.

Конверт состоит из данных, IV и индивидуального encrypted_key. open возвращает bool, текст приходит в output.

Передать складское задание одному получателюСамостоятельный пример
<?php
declare(strict_types=1);

$packet = json_decode(file_get_contents(__DIR__ . '/envelope.json'), true, 512, JSON_THROW_ON_ERROR);
$sealed = base64_decode($packet['ciphertext'], true);
$iv = base64_decode($packet['iv'], true);

$envelopeKey = base64_decode($packet['keys'][1], true);
$private = file_get_contents(__DIR__ . '/recipient-private.pem');
if (!openssl_open($sealed, $text, $envelopeKey, $private, 'aes-256-cbc', $iv)) { throw new RuntimeException('Конверт не открыт'); }
echo $text, "\n";
Ожидаемый результат
Склад: принять A1

keys[0] соответствует первому ключу в массиве получателей; в готовом пакете лаборатории склад — второй адресат с индексом 1. IV передаётся вместе с данными.

ПараметрДоговор
dataЗашифрованные байты конверта.
outputВосстановленная строка по ссылке.
encrypted_keyКопия симметричного ключа, зашифрованная для этого получателя.
private_keyЕго соответствующий закрытый ключ.
cipher_algoТочное имя алгоритма из операции seal.
ivIV от seal; nullable, но обязателен для требующего IV алгоритма.

Результат и отказ. true при успехе, false при отказе.

Показать документ обоим адресатам их собственными ключамиСамостоятельный пример
<?php
declare(strict_types=1);

$names = ['publisher', 'recipient'];
$public = array_map(static fn(string $name) => openssl_pkey_get_public(file_get_contents(__DIR__ . '/' . $name . '-public.pem')), $names);
if (openssl_seal('Склад: принять A1', $sealed, $keys, $public, 'aes-256-cbc', $iv) === false) { throw new RuntimeException('Конверт не создан'); }
foreach ($names as $index => $name) {
    $private = file_get_contents(__DIR__ . '/' . $name . '-private.pem');
    if (!openssl_open($sealed, $text, $keys[$index], $private, 'aes-256-cbc', $iv)) { throw new RuntimeException('Конверт не открыт'); }
    echo $name, ': ', $text, "\n";
}
Ожидаемый результат
publisher: Склад: принять A1
recipient: Склад: принять A1

Получатели используют один ciphertext, но разные encrypted_keys. Имена и индексы должны быть частью согласованного формата, чтобы копии ключей не перепутались.

Границы применения. Нельзя выбрать GCM только заменой имени: у этого интерфейса нет параметров тега. Сама схема не аутентифицирует отправителя. Старый default RC4 небезопасен.

Версии и совместимость. С PHP 8.0 cipher_algo обязателен; раньше default RC4. Ключи объектные с 8.0; выходной iv у seal nullable с 8.0.

Следующий шаг: openssl_cms_encrypt(), openssl_cms_sign().

openssl_pkey_deriveСогласовать общий секрет из чужого открытого и собственного закрытого ключа DH/ECDH.

PHP 7.3+ · openssl

openssl_pkey_derive(OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, int $key_length = 0): string|false

Согласовать общий секрет из чужого открытого и собственного закрытого ключа DH/ECDH.

Первые два аргумента задают стороны. Третий key_length в новом коде опускаем: это не KDF и не параметр требуемой стойкости.

Получить одинаковый секрет с двух сторонСамостоятельный пример
<?php
declare(strict_types=1);

$alicePrivate = file_get_contents(__DIR__ . '/alice-dh-private.pem');
$bobPrivate = file_get_contents(__DIR__ . '/bob-dh-private.pem');
$alice = openssl_pkey_derive(file_get_contents(__DIR__ . '/bob-dh-public.pem'), $alicePrivate);
$bob = openssl_pkey_derive(file_get_contents(__DIR__ . '/alice-dh-public.pem'), $bobPrivate);
if ($alice === false || $bob === false) { throw new RuntimeException('Согласование не выполнено'); }
echo hash_equals($alice, $bob) ? "Алиса и Боб получили общий секрет; его байты не публикуем\n" : "Согласование нарушено\n";
Ожидаемый результат
Алиса и Боб получили общий секрет; его байты не публикуем

Порядок аргументов важен: чужая открытая часть, затем собственная закрытая. Сам секрет не выводится; показано, смогут ли стороны продолжить протокол.

ПараметрДоговор
public_keyОткрытая часть удалённой стороны в поддерживаемой форме.
private_keyСвой закрытый DH/EC-ключ с совместимыми параметрами.
key_lengthИсторическая желаемая длина; default0. С PHP 8.5 параметр deprecated дажепри явнойпередаче 0. Больше размера простого результат не растёт; меньше — ECDH обрезается, DH отказывает.

Результат и отказ. Бинарная строка общего секрета либо false.

Повторить согласование на одной именованной кривойСамостоятельный пример
<?php
declare(strict_types=1);

$options = ['config' => __DIR__ . '/openssl.cnf', 'private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1'];
$aliceKey = openssl_pkey_new($options); $bobKey = openssl_pkey_new($options);
if ($aliceKey === false || $bobKey === false) { throw new RuntimeException('Ключи не созданы'); }
$alice = openssl_pkey_derive(openssl_pkey_get_details($bobKey)['key'], $aliceKey);
$bob = openssl_pkey_derive(openssl_pkey_get_details($aliceKey)['key'], $bobKey);
if ($alice === false || $bob === false) { throw new RuntimeException('Согласование не выполнено'); }
echo hash_equals($alice, $bob) ? "ECDH prime256v1: общий секрет согласован\n" : "Согласование нарушено\n";
Ожидаемый результат
ECDH prime256v1: общий секрет согласован

Ключи создаются заново, норезультат проверки согласованности стабилен. Это изолированная демонстрация; идентичность удалённой стороны здесь не задана.

Границы применения. Без аутентификации обмен допускает подмену собеседника. Не печатайте секрет ине подменяйте KDF его обрезанием.

Версии и совместимость. key_length deprecated с PHP 8.5; явное отсутствие третьего аргумента — путьмиграции.

Следующий шаг: openssl_pkey_new(), openssl_dh_compute_key().

openssl_dh_compute_keyПрочитать старое сообщение, где открытая DH-компонента передана отдельным числом.

PHP 5.3+ · openssl

openssl_dh_compute_key(string $public_key, OpenSSLAsymmetricKey $private_key): string|false

Прочитать старое сообщение, где открытая DH-компонента передана отдельным числом.

public_key принимает сырые байты числа удалённой стороны; private_key обязательно объект локального DH-ключа. Для PEM и ECDH удобнее pkey_derive.

Использовать открытое DH-число из старого формата обменаСамостоятельный пример
<?php
declare(strict_types=1);

$aliceKey = openssl_pkey_get_private(file_get_contents(__DIR__ . '/alice-dh-private.pem'));
$bobRaw = hex2bin(trim(file_get_contents(__DIR__ . '/bob-dh-public.hex')));
$secret = openssl_dh_compute_key($bobRaw, $aliceKey);
$reference = openssl_pkey_derive(file_get_contents(__DIR__ . '/alice-dh-public.pem'), file_get_contents(__DIR__ . '/bob-dh-private.pem'));
if ($secret === false || $reference === false) { throw new RuntimeException('DH не выполнен'); }
echo hash_equals($secret, $reference) ? "Открытое DH-число Боба прочитано: общий секрет согласован\n" : "Параметры не совпали\n";
Ожидаемый результат
Открытое DH-число Боба прочитано: общий секрет согласован

Первый аргумент — бинарное DH-число pub_key, а не PEM. hex2bin лишь восстанавливает это число из переносимой текстовой записи.

ПараметрДоговор
public_keyБинарное открытое DH-число удалённой стороны; не PEM ине hex-текст.
private_keyОбъект локального закрытого DH-ключа с теми же параметрами группы.

Результат и отказ. Бинарный общий секрет либо false.

Обнаружить неподдерживаемый тип до выбора другой функцииСамостоятельный пример
<?php
declare(strict_types=1);

$ec = openssl_pkey_new(['config' => __DIR__ . '/openssl.cnf', 'private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
if ($ec === false) { throw new RuntimeException('Ключ не создан'); }
$detail = openssl_pkey_get_details($ec);
$point = "\x04" . str_pad($detail['ec']['x'], 32, "\0", STR_PAD_LEFT) . str_pad($detail['ec']['y'], 32, "\0", STR_PAD_LEFT);
echo openssl_dh_compute_key($point, $ec) === false
    ? "PHP 8.5: этот интерфейс отверг EC; для ECDH используем pkey_derive\n" : "Поведение сборки изменилось\n";
Ожидаемый результат
PHP 8.5: этот интерфейс отверг EC; для ECDH используем pkey_derive

Это целевой тест границы API: текущий php-src проверяет тип EVP_PKEY_DH и возвращает false для EC. Здесь не строится собственный криптографический протокол.

Границы применения. Нужны одинаковые DH-параметры обеих сторон. PHP Manual содержит примечание об ECDH с PHP 8.1/OpenSSL 3, но PHP 8.5.11 явноотвергает EC в этой функции; исходники и приведённый запуск фиксируют различие. Используйте pkey_derive для ECDH.

Версии и совместимость. С PHP 8.0 private_key — объект вместо ресурса. Расхождение оговоркиоб ECDH проверено на PHP 8.5.11 и tagged php-src.

Следующий шаг: openssl_pkey_derive(), openssl_pkey_get_details().

openssl_pkcs12_exportПодготовить перенос ключа и сертификата в бинарную строку PKCS#12.

PHP 5.2.2+ · openssl

openssl_pkcs12_export( OpenSSLCertificate|string $certificate, string &$output, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, string $passphrase, array $options = []): bool

Подготовить перенос ключа и сертификата в бинарную строку PKCS#12.

Порядок: сертификат, место результата, закрытый ключ, пароль, необязательные настройки. Второй аргумент получает бинарную строку по ссылке.

Сохранить удостоверение издателя и прочитать владельцаСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkcs12_export($certificate, $container, $private, 'lab-password')) { throw new RuntimeException('Контейнер не создан'); }

if (!openssl_pkcs12_read($container, $parts, 'lab-password')) { throw new RuntimeException('Контейнер не открыт'); }
echo 'Переносим сертификат: ', openssl_x509_parse($parts['cert'])['subject']['CN'], "\n";
Ожидаемый результат
Переносим сертификат: catalog.example.test

Экспорт вернул bool, а бинарные данные оказались в переменной $container. Читаем только имя сертификата, не выводим закрытый ключ.

ПараметрДоговор
certificatePEM-сертификат, объект либо file://путь.
outputБинарный PKCS#12 по ссылке.
private_keyЗакрытый ключ, соответствующий сертификату.
passphraseПароль защиты контейнера; не пароль учётной записи.
optionsextracerts: один дополнительный сертификат или массив; friendly_name: понятное имя ключа/сертификата. Прочие ключи игнорируются.

Результат и отказ. true при успехе, false при отказе.

Перенести цепочку и подтвердить работу ключа после чтенияСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkcs12_export($certificate, $container, $private, 'lab-password', ['friendly_name' => 'Catalog publisher', 'extracerts' => file_get_contents(__DIR__ . '/lab-ca.pem')])) { throw new RuntimeException('Контейнер не создан'); }

if (!openssl_pkcs12_read($container, $parts, 'lab-password')) { throw new RuntimeException('Контейнер не открыт'); }
if (!openssl_sign('A5', $signature, $parts['pkey'], OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo openssl_verify('A5', $signature, $parts['cert'], OPENSSL_ALGO_SHA256) === 1 ? "A5: перенесённый ключ подписывает документ\n" : "A5: отказ\n";
echo 'Дополнительный сертификат: ', openssl_x509_parse($parts['extracerts'][0])['subject']['CN'], "\n";
Ожидаемый результат
A5: перенесённый ключ подписывает документ
Дополнительный сертификат: PHP Manual Lab CA

friendly_name — удобная подпись контейнера, не доказательство личности. Дополнительный CA включён для построения цепочки; доверять ему приложение должно по отдельному решению.

Границы применения. Контейнер содержит секрет. Поддержка старых P12 зависит от алгоритмов и providers OpenSSL: unsupported после обновления не всегда означает неверный пароль. Предпочтителен контролируемый перевыпуск современного контейнера, а не глобальное включение legacy provider без разбора.

Версии и совместимость. Объекты сертификатов и ключей принимаются с PHP 8.0.

Следующий шаг: openssl_pkcs12_read(), openssl_pkey_export().

openssl_pkcs12_export_to_fileПодготовить перенос ключа и сертификата в файл PKCS#12.

PHP 5.2.2+ · openssl

openssl_pkcs12_export_to_file( OpenSSLCertificate|string $certificate, string $output_filename, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, string $passphrase, array $options = []): bool

Подготовить перенос ключа и сертификата в файл PKCS#12.

Порядок: сертификат, место результата, закрытый ключ, пароль, необязательные настройки. Второй аргумент задаёт путь файла.

Сохранить удостоверение издателя и прочитать владельцаСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkcs12_export_to_file($certificate, __DIR__ . '/publisher.p12', $private, 'lab-password')) { throw new RuntimeException('Контейнер не создан'); }
$container = file_get_contents(__DIR__ . '/publisher.p12');

if (!openssl_pkcs12_read($container, $parts, 'lab-password')) { throw new RuntimeException('Контейнер не открыт'); }
echo 'Переносим сертификат: ', openssl_x509_parse($parts['cert'])['subject']['CN'], "\n";
Ожидаемый результат
Переносим сертификат: catalog.example.test

Экспорт вернул bool, а бинарные данные оказались в реальном файле publisher.p12. Читаем только имя сертификата, не выводим закрытый ключ.

ПараметрДоговор
certificatePEM-сертификат, объект либо file://путь.
output_filenameПуть результата.
private_keyЗакрытый ключ, соответствующий сертификату.
passphraseПароль защиты контейнера; не пароль учётной записи.
optionsextracerts: один дополнительный сертификат или массив; friendly_name: понятное имя ключа/сертификата. Прочие ключи игнорируются.

Результат и отказ. true при успехе, false при отказе.

Перенести цепочку и подтвердить работу ключа после чтенияСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$private = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkcs12_export_to_file($certificate, __DIR__ . '/publisher.p12', $private, 'lab-password', ['friendly_name' => 'Catalog publisher', 'extracerts' => file_get_contents(__DIR__ . '/lab-ca.pem')])) { throw new RuntimeException('Контейнер не создан'); }
$container = file_get_contents(__DIR__ . '/publisher.p12');

if (!openssl_pkcs12_read($container, $parts, 'lab-password')) { throw new RuntimeException('Контейнер не открыт'); }
if (!openssl_sign('A5', $signature, $parts['pkey'], OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo openssl_verify('A5', $signature, $parts['cert'], OPENSSL_ALGO_SHA256) === 1 ? "A5: перенесённый ключ подписывает документ\n" : "A5: отказ\n";
echo 'Дополнительный сертификат: ', openssl_x509_parse($parts['extracerts'][0])['subject']['CN'], "\n";
Ожидаемый результат
A5: перенесённый ключ подписывает документ
Дополнительный сертификат: PHP Manual Lab CA

friendly_name — удобная подпись контейнера, не доказательство личности. Дополнительный CA включён для построения цепочки; доверять ему приложение должно по отдельному решению.

Границы применения. Контейнер содержит секрет. Поддержка старых P12 зависит от алгоритмов и providers OpenSSL: unsupported после обновления не всегда означает неверный пароль. Предпочтителен контролируемый перевыпуск современного контейнера, а не глобальное включение legacy provider без разбора.

Версии и совместимость. Объекты сертификатов и ключей принимаются с PHP 8.0.

Следующий шаг: openssl_pkcs12_read(), openssl_pkey_export().

openssl_pkcs12_readИзвлечь ключ и сертификат из полученного контейнера без вывода секрета.

PHP 5.2.2+ · openssl

openssl_pkcs12_read(string $pkcs12, array &$certificates, string $passphrase): bool

Извлечь ключ и сертификат из полученного контейнера без вывода секрета.

pkcs12 — бинарные данные, certificates — массив по ссылке, passphrase — пароль. После true доступны cert, pkey и при наличии extracerts; перед дальнейшей подписью проверяем операции.

Прочитать учебный PKCS#12 и найти владельца ключаСамостоятельный пример
<?php
declare(strict_types=1);

$container = base64_decode(file_get_contents(__DIR__ . '/publisher.p12.b64'), true);
if (!openssl_pkcs12_read($container, $parts, 'lab-password')) { throw new RuntimeException('Контейнер не открыт'); }
echo 'Владелец: ', openssl_x509_parse($parts['cert'])['subject']['CN'], "\n";
echo openssl_x509_check_private_key($parts['cert'], $parts['pkey']) ? "Ключ соответствует сертификату\n" : "Пара не совпала\n";
Ожидаемый результат
Владелец: catalog.example.test
Ключ соответствует сертификату

Передаётся содержимое контейнера, а не имя файла. Base64 здесь нужна только для текстовой упаковки учебного fixture; обычный .p12 читают сразу file_get_contents.

ПараметрДоговор
pkcs12Содержимое PKCS#12, не путь.
certificatesВыходной массив PEM: cert, pkey, при наличии extracerts. Не печатайте массив целиком.
passphraseПароль контейнера.

Результат и отказ. true при успешном чтении, false при отказе.

Остановить импорт с неверным паролем, затем подписать документСамостоятельный пример
<?php
declare(strict_types=1);

$container = base64_decode(file_get_contents(__DIR__ . '/publisher.p12.b64'), true);
if (openssl_pkcs12_read($container, $wrongParts, 'wrong-password')) { throw new RuntimeException('Неверный пароль неожиданно принят'); }
echo "Неверный пароль: импорт остановлен\n";
while (openssl_error_string() !== false) {}
if (!openssl_pkcs12_read($container, $parts, 'lab-password')) { throw new RuntimeException('Контейнер не открыт'); }
if (!openssl_sign('A6', $signature, $parts['pkey'], OPENSSL_ALGO_SHA256)) { throw new RuntimeException('Подпись не создана'); }
echo openssl_verify('A6', $signature, $parts['cert'], OPENSSL_ALGO_SHA256) === 1 ? "Верный пароль: A6 подписан\n" : "A6: отказ\n";
Ожидаемый результат
Неверный пароль: импорт остановлен
Верный пароль: A6 подписан

Две независимые попытки используют разные выходные переменные; данные неуспешной попытки не считаются импортированными. Очередь ошибки первой попытки очищается перед второй.

Границы применения. Успех чтения не проверяет доверие сертификату. pkey после извлечения содержит закрытый материал. При unsupported сверяйте алгоритмы старого контейнера и providers; при invalid password не пытайтесь использовать незаполненный массив.

Следующий шаг: openssl_pkcs12_export(), openssl_error_string().

openssl_spki_newПодготовить запрос регистрации для системы, которая требует SPKAC.

PHP 5.6+ · openssl

openssl_spki_new(OpenSSLAsymmetricKey $private_key, string $challenge, int $digest_algo = OPENSSL_ALGO_MD5): string|false

Подготовить запрос регистрации для системы, которая требует SPKAC.

Первый аргумент — объект закрытого ключа, второй — challenge, третий — целочисленная константа алгоритма подписи.

Создать подписанный запрос регистрации ключаСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
if ($key === false) { throw new RuntimeException('Ключ не прочитан'); }
$request = openssl_spki_new($key, 'enrollment:A1', OPENSSL_ALGO_SHA256);
if ($request === false) { throw new RuntimeException('Запрос не создан'); }
if (str_starts_with($request, 'SPKAC=')) { $request = substr($request, 6); }
if (!openssl_spki_verify($request)) { throw new RuntimeException('Запрос не создан'); }
echo 'Запрос подписан; challenge: ', openssl_spki_export_challenge($request), "\n";
Ожидаемый результат
Запрос подписан; challenge: enrollment:A1

SHA-256 передан явно: исторический default MD5 не используется. Подписывает закрытая часть, в структуру помещается открытая.

ПараметрДоговор
private_keyОбъект закрытого ключа, полученный pkey_new/get_private.
challengeПроверочная строка регистрации; не пароль и не секрет внутри структуры.
digest_algoOPENSSL_ALGO_*; default MD5 исторический, здесь явно SHA256. Неизвестный алгоритм вызывает E_WARNING.

Результат и отказ. Строка SPKAC либо false.

Сохранить запрос и сверить ключ с ожидаемым издателемСамостоятельный пример
<?php
declare(strict_types=1);

$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/publisher-private.pem'));
$request = openssl_spki_new($key, 'enrollment:A2', OPENSSL_ALGO_SHA256);
if ($request === false) { throw new RuntimeException('Запрос не создан'); }
file_put_contents(__DIR__ . '/enrollment.spkac', $request);
$saved = trim(file_get_contents(__DIR__ . '/enrollment.spkac'));
if (str_starts_with($saved, 'SPKAC=')) { $saved = substr($saved, 6); }
$public = openssl_spki_export($saved);
echo $public === file_get_contents(__DIR__ . '/publisher-public.pem') ? "enrollment.spkac: открытый ключ издателя сохранён\n" : "Ключ не совпал\n";
Ожидаемый результат
enrollment.spkac: открытый ключ издателя сохранён

В файл попал запрос с открытой частью; закрытый PEM остаётся отдельным. Сравнение идентичного PEM уместно именно для данных этой лаборатории; произвольные представления одного ключа могут различаться.

Границы применения. Формат сам не удостоверяет личность и не защищает от повторного использования старой регистрации. Фраза Manual об открытой части, подписывающей CSR, неточна: запрос подписывается закрытым ключом, открытая часть включается в него.

Версии и совместимость. Объект ключа вместо ресурса с PHP 8.0.

Следующий шаг: openssl_spki_verify(), openssl_csr_new().

openssl_spki_verifyПроверить целостность подписанного запроса регистрации.

PHP 5.6+ · openssl

openssl_spki_verify(string $spki): bool

Проверить целостность подписанного запроса регистрации.

Функция принимает строку структуры SPKAC, не имя файла. Если внешний формат содержит служебный префикс SPKAC=, удаляют только этот начальный префикс; нельзя произвольно вырезать совпадения внутри данных.

Прочитать конкретный результат из сохранённого запросаСамостоятельный пример
<?php
declare(strict_types=1);

$request = trim(file_get_contents(__DIR__ . '/request.spkac'));
if (str_starts_with($request, 'SPKAC=')) { $request = substr($request, 6); }

echo openssl_spki_verify($request) ? "Подпись запроса регистрации цела\n" : "Запрос отклонён\n";
Ожидаемый результат
Подпись запроса регистрации цела

Сохранённый запрос создан открытой учебной парой; чтение его поля не устанавливает доверие к произвольному отправителю.

ПараметрДоговор
spkiСтрока подписанного открытого ключа и challenge, полученная от совместимого формата.

Результат и отказ. true при корректной подписи, false при отказе. Некорректный аргумент может вызвать E_WARNING.

Не принять корректный запрос от другой регистрацииСамостоятельный пример
<?php
declare(strict_types=1);

$request = trim(file_get_contents(__DIR__ . '/request.spkac'));
if (str_starts_with($request, 'SPKAC=')) { $request = substr($request, 6); }

if (!openssl_spki_verify($request)) { throw new RuntimeException('Подпись запроса нарушена'); }
$challenge = openssl_spki_export_challenge($request);
foreach (['enrollment:A1', 'enrollment:A2'] as $expected) {
    echo $expected, ': ', is_string($challenge) && hash_equals($expected, $challenge)
        ? 'challenge подходит; дальше проверяем срок и одноразовость' : 'другая регистрация, отказ', "\n";
}
Ожидаемый результат
enrollment:A1: challenge подходит; дальше проверяем срок и одноразовость
enrollment:A2: другая регистрация, отказ

Подпись и ожидаемый challenge проверяются отдельно. В реальном сервисе ожидаемое значение связано с сессией на сервере и погашается после успешного завершения.

Границы применения. Извлечение поля не является проверкой подписи. Даже после verify обязательны проверка ожидаемого challenge, срока, однократности и принадлежности регистрации.

Следующий шаг: openssl_spki_export_challenge(), openssl_csr_new().

openssl_spki_exportИзвлечь открытый PEM для следующей проверки подписи.

PHP 5.6+ · openssl

openssl_spki_export(string $spki): string|false

Извлечь открытый PEM для следующей проверки подписи.

Функция принимает строку структуры SPKAC, не имя файла. Если внешний формат содержит служебный префикс SPKAC=, удаляют только этот начальный префикс; нельзя произвольно вырезать совпадения внутри данных.

Прочитать конкретный результат из сохранённого запросаСамостоятельный пример
<?php
declare(strict_types=1);

$request = trim(file_get_contents(__DIR__ . '/request.spkac'));
if (str_starts_with($request, 'SPKAC=')) { $request = substr($request, 6); }

$public = openssl_spki_export($request);
if ($public === false) { throw new RuntimeException('Ключ не извлечён'); }
echo strtok($public, "\n"), "\n";
$signature = base64_decode(file_get_contents(__DIR__ . '/invoice.sig.b64'), true);
echo openssl_verify(file_get_contents(__DIR__ . '/invoice.txt'), $signature, $public, OPENSSL_ALGO_SHA256) === 1 ? "A1: ключ запроса подходит к подписи накладной\n" : "A1: отказ\n";
Ожидаемый результат
-----BEGIN PUBLIC KEY-----
A1: ключ запроса подходит к подписи накладной

Сохранённый запрос создан открытой учебной парой; чтение его поля не устанавливает доверие к произвольному отправителю.

ПараметрДоговор
spkiСтрока подписанного открытого ключа и challenge, полученная от совместимого формата.

Результат и отказ. PEM открытого ключа либо false. Некорректный аргумент может вызвать E_WARNING.

Не принять корректный запрос от другой регистрацииСамостоятельный пример
<?php
declare(strict_types=1);

$request = trim(file_get_contents(__DIR__ . '/request.spkac'));
if (str_starts_with($request, 'SPKAC=')) { $request = substr($request, 6); }

if (!openssl_spki_verify($request)) { throw new RuntimeException('Подпись запроса нарушена'); }
$challenge = openssl_spki_export_challenge($request);
foreach (['enrollment:A1', 'enrollment:A2'] as $expected) {
    echo $expected, ': ', is_string($challenge) && hash_equals($expected, $challenge)
        ? 'challenge подходит; дальше проверяем срок и одноразовость' : 'другая регистрация, отказ', "\n";
}
Ожидаемый результат
enrollment:A1: challenge подходит; дальше проверяем срок и одноразовость
enrollment:A2: другая регистрация, отказ

Подпись и ожидаемый challenge проверяются отдельно. В реальном сервисе ожидаемое значение связано с сессией на сервере и погашается после успешного завершения.

Границы применения. Извлечение поля не является проверкой подписи. Даже после verify обязательны проверка ожидаемого challenge, срока, однократности и принадлежности регистрации.

Следующий шаг: openssl_spki_new(), openssl_spki_verify().

openssl_spki_export_challengeУзнать, к какой регистрации относится запрос.

PHP 5.6+ · openssl

openssl_spki_export_challenge(string $spki): string|false

Узнать, к какой регистрации относится запрос.

Функция принимает строку структуры SPKAC, не имя файла. Если внешний формат содержит служебный префикс SPKAC=, удаляют только этот начальный префикс; нельзя произвольно вырезать совпадения внутри данных.

Прочитать конкретный результат из сохранённого запросаСамостоятельный пример
<?php
declare(strict_types=1);

$request = trim(file_get_contents(__DIR__ . '/request.spkac'));
if (str_starts_with($request, 'SPKAC=')) { $request = substr($request, 6); }

$challenge = openssl_spki_export_challenge($request);
if ($challenge === false) { throw new RuntimeException('Challenge не извлечён'); }
echo 'Регистрация: ', $challenge, "\n";
Ожидаемый результат
Регистрация: enrollment:A1

Сохранённый запрос создан открытой учебной парой; чтение его поля не устанавливает доверие к произвольному отправителю.

ПараметрДоговор
spkiСтрока подписанного открытого ключа и challenge, полученная от совместимого формата.

Результат и отказ. Строка challenge либо false. Некорректный аргумент может вызвать E_WARNING.

Не принять корректный запрос от другой регистрацииСамостоятельный пример
<?php
declare(strict_types=1);

$request = trim(file_get_contents(__DIR__ . '/request.spkac'));
if (str_starts_with($request, 'SPKAC=')) { $request = substr($request, 6); }

if (!openssl_spki_verify($request)) { throw new RuntimeException('Подпись запроса нарушена'); }
$challenge = openssl_spki_export_challenge($request);
foreach (['enrollment:A1', 'enrollment:A2'] as $expected) {
    echo $expected, ': ', is_string($challenge) && hash_equals($expected, $challenge)
        ? 'challenge подходит; дальше проверяем срок и одноразовость' : 'другая регистрация, отказ', "\n";
}
Ожидаемый результат
enrollment:A1: challenge подходит; дальше проверяем срок и одноразовость
enrollment:A2: другая регистрация, отказ

Подпись и ожидаемый challenge проверяются отдельно. В реальном сервисе ожидаемое значение связано с сессией на сервере и погашается после успешного завершения.

Границы применения. Извлечение поля не является проверкой подписи. Даже после verify обязательны проверка ожидаемого challenge, срока, однократности и принадлежности регистрации.

Следующий шаг: openssl_spki_new(), openssl_spki_verify().

openssl_password_hashПодготовить хранимое проверочное значение пароля через OpenSSL Argon2.

PHP 8.4+ · openssl

openssl_password_hash(string $algo, string $password, array $options = []): string

Подготовить хранимое проверочное значение пароля через OpenSSL Argon2.

Аргументы начинаются с имени алгоритма; пароль передаётся вторым. Третий — настройки расхода ресурсов; соль создаёт реализация.

Создать проверочное значение и подтвердить входСамостоятельный пример
<?php
declare(strict_types=1);

$hash = openssl_password_hash('argon2id', 'учебный пароль кабинета', ['memory_cost' => 65536, 'time_cost' => 3, 'threads' => 1]);
echo openssl_password_verify('argon2id', 'учебный пароль кабинета', $hash)
    ? "Кабинет: пароль подтверждён\n" : "Кабинет: вход отклонён\n";
Ожидаемый результат
Кабинет: пароль подтверждён

Хеш меняется между запусками благодаря соли, результат проверки остаётся тем же. Параметры примера демонстрируют расход ресурсов; рабочие значения выбирают с измерением нагрузки сервиса, не копированием универсального числа.

ПараметрДоговор
algoargon2id или argon2i; иное имя вызывает ValueError.
passwordИсходный пароль, который не сохраняют открытым текстом.
optionsmemory_cost — память в KiB; time_cost — число проходов (не секунды); threads — параллелизм. Пределы допустимых значений контролирует реализация; неподходящие настройки могут вызвать ValueError.

Результат и отказ. Строка кодированного хеша. Неизвестный algo вызывает ValueError; неизвестный сбой хеширования — Error.

Сохранить запись пользователя и отклонить неверный парольСамостоятельный пример
<?php
declare(strict_types=1);

$record = ['login' => 'warehouse', 'algorithm' => 'argon2id', 'hash' => openssl_password_hash('argon2id', 'пароль лаборатории', ['memory_cost' => 65536, 'time_cost' => 3, 'threads' => 1])];
file_put_contents(__DIR__ . '/account.json', json_encode($record, JSON_THROW_ON_ERROR));
$saved = json_decode(file_get_contents(__DIR__ . '/account.json'), true, 512, JSON_THROW_ON_ERROR);
foreach (['ошибка', 'пароль лаборатории'] as $input) {
    echo $saved['login'], ': ', openssl_password_verify($saved['algorithm'], $input, $saved['hash']) ? 'вход разрешён' : 'вход отклонён', "\n";
}
Ожидаемый результат
warehouse: вход отклонён
warehouse: вход разрешён

Файл нужен для наглядной изолированной лаборатории. В сервисе запись хранится в закрытом хранилище пользователей, исходный пароль не записывается в неё и не выводится в лог.

Границы применения. Доступность зависит от HAVE_OPENSSL_ARGON2 при сборке PHP/OpenSSL, а не только номера PHP. Проверьте function_exists при развёртывании; не подменяйте отсутствующий Argon2 быстрым SHA-256.

Версии и совместимость. Функции доступны только при сборке с OpenSSL Argon2. time_cost — параметр трудоёмкости, его фактическое время зависит от оборудования.

Следующий шаг: openssl_password_verify().

openssl_password_verifyПроверить введённый пароль через выбранный OpenSSL Argon2.

PHP 8.4+ · openssl

openssl_password_verify(string $algo, string $password, string $hash): bool

Проверить введённый пароль через выбранный OpenSSL Argon2.

Аргументы начинаются с имени алгоритма; пароль передаётся вторым. Третий — ранее сохранённый хеш.

Создать проверочное значение и подтвердить входСамостоятельный пример
<?php
declare(strict_types=1);

$hash = openssl_password_hash('argon2id', 'учебный пароль кабинета', ['memory_cost' => 65536, 'time_cost' => 3, 'threads' => 1]);
echo openssl_password_verify('argon2id', 'учебный пароль кабинета', $hash)
    ? "Кабинет: пароль подтверждён\n" : "Кабинет: вход отклонён\n";
Ожидаемый результат
Кабинет: пароль подтверждён

Хеш меняется между запусками благодаря соли, результат проверки остаётся тем же. Параметры примера демонстрируют расход ресурсов; рабочие значения выбирают с измерением нагрузки сервиса, не копированием универсального числа.

ПараметрДоговор
algoargon2id или argon2i; иное имя вызывает ValueError.
passwordВведённый пароль; секретный аргумент.
hashСтрока хеша, полученная при регистрации.

Результат и отказ. true при совпадении, false иначе; неизвестный algo вызывает ValueError.

Сохранить запись пользователя и отклонить неверный парольСамостоятельный пример
<?php
declare(strict_types=1);

$record = ['login' => 'warehouse', 'algorithm' => 'argon2id', 'hash' => openssl_password_hash('argon2id', 'пароль лаборатории', ['memory_cost' => 65536, 'time_cost' => 3, 'threads' => 1])];
file_put_contents(__DIR__ . '/account.json', json_encode($record, JSON_THROW_ON_ERROR));
$saved = json_decode(file_get_contents(__DIR__ . '/account.json'), true, 512, JSON_THROW_ON_ERROR);
foreach (['ошибка', 'пароль лаборатории'] as $input) {
    echo $saved['login'], ': ', openssl_password_verify($saved['algorithm'], $input, $saved['hash']) ? 'вход разрешён' : 'вход отклонён', "\n";
}
Ожидаемый результат
warehouse: вход отклонён
warehouse: вход разрешён

Файл нужен для наглядной изолированной лаборатории. В сервисе запись хранится в закрытом хранилище пользователей, исходный пароль не записывается в неё и не выводится в лог.

Границы применения. Доступность зависит от HAVE_OPENSSL_ARGON2 при сборке PHP/OpenSSL, а не только номера PHP. Проверьте function_exists при развёртывании; не подменяйте отсутствующий Argon2 быстрым SHA-256.

Версии и совместимость. Функции доступны только при сборке с OpenSSL Argon2. time_cost — параметр трудоёмкости, его фактическое время зависит от оборудования.

Следующий шаг: openssl_password_hash().

openssl_pkcs7_signВыпустить подписанный файл накладной в S/MIME.

PHP 4.0.6+ · openssl

openssl_pkcs7_sign( string $input_filename, string $output_filename, OpenSSLCertificate|string $certificate, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, ?array $headers, int $flags = PKCS7_DETACHED, ?string $untrusted_certificates_filename = null): bool

Выпустить подписанный файл накладной в S/MIME.

Первые два аргумента — обычные пути input/output, затем сертификат и закрытый ключ. Это файловые API: строки путей здесь не требуют file://; ключевые параметры имеют свои формы.

Подписать накладную и прочитать проверенный документСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$key = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_pkcs7_sign(__DIR__ . '/invoice.txt', __DIR__ . '/out.signed.eml', $certificate, $key, [], PKCS7_BINARY)) { throw new RuntimeException('Документ не подписан'); }
if (openssl_pkcs7_verify(__DIR__ . '/out.signed.eml', PKCS7_BINARY, null, [__DIR__ . '/lab-ca.pem'], null, __DIR__ . '/verified.txt') !== true) { throw new RuntimeException('Подпись не принята'); }
echo file_get_contents(__DIR__ . '/verified.txt');
Ожидаемый результат
Заказ A1: 1200 RUB

Закрытый ключ подписывает данные, сертификат сопровождает подпись. Доверие задано отдельным lab-ca.pem. Показан текст, извлечённый только после успешной проверки.

ПараметрДоговор
input_filenameПуть существующего файла с исходными байтами.
output_filenameПуть создаваемого контейнера.
certificateСертификат подписанта: PEM/объект/file://путь.
private_keyСоответствующий закрытый ключ.
headersМассив почтовых заголовков либо null; у CMS используется только для S/MIME.
flagsБитовая маска PKCS7_*; defaultPKCS7_DETACHED. BINARY сохраняет байты; DETACHED отделяет содержимое.
untrusted_certificates_filenamePEM-файл дополнительных сертификатов для включения; они помогают цепочке, но не становятся доверенными корнями.

Результат и отказ. true при успехе, false при отказе.

Подготовить форму обмена, которую ожидает получательСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$key = file_get_contents(__DIR__ . '/publisher-private.pem');
$headers = ['Subject' => 'Invoice A1', 'From: publisher@example.test'];
if (!openssl_pkcs7_sign(__DIR__ . '/invoice.txt', __DIR__ . '/mail.eml', $certificate, $key, $headers, PKCS7_BINARY | PKCS7_DETACHED, __DIR__ . '/lab-ca.pem')) { throw new RuntimeException('Подпись не создана'); }
$ok = openssl_pkcs7_verify(__DIR__ . '/mail.eml', PKCS7_BINARY, __DIR__ . '/signer.pem', [__DIR__ . '/lab-ca.pem']);
if ($ok !== true) { throw new RuntimeException('Подпись не принята'); }
echo 'Подписант: ', openssl_x509_parse(file_get_contents(__DIR__ . '/signer.pem'))['subject']['CN'], "\n";
echo strtok(file_get_contents(__DIR__ . '/mail.eml'), "\n"), "\n";
Ожидаемый результат
Подписант: catalog.example.test
Subject: Invoice A1

Показаны обе формы headers: ключ=>значение и полная строка. Эти внешние почтовые заголовки добавляются после подписи и не становятся подписанным текстом. Не допускайте пользовательских CR/LF в них. Для этого S/MIME-примера исходный текст архива явно хранится с CRLF: подпись относится к согласованным байтам строк.

Границы применения. Существование выходного файла не доказывает успешной подписи: проверяйте bool. Подписанный контейнер не шифрует содержимое. При доверии к CA отдельно устанавливают, что конкретный подписант уполномочен на действие.

Версии и совместимость. CMS API добавлен в PHP 8.0; PKCS7 принимает объекты с PHP 8.0.

Следующий шаг: openssl_pkcs7_verify(), openssl_x509_checkpurpose().

openssl_pkcs7_verifyПолучить текст накладной, подпись которого проверена относительно выбранного CA.

PHP 4.0.6+ · openssl

openssl_pkcs7_verify( string $input_filename, int $flags, ?string $signers_certificates_filename = null, array $ca_info = [], ?string $untrusted_certificates_filename = null, ?string $content = null, ?string $output_filename = null): bool|int

Получить текст накладной, подпись которого проверена относительно выбранного CA.

Вызов одновременно разбирает контейнер и проверяет подпись. Мы принимаем только === true. Пути content и certificates обозначают результаты, тогда как input_filename и sigfile — входы.

Принять документ только после проверки подписи и цепочкиСамостоятельный пример
<?php
declare(strict_types=1);

$ok = openssl_pkcs7_verify(__DIR__ . '/invoice.signed.eml', PKCS7_BINARY, __DIR__ . '/signer.pem', [__DIR__ . '/lab-ca.pem'], null, __DIR__ . '/verified.txt');
if ($ok !== true) { throw new RuntimeException('Подпись не принята'); }
echo file_get_contents(__DIR__ . '/verified.txt');
echo 'Подписант: ', openssl_x509_parse(file_get_contents(__DIR__ . '/signer.pem'))['subject']['CN'], "\n";
Ожидаемый результат
Заказ A1: 1200 RUB
Подписант: catalog.example.test

Отдельно показаны проверенные данные и извлечённый подписант. CA-файл получен заранее; сертификат из самого входного контейнера автоматически доверенным не становится.

ПараметрДоговор
input_filenameПодписанный контейнер; у CMS с DETACHED+sigfile — путь исходного отдельного документа.
flagsМаска PKCS7_*; обязательна. Опасные NOVERIFY/NOSIGS отключают часть проверки и не исправляют доверие.
signers_certificates_filenameВыходной файл PEM-сертификатов подписантов после успеха, не источник доверенных CA.
ca_infoМассив путей доверенных CA-файлов/хешированных каталогов.
untrusted_certificates_filenameВходной PEM-файл промежуточных сертификатов, не доверенный корень.
contentВыходной файл извлечённого проверяемого документа; использовать только после успешного bool.
output_filenameНеобязательный выходной PEM контейнера. В PHP 8.5 запись находится в ветке обработки файла подписантов: задайте также certificates/signers_certificates_filename.

Результат и отказ. true при успехе, false при неверной подписи/сертификате,−1 при ошибке.−1 truthy: обычный if недопустим.

Отклонить накладную с изменённой суммойСамостоятельный пример
<?php
declare(strict_types=1);

$original = file_get_contents(__DIR__ . '/invoice.signed.eml');
$changed = str_replace('1200', '9900', $original);
if ($changed === $original) { throw new RuntimeException('Текст для изменения не найден'); }
file_put_contents(__DIR__ . '/changed.eml', $changed);
foreach (['invoice.signed.eml', 'changed.eml'] as $file) {
    $ok = openssl_pkcs7_verify(__DIR__ . '/' . $file, PKCS7_BINARY, null, [__DIR__ . '/lab-ca.pem']);
    echo $file, ': ', $ok === true ? 'документ принят' : 'подпись не подтверждена', "\n";
}
Ожидаемый результат
invoice.signed.eml: документ принят
changed.eml: подпись не подтверждена

Изменённые данные сохранены в новом файле, оригинал остаётся для сравнения. Выходной content-файл после отказа нельзя использовать даже если он существует или частично записан. Готовая отделённая подпись была создана для CRLF-оригинала: архив сохраняет именно эти байты; замена CRLF на LF также нарушила бы подпись.

Границы применения. Успех доверенной цепочки не заменяет разрешения именно этого подписанта на заказ. У PKCS7 вход S/MIME имеет MIME-правила, включая ограничение 76 символов строки Base64 по RFC2045. Не исправляйте DER добавлением случайных заголовков или снятием проверки.

Версии и совместимость. output_filename добавлен в PHP 7.2; необязательные файловые аргументы nullable с PHP 8.0.

Следующий шаг: openssl_pkcs7_sign(), openssl_pkcs7_read(), openssl_error_string().

openssl_pkcs7_encryptЗашифровать документ для владельца известного сертификата.

PHP 4.0.6+ · openssl

openssl_pkcs7_encrypt( string $input_filename, string $output_filename, OpenSSLCertificate|array|string $certificate, ?array $headers, int $flags = 0, int $cipher_algo = OPENSSL_CIPHER_AES_128_CBC): bool

Зашифровать документ для владельца известного сертификата.

Передаются пути входа/выхода, сертификат или массив получателей, заголовки и форматные настройки. Это не тот же интерфейс, что строковый openssl_encrypt.

Адресовать накладную складу и открыть её ключом складаСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/recipient-cert.pem');
if (!openssl_pkcs7_encrypt(__DIR__ . '/invoice.txt', __DIR__ . '/encrypted.eml', $certificate, [], PKCS7_BINARY, OPENSSL_CIPHER_AES_256_CBC)) { throw new RuntimeException('Шифрование не выполнено'); }
if (!openssl_pkcs7_decrypt(__DIR__ . '/encrypted.eml', __DIR__ . '/opened.txt', $certificate, file_get_contents(__DIR__ . '/recipient-private.pem'))) { throw new RuntimeException('Документ не открыт'); }
echo file_get_contents(__DIR__ . '/opened.txt');
Ожидаемый результат
Заказ A1: 1200 RUB

Симметричный ключ создаёт контейнер; переданный сертификат выбирает получателя. AES-CBC скрывает содержимое, но этот пример не подтверждает автора; подпись добавляется отдельным этапом ниже.

ПараметрДоговор
input_filenameПуть исходного файла.
output_filenameПуть зашифрованного контейнера.
certificateОдин сертификат получателя либо массив сертификатов.
headersМассив внешних S/MIME-заголовков либо null. Именованные элементы становятся Name: value, числовые — полной строкой. Не включаются в зашифрованное тело.
flagsМаска PKCS7_*; default0. BINARY сохраняет байты.
cipher_algoOPENSSL_CIPHER_* int. Default с PHP 8.1 — AES128CBC, ранее RC2_40; здесь явно AES256CBC.

Результат и отказ. true при успехе, false при отказе.

Добавить второго получателя без копирования исходного документаСамостоятельный пример
<?php
declare(strict_types=1);

$names = ['publisher', 'recipient'];
$certificates = array_map(static fn(string $name) => file_get_contents(__DIR__ . '/' . $name . '-cert.pem'), $names);
if (!openssl_pkcs7_encrypt(__DIR__ . '/invoice.txt', __DIR__ . '/encrypted.eml', $certificates, [], PKCS7_BINARY, OPENSSL_CIPHER_AES_256_CBC)) { throw new RuntimeException('Шифрование не выполнено'); }

foreach ($names as $index => $name) {
    if (!openssl_pkcs7_decrypt(__DIR__ . '/encrypted.eml', __DIR__ . '/opened.txt', $certificates[$index], file_get_contents(__DIR__ . '/' . $name . '-private.pem'))) { throw new RuntimeException('Получатель не открыл документ'); }

    echo $name, ': ', trim(file_get_contents(__DIR__ . '/opened.txt')), "\n";
}
Ожидаемый результат
publisher: Заказ A1: 1200 RUB
recipient: Заказ A1: 1200 RUB

Массив сертификатов задаёт получателей одного контейнера. У PKCS7 cipher_algo остаётся целочисленной OPENSSL_CIPHER_* константой.

Границы применения. Шифрование не проверяет полномочия отправителя. Для CBC-контейнера проверка подписи должна быть частью согласованного обмена; успешная расшифровка не доказывает целостность. Не отправляйте письма через shell из учебного примера.

Версии и совместимость. DefaultAES128CBC с PHP 8.1: старое упоминание RC2-40 в описании PKCS7 не отражает текущую сигнатуру. Объект сертификата поддержан с PHP 8.0.

Следующий шаг: openssl_pkcs7_decrypt(), openssl_pkcs7_sign().

openssl_pkcs7_decryptВосстановить локально документ, зашифрованный для этого получателя.

PHP 4.0.6+ · openssl

openssl_pkcs7_decrypt( string $input_filename, string $output_filename, OpenSSLCertificate|string $certificate, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string|null $private_key = null): bool

Восстановить локально документ, зашифрованный для этого получателя.

Первые два аргумента — файловые пути. Третий и четвёртый — сертификат и ключ; file_get_contents превращает PEM-файл в содержимое. У CMS кодировка должна совпадать с входом.

Открыть полученный файл своим сертификатом и ключомСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/recipient-cert.pem');
$key = file_get_contents(__DIR__ . '/recipient-private.pem');
if (!openssl_pkcs7_decrypt(__DIR__ . '/invoice.encrypted.eml', __DIR__ . '/opened.txt', $certificate, $key)) { throw new RuntimeException('Документ не открыт'); }
echo file_get_contents(__DIR__ . '/opened.txt');
Ожидаемый результат
Заказ A1: 1200 RUB

Сертификат помогает контейнеру выбрать запись получателя, закрытый ключ открывает её. Данные появляются в выходном файле, а не в bool результата.

ПараметрДоговор
input_filenameПуть входного контейнера.
output_filenameПуть восстановленного документа; использовать только после успеха.
certificateСертификат адресата в поддерживаемой форме PEM/объект/file://путь, не обычное имя файла ключа.
private_keyСоответствующий закрытый ключ; defaultnull пытается получить ключ из аргумента certificate. Для отдельных файлов передавайте явно.

Результат и отказ. true при успехе, false при отказе.

Отказать владельцу другого сертификатаСамостоятельный пример
<?php
declare(strict_types=1);

foreach (['publisher', 'recipient'] as $name) {
    $certificate = file_get_contents(__DIR__ . '/' . $name . '-cert.pem');
    $key = file_get_contents(__DIR__ . '/' . $name . '-private.pem');
    $ok = openssl_pkcs7_decrypt(__DIR__ . '/invoice.encrypted.eml', __DIR__ . '/candidate.txt', $certificate, $key);

    echo $name, ': ', $ok ? trim(file_get_contents(__DIR__ . '/candidate.txt')) : 'не адресат, документ не использовать', "\n";
}
Ожидаемый результат
publisher: не адресат, документ не использовать
recipient: Заказ A1: 1200 RUB

Контейнер адресован только складу. Путь candidate.txt может быть создан до неуспеха, поэтому читать его допустимо лишь после true; имена файлов сами по себе успех не обозначают.

Границы применения. Не используйте частично записанный результат после false. Успешная расшифровка не сообщает автора и не проверяет вложенную подпись; если протокол подписывает затем шифрует, дальше обязательно verify.

Версии и совместимость. Объекты ключей принимаются с PHP 8.0. Текстовое описание CMS-параметров сокращено в Manual; их реальные формы соответствуют общей модели key/certificate.

Следующий шаг: openssl_pkcs7_verify(), openssl_pkcs7_encrypt().

openssl_pkcs7_readИзвлечь публичные сертификаты из PKCS7 для отдельного анализа.

PHP 7.2+ · openssl

openssl_pkcs7_read(string $data, array &$certificates): bool

Извлечь публичные сертификаты из PKCS7 для отдельного анализа.

Первый аргумент получает содержимое PEM-контейнера, второй — массив по ссылке. Название input_filename у CMS вводит в заблуждение: в реализации это строка данных.

Показать владельца сертификата внутри контейнераСамостоятельный пример
<?php
declare(strict_types=1);

$data = file_get_contents(__DIR__ . '/certificates.p7b.pem');
if (!openssl_pkcs7_read($data, $certificates)) { throw new RuntimeException('Контейнер не разобран'); }

foreach ($certificates as $pem) {
    echo 'Вложен сертификат: ', openssl_x509_parse($pem)['subject']['CN'], "\n";
}
Ожидаемый результат
Вложен сертификат: catalog.example.test

Чтение сертификатов не проверяет подписанный документ. Закрытых ключей из SignedData не извлекают: их там быть не должно.

ПараметрДоговор
dataСодержимое PEM CMS/PKCS7. Это не путь и не полное S/MIME-сообщение. Расширение .p7b само не обещает PEM.
certificatesВыходной массив PEM-элементов: сертификаты, атакже имеющиеся CRL. Для CRL нужен соответствующий разбор; не передавайте каждый произвольный элемент в x509_parse без определения типа.

Результат и отказ. true при разборе, false при отказе. В Manual страница отмечена как не полностью документированная; подробности формата сверены с PHP 8.5.11.

Исправить смешение имени файла и его содержимогоСамостоятельный пример
<?php
declare(strict_types=1);

if (openssl_pkcs7_read(__DIR__ . '/certificates.p7b.pem', $wrong) !== false) { throw new RuntimeException('Поведение сборки изменилось'); }
echo "Имя файла вместо содержимого: отказ\n";
$data = file_get_contents(__DIR__ . '/certificates.p7b.pem');
if (!openssl_pkcs7_read($data, $certificates)) { throw new RuntimeException('Контейнер не разобран'); }

foreach ($certificates as $pem) {
    echo 'Вложен сертификат: ', openssl_x509_parse($pem)['subject']['CN'], "\n";
}
file_put_contents(__DIR__ . '/extracted.pem', $certificates[0]);
echo "extracted.pem сохранён; доверие проверяем отдельно\n";
Ожидаемый результат
Имя файла вместо содержимого: отказ
Вложен сертификат: catalog.example.test
extracted.pem сохранён; доверие проверяем отдельно

У CMS аргумент называется input_filename, но PHP 8.5 читает переданную строку через memoryBIO как PEM. Это проверено запуском и подтверждено исходниками; слепая передача пути не работает.

Границы применения. Извлечение не подтверждает подпись, срок, доверие либо полномочия. Нельзя принять сертификат из неизвестного контейнера в корни доверия автоматически.

Следующий шаг: openssl_pkcs7_verify(), openssl_x509_parse().

openssl_cms_signВыпустить подписанный файл накладной в CMS.

PHP 8.0+ · openssl

openssl_cms_sign( string $input_filename, string $output_filename, OpenSSLCertificate|string $certificate, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key, ?array $headers, int $flags = 0, int $encoding = OPENSSL_ENCODING_SMIME, ?string $untrusted_certificates_filename = null): bool

Выпустить подписанный файл накладной в CMS.

Первые два аргумента — обычные пути input/output, затем сертификат и закрытый ключ. Это файловые API: строки путей здесь не требуют file://; ключевые параметры имеют свои формы.

Подписать накладную и прочитать проверенный документСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$key = file_get_contents(__DIR__ . '/publisher-private.pem');
if (!openssl_cms_sign(__DIR__ . '/invoice.txt', __DIR__ . '/out.cms.pem', $certificate, $key, [], OPENSSL_CMS_BINARY, OPENSSL_ENCODING_PEM)) { throw new RuntimeException('Документ не подписан'); }
if (openssl_cms_verify(__DIR__ . '/out.cms.pem', OPENSSL_CMS_BINARY, null, [__DIR__ . '/lab-ca.pem'], null, __DIR__ . '/verified.txt', null, null, OPENSSL_ENCODING_PEM) !== true) { throw new RuntimeException('Подпись не принята'); }
echo file_get_contents(__DIR__ . '/verified.txt');
Ожидаемый результат
Заказ A1: 1200 RUB

Закрытый ключ подписывает данные, сертификат сопровождает подпись. Доверие задано отдельным lab-ca.pem. Показан текст, извлечённый только после успешной проверки.

ПараметрДоговор
input_filenameПуть существующего файла с исходными байтами.
output_filenameПуть создаваемого контейнера.
certificateСертификат подписанта: PEM/объект/file://путь.
private_keyСоответствующий закрытый ключ.
headersМассив почтовых заголовков либо null; у CMS используется только для S/MIME.
flagsБитовая маска OPENSSL_CMS_*; default0. BINARY сохраняет байты; DETACHED отделяет содержимое.
encodingSMIME по умолчанию, DER либо PEM. Кодировка и attached/detached — разные решения.
untrusted_certificates_filenamePEM-файл дополнительных сертификатов для включения; они помогают цепочке, но не становятся доверенными корнями.

Результат и отказ. true при успехе, false при отказе.

Подготовить форму обмена, которую ожидает получательСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/publisher-cert.pem');
$key = file_get_contents(__DIR__ . '/publisher-private.pem');
$flags = OPENSSL_CMS_BINARY | OPENSSL_CMS_DETACHED;
if (!openssl_cms_sign(__DIR__ . '/invoice.txt', __DIR__ . '/invoice.sig.der', $certificate, $key, [], $flags, OPENSSL_ENCODING_DER)) { throw new RuntimeException('Подпись не создана'); }
$ok = openssl_cms_verify(__DIR__ . '/invoice.txt', $flags, null, [__DIR__ . '/lab-ca.pem'], null, null, null, __DIR__ . '/invoice.sig.der', OPENSSL_ENCODING_DER);
echo $ok ? "invoice.txt + invoice.sig.der: отделённая подпись подтверждена\n" : "Документ отклонён\n";
Ожидаемый результат
invoice.txt + invoice.sig.der: отделённая подпись подтверждена

Два файла имеют разные роли: input_filename содержит оригинал, sigfile у verify — входную подпись. DER задаётся явно; почтовые headers к DER не добавляются. Для этого S/MIME-примера исходный текст архива явно хранится с CRLF: подпись относится к согласованным байтам строк.

Границы применения. Существование выходного файла не доказывает успешной подписи: проверяйте bool. Подписанный контейнер не шифрует содержимое. При доверии к CA отдельно устанавливают, что конкретный подписант уполномочен на действие.

Версии и совместимость. CMS API добавлен в PHP 8.0; PKCS7 принимает объекты с PHP 8.0.

Следующий шаг: openssl_cms_verify(), openssl_x509_checkpurpose().

openssl_cms_verifyПолучить текст накладной, подпись которого проверена относительно выбранного CA.

PHP 8.0+ · openssl

openssl_cms_verify( string $input_filename, int $flags = 0, ?string $certificates = null, array $ca_info = [], ?string $untrusted_certificates_filename = null, ?string $content = null, ?string $pk7 = null, ?string $sigfile = null, int $encoding = OPENSSL_ENCODING_SMIME): bool

Получить текст накладной, подпись которого проверена относительно выбранного CA.

Вызов одновременно разбирает контейнер и проверяет подпись. Мы принимаем только === true. Пути content и certificates обозначают результаты, тогда как input_filename и sigfile — входы.

Принять документ только после проверки подписи и цепочкиСамостоятельный пример
<?php
declare(strict_types=1);

$ok = openssl_cms_verify(__DIR__ . '/invoice.cms.pem', OPENSSL_CMS_BINARY, __DIR__ . '/signer.pem', [__DIR__ . '/lab-ca.pem'], null, __DIR__ . '/verified.txt', null, null, OPENSSL_ENCODING_PEM);
if ($ok !== true) { throw new RuntimeException('Подпись не принята'); }
echo file_get_contents(__DIR__ . '/verified.txt');
echo 'Подписант: ', openssl_x509_parse(file_get_contents(__DIR__ . '/signer.pem'))['subject']['CN'], "\n";
Ожидаемый результат
Заказ A1: 1200 RUB
Подписант: catalog.example.test

Отдельно показаны проверенные данные и извлечённый подписант. CA-файл получен заранее; сертификат из самого входного контейнера автоматически доверенным не становится.

ПараметрДоговор
input_filenameПодписанный контейнер; у CMS с DETACHED+sigfile — путь исходного отдельного документа.
flagsМаска OPENSSL_CMS_*; default0. Опасные NOVERIFY/NOSIGS отключают часть проверки и не исправляют доверие.
certificatesВыходной файл PEM-сертификатов подписантов после успеха, не источник доверенных CA.
ca_infoМассив путей доверенных CA-файлов/хешированных каталогов.
untrusted_certificates_filenameВходной PEM-файл промежуточных сертификатов, не доверенный корень.
contentВыходной файл извлечённого проверяемого документа; использовать только после успешного bool.
pk7Необязательный выходной PEM контейнера. В PHP 8.5 запись находится в ветке обработки файла подписантов: задайте также certificates/signers_certificates_filename.
sigfileВходной файл отделённой подписи при DETACHED; input_filename тогда содержит документ. Не выходной файл. Для отдельного sigfile S/MIME не поддержан — выберите DER/PEM.
encodingКодировка подписи: SMIME по умолчанию, DER либо PEM.

Результат и отказ. bool: true при успехе, false при отказе.

Отклонить накладную с изменённой суммойСамостоятельный пример
<?php
declare(strict_types=1);

file_put_contents(__DIR__ . '/signature.der', base64_decode(file_get_contents(__DIR__ . '/invoice.detached.der.b64'), true));
file_put_contents(__DIR__ . '/changed.txt', str_replace('1200', '9900', file_get_contents(__DIR__ . '/invoice.txt')));
foreach (['invoice.txt', 'changed.txt'] as $file) {
    $ok = openssl_cms_verify(__DIR__ . '/' . $file, OPENSSL_CMS_BINARY | OPENSSL_CMS_DETACHED, null, [__DIR__ . '/lab-ca.pem'], null, null, null, __DIR__ . '/signature.der', OPENSSL_ENCODING_DER);
    echo $file, ': ', $ok === true ? 'документ принят' : 'подпись не подтверждена', "\n";
}
Ожидаемый результат
invoice.txt: документ принят
changed.txt: подпись не подтверждена

Изменённые данные сохранены в новом файле, оригинал остаётся для сравнения. Выходной content-файл после отказа нельзя использовать даже если он существует или частично записан. Готовая отделённая подпись была создана для CRLF-оригинала: архив сохраняет именно эти байты; замена CRLF на LF также нарушила бы подпись.

Границы применения. Успех доверенной цепочки не заменяет разрешения именно этого подписанта на заказ. У PKCS7 вход S/MIME имеет MIME-правила, включая ограничение 76 символов строки Base64 по RFC2045. Не исправляйте DER добавлением случайных заголовков или снятием проверки.

Версии и совместимость. Роли content/certificates/sigfile уточнены по tagged php-src8.5.11 и реальному запуску: описания на странице Manual местами неоднозначны.

Следующий шаг: openssl_cms_sign(), openssl_cms_read(), openssl_error_string().

openssl_cms_encryptЗашифровать документ для владельца известного сертификата.

PHP 8.0+ · openssl

openssl_cms_encrypt( string $input_filename, string $output_filename, OpenSSLCertificate|array|string $certificate, ?array $headers, int $flags = 0, int $encoding = OPENSSL_ENCODING_SMIME, string|int $cipher_algo = OPENSSL_CIPHER_AES_128_CBC): bool

Зашифровать документ для владельца известного сертификата.

Передаются пути входа/выхода, сертификат или массив получателей, заголовки и форматные настройки. Это не тот же интерфейс, что строковый openssl_encrypt.

Адресовать накладную складу и открыть её ключом складаСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/recipient-cert.pem');
if (!openssl_cms_encrypt(__DIR__ . '/invoice.txt', __DIR__ . '/encrypted.pem', $certificate, [], OPENSSL_CMS_BINARY, OPENSSL_ENCODING_PEM, OPENSSL_CIPHER_AES_256_CBC)) { throw new RuntimeException('Шифрование не выполнено'); }
if (!openssl_cms_decrypt(__DIR__ . '/encrypted.pem', __DIR__ . '/opened.txt', $certificate, file_get_contents(__DIR__ . '/recipient-private.pem'), OPENSSL_ENCODING_PEM)) { throw new RuntimeException('Документ не открыт'); }
echo file_get_contents(__DIR__ . '/opened.txt');
Ожидаемый результат
Заказ A1: 1200 RUB

Симметричный ключ создаёт контейнер; переданный сертификат выбирает получателя. AES-CBC скрывает содержимое, но этот пример не подтверждает автора; подпись добавляется отдельным этапом ниже.

ПараметрДоговор
input_filenameПуть исходного файла.
output_filenameПуть зашифрованного контейнера.
certificateОдин сертификат получателя либо массив сертификатов.
headersМассив внешних S/MIME-заголовков либо null. Именованные элементы становятся Name: value, числовые — полной строкой. Не включаются в зашифрованное тело.
flagsМаска OPENSSL_CMS_*; default0. BINARY сохраняет байты.
encodingКодировка результата: SMIME по умолчанию, PEM либо DER.
cipher_algoС PHP 8.5 имя алгоритма либо OPENSSL_CIPHER_* int. Default с PHP 8.1 — AES128CBC, ранее RC2_40; здесь явно AES256CBC.

Результат и отказ. true при успехе, false при отказе.

Добавить второго получателя без копирования исходного документаСамостоятельный пример
<?php
declare(strict_types=1);

$names = ['publisher', 'recipient'];
$certificates = array_map(static fn(string $name) => file_get_contents(__DIR__ . '/' . $name . '-cert.pem'), $names);
if (!openssl_cms_encrypt(__DIR__ . '/invoice.txt', __DIR__ . '/encrypted.pem', $certificates, [], OPENSSL_CMS_BINARY, OPENSSL_ENCODING_PEM, 'aes-256-cbc')) { throw new RuntimeException('Шифрование не выполнено'); }

foreach ($names as $index => $name) {
    if (!openssl_cms_decrypt(__DIR__ . '/encrypted.pem', __DIR__ . '/opened.txt', $certificates[$index], file_get_contents(__DIR__ . '/' . $name . '-private.pem'), OPENSSL_ENCODING_PEM)) { throw new RuntimeException('Получатель не открыл документ'); }

    echo $name, ': ', trim(file_get_contents(__DIR__ . '/opened.txt')), "\n";
}
Ожидаемый результат
publisher: Заказ A1: 1200 RUB
recipient: Заказ A1: 1200 RUB

Массив сертификатов задаёт получателей одного контейнера. Строковое имя cipher_algo поддерживается с PHP 8.5; ранее нужен целочисленный идентификатор.

Границы применения. Шифрование не проверяет полномочия отправителя. Для CBC-контейнера проверка подписи должна быть частью согласованного обмена; успешная расшифровка не доказывает целостность. Не отправляйте письма через shell из учебного примера.

Версии и совместимость. DefaultAES128CBC с PHP 8.1: старое упоминание RC2-40 в описании PKCS7 не отражает текущую сигнатуру. Строковый cipher_algo добавлен в PHP 8.5.

Следующий шаг: openssl_cms_decrypt(), openssl_cms_sign().

openssl_cms_decryptВосстановить локально документ, зашифрованный для этого получателя.

PHP 8.0+ · openssl

openssl_cms_decrypt( string $input_filename, string $output_filename, OpenSSLCertificate|string $certificate, OpenSSLAsymmetricKey|OpenSSLCertificate|array|string|null $private_key = null, int $encoding = OPENSSL_ENCODING_SMIME): bool

Восстановить локально документ, зашифрованный для этого получателя.

Первые два аргумента — файловые пути. Третий и четвёртый — сертификат и ключ; file_get_contents превращает PEM-файл в содержимое. У CMS кодировка должна совпадать с входом.

Открыть полученный файл своим сертификатом и ключомСамостоятельный пример
<?php
declare(strict_types=1);

$certificate = file_get_contents(__DIR__ . '/recipient-cert.pem');
$key = file_get_contents(__DIR__ . '/recipient-private.pem');
if (!openssl_cms_decrypt(__DIR__ . '/invoice.cms.encrypted.pem', __DIR__ . '/opened.txt', $certificate, $key, OPENSSL_ENCODING_PEM)) { throw new RuntimeException('Документ не открыт'); }
echo file_get_contents(__DIR__ . '/opened.txt');
Ожидаемый результат
Заказ A1: 1200 RUB

Сертификат помогает контейнеру выбрать запись получателя, закрытый ключ открывает её. Данные появляются в выходном файле, а не в bool результата.

ПараметрДоговор
input_filenameПуть входного контейнера.
output_filenameПуть восстановленного документа; использовать только после успеха.
certificateСертификат адресата в поддерживаемой форме PEM/объект/file://путь, не обычное имя файла ключа.
private_keyСоответствующий закрытый ключ; defaultnull пытается получить ключ из аргумента certificate. Для отдельных файлов передавайте явно.
encodingКодировка входного контейнера: SMIME по умолчанию, PEM либо DER.

Результат и отказ. true при успехе, false при отказе.

Отказать владельцу другого сертификатаСамостоятельный пример
<?php
declare(strict_types=1);

foreach (['publisher', 'recipient'] as $name) {
    $certificate = file_get_contents(__DIR__ . '/' . $name . '-cert.pem');
    $key = file_get_contents(__DIR__ . '/' . $name . '-private.pem');
    $ok = openssl_cms_decrypt(__DIR__ . '/invoice.cms.encrypted.pem', __DIR__ . '/candidate.txt', $certificate, $key, OPENSSL_ENCODING_PEM);

    echo $name, ': ', $ok ? trim(file_get_contents(__DIR__ . '/candidate.txt')) : 'не адресат, документ не использовать', "\n";
}
Ожидаемый результат
publisher: не адресат, документ не использовать
recipient: Заказ A1: 1200 RUB

Контейнер адресован только складу. Путь candidate.txt может быть создан до неуспеха, поэтому читать его допустимо лишь после true; имена файлов сами по себе успех не обозначают.

Границы применения. Не используйте частично записанный результат после false. Успешная расшифровка не сообщает автора и не проверяет вложенную подпись; если протокол подписывает затем шифрует, дальше обязательно verify.

Версии и совместимость. Объекты ключей принимаются с PHP 8.0. Текстовое описание CMS-параметров сокращено в Manual; их реальные формы соответствуют общей модели key/certificate.

Следующий шаг: openssl_cms_verify(), openssl_cms_encrypt().

openssl_cms_readИзвлечь публичные сертификаты из CMS для отдельного анализа.

PHP 8.0+ · openssl

openssl_cms_read(string $input_filename, array &$certificates): bool

Извлечь публичные сертификаты из CMS для отдельного анализа.

Первый аргумент получает содержимое PEM-контейнера, второй — массив по ссылке. Название input_filename у CMS вводит в заблуждение: в реализации это строка данных.

Показать владельца сертификата внутри контейнераСамостоятельный пример
<?php
declare(strict_types=1);

$data = file_get_contents(__DIR__ . '/invoice.cms.pem');
if (!openssl_cms_read($data, $certificates)) { throw new RuntimeException('Контейнер не разобран'); }

foreach ($certificates as $pem) {
    echo 'Вложен сертификат: ', openssl_x509_parse($pem)['subject']['CN'], "\n";
}
Ожидаемый результат
Вложен сертификат: catalog.example.test

Чтение сертификатов не проверяет подписанный документ. Закрытых ключей из SignedData не извлекают: их там быть не должно.

ПараметрДоговор
input_filenameСодержимое PEM CMS/PKCS7. Это не путь и не полное S/MIME-сообщение. Расширение .p7b само не обещает PEM.
certificatesВыходной массив PEM-элементов: сертификаты, атакже имеющиеся CRL. Для CRL нужен соответствующий разбор; не передавайте каждый произвольный элемент в x509_parse без определения типа.

Результат и отказ. true при разборе, false при отказе. В Manual страница отмечена как не полностью документированная; подробности формата сверены с PHP 8.5.11.

Исправить смешение имени файла и его содержимогоСамостоятельный пример
<?php
declare(strict_types=1);

if (openssl_cms_read(__DIR__ . '/invoice.cms.pem', $wrong) !== false) { throw new RuntimeException('Поведение сборки изменилось'); }
echo "Имя файла вместо содержимого: отказ\n";
$data = file_get_contents(__DIR__ . '/invoice.cms.pem');
if (!openssl_cms_read($data, $certificates)) { throw new RuntimeException('Контейнер не разобран'); }

foreach ($certificates as $pem) {
    echo 'Вложен сертификат: ', openssl_x509_parse($pem)['subject']['CN'], "\n";
}
file_put_contents(__DIR__ . '/extracted.pem', $certificates[0]);
echo "extracted.pem сохранён; доверие проверяем отдельно\n";
Ожидаемый результат
Имя файла вместо содержимого: отказ
Вложен сертификат: catalog.example.test
extracted.pem сохранён; доверие проверяем отдельно

У CMS аргумент называется input_filename, но PHP 8.5 читает переданную строку через memoryBIO как PEM. Это проверено запуском и подтверждено исходниками; слепая передача пути не работает.

Границы применения. Извлечение не подтверждает подпись, срок, доверие либо полномочия. Нельзя принять сертификат из неизвестного контейнера в корни доверия автоматически.

Следующий шаг: openssl_cms_verify(), openssl_x509_parse().

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.