As funções openssl_sign(), openssl_verify() e openssl_pkey_* são camadas finas sobre o OpenSSL com que o PHP foi compilado — o 3.5.7 na imagem oficial php:8.5.11 —, por isso as curvas e o comportamento seguem os dele: assinaturas DER, nonces aleatórios, nenhuma normalização low-S e uma verificação que aceita high-S. O hash sempre vem do argumento de algoritmo; não existe função que assine um digest pré-calculado.
O valor de retorno de openssl_verify() é a armadilha clássica do PHP. A função retorna 1 para uma assinatura válida, 0 para uma inválida e -1 quando nem consegue interpretar a assinatura — foi o que uma assinatura r‖s bruta de 64 bytes vinda do WebCrypto ou de um JWT produziu no teste. No PHP, -1 é avaliado como verdadeiro, então if (openssl_verify(...)) aceita lixo; compare com === 1, como faz o exemplo. Assinaturas brutas precisam primeiro ser encapsuladas em DER, que é o que toda biblioteca JWT para PHP faz internamente; o conversor DER ⇄ bruto mostra as duas formas.
As chaves são passadas como strings PEM, certificados ou recursos de chave: SPKI PEM e X.509 PEM funcionaram, enquanto bytes SPKI DER brutos foram rejeitados de imediato. As chaves privadas são carregadas de PKCS#8 (simples ou criptografado) e de SEC1 EC PRIVATE KEY, e openssl_pkey_export() grava PKCS#8. As curvas cobertas vão da P-224 à P-521, além da secp256k1 e das Brainpool, e as chaves Ed25519 e Ed448 funcionam por meio de OPENSSL_KEYTYPE_ED25519 e OPENSSL_KEYTYPE_ED448 com o algoritmo 0.
Os algoritmos pós-quânticos não são acessíveis a partir do código PHP, embora o OpenSSL vinculado os implemente: a CLI da mesma imagem listou o ML-DSA, mas a extensão não define nenhum tipo de chave para ele. Para JWTs, use o firebase/php-jwt, o web-token ou o lcobucci/jwt em vez de montar tokens à mão.