Перейти к содержимому
ecdsa.com
PHP

PHP openssl extension

Расширение openssl, входящее в PHP: подписи ECDSA в DER на кривых той версии OpenSSL, с которой скомпонован PHP, типы ключей Ed25519 и Ed448 и функция openssl_verify(), которая для некорректных входных данных возвращает -1 — значение, истинное в логическом контексте.
Лицензия
PHP-3.01
Проверенная версия
8.5.11 ·
Исходный код
github.com/php/php-src
Документация
www.php.net/manual/en/book.openssl.php

Совместимость вкратце

ECDSAEdDSA

Кривые ECDSA
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
Форматы закрытых ключей
pkcs8-pemencrypted-pkcs8sec1-pem

openssl_sign(), openssl_verify() и функции openssl_pkey_* — тонкие обёртки над OpenSSL, с которым собран PHP (3.5.7 в официальном образе php:8.5.11), поэтому кривые и поведение определяются этой версией: подписи в DER, случайные nonce, отсутствие нормализации low-S и проверка, которая принимает high-S. Хеш-функция всегда задаётся аргументом алгоритма; функции, которая подписывает заранее вычисленный дайджест, нет.

Возвращаемое значение openssl_verify() — классическая ловушка PHP. Функция возвращает 1 для верной подписи, 0 для неверной и -1, если подпись вообще не удаётся разобрать; именно это в ходе проверки получилось с 64-байтовой подписью raw r‖s из WebCrypto или JWT. В PHP -1 в логическом контексте истинно, поэтому if (openssl_verify(...)) принимает мусор; сравнивайте результат с === 1, как в примере. Подписи raw сначала нужно обернуть в DER, и каждая библиотека JWT для PHP делает это внутри; конвертер DER ⇄ raw показывает обе формы.

Ключи передаются как строки PEM, сертификаты или ресурсы ключей: SPKI PEM и X.509 PEM сработали, а байты SPKI DER без обёртки PEM были сразу отклонены. Закрытые ключи загружаются из PKCS#8 (обычного или зашифрованного) и из SEC1 EC PRIVATE KEY, а openssl_pkey_export() записывает PKCS#8. Поддерживаются кривые от P-224 до P-521, secp256k1 и Brainpool, а ключи Ed25519 и Ed448 работают через OPENSSL_KEYTYPE_ED25519 и OPENSSL_KEYTYPE_ED448 с алгоритмом 0.

