跳到主要内容
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

ECDSA 曲线
secp256k1
EdDSA
none
公钥格式
sec1-uncompressedsec1-compressed
私钥格式
pkcs8-pempkcs8-derraw-scalar

coincurve 用 CFFI 封装 libsecp256k1,并提供预编译的 wheel,因此 Python 代码无需编译器就能用上 Bitcoin Core 所用的同一套常量时间 secp256k1 实现。21.0.0 版只发布了 CPython 3.10 到 3.13 的 wheel;在 Python 3.14 上,pip 会退回到从源代码构建,而在没有 C 工具链的 slim 镜像中构建会失败——这就是本次检查在 Python 3.12 上运行的原因。

默认设置适合比特币,而不适合以太坊。sign() 用 SHA-256 对消息计算哈希,使用 RFC 6979 nonce(本次检查与一个独立的参考签名实现结果一致),并返回 s 已处于低半区的 DER 签名。以太坊、Nostr 和大多数钱包使用 keccak-256 计算哈希,或者直接对 32 字节的摘要签名,因此要传入 hasher 或 hasher=None;保留默认值会对错误的哈希生成看似有效的签名。

验证继承了 libsecp256k1 的严格性。对于由有效签名镜像得到的 high-S 形式,verify() 返回 False——来自 OpenSSL、Java 或云 KMS 的签名,如果不先对 s 做规范化,大约有一半会验证失败。非 DER 输入会抛出 ValueError: The DER-encoded signature could not be parsed.,而不是返回 False;原始的 64 字节签名需要先重新编码为 DER 再交给 verify(),DER ⇄ 原始格式转换器可以同时显示这两种形式。

除了普通 ECDSA,sign_recoverable() 返回 65 字节的 r ‖ s ‖ 恢复 ID 形式,PublicKey.from_signature_and_message() 可以恢复出公钥——以太坊正是这样从签名推导出地址的;sign_schnorr() 为 Taproot 和 Nostr 生成 BIP-340 签名。公钥是 SEC1 点,默认为压缩形式;私钥是 32 字节的原始标量,to_pem()/to_der() 写出 PKCS#8。

示例展示了两种模式:默认的 SHA-256,以及按以太坊的要求对 keccak 摘要做可恢复签名。可以用比特币消息验证工具或以太坊签名验证工具核对结果。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/coincurve/run.sh 于2026年10月20日在容器镜像 python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 中运行了 coincurve 21.0.0;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    der

    sign() 返回 DER;sign_recoverable() 返回 65 字节的 r ‖ s ‖ 恢复 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) 与对消息 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 坐标的公钥(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

    除非传入 compressed=False,否则 format() 返回 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

库