跳到正文
Hacker News· ahlCVA·· 17 天前同事件AI 评分70

Claude 揭示 NIST ECDSA 曲线种子源自 lrand48 与 Unix 时间戳

Claude reveals ECDSA curve origins

AI 导读

Anthropic 在 2026 年 10 月发布的文章介绍,Claude 通过推理发现 NIST ECDSA 曲线 P-192、P-224、P-256、P-384 和 P-521 的种子均来自 C 库的 lrand48 随机数生成器,并由 1997 年 5 月至 1998 年 4 月之间的 Unix 时间戳初始化,并非此前猜测的哈希算法。

同一件事,精选收录的是《Claude 半小时破解 NIST 椭圆曲线 20 年种子悬案》

正文 · AI 翻译

"这不过是时间戳而已":这一最受欢迎密码学标准起源的数十年悬案终于水落石出。

我们追查了密码学界的这一长期悬而未决的问题,以消除人们对 ECDSA——我们应用最广泛的密码学标准之一——的疑虑。

读者可以将此视为一个有趣的收尾章节,以及多年猜测的终结。无需采取任何行动,也无需改变任何做法。大多数研究人员对这些曲线早已抱有信心。尽情享用吧。

ECDSA 是应用最广泛的椭圆曲线密码学标准。它于 2000 年被 NIST 采纳,作为 FIPS 186-2 数字签名标准 的一部分,该标准列出了一组不同大小和安全等级的标准椭圆曲线。对于素数域和二元域上的曲线,规范发布了一个"种子"参数,其哈希值用于定义曲线的系数 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 年去世,当时其他同事也未能提供帮助,因此这些种子最初是如何推导出来的问题仍然悬而未决。

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 个字中都带有最高位为 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 库的早期版本附带了相同的代码。

这里的方法很简单:

  1. 用一个小整数为生成器播种。
  2. 丢弃七个输出。
  3. 从模 p 化简的 26 位输出生成候选 x 坐标。
  4. 重复步骤 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 美元将捐赠给互联网安全研究小组。

关于我们

这项工作由 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