Saltar al contenido
ecdsa.com
Python

coincurve

coincurve de Ofek Lev, bindings de Python para libsecp256k1 de Bitcoin Core: firmas DER sobre SHA-256 por defecto, RFC 6979, low-S estricto al firmar y al verificar, y firmas recuperables y Schnorr BIP-340.
Licencia
MIT OR Apache-2.0
Versión comprobada
21.0.0 ·
Código fuente
github.com/ofek/coincurve
Documentación
ofek.dev/coincurve/

Compatibilidad de un vistazo

ECDSA

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

coincurve envuelve libsecp256k1 con CFFI y distribuye wheels precompilados, de modo que el código Python obtiene sin compilador la misma implementación de secp256k1 de tiempo constante que usa Bitcoin Core. La versión 21.0.0 publica wheels solo para CPython de 3.10 a 3.13; en Python 3.14, pip recurre a compilar desde el código fuente, lo que falla en imágenes slim sin herramientas de compilación de C: por eso la comprobación se ejecutó en Python 3.12.

Los valores predeterminados se adaptan a Bitcoin más que a Ethereum. sign() calcula el hash del mensaje con SHA-256, usa un nonce RFC 6979 (en la comprobación, la firma coincidió con la de un firmante de referencia independiente) y devuelve una firma DER con s ya en la mitad baja. Ethereum, Nostr y la mayoría de las billeteras calculan el hash con keccak-256 o firman un resumen de 32 bytes, así que pasa un hasher o hasher=None; si dejas el valor predeterminado, obtienes firmas de aspecto válido sobre el hash equivocado.

La verificación hereda el rigor de libsecp256k1. verify() devolvió False para la forma high-S reflejada de una firma válida: una firma de OpenSSL, Java o un KMS en la nube falla aproximadamente la mitad de las veces salvo que s se normalice antes. Una entrada que no es DER lanza ValueError: The DER-encoded signature could not be parsed. en lugar de devolver False; las firmas en bruto de 64 bytes necesitan verify() sobre su codificación DER, y el conversor DER ⇄ en bruto muestra ambas formas.

Más allá del ECDSA básico, sign_recoverable() devuelve la forma r ‖ s ‖ ID de recuperación de 65 bytes y PublicKey.from_signature_and_message() recupera la clave, que es como se derivan las direcciones de Ethereum a partir de las firmas; sign_schnorr() produce firmas BIP-340 para Taproot y Nostr. Las claves públicas son puntos SEC1, comprimidos por defecto; las claves privadas son escalares en bruto de 32 bytes, y to_pem()/to_der() escriben PKCS#8.

El ejemplo muestra ambos modos: el SHA-256 predeterminado y un resumen keccak firmado de forma recuperable, como espera Ethereum. Comprueba los resultados con el verificador de mensajes de Bitcoin o con el verificador de firmas de Ethereum.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/coincurve/run.sh ejecutó coincurve 21.0.0 en la imagen de contenedor python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Codificación de la firma ECDSA

    der

    sign() devuelve DER; sign_recoverable() devuelve 65 bytes de r ‖ s ‖ ID de recuperación.

    Registrado por la comprobación
    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 predeterminado

    sha256

    La función hash predeterminada es SHA-256: sign(message) coincidió con una firma RFC 6979 de referencia sobre el SHA-256 del mensaje.

    Registrado por la comprobación
    fact ecdsa-default-hash = sha256
  • De qué calcula el hash la llamada de firma

    hashes-message-or-digest

    sign(digest, hasher=None) firma un resumen de 32 bytes sin modificarlo y dio la misma firma que sign(message) para su SHA-256.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign(sha256(m), hasher=None) equals sign(m): True
  • Nonce de ECDSA

    rfc6979

    RFC 6979: la firma coincidió con la de una implementación independiente del RFC 6979 (verificada a su vez con el vector de prueba del RFC), con s normalizado.

    Registrado por la comprobación
    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 al firmar

    low-s

    Ningún valor high-S entre 64 firmas.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = low-s
  • High-S al verificar

    rejects-high-s

    verify() devolvió False para la misma firma con s sustituido por n − s.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: verify() on (r, n - s): False
  • Firma ECDSA mal formada

    throws

    Una firma en bruto de 64 bytes o un blob de 3 bytes lanza ValueError: The DER-encoded signature could not be parsed.; una firma válida sobre otro mensaje devuelve False.

    Registrado por la comprobación
    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

    Solo secp256k1.

    Registrado por la comprobación
    fact ecdsa-curves = secp256k1
  • Firmas Schnorr

    BIP-340

    sign_schnorr() produce firmas BIP-340 de 64 bytes que se verifican con la clave pública x-only.

    Registrado por la comprobación
    fact schnorr = BIP-340
    evidence schnorr: PrivateKey.sign_schnorr available: True
  • EdDSA

    none

    Sin EdDSA.

    Registrado por la comprobación
    fact eddsa = none
  • Algoritmos poscuánticos

    none

    Sin algoritmos poscuánticos.

    Registrado por la comprobación
    fact pqc = none
  • Formatos de clave pública

    sec1-uncompressedsec1-compressed

    Las claves públicas se cargan desde puntos SEC1 comprimidos o desde puntos sin comprimir; un x‖y de 64 bytes se rechazó con The public key could not be parsed or is invalid.

    Registrado por la comprobación
    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 de clave pública predeterminado

    sec1-compressed

    format() devuelve el punto comprimido de 33 bytes salvo que se pase compressed=False.

    Registrado por la comprobación
    fact default-public-key = sec1-compressed
  • Formatos de clave privada

    pkcs8-pempkcs8-derraw-scalar

    Las claves privadas son escalares en bruto de 32 bytes; to_pem() y to_der() escriben PKCS#8 (-----BEGIN PRIVATE KEY-----), que from_pem() y from_der() vuelven a leer.

    Registrado por la comprobación
    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

Firma con el SHA-256 predeterminado y verifica; después firma un resumen keccak-256 de forma recuperable y recupera la clave 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))

El arnés de pruebas de fragmentos de código ejecuta cada ejemplo tal cual en la imagen de contenedor python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1, y la ejecución falla si la salida no coincide.

Configuración
pip install --quiet --root-user-action=ignore coincurve==21.0.0 pycryptodome==4.1.0
Comando
python usage.py
La salida debe coincidir (expresión regular)
verified: True\nrecovered matches: True 65

Bibliotecas