Library Interop Cheatsheet — шпаргалка по совместимости библиотек
Вы подписываете в библиотеке A, а проверяете в библиотеке B — что сломается? Выберите обе стороны и получите чек-лист конкретных расхождений со строкой кода, которая исправляет каждое из них: кодировка подписи, обработка high-S, формат открытого ключа и детерминированные nonce.
Статические справочные данные — никаких входных данных и сетевых запросов, отправлять нечего
openssl dgst / openssl pkeyutl
браузеры, Deno, webcrypto в Node
Подпись в OpenSSL CLI → проверка в WebCrypto (crypto.subtle)
чтобы всё заработало, нужно исправить 2 пунктаФорматы подписи не совпадают: на выходе ASN.1 DER, ожидается raw r‖s
OpenSSL CLI по умолчанию выдаёт ASN.1 DER, а WebCrypto (crypto.subtle) ожидает raw r‖s. С ключом и математикой всё в порядке — проверяющая сторона просто не может прочитать байты: у DER-блока переменной длины не та длина, которую ждёт парсер фиксированной ширины.
Исправить можно на любой стороне: обе кодировки несут одну и ту же информацию, поэтому преобразование происходит без потерь и не затрагивает криптографию.
Встроенного способа нет
CLI не умеет выводить подписи ECDSA в формате P1363. Извлеките два INTEGER из структуры DER и дополните каждый слева до размера кривой (32 байта для P-256) — `openssl asn1parse -inform DER -in sig.der` выводит r и s, так что преобразование можно проверить на глаз.
Встроенного способа нет
Режима DER нет. Разделите результат фиксированной длины пополам, удалите ведущие нули из каждой половины и оберните два целых числа в DER SEQUENCE, прежде чем передавать их коду проверки в стиле OpenSSL.
† Поведение различается в разных версиях и сборках — проверьте его в своей версии.
См. также: конвертер DER ⇄ raw · форматы подписи ECDSA: DER и raw
Форматы открытого ключа не совпадают: SPKI PEM и JWK
OpenSSL CLI экспортирует открытый ключ в одном формате (SPKI PEM), а WebCrypto (crypto.subtle) ожидает другой (JWK). Это действительно разные контейнеры: SPKI оборачивает точку, добавляя к ней идентификатор кривой, JWK записывает координаты x и y в поля base64url, а SEC1-точка — это только байты координат. Сам ключевой материал во всех трёх одинаков.
OpenSSL CLI: `openssl ec -pubin -in pub.pem -pubout -outform DER` выдаёт тот же ключ в виде двоичного SPKI DER, а `openssl ec -pubin -in pub.pem -text -noout` выводит несжатую точку SEC1 в hex.
WebCrypto (crypto.subtle): importKey("spki", derBytes, …) принимает SPKI *DER*: сначала удалите строки заголовка и окончания PEM и декодируйте base64, потому что сам текст PEM отвергается.
См. также: проверка подписей ECDSA
Ограничьтесь кривыми P-256 / P-384 / P-521 — их поддерживают обе стороны
Наборы кривых не совпадают: secp256k1 — только в OpenSSL CLI. Выберите ключ на общей кривой, и эта проблема не возникнет.
OpenSSL CLI: Всё, что включено в сборке: кривые NIST над простыми полями, secp256k1, Brainpool и другие — точный список для вашего исполняемого файла выдаёт `openssl ecparam -list_curves`.
WebCrypto (crypto.subtle): Только три кривые NIST. secp256k1 не входит в спецификацию, поэтому для ключей Bitcoin и Ethereum вместо WebCrypto нужна библиотека на JavaScript.
OpenSSL CLI использует случайный nonce, если явно не включить RFC 6979
Обычно при подписи используется случайный nonce. Начиная с версии 3.2 параметр подписи `nonce-type:1` включает детерминированный ECDSA (RFC 6979) при явно указанном дайджесте: `openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` или `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`.
Это правильное поведение, а не ошибка, но из-за него один и тот же ключ и сообщение при каждом запуске дают разные байты, поэтому тест, сравнивающий подписи побайтно, упадёт. Сравнивайте вместо этого результаты проверки.
† Поведение различается в разных версиях и сборках — проверьте его в своей версии.
Сравнение всех библиотек
Только значения по умолчанию — любую строку можно изменить флагом или другим вызовом. Чтобы увидеть все столбцы, прокрутите таблицу вбок.
| Библиотека | Формат подписи по умолчанию | Как переключить | Политика low-S | RFC 6979 | Открытый ключ по умолчанию |
|---|---|---|---|---|---|
OpenSSL CLI openssl dgst / openssl pkeyutl подписывающая сторона | ASN.1 DER ASN.1 DER — SEQUENCE из двух INTEGER, поэтому подпись P-256 обычно занимает 70–72 байта, и её длина меняется от подписи к подписи. | В CLI нет режима raw r‖s — преобразуйте DER на выходе сторонними средствами.† | Принимает обе формы Политики low-S нет ни при подписи, ни при проверке: примерно в половине случаев подписи получаются с s в верхней половине диапазона, а проверке это безразлично. | Включается явно Обычно при подписи используется случайный nonce. Начиная с версии 3.2 параметр подписи `nonce-type:1` включает детерминированный ECDSA (RFC 6979) при явно указанном дайджесте: `openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` или `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`.† | SPKI PEM SPKI PEM: блок `-----BEGIN PUBLIC KEY-----`, записанный командой `openssl ec -pubout`. Учтите, что `-----BEGIN EC PUBLIC KEY-----` не существует: блок с меткой EC содержит закрытый ключ. |
Node.js node:crypto crypto.sign / crypto.verify / KeyObject | ASN.1 DER ASN.1 DER, если явно не запросить другую кодировку. | `dsaEncoding: "ieee-p1363"` в объекте ключа — задайте его и при подписи, *и* при проверке. | Принимает обе формы Принимает обе половины диапазона и ничего не нормализует: внутри работает OpenSSL. | Нет — случайный nonce Случайный nonce: один и тот же ключ и сообщение при каждом вызове дают разные байты подписи. | SPKI PEM Обычный вариант — `publicKey.export({ type: "spki", format: "pem" })`; JWK можно получить одним вызовом `export({ format: "jwk" })`. |
WebCrypto (crypto.subtle) браузеры, Deno, webcrypto в Node проверяющая сторона | Raw r‖s (P1363) Только raw r‖s фиксированной длины: 64 байта для P-256, 96 для P-384, 132 для P-521. Другой кодировки спецификация не определяет. | Переключать нечего: WebCrypto никогда не работает с DER. Преобразуйте подпись вне API. | Принимает обе формы Правила low-S нет ни при подписи, ни при проверке. | Нет — случайный nonce Случайный nonce, и API не позволяет передать собственный. | JWK При импорте и экспорте вы всегда указываете формат: «jwk», «spki» (байты DER) или «raw» (несжатая точка SEC1). JWK — форма, в которой ключи чаще всего передаёт веб-код. |
Go crypto/ecdsa SignASN1 / VerifyASN1 или пара big.Int | ASN.1 DER `SignASN1` (именно её вызывает `crypto.Signer`) возвращает ASN.1 DER. Низкоуровневая функция `Sign` отдаёт r и s как `*big.Int`: пока вы их не закодируете, это ни DER, ни raw. | `SignASN1`/`VerifyASN1` — для DER; `Sign`/`Verify` вместе с `FillBytes` — для raw фиксированной ширины. | Принимает обе формы Обработки low-S нет вообще: проверка принимает любую половину диапазона. | Нет — случайный nonce Несовместимо с RFC 6979. Последние версии выводят nonce одновременно из ключа, сообщения и свежих случайных данных, поэтому байты подписи при каждом запуске разные и зафиксировать их в тестах нельзя.† | SPKI DER `x509.MarshalPKIXPublicKey` выдаёт SPKI DER; блок PEM получается одним вызовом `pem.EncodeToMemory`. |
Python cryptography (pyca) hazmat.primitives.asymmetric.ec | ASN.1 DER DER — `private_key.sign(...)` возвращает ASN.1 SEQUENCE. | `decode_dss_signature` / `encode_dss_signature` преобразуют DER в пару целых чисел и обратно; дополнение до фиксированной ширины добавляете вы сами. | Принимает обе формы Принимает обе половины диапазона; s никто не нормализует. | Включается явно По умолчанию случайный nonce. Свежие релизы принимают в объекте ECDSA флаг детерминированной подписи, и для неё внутри нужен свежий OpenSSL — прежде чем на неё полагаться, проверьте, с каким OpenSSL собран ваш wheel.† | SPKI PEM `public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)` — тот же блок PEM, что и у OpenSSL. |
Python ecdsa (на чистом Python) SigningKey / VerifyingKey, sigencode= и sigdecode= | Raw r‖s (P1363) Raw r‖s: `sign()` возвращает оба значения, записанные подряд и дополненные нулями до размера кривой. | Аргументы `sigencode=` и `sigdecode=` задают кодировку при каждом вызове подписи и проверки. | Принимает обе формы Принимаются обе половины диапазона. В библиотеке есть и канонизирующие кодировщики, которые переводят s в нижнюю половину; они существуют наряду с обычными, и выбирать их нужно явно.† | Включается явно Детерминированная подпись по RFC 6979 доступна как отдельный метод подписи; обычный вызов sign() использует случайные данные. | SEC1-точка Ключи передаются как точка без обёртки: `VerifyingKey.to_string()` по умолчанию возвращает x‖y без префикса 0x04, а стеки на основе SPKI такой результат как есть не примут.† |
Java JCA (SunEC) java.security.Signature | ASN.1 DER `SHA256withECDSA` выдаёт ASN.1 DER. | Добавьте `inP1363Format` к имени алгоритма — тот же класс, другой формат передачи. | Принимает обе формы Политики low-S нет ни при подписи, ни при проверке. | Нет — случайный nonce SunEC подписывает со случайным nonce. Детерминированный ECDSA можно получить, подключив сторонний провайдер, например Bouncy Castle.† | SPKI DER `publicKey.getEncoded()` возвращает X.509 SPKI DER. Поддержки PEM в JDK нет: обернуть ключ в base64 придётся вам самим. |
Rust p256 / k256 (RustCrypto) ecdsa::Signature, SigningKey, VerifyingKey | Raw r‖s (P1363) Тип `Signature` по умолчанию — пара фиксированной ширины: 64 байта для P-256 и secp256k1. DER — отдельное явное преобразование.† | Пара фиксированной ширины — тип по умолчанию; преобразование в DER и обратно выполняется явно.† | Принимает обе формы Крейт для secp256k1 при подписи нормализует s в нижнюю половину, как принято в Bitcoin; крейты для кривых NIST этого не делают. Проверка принимает обе половины, поэтому, если другая сторона строгая, нормализуйте на стороне подписи.† | Да, по умолчанию По умолчанию детерминированные nonce по RFC 6979: один и тот же ключ и сообщение всегда дают одни и те же байты. Вариант со случайным nonce нужно включать явно. | SEC1-точка `VerifyingKey` сериализуется в закодированную точку SEC1: 33 байта в сжатой форме, 65 — в несжатой. SPKI DER и PEM доступны через сопутствующие трейты SPKI/PKCS#8.† |
libsecp256k1 (Bitcoin Core) secp256k1_ecdsa_sign / _verify | На ваш выбор Ни то, ни другое, пока вы не сериализуете подпись: она хранится в непрозрачной внутренней структуре, а при выводе вы выбираете compact (raw r‖s) или DER. | Сериализуйте в compact для raw r‖s или в DER для сетевого формата Bitcoin; выбор делается при каждом вызове. | Требует low-S Строгая библиотека. При подписи всегда получается low-S, а подпись с high-S при проверке сразу отвергается; функцию нормализации библиотека предоставляет именно для того, чтобы исправлять подписи, пришедшие от универсальной подписывающей библиотеки. | Да, по умолчанию По умолчанию RFC 6979; при необходимости можно передать другую функцию генерации nonce. | SEC1-точка Точки SEC1: 33 байта в сжатой форме или 65 байт в несжатой. Ни SPKI, ни PEM, ни JWK в API нет. |
PHP openssl_* openssl_sign / openssl_verify | ASN.1 DER DER — `openssl_sign()` возвращает ровно то, что выдал OpenSSL. | Режима raw нет вообще: преобразуйте в пользовательском коде или возьмите библиотеку JOSE, которая уже это делает. | Принимает обе формы Политики low-S нет: поведение OpenSSL наследуется без изменений. | Нет — случайный nonce Случайный nonce; детерминированного режима расширение не предоставляет. | SPKI PEM Строки PEM на входе и на выходе: `openssl_pkey_get_public()` принимает блок `-----BEGIN PUBLIC KEY-----` или целый сертификат. |
Ruby OpenSSL OpenSSL::PKey::EC | ASN.1 DER DER — `key.sign(digest, data)` возвращает ASN.1 SEQUENCE. | Встроенного режима raw нет: преобразуйте вручную через OpenSSL::ASN1 или доверьте это гему для JOSE. | Принимает обе формы Политики low-S нет — поведение OpenSSL без изменений. | Нет — случайный nonce Случайный nonce; детерминированного варианта нет. | SPKI PEM SPKI PEM. В последних версиях есть отдельный метод экспорта открытого ключа, и им стоит пользоваться: обычный `to_pem` для ключа с закрытой частью записывает закрытый ключ.† |
† Поведение различается в разных версиях и сборках — проверьте его в своей версии.
Что дальше
- Конвертер DER ⇄ raw — вставьте имеющуюся подпись и получите её в другой кодировке, вместе с r, s и проверкой high-S.
- Проверка подписей ECDSA — разрешите спор о том, верна ли подпись вообще, независимо от обеих библиотек.
- Форматы подписи ECDSA: DER и raw (P1363) — разбор на уровне байтов, который стоит за каждой строкой таблицы.
How it works
A signature that verifies in one library and fails in another is almost never wrong mathematically: the two sides simply disagree about packaging. An ECDSA signature is a pair of integers (r, s), and each ecosystem picked its own way to serialize them — ASN.1 DER in the OpenSSL and X.509 lineage, fixed-width r‖s in WebCrypto, JWS and most blockchain stacks. On top of that sit two more sources of disagreement: whether s must be in the lower half of the range (the low-S rule that Bitcoin enforces and general-purpose libraries ignore), and how the public key itself is wrapped — an SPKI structure, a JWK object, or the bare SEC1 point.
This page compares those defaults for a pair of libraries and lists only the differences that actually cause a failure, together with the switch or conversion that removes each one. Every row describes what a library does when you call the obvious API without extra arguments — not what it is capable of. Nearly all of it is configurable, which is exactly why two teams reading the same documentation end up incompatible, and why the fix is usually one option rather than new code.