openssl_sign(), openssl_verify() y las funciones openssl_pkey_* son envoltorios finos sobre el OpenSSL con el que se compiló PHP —3.5.7 en la imagen oficial php:8.5.11—, así que las curvas y el comportamiento son los suyos: firmas DER, nonces aleatorios, sin normalización low-S y una verificación que acepta high-S. El hash siempre viene del argumento de algoritmo; no hay ninguna función que firme un resumen precalculado.
El valor de retorno de openssl_verify() es la trampa clásica de PHP. Devuelve 1 para una firma válida, 0 para una no válida y -1 cuando la firma ni siquiera se puede analizar, que es lo que produjo en la comprobación una firma r‖s en bruto de 64 bytes procedente de WebCrypto o de un JWT. En PHP, -1 se evalúa como verdadero, así que if (openssl_verify(...)) acepta basura; compara con === 1, como hace el ejemplo. Las firmas en bruto deben envolverse antes en DER, que es lo que hace internamente toda biblioteca JWT de PHP; el conversor DER ⇄ en bruto muestra las dos formas.
Las claves se pasan como cadenas PEM, certificados o recursos de clave: SPKI PEM y X.509 PEM funcionaron, mientras que los bytes SPKI DER en bruto se rechazaron de plano. Las claves privadas se cargan desde PKCS#8 (sin cifrar o cifrado) y desde SEC1 EC PRIVATE KEY, y openssl_pkey_export() escribe PKCS#8. Las curvas abarcaron de P-224 a P-521, secp256k1 y Brainpool, y las claves Ed25519 y Ed448 funcionan mediante OPENSSL_KEYTYPE_ED25519 y OPENSSL_KEYTYPE_ED448 con el algoritmo 0.
Los algoritmos poscuánticos no son accesibles desde código PHP, aunque el OpenSSL enlazado los implementa: la CLI de la misma imagen enumeró ML-DSA, pero la extensión no define ningún tipo de clave para él. Para JWT, usa firebase/php-jwt, web-token o lcobucci/jwt en lugar de montar los tokens a mano.