椭圆曲线密码学:从 ECDSA 到 BLS

密码学 2026-09-15 · 阅读约 23 分钟

椭圆曲线密码学(ECC)是现代非对称加密的绝对主力:TLS 握手用它做密钥交换,比特币和以太坊用它签交易,Signal 协议用它建会话。它战胜 RSA 的理由很朴素——同等安全强度下密钥短一个数量级:256 位 ECC ≈ 3072 位 RSA。本文从椭圆曲线的群运算讲起,完整推导 ECDSA 的签名与验证,再用一段纯 Python 代码把 ECDSA 真正跑起来,并用教学模型演示 BLS 签名『千百个签名聚合成一个』的魔法性质。

一、椭圆曲线:一个『加法好算、除法难算』的群

密码学用的椭圆曲线定义在有限域上。以比特币选用的 secp256k1 为例,曲线方程是 y² = x³ + 7 (mod P),其中 P 是一个 256 位大素数。曲线上的点构成一个阿贝尔群,定义了两种运算:

  • 点加法 P + Q:几何上是过两点的直线与曲线的第三个交点关于 x 轴的对称点;代数上是一组有限域上的公式;
  • 标量乘法 k·G:把基点 G 自加 k 次。用翻倍-相加算法只需约 256 次点运算就能完成。

安全性的根基是椭圆曲线离散对数问题(ECDLP):给定 G 和 k·G,反推 k 在计算上不可行——最好的通用算法也需要约 2^128 次运算。这就是『知道公钥推不出私钥』的数学保证。本文实验中的点加、点倍、标量乘都是这段数学的直接实现。

二、ECDSA:签名与验证的完整推导

设私钥为 sk,公钥 pk = sk·G,消息哈希 z = H(m)。签名与验证的流程:

签名:  随机选 k ∈ [1, N-1]
       R = k·G,  r = R.x mod N
       s = k⁻¹·(z + r·sk) mod N
       输出签名 (r, s)

验证:  w = s⁻¹ mod N
       X = (z·w)·G + (r·w)·pk
       检查 X.x mod N == r

验证为什么成立?把 pk = sk·G 代入:

X = (z/s)·G + (r·sk/s)·G = ((z + r·sk)/s)·G
  而 s = (z + r·sk)/k  →  (z + r·sk)/s = k
  所以 X = k·G = R,  X.x mod N = r  ✓

伪造者没有 sk,构造不出满足这个等式的 (r, s)。但 ECDSA 有一个著名的命门:随机数 k 绝对不许重用。两次签名若用了同一个 k,由 s₁ - s₂ = k⁻¹(z₁ - z₂) 可直接解出 k,再代回解出私钥 sk。2010 年 Sony PS3 的签名密钥就是这样被公开的——它的 k 是固定值;Android 早期的 SecureRandom 缺陷也导致过一批比特币钱包私钥泄露。RFC 6979 的确定性 k(用私钥和消息哈希派生)就是为了根除这类实现事故。

三、BLS:配对带来的聚合魔法

ECDSA 的签名是一问一答:一个签名对应一次验证。区块链这类场景里,一个区块可能有几万个验证者签名,逐个验证成本太高。BLS 签名(Boneh-Lynn-Shacham)利用双线性配对改变了游戏规则。

配对是一个映射 e: G₁ × G₂ → G_T,满足双线性:e(a·P, b·Q) = e(P, Q)^(a·b)。基于配对的 BLS 流程极其优雅:

  • 签名:把消息哈希映射到曲线上的点 H(m),签名就是 σ = sk·H(m)——一行乘法;
  • 验证:检查 e(σ, G) == e(H(m), pk),由双线性,左边 = e(H(m), G)^sk,右边 = e(H(m), sk·G) = e(H(m), G)^sk,相等 ✓;
  • 聚合:n 个签名直接相加 σ_agg = σ₁ + … + σ_n,验证时用一次等式 e(σ_agg, G) == Π e(H(mᵢ), pkᵢ) 同时确认全部签名有效——无论聚合多少签名,聚合签名大小不变。

代价是配对运算昂贵:一次配对约相当于几百次标量乘法。所以 BLS 的定位很清晰——单签名场景 ECDSA/Schnorr 更快,聚合与阈值签名场景 BLS 无可替代。以太坊共识层用 BLS12-381 曲线,把每个 epoch 数万验证者的签名聚合成一个 96 字节签名,一次配对等式验证全网共识投票,这是 BLS 最大规模的生产实践。

四、动手实验:把 ECDSA 真正跑起来

下面代码用纯 Python 完整实现 secp256k1 上的 ECDSA 签名与验证(真实的数学,非模拟),并用一个明确标注的教学结构演示 BLS 聚合验证的代数性质。零依赖,Python 3 直接运行:

import hashlib, os

P = 2**256 - 2**32 - 977   # secp256k1
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
     0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)

def ec_add(a, b):
    if a is None: return b
    if b is None: return a
    if a[0] == b[0] and (a[1] != b[1] or a[1] == 0): return None
    lam = ((3*a[0]*a[0]) * pow(2*a[1], P-2, P) if a == b
           else (b[1]-a[1]) * pow((b[0]-a[0]) % P, P-2, P)) % P
    x = (lam*lam - a[0] - b[0]) % P
    return (x, (lam*(a[0]-x) - a[1]) % P)

def ec_mul(k, pt):
    r, add = None, pt
    while k:
        if k & 1: r = ec_add(r, add)
        add = ec_add(add, add); k >>= 1
    return r

