本文へスキップ
ecdsa.com
Python

coincurve

Ofek Lev による coincurve は、Bitcoin Core の libsecp256k1 の Python バインディングです。デフォルトでは SHA-256 に対する DER 署名を生成し、RFC 6979 を使い、署名側でも検証側でも low-S を厳格に適用します。復元可能な署名と BIP-340 Schnorr 署名にも対応しています。
ライセンス
MIT OR Apache-2.0
確認したバージョン
21.0.0 ·
ソースコード
github.com/ofek/coincurve
ドキュメント
ofek.dev/coincurve/

互換性の概要

ECDSA

EdDSA
none
公開鍵の形式
sec1-uncompressedsec1-compressed
秘密鍵の形式
pkcs8-pempkcs8-derraw-scalar

coincurve は libsecp256k1 を CFFI でラップし、ビルド済みの wheel を配布しています。そのため Python のコードは、コンパイラーなしで Bitcoin Core と同じ定数時間の secp256k1 実装を使えます。バージョン 21.0.0 が wheel を公開しているのは CPython 3.10〜3.13 だけです。Python 3.14 では pip がソースからのビルドに切り替わり、C のツールチェーンがないスリムなイメージでは失敗します。チェックを Python 3.12 で実行したのはこのためです。

デフォルト設定は Ethereum ではなく Bitcoin 向けです。sign() はメッセージを SHA-256 でハッシュし、RFC 6979 の nonce を使い(チェックでは独立した参照用の署名実装と一致しました)、s が最初から low-S になっている DER 署名を返します。Ethereum、Nostr、多くのウォレットは keccak-256 でハッシュするか 32 バイトのダイジェストに署名するため、hasher を渡すか hasher=None を指定してください。デフォルトのままにすると、一見有効でも誤ったハッシュに対する署名が生成されます。

検証は libsecp256k1 の厳格さをそのまま受け継いでいます。有効な署名の s を n − s に置き換えた high-S 形式に対して、verify() は False を返しました。そのため、OpenSSL、Java、クラウドの KMS による署名は、先に s を正規化しない限り約半分の確率で失敗します。DER ではない入力に対しては、False を返さずに ValueError: The DER-encoded signature could not be parsed. が発生します。64 バイトの生の署名は DER に再エンコードしてから verify() に渡す必要があります。DER ⇄ 生形式の変換ツールで両方の形式を確認できます。

通常の ECDSA に加えて、sign_recoverable() は r ‖ s ‖ リカバリ ID からなる 65 バイトの形式を返し、PublicKey.from_signature_and_message() は公開鍵を復元します。Ethereum のアドレスを署名から導出するのはこの仕組みです。sign_schnorr() は Taproot や Nostr 向けの BIP-340 署名を生成します。公開鍵は SEC1 の点で、デフォルトでは圧縮形式です。秘密鍵は 32 バイトの生のスカラーで、to_pem()/to_der() は PKCS#8 を書き出します。

サンプルでは両方のモードを示しています。デフォルトの SHA-256 と、Ethereum が想定するとおりに keccak のダイジェストに復元可能な形で署名する方法です。結果は Bitcoin メッセージ検証ツールまたは Ethereum 署名検証ツールで確認してください。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/coincurve/run.sh がコンテナイメージ python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 内で coincurve 21.0.0 を実行しました。このページの内容はその出力と一致していなければならず、各事実には該当する出力行を示しています。

  • ECDSA 署名のエンコーディング

    der

    sign() は DER を返し、sign_recoverable() は r ‖ s ‖ リカバリ ID からなる 65 バイトを返します。

    チェックによる記録
    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) は、メッセージの SHA-256 に対する RFC 6979 の参照署名と一致しました。

    チェックによる記録
    fact ecdsa-default-hash = sha256
  • 署名呼び出しがハッシュする対象

    hashes-message-or-digest

    sign(digest, hasher=None) は 32 バイトのダイジェストにそのまま署名します。メッセージの SHA-256 を渡すと、sign(message) と同じ署名になりました。

    チェックによる記録
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign(sha256(m), hasher=None) equals sign(m): True
  • ECDSA の nonce

    rfc6979

    RFC 6979 です。署名は、独立した別の RFC 6979 実装(その実装自体も RFC のテストベクトルで確認済み)で s を正規化した結果と一致しました。

    チェックによる記録
    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 バイトの生の署名や 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 座標のみの公開鍵で検証できます。

    チェックによる記録
    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() は、compressed=False を渡さない限り 33 バイトの圧縮形式の点を返します。

    チェックによる記録
    fact default-public-key = sec1-compressed
  • 秘密鍵の形式

    pkcs8-pempkcs8-derraw-scalar

    秘密鍵は 32 バイトの生のスカラーです。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

ライブラリ