Anthropic Research更新于

Claude 揭示 ECDSA 曲线起源

Anthropic称,Claude在半小时内解开NIST ECDSA曲线种子的来源之谜:种子并非哈希产物,而是以Unix时间戳为种子的lrand48随机数生成器输出。文章强调这不是密码学破解,反而降低了曲线含后门的可能性。

配图来源 · Anthropic Research
前沿红队

Claude 揭示 ECDSA 曲线起源

2026年10月16日

“这只是时间戳”:围绕最受欢迎的密码学标准之一的起源、长达数十年的谜团已经被解开了。

我们追寻了密码学界这一长期悬而未决的问题,以消除人们对 ECDSA 的疑虑,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 使用一句幽默短语的哈希生成了这些种子:

“我记得用的是计数器而不是多次哈希,但我不清楚细节。那段消息大致是 ‘Give Bob and Jerry a raise’ 或 ‘Bob and Jerry rule’ 之类的内容。实际编写代码的是 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),迭代若干次(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 library 也附带了相同的代码。

这里的配方很简单:

  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 美元奖金将捐赠给 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 Universal( 将其贡献至公有领域。https://creativecommons.org/publicdomain/zero/1.0/).

相关内容

用 Claude 设计更高效的量子电路

我们向 Claude 提供了八个量子电路的通俗语言规范,以及一个用于检查和评分其设计的自动评估器。Claude 主要在独立工作的情况下,改进了已发表的最佳电路,在某些情况下提升了一个数量级或更多,这可能使实用量子计算机的到来比之前预期的更近。

阅读更多

AI 模型可能能够在无需数据中心的情况下自我复制

我们的实验表明,小型 AI 模型可能只需几个月就能具备在 Android 电视和智能烤箱等商用物联网设备上进行自我复制的能力。

阅读更多

加强我们对蒸馏攻击的防御

非法蒸馏使模型开发者能够复制前沿 AI 能力,而无需建立相应强大的保障措施。在这里,我们描述了如何将蒸馏率降低了 90% 以上。

阅读更多

订阅 Frontier Red Team 新闻通讯

获取我们最新红队测试研究与发现的更新。

原始出处

Anthropic Research

内容说明

原始发布及相关权利归来源方。

机器翻译 · 请以原文为准