openssl_sign(), openssl_verify() und die Funktionen openssl_pkey_* sind dünne Wrapper um das OpenSSL, gegen das PHP gebaut wurde – 3.5.7 im offiziellen Image php:8.5.11 –, Kurven und Verhalten richten sich also danach: DER-Signaturen, zufällige Nonces, keine Low-S-Normalisierung und eine Prüfung, die High-S akzeptiert. Der Hash ergibt sich immer aus dem Argument für den Algorithmus; eine Funktion, die einen vorab berechneten Digest signiert, gibt es nicht.
Der Rückgabewert von openssl_verify() ist die klassische PHP-Falle. Die Funktion gibt 1 für eine gültige Signatur zurück, 0 für eine ungültige und -1, wenn sich die Signatur nicht einmal parsen lässt – genau das ergab im Test eine rohe r‖s-Signatur mit 64 Byte aus WebCrypto oder einem JWT. In PHP gilt -1 als wahr, daher akzeptiert if (openssl_verify(...)) Datenmüll; vergleiche mit === 1, wie es das Beispiel tut. Rohe Signaturen müssen zuerst in DER verpackt werden, was jede JWT-Bibliothek für PHP intern erledigt; der Konverter DER ⇄ Raw zeigt die beiden Formen.
Schlüssel werden als PEM-Strings, Zertifikate oder Schlüsselressourcen übergeben: SPKI-PEM und X.509-PEM funktionierten, rohe SPKI-DER-Bytes dagegen wurden rundweg abgelehnt. Private Schlüssel lassen sich aus PKCS#8 (unverschlüsselt oder verschlüsselt) und aus SEC1 EC PRIVATE KEY laden, und openssl_pkey_export() schreibt PKCS#8. Die Kurven umfassten P-224 bis P-521, secp256k1 und Brainpool, und Ed25519- und Ed448-Schlüssel funktionieren über OPENSSL_KEYTYPE_ED25519 und OPENSSL_KEYTYPE_ED448 mit dem Algorithmus 0.
Post-Quanten-Algorithmen sind aus PHP-Code nicht erreichbar, obwohl das gelinkte OpenSSL sie implementiert: Die CLI im selben Image listete ML-DSA auf, doch die Erweiterung definiert dafür keinen Schlüsseltyp. Für JWTs nimm firebase/php-jwt, web-token oder lcobucci/jwt, statt Tokens von Hand zusammenzubauen.