Перейти к содержимому
ecdsa.com

Library Interop Cheatsheet — шпаргалка по совместимости библиотек

Вы подписываете в библиотеке A, а проверяете в библиотеке B — что сломается? Выберите обе стороны и получите чек-лист конкретных расхождений со строкой кода, которая исправляет каждое из них: кодировка подписи, обработка high-S, формат открытого ключа и детерминированные nonce.

Статические справочные данные — никаких входных данных и сетевых запросов, отправлять нечего

openssl dgst / openssl pkeyutl

браузеры, Deno, webcrypto в Node

Подписывающая сторона
Проверяющая сторона
Библиотека
OpenSSL CLI
WebCrypto (crypto.subtle)
Подпись
ASN.1 DER
Raw r‖s (P1363)
Low-S
Принимает обе формы · подписывает в любой форме
Принимает обе формы · подписывает в любой форме
RFC 6979
Включается явно
Нет — случайный nonce
Открытый ключ
SPKI PEM
JWK
Кривые
P-256, P-384, P-521, secp256k1
P-256, P-384, P-521

Подпись в OpenSSL CLI → проверка в WebCrypto (crypto.subtle)

чтобы всё заработало, нужно исправить 2 пункта
Не работает

Форматы подписи не совпадают: на выходе ASN.1 DER, ожидается raw r‖s

OpenSSL CLI по умолчанию выдаёт ASN.1 DER, а WebCrypto (crypto.subtle) ожидает raw r‖s. С ключом и математикой всё в порядке — проверяющая сторона просто не может прочитать байты: у DER-блока переменной длины не та длина, которую ждёт парсер фиксированной ширины.

Исправить можно на любой стороне: обе кодировки несут одну и ту же информацию, поэтому преобразование происходит без потерь и не затрагивает криптографию.

На подписывающей сторонеВ OpenSSL CLI выдавайте raw r‖s

Встроенного способа нет

CLI не умеет выводить подписи ECDSA в формате P1363. Извлеките два INTEGER из структуры DER и дополните каждый слева до размера кривой (32 байта для P-256) — `openssl asn1parse -inform DER -in sig.der` выводит r и s, так что преобразование можно проверить на глаз.

На проверяющей сторонеВ WebCrypto (crypto.subtle) принимайте ASN.1 DER

Встроенного способа нет

Режима 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-SRFC 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` для ключа с закрытой частью записывает закрытый ключ.†

† Поведение различается в разных версиях и сборках — проверьте его в своей версии.

Что дальше

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.