2.4 节实现了椭圆曲线上的标量乘法 。本节在此基础上,完成数字签名的完整密码学协议——ECDSA(Elliptic Curve Digital Signature Algorithm),并深入剖析2010 年 Sony PlayStation 3 私钥泄露事件,理解为什么"随机数 的唯一性"比任何算法细节都更重要。
2.5.1 ECDSA 签名的数学流程
输入
- 私钥 (1 ≤ < )
- 消息
签名过程
- 计算消息哈希:。比特币使用 double-SHA256:。
- 生成密码学安全随机数 (1 ≤ < )。 必须每次签名都不同。
- 计算椭圆曲线点:。
- 取 的 坐标:。若 ,重新选择 。
- 计算:
若 ,重新选择 。
- 输出签名:。在比特币中,签名通常使用 DER 编码(约 71–72 字节)。
签名验证流程
输入:公钥 ,消息 ,签名 。
- 验证 。
- 计算消息哈希(同样的哈希函数):。
- 计算:
- 计算椭圆曲线点:。
- 验证通过当且仅当 。
为什么验签等式成立?
这是 ECDSA 的数学核心:
代入 :
等式要求 ,这正是 的定义。所以验签等式等价于"这个签名只能由掌握 (即 的私钥持有者)的人"才能产生。
2.5.2 完整 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=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 事件:随机数 的致命秘密
事件背景
2010 年,黑客组织 fail0overflow 在著名的 CCC 黑客大会上展示了如何破解 Sony PlayStation 3 的安全系统。他们的核心发现令全场哗然:
Sony 在 PS3 固件的 ECDSA 实现中,对每一条消息使用了固定的 值。
攻击数学:为什么固定 = 私钥泄露
假设用同一个 对两条不同消息 签名:
注意 也相同(因为 , 相同 → 相同 → 相同)。
攻击者获得 和公共的 :
一旦得到 ,私钥立即暴露:
只需要两个使用相同 的签名,私钥就被完全破解。
历史后果
- 2010 年 12 月:fail0overflow 发布演示,展示从固定 恢复出 Sony 的主私钥。
- 这意味着任何人都可以用 Sony 的私钥"签名"自定义固件,让 PS3 认为它是官方授权的。
- Sony 紧急起诉多名黑客(包括 George Hotz / "GeoHot")。
- 但私钥已无法挽回——数学保证一旦泄露,没有任何技术方法可以"撤销"一个已公开的公钥对应私钥。
/**
* 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,私钥不可逆转地泄露!`);如何正确生成 ?
方案一:真随机(CSPRNG)
每次签名时从 crypto.getRandomValues 生成 256 位均匀随机数。风险:随机源可能污染/可预测(某些 IoT 设备的 /dev/urandom 可能熵不足)。
方案二:确定性 (RFC 6979)—— 推荐
用私钥 和消息哈希 作为输入,通过 HMAC-SHA256 计算确定性 :
优点:
- 对同一 总是产生相同 ,保证签名可复现。
- 不依赖外部随机源,避免熵不足问题。
- 不同消息产生不同的 ,完全免疫"固定 "攻击。
比特币的 libsecp256k1 默认使用 RFC 6979 确定性签名。
2.5.4 ECDSA vs Schnorr 签名
比特币在 2021 年的 Taproot 升级中引入了 Schnorr 签名(BIP-340)。以下是两者对比:
| 特性 | ECDSA | Schnorr |
|---|---|---|
| 签名大小 | ~71 字节(DER 编码) | 64 字节(r, s 各 32 字节) |
| 数学复杂度 | 中等(需取模逆元) | 简化(基于线性点加) |
| 可聚合性 | ❌(签名不能数学合并) | ✅(MuSig: ,得到联合签名) |
| 线性验证 | ❌ | ✅(可批量验证多个签名) |
| 公钥恢复 | ✅(从签名恢复公钥,节省存储) | ❌(BIP-340 中必须显式携带公钥) |
| 标准安全性证明 | 较复杂 | 基于标准离散对数假设的简洁证明 |
Schnorr 签名的核心优势——线性:
两个参与方可以各自独立计算自己的 和 ,然后简单相加得到一个"组合签名",验证方只需对组合公钥验证一次。这对多签钱包和闪电网络有巨大价值。
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 验签中需要计算 ,这使得签名之间不能线性组合。Schnorr 的设计避免了取逆,保留了线性结构。
核心认知
- ECDSA 签名 = 两个数 。 是随机点 的 坐标, 将消息哈希 、 和私钥 绑定在一起。验证等式的本质是用公钥"解开"这个绑定。
- 是签名的灵魂。 不是算法选择也不是性能参数—— 的一次重复 = 私钥泄露。 Sony 事件以数十亿美元的代价证明了这一点。
- 确定性 (RFC 6979)比纯随机更安全。 在密码学史上,随机源失败导致的漏洞(Debian OpenSSL 2008、Sony PS3 2010)远比确定性算法多。用 HMAC 从 派生 是最佳实践。
- Schnorr 是 ECDSA 的优雅进化。 更强的数学性质(线性可聚合、可批量验证、更简洁的形式化安全证明)使其成为比特币 Taproot 升级的基础。未来多签和跨链协议将大量使用 Schnorr/MuSig。
下一预告:2.6 节将走出纯数学,进入实际应用——钱包的密钥管理。从密码学安全随机数生成 128–256 位熵,到 BIP-39 助记词,再到 BIP-32/BIP-44 的层级确定性钱包,以及冷存储和硬件钱包的安全实践。
评论
0评论加载中…