卷 II · 对答CH 06深度 6/23

「他知道」的严格意思

Peggy 通过了验证。但「通过验证」和「她真的知道那个秘密」之间,隔着一条需要跨过去的沟——也许她只是运气好,也许她有别的路子算出了那个 s。密码学处理这个问题的方式非常不哲学:如果她能对两个不同的挑战都答对,我就能拿这两条记录,当场把她的私钥算出来。本机实测,一行除法,和真私钥逐位相同。

抽取器2 条记录PS3

◻ 本章先赊三条
第一条。「知道」在这里不是一个心理状态,而是一个可操作的定义:能被抠出来,就叫知道。 第二条。这个定义顺手给出了可靠性的证明方法:一个不知道秘密的人如果还能高概率通过,那我就能从他身上抠出秘密——矛盾。 第三条。同一段数学,写在教科书里叫「抽取器」,写在漏洞报告里叫「因随机数复用导致私钥泄露」。它已经至少两次造成了真实世界的重大事故。

问题在哪

回到第 5 章那三步。Victor 看到的全部东西是三个数:Rcs,外加验证等式成立。现在问:这三个数能说明什么?

不能说明她知道 x。因为一个不知道 x 的人,也能造出满足这个等式的三元组——第 5 章已经提过做法:先挑 c 和 s,再倒推 R = s·G − c·P。这样造出来的 (R, c, s) 完美满足验证等式,而造它的人对 x 一无所知。

(记住这句话。第 7 章整章都建立在它上面——那正是零知识的来源。这一章和下一章讲的是同一个数学事实的两面。)

那真交互凭什么算数?因为顺序不同。倒推的做法要求「先知道 c 再定 R」,而真协议里 R 必须先交。于是问题变成:一个先交了 R 的人,能对多少个不同的 c 给出正确的 s?

两条记录,一行除法

假设有这么一个人:他交了 R,然后无论 Victor 抛出什么 c,他都能答对。那我们做一件在现实中做不到、但在分析里完全合法的事——把时间倒回去,让他对同一个 R 回答两次不同的挑战:

第一次   R, c₁ → s₁      满足  s₁·G = R + c₁·P
第二次   R, c₂ → s₂      满足  s₂·G = R + c₂·P     (同一个 R!)

两式相减:
        (s₁ − s₂)·G = (c₁ − c₂)·P = (c₁ − c₂)·x·G

两边都是「走了多少步」,所以:
        s₁ − s₂ = (c₁ − c₂)·x

于是:
        x = (s₁ − s₂) / (c₁ − c₂)        ← 一个除法,秘密掉出来了

本机在真的 secp256k1 上跑这一步:

真私钥 x(生成时随机取的)      3eba1b6022f549cf385d4c8d6ee5c3f1…
抽取器算出的 (s₁−s₂)/(c₁−c₂)   3eba1b6022f549cf385d4c8d6ee5c3f1…
★ 两者相同                      True
用掉的问答记录条数                 2

两条。不是两千条,不是需要什么统计分析——两条记录,一个模除法,256 比特的私钥完整地掉出来。

◆ 于是「知道」有了定义

我们说一个人「知道」某个秘密,当且仅当:存在一个算法(叫「抽取器」),它把这个人当成一个可以反复调用、可以倒带的黑盒,就能把那个秘密提取出来。

这个定义有三个漂亮的地方:

  • 它不关心他脑子里想什么。他可能是背下来的,可能是现算的,可能是从别人那里买的——都不重要。能被抠出来就是知道。
  • 它把「可靠性」变成了可证明的东西。要证明骗子过不去,只需反证:假设一个不知道 x 的人能以不可忽略的概率通过,那就用抽取器从他身上抠出 x——可他明明不知道 x,矛盾。所以这样的人不存在。
  • 它解释了协议为什么必须长成那样。Schnorr 的三步之所以是三步,正是为了让抽取器有下手的地方:必须有一个「先固定下来、后面两次调用中保持不变」的量(那个 R),抽取器才有支点。
✎ 术语正名

教科书把这条性质叫 knowledge soundness(知识可靠性),把满足它的协议叫 proof of knowledge(知识证明)。中文一般译成「知识论证」,容易让人以为是在讨论认识论。

它和普通可靠性的区别值得说清楚:

  • 普通可靠性:如果这句话是假的,你过不去。(比如「这个数是合数」——它要么是要么不是。)
  • 知识可靠性:这句话可能是真的(那个 P 当然对应某个 x,数学上一定存在),但你必须真的握着它才能通过。

对钱包、身份、签名这类场景,需要的永远是后者:「存在一把私钥」是废话,「手里有那把私钥」才是要证的事。

同一段数学,写在事故报告里

上面那个抽取器有一个前提:两次回答用了同一个 R,也就是同一个随机数 k。在真实的签名系统里,k 每次都必须是全新的随机数——它有个名字叫 nonce(用一次的数)。

如果某个实现偷懒,让 k 重复了,会发生什么?攻击者就变成了抽取器。他不需要倒带任何人,只需要在网上收集两个签名。

年份事故后果
2010索尼 PlayStation 3。它的 ECDSA 签名实现把 k 写成了一个常数fail0verflow 在 27C3 大会上现场演示:收集两个官方签名,算出索尼的主签名私钥。此后任何人都能签出「官方认证」的 PS3 软件。这是一台游戏机的代码签名体系被一行除法终结。
2013Android 的 SecureRandom 缺陷。某些版本上它没有被正确播种,导致不同签名复用了 k多个安卓比特币钱包的私钥被从链上公开的签名里算了出来,币被直接转走。修复方案是全网用户重新生成地址
今天的对策:确定性 nonce(RFC 6979 / EdDSA)不再依赖随机数发生器,而是令 k = H(私钥 ‖ 消息)。同一条消息签两次得到同一个 k,但不同消息永远不同——而且它不需要任何熵源。

