-
@ Cryptape
2025-04-28 08:25:21作者:叶万标,Nervos Network CKB 核心开发者
Secp256k1 与 ECDSA 椭圆曲线不仅常见,更是当今区块链数字签名的基础。从比特币(见 Bitcoin Wiki)到以太坊(见 Yellow Paper, Appendix E. Precompiled Contract)再到 CKB (见 RFC 24 相关部分) ,secp256k1 + ECDSA 都是它们的默认选择。它们让你通过私钥证明所有权——你的链上资产属于你,且仅属于你。
椭圆曲线之所以在密码学中有优势,因为它们可以在较小的密钥长度下提供更高的安全性。 但是, 它们真的是安全且完美的算法吗?美国国家标准与技术研究院(NIST)最近认为 secp256k1 存在一些安全风险,已经不建议使用。作为替代,它们建议使用另一条名为 secp256r1 的椭圆曲线(见 Recommendations for Discrete Logarithm-based Cryptography, p1)。另一方面,比特币自身也在改变——在 2021 年引入了一种叫做 Schnorr 的签名算法来尝试替代 ECDSA。
促成这些改变的本质原因是因为 ECDSA 签名算法自身的问题——极其容易受到攻击,并造成过许多灾难性的后果。在本次分享中,我将带领大家回顾历史,并尝试重现这些历史上的著名攻击:随机数重用攻击、心脏点攻击、交易延展攻击、旁路攻击。最后我将指出 CKB 所采用的隔离见证,配合支持加密算法升级的方式——交易哈希不包含 ECDSA 签名,并允许算法升级,在确保安全的同时让用户可以采用更适合自己需求的方案。
本文会使用一些数学符号,约定如下:
|
m
| 消息 | 256 位整数 | | --- | --- | --- | |r
| 签名的一部分 | 256 位整数 | |s
| 签名的一部分 | 256 位整数 | |k
| 随机数字,在签名时需要用到 | 256 位整数 | |g
| 椭圆曲线的生成点,代表坐标 x 和 y | 两个 256 位整数 |随机数重用攻击
因为比特币的原因,secp256k1 椭圆曲线以及 ECDSA 签名算法变得无人不知、无人不晓。但其实在比特币之前,它们也并非无人问津。例如在 Playstation 3 时代,索尼就使用存储在公司总部的私钥将其 Playstation 固件标记为有效且未经修改。Playstation 3 只需要一个公钥来验证签名是否来自索尼。 但不幸的是,索尼因为他们糟糕的代码实现而遭到了黑客的破解,这意味着他们今后发布的任何系统更新都可以毫不费力地解密。
在 fail0overflow 大会上, 黑客展示了索尼 ECDSA 的部分代码, 发现索尼让随机数的值始终保持 4, 这导致了 ECDSA 签名步骤中的随机私钥 k 始终会得到相同的值。ECDSA 签名要求随机数 k 是严格随机的, 如果重复使用 k, 将直接导致私钥泄露.
python get_random_number(): # Chosen by fair dice roll. Guaranteed to be random. return 4
例:有以下信息,求私钥 prikey
-
信息 m₁ 及其签名 (r₁, s₁);
-
信息 m₂ 及其签名 (r₂, s₂);
-
信息 m₁ 和 m₂ 使用相同的随机数 k 进行签名, k 的具体数据则未知。
答:
plaintext s₁ = (m₁ + prikey * r₁) / k s₂ = (m₂ + prikey * r₂) / k = (m₂ + prikey * r₁) / k s₁ / s₂ = (m₁ + prikey * r₁) / (m₂ + prikey * r₁) prikey = (s₁ * m₂ - s₂ * m₁) / (s₂ - s₁) / r₁
这里有一个实际的例子可以帮助大家更直观的理解,如何通过两个使用相同随机数 k 的签名来还原私钥:
```python import pabtc
m1 = pabtc.secp256k1.Fr(0x72a963cdfb01bc37cd283106875ff1f07f02bc9ad6121b75c3d17629df128d4e) r1 = pabtc.secp256k1.Fr(0x741a1cc1db8aa02cff2e695905ed866e4e1f1e19b10e2b448bf01d4ef3cbd8ed) s1 = pabtc.secp256k1.Fr(0x2222017d7d4b9886a19fe8da9234032e5e8dc5b5b1f27517b03ac8e1dd573c78)
m2 = pabtc.secp256k1.Fr(0x059aa1e67abe518ea1e09587f828264119e3cdae0b8fcaedb542d8c287c3d420) r2 = pabtc.secp256k1.Fr(0x741a1cc1db8aa02cff2e695905ed866e4e1f1e19b10e2b448bf01d4ef3cbd8ed) s2 = pabtc.secp256k1.Fr(0x5c907cdd9ac36fdaf4af60e2ccfb1469c7281c30eb219eca3eddf1f0ad804655)
prikey = (s1 * m2 - s2 * m1) / (s2 - s1) / r1 assert prikey.x == 0x5f6717883bef25f45a129c11fcac1567d74bda5a9ad4cbffc8203c0da2a1473c ```
心脏点攻击
心脏点攻击(invalid curve attacks)指攻击者通过生成不在标准曲线上的点,通过这种方式绕过签名验证、密钥生成或者其他基于曲线的操作。
在签名过程中,攻击者可以通过某种方式构造一个无效的公钥。该无效公钥与攻击者的私钥之间存在某种数学关系(例如,攻击者通过伪造一个无效的公钥进行签名),这使得攻击者能够生成一个看似有效的签名。正常情况下,签名验证算法会检查公钥是否在 secp256k1 曲线范围内。如果公钥无效,系统应该拒绝该签名。但是,假设系统没有进行充分的曲线点有效性检查,攻者可能会提交一个包含无效公钥和伪造签名的请求。在某些情况下,系统可能会错误地接受这个无效签名,认为它是合法的。攻击者的签名可能会通过系统的检查,导致恶意的交易或操作被错误地认为是有效的,从而执行某些非法操作,比如转移资金或修改数据。
一个现实中的例子是 OpenSSL 中的椭圆曲线验证漏洞。2015 年,OpenSSL v1.0.2 之前的一个版本存在一个椭圆曲线验证漏洞。攻击者可以通过构造一个无效的椭圆曲线点并将其用作公钥, 利用 OpenSSL 的某些漏洞绕过验证,进而攻击使用该库的系统。这个漏洞被称为 CVE-2015-1786, 它允许攻击者通过伪造无效的公钥来绕过签名验证。同样的问题也曾发生在 Bitocin Core 使用的 ECDSA 库中,早期版本的库没有对椭圆曲线点进行足够的检查。
在这个漏洞被修复之前,攻击者可以在不进行正确验证的情况下,绕过系统对曲线有效性的检查, 从而导致可能的拒绝服务或其他安全问题。
交易延展性攻击
在古代,如果我们把一枚金币敲变形之后,虽然形状有所改变,但质量却没有发生变化,在市场交易中它仍然会被认可为一枚金币,甚至您将金币敲成金块,它依然会被认可,这种特性呢被称为“延展性”或“可锻性”。
Mt. Gox(门头沟)一度是世界上最大的比特币交易所。该公司总部位于东京,估计 2013 年占比特币交易量的 70%。2014 年,门头沟交易所被黑客攻击,造成了约 85 万枚比特币的损失。在门头沟事件中,黑客所采用的是一种名为交易延展性攻击(transaction malleability attack)的手法。
此次攻击的具体过程如下:攻击者首先在门头沟发起一笔提现交易 a, 接着在交易 a 被确认之前通过篡改交易签名,使得标识一笔交易唯一性的交易哈希发生改变,生成伪造的交易 b。之后,交易 b 被区块链确认,而交易所则收到了交易 a 失败的信息。交易所误认为提现失败从而重新为攻击者构造一笔新的提现交易。
要使得攻击成立,其核心是攻击者能够修改交易的签名部分(如输入的签名)或者其他非关键的字段, 从而改变交易的哈希值,但不会改变交易的实际内容.
巧合的是,secp256k1 + ECDSA 确实存在一种十分便捷的方式,使得攻击者可以修改签名结果的同时仍然能通过签名验证。如果我们分析 ECDSA 验签算法,会发现验签结果和签名(r, s)中的 s 值的符号是无关的。
为了验证这一点,我们编写如下测试代码:
```python import pabtc prikey = pabtc.secp256k1.Fr(1) pubkey = pabtc.secp256k1.G * prikey msg = pabtc.secp256k1.Fr(0x72a963cdfb01bc37cd283106875ff1f07f02bc9ad6121b75c3d17629df128d4e)
r, s, _ = pabtc.ECDSA.sign(prikey, msg) assert pabtc.ECDSA.verify(pubkey, msg, r, +s) assert pabtc.ECDSA.verify(pubkey, msg, r, -s) ``` 在上述代码中,我们使用私钥对一条消息进行了签名,然后对签名中的 s 值取负号,发现修改后的签名依然能通过 ECDSA 验证。
比特币在早期版本中存在这种攻击的风险,攻击者通过延展性攻击破坏了交易的不可篡改性,导致了严重的安全问题。为了解决这一问题,比特币在 Segregated Witness (SegWit) 升级中做了改进,SegWit 将交易的签名部分与其他数据分开存储,使得即使攻击者篡改签名部分,交易哈希也不再受影响,从而解决了交易延展性问题。
这个问题在其他区块链系统中也有类似的影响,因此许多项目都采取了类似 SegWit 的解决方案,来确保交易的完整性和可追溯性。另一种解决方案是以太坊所采取的,以太坊对签名中的 s 做了额外的要求,要求 s 必须小于
pabtc.secp256k1.N / 2
。您可以在 Ethereum Yellow paper 的 Appendix F. Signing Transactions (p. 26) 部分找到以太坊针对交易延展性攻击的详细解决方式。有诗云:
门头交易所,用户真是多,
比特币被盗,大伙冷汗冒,
黑客改哈希,交易无踪兆,
冷钱包空空,财富随风飘。旁路攻击
我坐飞机旁边有个大哥一直在看股票,我俩聊了几句股票。他说今年行情不好,让我猜他亏了多少钱。
我说:“也就十来万吧。”
大哥一愣,问我:“你咋猜的呢?”
我说:“虽然你穿着衬衫西裤,看着很商务,但是却背了个瑞士军刀牌双肩包,大老板有背这个的么?一看你就是个跑业务的。再看你戴了块阿玛尼这种杂牌子手表,三十多岁的人了,连个劳力士都没混上,说明收入很一般。你的衬衣是旧的,但是熨得很板正,领子也干净,这都是你老婆给你收拾的。你包上有个 HelloKitty 小挂件,这应该是你女儿给你挂的。你自选股里都是一些 5G 移动芯片之类的股票,你觉得自己很懂,你应该是互联网企业上班的。方方面面综合下来,你的可支配资金也就 20-30 万,结合今年的行情,亏损 10 万左右。再看看你这个黑眼圈和与年龄不成比例的稀疏发型,压力不小。你老婆应该还不知道你股票亏了这么多钱。刚才看到你手机界面上还有炒虚拟币的软件,在最后一位,说明是最近刚刚下载的。如果你股票再亏,你就打算去炒虚拟币放手一搏,但是你只会亏得更惨。说完我点了下他手机炒股软件界面,上面显示总投入 28 万,当前亏损 10.2 万。”
大哥沉默了,一路上再也没跟我说一句话,只是偶尔低头用食指关节揉一揉微微发红的的眼眶,飞机餐的盒饭打开了,但是没吃。
上述故事来自中国互联网, 最早出现在 2015 年,由于被转载太多次,因此作者实在不明。在这个故事里,“我”就对大哥发动了一次旁路攻击。大哥虽然没有向我透露任何关于自身的投资信息,但是由于大哥的资产收益会影响大哥的穿着,因此我们可以通过大哥的穿着来反向推断大哥的资产收益。
在密码学中,所谓的旁路攻击(side-channel attacks),就是一种利用设备执行任务时产生的物理或行为信息(如执行时间、用电模式、电磁辐射等)来破解密码或签名方案的方法。对于 secp256k1 椭圆曲线和 ECDSA 签名方案,这种攻击可能通过分析关键运算的执行特性来推断私钥。
在 ECDSA 中,签名过程涉及生成一个随机数 k,然后用它来计算签名的一部分。这个随机数的安全性至关重要,如果 k 被泄漏,攻击者就能通过它恢复私钥.
例:有以下信息,请计算 secp256k1 的私钥:
-
消息
m = 0x72a963cdfb01bc37cd283106875ff1f07f02bc9ad6121b75c3d17629df128d4e
-
随机数字
k = 0x1058387903e128125f2715d7de954f53686172b78c3f919521ae4664f30b00ca
-
签名
-
r = 0x75ee776c554b1dd5e1680a4cc9a3d0e8cb11400742d8af0222ce383e642f98db
-
s = 0x35fd48c9157256558184e20c9392ff3c9517f9753e3745aede06cab285f4bc0d
答:根据 ECDSA 签名算法,容易得到私钥计算公式为
prikey = (s * k - m) / r
, 代入数字计算,得到私钥为 1。验证代码如下:
```python import pabtc
m = pabtc.secp256k1.Fr(0x72a963cdfb01bc37cd283106875ff1f07f02bc9ad6121b75c3d17629df128d4e) k = pabtc.secp256k1.Fr(0x1058387903e128125f2715d7de954f53686172b78c3f919521ae4664f30b00ca) r = pabtc.secp256k1.Fr(0x75ee776c554b1dd5e1680a4cc9a3d0e8cb11400742d8af0222ce383e642f98db) s = pabtc.secp256k1.Fr(0x35fd48c9157256558184e20c9392ff3c9517f9753e3745aede06cab285f4bc0d)
prikey = (s * k - m) / r assert prikey == pabtc.secp256k1.Fr(1) ```
随机数字 k 的计算涉及到椭圆曲线点乘和逆元操作(通常通过扩展欧几里得算法实现)。这些操作的时间可能会与 k 相关,旁路攻击者可以测量执行时间差异来提取 k。为了揭示原理,我将尝试把攻击过程简化。
例:有未知随机数字 k,现在黑客通过某种手段可探测出
g * k
的执行时间,请尝试是否可以得到随机数字 k 的一些信息。答:观察椭圆曲线上的点的乘法算法,得出当 k 的比特位不同时,会执行不同的操作。当比特位为 0 时,其计算量小于比特位为 1 时。我们事先取两个不同的 k 值,一个大多数位为 0,另一个大多数位位 1,计算它们的执行时间之差。当有新的未知 k 进行计算时,探测得到它的执行时间,与前两个值进行比对,可大致得到未知 k 其比特位为 1 的数量。实验代码如下。注意,为了简化攻击步骤,在实验代码中我们假设所有参与计算的 k 的第一个比特位始终为 1。
```python import pabtc import random import timeit
k_one = pabtc.secp256k1.Fr(0x8000000000000000000000000000000000000000000000000000000000000000) # Has one '1' bits k_255 = pabtc.secp256k1.Fr(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) # Has 255 '1' bits k_unknown = pabtc.secp256k1.Fr(random.randint(0, pabtc.secp256k1.N - 1) | k_one.x) # The unknown k
a = timeit.timeit(lambda: pabtc.secp256k1.G * k_one, number=1024) b = timeit.timeit(lambda: pabtc.secp256k1.G * k_255, number=1024) c = timeit.timeit(lambda: pabtc.secp256k1.G * k_unknown, number=1024)
d = (c - a) / ((b - a) / 254) print(d) ```
上述攻击过程是旁路攻击中的时间攻击(timing attacks),如果要对该攻击做防护,可以通过在代码中引入常量时间操作(constant-time operations)来避免泄露信息。例如,使用固定时间的加法和乘法,防止时间差异被利用。
在实际应用中,为了避免密码学算法中的旁路攻击,需要在算法、硬件和软件层面做出多方面的安全优化。不过由于 secp256k1 与 ECDSA 方案在设计时未充分考虑该攻击方式,因此防护此类攻击非常困难且复杂。
CKB 的方式:隔离见证 + 加密算法可升级
在交易结构上,CKB 采用了比特币的隔离见证方案,也就是交易哈希不会包含 ECDSA 签名。这种设计可以防止交易延展性攻击。详情见 RFC 的 Transaction Hash 部分。
Secp256k1 + ECDSA 方案是 CKB 的默认签名方案,但得益于 CKB 的自定义密码学原语和原生账户抽象能力,在 CKB 上可以非常容易地实现更多其它加密算法和升级现有算法。CKB 上的开发者可以选择任意信任算法来保护资产,包括但不限于:
-
RSA, Ed25519 and more
结语
总之,虽然 secp256k1 和 ECDSA 在许多应用中广泛使用,并且它们在合理的实现和正确的使用下是相当安全的,但也不能忽视它们存在的一些潜在漏洞。得益于比特币的发展,secp256k1 和 ECDSA 名声大噪的同时也吸引了更多的密码学家和不怀好意的黑客们。未来,更多关于 sepc256k1 的一些攻击方式可能会逐步被发现并利用。因此,保持警惕,及时更新以及遵循最新的安全最佳实践对于确保系统安全至关重要。随着加密领域的不断进步,目前已经有一些更加安全且高效的替代方案出现。但无论如何,理解并应对当前的风险,仍然是我们每一个开发者的责任和挑战。
🧑💻 关于作者
叶万标是 CKB-VM 的核心开发者之一,专注于提升虚拟机的性能与能力。他一直在探索指令集设计与宏指令融合等方向,以使系统更加高效且灵活。
他的文章和演讲包括:
-