本文へスキップ
ecdsa.com
PHP

PHP openssl extension

PHP に同梱されている openssl 拡張です。リンクされた OpenSSL の曲線で DER 形式の ECDSA 署名を扱い、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
公開鍵の形式
spki-pemx509
秘密鍵の形式
pkcs8-pemencrypted-pkcs8sec1-pem

openssl_sign()、openssl_verify()、openssl_pkey_* 関数は、PHP のビルド時にリンクされた OpenSSL(公式の php:8.5.11 イメージでは 3.5.7)の薄いラッパーです。そのため、曲線と動作は OpenSSL に従います。署名は DER、nonce はランダム、low-S 正規化はなく、検証は high-S も受け入れます。ハッシュは常にアルゴリズム引数で決まり、事前に計算したダイジェストに署名する関数はありません。

openssl_verify() の戻り値は、PHP の典型的な落とし穴です。有効な署名には 1、無効な署名には 0 を返し、署名を解析することすらできない場合は -1 を返します。チェックでは、WebCrypto や JWT から取り出した 64 バイトの生の r‖s 署名がこれに当たりました。PHP では -1 は真と評価されるため、if (openssl_verify(...)) は不正なデータも受け入れてしまいます。サンプルのように === 1 で比較してください。生の署名は先に DER で包む必要があり、PHP の JWT ライブラリはどれも内部でこれを行っています。DER ⇄ 生形式の変換ツールで 2 つの形式を確認できます。

鍵は PEM 文字列、証明書、鍵リソースのいずれかで渡します。SPKI の PEM と X.509 の PEM は動作しましたが、SPKI DER の生のバイト列はそもそも受け付けられませんでした。秘密鍵は 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 を指定して使えます。

リンクされた OpenSSL がポスト量子アルゴリズムを実装していても、PHP のコードからは使えません。同じイメージの CLI では ML-DSA が一覧に表示されましたが、拡張にはそれに対応する鍵の種類が定義されていません。JWT には、トークンを手作業で組み立てるのではなく、firebase/php-jwt、web-token、lcobucci/jwt を使ってください。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/php-openssl/run.sh がコンテナイメージ php:8.5.11-cli-trixie 内で PHP openssl extension 8.5.11 を OpenSSL 3.5.7 9 Jun 2026 で実行しました。このページの内容はその出力と一致していなければならず、各事実には該当する出力行を示しています。

  • ECDSA 署名のエンコーディング

    der

    openssl_sign() は ASN.1 DER を返します。

    チェックによる記録
    fact ecdsa-signature-encoding = der
  • ECDSA の nonce

    random

    nonce はランダムです。同じメッセージの 2 つの署名は異なります。

    チェックによる記録
    fact ecdsa-nonce = random
  • 署名時の low-S

    either

    s は正規化されません。64 個の署名の中に high-S の値が現れました。

    チェックによる記録
    fact ecdsa-low-s-sign = either
  • 検証時の high-S

    accepts-high-s

    s を n − s に置き換えた署名に対して、openssl_verify() は 1 を返しました。

    チェックによる記録
    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

    生の 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

    OPENSSL_KEYTYPE_ED25519 と OPENSSL_KEYTYPE_ED448 による Ed25519 と Ed448 です。libsodium の sodium_crypto_sign_* も同梱されています。

    チェックによる記録
    fact eddsa = Ed25519, Ed448
    evidence eddsa: OPENSSL_KEYTYPE_ED25519 defined: true; sodium_crypto_sign available: true
  • ポスト量子アルゴリズム

    none

    イメージの OpenSSL CLI は 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 と、PEM 形式の X.509 証明書です。SPKI DER の生のバイト列は拒否されました。

    チェックによる記録
    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

ライブラリ