跳到主要内容
ecdsa.com
Python

python-ecdsa

tlsfuzzer 维护的纯 Python ECDSA 和 EdDSA 库:除非另行指定,否则对 SHA-1 摘要生成原始 r‖s 签名;默认使用随机 nonce,换一个方法即可使用 RFC 6979;公钥为裸 x‖y。
许可证
MIT
检查的版本
0.19.2 ·
源代码
github.com/tlsfuzzer/python-ecdsa
文档
ecdsa.readthedocs.io/en/latest/

兼容性概览

ECDSAEdDSA

ECDSA 曲线
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
公钥格式
spki-pemspki-dersec1-uncompressedsec1-compressedraw-xy
私钥格式
pkcs8-pemsec1-pemsec1-derraw-scalar

ecdsa 用纯 Python 实现 ECDSA 和 EdDSA,底层不依赖 OpenSSL。因此它在任何环境下都很容易安装,适合用于测试向量和教学,而且仍是 python-jose 等较老 JOSE 技术栈的依赖。它的维护者声明,该库并非为抵御侧信道攻击而设计,因此生产环境中的签名应交给 pyca/cryptography 或 coincurve。

几乎所有互操作失败都源于三个默认设置。哈希默认为 SHA-1:不指定 hashfunc 创建的 SigningKey 对 SHA-1 摘要签名——本次检查用 pyca/cryptography 验证默认签名,证实了这一点:用 SHA-1 验证通过,用 SHA-256 则失败。签名是原始 r‖s(在 P-256 上为 64 字节),而不是 DER。此外,VerifyingKey.to_string() 返回不带 0x04 前缀的 64 字节裸 x‖y,OpenSSL、Go 和 WebCrypto 都不会把它当作一个点来读取。

除非调用 sign_deterministic(),否则签名使用随机 nonce;该方法复现了 RFC 6979 的 P-256 测试向量。除非选用某个 *_canonize 编码器,否则 s 保持计算出的原值——大约一半的签名是 high-S——验证时两个半区都接受。每种编码选择都在每次调用时通过 sigencode= 和 sigdecode= 指定,因此签名方和验证方必须指定同一对;把 DER 签名交给默认的原始格式解码器,会抛出 BadSignatureError: Malformed formatting of signature。

它的曲线支持是 Python 库中最广的——P-224 到 P-521、secp256k1 和 Brainpool——另外还有 Ed25519 和 Ed448。密钥可以以 SPKI PEM 或 DER、SEC1 点和原始标量的形式导入;除非传入 format="pkcs8",否则 to_pem() 写出 SEC1 EC PRIVATE KEY。

示例一次性修正了全部三个默认设置:SHA-256、确定性 nonce,以及规范的 low-S DER 签名。要拿它生成的签名与其他库对照检查,最快的途径是 DER ⇄ 原始格式转换器和测试向量。

