第 7 章:VSS 与分布式密钥生成(DKG)
学习定位:前置为 Shamir 和 FROST;本章先做同步、内存内 DKG。读完第 8 章后,再在综合项目中加入乱序、重放、持久化和崩溃。
可信 Dealer 能方便地生成 Shamir 份额,却是明显的单点:它曾经知道完整私钥,也可能给不同参与者发送不一致份额。可验证秘密共享(VSS)让接收者验证份额确实来自某个已承诺的多项式;分布式密钥生成(DKG)再让每名参与者都贡献一个多项式,使最终私钥从未在任何单一位置出现。
本章先实现教学型 Feldman VSS,再把多个 VSS 实例组合成 DKG。它能建立正确的心智模型,但“投诉与仲裁”的具体安全性高度依赖所选 DKG 协议、网络模型和广播原语。生产实现必须选择并严格实现一份明确规范,不能把本章流程图当作完整安全证明。
学习目标
完成本章后,你应该能够:
- 解释普通 Shamir 分享为什么无法让接收者验证 Dealer;
- 推导 Feldman VSS 的份额验证等式;
- 区分 Feldman 的计算隐藏性与 Pedersen 承诺的隐藏目标;
- 描述 DKG 中每名参与者同时作为 Dealer 和接收者的过程;
- 计算最终密钥份额、公开验证份额与组公钥;
- 明确认证私密信道、一致广播、同步模型和腐败阈值等假设;
- 解释投诉、响应、资格集合和公开份额泄露之间的关系;
- 在 Rust 中实现可注入恶意行为的本地 DKG 模拟器。
7.1 Shamir 还缺少什么
一个 Dealer 选择次数至多为 \(t-1\) 的多项式:
\[ f(u)=a_0+a_1u+\cdots+a_{t-1}u^{t-1}\pmod q \]
并向参与者 \(i\) 私下发送:
\[ s_i=f(i) \]
普通 Shamir 提供恢复门限和信息论保密性,却不回答:
- Dealer 发来的 \(s_i\) 是否真的位于一个次数小于 \(t\) 的共同多项式上?
- Dealer 是否给不同人承诺了不同多项式?
- 某个接收者声称“份额无效”时,究竟是谁撒谎?
- Dealer 是否选择了不符合协议的常数项或退化多项式?
VSS 的目的,就是让份额具有可验证的一致性证据。
7.2 Feldman VSS
Dealer 为每个多项式系数发布曲线点承诺:
\[ A_k=a_kG,\qquad k=0,1,\ldots,t-1 \]
参与者 \(i\) 收到私密份额 \(s_i\) 后检查:
\[ s_iG\stackrel?=\sum_{k=0}^{t-1}i^kA_k \]
正确性来自:
\[ \begin{aligned} s_iG &=f(i)G\\ &=\left(\sum_{k=0}^{t-1}a_ki^k\right)G\\ &=\sum_{k=0}^{t-1}i^k(a_kG)\\ &=\sum_{k=0}^{t-1}i^kA_k \end{aligned} \]
其中 \(A_0=a_0G\) 是该 Dealer 的公开密钥贡献。
Feldman 能保证什么
- 一旦一组承诺被全体一致接收,Dealer 很难为同一个 ID 找到两个不同且都通过验证的份额;
- 每个接收者可以本地验证自己的份额;
- 后续可以从承诺计算公开验证份额,而不公开私密份额。
Feldman 不能单独保证什么
- 承诺一致广播。 恶意 Dealer 可能向不同人发送不同的 \(A_k\);需要认证的一致/可靠广播阻止分叉。
- 无条件隐藏。 \(A_k=a_kG\) 的隐藏性依赖离散对数困难,不是信息论隐藏承诺。
- 自动仲裁投诉。 等式只能判定给定份额与给定承诺是否匹配,不能证明私聊中最初发送了哪些字节。
- 可用性。 Dealer 或接收者仍可离线、拒绝发送或制造投诉。
Pedersen VSS 通常使用额外随机多项式和第二生成元 \(H\) 来获得隐藏承诺。在循环群中总存在 \(H=hG\),关键不是“代数独立”,而是任何参与者都不知道 \(h=\log_G H\);否则知道该关系的人可以为同一承诺构造等价开口,破坏绑定性。\(H\) 应按具体协议通过可验证的 hash-to-group 或规定的生成过程得到。本项目应先吃透 Feldman,再选定一份具体 DKG 规范决定是否引入 Pedersen;不要自行把两套公式拼接。
7.3 从多个 VSS 组合出 DKG
设有 \(n\) 个参与者,每个参与者 \(j\) 都充当一次 Dealer。
参与者 \(j\) 选择:
\[ f_j(u)=a_{j,0}+a_{j,1}u+\cdots+a_{j,t-1}u^{t-1} \]
它一致广播系数承诺:
\[ A_{j,k}=a_{j,k}G \]
并通过认证且保密的点对点信道向参与者 \(i\) 发送:
\[ s_{j\rightarrow i}=f_j(i) \]
接收者 \(i\) 对每个 Dealer \(j\) 验证:
\[ s_{j\rightarrow i}G \stackrel?= \sum_{k=0}^{t-1}i^kA_{j,k} \]
投诉和解决阶段结束后,全体诚实参与者必须得到同一个合格 Dealer 集合 \(Q\)。最终,参与者 \(i\) 的私密份额为:
\[ s_i=\sum_{j\in Q}s_{j\rightarrow i}\pmod q \]
组私钥在数学上等于:
\[ x=\sum_{j\in Q}a_{j,0}\pmod q \]
但协议中任何人都不应计算或记录这个 \(x\)。组公钥直接由公开承诺得到:
\[ Y=\sum_{j\in Q}A_{j,0} \]
参与者 \(i\) 的公开验证份额为:
\[ \begin{aligned} Y_i &=s_iG\\ &=\sum_{j\in Q}\sum_{k=0}^{t-1}i^kA_{j,k} \end{aligned} \]
这正是 FROST 签名阶段需要的 (group public key, signing share, verification share) 密钥包。
定稿前必须按目标协议验证所有公开元素,并至少确认组公钥 \(Y\ne\mathcal O\)。若 \(Y=\mathcal O\),普通 Schnorr 验证退化为 \(zG=R\),任何人都能任选 \(z\) 并令 \(R=zG\) 伪造签名;本次 DKG 必须失败关闭,并按所选 DKG 规范用新会话和新随机性重新开始。
不要把 FROST 签名元素与 VSS 系数承诺当成同一种解析类型:RFC 9591 的签名公钥、nonce commitment 和最终 \(R\) 按套件拒绝单位元;而 VSS/DKG 系数允许标量系数为零,对应的系数承诺可能是单位元。后者必须使用所选 VSS/DKG 协议自己的 CoefficientCommitment 类型和编码规则,在协议允许的位置接受单位元,同时仍严格拒绝非法编码。不能把一个层的 identity 规则机械套到另一个层。
从某个对手联盟的视角看,只要合格集合中至少有一个常数贡献对该联盟仍然不可预测且未泄露,它就不能计算最终组私钥。这不是“任何单方都必然不知道 \(x\)”:知识取决于该方掌握哪些 Dealer 贡献;若 \(Q\) 只剩一个 Dealer,该 Dealer 就知道完整 \(x\)。具体 DKG 必须用明确的腐败阈值、合格集合条件和安全证明说明哪些联盟不知道密钥。更强的抗偏置、终止和鲁棒性结论也不能只从“有一个诚实 Dealer”推出。
7.4 DKG 的阶段与不可跳过的屏障
一个教学状态机可以划分为:
- 登记阶段:冻结组 ID、epoch、参与者身份、公钥、标量 ID、\(n\)、\(t\)、密码套件;
- 承诺阶段:每个 Dealer 产生多项式并一致广播系数承诺;
- 私密分发阶段:每个 Dealer 向每个接收者发送加密且认证的份额;
- 验证阶段:接收者针对已冻结的承诺验证所有份额;
- 投诉阶段:广播结构化投诉,不允许只在私聊中说“有问题”;
- 解决阶段:按所选协议提交响应证据,确定虚假投诉或无效 Dealer;
- 资格阶段:用确定性规则冻结共同集合 \(Q\);
- 定稿阶段:各自计算 \(s_i,Y_i,Y\),删除临时 Dealer 多项式和收到的分量份额;
- 确认阶段:证明各方得到同一组公钥和 roster/epoch,再允许进入签名。
每一阶段都应绑定前一阶段 transcript 的摘要。不能在一部分人仍收承诺时,让另一部分人先公布投诉;否则恶意方可能根据后来信息自适应改变自己的输入。
7.5 投诉与解决:最容易被低估的部分
若参与者 \(i\) 发现来自 Dealer \(j\) 的份额无效或未收到份额,应发布包含以下上下文的投诉:
- DKG 会话 ID、组 ID 和 epoch;
- Dealer 与接收者身份;
- 被引用的承诺集合摘要;
- 投诉类型:缺失、解码失败、认证失败或 VSS 等式失败;
- 所选规范要求的证据。
之后如何解决,必须由具体协议规定。一个常见的教学流程是 Dealer 对争议份额作认证广播,所有人按已冻结承诺验证:
- 若公开份额不满足等式,Dealer 被剔除;
- 若份额满足等式,则投诉者可能提交了虚假投诉;
- 若 Dealer 不按时响应,则按规则剔除。
但这里有严重的泄露预算:公开一个 \(s_{j\rightarrow i}\) 就公开了 Dealer 多项式上的一个点;攻击者累计知道达到 \(t\) 个不同点,即可重构该 Dealer 的常数贡献。预算不能只数公开投诉:若攻击者已经控制 \(f<t\) 个接收者并持有它们的私密份额,再公开 \(t-f\) 个新的不同份额就已足够。历史投诉、重试和日志中的点也必须累计计算。不同 DKG 协议会通过投诉上限、Dealer 剔除、加密证据、可验证加密或不同的分享结构处理这个问题。实现者不能简单规定“所有争议份额都上链/广播”,更不能无限重试同一多项式。
同样,采用“只要有人投诉就剔除 Dealer”的简化规则,会让恶意接收者任意排除诚实贡献,可能影响活性和输出偏置。教学模拟可以实现这种模式来观察问题,但必须命名为不安全策略,不能作为正式 DKG。
投诉流程必须解决三个目标之间的张力:
- 判断 Dealer 与接收者谁违反协议;
- 让所有诚实参与者得到相同的 \(Q\);
- 不因公开过多份额破坏秘密性。
7.6 必须明确写进设计的假设
身份和信道
- 参与者 roster 在会话开始前已认证并冻结;
- 广播消息具备来源认证、完整性与一致性,不能只靠普通群聊;
- 私密份额通过同时具备机密性和认证性的点对点信道发送;
- 每条消息绑定 session、epoch、sender、receiver、round 和 transcript 摘要。
网络模型
协议是同步、部分同步还是异步,会改变超时、投诉和终止规则。“十秒未收到就剔除”只是同步假设下的策略;网络分区时可能让诚实节点得到不同资格集合。
对手模型
要区分:
- 签名门限 \(t\);
- 参与者总数 \(n\);
- 最多恶意或被攻破的参与者数 \(f\);
- 对手是静态腐败还是可以在运行中自适应腐败;
- 对手能否读取崩溃后的磁盘或历史备份。
Shamir 层面少于 \(t\) 份不能恢复秘密,不代表任意 DKG 都能在 \(f<t\) 时保持一致和终止。不同同步模型与 DKG 协议可能要求 \(n>2f\)、\(n>3f\) 或其他条件。选择实现规范时必须原样记录它的限制,不要从本章推导一个通用比例。
密码学假设
- 所选群中的离散对数问题困难;
- 点和标量解码严格且规范;
- 随机数生成器安全,参与者之间的随机贡献独立;
- 哈希及域分离满足所选协议的模型;
- 临时多项式系数和收到的分量份额能在定稿后安全删除。
7.7 Rust 动手实践:先做可观察模拟器
本章只要求单进程、同步、内存内、确定性事件队列。参与者仍各自持有独立状态,但不实现真实 socket、TLS、数据库或超时器。这样可以先验证 VSS、投诉和共同资格集合的协议逻辑。
乱序/重放防护、持久化、并发与崩溃恢复依赖第 8 章的工程模型,应在读完第 8 章后放入实验 5 和综合项目验收;不要在尚未定义 envelope 与状态机时自行发明网络行为。
任务一:实现 Feldman VSS
建议的领域边界:
#![allow(unused)]
fn main() {
struct DealerPolynomial(Vec<SecretScalar>);
struct CoefficientCommitments(Vec<GroupElement>);
struct Share {
dealer: ParticipantId,
receiver: ParticipantId,
value: SecretScalar,
}
fn verify_vss_share(
receiver: ParticipantId,
share: &SecretScalar,
commitments: &CoefficientCommitments,
) -> bool;
}
测试:
- 所有正确份额通过;
- 改动份额一个标量单位后失败;
- 改动任一承诺后相关验证失败;
- 承诺数量不等于 \(t\)、ID 为零、点解码失败时拒绝;
- 用同一个多项式的任意 \(t\) 份可重构常数项,而 \(t-1\) 份不能唯一确定它。
最后一项最好先在小素数域中演示信息论不确定性,再回到真实曲线实现。
任务二:实现消息驱动的 DKG 状态机
不要写一个能直接访问所有参与者秘密的“大上帝函数”。为每个参与者建立独立状态,并通过显式消息驱动:
Registered
-> CommitmentsCollected
-> SharesDistributed
-> ComplaintsFrozen
-> ResolutionsFrozen
-> QualifiedSetFrozen
-> Finalized | Aborted
测试工具可以检查全局秘密来断言正确性,但生产模块不能暴露重构完整私钥的接口。
任务三:注入恶意行为
为模拟网络加入故障脚本:
- Dealer 给一个接收者发送错误份额;
- Dealer 对不同参与者广播不同承诺;
- Dealer 漏发一个份额;
- 接收者提出虚假投诉;
- Dealer 在解决阶段沉默;
- 两个节点尝试冻结不同的 \(Q\)。
- 多个 Dealer 的常数项承诺相消,使最终 \(Y=\mathcal O\)。
每次实验记录:谁能检测、需要什么证据、协议应继续还是中止、是否泄露了份额、所有诚实节点最终是否得到同一 \(Y\)。
读完第 8 章后再扩展事件队列,注入重复、乱序、旧 session 消息和崩溃;这些属于综合项目的工程验收,不是本章核心 DKG 公式的过关条件。
任务四:接入上一章 FROST
DKG 成功后,为每个参与者只输出:
#![allow(unused)]
fn main() {
struct KeyPackage {
ciphersuite: CiphersuiteId,
group_id: GroupId,
epoch: u64,
threshold: usize,
roster: Vec<ParticipantId>, // 严格递增、唯一、已验证
participant: ParticipantId,
signing_share: SecretShare,
verification_share: GroupElement,
group_public_key: GroupPublicKey, // 已验证且非单位元
roster_hash: [u8; 32],
}
}
让第 6 章的所有 2-of-3 FROST 测试在这些 DKG 输出上重新通过。额外断言所有参与者的 ciphersuite、group_public_key、threshold、有序 roster、roster_hash 和 epoch 完全相同。
7.8 份额刷新与成员变更不是同一件事
主动份额刷新通常让参与者贡献常数项为零的多项式:
\[ g_j(0)=0 \]
把新贡献加入旧份额后,组私钥和组公钥不变。诚实节点应立即停用并销毁旧份额,但旧份额并不会在密码学意义上失去签名能力:如果攻击者已经取得某一旧 epoch 的达到门限份额集合,它仍可为同一个组公钥生成有效签名,普通验证者无法从签名中识别 epoch。
刷新承诺需要单独的 identity 规则。因为 \(g_j(0)=0\),其常数项承诺必须是单位元;这在 RefreshCoefficientCommitment 中是合法且应被验证的性质,不能使用会拒绝单位元的 FROST 签名元素解析器。其他系数也应按刷新协议的规则编码和验证。
投诉泄露预算也更紧:次数至多为 \(t-1\) 且常数项已知为零的刷新多项式,只需 \(t-1\) 个不同非零点即可完全重构。若对手已控制 \(f<t-1\) 个接收者,再公开 \(t-1-f\) 个新的不同刷新份额就足够;若 \(f\ge t-1\),它已经拥有足够点。历史投诉与重试同样要累计。不要把普通 DKG 中的 \(t-f\) 预算直接复用到 refresh。
刷新只有在每个时期被攻破的份额始终少于门限、旧份额和刷新临时量确实及时删除时,才有助于抵抗跨时期逐个攻破参与者的移动对手。epoch 能约束诚实状态机并防止误混新旧份额,却不能撤销已经泄露的旧 quorum。若旧份额可能已达到门限泄露,应生成新的组密钥并让所有验证方迁移,而不是只做刷新。
成员增删、门限变化或把份额迁移给新 roster 属于 resharing,涉及旧组对新组的授权、身份映射和可用性假设。它不能简化为“给新成员复制一个份额”,也不应与零常数刷新共用含糊 API。若 resharing 保持原组公钥,达到旧门限的旧份额集合仍然具有签名能力;需要密码学意义上的撤销时,应迁移到新组公钥。
任何 DKG、刷新或 resharing 都应递增 epoch。应用层认证 envelope 和本地密钥包必须绑定 epoch,防止诚实节点混用不同时期的份额和验证份额;RFC 9591 的 \(H_1/H_2\) 本身不包含 epoch。若最终验证者也必须验证 epoch,它必须进入待签消息。
常见错误
- 把
A_0当成秘密。 它是 Dealer 的公开密钥贡献;秘密的是对应标量。 - 只验证最终组公钥。 单个坏份额可能让某参与者以后无法签名,必须逐 Dealer 验证。
- 用普通广播模拟一致广播。 恶意发送者可以向不同节点展示不同承诺。
- 私密发送但不认证。 加密不能证明份额来自声明的 Dealer,也不能防篡改。
- 无限公开争议份额。 攻击者累计掌握达到 \(t\) 个不同份额(包括已攻破节点的私密份额)就会重构 Dealer 的贡献。
- 每个节点自己决定超时和资格集合。 这会产生不同 \(Q\) 和不同组公钥。
- DKG 后重构完整私钥做“校验”。 应用公开等式与门限签名验证,而不是制造单点秘密。
- 假设
f<t自动保证 DKG 活性。 具体容错界取决于协议和网络模型。 - 保留所有临时多项式方便调试。 这扩大历史密钥泄露面,日志和崩溃转储尤其危险。
- 把刷新当作成员变更。 两者的安全目标、消息和 epoch 语义不同。
- 接受单位元组公钥。 它会让 Schnorr 验证失去不可伪造性;定稿必须拒绝。
- 所有公开点共用一种解析类型。 FROST 签名元素拒绝单位元,refresh 常数项承诺却必须是单位元;两层需要各自的类型和规则。
- refresh 沿用 DKG 的投诉泄露预算。 已知零常数项会把重构所需的非零点数从 \(t\) 降为 \(t-1\)。
本章检查题
- 推导 Feldman 份额验证等式,并说明验证者为何不需要知道多项式系数。
- 如果 Dealer 向 Alice 和 Bob 发送不同承诺列表,即便两人的份额都各自验证通过,会发生什么?
- DKG 中最终组公钥如何从公开数据计算?为何不需要重构组私钥?
- 为什么所有诚实参与者必须冻结完全相同的资格集合 \(Q\)?
- 若攻击者已控制 \(f\) 个接收者,还需看到多少个同一 Dealer 的额外不同有效份额,就能重构次数为 \(t-1\) 的多项式?
- “少于 \(t\) 人不知道私钥”和“协议在 \(f\) 个恶意节点下会终止”为什么是不同结论?
- 一个虚假投诉可能造成哪些安全性或可用性影响?
- 份额刷新为什么可以保持组公钥不变?
完成标准
当本地模拟器能检测错误份额和承诺分叉,能按确定性规则让所有诚实节点得到相同的 \(Q\)、\(Y\) 与验证份额,并能把 DKG 输出直接用于 FROST 测试时,本章学习目标已经达到。进入真实网络前,还必须为所选 DKG 规范写清对手界、网络假设、广播实现和投诉泄露预算。