← 文章 / 科技资讯
Hacker News 1小时前 · 2026-10-17 03:51:17 · 7 阅读

Claude 揭开 ECDSA 曲线种子起源之谜

Claude reveals ECDSA curve origins

Oct 16, 2026

“不过是时间戳”:一个备受普及的加密标准诞生之谜,历经数十载,终于揭晓。

我们跟进并解决了这个 cryptographic 界长期悬而未决的问题,以消除大家对 ECDSA 的疑虑。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时,并未说明其选取过程,由此引发了外界猜测其可能被刻意设计以包含后门。此次发现大大降低了这种可能性。种子(seeds)的生成方式非常直接:使用一个通用的伪随机数发生器,并以时钟作为初始化输入,任何人都可以复现这一过程。这意味着被篡改的空间极小,我们可以确信这些参数是安全的。

发现过程

此前基于传闻构建的关键词搜索一无所获。Claude 换了一个思路:预判生成曲线的软件应当如何工作。Claude 推测该软件使用了上世纪90年代C语言标准库中自带的伪随机数发生器(PRNG)。这些老旧的 PRNG 强度较弱,其输出特征容易被识别。识别出输出模式不仅能揭示所使用的 PRNG 类型,还可能反向推导出其初始状态。

Claude 编写了一个扫描器,对每个已发布的种子进行逐比特滑动,测试相邻数据块是否对应上世纪90年代生成器连续输出的结果。该扫描覆盖了生成器与输出切片组合的34种情况。在此过程中,Claude 注意到部分种子的最高位存在规律:它们全为零。若以五个32位字(word)表示,十个种子中有四个在所有20个字的最高位均为0。这种情况自然发生的概率仅为1/5810。这是线性同余生成器 lrand48 的典型特征,该函数仅返回48位状态的高31位。一旦知道寻找的方向,这种模式肉眼即可辨认。为了恢复状态,Claude 取第一个字,遍历17个隐藏位的全部 $2^{17}$ 种可能值,并验证后续四个字是否符合预期。这一方法在所有讨论到的四个种子上均验证成功。

素数曲线的种子同样源自 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. 用 26 位输出对 p 取模,得到候选 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 Solanas 的贡献。面对被贴上“邪恶互联网梗”和“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)}")
Copy

该代码的输出结果:

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 = [  # 名称, 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 % 2**26
    for _ in range(7):                   # 前7次为单limb抽取
        limb()
    limbs = -(-p.bit_length() // 26) + 1 # zrandomb(p): p的limb数加1,高位limb在前
    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:   # 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}")
Copy

该代码的输出如下:

prime192v1           种子 20000  尝试次数 2  匹配公开 Gx:是
prime239v1           种子 20420  尝试次数 3  匹配公开 Gx:是
prime239v2           种子 20507  尝试次数 2  匹配公开 Gx:是
prime239v3           种子 20509  尝试次数 1  匹配公开 Gx:是
prime192v2           种子 20512  尝试次数 5  匹配公开 Gx:是
prime192v3           种子 20517  尝试次数 1  匹配公开 Gx:是
复制

许可

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

原始来源: Hacker News

评论 (0)