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

coincurve

coincurve от Ofek Lev — привязки Python к libsecp256k1 из Bitcoin Core: по умолчанию подписи DER по SHA-256, RFC 6979, строгий low-S и при подписи, и при проверке, подписи с восстановлением ключа и подписи Schnorr по BIP-340.
Лицензия
MIT OR Apache-2.0
Проверенная версия
21.0.0 ·
Исходный код
github.com/ofek/coincurve
Документация
ofek.dev/coincurve/

Совместимость вкратце

ECDSA

EdDSA
none
Форматы открытых ключей
sec1-uncompressedsec1-compressed

coincurve оборачивает libsecp256k1 через CFFI и поставляется в виде готовых wheel-пакетов, поэтому код на Python без компилятора получает ту же реализацию secp256k1 с постоянным временем выполнения, что и Bitcoin Core. Версия 21.0.0 публикует wheel-пакеты только для CPython от 3.10 до 3.13; на Python 3.14 pip переходит к сборке из исходного кода, которая в образах slim без набора инструментов для C завершается ошибкой; именно поэтому проверка выполнялась на Python 3.12.

Настройки по умолчанию рассчитаны на Bitcoin, а не на Ethereum. sign() хеширует сообщение SHA-256, использует nonce по RFC 6979 (результат совпал с независимой эталонной реализацией подписи) и возвращает подпись DER, в которой s уже находится в нижней половине. Ethereum, Nostr и большинство кошельков хешируют keccak-256 или подписывают 32-байтовый дайджест, поэтому передавайте свой hasher или hasher=None; если оставить значение по умолчанию, получатся подписи, которые выглядят корректными, но вычислены не от того хеша.

Проверка унаследовала строгость libsecp256k1. verify() вернул False для зеркальной формы high-S верной подписи, поэтому подпись из OpenSSL, Java или облачного KMS примерно в половине случаев не проходит проверку, если сначала не нормализовать s. Входные данные не в формате DER вызывают ValueError: The DER-encoded signature could not be parsed. вместо возврата False; 64-байтовые подписи raw нужно передавать в verify() после повторного кодирования в DER, а конвертер DER ⇄ raw показывает обе формы.

Помимо обычного ECDSA, sign_recoverable() возвращает 65-байтовую форму r ‖ s ‖ recovery ID, а PublicKey.from_signature_and_message() восстанавливает по ней ключ: так из подписей получают адреса Ethereum; sign_schnorr() создаёт подписи BIP-340 для Taproot и Nostr. Открытые ключи — точки SEC1, по умолчанию сжатые; закрытые ключи — 32-байтовые скаляры в формате raw, а to_pem()/to_der() записывают PKCS#8.

