教程区块链区块链技术ch022.5 数字签名与验签:ECDSA 完整流程与 Sony PS3 事件

本页目录

2.4 节实现了椭圆曲线上的标量乘法 P=d×GP = d \times G。本节在此基础上,完成数字签名的完整密码学协议——ECDSA(Elliptic Curve Digital Signature Algorithm),并深入剖析2010 年 Sony PlayStation 3 私钥泄露事件,理解为什么"随机数 kk 的唯一性"比任何算法细节都更重要。

2.5.1 ECDSA 签名的数学流程

输入

  • 私钥 dd(1 ≤ dd < nn
  • 消息 mm

签名过程

  1. 计算消息哈希:e=H(m)e = H(m)。比特币使用 double-SHA256:e=SHA256(SHA256(m))e = \text{SHA256}(\text{SHA256}(m))
  2. 生成密码学安全随机数 kk(1 ≤ kk < nn)。kk 必须每次签名都不同。
  3. 计算椭圆曲线点:R=k×GR = k \times G
  4. RRxx 坐标:r=x(R)modnr = x(R) \mod n。若 r=0r = 0,重新选择 kk
  5. 计算:
s=k1(e+rd)modns = k^{-1} \cdot (e + r \cdot d) \mod n

s=0s = 0,重新选择 kk

  1. 输出签名(r,s)(r, s)。在比特币中,签名通常使用 DER 编码(约 71–72 字节)。

签名验证流程

输入:公钥 P=d×GP = d \times G,消息 mm,签名 (r,s)(r, s)

  1. 验证 r,s[1,n1]r, s \in [1, n-1]
  2. 计算消息哈希(同样的哈希函数):e=H(m)e = H(m)
  3. 计算:
u1=es1modn,ν2=rs1modnu_1 = e \cdot s^{-1} \mod n, \quad \nu_2 = r \cdot s^{-1} \mod n
  1. 计算椭圆曲线点:R=ν1×G+ν2×PR' = \nu_1 \times G + \nu_2 \times P
  2. 验证通过当且仅当 x(R)modn=rx(R') \mod n = r

为什么验签等式成立?

这是 ECDSA 的数学核心:

ν1G+ν2P=(es1)G+(rs1)P=(es1+rds1)G=s1(e+rd)G\nu_1 G + \nu_2 P = (es^{-1})G + (rs^{-1})P = (es^{-1} + rd s^{-1})G = s^{-1}(e + rd)G

代入 s=k1(e+rd)s = k^{-1}(e + rd)

s1(e+rd)G=(k1(e+rd))1(e+rd)G=k(e+rd)1(e+rd)G=kG=Rs^{-1}(e + rd)G = (k^{-1}(e + rd))^{-1}(e + rd)G = k(e + rd)^{-1}(e + rd)G = kG = R

等式要求 x(R)x(R)(modn)x(R') \equiv x(R) \pmod{n},这正是 rr 的定义。所以验签等式等价于"这个签名只能由掌握 dd(即 P=dGP = dG 的私钥持有者)的人"才能产生。

2.5.2 完整 TypeScript 实现

typescript
// secp256k1 参数(复用 2.4 节定义)
const { p, a, b, G, n } = SECP256K1;

/**
 * 简化的 SHA-256 哈希(复用 2.2 节实现)
 * 真实中应对消息先标准化
 */
function sha256Msg(msg: string): bigint {
  const hash = sha256(msg); // 复用 2.2 节完整实现
  return BigInt('0x' + hash);
}

/**
 * CSPRNG 模拟:生成 [1, n-1] 范围内的密码学安全随机数
 * 真实实现应使用 crypto.getRandomValues 并拒绝偏置值
 */
function randomK(): bigint {
  const buf = new Uint8Array(32);
  for (let i = 0; i < 32; i++) buf[i] = Math.floor(Math.random() * 256);
  let k = 0n;
  for (let i = 0; i < 32; i++) {
    k = (k << 8n) | BigInt(buf[i]);
  }
  k = (k % (n - 1n)) + 1n; // 1..n-1
  return k;
}

/**
 * ECDSA 签名
 * @param d 私钥
 * @param m 消息
 * @returns 签名 (r, s)
 */
function ecdsaSign(d: bigint, m: string): { r: bigint; s: bigint } {
  const e = sha256Msg(m) % n;
  let k: bigint, R: ECPoint, r: bigint, s: bigint;
  
  do {
    k = randomK();
    R = scalarMultiply(k, new ECPoint(G.x, G.y), a, p);
    r = R.x! % n;
  } while (r === 0n);
  
  const kInv = modInverse(k, n);
  s = (kInv * (e + r * d)) % n;
  
  // 低 s 值规范(BIP-62):如果 s > n/2,使用 n-s 以压缩签名空间
  if (s > n / 2n) {
    s = n - s;
  }
  
  if (s === 0n) throw new Error("s is zero, retry");
  
  return { r, s };
}

/**
 * ECDSA 验证
 * @param P 公钥
 * @param m 消息
 * @param sig 签名 (r, s)
 * @returns 是否有效
 */
function ecdsaVerify(P: ECPoint, m: string, sig: { r: bigint; s: bigint }): boolean {
  const { r, s } = sig;
  if (r <= 0n || r >= n || s <= 0n || s >= n) return false;
  
  const e = sha256Msg(m) % n;
  const sInv = modInverse(s, n);
  const u1 = (e * sInv) % n;
  const u2 = (r * sInv) % n;
  
  const Gp = new ECPoint(G.x, G.y);
  const u1G = scalarMultiply(u1, Gp, a, p);
  const u2P = scalarMultiply(u2, new ECPoint(P.x, P.y), a, p);
  const R = ecAdd(u1G, u2P, a, p);
  
  if (R.isInfinity()) return false;
  return (R.x! % n) === r;
}

// --- 完整流程验证 ---
console.log("=== ECDSA 签名验证完整演示 ===");

// 1. 生成密钥对
const d = 0xdeadbeef0123456789abcdef0123456789abcdef0123456789abcdef012345n; // 私钥
const pubKey = scalarMultiply(d, new ECPoint(G.x, G.y), a, p);
console.log(`私钥 d: 0x${d.toString(16).slice(0, 20)}...`);
console.log(`公钥 P: ${pubKey.toString()}`);

// 2. 签名
const message = "Transfer 1.0 BTC to Alice";
const sig = ecdsaSign(d, message);
console.log(`\n消息: "${message}"`);
console.log(`签名: (r=0xsig.r.toString(16).slice(0,16)...,s=0x{sig.r.toString(16).slice(0, 16)}..., s=0x{sig.s.toString(16).slice(0, 16)}...)`);

// 3. 验证(通过)
const valid = ecdsaVerify(pubKey, message, sig);
console.log(`\n✅ 原始消息验签: ${valid}`);

// 4. 篡改后验证(应失败)
const tamperedMsg = "Transfer 100.0 BTC to Mallory";
const tamperedValid = ecdsaVerify(pubKey, tamperedMsg, sig);
console.log(`❌ 篡改消息验签: ${tamperedValid} (应为 false)`);

// 5. 用错误公钥验证(应失败)
const wrongKey = scalarMultiply(0x1234n, new ECPoint(G.x, G.y), a, p);
const wrongKeyValid = ecdsaVerify(wrongKey, message, sig);
console.log(`❌ 错误公钥验签: ${wrongKeyValid} (应为 false)`);

2.5.3 Sony PS3 事件:随机数 kk 的致命秘密

事件背景

2010 年,黑客组织 fail0overflow 在著名的 CCC 黑客大会上展示了如何破解 Sony PlayStation 3 的安全系统。他们的核心发现令全场哗然:

Sony 在 PS3 固件的 ECDSA 实现中,对每一条消息使用了固定的 kk 值。

攻击数学:为什么固定 kk = 私钥泄露

假设用同一个 kk 对两条不同消息 m1,m2m_1, m_2 签名:

s1=k1(e1+rd)modns2=k1(e2+rd)modns_1 = k^{-1}(e_1 + r d) \mod n\\ s_2 = k^{-1}(e_2 + r d) \mod n

注意 rr 也相同(因为 r=x(kG)r = x(kG)kk 相同 → RR 相同 → rr 相同)。

攻击者获得 (r,s1),(r,s2)(r, s_1), (r, s_2) 和公共的 e1=H(m1),e2=H(m2)e_1 = H(m_1), e_2 = H(m_2)

s1s2=k1(e1e2)modns_1 - s_2 = k^{-1}(e_1 - e_2) \mod n
k=(e1e2)(s1s2)1modn\Rightarrow k = (e_1 - e_2) \cdot (s_1 - s_2)^{-1} \mod n

一旦得到 kk,私钥立即暴露:

d=(s1ke1)r1modnd = (s_1 k - e_1) \cdot r^{-1} \mod n

只需要两个使用相同 kk 的签名,私钥就被完全破解。

历史后果

  • 2010 年 12 月:fail0overflow 发布演示,展示从固定 kk 恢复出 Sony 的主私钥
  • 这意味着任何人都可以用 Sony 的私钥"签名"自定义固件,让 PS3 认为它是官方授权的。
  • Sony 紧急起诉多名黑客(包括 George Hotz / "GeoHot")。
  • 但私钥已无法挽回——数学保证一旦泄露,没有任何技术方法可以"撤销"一个已公开的公钥对应私钥
typescript
/**
 * Sony PS3 攻击演示:从两个使用相同 k 的签名中恢复私钥
 */
function sonyPs3Attack(
  m1: string,
  sig1: { r: bigint; s: bigint },
  m2: string,
  sig2: { r: bigint; s: bigint },
): bigint {
  const e1 = sha256Msg(m1) % n;
  const e2 = sha256Msg(m2) % n;
  const { r, s: s1 } = sig1;
  const { s: s2 } = sig2;
  
  // 步骤 1: 恢复 k
  const k = ((e1 - e2) * modInverse((s1 - s2), n)) % n;
  if (k < 0n) k += n;
  console.log(`攻击恢复 k: 0x${k.toString(16).slice(0, 16)}...`);
  
  // 步骤 2: 恢复 d
  let d = ((s1 * k - e1) * modInverse(r, n)) % n;
  if (d < 0n) d += n;
  return d;
}

// --- 模拟 Sony 的错误 ---
const fixedK = 0xbadc0de123456789abcdef0n; // Sony 使用的"固定随机数"
console.log("\n=== Sony PS3 攻击模拟 ===");
console.log(`Sony 错误: 固定使用 k = 0x${fixedK.toString(16).slice(0, 16)}...`);

// 用固定 k 签两条消息(模拟 Sony 的固件签名)
const msg1 = "Firmware_v3.41_signed_by_Sony";
const msg2 = "Custom_firmware_authorized"; // 攻击者控制的消息

// 签名 1: 使用"错误"的固定 k
const e1 = sha256Msg(msg1) % n;
const R1 = scalarMultiply(fixedK, new ECPoint(G.x, G.y), a, p);
const r = R1.x! % n; // r 相同!
const kInv = modInverse(fixedK, n);
const s1 = (kInv * (e1 + r * d)) % n;

// 签名 2: 攻击者可以构造任意消息
const e2 = sha256Msg(msg2) % n;
const s2 = (kInv * (e2 + r * d)) % n;

console.log(`签名1: (r, s1)`);
console.log(`签名2: (r, s2)`);
console.log(`注意: r 相同(因为 k 相同),这是致命线索!`);

// 攻击者执行攻击
const recoveredD = sonyPs3Attack(msg1, { r, s: s1 }, msg2, { r, s: s2 });
console.log(`\n🔓 恢复出的私钥: 0x${recoveredD.toString(16).slice(0, 20)}...`);
console.log(`原始私钥:    0x${d.toString(16).slice(0, 20)}...`);
console.log(`恢复正确?   ${recoveredD === d}`);
console.log(`\n💀 一旦两个签名使用相同 k,私钥不可逆转地泄露!`);

如何正确生成 kk

方案一:真随机(CSPRNG)

每次签名时从 crypto.getRandomValues 生成 256 位均匀随机数。风险:随机源可能污染/可预测(某些 IoT 设备的 /dev/urandom 可能熵不足)。

方案二:确定性 kk(RFC 6979)—— 推荐

用私钥 dd 和消息哈希 ee 作为输入,通过 HMAC-SHA256 计算确定性 kk

k=HMAC-SHA256(key=d,data=e0x00 (padding))k = \text{HMAC\text{-}SHA256}(\text{key}=d, \text{data}=e \parallel 0x00 \text{ (padding)})

优点

  • 对同一 (d,m)(d, m) 总是产生相同 kk,保证签名可复现。
  • 不依赖外部随机源,避免熵不足问题。
  • 不同消息产生不同的 kk,完全免疫"固定 kk"攻击。

比特币的 libsecp256k1 默认使用 RFC 6979 确定性签名。

2.5.4 ECDSA vs Schnorr 签名

比特币在 2021 年的 Taproot 升级中引入了 Schnorr 签名(BIP-340)。以下是两者对比:

特性ECDSASchnorr
签名大小~71 字节(DER 编码)64 字节(r, s 各 32 字节)
数学复杂度中等(需取模逆元)简化(基于线性点加)
可聚合性❌(签名不能数学合并)✅(MuSig: k=k1+k2,s=s1+s2k = k_1 + k_2, s = s_1 + s_2,得到联合签名)
线性验证✅(可批量验证多个签名)
公钥恢复✅(从签名恢复公钥,节省存储)❌(BIP-340 中必须显式携带公钥)
标准安全性证明较复杂基于标准离散对数假设的简洁证明

Schnorr 签名的核心优势——线性

σagg=(R1+R2,s1+s2)=(Ragg,sagg)\sigma_{agg} = (R_1 + R_2, s_1 + s_2) = (R_{agg}, s_{agg})

两个参与方可以各自独立计算自己的 RiR_isis_i,然后简单相加得到一个"组合签名",验证方只需对组合公钥验证一次。这对多签钱包和闪电网络有巨大价值。

graph TD
    P1[参与方1: s1, R1] -->|s=s1+s2<br/>R=R1+R2| Agg[聚合签名<br/>64 字节]
    P2[参与方2: s2, R2] -->|协同计算| Agg
    P3[验证方] -->|单次验签<br/>验证 P_agg| Agg

关键区别:ECDSA 验签中需要计算 s1s^{-1},这使得签名之间不能线性组合。Schnorr 的设计避免了取逆,保留了线性结构。

核心认知

  1. ECDSA 签名 = 两个数 (r,s)(r, s)rr 是随机点 R=kGR = kGxx 坐标,ss 将消息哈希 eerr 和私钥 dd 绑定在一起。验证等式的本质是用公钥"解开"这个绑定。
  1. kk 是签名的灵魂。 不是算法选择也不是性能参数——kk 的一次重复 = 私钥泄露。 Sony 事件以数十亿美元的代价证明了这一点。
  1. 确定性 kk(RFC 6979)比纯随机更安全。 在密码学史上,随机源失败导致的漏洞(Debian OpenSSL 2008、Sony PS3 2010)远比确定性算法多。用 HMAC 从 (d,m)(d, m) 派生 kk 是最佳实践。
  1. Schnorr 是 ECDSA 的优雅进化。 更强的数学性质(线性可聚合、可批量验证、更简洁的形式化安全证明)使其成为比特币 Taproot 升级的基础。未来多签和跨链协议将大量使用 Schnorr/MuSig。

下一预告:2.6 节将走出纯数学,进入实际应用——钱包的密钥管理。从密码学安全随机数生成 128–256 位熵,到 BIP-39 助记词,再到 BIP-32/BIP-44 的层级确定性钱包,以及冷存储和硬件钱包的安全实践。

评论

0

评论加载中…

发表评论

0/2000