事实及其依据的检查

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

  • ECDSA 签名编码

    raw

    在 P-256 上,sign() 返回 64 字节的原始 r‖s;sigencode_der 生成 DER,此时验证需要使用 sigdecode_der。

    检查记录
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: sigencode_der output parses as DER: True; verifies with sigdecode_der: verified
  • 默认哈希

    sha1

    不指定 hashfunc 时,密钥对 SHA-1 摘要签名:默认签名在 pyca/cryptography 中用 SHA-1 验证通过,用 SHA-256 则未通过。

    检查记录
    fact ecdsa-default-hash = sha1
    evidence ecdsa-default-hash: checked by verifying the default signature in pyca/cryptography with SHA-1 and with SHA-256
  • 签名时的 low-S

    either

    普通编码器保留计算出的 s(64 个签名中出现了 high-S);sigencode_string_canonize 和 sigencode_der_canonize 会对其规范化。

    检查记录
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 plain signatures: True; among 64 with sigencode_string_canonize: False
    evidence ecdsa-low-s-sign: sigencode_strings_canonize exists: True
  • 验证时的 high-S

    accepts-high-s

    把 s 替换为 n − s 的签名验证通过。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
  • ECDSA 的 nonce

    rfc6979-opt-in

    sign() 使用随机 nonce;sign_deterministic() 复现了 RFC 6979 附录 A.2.5。

    检查记录
    fact ecdsa-nonce = rfc6979-opt-in
    evidence ecdsa-nonce: sign() repeats: False; sign_deterministic() reproduces RFC 6979 A.2.5: True
  • 签名调用对哪些内容做哈希

    hashes-message-or-digest

    sign() 会先对消息计算哈希;sign_digest() 对你自行计算的摘要签名,该签名针对原消息验证通过。

    检查记录
    fact ecdsa-prehash = hashes-message-or-digest
  • 格式错误的 ECDSA 签名

    throws

    长度错误的输入会抛出 BadSignatureError: Malformed formatting of signature;编码有效但针对错误消息的签名会抛出 BadSignatureError: Signature verification failed。

    检查记录
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: DER where raw is expected: BadSignatureError: Malformed formatting of signature
    evidence ecdsa-malformed-signature: 3 bytes: BadSignatureError: Malformed formatting of signature
    evidence ecdsa-malformed-signature: wrong message: BadSignatureError: Signature verification failed
  • ECDSA 曲线

    P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-224、P-256、P-384、P-521、secp256k1 和 Brainpool r1 曲线。

    检查记录
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    Ed25519 和 Ed448。

    检查记录
    fact eddsa = Ed25519, Ed448
  • 后量子算法

    none

    不支持后量子算法。

    检查记录
    fact pqc = none
  • 默认公钥格式

    raw-xy

    VerifyingKey.to_string() 返回不带 0x04 前缀的 64 字节 x‖y;要得到 SEC1 编码,需传入 "uncompressed" 或 "compressed"。

    检查记录
    fact default-public-key = raw-xy
  • 公钥格式

    spki-pemspki-dersec1-uncompressedsec1-compressedraw-xy

    公钥可从 SPKI PEM 和 DER、SEC1 点以及裸 x‖y 加载。

    检查记录
    fact public-key-formats = spki-pem, spki-der, sec1-uncompressed, sec1-compressed, raw-xy
  • 私钥格式

    pkcs8-pemsec1-pemsec1-derraw-scalar

    私钥可从 PKCS#8 PEM、SEC1 PEM 和 DER 以及原始标量加载;to_pem() 默认写出 -----BEGIN EC PRIVATE KEY-----。

    检查记录
    fact private-key-formats = pkcs8-pem, sec1-pem, sec1-der, raw-scalar
    evidence private-key-formats: to_pem() without format writes -----BEGIN EC PRIVATE KEY-----

用法

使用 SHA-256、RFC 6979 nonce 和规范的 low-S DER 编码签名,然后验证。
import hashlib

from ecdsa import NIST256p, SigningKey
from ecdsa.util import sigdecode_der, sigencode_der_canonize

key = SigningKey.generate(curve=NIST256p, hashfunc=hashlib.sha256)  # the default would be SHA-1
public = key.get_verifying_key()
message = b"order #1042 approved"

# RFC 6979 nonce, DER encoding, s normalized to the low half: what OpenSSL-style
# verifiers read and what strict low-S verifiers accept.
signature = key.sign_deterministic(message, sigencode=sigencode_der_canonize)
again = key.sign_deterministic(message, sigencode=sigencode_der_canonize)

print("deterministic:", signature == again)
print("verified:", public.verify(signature, message, sigdecode=sigdecode_der))
print(public.to_pem().decode().splitlines()[0])

代码片段测试工具会在容器镜像 python:3.14.8-slim-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
pip install --quiet --root-user-action=ignore ecdsa==0.19.2
命令
python usage.py
输出必须匹配(正则表达式)
deterministic: True\nverified: True\n-----BEGIN PUBLIC KEY-----

库