跳到正文
Anthropic:Research·· 1 天前精选AI 评分78

Anthropic 用 Claude 揭开 NIST ECDSA 曲线种子来源之谜

Claude reveals ECDSA curve origins

AI 导读

Anthropic 团队让内部研究模型在不到半小时内发现 NIST P-192、P-224、P-256、P-384、P-521 等曲线种子的来源:它们并非哈希结果,而是以 Unix 时间戳初始化的 C 库 lrand48 随机数生成器的输出,例如 P-256 需要 1842 次迭代。

推荐理由

原文由当事团队完整复盘 Claude 如何破解 NIST 曲线种子的来源,附可复现代码,读者能据此理解破解思路与结论。

正文 · AI 翻译

“只是时间戳”:这个最流行的密码学标准之一的起源之谜,历经数十年,如今终于被解开。

我们追查了密码学界这个长期悬而未决的问题,以消除人们对 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 年去世,当时的其他同事也未予回应,这使得这些种子最初如何推导出来的问题依然悬而未决。

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 美元将捐赠给 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 模型可能只需数月就能具备在商用 IoT 设备(如 Android 电视和智能烤箱)上自我复制的能力。

阅读更多

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

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

阅读更多

来源:Anthropic:Research · anthropic.com