实验 06:综合项目验收
本实验不再引入新的公式,而是把前五个实验组合成一套边界清楚、可以重复演示的本地 2-of-3 FROST 学习系统。
综合项目通过不代表生产安全。最终产物仍应醒目标记为教育实现,不得承载真实资产或生产身份。
本项目只要求单进程事件队列和可注入崩溃的教学存储。真实网络/TLS、生产数据库、HSM、侧信道保证、云部署和真实资产全部排除在验收范围外。RFC 9591 字节互操作作为单独加分项,不与核心公式正确性混为一个结论。
最终目标
三个参与者 Alice、Bob、Carol 各自持有一份长期密钥份额。任意两人可以在不恢复完整私钥的情况下,对一条明确的消息生成普通 Schnorr 签名。
系统必须同时展示四类结果:
- 正确路径:三个合法
2-of-3子集都能签名; - 错误路径:篡改、重复 ID、非法编码和坏份额被准确拒绝;
- 攻击路径:测试专用代码复现 nonce 重用泄露;
- 恢复路径:在关键持久化边界崩溃后,nonce 不会跨 transcript 重用。
先用可信 Dealer 完成核心里程碑;完整毕业项目必须再把密钥来源替换为第 7 章的本地 DKG 模拟器。两者分开验收,避免同时调试密钥生成与签名公式。
两个完成级别
- Level 1 — 核心里程碑:可信 Dealer → 三个
2-of-3子集完成 FROST → 对抗性测试。 - Level 2 — 完整毕业项目:本地同步 DKG → FROST → 持久 nonce 状态 → 崩溃/乱序/重放故障注入。
Level 1 已经是一套完整的门限签名学习成果;Level 2 才表示你把“无可信 Dealer”和协议工程也接入了同一条可验证主线。
前置闸门
开始综合项目前,确认:
- 玩具有限域的全部穷举测试通过;
- Scalar 版 Shamir 的所有
2-of-3子集通过; - 普通 Schnorr 已拒绝单位元公钥和单位元承诺;
- 普通 Schnorr nonce 重用实验能恢复测试私钥;
- FROST binding factor 和 Schnorr challenge 使用不同的域分离函数;
SigningNonces/ReservedSigningNonces不能被复制,且第二轮按值移动预留句柄,用持久化 compare-and-swap 完成消费;- 你能口头解释为什么这些条件仍不足以证明生产安全。
任一条件不满足,就先回到对应实验修复,不要在综合层用特殊分支掩盖底层错误。
建议的最终目录
src/
lib.rs
error.rs
toy_field.rs # 仅学习/测试,真实协议不依赖
suite.rs # Ristretto255/SHA-512 与规范编码
identifier.rs
transcript.rs # H1/H2/H3/H4/H5 或教学阶段的明确等价层
shamir.rs
schnorr.rs
frost/
mod.rs
types.rs
dealer.rs
round1.rs
round2.rs
aggregate.rs
state/
mod.rs
nonce_store.rs
signer.rs
examples/
two_of_three.rs
tests/
shamir_properties.rs
schnorr_negative.rs
frost_subsets.rs
frost_adversarial.rs
crash_recovery.rs
encoding.rs
ui.rs
tests/ui/
nonces_cannot_be_reused.rs
这只是职责划分,不要求第一天建立所有文件。一次只迁移一个已通过测试的模块。
核心 API 边界
最终实现至少应让下列对象不可混用:
ParticipantId
SigningShare secret, long-lived
VerifyingShare public
GroupPublicKey public, validated, non-identity
KeyPackage secret share + suite/group/epoch/roster/threshold
SigningNonces secret, one-time, move-only
NonceId / ReservedSigningNonces
SigningCommitments public, validated
CoefficientCommitment public, protocol-specific identity rules
SigningPackage message + ordered commitments
AuthenticatedSigningRequest / AuthenticatedSignatureShare
SignatureShare public protocol response
Signature public, validated
SessionId / GroupId / Epoch
设计检查:
SigningShare与普通Scalar不能随意互换;KeyPackage不可把秘密份额与 ciphersuite、group/epoch、有序 roster 或 threshold 分离使用;SigningNonces不实现Clone、Copy、明文Debug或通用序列化;- 公开 Round 2 入口只接受 nonce store 生成的
ReservedSigningNonces和通过消息认证的 request,不暴露只接受裸 package/nonce 的绕过路径; GroupPublicKey只能从验证过的非单位元点创建;CoefficientCommitment与 FROST 的SigningCommitments使用不同构造器;不要把签名层的“拒绝单位元”规则套给所有 VSS 系数;Signature的字段私有,解析器拒绝单位元R与非规范z;SigningPackage构造时要求承诺已按 ID 严格递增,并拒绝乱序、重复、未知 ID 和未达门限的输入;只有协调者内部从 map 建包的 builder 可在编码前显式排序;- RFC 9591 兼容路径还检查 participant identifier 位于
[1, MAX_PARTICIPANTS]; - 应用 envelope 的 session/epoch 不被擅自塞入 RFC 9591 的
H1/H2; - 若这些业务字段需要最终验证者确认,它们进入被签消息。
阶段 A:密钥材料
任务
用可信 Dealer 生成:
- 非零组私钥
x,但只允许存在于 keygen 的受限作用域; - 三份
2-of-3Shamir signing share; - 三份 verification share;
- 非单位元 group public key;
- roster、threshold、suite 和 key package 标识。
Dealer 返回以后,主签名 API 不得再提供恢复 x 的入口。
验收
- 任意两份在测试夹具中可验证对应同一个 group public key;
- 一份不能调用恢复 API;
- 重复/零 participant ID 被拒绝;
- 组公钥为单位元时 keygen 失败;
- key package 的 group、epoch、roster hash 完全一致。
阶段 A+:用本地 DKG 替换 Dealer
完成 Level 1 后,保持 FROST 签名 API 不变,只替换 KeyPackage 的来源。使用单进程同步事件队列模拟三个独立节点:
- 每个节点各自生成次数至多为 1 的随机多项式;
- 广播 Feldman 系数承诺;
- 向另外两名参与者发送私密分量份额;
- 每个接收者逐 Dealer 验证份额;
- 冻结投诉、响应和共同资格集合
Q; - 各自聚合自己的 signing share 与 verification share;
- 从常数项承诺计算共同、非单位元的 group public key;
- 删除临时 Dealer 多项式和分量份额;
- 把三个
KeyPackage原样交给阶段 B。
核心 DKG 验收:
- 三方得到相同的 group public key、roster hash、epoch 和资格集合;
Q满足所选教学 DKG 的最小合格集合条件;不能在只剩一个 Dealer、使其知道完整组私钥时仍宣称“分布式生成成功”;- 三方各自的 verification share 与 signing share 对应;
- 正常路径从不计算完整组私钥;
- 篡改一个私密份额会被指定接收者检测;
- 对不同接收者分叉承诺时,所有诚实节点按同一规则共同中止;
- 虚假投诉、Dealer 沉默或最终
Y为单位元时,共同中止而不是各自定稿; - DKG 输出继续通过阶段 B 的三个签名子集测试。
本阶段仍不实现真实网络。乱序、重放和崩溃由后面的事件队列故障注入统一验收。
阶段 B:两轮签名
Round 1
每位候选签名者:
- 按选定套件生成 hiding/binding nonce;
- 计算两个公开承诺;
- 在本地 nonce store 中原子预留给一个 session;
- 发布承诺;
- 从此不允许把该 nonce 对退回可用库存。
Round 2
每位最终签名者:
- 验证应用 envelope、自己的承诺和完整 signing package;
- 独立重算 binding factors、group commitment、challenge 和拉格朗日系数;
- 若任一输入点或聚合
R为单位元,失败并烧毁 nonce; - 先耐久记录消费状态与响应,再释放 signature share;
- 协调者验证每一份,再聚合;
- 用普通 Schnorr 验证器检查最终签名。
验收
对 {Alice,Bob}、{Alice,Carol}、{Bob,Carol} 分别运行:
- Round 1 使用全新 nonce;
- 每个 signature share 单独验证;
- 聚合签名由同一 group public key 验证;
- 最终签名字节不包含 roster、门限或签名者身份;
- 三组签名都不能访问完整私钥。
阶段 C:失败矩阵
为每一行建立独立测试。不要只断言 is_err(),还要断言错误发生在预期边界。
| 场景 | 最早拒绝位置 | nonce 结果 |
|---|---|---|
| 少于门限 | signing package 构造 | 未发布者可保留;已发布者烧毁 |
| participant ID 为 0/重复 | 解码或 package 构造 | 未使用 |
| 自己的承诺被替换 | 签名者接受 package 前 | 烧毁 |
| 消息与批准内容不同 | 应用策略检查 | 烧毁 |
| commitment list 乱序 | 规范解析 | 烧毁或按 API 约定拒绝前不发布 |
| 输入点为单位元 | 点解析/验证 | 烧毁 |
聚合 R 为单位元 | challenge 计算前 | 全部烧毁 |
| signature share 被修改 | 聚合前份额验证 | 已消费,不能重试 |
| nonce 为 A 预留,却收到 session B 的签名请求 | 签名者 envelope / nonce store 前置检查 | A 的记录烧毁,不改绑 B |
| share envelope 来自另一 session | 协调者 envelope / session store 前置检查 | 不触碰签名者 nonce store |
| 最终签名验证失败 | 发布签名前 | 已消费,创建新会话 |
同一种错误在不同状态发生时,nonce 处理可能不同;测试名称应把状态写出来。
阶段 D:崩溃恢复
为签名者建立最小状态:
Fresh -> Reserved -> CommitmentPublished
-> ConsumedAndResponsePersisted -> Completed
\-> Burned
ConsumedAndResponsePersisted 是一次原子事务,不是先保存 response、后标记 consumed 的两个可崩溃步骤。Completed 只表示传输/确认完成,nonce 在前一状态就已不可再用。
在下面每个边界注入崩溃:
- nonce 生成后、预留前;
- 预留提交后、承诺发送前;
- 承诺发送后、package 到达前;
- response 计算后、消费事务提交前;
- 消费事务提交后、response 发送前;
- response 发送后、确认前;
- 从旧数据库快照启动;
- 两个进程同时争抢同一个 nonce。
每个恢复测试必须证明:
- 同一 nonce ID 不会对应两个不同 transcript;
- 已发布或状态不确定的 nonce 不会回到库存;
- 允许幂等重发时,只重发已持久化的完全相同响应;
- 无法证明安全时中止,而不是重新计算。
阶段 E:标准对照
把“协议逻辑正确”和“RFC 9591 字节兼容”分成两个独立标签。
edu
- 公式、类型和失败测试正确;
- 可能使用自定义域标签或简化线格式;
- 不声称与其他实现互操作。
rfc9591-conformant-study
- 锁定 RFC 9591 的 Ristretto255/SHA-512 ciphersuite;
- 实现规定的
H1至H5、nonce_generate、序列化和输入验证; - participant identifier 是
[1, MAX_PARTICIPANTS]内的NonZeroScalar,且互不重复; - 官方 ristretto255 测试向量逐项一致;
- 与成熟实现做差分测试;
- 仍然保留“未经审计、禁止生产使用”的声明。
不要为了让最终签名“碰巧验证”而跳过中间值对照。至少比较:
- nonce commitments;
- binding factors;
- group commitment;
- challenge;
- 每个 signature share;
- 最终签名字节。
核心验收矩阵
| 能力 | 必须提供的证据 |
|---|---|
| 数学正确性 | 手算例与性质测试得到相同结果 |
| DKG 正常流程 | 三个节点得到同一非单位元公钥,正常路径从不重构完整私钥 |
| DKG 恶意输入 | 错误份额、承诺分叉、虚假投诉导致共同剔除或共同中止 |
三个 2-of-3 子集 | 三组独立成功测试 |
| 普通验证兼容 | 聚合签名由单方 Schnorr verifier 接受 |
| 不重构私钥 | 正常签名路径无 combine/recover 调用 |
| 份额可归责 | 修改每个 z_i 都定位到对应 ID |
| identity 防护 | X=O、输入 commitment =O、聚合 R=O 均有负例 |
| nonce 一次性 | 编译期 move 测试 + 持久化/崩溃测试 |
| transcript 唯一 | 黄金字节向量、乱序/重复/长度歧义负例 |
| 会话隔离 | 跨 session、group、epoch 消息全部拒绝 |
| 解析健壮性 | 任意畸形字节返回错误且不 panic |
| 安全边界 | README 和实验报告明确未覆盖项 |
RFC 9591 对照不在上表的核心毕业门槛中。只有选择 rfc9591-conformant-study 加分路线时,才额外要求官方向量逐项一致并与成熟实现完成差分测试。
演示程序
examples/two_of_three.rs 只演示公开流程,不打印秘密:
1. 显示 group public key 指纹和 roster
2. 选择 Alice + Carol
3. 显示两轮中的公开 commitment
4. 显示每个 signature share 的验证结果
5. 显示最终签名与普通验证结果
6. 篡改消息并显示验证失败
禁止输出 signing share、nonce、Dealer 多项式或完整私钥。若需要攻击演示,使用单独的 #[cfg(test)] 夹具和固定玩具参数。
项目报告
综合项目完成时,提交一份短报告,至少回答:
- 你的实现采用哪个确切密码套件和版本?
- 哪些模块是自己实现的,哪些基础原语来自库?
- 正常路径中是否任何时刻出现完整私钥?
- nonce 在发布承诺、崩溃和重试后如何处理?
- 哪些网络和身份假设尚未实现?
- 哪三个测试最可能阻止真实事故?为什么?
- 与 RFC 9591 测试向量还有哪些差异?
- 如果旧 epoch 的完整份额集合泄露,为什么 refresh 不能撤销它?
- 当前项目最不确定的安全结论是什么?
完成定义
只有当以下三句话都真实,综合项目才算完成:
- “我能从公式解释每个公开量和秘密量。”
- “我能用测试展示错误输入在何处、为什么被拒绝。”
- “我能清楚说明这套实现为何仍不能用于生产。”
如果只能生成一枚有效签名,却不能回答这三点,项目仍处于演示阶段。