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 を使ってください。