Zum Inhalt springen
ecdsa.com
Python

coincurve

coincurve von Ofek Lev, Python-Bindings für libsecp256k1 aus Bitcoin Core: standardmäßig DER-Signaturen über SHA-256, RFC 6979, striktes Low-S beim Signieren und Prüfen, wiederherstellbare Signaturen und Schnorr-Signaturen nach BIP-340.
Lizenz
MIT OR Apache-2.0
Geprüfte Version
21.0.0 ·
Quellcode
github.com/ofek/coincurve
Dokumentation
ofek.dev/coincurve/

Kompatibilität auf einen Blick

ECDSA

ECDSA-Kurven
secp256k1
EdDSA
none
Formate öffentlicher Schlüssel
sec1-uncompressedsec1-compressed
Formate privater Schlüssel
pkcs8-pempkcs8-derraw-scalar

coincurve bindet libsecp256k1 über CFFI ein und liefert vorkompilierte Wheels mit, sodass Python-Code ohne Compiler dieselbe zeitkonstante secp256k1-Implementierung erhält, die Bitcoin Core verwendet. Version 21.0.0 veröffentlicht Wheels nur für CPython 3.10 bis 3.13; unter Python 3.14 weicht pip auf einen Build aus dem Quellcode aus, der in schlanken Images ohne C-Toolchain scheitert – deshalb lief die Prüfung unter Python 3.12.

Die Standardeinstellungen passen eher zu Bitcoin als zu Ethereum. sign() hasht die Nachricht mit SHA-256, verwendet eine Nonce nach RFC 6979 (die Prüfung ergab Übereinstimmung mit einem unabhängigen Referenz-Signierer) und gibt eine DER-Signatur zurück, deren s bereits in der unteren Hälfte liegt. Ethereum, Nostr und die meisten Wallets hashen mit keccak-256 oder signieren einen 32-Byte-Digest; übergib daher einen hasher oder hasher=None. Bleibt der Standard stehen, entstehen gültig aussehende Signaturen über den falschen Hash.

Die Prüfung übernimmt die Strenge von libsecp256k1. verify() gab für die gespiegelte High-S-Form einer gültigen Signatur False zurück – eine Signatur aus OpenSSL, Java oder einem Cloud-KMS scheitert etwa in der Hälfte der Fälle, wenn s nicht vorher normalisiert wird. Eingaben, die kein DER sind, lösen ValueError: The DER-encoded signature could not be parsed. aus, statt False zurückzugeben; für rohe 64-Byte-Signaturen braucht verify() eine Neukodierung als DER, und der Konverter DER ⇄ Raw zeigt beide Formen.

Über einfaches ECDSA hinaus liefert sign_recoverable() die 65 Byte lange Form r ‖ s ‖ Recovery-ID, und PublicKey.from_signature_and_message() stellt daraus den Schlüssel wieder her – so werden Ethereum-Adressen aus Signaturen abgeleitet; sign_schnorr() erzeugt BIP-340-Signaturen für Taproot und Nostr. Öffentliche Schlüssel sind SEC1-Punkte, standardmäßig komprimiert; private Schlüssel sind rohe 32-Byte-Skalare, und to_pem()/to_der() schreiben PKCS#8.

Das Beispiel zeigt beide Modi: den Standard mit SHA-256 und einen keccak-Digest, der wiederherstellbar signiert wird, wie Ethereum es erwartet. Prüfe die Ergebnisse mit dem Bitcoin-Nachrichten-Verifizierer oder dem Ethereum-Signatur-Verifizierer.

Fakten und die Prüfungen dahinter