这是这本书里第一次出现、但绝不是最后一次的模式:一个概念在理论里是「安全性证明的工具」,在工程里就是「攻击代码」。抽取器、模拟器(下一章)、可提取性攻击——这个领域里几乎每一个正面概念,翻个面就是一个漏洞类别。

⌨ 自己跑一遍

把抽取器写出来只要一行。下面用第 5 章那个小群,二十行看完全过程:

import random

p, q, g = 2039, 1019, pow(3, 2, 2039)
x = random.randrange(1, q)              # ⟦ 私钥 ⟧
P = pow(g, x, p)

# 证明者犯的错:两次回答用了同一个 k
k = random.randrange(1, q)
R = pow(g, k, p)
c1, c2 = 111, 222                       # 两个不同的挑战
s1 = (k + c1 * x) % q
s2 = (k + c2 * x) % q

# 抽取器:一行
recovered = (s1 - s2) * pow(c1 - c2, -1, q) % q

print('真私钥  ', x)
print('抠出来的', recovered)
print('相同?  ', recovered == x)
# 真私钥   431          ← 每次跑都不一样
# 抠出来的 431          ← 但这两行永远相同
# 相同?   True

把它变成一次真实的攻击也只多两行——你甚至不需要知道 k:

# 攻击者只看到网上公开的两个签名 (R, c1, s1) 和 (R, c2, s2)
# 他连 k 是多少都不用知道,k 在减法里被消掉了
assert (s1 - s2) % q == ((c1 - c2) * x) % q
print('顺便把 k 也算出来:', (s1 - c1 * recovered) % q, '(真值', k, ')')

试着把 c1, c2 改成相同的值——除法会因为除数为零而失败。这正是定义里「两个不同的挑战」那个词的分量:抽取器需要的不是两次回答,是两次不同问题的回答。

python3 -c " import random p,q,g=2039,1019,pow(3,2,2039); x=random.randrange(1,q); k=random.randrange(1,q) s1,s2=(k+111*x)%q,(k+222*x)%q print(x, (s1-s2)*pow(111-222,-1,q)%q)"

在线跑:python.org/shellpow(a, -1, q) 是 Python 3.8 起支持的模逆元写法。

▸ 在现实里
  • 面试题的设计。你想判断候选人是不是「真的懂」一个东西,做法通常是从不同角度追问两次——如果他两个角度都答得上,你就相信他不是背的。这就是抽取器的日常版本:真懂的人能答任意角度,背的人只能答准备过的那一个。这个类比比它看起来更贴切:抽取器要的正是「同一个立场下,两个不同的挑战」。
  • 「你知道密码吗」的两种问法。让你输入密码,是把见证交出去;让你回答「密码的第 3、7、11 位是什么」(某些银行电话客服的做法),本质上是在做一次挑战–响应。后者更接近这本书,但它有个致命问题:多问几次,客服那边就能把整个密码拼出来——他自己成了抽取器。
  • 硬件钱包为什么值钱。它的全部价值就是让「抽取器」在物理上跑不起来:私钥永远不出安全芯片,nonce 由芯片内部确定性生成。买硬件钱包买的不是加密,是「不可提取」
  • 侧信道攻击的本质也是这个。通过功耗、时序、电磁辐射恢复私钥的攻击,做的事情就是「用非预期的通道多问几个问题」,直到方程足够多、秘密被解出来。密码学里所有的「泄露」,最后都归结成「攻击者攒够了几条不同的记录」。
✗ 这个直觉是错的

「既然抽取器能从证明者身上把私钥抠出来,那这个协议不是很危险吗?验证者岂不是可以一直问,问到把秘密套出来为止?」

不能,而且原因很精确:抽取器需要的是「同一个 R 上的两次不同回答」,而这在真实交互里做不到。诚实的 Peggy 每一轮都会换一个全新的 k,于是每一轮的 R 都不同,两条记录之间没有任何公共支点——上面那个减法消不掉 k,方程解不出来。

抽取器是分析时的思想实验:它假设我们能像调试器一样把证明者「倒带」到发出 R 之后、收到挑战之前的那个瞬间,然后喂给他不同的挑战。现实中的验证者没有这个能力——他只能顺着时间往前走。

这个区别就是整个安全性证明的支点。而它同时给出了一条极其实用的工程铁律:只要哪个实现让 nonce 重复了,攻击者就白得了「倒带」能力。索尼和那些安卓钱包,正是把这个不该给的能力送了出去。判据是:看到签名/证明的实现,第一件事是找 k 从哪来。它要是不随机、或者可能重复,剩下的代码写得再漂亮都没用。

✓ 结账

这一章的一句话

密码学不问「他心里知不知道」,只问「我能不能从他身上把它抠出来」——而这个把定义变成算法的动作,既是可靠性证明的全部内容,也是现实中两起重大事故的直接成因。

下一章翻到同一枚硬币的反面。这一章我们说:一个不知道 x 的人,也能造出满足验证等式的三元组(先挑 c、s,再倒推 R)。那句话在这一章是个麻烦,而在下一章它就是全部的答案既然验证者看到的东西他自己在家就能打印出来,那他从中学到的知识精确地等于零。下一章会在一个小群上把所有可能的记录穷举一遍——真证明者 121 条,模拟器 121 条,逐条对上,总变差距离精确为 0