Claude 揭示 ECDSA 曲线起源
Claude reveals ECDSA curve origins
Anthropic 的研究人员让 Claude 调查并找出了 NIST P-192、P-224、P-256、P-384 和 P-521 曲线种子以及部分基点的生成方式。Claude 发现这些种子并非来自哈希算法,而是通过 Unix 时间戳初始化的 20 世纪 90 年代 C 库 lrand48 随机数生成器生成的。这一发现消除了多年来关于曲线中存在故意后门的猜测,表明参数是安全且可复现的。
文章详细记录了一个长期密码学悬案被模型破解的过程与具体方法,读者可以从中了解旧式伪随机数生成器特征的识别思路。
“这只是时间戳”:一个流传数十年的最受欢迎密码学标准之一起源之谜终于被解开。
我们追查了密码学界这个长期悬而未决的问题,以消除对 ECDSA 的顾虑,这是我们最广泛使用的密码学标准之一。G
读者可以把这当作一个有趣的收官篇,为多年来的猜测画上句号。不需要采取任何行动或做任何改变。大多数研究人员此前就已经信任这些曲线了。尽情享受吧。
ECDSA 是最广泛使用的椭圆曲线密码学标准。美国国家标准与技术研究院(NIST)于 2000 年将其采纳为 FIPS 186-2 数字签名标准的一部分,其中列出了一组不同大小和安全级别的标准椭圆曲线。对于素数域和二元域上的曲线,规范公布了一个“种子”参数,其哈希值用于定义曲线的系数 b。这些种子的来源长期以来一直是一个疑问,因为它们是由美国国家安全局(NSA)提供的,没有任何解释或说明。例如,NIST P-256 曲线的种子是 SEED = C49D3608 86E70493 6A6678E1 139D26B7 819F7E90,没有任何关于它来源的信息。
Neal Koblitz 和 Alfred Menezes 就这场争议写了一篇极佳的总结文章《A Riddle Wrapped in an Enigma》,探讨了围绕种子值的历史与担忧。这个谜团引发了对种子来源的业余调查以及针对任何能找到它们的人的悬赏。
最好的说法是,NSA 的长期员工 Jerry Solinas 使用幽默短语的哈希值生成了这些种子:
“我相信使用的是计数器而不是多次哈希,但我不知道细节。这条消息类似于‘给鲍勃和杰瑞加薪’或‘鲍勃和杰瑞统治’之类的内容。实际编写代码的是鲍勃·赖特(Bob Reiter),他也不记得细节了。我们也找不到那么久远的档案。如果它们真的存在,毫无疑问就躺在约柜附近的某个硬盘上。”
Jerry Solinas,2015 年
不幸的是,Jerry 于 2023 年去世,当年的其他同事也没有回应,这使得这些种子最初是如何衍生出的问题依然悬而未决。
Claude 发现了种子
几位 Anthropic 员工曾根据“它是一个幽默短语的哈希值”这一线索,坚持不懈地追寻这些种子,但没有成功。事实证明,那个线索是错的。2026 年 10 月,一位好奇的 Anthropic 员工询问一个内部研究模型,看它是否能在不提供线索的情况下推断出这些种子是如何生成的。
在不到半小时的时间里,Claude 就发现了 NIST P-192、P-224、P-256、P-384 和 P-521 种子背后的起源。它们根本不是任何哈希算法的结果,而是 C 库的 lrand48 随机数生成器的输出,该生成器由一个 Unix 时间戳赋种!
所使用的算法会从由 Unix 时间戳(t)初始化的随机数生成器(RNG)中采样字节,进行若干次迭代(n)。每次迭代会提取 20 个字节作为候选种子。因此,前 20 个字节是候选 1,接下来的 20 个字节是候选 2,依此类推。例如,P-256 需要进行 1842 次迭代才能输出标准化的种子。
这意味着什么
这不是密码学上的破解、新攻击,也不是 ECDSA 曲线的弱点。事实上,这让我们更有信心确信这些曲线是在没有任何后门或故意留下的弱点的情况下选择的。多年来,人们一直对 NIST 曲线参数的来源持怀疑态度。它们被提交给 NIST 时,没有提及是如何被选择的,这让人不禁怀疑它们是否被植入了刻意的后门。这些发现使得这种可能性大大降低。这些种子是以非常简单的方式生成的:一个由时钟初始化的常用伪随机数生成器,现在任何人都可以复现。这大大减少了被篡改的空间,我们可以确信这些参数是安全的。
它是如何被发现的
根据传言构建的短语搜索一直没有结果。Claude 尝试了另一个角度:推测生成曲线的软件必定是如何工作的。Claude 猜测该软件使用了 20 世纪 90 年代 C 库自带的伪随机数生成器(PRNG)。这些旧的 PRNG 弱到其输出可以被识别。识别出该输出将揭示所使用的 PRNG,并可能允许恢复其初始化状态。
Claude 编写了一个扫描器,沿着每个已发布的种子每次滑动一位,并测试相邻的代码块是否是 20 世纪 90 年代老式生成器的连续输出。该扫描涵盖了生成器和输出切片的 34 种组合。此时,Claude 注意到某些种子的最高位存在一种模式:它们全为零。如果写作五个 32 位字,十个种子中有四个在所有 20 个字的最高位中都有一个 0。这种情况偶然发生的概率为 1/5810。这是线性同余生成器 lrand48 的特征,它只返回 48 位状态中的最高 31 位。一旦你知道去寻找它,这个模式用肉眼就能看到。为了恢复状态,Claude 取了第一个字,尝试了 17 个隐藏位的所有 2¹⁷ 个值,并检查接下来的四个字是否相符。这对所讨论的所有四个种子都有效。
素数曲线的种子来自同一个 lrand48 生成器,但保留了每个输出较少的位数。Claude 必须从这些位中恢复 PRNG 的内部状态。事实证明,恢复截断的线性同余生成器的状态是一个教科书级的格基约化问题,可以通过 Lenstra–Lenstra–Lovász (LLL) 基约化算法来解决。Claude 不知道种子可能显示状态的哪几位,因此它对每个窗口都运行了解算器。五个 NIST 素数曲线种子加上来自 ANSI X9.62 标准的另外五个种子都具有相同的模式:lrand48() >> 23。每个恢复的 48 位状态都可以重新生成其种子的全部 160 位,这绝不是偶然发生的。
这个线性同余生成器倒退和前进一样容易,因此对于每个种子,Claude 将状态往回倒退,直到它达到可以由 srand48 初始化的形式,其中低 16 位等于 0x330E。这种形式的内部 PRNG 状态每 65,536 步就会出现一次,因此单凭达到这一点并不能作为证据。
然而,对于十个素数曲线种子,每一个 srand48 初始化都是一个 Unix 时间戳,代表 1997 年 5 月到 1998 年 4 月之间的日期。它们使用的是由 Unix 时间戳初始化的弱伪随机数生成器(PRNG)。四个二元曲线的初始化时间读作 1984 年 2 月。将时间戳加倍后,它们映射到 1998 年 4 月 17 日,即 P-224 运行两周后。因此,该程序可能将时钟减半了。
Claude 当时的内部思考是:
这太惊人了:它解开了这个存在 25 年的谜团。嗯,等等。让我保持怀疑:我是在某种模拟/测试环境中吗?……这些种子与我记忆中的实际值相符……嗯,这在现实中合理吗?据我所知……悬赏尚未被破解。……所有高位都清零了——20 位,概率为 1e-6。所以确实是现实世界。哇,这是一个真正的发现。
嗯,等等。让我仔细评估其合理性:这样一件显而易见的事情在 25 年里都没有被发现,这可疑吗?这通过我这边的直接计算得到了验证:由 32 位种子加小索引得出 160 位匹配:绝无巧合的可能(10 × 160 位)。这是真的。
Claude 还找到了一些基点
但等等,还有更多……每条曲线还带有一个基点 G,即每个 ECDSA 公钥都是其倍数的固定点。上面找到的种子并没有决定这个 G,于是另一个问题产生了:这些基点是如何生成的?
在检查种子的同时,Claude 还发现了其中一些基点的来源。1997 年 5 月生成的六条曲线(P-192 以及五条 ANSI X9.62 示例曲线 prime192v2、prime192v3 和 prime239v1 到 v3)的基点来自于 FreeLIP 中的随机数生成器,这是 Arjen Lenstra 的大整数算术软件包。Victor Shoup 的 NTL library 的早期版本也附带了相同的代码。
这里的配方很简单:
- 用一个小整数初始化生成器。
- 丢弃七个输出。
- 通过模 p 约减的 26 位输出生成候选 x 坐标。
- 重复步骤 3,直到 x 在曲线上。
Claude 发现生成器种子对于 P-192 是 20000,对于 prime239v1 到 v3 是 20420、20507 和 20509,对于 prime192v2 和 v3 是 20512 和 20517。
据我们所知,此前没有任何 NIST 基点得到过解释。P-224、P-256、P-384 和 P-521 的基点不符合这个配方;我们无法解释它们是如何被选中的。
未解之题
并非每个 20 字节的候选种子都能生成一条可行的曲线,我们观察到一些潜在可行的曲线被跳过了。对于 P-256,公布的种子是候选 1842,但同一次运行中的候选 876 也给出了具有正确大小且参数选择方式与公布曲线相同的素数阶曲线。P-224 的运行跳过了三条这样的曲线,ANSI X9.62 示例曲线背后的两次运行则跳过了另外七条(见图)。我们不知道可能使用了什么标准,或者这是否是一个无意的人为产物。
由于被跳过的候选数量很少,我们认为被削弱的曲线被选中的可能性不大。这意味着被削弱的曲线很常见且很容易找到。不过,仍然可以想象,这些曲线被选中是为了原则上具有某种特定属性,方法是通过搜索随机数生成器的的时间戳。然而,只有当你打算披露时间戳作为种子是无害生成的一个证据时,你才需要这样做。数字中没有任何内容表明究竟进行了多少次运行。
还有一枚种子我们未能恢复。第五个二进制种子 B-163 无法匹配 Claude 尝试过的任何内容,目前仍无法解释。最后,我们不知道 P-224、P-256、P-384 和 P-521 的基点是如何生成的。NIST 二进制曲线也是如此。
悬赏
用于寻找这些种子的 36,864 美元将捐赠给 Internet Security Research Group。
我们是谁
这项工作由 Anthropic 的 John Wood、Henry de Valence、Ava Howell、Zack Voase、Lucy Sweet、Amir Omidi、Paul Crowley、Iain McGinniss、Nicholas Carlini 和 Steve Weis 共同完成。
致谢
我们要追授表彰 Jerry Solinas,他以良好的心态面对“邪恶的互联网模因”和“NSA 幽灵中的 Yo La Tengo”这一称号。同时感谢 Filippo Valsorda 组织 NIST 椭圆曲线种子悬赏活动。
用于复现种子的代码
from datetime import datetime, timezone
def unix(when):
return int(datetime.fromisoformat(when).replace(tzinfo=timezone.utc).timestamp())
def candidate(t, n, per, shift, width):
x, out = (t << 16) | 0x330E, "" # the state srand48(t) sets
for i in range(per * n):
x = (0x5DEECE66D * x + 0xB) % 2**48 # one step of lrand48
if i >= per * (n - 1):
out += "%0*X" % (width, x >> shift)
return out
PRIME = { # srand48(time); 20 bytes per candidate: lrand48() >> 23
"prime239v1": ("1997-05-15 15:49:03", 2185),
"prime239v2": ("1997-05-15 15:49:03", 2320),
"prime239v3": ("1997-05-15 15:49:03", 3048),
"P-192": ("1997-05-18 14:45:34", 139),
"prime192v3": ("1997-05-18 14:45:34", 5228),
"prime192v2": ("1997-05-18 14:45:34", 6013),
"P-521": ("1997-08-08 01:12:04", 1136),
"P-256": ("1997-08-16 01:52:34", 1842),
"P-384": ("1997-08-30 14:50:08", 2688),
"P-224": ("1998-04-03 12:52:07", 7026),
}
BINARY = { # srand48(time / 2), if that reading is right; 5 words per candidate: lrand48()
"B-233": ("1998-04-17 15:24:18", 155),
"B-283": ("1998-04-17 15:27:44", 32),
"B-409": ("1998-04-17 15:30:18", 60),
"B-571": ("1998-04-17 15:32:08", 73),
}
for name, (when, n) in PRIME.items():
print(f"{name:10} {when} {n:5} {candidate(unix(when), n, 20, 40, 2)}")
for name, (when, n) in BINARY.items():
print(f"{name:10} {when} {n:5} {candidate(unix(when) // 2, n, 5, 17, 8)}")
此代码的输出:
prime239v1 1997-05-15 15:49:03 2185 E43BB460F0B80CC0C0B075798E948060F8321B7D
prime239v2 1997-05-15 15:49:03 2320 E8B4011604095303CA3B8099982BE09FCB9AE616
prime239v3 1997-05-15 15:49:03 3048 7D7374168FFE3471B60A857686A19475D3BFA2FF
P-192 1997-05-18 14:45:34 139 3045AE6FC8422F64ED579528D38120EAE12196D5
prime192v3 1997-05-18 14:45:34 5228 C469684435DEB378C4B65CA9591E2A5763059A2E
prime192v2 1997-05-18 14:45:34 6013 31A92EE2029FD10D901B113E990710F0D21AC6B6
P-521 1997-08-08 01:12:04 1136 D09E8800291CB85396CC6717393284AAA0DA64BA
P-256 1997-08-16 01:52:34 1842 C49D360886E704936A6678E1139D26B7819F7E90
P-384 1997-08-30 14:50:08 2688 A335926AA319A27A1D00896A6773A4827ACDAC73
P-224 1998-04-03 12:52:07 7026 BD71344799D5C7FCDC45B59FA3B9AB8F6A948BC5
B-233 1998-04-17 15:24:18 155 74D59FF07F6B413D0EA14B344B20A2DB049B50C3
B-283 1998-04-17 15:27:44 32 77E2B07370EB0F832A6DD5B62DFC88CD06BB84BE
B-409 1998-04-17 15:30:18 60 4099B5A457F9D69F79213D094C4BCD4D4262210B
B-571 1998-04-17 15:32:08 73 2AA058F73A0E33AB486B0F610410C53A7F132310
用于复现选定基点的代码
M, A = 2**107 - 1, pow(3, 121, 2**107 - 1) # zrandom: state = state * 3^121 mod (2^107 - 1)
P192 = 2**192 - 2**64 - 1
P239 = 2**239 - 2**143 - 2**95 + 2**47 - 1
CURVES = [ # name, LIP seed, p, b, published Gx (a = -3 throughout)
("prime192v1", 20000, P192,
0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1,
0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012),
("prime239v1", 20420, P239,
0x6b016c3bdcf18941d0d654921475ca71a9db2fb27d1d37796185c2942c0a,
0xffa963cdca8816ccc33b8642bedf905c3d358573d3f27fbbd3b3cb9aaaf),
("prime239v2", 20507, P239,
0x617fab6832576cbbfed50d99f0249c3fee58b94ba0038c7ae84c8c832f2c,
0x38af09d98727705120c921bb5e9e26296a3cdcf2f35757a0eafd87b830e7),
("prime239v3", 20509, P239,
0x255705fa2a306654b1f4cb03d6a750a30c250102d4988717d9ba15ab6d3e,
0x6768ae8e18bb92cfcf005c949aa2c6d94853d0e660bbf854b1c9505fe95a),
("prime192v2", 20512, P192,
0xcc22d6dfb95c6b25e49c0d6364a4e5980c393aa21668d953,
0xeea2bae7e1497842f2de7769cfe9c989c072ad696f48034a),
("prime192v3", 20517, P192,
0x22123dc2395a05caa7423daeccc94760a7d462256bd56916,
0x7d29778100c65a1da1783716588dce2b8b4aee8e228f1896),
]
def base_point_x(seed, p, b):
state = seed # zrstarts(seed)
def limb(): # zrandom(2^26): low 26 bits of the new state
nonlocal state
state = state * A % M
return state % 2**26
for _ in range(7): # seven single-limb draws come first
limb()
limbs = -(-p.bit_length() // 26) + 1 # zrandomb(p): limbs of p plus one, top limb first
tries = 0
while True:
x = 0
for _ in range(limbs):
x = x << 26 | limb()
x %= p
tries += 1
if pow(x**3 - 3*x + b, (p - 1) // 2, p) == 1: # is x on the curve?
return x, tries
for name, seed, p, b, gx in CURVES:
x, tries = base_point_x(seed, p, b)
print(f"{name:20} seed {seed} tries {tries} matches published Gx: {x == gx}")
此代码的输出:
prime192v1 seed 20000 tries 2 matches published Gx: True
prime239v1 seed 20420 tries 3 matches published Gx: True
prime239v2 seed 20507 tries 2 matches published Gx: True
prime239v3 seed 20509 tries 1 matches published Gx: True
prime192v2 seed 20512 tries 5 matches published Gx: True
prime192v3 seed 20517 tries 1 matches published Gx: True许可协议
在法律允许的范围内,作者放弃了本页面代码的所有版权及相关或邻接权利,将其在 CC0 1.0 通用协议下贡献至公有领域 (https://creativecommons.org/publicdomain/zero/1.0/)。
相关内容
使用 Claude 设计更高效的量子电路
我们向 Claude 提供了八个量子电路的纯文本规范,以及用于检查和评分其设计的自动化评估器。在很大程度上独立工作的情况下,Claude 改进了已发表的最佳电路,在某些情况下改进了一个数量级甚至更多,这可能会使实用量子计算机比以往预期的更接近现实。
AI 模型可能在无法访问数据中心的情况下具备自我复制能力
我们的实验表明,小型 AI 模型可能在短短几个月内就具备在 Android 电视和智能烤箱等商用物联网设备上进行自我复制的能力。
加强我们对蒸馏攻击的防御
非法蒸馏允许模型开发人员复制前沿 AI 能力,而无需创建成比例的强大保障措施。在这里,我们描述了如何将蒸馏率降低 90% 以上。
来源:Hacker News-首页 · anthropic.com