10 月 16 日,Anthropic 的 Frontier Red Team 发了一篇研究文章:NIST P-256 等椭圆曲线的种子,困扰密码学界二十多年的"来源之谜",被一个内部 Claude 研究模型在半小时内解开了。答案出人意料地朴素:这些种子不是什么英文句子的哈希,而是 1997 年某台机器上,C 标准库 lrand48 用 Unix 时间戳初始化后吐出来的随机数。
同一天,赏金组织者、密码学工程师 Filippo Valsorda 用 Go 重写算法独立验证了结果,并确认 2023 年设立的赏金已被领取。
开发者值得看看这件事,原因有三个:P-256 就在你每天用的 TLS 证书、JWT(ES256)、SSH 密钥里;它是"AI 做研究"里少见的、结果可以被任何人一分钟内复核的例子;破解思路本身,正好是一堂"为什么不能用时间戳做随机种子"的实战课。
数据以 2026 年 10 月 17 日能查到的原文为准。
一、它是什么
| 项目 | 内容 |
|---|---|
| 发布日期 | 2026-10-16(Anthropic 研究文章、Filippo Valsorda 博客同日) |
| 发布方 | Anthropic Frontier Red Team,署名 10 人,含 Nicholas Carlini、Steve Weis、Paul Crowley 等 |
| 用的模型 | 原文只写"internal research model",具体型号未公开 |
| 用时 | 官方称"不到半小时"(外部无法验证,能验证的是结果) |
| 解开了什么 | P-192、P-224、P-256、P-384、P-521,ANSI X9.62 的 5 条示例素数曲线,以及 B-233、B-283、B-409、B-571 共 14 个种子;另外解释了 6 个基点 |
| 没解开的 | B-163 的种子;P-224/256/384/521 和所有二元曲线的基点 |
| 安全影响 | 原文明确说"不是密码学破解、不是新攻击、不是曲线弱点",不需要采取任何行动 |
| 赏金 | 原始 12,288 美元,捐给慈善机构时翻三倍,即 36,864 美元,全部捐给 Let's Encrypt 背后的 ISRG |
| 复现代码 | 原文页面内嵌 Python,CC0 授权;Filippo 的 Go 版在 GitHub FiloSottile/mostly-harmless |
背景:这个"谜"为什么存在
2000 年 NIST 在 FIPS 186-2 里发布这批曲线时,用的是 ANSI X9.62 的"可验证随机"流程:拿一个 160 位种子,做 SHA-1,用结果推出曲线系数 b。这样任何人都能验证"b 确实是种子哈希出来的"。
问题在于,种子本身从哪来,没人说得清。种子由 NSA 提供,经手人 Jerry Solinas 在 2015 年回忆说,种子是某句玩笑话的哈希,类似 SHA1("Jerry deserves a raise."),但原句随着换电脑丢了。Solinas 在 2023 年初去世,线索就断了。
"种子来源不明"给了怀疑空间:如果 NSA 能反复换种子,直到碰上一条只有它知道弱点的曲线,可验证随机就成了摆设。Bernstein 和 Lange 等人一直在讨论这种可能性。于是 2023 年 10 月,Filippo 设了赏金:谁能找出这几条曲线的"原句",谁就拿钱。
结果答案根本不是原句,Solinas 记错了。
二、Claude 是怎么破的
推理链条不长,可以拆成四步。
2.1 第一步:换个假设
此前几位 Anthropic 员工都顺着"玩笑话哈希"去搜,没有结果。这次提问的员工故意没告诉 Claude 这条线索。Claude 自己提出的假设是:生成程序用了 1990 年代 C 库自带的伪随机数生成器(PRNG),这类生成器很弱,它的输出有可识别的指纹。
它写了一个扫描器,沿着每个种子逐位滑动,检查相邻片段像不像同一个生成器的连续输出,一共试了 34 种"生成器 × 输出切片"组合。
2.2 第二步:最高位全是 0
把每个 160 位种子拆成 5 个 32 位字。10 个种子里,有 4 个的全部 20 个字最高位都是 0。原文给出的随机出现概率是 1/5810。
这对得上 lrand48:它内部是 48 位线性同余生成器(LCG),每次只返回状态的高 31 位。装进 32 位字,最高位当然永远是 0。
lrand48 的 48 位内部状态
┌────────────── 高 31 位 ──────────────┬──── 低 17 位 ────┐
│ 1110100110101011001111111110000 │ 01001100100100011│
└──────────────────────────────────────┴──────────────────┘
│ 返回给调用者
▼
32 位字:0 1110100110101011001111111110000 = 74D59FF0(B-233 种子第 1 个字)
↑ 最高位必然为 0
2.3 第三步:恢复生成器状态
知道是 lrand48 之后,问题变成:已知输出,倒推出内部状态。
- 二元曲线(B-233 等):种子由 5 个完整的 31 位输出组成,每次只藏了低 17 位。直接枚举 2^17 = 131072 种可能,再看后 4 个字能不能对上。4 个种子全部命中。
- 素数曲线(P-256 等):每个字节只取了状态最高 8 位(相当于
lrand48() >> 23),藏起来的位更多,没法暴力枚举。这是"截断 LCG 状态恢复",属于教科书级的格问题,用 LLL 格基约化就能解。Claude 自己写了格求解器,每个 48 位状态都能完整再生 160 位种子。
2.4 第四步:倒推出时间戳
LCG 可以倒着跑。srand48(t) 初始化时,会把状态设成"高 32 位是 t,低 16 位固定是 0x330E"。所以 Claude 把恢复出的状态一步步往回退,直到低 16 位等于 0x330E。
单看这一条不算证据:每 65536 步就会碰巧出现一次。真正有说服力的是,回退到的高 32 位全是合理的 Unix 时间戳,集中在 1997 年 5 月到 1998 年 4 月之间。
| 曲线 | 时间戳(UTC) | 第几个候选 |
|---|---|---|
| P-192 | 1997-05-18 14:45:34 | 139 |
| 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 |
| B-233 | 1998-04-17 15:24:18 | 155 |
二元曲线有个小插曲:直接算出来的时间是 1984 年 2 月,乘以 2 之后变成 1998 年 4 月 17 日,比 P-224 晚两周。原文推测当时的程序把时钟值减半了。
整条流水线还原出来大概是这样:
srand48(当前时间) ──> 循环生成 20 字节候选种子
│
▼
SHA-1 推出系数 b,检查:
判别式非零、阶为素数、嵌入度足够大 ...
│
不合格 ────┘ 合格 ──> 公布(或者被跳过,原因不明)
2.5 自己验证一下
我按原文和 Filippo 代码的描述写了一个最小复现,Python 3.14 实测输出和 FIPS 186 里公布的 P-256 种子完全一致:
# 复现 NIST P-256 的种子:srand48(871696354) 之后的第 1842 个 20 字节块
A, C, MASK = 0x5DEECE66D, 0xB, (1 << 48) - 1
x = (871696354 << 16) | 0x330E # srand48(t):高 32 位放时间戳,低 16 位固定 0x330E
for n in range(1, 1843):
seed = bytearray()
for _ in range(20):
x = (x * A + C) & MASK # 48 位线性同余,前进一步
r = x >> 17 # lrand48() 返回高 31 位
seed.append(r >> 23) # 只留最高 8 位,拼成一个字节
print(seed.hex().upper())
# C49D360886E704936A6678E1139D26B7819F7E90
把时间戳换成 446413329(1998-04-17 15:24:18 除以 2)、每个候选改为 5 个完整的 31 位输出,第 155 个候选就是 B-233 的种子 74D59FF0...049B50C3,我也跑通了。
一个小分歧:Anthropic 写的是 lrand48,Filippo 代码注释写的是 (int)(drand48() * 256)。两者是同一个 48 位引擎,drand48()*256 取整和 lrand48() >> 23 都等于状态的最高 8 位,结果相同。
三、这对 P-256 意味着什么
3.1 后门说法基本可以放下了
后门假说的核心是"种子选择有足够的自由度,可以反复挑"。现在种子来自"时间戳 + 候选序号",Filippo 估算即使按宽松假设,自由度也只有"勉强 30 bits",而 Bernstein 和 Lange 2017 年描述的那种挑弱曲线手法需要约 70 bits。他的结论是:这"完全排除"了那种手法。
Anthropic 的措辞更克制:结果让他们"更确信曲线没有后门",但也承认,理论上仍可能通过搜索时间戳挑出带某种性质的曲线,而数据里看不出当年一共跑了多少次。
两边说法的差距值得注意:一个说"完全排除",一个说"更有信心"。我倾向 Filippo 的算法账,但"跑了多少次"确实没有记录。
3.2 仍然没解释的部分
- 被跳过的候选:P-256 那次运行里,第 876 个候选同样能生成尺寸正确、素数阶的曲线,却被跳过了,用的是 1842。P-224 的运行跳过了 3 条,ANSI X9.62 的两次运行跳过了 7 条。Filippo 说合格候选大约是公布数量的两倍,还有一条筛选标准没找到。
- 基点:只解释了 6 个(来自 Arjen Lenstra 的 FreeLIP 库生成器),P-256 的基点怎么选的仍然未知。
- B-163 的种子没有对上任何方案。
原文附了一张安全性检查表。按我对表的读法:876 号候选的"扭曲曲线安全性"约 2^99.9,略低于表注的 2^100 阈值,P-256 是 2^120.2。但这解释不了为什么跳过它:表里 P-224 自己的扭曲安全性只有 2^58.3,照样被公布了;而且表上标注扭曲攻击的公开时间是 2000–2008 年,晚于 1997 年。原文对此没有下结论,我也不下。
3.3 你需要做什么
什么都不用做。原文、Filippo、DEV 社区的复盘文章说法一致:不用换曲线、不用轮换密钥。Filippo 甚至说他现在个人更推荐 NIST 曲线,而不是 Curve25519/Ed25519,同时调侃这批曲线刚"平反",就要让位给后量子签名了。
四、和以往的方案对比
| NIST P-256(种子生成于 1997) | Curve25519 | Brainpool | |
|---|---|---|---|
| 参数怎么来的 | 种子过 SHA-1 推出 b;种子来源 2026 年才查清 | 按公开的设计准则选"满足条件的最小值" | 种子取自 π、e 等公开常数 |
| 能否验证"没挑过" | 现在能复现,但仍有被跳过的候选没解释 | 准则公开,基本无可挑空间 | 取数规则公开,可以复现 |
| 部署情况 | TLS、WebAuthn、ES256、各种 HSM 默认 | SSH、Signal、WireGuard 主流 | 欧洲部分政府和行业标准 |
这张表说明,所谓"nothing-up-my-sleeve"(袖子里没藏东西)的参数生成,关键不在用什么数字,而在挑选空间有多大、过程能不能被复现。P-256 这次补上了"能复现",挑选空间也被压到约 30 bits。
五、对 AI 研究能力该怎么看
5.1 这个案例特别在哪
AI 参与科研的新闻很多,但大多难以复核:要么数据不公开,要么需要实验室条件。这一次不同:
- 答案是"一个时间戳 + 一个序号",十几行代码就能复现,Filippo 当天就用 Go 独立复核了。
- 问题有明确的历史背景:人类专家悬赏了三年,几位员工按原线索搜索都失败了。
- Claude 做对的关键一步是不信线索,换了一个假设,然后用标准工具(扫描器、LLL)把它做实。
5.2 但要打几个折扣
- "半小时"是官方自述,模型型号、算力、提示词都没公开,外界只能验证结果,没法验证过程。
- 用到的技术(截断 LCG 恢复、LLL)都是教科书内容,难点在"想到往这个方向试"。这说明模型在"提出可检验的假设并快速验证"上已经很强,但不等于它做出了新的密码学。
- 原文里 Claude 的内部思考还出现过一句"我是不是在模拟测试环境里"。随后它用"32 位种子加小序号,匹配出 160 位,不可能是巧合"说服了自己。这段记录可以看出它在主动排除巧合。
- daily.dev 统计的社区讨论约 140 条评论,其中相当一部分在争论另一件事:AI 实验室把网络安全能力限制在内部,外部用户用不上同等能力。
另据 Techmeme 转引,署名作者之一 Steve Weis 在 X 上说,Claude 还反推出了 1977 年 DES S 盒测试向量是在 PDP-11 的 Unix V6 上用 rand() 生成的。这条我没找到正式文章,只当线索看。
六、给开发者的实际教训
最实用的启发其实是随机数:用时间做种子的非密码学 PRNG,二十多年后照样能被倒推出来。
可以用下面这条命令在自己的代码库里粗查一遍(命中不等于漏洞,关键看输出有没有用在令牌、密码、重置链接、密钥上):
git grep -nE "rand48|srand\(|Math\.random|math/rand|random\.seed|random\.randint" -- ':!*.lock'
需要安全随机数时,各语言用这些:
| 语言 | 不要用 | 应该用 |
|---|---|---|
| Python | random | secrets.token_hex()、secrets.token_urlsafe() |
| Node.js | Math.random() | crypto.randomBytes()、crypto.randomUUID() |
| Go | math/rand | crypto/rand |
| 浏览器 | Math.random() | crypto.getRandomValues() |
| C | rand()、lrand48() | getrandom()、arc4random_buf() |
七、我的判断
- 对密码学:这是一次"考古",不是破解。P-256 的可信度反而提高了,后门说法基本可以收场。剩下被跳过的候选和基点来源,属于"还有好奇心,但不影响安全"的问题。
- 对 AI:这是目前我见过最干净的"AI 做研究"案例之一:问题有历史,答案能一分钟复核,独立专家当天背书。它展示的能力是"提出别人没想到的假设,并用标准工具快速证伪或证实",这正是安全研究里最耗人的部分。
- 要警惕的地方:同样的能力反过来就是"从公开数据里倒推出别人的随机数状态"。这次的对象是 1997 年的 C 库,下一次可能是你项目里某个用
Math.random()生成的邀请码。比起感叹 AI 多厉害,先去 grep 一下自己的代码更实在。
参考资料
- Anthropic,Claude reveals ECDSA curve origins(2026-10-16):www.anthropic.com/research/ec…
- Filippo Valsorda,The NIST Elliptic Curves Seeds Bounty Has Been Claimed(2026-10-16):words.filippo.io/seeds-solve…
- Filippo Valsorda,验证代码 seeds.go:github.com/FiloSottile…
- Filippo Valsorda,赏金公告(2023-10-05):words.filippo.io/seeds-bount…
- Gabriel Anhaia,Claude Traced the NIST Curve Seeds to a 1997 Clock and lrand48(DEV,2026-10-17):dev.to/gabrielanha…
- Techmeme 当日汇总:www.techmeme.com/261016/p20
- daily.dev 讨论汇总:daily.dev/posts/claud…