def ecdsa_sign(sk, msg):
    z = int.from_bytes(hashlib.sha256(msg).digest(), 'big') % N
    while True:
        k = int.from_bytes(os.urandom(32)) % (N-1) + 1
        r = ec_mul(k, G)[0] % N
        if r == 0: continue
        s = pow(k, -1, N) * (z + r * sk) % N
        if s: return (r, s)

def ecdsa_verify(pk, msg, sig):
    r, s = sig
    z = int.from_bytes(hashlib.sha256(msg).digest(), 'big') % N
    w = pow(s, -1, N)
    X = ec_add(ec_mul(z*w % N, G), ec_mul(r*w % N, pk))
    return X is not None and X[0] % N == r

sk = int.from_bytes(os.urandom(32)) % (N-1) + 1
pk = ec_mul(sk, G)
sig = ecdsa_sign(sk, 'hello'.encode())
print(ecdsa_verify(pk, 'hello'.encode(), sig))   # True
print(ecdsa_verify(pk, 'forged'.encode(), sig))  # False

完整脚本(含 BLS 聚合教学演示)在沙箱上的真实运行输出:

== ECDSA 签名与验证 ==
  签名 r = 0x304791563ea0d5874e153f18a94b1f00c73006438596a748315dfc87e0dbb555...
  正确签名验证: 通过
  篡改消息验证: 失败 -> 伪造被检测

== BLS 聚合签名(玩具配对教学演示) ==
  3 个签名聚合成 1 个, 大小不变
  聚合验证(一次验证全部交易): 通过
  篡改一笔后聚合验证: 失败 -> 可检测

校验通过: ECDSA 可验可防伪造, BLS 聚合一次验证多个签名且篡改可检测

五、结果解读

  • ECDSA 的真实运算:实验里的点加、标量乘、模逆都是按 secp256k1 的真实参数执行的,签名结构 (r, s) 与比特币交易里的完全一致。篡改消息后验证失败,正是 ECDLP 困难性在工程上的投影。
  • BLS 演示的边界:实验用『标量相加 + 乘法同态』的玩具结构演示了聚合验证的代数骨架——Σ sigᵢ = Σ hᵢ·skᵢ 两侧相等当且仅当每个签名都正确。真实 BLS 把这个等式搬到配对群上,安全强度来自配对群的离散对数困难性,且无法在标量层面反推(这正是玩具结构『玩具』的地方:它可从点反推标量,故绝无安全性可言)。聚合后大小不变、一次验证全批这两个性质则是完全真实的。
  • 开发插曲:实验初版把中文直接写进了 bytes 字面量,触发 SyntaxError——Python 的 bytes 只接受 ASCII 字面字符,中文需先 .encode('utf-8')。密码学代码里编码问题不是小事:同一份消息在签名端和验证端字节表示不同,验证必然失败。

六、工程应用

  • 比特币/以太坊转账:secp256k1 + ECDSA;比特币 Taproot 升级(BIP340)后转向 Schnorr 签名,获得线性聚合能力且证明更简洁;
  • 以太坊共识层:BLS12-381 上的 BLS 聚合签名承载全网验证者投票;
  • Ed25519:Curve25519 上的 EdDSA,用确定性随机数根除 k 重用事故,性能与实现鲁棒性俱佳,是新建系统(SSH、Signal、大量 API 签名)的首选;
  • TLS 1.3 的 ECDHE:x25519 密钥交换——本站《TLS 1.3 握手协议深度剖析》里的实验用的正是同一套点运算;
  • 阈值签名(TSS):BLS 天然支持 n-of-n 与 t-of-n 阈值签名(密钥分片 + 签名聚合),是多方托管钱包与分布式验证者技术(DVT)的基石。

七、常见误区

  1. 自己实现曲线运算用于生产:本文代码用于理解数学,未做常数时间防护、未做公钥合法性校验(易受无效曲线攻击),生产环境必须用 libsecp256k1、BoringSSL 这类成熟库。
  2. 忽视随机数质量:ECDSA 的 k 重用一次即私钥泄露。优先选 Ed25519 或带 RFC 6979 确定性 k 的实现。
  3. 以为 BLS 全面优于 ECDSA:配对运算比标量乘贵两个数量级,单签名验证 BLS 更慢;它的价值只在聚合/阈值场景。
  4. 盲信曲线参数:NIST P-256 的参数生成过程不透明曾引发争议;secp256k1 与 Curve25519 的参数是可验证推导的。选型时『参数从哪来』与『曲线多大』同样重要。
  5. 忽略哈希到曲线的细节:BLS 需要把消息哈希映射为曲线点(hash-to-curve,RFC 9380),朴素取模会引入分布偏差,曾有真实实现因此产生可伪造签名。

八、参考文献

  • Boneh, D., Lynn, B., Shacham, H. Short Signatures from the Weil Pairing. Journal of Cryptology 17(4), 2004.(BLS 原始论文)
  • Johnson, D., Menezes, A., Vanstone, S. The Elliptic Curve Digital Signature Algorithm (ECDSA). IJIS 1(1), 2001.
  • Koblitz, N. Elliptic Curve Cryptosystems. Mathematics of Computation 48(177), 1987.
  • Bernstein, D. et al. High-Speed High-Security Signatures (Ed25519). Journal of Cryptographic Engineering 2(2), 2012.
  • Hankerson, D., Menezes, A., Vanstone, S. Guide to Elliptic Curve Cryptography. Springer, 2004.