订阅 Frontier Red Team 通讯
获取我们最新的红队研究与发现动态。
“只是时间戳”:一个最流行的密码学标准之一,其起源长达数十年的谜团终于被解开。
我们追查了密码学界这个长期悬而未决的问题,以消除人们对 ECDSA 的担忧,它是我们使用最广泛的密码学标准之一。
读者应将其视为一个有趣的收尾篇章,也是多年猜测的终结。无需采取任何行动,也无需改变任何做法。大多数研究者本就信任这些曲线。尽情享受吧。
ECDSA 是使用最广泛的椭圆曲线密码学标准。它于 2000 年被 NIST 采纳,作为 FIPS 186-2 数字签名标准的一部分,该标准列出了一组不同大小和安全级别的标准椭圆曲线。对于素域和二元域上的曲线,规范中发布了一个“种子”(seed)参数,其哈希值被用来定义曲线的系数 b。这些种子的来源一直是一个长期存在的问题,因为它们由 NSA 提供,却没有任何解释或理由。例如,NIST P-256 曲线的种子为 SEED = C49D3608 86E70493 6A6678E1 139D26B7 819F7E90
却没有任何信息说明它来自何处。
Neal Koblitz 和 Alfred Menezes 就这一争议写了一篇出色的综述,“谜中之谜”,其中讨论了这些种子值的历史和相关担忧。这个谜团引发了对种子起源的业余调查,以及对任何能找到它们的人悬赏。
最广为流传的说法是,NSA 的长期雇员 Jerry Solinas 使用一个幽默口令的哈希生成了这些种子:
“我相信当时用的是计数器而不是多次哈希,但我不清楚细节。那条消息大概是‘给 Bob 和 Jerry 加薪’或‘Bob 和 Jerry 最棒’之类的话。实际写代码的是 Bob Reiter,他也不记得细节了。我们也找不到那么久远的档案。如果它们存在,无疑正躺在约柜附近的某个硬盘上。”
Jerry Solinas,2015 年
不幸的是,Jerry 于 2023 年去世,当时其他同事也未予回应,使得这些种子最初如何推导出来的问题依然悬而未决。
在得知种子可能是某个幽默短语的哈希这一线索后,几位 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 猜测该软件使用了 1990 年代 C 库附带的伪随机数生成器(PRNG)。这些旧的 PRNG 足够弱,其输出可以被识别。识别出输出将揭示使用了哪个 PRNG,并可能允许恢复其初始化状态。
Claude 编写了一个扫描器,每次一位地沿着每个已发布的种子滑动,并测试相邻块是否是 1990 年代生成器的连续输出。扫描覆盖了生成器和输出切片的 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 位)。这是真的。
但是等等,还有更多……每条曲线还附带一个基点 G,即每个 ECDSA 公钥都是其倍数的固定点。上面找到的种子并不能确定这个 G,因此另一个问题出现了:这些基点是如何生成的?
在检查种子时,Claude 还发现了一些基点的来源。1997 年 5 月生成的六条曲线(P-192 和五条 ANSI X9.62 示例曲线 prime192v2、prime192v3 以及 prime239v1 至 v3)的基点来自 FreeLIP 中的随机数生成器,这是 Arjen Lenstra 的长整数算术包。Victor Shoup 的 NTL 库的早期版本附带了相同的代码。
这里的配方很简单:
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 美元将捐赠给 互联网安全研究小组。
这项工作由 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 在很大程度上独立工作,改进了已发表的最佳电路,在某些情况下提升了一个数量级甚至更多,这可能使实用量子计算机比先前预期的更接近现实。
未来几年,AI 将如何重塑经济?关于 AI 对未来经济影响的核心分歧根源是什么?本说明提供了关于这些问题的新调查证据。
AI 如何重构了 Anthropic 的工作与就业?作为 AI 应用前沿的公司,Anthropic 可能是 AI 将如何改变整个经济中的工作组织、招聘和劳动力需求的领先指标。
获取我们最新的红队研究和发现的最新动态。
