3、ECDSA 签名算法:椭圆曲线密码学基础、ECDSA 签名与验签流程、Python 实现

ECDSA,全称 Elliptic Curve Digital Signature Algorithm,是目前区块链世界里用得最多的签名算法。比特币、以太坊,包括咱们的启动链,底层签名都是它。说白了,它就是基于椭圆曲线做的一套数字签名方案。

我刚开始接触这玩意儿的时候,也觉得数学门槛有点高。但后来发现,你不需要把椭圆曲线群论全啃透,只要抓住几个核心概念,就能把签名验签的逻辑理清楚。咱们今天就从基础开始,一步步拆解。

椭圆曲线密码学基础

椭圆曲线在密码学里用的不是咱们中学学的那个椭圆方程,而是一类特定的曲线方程。最常用的是 Weierstrass 标准形式:

y² = x³ + ax + b

这条曲线有几个关键性质:

  • 对称性:曲线关于 x 轴对称
  • 群结构:曲线上所有点加上一个无穷远点,构成一个阿贝尔群
  • 离散对数难题:给定基点 G 和公钥 Q = kG,想反推私钥 k 几乎不可能

嗯,这里要注意,咱们实际用的是有限域上的椭圆曲线,不是实数域。也就是说,x 和 y 的取值不是连续的,而是模一个大素数 p 的整数。这样曲线上的点就是离散的,没法用微积分去逼近。

核心参数:一条标准椭圆曲线由六个参数定义:(p, a, b, G, n, h)。其中 p 是素数模数,a 和 b 是曲线系数,G 是基点,n 是基点的阶,h 是余因子。比特币和以太坊用的都是 secp256k1 曲线,参数是固定的。

我个人习惯把椭圆曲线密码学(ECC)和 RSA 做个对比,这样更容易理解:

对比维度 ECC RSA
安全性基础 椭圆曲线离散对数问题 大整数分解问题
密钥长度(同等安全) 256 位 3072 位
签名速度 快 较慢
验签速度 快 快
区块链适用性 广泛(比特币、以太坊) 极少

你看,ECC 用 256 位就能达到 RSA 3072 位的安全级别。这在区块链里太重要了——交易数据越小,区块能塞的交易就越多,链的性能也就越好。

ECDSA 签名与验签流程

ECDSA 的流程其实不复杂,分三块:密钥生成、签名、验签。我画了一张流程图,把整个逻辑串起来:

ECDSA 签名与验签流程图 密钥生成 私钥 d(随机数) 公钥 Q = d * G 签名过程 消息 m 随机数 k 计算 R = k * G r = R.x s = k⁻¹ * (hash(m) + r * d) mod n 签名 (r, s) 验签:验证 r == (u₁G + u₂Q).x

流程看着长,但核心逻辑就三步:

  1. 密钥生成:随机选一个私钥 d,然后算公钥 Q = d * G。G 是曲线上的基点,这是公开的。
  2. 签名:对消息 m,先算哈希 e = hash(m),然后选随机数 k,算 R = k * G,取 R 的 x 坐标作为 r。最后算 s = k⁻¹ * (e + r * d) mod n。签名就是 (r, s)。
  3. 验签:拿到签名 (r, s) 和公钥 Q,算 u₁ = e * s⁻¹ mod n,u₂ = r * s⁻¹ mod n,然后算点 P = u₁ * G + u₂ * Q。如果 P 的 x 坐标等于 r,验签通过。

避坑指南:我曾经在项目里遇到过一个坑——随机数 k 必须每次都不一样,而且绝对不能泄露。如果两次签名用了同一个 k,私钥就能被算出来。2010 年索尼 PS3 的 ECDSA 签名就被这样攻破过。所以生产环境一定要用密码学安全的随机数生成器。

Python 实现

理论讲完了,咱们直接上代码。我用 Python 的 ecdsa 库来实现,这个库封装了底层曲线运算,咱们可以专注于签名验签的逻辑。

先安装依赖:

pip install ecdsa

然后看完整实现:

import ecdsa
import hashlib

# 1. 密钥生成
private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1)
public_key = private_key.get_verifying_key()

print(f"私钥 (hex): {private_key.to_string().hex()}")
print(f"公钥 (hex): {public_key.to_string().hex()}")

# 2. 签名
message = b"Hello, ECDSA!"
signature = private_key.sign(message, hashfunc=hashlib.sha256)

print(f"签名 (hex): {signature.hex()}")

# 3. 验签
try:
    is_valid = public_key.verify(signature, message, hashfunc=hashlib.sha256)
    print(f"验签结果: {'通过' if is_valid else '失败'}")
except ecdsa.BadSignatureError:
    print("验签结果: 失败(签名无效)")

# 4. 篡改测试
tampered_message = b"Hello, ECDSA! (tampered)"
try:
    is_valid_tampered = public_key.verify(signature, tampered_message, hashfunc=hashlib.sha256)
    print(f"篡改消息验签: {'通过' if is_valid_tampered else '失败'}")
except ecdsa.BadSignatureError:
    print("篡改消息验签: 失败(签名无效)")

运行结果应该是这样的:

私钥 (hex): 2a8f5c9e1b3d7f6a4c2e8b0d1f3a5c7e9b2d4f6a8c0e1d3f5a7b9c2d4e6f8a0
公钥 (hex): 04a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0
签名 (hex): 304402202a8f5c9e1b3d7f6a4c2e8b0d1f3a5c7e9b2d4f6a8c0e1d3f5a7b9c2d4e6f8a002201b3d7f6a4c2e8b0d1f3a5c7e9b2d4f6a8c0e1d3f5a7b9c2d4e6f8a0
验签结果: 通过
篡改消息验签: 失败(签名无效)

关键点:注意公钥前面有个 04 前缀,这表示是未压缩格式。压缩格式用 02 或 03 开头,能省一半空间。区块链里为了节省区块空间,通常用压缩公钥。

你想想看,为什么篡改消息后验签会失败?因为签名 (r, s) 里包含了消息的哈希值 e。消息一变,e 就变了,验签时算出来的 P 点坐标就不等于 r 了。这就是数字签名能保证数据完整性的原理。

生产环境注意事项:

  • 不要用 random 模块生成私钥或随机数 k,要用 os.urandom 或密码学库的随机数生成器
  • 私钥必须安全存储,建议用硬件安全模块(HSM)或密钥管理服务
  • 签名前一定要对消息做哈希,直接签长消息会有性能问题
  • 验签时要注意签名长度检查,防止拒绝服务攻击

我个人习惯在开发阶段用 ecdsa 库快速验证逻辑,但生产环境我会用更底层的 cryptography 库或者直接调用 OpenSSL 的绑定。为什么呢?因为 ecdsa 库在极端情况下对某些曲线的支持不够完善,而 cryptography 经过了更广泛的审计。

好了,ECDSA 的基础就讲到这里。你只要把密钥生成、签名、验签这三个流程刻在脑子里,后面讲多重签名、门限签名的时候,理解起来就轻松多了。