Hacker News Best

Claude reveals ECDSA curve origins

8.5内容质量

TL;DR · AI 摘要

Claude模型揭示NIST ECDSA曲线种子源自Unix时间戳生成的随机数,而非此前猜测的幽默短语哈希。

核心要点

  • NIST ECDSA曲线种子由Unix时间戳初始化的lrand48随机数生成器生成
  • Jerry Solinas曾猜测种子来自幽默短语哈希但未证实
  • Claude在半小时内破解了5种NIST曲线种子生成机制

结构提纲

按章节快速跳转。

  1. 揭示NIST ECDSA曲线种子起源的长期谜题已被Claude解决。

  2. NIST于2000年采用ECDSA标准并定义了包含种子参数的椭圆曲线。

  3. NSA提供的种子参数缺乏解释,引发学术界长期猜测。

  4. Claude通过Unix时间戳初始化的随机数生成器还原了种子生成过程。

  5. 模型在半小时内确认5种NIST曲线种子均非哈希结果。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • ECDSA曲线种子起源
    • 发现过程
      • Claude模型破解
    • 生成方法
      • Unix时间戳初始化lrand48
    • 相关人物
      • Jerry Solinas
      • Bob Reiter

金句 / Highlights

值得收藏与分享的关键句。

#密码学#NIST#ECDSA#随机数生成#Anthropic
打开原文

Claude揭示ECDSA曲线起源 \ Anthropic

Frontier Red Team

Claude揭示ECDSA曲线起源

2026年10月16日

“这只是时间戳”:一个持续数十年的谜团——最流行密码学标准之一的起源,如今终于有了答案。

我们致力于解决密码学界长期悬而未决的问题,以消除对ECDSA(我们最广泛使用的密码学标准之一)的疑虑。

读者应将此视为一个有趣的终章,也是多年猜测的终结。无需采取任何行动或改变现有做法。大多数研究人员早已信任这些曲线。尽情享受吧。

ECDSA是目前最广泛使用的椭圆曲线密码学标准。它于2000年被NIST采纳为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和Jerry加薪’或‘Bob和Jerry统治一切’之类的。实际上编写代码的是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猜测该软件使用了随1990年代C语言库附带的伪随机数生成器(PRNG)。这些老旧的PRNG输出具有足够弱的特征,可以被识别。识别这些输出将揭示使用的PRNG类型,并可能恢复其初始化状态。

Claude编写了一个扫描器,逐位滑动每个公布的种子,并测试相邻数据块是否为1990年代生成器的连续输出。扫描覆盖了34种生成器和输出切片的组合。此时,Claude注意到某些种子的高位存在模式:它们全部为零。将这些种子写成五个32位字时,十个种子中有四个在所有20个字的高位都为零。这种情况发生的概率为1/5810。这是线性同余生成器lrand48的特征,该生成器仅返回48位状态的前31位。一旦知道要寻找这种模式,该特征肉眼即可识别。为恢复状态,Claude取第一个字,尝试所有2¹⁷种可能的17位隐藏值,并验证接下来的四个字是否符合。这种方法对所有四个种子都有效。

素数曲线的种子同样来自lrand48生成器,但保留了更少的输出位。Claude需要从这些位中恢复PRNG的内部状态。结果表明,恢复截断线性同余生成器的状态是一个教科书级的格问题,可通过Lenstra–Lenstra–Lovász(LLL)基约简算法解决。由于不清楚种子可能显示的状态位,Claude对所有窗口运行了解算器。五个NIST素数曲线种子加上来自ANSI X9.62标准的另外五个种子都呈现出相同模式:lrand48() >> 23。每个恢复的48位状态都能再生出其种子的全部160位,这种情况并非偶然发生。

这种线性同余生成器正向和反向运行同样容易,因此对于每个种子,Claude将状态倒推,直到达到srand48初始化时的形式,即低16位等于0x330E。这种形式的内部PRNG状态每65536步会出现一次,因此发现一个本身并不能作为证据。

然而,对于十个素数曲线种子,每个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 库早期版本也包含相同的代码。

这里的生成方法很简单:

  • 使用一个小整数作为生成器的种子。
  • 丢弃前七次输出。
  • 从26位输出中取模 p 生成候选 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 美元奖金将捐赠给互联网安全研究小组(ISRG)。

我们是谁

这项工作由 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 椭圆曲线种子漏洞赏金活动。

生成种子的代码

code
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)}")

复制

该代码的输出结果:

code
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 31A92EE2029FD10C53A7F132310

生成选定基点的代码

code
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

 /think

CURVES = [ # 名称, LIP 种子, p, b, 已发布的 Gx (a = -3 一直使用) ("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): 新状态的低 26 位 nonlocal state state = state * A % M return state % 226 for _ in range(7): # 七次单字节抽取在前 limb() limbs = -(-p.bit_length() // 26) + 1 # zrandomb(p): p 的字节加一,最高字节在前 tries = 0 while True: x = 0 for _ in range(limbs): x = x << 26 | limb() x %= p tries += 1 if pow(x3 - 3*x + b, (p - 1) // 2, p) == 1: # x 是否在曲线上? 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}")

code

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

code

## 许可证

在法律允许的范围内,作者已放弃本页面代码的所有版权及相关或邻接权利,根据CC0 1.0通用许可证(https://creativecommons.org/publicdomain/zero/1.0/)将其奉献给公共领域。

## 相关内容

### 与Claude一起设计更高效的量子电路

我们向Claude提供了八个量子电路的自然语言规范,以及一个自动评估器来检查和评分其设计。在很大程度上独立工作后,Claude改进了已发表的最佳电路,某些情况下提升了十倍或更多,这可能使实用量子计算机的实现比预期更早。

阅读更多

### AI期望差距

未来几年AI将如何重塑经济?关于AI未来对经济影响的核心分歧点是什么?这份笔记对这些问题提供了新的调查证据。

### Anthropic的就业变化

AI如何重塑Anthropic的工作和就业?作为AI采用的前沿公司,Anthropic可能是AI如何改变整个经济中工作组织、招聘和劳动力需求的领先指标。