ECDSA 椭圆曲线签名:比 RSA 更短却更安全
同样 128 位安全强度,RSA 密钥要 3072 位,ECDSA 只要 256 位——签名还快一到两个数量级。但它有一条致命红线:nonce 一旦重复,私钥瞬间泄漏。今天讲透原理与这条红线。
一、背景与痛点
你每天刷的 HTTPS、签的 Git commit、转的比特币,很多都跑在 ECDSA 上。Bitcoin 从 2009 年起就用 secp256k1 上的 ECDSA 签名每一笔交易;TLS 1.2 末期大量证书从 RSA 迁移到 ECDSA,只因为它"又小又快"。同样的安全强度下,RSA 需要 3072 位密钥、签名 256 字节,而 ECDSA 用 256 位曲线、签名只有 64 字节,私钥签名还快约两个数量级。对带宽和算力都敏感的移动端、物联网、区块链,这个差距是决定性的。
但 ECDSA 是把双刃剑,它的安全性严重依赖一个叫 nonce(临时随机数 kkk)的中间量。2010 年 Sony PS3 就是因为所有签名复用了同一个 kkk,攻击者从两条签名就还原出了索尼的私钥,整个主机的安全体系崩塌。所以理解 ECDSA,一半是理解椭圆曲线,另一半是理解"kkk 绝对不能重复"这条红线。
二、核心原理
1. 从 ECDH 到签名
椭圆曲线密码学把 Diffie-Hellman 搬到了椭圆曲线群上。设 EEE 是 Fp\mathbb{F}_pFp 上的曲线、GGG 是阶为 nnn 的基点,双方各选私钥 a,ba,ba,b、计算公钥 A=aG, B=bGA=aG,\;B=bGA=aG,B=bG 并交换,就能各自算出同一个共享秘密:
S=aB=a(bG)=abG=bAS = aB = a(bG) = abG = bAS=aB=a(bG)=abG=bA
外人只知道 G,A,BG, A, BG,A,B,想凑出 SSS 就得先求出 aaa——这正是 ECDLP(椭圆曲线离散对数)难题。ECDSA 则用同一套群运算做签名:私钥 ddd、公钥 Q=dGQ=dGQ=dG。
2. 签名与验证公式
对消息 mmm,先算散列 z=H(m)z=H(m)z=H(m),再选一个临时随机数 kkk,令 (x1,y1)=kG(x_1,y_1)=kG(x1,y1)=kG、r=x1 mod nr=x_1\bmod nr=x1modn,则签名为:
s=k−1(z+r⋅d) mod n,签名=(r,s)s = k^{-1}(z + r\cdot d)\bmod n, \qquad \text{签名} = (r, s)s=k−1(z+r⋅d)modn,签名=(r,s)
验证方计算 u1=zs−1 mod nu_1=zs^{-1}\bmod nu1=zs−1modn、u2=rs−1 mod nu_2=rs^{-1}\bmod nu2=rs−1modn,再算 (x1,y1)=u1G+u2Q(x_1,y_1)=u_1G+u_2Q(x1,y1)=u1G+u2Q,核对 r=x1 mod nr = x_1\bmod nr=x1modn 是否成立。它之所以成立,全靠一次巧妙的抵消:
u1G+u2Q=s−1(z+rd)G=s−1(sk)G=kGu_1G + u_2Q = s^{-1}(z + rd)G = s^{-1}(sk)G = kGu1G+u2Q=s−1(z+rd)G=s−1(sk)G=kG
于是验证方算出的 x1x_1x1 与签名时的 x1x_1x1 完全一致,验证通过。
3. 致命红线:nonce 重用
kkk 只要重复一次,私钥就没了。假设两条不同消息用同一个 kkk,签名分别是 (r,s1),(r,s2)(r,s_1),(r,s_2)(r,s1),(r,s2),注意它们的 rrr 相同:
s1−s2=k−1(z1−z2) mod ns_1 - s_2 = k^{-1}(z_1 - z_2)\bmod ns1−s2=k−1(z1−z2)modn
于是 k=(z1−z2)(s1−s2)−1 mod nk=(z_1-z_2)(s_1-s_2)^{-1}\bmod nk=(z1−z2)(s1−s2)−1modn,再代回即可解出私钥 d=(s1k−z1)r−1 mod nd=(s_1k-z_1)r^{-1}\bmod nd=(s1k−z1)r−1modn。Sony PS3 就是栽在这里。工程上的标准解法是 RFC 6979:不再用随机数发生器选 kkk,而是从私钥和消息确定性派生:
k=HMAC_DRBG(d ∥ H(m))k = \text{HMAC\_DRBG}(d \,\|\, H(m))k=HMAC_DRBG(d∥H(m))
这样同一消息永远得到同一个 kkk,既杜绝了重复,也免疫了"随机源太弱"的隐患。
4. 为什么 256 位就够
ECDSA 的安全性建立在 ECDLP 之上:目前求解它最强的通用算法是 Pollard rho,需要约 O(n)O(\sqrt{n})O(n) 步。256 位曲线的 n≈2256n\approx 2^{256}n≈2256,n≈2128\sqrt{n}\approx 2^{128}n≈2128,恰好对应 128 位安全强度;而 RSA 要达到同样的强度,模数得大到 3072 位。这正是 ECDSA 密钥更短、签名更快的根本原因——安全性来自"求解起来困难的曲线群",而不是"更大的数字"。签名时用 Montgomery ladder 等常数时间算法求 kGkGkG,还能顺带挡掉计时侧信道。
三、代码实战
下面在一条玩具曲线 y2=x3+x+32(mod101)y^2 = x^3 + x + 32 \pmod{101}y2=x3+x+32(mod101)(基点 G=(4,10)G=(4,10)G=(4,10),阶 n=101n=101n=101)上跑通 ECDSA,并复现 nonce 重用攻击:
import hashlib, random
p, a, b, n, G = 101, 1, 32, 101, (4, 10)
def padd(P, Q):
if P is None: return Q
if Q is None: return P
x1, y1 = P; x2, y2 = Q
if x1 == x2 and (y1 + y2) % p == 0: return None
if P == Q:
lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p
else:
lam = (y2 - y1) * pow((x2 - x1) % p, -1, p) % p
x3 = (lam * lam - x1 - x2) % p
return (x3, (lam * (x1 - x3) - y1) % p)
def pmul(k, P):
R = None
while k:
if k & 1: R = padd(R, P)
P = padd(P, P); k >>= 1
return R
def zhash(m): return int(hashlib.sha256(m).hexdigest(), 16) % n or 1
def sign(d, k, msg):
z = zhash(msg); r = pmul(k, G)[0] % n
s = pow(k, -1, n) * (z + r * d) % n
return (r, s)
def verify(Q, msg, sig):
r, s = sig
w = pow(s, -1, n); u1 = zhash(msg) * w % n; u2 = r * w % n
P = padd(pmul(u1, G), pmul(u2, Q))
return P is not None and r == P[0] % n
random.seed(7)
d = random.randint(2, n - 1); Q = pmul(d, G)
print("私钥 d =", d, "公钥 Q =", Q)
sig = sign(d, 42, b"transfer 100 to Bob")
print("签名 (r,s) =", sig, "验证 =", verify(Q, b"transfer 100 to Bob", sig))
sig2 = sign(d, 42, b"transfer 100 to Eve") # 故意复用 k=42
print("第二个签名 =", sig2, "r 相同?", sig[0] == sig2[0])
# 攻击者从两条签名恢复私钥
z1 = zhash(b"transfer 100 to Bob")
z2 = zhash(b"transfer 100 to Eve")
k_rec = (z1 - z2) * pow(sig[1] - sig2[1], -1, n) % n
d_rec = (sig[1] * k_rec - z1) * pow(sig[0], -1, n) % n
print("恢复 k =", k_rec, "恢复私钥 =", d_rec, "正确?", d_rec == d)
运行输出:
私钥 d = 43 公钥 Q = (56, 41)
签名 (r,s) = (86, 1) 验证 = True
第二个签名 = (86, 45) r 相同? True
恢复 k = 42 恢复私钥 = 43 正确? True
签名验证顺利通过,而一旦两条签名复用了同一个 k=42k=42k=42,攻击者只靠这两条签名就在毫秒内反推出了私钥 43——这就是 Sony PS3 漏洞的完整复现。真实场景里 nnn 是 256 位大数,但背后的数学原理一模一样。
四、关键经验/避坑
- nonce 绝不能重复或可预测:kkk 泄漏等于私钥泄漏;生产环境一律用 RFC 6979 确定性 nonce,别用
random.randint选 kkk。 - 拒绝无效曲线点:验签前必须确认公钥真的在曲线上,否则会遭遇 invalid curve 攻击(攻击者用低阶点做 Pohlig-Hellman 反推私钥)。
- 实现要常数时间:用常数时间的算法实现群运算,避免 timing、缓存等侧信道泄漏私钥位。
- 曲线参数别自己造:用 NIST P-256、secp256k1、Curve25519 等标准曲线,它们已避开 anomalous、低嵌入度等高危情形。
- ECDSA vs RSA 按场景选:要短签名、高吞吐(区块链、TLS)选 ECDSA;只有为兼容老系统才继续用 RSA。更现代的 Ed25519 用 Edwards 形式 + 确定性 nonce,基本解决了 ECDSA 的工程痛点。
五、完整系列推荐
📚 本文选自《密码学与零知识证明》100 期系统教程(第 034 期:ECDH 密钥交换与 ECDSA 签名),每期配可运行 Python 代码。
完整系列(100 期正文 + 3 篇番外,每期文章+代码)已在 ima 知识号【Kruptos】持续更新:
- 🗂 69+ 技术知识库:计算机视觉、强化学习、推荐系统、大模型微调、操作系统、AI Agent……几乎覆盖全部软硬件技术栈
- 🧠 8 款 AI 技能:系列生产、知识库管理、CMMI 受管开发、自进化 Agent 等,已在 ima 技能广场上架,即装即用
- ✅ 全部免费订阅,后续更新自动推送
🔍 订阅方式:打开 ima(腾讯智能工作台)→ 搜索「Kruptos」→ 一键订阅;或在 ima 内直接搜索《密码学与零知识证明》。
作者:Kruptos(西电毕业,13 年无线通信/DSP/嵌入式科研,现深耕 AI 与云原生)
原创内容,转载注明出处。

3771

被折叠的 条评论
为什么被折叠?



