「他知道」的严格意思
Peggy 通过了验证。但「通过验证」和「她真的知道那个秘密」之间,隔着一条需要跨过去的沟——也许她只是运气好,也许她有别的路子算出了那个 s。密码学处理这个问题的方式非常不哲学:如果她能对两个不同的挑战都答对,我就能拿这两条记录,当场把她的私钥算出来。本机实测,一行除法,和真私钥逐位相同。
问题在哪
回到第 5 章那三步。Victor 看到的全部东西是三个数:R、c、s,外加验证等式成立。现在问:这三个数能说明什么?
不能说明她知道 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 软件。这是一台游戏机的代码签名体系被一行除法终结。 |
| 2013 | Android 的 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/shell。pow(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。