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

Дерево руководства
Глава 97 · От первого результата к API
OpenSSL: подписи, ключи, сертификаты и защищённые файлы
Проверить накладную, сохранить ключ, прочитать сертификат и обменяться защищённым документом.
Поставщик прислал накладную: можно ли принять её байты?
В invoice.txt написано Заказ A1: 1200 RUB. Вместе с файлом пришла подпись — отдельный короткий набор байтов, созданный закрытым ключом поставщика. У нас уже есть его открытый ключ, полученный по доверенному каналу. Нужно увидеть конкретное решение: Принять накладную A1: подпись поставщика верна.
- invoice.txt: Заказ A1: 1200 RUB + перевод строки
- invoice.sig.b64: подпись этих точных байтов
- publisher-public.pem: заранее доверенный открытый ключ
- openssl_verify сопоставляет документ, подпись, ключ и SHA-256
- Результат 1 → принять; любой другой результат → остановиться
Ключ — данные для криптографической операции. У пары ключей закрытая часть позволяет подписывать, открытая — проверять. Аналогия с печатью помогает понять роли, но у файла нет видимого оттиска: проверяются все байты математическим алгоритмом. Подпись не прячет текст. Если злоумышленник заменит и документ, и ключ, одной успешной проверки будет недостаточно: доверие к открытой части устанавливают заранее.
В архиве примера есть все три входных файла. $invoice = сохраняет содержимое в переменную; file_get_contents(...) читает файл. __DIR__ — папка скрипта, точка соединяет её с именем. Base64 — запись двоичных байтов печатными символами; base64_decode(..., true) возвращает исходную подпись и строго проверяет алфавит. В нашем fixture она исправна. Вызов openssl_verify получает документ, подпись, открытый ключ и SHA-256 в этом порядке. === 1 требует именно успешный код, а ? ... : ... выбирает текст решения.
<?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().
Как выбрать задачу: подпись, шифрование, пароль или 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_CONF: полный путь к файлу
- Затем историческая переменная SSLEAY_CONF
- Затем default certificate area, выбранная при сборке OpenSSL
- 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.cafile | Default пустая строка, INI_PERDIR. Файл доверенных CA для verify_peer при проверке удалённой стороны. |
| openssl.capath | Default пустая строка, INI_PERDIR. Правильно хешированный каталог сертификатов; поиск используется, если cafile не задан или нужный сертификат там не найден. |
| openssl.libctx | Default 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-теста.
Назначение сертификата выбирается одним значением
Сертификат склада может разрешать 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_PADDING | PKCS#1 v1.5; исторический default ряда RSA-функций. Для расшифровки учитывайте padding-oracle и provider implicit rejection. |
OPENSSL_PKCS1_OAEP_PADDING | OAEP для public_encrypt/private_decrypt; с PHP 8.5 digest_algo позволяет явно задать хеш OAEP. |
OPENSSL_PKCS1_PSS_PADDING | PSS для подписи/проверки RSA с PHP 8.5, не формат шифрования документа. |
OPENSSL_NO_PADDING | Сырой RSA без схемы подготовки. Нельзя безопасно шифровать произвольные пользовательские байты напрямую. |
OPENSSL_SSLV23_PADDING | Исторически перечислен Manual, отсутствует в нашей OpenSSL 3-сборке; перед использованием старого кода проверяйте defined. |
Тип ключа определяет допустимое действие
Один ключевой объект необязательно умеет всё. В учебнике RSA применяется к подписи и короткому OAEP-блоку, EC — к ECDSA/ECDH с совместимыми настройками. Для современных Curve25519/448 назначения разделены прямо в типах.
| Константа | Механизм |
|---|---|
OPENSSL_KEYTYPE_RSA | RSA: операции подписи/шифрования в согласованной схеме. |
OPENSSL_KEYTYPE_DSA | DSA: подпись; нужен совместимый получатель и политика протокола. |
OPENSSL_KEYTYPE_DH | DH: согласование секрета, не подпись. |
OPENSSL_KEYTYPE_EC | Эллиптические кривые: соответствующие ECDSA/ECDH-операции. |
OPENSSL_KEYTYPE_X25519OPENSSL_KEYTYPE_X448 | Согласование секрета; с PHP 8.4 при OpenSSL 3+. |
OPENSSL_KEYTYPE_ED25519OPENSSL_KEYTYPE_ED448 | EdDSA-подписи; с PHP 8.4 при OpenSSL 3+. В PHP 8.5 алгоритм 0 у sign/verify позволяет операции, не использующие внешний хеш. |
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: запрещает двойное содержимое при проверке, когда одновременно заданы внешние данные и вложенные. Это защита от неоднозначного выбора контента, а не универсальный способ сжать документ. |
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 не гарантирует доступность алгоритма у активного provider. Для современной подписи примеры выбирают SHA-256; старые MD/RC2/DES перечислены для полноты чтения существующих программ.
| Константа | Смысл |
|---|---|
OPENSSL_ALGO_SHA1 | SHA-1 — исторический default sign/verify; явно выбирайте согласованный современный алгоритм. |
OPENSSL_ALGO_SHA224OPENSSL_ALGO_SHA256OPENSSL_ALGO_SHA384OPENSSL_ALGO_SHA512 | Идентификаторы семейства SHA-2 для API, принимающих целочисленный algorithm. |
OPENSSL_ALGO_RMD160 | RIPEMD-160 для совместимости. |
OPENSSL_ALGO_MD5OPENSSL_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_CBCOPENSSL_CIPHER_AES_192_CBCOPENSSL_CIPHER_AES_256_CBC | Целочисленные идентификаторы AES-CBC для PKCS7/CMS. CBC не является аутентифицированным шифрованием. |
OPENSSL_CIPHER_RC2_40OPENSSL_CIPHER_RC2_64OPENSSL_CIPHER_RC2_128OPENSSL_CIPHER_DESOPENSSL_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_DATA | encrypt/decrypt работают с сырыми байтами вместо встроенного Base64. |
OPENSSL_ZERO_PADDING | Отключает стандартное блочное дополнение. Название не означает«добавить нули»: длина должна быть кратна размеру блока, иначе отказ. |
OPENSSL_DONT_ZERO_PAD_KEY | Запретить дополнение короткого ключа нулями; это не KDF и не проверка пароля. |
OPENSSL_ENCODING_SMIMEOPENSSL_ENCODING_DEROPENSSL_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. Создавать собственный наследник или доставать «секретное свойство» не требуется.
- publisher-private.pem → openssl_pkey_get_private
- Объект $key передан openssl_sign для накладной A1
- openssl_pkey_get_details → только выбранная открытая часть для получателя
- pkey_export → отдельно сохранённый PEM; объект сам ничего не пишет
- Все ссылки отпущены → 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 означает шифрование с аутентификацией и дополнительными данными; здесь аутентификация означает проверку знания общего ключа, а не установление личности пользователя.
- Ключ: 32 случайных байта, остаётся секретным
- IV/nonce: 12 новых случайных байтов для этого шифрования
- Текст заметки → ciphertext + отдельный tag
- Получатель использует те же key, IV, tag и AAD
- Успех → исходная заметка; изменение → 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 сам по себе не является сертификатом и не подтверждает право на указанное имя.
- Локальный закрытый ключ подписывает CSR
- CSR содержит открытый ключ + запрошенное имя catalog.example.test
- Центр проверяет право на имя и свою политику выпуска
- После разрешения центр создаёт X.509-сертификат
- Получатель проверяет цепочку к известному 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, срок, назначение и имя ожидаемого сервера.
- x509_read: из PEM получен объект
- x509_parse: прочитаны владелец/издатель/даты/SAN
- x509_verify: проверена подпись известным ключом издателя
- x509_checkpurpose: локальная цепочка и назначение относительно выбранного CA
- Настоящий 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, где алгоритм подписи и хеширование заданы целиком.
Один документ, два получателя: что находится в цифровом конверте
- Данные шифруются одним случайным симметричным ключом
- Для каждого адресата RSA шифрует свою копию этого ключа
- Отправляются ciphertext + IV + зашифрованный ключ нужного адресата
- Адресат открывает свою копию ключа и восстанавливает документ
seal/open — старый интерфейс такого конверта. Он не возвращает отдельный тег аутентификации. Ни AES-CBC, ни успешное раскрытие сами по себе не подтверждают неизменность и автора. Примеры показывают механику совместимости в лаборатории; для реального обмена используйте согласованный защищённый протокол/контейнер с проверкой отправителя.
Согласование секрета: стороны обмениваются открытыми значениями
Алиса иБоб хотят получить общий секрет, не отправляя его напрямую. Каждый хранит свой закрытый ключ и получает открытую часть другого. DH работает в конечной группе, ECDH — на эллиптической кривой. Параметры сторон должны совпадать; для DH архив использует стандартную ffdhe2048 из RFC7919, а не придуманные небольшие числа.
Общий результат ещё не является полноценным защищённым соединением. Протокол должен удостоверить собеседника, применить согласованную KDF для ключей нужного назначения и учесть повторные сообщения. Для веб-клиента эту работу делает TLS. Пример ниже показывает согласование, без заявления оготовом сетевом протоколе.
Если протокол требует Ed25519 или X25519
Похожие числа в именах не делают ключи взаимозаменяемыми. Ed25519 используется для подписи, X25519 — для согласования секрета. Получатель должен поддерживать выбранный протокол. Сначала покажем подпись записи об остатке, затем отдельно согласование; в этих примерах нужен PHP 8.5 с OpenSSL 3.
<?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-примера. Подпись проверена настоящим вызовом.
<?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.pem | CMS SignedData с вложенной накладной в PEM. | openssl_cms_verify с OPENSSL_ENCODING_PEM |
| invoice.detached.der | Только отделённая CMS-подпись в DER; исходная накладная нужна отдельно. | openssl_cms_verify с DE Т ACHED, sigfile и DER |
| certificates.p7b.pem | PEM PKCS7, из которого можно извлечь сертификаты. | openssl_pkcs7_read, затем отдельно проверять доверие |
Расширение .p7b или .p7m не определяет кодировку однозначно. Ошибка no content type часто означает, что DER отдали S/MIME-парсеру. Здесь выбран явный формат, а не отключена проверка подписи. Файлы остаются локальными; ни один пример не отправляет почту.
Подпись может быть attached: документ внутри контейнера, либо detached: документ хранится отдельно. BINARY сохраняет исходные байты без канонизации строк CRLF. Текстовый режим нужен только если обе стороны согласовали правила преобразования; иначе подпись относится уже к другим байтам.
Законченный локальный обмен: подписать, адресовать складу, проверить после открытия
Требование выросло: склад должен прочитать накладную, убедиться в её неизменности и увидеть сертификат подписанта. Сначала издатель создаёт подписанный CMS в PEM. Затем этот файл шифруется для склада. Склад открывает внешний контейнер, проверяет внутреннюю подпись относительно известного CA и только после этого использует текст.
- invoice.txt → подпись ключом издателя → signed.pem
- signed.pem → шифрование для сертификата склада → delivery.pem
- delivery.pem → закрытый ключ склада → received.pem
- received.pem → подпись и Lab CA → verified.txt
<?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 options | config_section_name, а не опечатку config_section_section. |
| cms_verify | certificates и 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 — операция не выполнена. Подписанный документ при этом не шифруется.
<?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-настройки не являются настройками генератора ключа. |
| rsa | n (модуль, обязателен), d (закрытая степень, обязательна), e (открытая степень), p/q (простые), dmp1/dmq1 (d mod p−1/q−1), iqmp (q⁻¹ mod p). Значения — бинарные строки чисел, не десятичный текст. |
| dsa | p, q, g — параметры группы; priv_key/pub_key — закрытая/открытая компоненты. Все поля этой формы необязательны; библиотека определяет допустимые комбинации. |
| dh | p, g — общий модуль и генератор; priv_key/pub_key — локальные компоненты. Для обмена стороны используют одинаковые параметры. |
| ec | curve_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. |
<?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 при невозможности разобрать ключ или открыть защищённую часть.
<?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 при невозможности разобрать ключ или открыть защищённую часть.
<?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. Это общая часть результата. |
| rsa | n/e — открытые компоненты; d/p/q/dmp1/dmq1/iqmp — закрытые компоненты, если доступны. В отличие от options генератора, это описание уже существующего ключа. |
| dsa | p/q/g — параметры; pub_key и при наличии priv_key — компоненты ключа. |
| dh | p/g — параметры, pub_key/priv_key — компоненты. Состав зависит от доступных данных ключа. |
| ec | curve_name/curve_oid — идентификаторы кривой; x/y — открытая точка; d — закрытая компонента, если доступна. |
| x25519, ed25519, x448, ed448 | С PHP8.4: вложенные pub_key/priv_key соответствующего типа; закрытая компонента может отсутствовать у публичного объекта. |
<?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 в первом примере нужен только для наблюдения миграционной проблемы.
<?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 вызов не закрывает объект и не отзывает ключ.
| Параметр | Договор |
|---|---|
| key | OpenSSLAsymmetricKey; до PHP 8 это был ресурс ключа. |
Результат и отказ. void; с PHP 8.0 нет эффекта, функция устарела.
<?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 в первом примере нужен только для наблюдения миграционной проблемы.
<?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 вызов не закрывает объект и не отзывает ключ.
| Параметр | Договор |
|---|---|
| key | OpenSSLAsymmetricKey; до PHP 8 это был ресурс ключа. |
Результат и отказ. void; с PHP 8.0 нет эффекта, функция устарела.
<?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; приложение проверяет длину заранее. |
| tag | AEAD-метка. В encrypt — выход по ссылке; в decrypt — обязательная для выбранного GCM-контракта входная строка байтов. |
| aad | Дополнительные аутентифицируемые байты: видимы, но защищены от изменения. Обе стороны передают точно одинаковые значения. |
| tag_length | Длина создаваемой метки, default16. Для GCM PHP допускает 4…16; в учебном контракте всегда 16, уменьшение требует отдельного протокольного решения. |
Результат и отказ. Шифротекст строкой либо false. Без RAW_DATA строка уже Base64. Неизвестный алгоритм и неподходящий IV вызывают Warning; не используйте частичный результат после отказа.
<?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; приложение проверяет длину заранее. |
| tag | AEAD-метка. В encrypt — выход по ссылке; в decrypt — обязательная для выбранного GCM-контракта входная строка байтов. |
| aad | Дополнительные аутентифицируемые байты: видимы, но защищены от изменения. Обе стороны передают точно одинаковые значения. |
Результат и отказ. Исходная строка либо false. При неправильном tag GCM возвращает false; выдавать данные дальше нельзя. Неизвестный алгоритм/ошибочный IV могут дополнительно породить Warning.
<?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.
<?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.
<?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 запрашивает бинарный результат.
<?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. |
| binary | false по умолчанию: 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 и провайдеров.
| Параметр | Договор |
|---|---|
| aliases | false по умолчанию: основные имена; 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 и провайдеров.
| Параметр | Договор |
|---|---|
| aliases | false по умолчанию: основные имена; 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 означает отказ получения списка.
<?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, бит |
|---|---|---|
| 80 | 1024 | 160 |
| 112 | 2048 | 224 |
| 128 | 3072 | 256 |
| 192 | 7680 | 384 |
| 256 | 15360 | 512 |
<?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, когда очередь пуста. Последнее прочитанное сообщение относится к самой недавней ошибке очереди.
<?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.
<?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_key | OpenSSLAsymmetricKey либо переменная 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_type | RSA по умолчанию; известные типы 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. |
<?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 добавляет описание для человека.
<?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_text | true по умолчанию: без дополнительного описания; 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 добавляет описание для человека.
<?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_text | true по умолчанию: без дополнительного описания; 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_names | true: короткие ключи 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 использован в реальной операции. Само извлечение открытой части не превращает запрос в доверенный сертификат.
| Параметр | Договор |
|---|---|
| csr | CSR в объектной, PEM либо file://форме. |
| short_names | Аргумент игнорируется; изменение true/false не меняет объект ключа. |
Результат и отказ. OpenSSLAsymmetricKey или false.
<?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.
<?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-сервера.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, 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 показаны отдельно.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа. |
| short_names | true по умолчанию: короткие 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
Показаны тип и имя из настоящего экспортированного результата. Это публичный сертификат; закрытого ключа в нём нет.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа. |
| output | Выходная строка PEM по ссылке. |
| no_text | true: только PEM; false: также читаемая информация. |
Результат и отказ. true при успехе, false при отказе.
<?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
Показаны тип и имя из настоящего экспортированного результата. Это публичный сертификат; закрытого ключа в нём нет.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа. |
| output_filename | Путь PEM-результата. |
| no_text | true: только PEM; false: также читаемая информация. |
Результат и отказ. true при успехе, false при отказе.
<?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-оболочки. При новом выпуске сертификата он изменится; это требует согласованного обновления ожидаемого значения.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, PEM-строка либо file://путь к PEM-сертификату. Сертификат не содержит закрытого ключа. |
| digest_algo | Имя дайджеста из get_md_methods; default sha1, в примерах sha256. |
| binary | false: строчный 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: ключ от другого сертификата
Так можно обнаружить ошибочно подобранные файлы перед запуском сервиса. Прочитать закрытый ключ и сопоставить его открытые параметры — отдельные действия.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, 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 разрешает считать подпись подходящей.
<?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
Проверена именно подпись внутри сертификата. Это не построение полной цепочки и не подтверждение сетевого соединения.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, 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.
<?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-рукопожатие; эти проверки здесь не заявлены.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate, 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; второй показывает обычную область видимости.
<?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-кода: функция больше не закрывает объект. Она также не отзывает сертификат.
| Параметр | Договор |
|---|---|
| certificate | OpenSSLCertificate; до 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 читает их значения.
<?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.
<?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.
<?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 | Закрытый ключ автора блока. |
| padding | PKCS1_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.
<?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 | Открытый ключ автора блока. |
| padding | PKCS1_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. |
| iv | IV от 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.
<?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. Читаем только имя сертификата, не выводим закрытый ключ.
| Параметр | Договор |
|---|---|
| certificate | PEM-сертификат, объект либо file://путь. |
| output | Бинарный PKCS#12 по ссылке. |
| private_key | Закрытый ключ, соответствующий сертификату. |
| passphrase | Пароль защиты контейнера; не пароль учётной записи. |
| options | extracerts: один дополнительный сертификат или массив; 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. Читаем только имя сертификата, не выводим закрытый ключ.
| Параметр | Договор |
|---|---|
| certificate | PEM-сертификат, объект либо file://путь. |
| output_filename | Путь результата. |
| private_key | Закрытый ключ, соответствующий сертификату. |
| passphrase | Пароль защиты контейнера; не пароль учётной записи. |
| options | extracerts: один дополнительный сертификат или массив; 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; перед дальнейшей подписью проверяем операции.
<?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_algo | OPENSSL_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";
Ожидаемый результат
Кабинет: пароль подтверждён
Хеш меняется между запусками благодаря соли, результат проверки остаётся тем же. Параметры примера демонстрируют расход ресурсов; рабочие значения выбирают с измерением нагрузки сервиса, не копированием универсального числа.
| Параметр | Договор |
|---|---|
| algo | argon2id или argon2i; иное имя вызывает ValueError. |
| password | Исходный пароль, который не сохраняют открытым текстом. |
| options | memory_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";
Ожидаемый результат
Кабинет: пароль подтверждён
Хеш меняется между запусками благодаря соли, результат проверки остаётся тем же. Параметры примера демонстрируют расход ресурсов; рабочие значения выбирают с измерением нагрузки сервиса, не копированием универсального числа.
| Параметр | Договор |
|---|---|
| algo | argon2id или 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_filename | PEM-файл дополнительных сертификатов для включения; они помогают цепочке, но не становятся доверенными корнями. |
Результат и отказ. 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_algo | 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_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 отделяет содержимое. |
| encoding | SMIME по умолчанию, DER либо PEM. Кодировка и attached/detached — разные решения. |
| untrusted_certificates_filename | PEM-файл дополнительных сертификатов для включения; они помогают цепочке, но не становятся доверенными корнями. |
Результат и отказ. 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