Пример показывает оба режима: SHA-256 по умолчанию и подпись дайджеста keccak с восстановлением ключа, как этого ожидает Ethereum. Сверяйте результаты с проверкой сообщений Bitcoin или проверкой подписей Ethereum.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/coincurve/run.sh запустила coincurve 21.0.0 в образе контейнера python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Кодировка подписи ECDSA

    der

    sign() возвращает DER; sign_recoverable() возвращает 65 байт: r ‖ s ‖ recovery ID.

    Что записала проверка
    fact ecdsa-signature-encoding = der
    evidence ecdsa-signature-encoding: sign_recoverable() returns 65 bytes (r||s||recovery id)
    evidence ecdsa-signature-encoding: verify_signature helper on the DER signature: True
  • Хеш-функция по умолчанию

    sha256

    Хеш-функция по умолчанию — SHA-256: результат sign(message) совпал с эталонной подписью по RFC 6979, вычисленной от SHA-256 сообщения.

    Что записала проверка
    fact ecdsa-default-hash = sha256
  • Что хеширует вызов подписи

    hashes-message-or-digest

    sign(digest, hasher=None) подписывает 32-байтовый дайджест как есть; подпись совпала с результатом sign(message) для сообщения с этим хешем SHA-256.

    Что записала проверка
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign(sha256(m), hasher=None) equals sign(m): True
  • Nonce в ECDSA

    rfc6979

    RFC 6979: с учётом нормализации s подпись совпала с результатом независимой реализации RFC 6979 (её саму сверили с тестовым вектором из RFC).

    Что записала проверка
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: sign(message) equals the reference RFC 6979 signature over SHA-256(message) with s normalized: True
  • Low-S при подписи

    low-s

    Среди 64 подписей нет ни одного значения high-S.

    Что записала проверка
    fact ecdsa-low-s-sign = low-s
  • High-S при проверке

    rejects-high-s

    Для той же подписи, в которой s заменили на n − s, verify() вернул False.

    Что записала проверка
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: verify() on (r, n - s): False
  • Некорректная подпись ECDSA

    throws

    64-байтовая подпись raw или 3-байтовый фрагмент данных вызывают ValueError: The DER-encoded signature could not be parsed.; корректная подпись другого сообщения даёт False.

    Что записала проверка
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: raw 64 bytes: ValueError: The DER-encoded signature could not be parsed.
    evidence ecdsa-malformed-signature: 3 bytes: ValueError: The DER-encoded signature could not be parsed.
    evidence ecdsa-malformed-signature: wrong message: False
  • Кривые ECDSA

    secp256k1

    Только secp256k1.

    Что записала проверка
    fact ecdsa-curves = secp256k1
  • Подписи Schnorr

    BIP-340

    sign_schnorr() создаёт 64-байтовые подписи BIP-340, которые проходят проверку по открытому ключу x-only.

    Что записала проверка
    fact schnorr = BIP-340
    evidence schnorr: PrivateKey.sign_schnorr available: True
  • EdDSA

    none

    EdDSA нет.

    Что записала проверка
    fact eddsa = none
  • Постквантовые алгоритмы

    none

    Постквантовых алгоритмов нет.

    Что записала проверка
    fact pqc = none
  • Форматы открытых ключей

    sec1-uncompressedsec1-compressed

    Открытые ключи загружаются из сжатых или несжатых точек SEC1; 64-байтовое значение x‖y было отклонено с ошибкой The public key could not be parsed or is invalid.

    Что записала проверка
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: 64-byte x||y: ValueError: The public key could not be parsed or is invalid.
  • Формат открытого ключа по умолчанию

    sec1-compressed

    format() возвращает 33-байтовую сжатую точку, если не передать compressed=False.

    Что записала проверка
    fact default-public-key = sec1-compressed
  • Форматы закрытых ключей

    pkcs8-pempkcs8-derraw-scalar

    Закрытые ключи — 32-байтовые скаляры в формате raw; to_pem() и to_der() записывают PKCS#8 (-----BEGIN PRIVATE KEY-----), а from_pem() и from_der() читают его обратно.

    Что записала проверка
    fact private-key-formats = pkcs8-pem, pkcs8-der, raw-scalar
    evidence private-key-formats: to_pem() writes -----BEGIN PRIVATE KEY-----; to_der() structure: pkcs8

Использование

Подпишите с SHA-256 по умолчанию и проверьте подпись, затем подпишите дайджест keccak-256 с возможностью восстановления и восстановите открытый ключ.
from coincurve import PrivateKey, PublicKey
from Crypto.Hash import keccak  # pycryptodome, for Ethereum-style hashing

key = PrivateKey()
message = b"order #1042 approved"

# Default: SHA-256 of the message, RFC 6979 nonce, low-S, DER output.
der = key.sign(message)
print("verified:", PublicKey(key.public_key.format()).verify(der, message))

# Ethereum and other keccak users must pass the hasher, or sign a ready digest with hasher=None.
digest = keccak.new(data=message, digest_bits=256).digest()
recoverable = key.sign_recoverable(digest, hasher=None)  # r || s || recovery id, 65 bytes
recovered = PublicKey.from_signature_and_message(recoverable, digest, hasher=None)
print("recovered matches:", recovered.format() == key.public_key.format(), len(recoverable))

Тестовый стенд для фрагментов кода запускает каждый пример без изменений в образе контейнера python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1, и запуск завершается ошибкой, если вывод не совпадает.

Подготовка
pip install --quiet --root-user-action=ignore coincurve==21.0.0 pycryptodome==4.1.0
Команда
python usage.py
Вывод должен совпадать (регулярное выражение)
verified: True\nrecovered matches: True 65

Библиотеки