Постквантовые алгоритмы недоступны из кода на PHP, хотя скомпонованный с ним OpenSSL их реализует: в выводе CLI из того же образа есть ML-DSA, но расширение не определяет для него тип ключа. Для JWT используйте firebase/php-jwt, web-token или lcobucci/jwt, а не собирайте токены вручную.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/php-openssl/run.sh запустила PHP openssl extension 8.5.11 в среде OpenSSL 3.5.7 9 Jun 2026 в образе контейнера php:8.5.11-cli-trixie; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Кодировка подписи ECDSA

    der

    openssl_sign() возвращает ASN.1 DER.

    Что записала проверка
    fact ecdsa-signature-encoding = der
  • Nonce в ECDSA

    random

    Случайный nonce: две подписи одного сообщения различаются.

    Что записала проверка
    fact ecdsa-nonce = random
  • Low-S при подписи

    either

    s не нормализуется: среди 64 подписей встретились значения high-S.

    Что записала проверка
    fact ecdsa-low-s-sign = either
  • High-S при проверке

    accepts-high-s

    openssl_verify() вернула 1 для подписи, в которой s заменили на n − s.

    Что записала проверка
    fact ecdsa-low-s-verify = accepts-high-s
  • Что хеширует вызов подписи

    hashes-message

    Данные всегда хешируются алгоритмом из аргумента; функции для подписи дайджеста нет.

    Что записала проверка
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: openssl_sign() always hashes with the algorithm given; there is no digest-signing function
  • Некорректная подпись ECDSA

    returns-error

    Для подписи raw r‖s и 3-байтового фрагмента данных openssl_verify() возвращает -1, а для верной подписи другого сообщения — 0.

    Что записала проверка
    fact ecdsa-malformed-signature = returns-error
    evidence ecdsa-malformed-signature: raw r||s: openssl_verify returned -1
    evidence ecdsa-malformed-signature: 3 bytes: openssl_verify returned -1
    evidence ecdsa-malformed-signature: wrong message: openssl_verify returned 0
  • Возвращаемые значения проверки

    -10

    Помимо 1 для верной подписи, в ходе проверки встретились -1 (не удалось разобрать) и 0 (неверная подпись); в PHP (bool) -1 равно true.

    Что записала проверка
    fact verify-return-values = -1, 0
    evidence verify-return-values: -1 is truthy in PHP: (bool) -1 === true
  • Кривые ECDSA

    P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    Подпись и проверка работают на P-224, P-256, P-384, P-521, secp256k1 и brainpoolP256r1/P384r1/P512r1.

    Что записала проверка
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    Ed25519 и Ed448 через OPENSSL_KEYTYPE_ED25519 и OPENSSL_KEYTYPE_ED448; в комплект также входит sodium_crypto_sign_* из libsodium.

    Что записала проверка
    fact eddsa = Ed25519, Ed448
    evidence eddsa: OPENSSL_KEYTYPE_ED25519 defined: true; sodium_crypto_sign available: true
  • Постквантовые алгоритмы

    none

    В расширении нет констант типов ключей для постквантовых алгоритмов, хотя CLI OpenSSL в образе выводит ML-DSA.

    Что записала проверка
    fact pqc = none
    evidence pqc: the openssl CLI of the same image lists 3 ML-DSA signature algorithm lines; the extension has no ML-DSA key type constant (openssl_pkey_new with an unknown type: false)
  • Форматы открытых ключей

    spki-pemx509

    SPKI PEM и сертификаты X.509 в PEM; байты SPKI DER без обёртки PEM были отклонены.

    Что записала проверка
    fact public-key-formats = spki-pem, x509
    evidence public-key-formats: SPKI DER bytes passed as the key: openssl_verify returned false
  • Форматы закрытых ключей

    pkcs8-pemencrypted-pkcs8sec1-pem

    PKCS#8 PEM, зашифрованный PKCS#8 и SEC1 EC PRIVATE KEY; openssl_pkey_export() записывает PKCS#8.

    Что записала проверка
    fact private-key-formats = pkcs8-pem, encrypted-pkcs8, sec1-pem
    evidence private-key-formats: openssl_pkey_export writes -----BEGIN PRIVATE KEY-----

Использование

Подпишите данные на P-256 и выясните, почему результат openssl_verify() нужно сравнивать с === 1.
<?php
$key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
$publicPem = openssl_pkey_get_details($key)['key'];
$message = 'order #1042 approved';

openssl_sign($message, $der, $key, OPENSSL_ALGO_SHA256); // ASN.1 DER

// openssl_verify returns 1 (valid), 0 (invalid) or -1 (error). -1 is truthy,
// so compare strictly with === 1, never with a loose if ($result).
$valid = openssl_verify($message, $der, $publicPem, OPENSSL_ALGO_SHA256);
$garbage = openssl_verify($message, random_bytes(64), $publicPem, OPENSSL_ALGO_SHA256);

echo 'valid: ', var_export($valid === 1, true), ', raw 64 bytes gives ', $garbage, ', which is truthy: ', var_export((bool) $garbage, true), "\n";

Тестовый стенд для фрагментов кода запускает каждый пример без изменений в образе контейнера php:8.5.11-cli-trixie, и запуск завершается ошибкой, если вывод не совпадает.

Команда
php usage.php 2>/dev/null
Вывод должен совпадать (регулярное выражение)
valid: true, raw 64 bytes gives -1, which is truthy: true

Библиотеки