Nichts im Folgenden ist aus einer Dokumentation abgeschrieben. Das Prüfskript content/libraries/verify/coincurve/run.sh hat coincurve 21.0.0 am 20. Oktober 2026 im Container-Image python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ausgeführt; die Seite muss mit seiner Ausgabe übereinstimmen, und jeder Fakt führt die zugehörigen Zeilen auf.

  • Kodierung von ECDSA-Signaturen

    der

    sign() liefert DER; sign_recoverable() liefert 65 Byte aus r ‖ s ‖ Recovery-ID.

    Von der Prüfung aufgezeichnet
    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
  • Standard-Hash

    sha256

    Der Standard-Hasher ist SHA-256: sign(message) stimmte mit einer Referenzsignatur nach RFC 6979 über den SHA-256-Hash der Nachricht überein.

    Von der Prüfung aufgezeichnet
    fact ecdsa-default-hash = sha256
  • Was der Signaturaufruf hasht

    hashes-message-or-digest

    sign(digest, hasher=None) signiert einen 32-Byte-Digest unverändert und lieferte dieselbe Signatur wie sign(message), wenn der Digest der SHA-256-Hash dieser Nachricht ist.

    Von der Prüfung aufgezeichnet
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign(sha256(m), hasher=None) equals sign(m): True
  • ECDSA-Nonce

    rfc6979

    RFC 6979: Die Signatur stimmte mit einer unabhängigen Implementierung von RFC 6979 überein (die ihrerseits gegen den Testvektor des RFC geprüft wurde), mit normalisiertem s.

    Von der Prüfung aufgezeichnet
    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 beim Signieren

    low-s

    Kein High-S-Wert unter 64 Signaturen.

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-sign = low-s
  • High-S bei der Prüfung

    rejects-high-s

    verify() gab False für dieselbe Signatur zurück, in der s durch n − s ersetzt war.

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: verify() on (r, n - s): False
  • Fehlerhafte ECDSA-Signatur

    throws

    Eine rohe 64-Byte-Signatur oder ein 3-Byte-Blob löst ValueError: The DER-encoded signature could not be parsed. aus; eine gültige Signatur über eine andere Nachricht ergibt False.

    Von der Prüfung aufgezeichnet
    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-Kurven

    secp256k1

    Nur secp256k1.

    Von der Prüfung aufgezeichnet
    fact ecdsa-curves = secp256k1
  • Schnorr-Signaturen

    BIP-340

    sign_schnorr() erzeugt 64 Byte lange BIP-340-Signaturen, die die Prüfung gegen den öffentlichen Schlüssel in x-only-Form bestehen.

    Von der Prüfung aufgezeichnet
    fact schnorr = BIP-340
    evidence schnorr: PrivateKey.sign_schnorr available: True
  • EdDSA

    none

    Kein EdDSA.

    Von der Prüfung aufgezeichnet
    fact eddsa = none
  • Post-Quanten-Algorithmen

    none

    Keine Post-Quanten-Algorithmen.

    Von der Prüfung aufgezeichnet
    fact pqc = none
  • Formate öffentlicher Schlüssel

    sec1-uncompressedsec1-compressed

    Öffentliche Schlüssel werden aus SEC1-Punkten in komprimierter oder unkomprimierter Form geladen; ein 64 Byte langes x‖y wurde mit The public key could not be parsed or is invalid. abgelehnt.

    Von der Prüfung aufgezeichnet
    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.
  • Standardformat öffentlicher Schlüssel

    sec1-compressed

    format() liefert den komprimierten Punkt mit 33 Byte, sofern nicht compressed=False übergeben wird.

    Von der Prüfung aufgezeichnet
    fact default-public-key = sec1-compressed
  • Formate privater Schlüssel

    pkcs8-pempkcs8-derraw-scalar

    Private Schlüssel sind rohe 32-Byte-Skalare; to_pem() und to_der() schreiben PKCS#8 (-----BEGIN PRIVATE KEY-----), das from_pem() und from_der() wieder einlesen.

    Von der Prüfung aufgezeichnet
    fact private-key-formats = pkcs8-pem, pkcs8-der, raw-scalar
    evidence private-key-formats: to_pem() writes -----BEGIN PRIVATE KEY-----; to_der() structure: pkcs8

Verwendung

Signiere mit dem Standard SHA-256 und prüfe, signiere dann einen keccak-256-Digest wiederherstellbar und stelle den öffentlichen Schlüssel wieder her.
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))

Die Snippet-Testumgebung führt jedes Beispiel unverändert im Container-Image python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 aus, und der Lauf schlägt fehl, wenn die Ausgabe nicht übereinstimmt.

Einrichtung
pip install --quiet --root-user-action=ignore coincurve==21.0.0 pycryptodome==4.1.0
Befehl
python usage.py
Ausgabe muss passen (regulärer Ausdruck)
verified: True\nrecovered matches: True 65

Bibliotheken