openssl_sign(), openssl_verify() и функции openssl_pkey_* — тонкие обёртки над OpenSSL, с которым собран PHP (3.5.7 в официальном образе php:8.5.11), поэтому кривые и поведение определяются этой версией: подписи в DER, случайные nonce, отсутствие нормализации low-S и проверка, которая принимает high-S. Хеш-функция всегда задаётся аргументом алгоритма; функции, которая подписывает заранее вычисленный дайджест, нет.
Возвращаемое значение openssl_verify() — классическая ловушка PHP. Функция возвращает 1 для верной подписи, 0 для неверной и -1, если подпись вообще не удаётся разобрать; именно это в ходе проверки получилось с 64-байтовой подписью raw r‖s из WebCrypto или JWT. В PHP -1 в логическом контексте истинно, поэтому if (openssl_verify(...)) принимает мусор; сравнивайте результат с === 1, как в примере. Подписи raw сначала нужно обернуть в DER, и каждая библиотека JWT для PHP делает это внутри; конвертер DER ⇄ raw показывает обе формы.
Ключи передаются как строки PEM, сертификаты или ресурсы ключей: SPKI PEM и X.509 PEM сработали, а байты SPKI DER без обёртки PEM были сразу отклонены. Закрытые ключи загружаются из 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.
Постквантовые алгоритмы недоступны из кода на PHP, хотя скомпонованный с ним OpenSSL их реализует: в выводе CLI из того же образа есть ML-DSA, но расширение не определяет для него тип ключа. Для JWT используйте firebase/php-jwt, web-token или lcobucci/jwt, а не собирайте токены вручную.