openssl_sign(), openssl_verify() et les fonctions openssl_pkey_* sont de fines surcouches de l’OpenSSL avec lequel PHP a été compilé (3.5.7 dans l’image officielle php:8.5.11) : courbes et comportements en découlent, avec des signatures DER, des nonces aléatoires, aucune normalisation low-S et une vérification qui accepte high-S. La fonction de hachage vient toujours de l’argument d’algorithme ; aucune fonction ne signe un condensat précalculé.
La valeur de retour d’openssl_verify() est le piège classique de PHP. Elle vaut 1 pour une signature valide, 0 pour une signature invalide et -1 quand la signature ne peut même pas être analysée, ce qu’a produit lors des essais une signature r‖s brute de 64 octets issue de WebCrypto ou d’un JWT. En PHP, -1 est évalué comme vrai, si bien que if (openssl_verify(...)) accepte n’importe quoi ; comparez avec === 1, comme le fait l’exemple. Les signatures brutes doivent d’abord être enveloppées en DER, ce que fait en interne chaque bibliothèque JWT pour PHP ; le convertisseur DER ⇄ brut montre les deux formes.
Les clés se passent sous forme de chaînes PEM, de certificats ou de ressources de clé : SPKI PEM et X.509 PEM ont fonctionné, tandis que des octets SPKI DER bruts ont été rejetés d’emblée. Les clés privées se chargent depuis du PKCS#8 (en clair ou chiffré) et depuis une EC PRIVATE KEY SEC1, et openssl_pkey_export() écrit du PKCS#8. Les courbes couvertes vont de P-224 à P-521, avec secp256k1 et Brainpool, et les clés Ed25519 et Ed448 fonctionnent via OPENSSL_KEYTYPE_ED25519 et OPENSSL_KEYTYPE_ED448 avec l’algorithme 0.
Les algorithmes post-quantiques ne sont pas accessibles depuis le code PHP, bien que l’OpenSSL lié les implémente : la CLI de la même image listait ML-DSA, mais l’extension ne définit aucun type de clé correspondant. Pour les JWT, utilisez firebase/php-jwt, web-token ou lcobucci/jwt plutôt que d’assembler les jetons à la main.