Pular para o conteúdo
ecdsa.com
Python

coincurve

O coincurve, de Ofek Lev, bindings Python para a libsecp256k1 do Bitcoin Core: assinaturas DER sobre SHA-256 por padrão, RFC 6979, low-S estrito nos dois lados, assinaturas recuperáveis e assinaturas Schnorr BIP-340.
Licença
MIT OR Apache-2.0
Versão verificada
21.0.0 ·
Código-fonte
github.com/ofek/coincurve
Documentação
ofek.dev/coincurve/

Visão geral da compatibilidade

ECDSA

Curvas ECDSA
secp256k1
EdDSA
none
Formatos de chave pública
sec1-uncompressedsec1-compressed
Formatos de chave privada
pkcs8-pempkcs8-derraw-scalar

O coincurve encapsula a libsecp256k1 com CFFI e distribui wheels pré-compilados, então o código Python recebe, sem precisar de um compilador, a mesma implementação de secp256k1 em tempo constante que o Bitcoin Core usa. A versão 21.0.0 publica wheels apenas para CPython 3.10 a 3.13; no Python 3.14, o pip recorre a um build a partir do código-fonte, que falha em imagens slim sem uma toolchain de C — por isso o teste rodou no Python 3.12.

Os padrões servem ao Bitcoin, e não ao Ethereum. sign() calcula o hash da mensagem com SHA-256, usa um nonce RFC 6979 (o resultado do teste coincidiu com o de um signatário de referência independente) e retorna uma assinatura DER com o s já na metade baixa. O Ethereum, o Nostr e a maioria das carteiras calculam o hash com keccak-256 ou assinam um digest de 32 bytes, então passe um hasher ou hasher=None; manter o padrão produz assinaturas de aparência válida sobre o hash errado.

A verificação herda o rigor da libsecp256k1. verify() retornou False para a forma high-S espelhada de uma assinatura válida — uma assinatura vinda do OpenSSL, do Java ou de um KMS de nuvem falha em cerca de metade das vezes, a menos que o s seja normalizado antes. Uma entrada que não é DER lança ValueError: The DER-encoded signature could not be parsed. em vez de retornar False; assinaturas brutas de 64 bytes precisam passar para a codificação DER antes de verify(), e o conversor DER ⇄ bruto mostra as duas formas.

Além do ECDSA simples, sign_recoverable() retorna a forma de 65 bytes r ‖ s ‖ ID de recuperação, e PublicKey.from_signature_and_message() recupera a chave, que é como endereços Ethereum são derivados a partir de assinaturas; sign_schnorr() produz assinaturas BIP-340 para Taproot e Nostr. As chaves públicas são pontos SEC1, comprimidos por padrão; as chaves privadas são escalares brutos de 32 bytes, e to_pem()/to_der() gravam PKCS#8.

O exemplo mostra os dois modos: o padrão com SHA-256 e um digest keccak assinado de forma recuperável, como o Ethereum espera. Confira os resultados no verificador de mensagens Bitcoin ou no verificador de assinaturas Ethereum.

Os fatos e as verificações por trás deles

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/coincurve/run.sh executou coincurve 21.0.0 na imagem de contêiner python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 em 20 de outubro de 2026; a página precisa corresponder ao que a verificação imprimiu, e cada fato lista essas linhas.

  • Codificação da assinatura ECDSA

    der

    sign() retorna DER; sign_recoverable() retorna 65 bytes: r ‖ s ‖ ID de recuperação.

    Registrado pela verificação
    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
  • Hash padrão

    sha256

    A função de hash padrão é SHA-256: sign(message) coincidiu com uma assinatura RFC 6979 de referência sobre o SHA-256 da mensagem.

    Registrado pela verificação
    fact ecdsa-default-hash = sha256
  • Entrada do hash na chamada de assinatura

    hashes-message-or-digest

    sign(digest, hasher=None) assina um digest de 32 bytes sem alterá-lo e, com o SHA-256 da mensagem, produziu a mesma assinatura que sign(message).

    Registrado pela verificação
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign(sha256(m), hasher=None) equals sign(m): True
  • Nonce do ECDSA

    rfc6979

    RFC 6979: a assinatura coincidiu com a de uma implementação independente da RFC 6979 (ela própria conferida com o vetor de teste da RFC), com o s normalizado.

    Registrado pela verificação
    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 ao assinar

    low-s

    Nenhum valor high-S em 64 assinaturas.

    Registrado pela verificação
    fact ecdsa-low-s-sign = low-s
  • High-S ao verificar

    rejects-high-s

    verify() retornou False para a mesma assinatura com s substituído por n − s.

    Registrado pela verificação
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: verify() on (r, n - s): False
  • Assinatura ECDSA malformada

    throws

    Uma assinatura bruta de 64 bytes ou um blob de 3 bytes lança ValueError: The DER-encoded signature could not be parsed.; uma assinatura válida sobre outra mensagem retorna False.

    Registrado pela verificação
    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
  • Curvas ECDSA

    secp256k1

    Apenas secp256k1.

    Registrado pela verificação
    fact ecdsa-curves = secp256k1
  • Assinaturas Schnorr

    BIP-340

    sign_schnorr() produz assinaturas BIP-340 de 64 bytes que são verificadas com a chave pública x-only.

    Registrado pela verificação
    fact schnorr = BIP-340
    evidence schnorr: PrivateKey.sign_schnorr available: True
  • EdDSA

    none

    Sem EdDSA.

    Registrado pela verificação
    fact eddsa = none
  • Algoritmos pós-quânticos

    none

    Sem algoritmos pós-quânticos.

    Registrado pela verificação
    fact pqc = none
  • Formatos de chave pública

    sec1-uncompressedsec1-compressed

    Chaves públicas são carregadas de pontos SEC1 na forma não comprimida ou comprimida; um x‖y de 64 bytes foi rejeitado com The public key could not be parsed or is invalid.

    Registrado pela verificação
    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.
  • Formato padrão da chave pública

    sec1-compressed

    format() retorna o ponto comprimido de 33 bytes, a menos que se passe compressed=False.

    Registrado pela verificação
    fact default-public-key = sec1-compressed
  • Formatos de chave privada

    pkcs8-pempkcs8-derraw-scalar

    As chaves privadas são escalares brutos de 32 bytes; to_pem() e to_der() gravam PKCS#8 (-----BEGIN PRIVATE KEY-----), que from_pem() e from_der() leem de volta.

    Registrado pela verificação
    fact private-key-formats = pkcs8-pem, pkcs8-der, raw-scalar
    evidence private-key-formats: to_pem() writes -----BEGIN PRIVATE KEY-----; to_der() structure: pkcs8

Uso

Assine com o SHA-256 padrão e verifique; depois, assine um digest keccak-256 de forma recuperável e recupere a chave pública.
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))

O executor de trechos de código roda cada exemplo literalmente na imagem de contêiner python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1, e a execução falha se a saída não corresponder.

Configuração
pip install --quiet --root-user-action=ignore coincurve==21.0.0 pycryptodome==4.1.0
Comando
python usage.py
A saída deve corresponder a (expressão regular)
verified: True\nrecovered matches: True 65

Bibliotecas