Гем openssl входит в поставку Ruby и служит обёрткой над системной библиотекой OpenSSL — в образе ruby:4.0.7 это версия 3.5.7, а версия самого гема — 4.0.2. OpenSSL::PKey::EC#sign(digest, data) выдаёт подпись ASN.1 DER со случайным nonce и без нормализации low-S, а verify принимает high-S. dsa_sign_asn1 подписывает уже вычисленный вами дайджест.
Некорректные подписи вызывают исключение, а не возвращают false: 64-байтовая подпись raw r‖s или 3-байтовый фрагмент данных вызвали OpenSSL::PKey::PKeyError: EVP_DigestVerify, тогда как корректная по форме подпись другого сообщения вернула false. Код, который проверяет только логическое значение, падает на подписи в стиле JWS, поэтому либо перехватывайте PKeyError, либо сначала преобразуйте подпись, как в примере; конвертер DER ⇄ raw показывает обе формы рядом.
В OpenSSL 3 ключи неизменяемы: присваивание private_key существующему ключу EC вызвало ошибку pkeys are immutable on OpenSSL 3.0, поэтому не работают старые руководства, в которых ключ собирают, задавая точку или скаляр. Вместо этого создавайте ключи из DER или PEM — отдельную точку SEC1 сначала нужно обернуть в структуру SPKI. Обратите внимание, какой метод что записывает: to_pem для закрытого ключа записывает SEC1 EC PRIVATE KEY вместе с секретом, private_to_pem записывает PKCS#8, а публиковать следует результат public_to_pem.
Набор кривых совпадает с OpenSSL: от P-224 до P-521, secp256k1 и кривые Brainpool r1, а также Ed25519 и Ed448 через OpenSSL::PKey.generate_key. Через тот же общий API удалось сгенерировать ключи ML-DSA-44/65/87 и SLH-DSA, подписать ими данные и проверить подписи; ключи ML-KEM генерируются, но метода инкапсуляции в PKey версии 4.0.2 нет. Для JWT поверх этого гема построен ruby-jwt.