Python实现Schnorr协议:零知识证明入门与实践

📅 2026/7/28 2:58:59
Python实现Schnorr协议:零知识证明入门与实践
1. 项目概述为什么从Schnorr协议切入零知识证明如果你对密码学或者区块链技术有所关注大概率听说过“零知识证明”这个词。它听起来很酷但总让人觉得是数学博士的专属领域充满了复杂的椭圆曲线和群论。今天我想用一个更接地气的角度带你亲手实现一个最简单的零知识证明系统。我们不从那些庞然大物比如zk-SNARKs开始而是选择Schnorr协议。原因很简单Schnorr协议是理解现代零知识证明和数字签名如比特币的Taproot升级所用的技术的绝佳基石。它结构清晰数学优美用Python几十行代码就能跑起来非常适合作为我们窥探这个神秘世界的第一扇窗。简单来说我们要做的这件事是证明者Prover向验证者Verifier证明自己知道一个秘密值比如私钥而整个过程验证者除了“证明者确实知道这个秘密”之外得不到关于秘密本身的任何额外信息。这就是“零知识”的核心。你可能会想这有什么用应用场景其实非常广泛在不泄露密码的前提下完成身份认证、在区块链上证明你拥有某个资产而不暴露账户余额、甚至是在保护隐私的前提下进行数据计算验证。通过Python实现它不仅能让你透彻理解协议每一步的交互逻辑更能让你亲手“触摸”到那些抽象的密码学概念比如承诺、挑战和响应它们是如何通过代码流动起来的。2. 核心原理拆解Schnorr协议的三幕戏在动手写代码之前我们必须把舞台搭好理解演员和剧本。Schnorr协议本质上是一个“三步交互式证明”它像一场精心设计的魔术表演包含了承诺、挑战和揭晓三个关键环节。2.1 角色与舞台设定首先明确我们的角色和舞台证明者Prover我们称之为Alice。她拥有一个秘密的私钥x这是她不想告诉任何人的东西。验证者Verifier我们称之为Bob。他的目的是确认Alice是否真的知道x但自己不想、也不能知道x具体是多少。公共参数Public Parameters这是双方都知道的舞台布景。主要包括一个循环群G通常使用椭圆曲线上的点群比如比特币常用的secp256k1。为了简化在入门阶段我们可以用一个大的素数p下的乘法模群来模拟即整数模p的乘法群。这能让我们避开椭圆曲线库的初始复杂性专注于协议逻辑。该群的一个生成元g。在乘法模群中g是一个模p的原根。对应的公钥y g^x mod p。这是由私钥x计算得出的并且是公开的。Bob知道y但不知道x。协议的目标就是Alice向Bob证明她知道那个能满足y g^x mod p的x。2.2 协议交互的三步流程整个协议像一场对话只有三个回合承诺CommitmentAlice首先随机生成一个临时秘密r在密码学中称为nonce一次性随机数。她用这个r计算一个承诺值R g^r mod p。然后她把R发送给Bob。R就像是Alice把r“锁”在一个盒子里交给了BobBob能看到盒子但打不开。这一步是后续所有“零知识”属性的基础因为它先把一个随机状态固定下来。挑战ChallengeBob收到R后随机生成一个挑战数c然后把它发送给Alice。这个c必须是随机的并且每次验证都应该不同这确保了证明是“新鲜”的防止重放攻击。响应ResponseAlice收到挑战c后利用自己的秘密x和临时秘密r计算一个响应值s r c * x mod q。这里的q是群的阶在简化模型中我们可以先令q p-1对于素数模乘法群阶是p-1。随后Alice将s发送给Bob。验证环节 Bob现在手上有三样东西公开的公钥yAlice发来的承诺R和响应s以及自己生成的挑战c。他进行一个简单的验证计算 检查等式g^s mod p R * y^c mod p是否成立。 如果成立Bob就相信Alice知道私钥x否则证明失败。注意这个等式的美妙之处在于Bob完全不需要知道x或r。他只需要进行几次模幂运算就能完成验证。这正是零知识证明的魔力——验证者能验证一个陈述的真伪却学不到任何用于证明该陈述的秘密信息。2.3 安全性浅析为什么它是“零知识”且可靠的完备性Completeness如果Alice诚实地执行协议那么等式g^s g^(rcx) g^r * (g^x)^c R * y^c必然成立。所以诚实的证明总能通过验证。可靠性Soundness如果一个不知道x的假冒者我们称她为Eve想骗过Bob她几乎不可能成功。因为在发送承诺R之后挑战c是随机且不可预测的。Eve必须能针对Bob可能发出的任何一个c都计算出一个有效的s来满足等式这等价于直接解出离散对数x在计算上是不可行的。零知识性Zero-KnowledgeBob从整个交互中即(R, c, s)三元组能自己模拟出一个完全一样的、有效的对话记录而无需Alice参与。模拟的方法是先随机生成c和s然后反向计算出R g^s * y^(-c) mod p。这样生成的(R, c, s)在分布上与真实交互产生的无法区分。既然Bob自己能“无中生有”地造出看似有效的证明那么这个证明过程显然没有泄露任何关于x的知识。理解了这三幕戏我们就可以把剧本翻译成Python代码了。3. 环境准备与基础工具函数实现我们选择Python因为它语法简洁拥有丰富的数学库非常适合做原理性原型。这里我们不直接使用成熟的密码学库如cryptography而是从最底层的数学运算开始构建以加深理解。3.1 环境搭建与依赖确保你安装了Python 3.8或更高版本。我们主要依赖Python内置的random模块和pow函数进行大数运算。为了更清晰地展示我们也会使用secrets模块来生成密码学安全的随机数。# 不需要额外安装包使用标准库即可。 # 但为了更好的演示我们可以创建一个干净的虚拟环境可选 # python -m venv zkp-env # source zkp-env/bin/activate # Linux/Mac # zkp-env\Scripts\activate # Windows3.2 核心数学函数实现在密码学中我们经常需要在很大的整数比如2048位上进行模幂运算。Python的pow函数内置了三参数模式pow(base, exponent, modulus)可以高效地计算(base^exponent) mod modulus这正是我们需要的。我们来编写几个基础工具函数import random import secrets def generate_prime(bits256): 生成一个指定位数的大素数简化版用于教学。 注意工业级应用应使用标准库如cryptography或经过审计的库如gmpy2来生成安全素数。 这里使用一个简单的方法生成一个大概率是素数的数。 while True: # 生成一个奇数 p secrets.randbits(bits) p | (1 bits - 1) | 1 # 确保是bits位且为奇数 # 简单的素性测试费马小定理不够严谨但用于演示 if pow(2, p-1, p) 1: # 可以增加更多测试如米勒-拉宾测试这里为简洁省略 return p def find_generator(p): 在素数p的乘法群中寻找一个生成元原根。 这是一个简化的寻找方法适用于教学演示。 对于素数p群的阶是 p-1。 我们尝试随机数g检查对于p-1的所有素因子q是否 g^((p-1)/q) mod p ! 1。 # 分解 p-1 的质因子简化这里我们假设p-1有一些小因子实际应使用分解算法 # 为了演示我们直接尝试2到100之间的数看是否是原根。 # 这是一个非常低效且不严谨的方法仅用于小p或演示。 factors [2] # 假设2是p-1的一个因子对于安全素数p2q1因子就是2和q # 更实际的做法是先分解p-1 for g in range(2, p): is_generator True for q in factors: if pow(g, (p-1)//q, p) 1: is_generator False break if is_generator: return g return None # 理论上对于素数应该总能找到 def mod_inverse(a, p): 计算 a 在模 p 下的乘法逆元。 使用扩展欧几里得算法。 当 p 是素数时也可以使用费马小定理a^{-1} ≡ a^{p-2} mod p return pow(a, p-2, p) # 费马小定理求逆元要求p是素数且a与p互质实操心得在实际的密码学应用中生成安全的素数和寻找生成元是极其关键且复杂的一步。我们这里的实现是高度简化的绝对不应用于任何生产环境。真正的系统会使用像cryptography库中定义好的、经过充分测试的椭圆曲线群如SECP256K1及其生成元点。这里手动实现的目的是为了让你看清群、生成元、模运算这些基础构件是如何工作的。4. Schnorr协议完整Python实现现在让我们把理论转化为代码。我们将分别实现证明者Prover和验证者Verifier的类。4.1 定义公共参数类首先我们需要一个类来封装双方共享的公共参数。class SchnorrPublicParameters: 封装Schnorr协议的公共参数。 def __init__(self, pNone, gNone, qNone): 初始化公共参数。 :param p: 素数模数 :param g: 生成元 :param q: 群的阶在简化模型中q p-1 if p is None or g is None: # 如果没有提供则生成一组演示用的参数安全性很低 self.p generate_prime(bits64) # 64位仅用于演示实际需要至少2048位 self.q self.p - 1 # 简化假设 self.g find_generator(self.p) if self.g is None: raise ValueError(无法找到生成元请检查素数p。) else: self.p p self.g g self.q q if q is not None else p - 1 print(f[公共参数] 素数 p: {self.p}) print(f[公共参数] 生成元 g: {self.g}) print(f[公共参数] 阶 q: {self.q}) def get_parameters(self): return self.p, self.g, self.q4.2 实现证明者Prover类证明者需要生成密钥对并能够执行协议的三步流程。class Prover: Schnorr协议的证明者。 def __init__(self, params): 初始化证明者生成私钥和公钥。 :param params: SchnorrPublicParameters 实例 self.params params self.p, self.g, self.q params.get_parameters() # 生成私钥 x一个在 [1, q-1] 范围内的随机数 self.secret_key secrets.randbelow(self.q - 1) 1 # 计算公钥 y g^x mod p self.public_key pow(self.g, self.secret_key, self.p) print(f[证明者] 私钥 x (保密): {self.secret_key}) print(f[证明者] 公钥 y: {self.public_key}) def get_public_key(self): return self.public_key def generate_commitment(self): 第一步生成承诺。 随机选择 r计算 R g^r mod p。 :return: 承诺值 R self.r secrets.randbelow(self.q - 1) 1 # 临时秘密 r self.R pow(self.g, self.r, self.p) # 承诺 R print(f[证明者] 生成临时秘密 r: {self.r}) print(f[证明者] 计算承诺 R g^r mod p: {self.R}) return self.R def generate_response(self, challenge): 第三步生成响应。 计算 s r challenge * secret_key mod q。 :param challenge: 验证者发来的挑战值 c :return: 响应值 s # 注意这里的运算是模 q而不是模 p。 self.s (self.r challenge * self.secret_key) % self.q print(f[证明者] 收到挑战 c: {challenge}) print(f[证明者] 计算响应 s r c*x mod q: {self.s}) return self.s4.3 实现验证者Verifier类验证者持有公钥负责发起挑战并验证最终的响应。class Verifier: Schnorr协议的验证者。 def __init__(self, params, public_key): 初始化验证者。 :param params: SchnorrPublicParameters 实例 :param public_key: 证明者的公钥 y self.params params self.p, self.g, self.q params.get_parameters() self.public_key public_key print(f[验证者] 已知公钥 y: {self.public_key}) def generate_challenge(self): 第二步生成挑战。 随机选择一个挑战值 c。 在实际协议中c的范围通常是 [0, 2^t - 1]其中t是安全参数如256。 这里我们简化在 [0, q-1] 范围内随机选取。 :return: 挑战值 c self.challenge secrets.randbelow(self.q) print(f[验证者] 生成随机挑战 c: {self.challenge}) return self.challenge def verify(self, commitment, response): 验证步骤。 检查 g^s mod p R * y^c mod p 是否成立。 :param commitment: 证明者发来的承诺 R :param response: 证明者发来的响应 s :return: 验证结果 True/False print(f[验证者] 收到承诺 R: {commitment}) print(f[验证者] 收到响应 s: {response}) print(f[验证者] 开始验证: g^s mod p R * y^c mod p ?) left_side pow(self.g, response, self.p) right_side (commitment * pow(self.public_key, self.challenge, self.p)) % self.p print(f[验证者] 计算左边 g^s mod p: {left_side}) print(f[验证者] 计算右边 R * y^c mod p: {right_side}) if left_side right_side: print([验证者] 验证成功证明者确实知道私钥。) return True else: print([验证者] 验证失败证明可能无效。) return False4.4 整合与模拟完整协议流程最后我们写一个主函数来模拟Alice和Bob的一次完整交互。def main(): 模拟一次完整的Schnorr身份识别协议。 print( 开始 Schnorr 零知识证明协议模拟 \n) # 1. 建立公共参数双方共享 print(阶段1: 建立公共参数) params SchnorrPublicParameters() print() # 2. 证明者Alice生成密钥对 print(阶段2: 证明者生成密钥) alice Prover(params) print() # 3. 验证者Bob获取Alice的公钥 print(阶段3: 验证者初始化) bob Verifier(params, alice.get_public_key()) print() # 4. 协议交互开始 print(阶段4: 协议交互) # 4.1 Alice生成并发送承诺 R R alice.generate_commitment() print() # 4.2 Bob生成并发送挑战 c c bob.generate_challenge() print() # 4.3 Alice计算并发送响应 s s alice.generate_response(c) print() # 4.4 Bob进行验证 verification_result bob.verify(R, s) print() print(f 协议结束验证结果: {verification_result} ) if __name__ __main__: main()将以上所有代码块按顺序保存到一个Python文件例如schnorr_zkp.py并运行你就能在控制台看到一次完整的Schnorr协议交互过程。你会看到每一步的计算结果并最终看到验证成功或失败的输出。5. 关键细节剖析与安全强化讨论代码跑通了但里面有很多为了教学而简化的地方。一个真正可用的系统需要考虑更多。5.1 群的选择从模乘群到椭圆曲线我们的示例使用了素数模乘法群(Z/pZ)*。这在理论上是可行的但效率和安全性与现代标准有差距。效率问题为了达到足够的安全性例如128位安全强度素数p需要非常大约3000位导致模幂运算非常缓慢。现代实践实际的Schnorr签名如比特币的Taproot和许多零知识证明系统都建立在椭圆曲线群之上。椭圆曲线群能在更短的密钥长度如256位下提供同等的安全性计算效率也高得多。例如SECP256K1曲线就是比特币使用的。如何升级在Python中你可以使用cryptography库或ecdsa库来操作椭圆曲线。生成元点、点乘运算对应我们的模幂都有现成的、高度优化的函数。将上述代码中的pow(g, x, p)替换为椭圆曲线的标量乘法x * G将模乘法替换为点的加法协议的逻辑完全不变。5.2 随机数的质量secrets与random我们代码中使用了secrets.randbelow()来生成私钥x和临时秘密r。这是正确的因为secrets模块旨在生成密码学安全的随机数能抵御攻击者预测。绝对禁止使用random.randint()或random.getrandbits()来生成密码学密钥或nonce。这些函数生成的随机数可能具有可预测性会彻底破坏系统的安全性。临时秘密r的重要性r必须是一次性的且绝对保密。如果同一个r被用于两个不同的挑战c攻击者就能通过联立方程解出私钥x。这就是为什么它被称为“nonce”Number used ONCE。5.3 挑战值的空间与“承诺-挑战”模式的不可篡改性挑战c必须来自一个足够大的空间比如256位并且是验证者在收到承诺R之后才随机生成的。这个顺序至关重要。如果顺序颠倒即证明者先知道c再生成R那么证明者就可以作弊。她可以随机选一个s然后计算R g^s * y^(-c) mod p来通过验证而她根本不知道x。挑战空间大小如果c的取值范围太小比如只有0和1那么攻击者即使不知道x也有50%的概率猜对c并提前准备好有效的R和s。通过多次重复协议每次用新的随机r可以将成功欺骗的概率降到极低。这就是为什么交互式证明有时需要重复多轮。5.4 从交互式到非交互式Fiat-Shamir启发式我们的实现是交互式的需要证明者和验证者在线来回通信。在实际应用中如区块链交易签名我们更需要非交互式证明即证明者可以独立生成一个证明字符串任何验证者稍后都可以验证它。Fiat-Shamir变换是实现这一点的经典技术。其核心思想是证明者自己来模拟挑战c的生成但不是随机生成而是将承诺R和要证明的陈述比如公钥y和某个消息m一起通过一个密码学哈希函数如SHA256来计算c Hash(R || y || m)。这样做的意义哈希函数是确定性的且扮演了“随机预言机”的角色。只要哈希函数是安全的那么c就相当于一个不可预测的、由R和上下文决定的“随机”挑战。证明者就可以在不与验证者交互的情况下完成承诺、挑战自生成、响应的全过程最终输出(R, s)作为签名或证明。代码修改在非交互式版本中Prover类会有一个sign(message)方法内部计算c hash_to_int(R, public_key, message)然后计算s。验证者Verifier的verify(message, signature)方法会使用相同的哈希函数从收到的R和消息中重新计算出c然后进行同样的验证等式检查。6. 常见问题与调试技巧实录在亲手实现和运行代码的过程中你可能会遇到一些典型问题。这里记录了我踩过的一些坑和解决方法。6.1 验证等式不成立这是最常见的问题。请按以下顺序排查检查模数是否一致这是最隐蔽的错误。在计算响应s r c*x时我们是在模q群的阶下运算。而在验证等式g^s mod p中是在模p下运算。务必确保s的计算使用了正确的模数q。在我们的简化模型中q p - 1但这不是普遍真理。在椭圆曲线群中q是曲线的阶一个与p不同的素数。症状左右两边数值相差巨大或者看起来毫无关系。解决仔细检查Prover.generate_response方法中的% self.q和Verifier.verify方法中的% self.p。确保它们不同。检查随机数范围私钥x和临时秘密r必须在[1, q-1]范围内因为0会导致公钥或承诺为1失去安全性。如果错误地使用了模p的范围可能会导致计算错误。症状偶尔验证失败尤其是当随机数生成边界错误时。解决确认代码中使用的是secrets.randbelow(self.q - 1) 1。打印调试像我们的示例代码一样在每一个计算步骤后打印出中间变量r,R,c,s,left_side,right_side。手动用计算器验证一到两个步骤看是否与代码输出一致。6.2 性能问题与参数选择问题当素数p很大时比如尝试1024位密钥生成和模幂运算会变得非常慢。解决教学演示将generate_prime(bits64)中的bits调小例如改为32或48可以快速看到结果。切记这只是为了演示。进阶探索转向椭圆曲线库。安装cryptography(pip install cryptography)使用其内置的椭圆曲线如SECP256R1。你会发现在同等安全级别下256位的椭圆曲线运算比2048位的模乘运算快几个数量级。6.3 理解“零知识”的模拟过程你可能对“Bob能自己模拟对话记录”这一点感到困惑。可以尝试在代码中添加一个模拟器函数def simulate_proof(params, public_key): 验证者在不与证明者交互的情况下模拟一个有效的证明三元组 (R, c, s)。 p, g, q params.get_parameters() y public_key # 1. 随机生成挑战和响应顺序和真实协议相反 c_simulated secrets.randbelow(q) s_simulated secrets.randbelow(q) # 2. 反向计算承诺 R g^s * y^(-c) mod p # 计算 y^(-c) mod p即 y^{p-1-c} mod p但更简单的方法是求逆元 y_inv pow(y, p-2, p) # 费马小定理求逆因为p是素数 R_simulated (pow(g, s_simulated, p) * pow(y_inv, c_simulated, p)) % p print(f[模拟器] 生成的随机挑战 c: {c_simulated}) print(f[模拟器] 生成的随机响应 s: {s_simulated}) print(f[模拟器] 反向计算的承诺 R g^s * y^(-c): {R_simulated}) print(f[模拟器] 三元组 (R, c, s) 看起来和真实交互生成的一模一样。) # 验证这个模拟的三元组是否能通过验证 left pow(g, s_simulated, p) right (R_simulated * pow(y, c_simulated, p)) % p print(f[模拟器] 验证模拟的证明: g^s mod p R * y^c mod p ? {left right}) return R_simulated, c_simulated, s_simulated在主函数中调用这个模拟器并对比真实交互生成的三元组。你会发现从数据分布上看两者无法区分。这就是“零知识”的直观体现验证者看到的对话记录他自己也能造出来所以这个记录里不包含任何关于秘密x的“知识”。通过这个从理论到代码再从代码回溯理论的完整循环你应该对Schnorr协议如何作为零知识证明系统运作有了扎实的理解。它就像一块密码学的乐高积木是构建更复杂隐私保护系统的核心组件。