A gem openssl vem com o Ruby e encapsula o OpenSSL do sistema — o 3.5.7 na imagem ruby:4.0.7, com a gem na versão 4.0.2. OpenSSL::PKey::EC#sign(digest, data) produz ASN.1 DER com um nonce aleatório e sem normalização low-S, e verify aceita high-S. dsa_sign_asn1 assina um digest que você já calculou.
Assinaturas malformadas lançam uma exceção em vez de retornar false: uma assinatura r‖s bruta de 64 bytes ou um blob de 3 bytes lançaram OpenSSL::PKey::PKeyError: EVP_DigestVerify, enquanto uma assinatura bem formada sobre outra mensagem retornou false. Código que só confere o booleano quebra com uma assinatura no estilo JWS; por isso, capture PKeyError ou converta antes, como faz o exemplo. O conversor DER ⇄ bruto mostra as duas formas lado a lado.
As chaves são imutáveis no OpenSSL 3: atribuir private_key a uma chave EC existente lançou pkeys are immutable on OpenSSL 3.0, o que quebra tutoriais mais antigos que montam uma chave definindo o ponto ou o escalar. Em vez disso, construa as chaves a partir de DER ou PEM — um ponto SEC1 isolado precisa antes ser encapsulado em uma estrutura SPKI. Repare no que cada método grava: to_pem em uma chave privada grava EC PRIVATE KEY SEC1, incluindo o segredo, private_to_pem grava PKCS#8, e public_to_pem é o que deve ser publicado.
As curvas disponíveis acompanham o OpenSSL: da P-224 à P-521, secp256k1 e as curvas Brainpool r1, além de Ed25519 e Ed448 por meio de OpenSSL::PKey.generate_key. A mesma API genérica gerou chaves ML-DSA-44/65/87 e SLH-DSA e assinou e verificou com elas; é possível gerar chaves ML-KEM, mas PKey não oferece nenhum método de encapsulamento na versão 4.0.2. Para JWTs, a ruby-jwt se apoia nesta gem.