454 lines
14 KiB
Markdown
454 lines
14 KiB
Markdown
# MPC 分布式签名系统 - TSS 协议详解
|
||
|
||
## 1. 概述
|
||
|
||
本系统使用 **门限签名方案 (Threshold Signature Scheme, TSS)** 实现分布式密钥管理和签名。基于 [bnb-chain/tss-lib](https://github.com/bnb-chain/tss-lib) 库,采用 GG20 协议。
|
||
|
||
### 1.1 核心概念
|
||
|
||
| 术语 | 定义 |
|
||
|------|------|
|
||
| t-of-n | t+1 个参与方中的任意组合可以签名,需要 n 个参与方共同生成密钥 |
|
||
| DKG | 分布式密钥生成 (Distributed Key Generation) |
|
||
| TSS | 门限签名方案 (Threshold Signature Scheme) |
|
||
| Party | MPC 协议中的参与方 |
|
||
| Share | 密钥分片,每个 Party 持有一份 |
|
||
|
||
### 1.2 安全属性
|
||
|
||
- **无单点故障**: 私钥从未以完整形式存在
|
||
- **门限安全**: 需要 t+1 个分片才能签名
|
||
- **抗合谋**: t 个恶意方无法伪造签名
|
||
- **可审计**: 每次签名可追踪参与方
|
||
|
||
## 2. 阈值参数说明
|
||
|
||
### 2.1 tss-lib 参数约定
|
||
|
||
在 tss-lib 中,`threshold` 参数定义如下:
|
||
- `threshold = t` 表示需要 **t+1** 个签名者
|
||
- 例如: `threshold=1` 需要 2 个签名者
|
||
|
||
### 2.2 常见阈值方案
|
||
|
||
| 方案 | tss-lib threshold | 总参与方 (n) | 签名者数 (t+1) | 应用场景 |
|
||
|------|-------------------|-------------|---------------|---------|
|
||
| 2-of-3 | 1 | 3 | 2 | 个人钱包 + 设备 + 恢复 |
|
||
| 3-of-5 | 2 | 5 | 3 | 企业多签 |
|
||
| 4-of-7 | 3 | 7 | 4 | 机构托管 |
|
||
| 5-of-9 | 4 | 9 | 5 | 大型组织 |
|
||
|
||
### 2.3 阈值选择建议
|
||
|
||
```
|
||
安全性 vs 可用性权衡:
|
||
|
||
高安全性 ◄────────────────────────► 高可用性
|
||
5-of-9 4-of-7 3-of-5 2-of-3
|
||
|
||
建议:
|
||
- 个人用户: 2-of-3 (设备 + 服务器 + 恢复)
|
||
- 小型企业: 3-of-5 (3 管理员 + 1 服务器 + 1 恢复)
|
||
- 大型企业: 4-of-7 或更高
|
||
```
|
||
|
||
## 3. 密钥生成协议 (Keygen)
|
||
|
||
### 3.1 协议流程
|
||
|
||
```
|
||
Round 1: 承诺分发
|
||
┌────────────┐ ┌────────────┐ ┌────────────┐
|
||
│ Party 0 │ │ Party 1 │ │ Party 2 │
|
||
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
|
||
│ │ │
|
||
│ 生成随机多项式 │ │
|
||
│ 计算承诺 Ci │ │
|
||
│ │ │
|
||
│◄─────────────────┼──────────────────┤ 广播承诺
|
||
├──────────────────►◄─────────────────┤
|
||
│ │ │
|
||
|
||
Round 2: 秘密分享
|
||
│ │ │
|
||
│ 计算 Shamir 分片│ │
|
||
│ 发送 share_ij │ │
|
||
│ │ │
|
||
│──────────────────► │ 点对点
|
||
│ ◄──────────────────│
|
||
◄──────────────────│ │
|
||
│ │──────────────────►
|
||
│ │ │
|
||
|
||
Round 3: 验证与聚合
|
||
│ │ │
|
||
│ 验证收到的分片 │ │
|
||
│ 计算最终密钥分片 │ │
|
||
│ 计算公钥 PK │ │
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
Share_0 Share_1 Share_2
|
||
│ │ │
|
||
└──────────────────┼──────────────────┘
|
||
│
|
||
公钥 PK (相同)
|
||
```
|
||
|
||
### 3.2 代码实现
|
||
|
||
```go
|
||
// pkg/tss/keygen.go
|
||
func RunLocalKeygen(threshold, totalParties int) ([]*LocalKeygenResult, error) {
|
||
// 验证参数
|
||
if threshold < 1 || threshold > totalParties {
|
||
return nil, ErrInvalidThreshold
|
||
}
|
||
|
||
// 创建 Party IDs
|
||
partyIDs := make([]*tss.PartyID, totalParties)
|
||
for i := 0; i < totalParties; i++ {
|
||
partyIDs[i] = tss.NewPartyID(
|
||
fmt.Sprintf("party-%d", i),
|
||
fmt.Sprintf("party-%d", i),
|
||
big.NewInt(int64(i+1)),
|
||
)
|
||
}
|
||
sortedPartyIDs := tss.SortPartyIDs(partyIDs)
|
||
peerCtx := tss.NewPeerContext(sortedPartyIDs)
|
||
|
||
// 创建各方的通道和 Party 实例
|
||
outChs := make([]chan tss.Message, totalParties)
|
||
endChs := make([]chan *keygen.LocalPartySaveData, totalParties)
|
||
parties := make([]tss.Party, totalParties)
|
||
|
||
for i := 0; i < totalParties; i++ {
|
||
outChs[i] = make(chan tss.Message, totalParties*10)
|
||
endChs[i] = make(chan *keygen.LocalPartySaveData, 1)
|
||
params := tss.NewParameters(
|
||
tss.S256(), // secp256k1 曲线
|
||
peerCtx,
|
||
sortedPartyIDs[i],
|
||
totalParties,
|
||
threshold,
|
||
)
|
||
parties[i] = keygen.NewLocalParty(params, outChs[i], endChs[i])
|
||
}
|
||
|
||
// 启动所有 Party
|
||
for i := 0; i < totalParties; i++ {
|
||
go parties[i].Start()
|
||
}
|
||
|
||
// 消息路由
|
||
go routeMessages(parties, outChs, sortedPartyIDs)
|
||
|
||
// 收集结果
|
||
results := make([]*LocalKeygenResult, totalParties)
|
||
for i := 0; i < totalParties; i++ {
|
||
saveData := <-endChs[i]
|
||
results[i] = &LocalKeygenResult{
|
||
SaveData: saveData,
|
||
PublicKey: saveData.ECDSAPub.ToECDSAPubKey(),
|
||
PartyIndex: i,
|
||
}
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
```
|
||
|
||
### 3.3 SaveData 结构
|
||
|
||
每个 Party 保存的数据:
|
||
|
||
```go
|
||
type LocalPartySaveData struct {
|
||
// 本方的私钥分片 (xi)
|
||
Xi *big.Int
|
||
|
||
// 所有方的公钥分片 (Xi = xi * G)
|
||
BigXj []*crypto.ECPoint
|
||
|
||
// 组公钥
|
||
ECDSAPub *crypto.ECPoint
|
||
|
||
// Paillier 密钥对 (用于同态加密)
|
||
PaillierSK *paillier.PrivateKey
|
||
PaillierPKs []*paillier.PublicKey
|
||
|
||
// 其他预计算数据...
|
||
}
|
||
```
|
||
|
||
## 4. 签名协议 (Signing)
|
||
|
||
### 4.1 协议流程
|
||
|
||
```
|
||
签名协议 (GG20 - 6 轮):
|
||
|
||
Round 1: 承诺生成
|
||
┌────────────┐ ┌────────────┐
|
||
│ Party 0 │ │ Party 1 │
|
||
└─────┬──────┘ └─────┬──────┘
|
||
│ │
|
||
│ 生成随机 ki │
|
||
│ 计算 γi = ki*G │
|
||
│ 广播 C(γi) │
|
||
│ │
|
||
│◄────────────────►│
|
||
│ │
|
||
|
||
Round 2: Paillier 加密
|
||
│ │
|
||
│ 加密 ki │
|
||
│ MtA 协议开始 │
|
||
│ │
|
||
│◄────────────────►│
|
||
│ │
|
||
|
||
Round 3: MtA 响应
|
||
│ │
|
||
│ 计算乘法三元组 │
|
||
│ │
|
||
│◄────────────────►│
|
||
│ │
|
||
|
||
Round 4: Delta 分享
|
||
│ │
|
||
│ 计算 δi │
|
||
│ 广播 │
|
||
│ │
|
||
│◄────────────────►│
|
||
│ │
|
||
|
||
Round 5: 重构与验证
|
||
│ │
|
||
│ 重构 δ = Σδi │
|
||
│ 计算 R = δ^-1*Γ │
|
||
│ 计算 r = Rx │
|
||
│ │
|
||
│◄────────────────►│
|
||
│ │
|
||
|
||
Round 6: 签名聚合
|
||
│ │
|
||
│ 计算 si = ... │
|
||
│ 广播 si │
|
||
│ │
|
||
│◄────────────────►│
|
||
│ │
|
||
▼ ▼
|
||
最终签名 (r, s)
|
||
```
|
||
|
||
### 4.2 代码实现
|
||
|
||
```go
|
||
// pkg/tss/signing.go
|
||
func RunLocalSigning(
|
||
threshold int,
|
||
keygenResults []*LocalKeygenResult,
|
||
messageHash []byte,
|
||
) (*LocalSigningResult, error) {
|
||
signerCount := len(keygenResults)
|
||
if signerCount < threshold+1 {
|
||
return nil, ErrInvalidSignerCount
|
||
}
|
||
|
||
// 创建 Party IDs (必须使用原始索引)
|
||
partyIDs := make([]*tss.PartyID, signerCount)
|
||
for i, result := range keygenResults {
|
||
idx := result.PartyIndex
|
||
partyIDs[i] = tss.NewPartyID(
|
||
fmt.Sprintf("party-%d", idx),
|
||
fmt.Sprintf("party-%d", idx),
|
||
big.NewInt(int64(idx+1)),
|
||
)
|
||
}
|
||
sortedPartyIDs := tss.SortPartyIDs(partyIDs)
|
||
peerCtx := tss.NewPeerContext(sortedPartyIDs)
|
||
|
||
// 转换消息哈希
|
||
msgHash := new(big.Int).SetBytes(messageHash)
|
||
|
||
// 创建签名方
|
||
outChs := make([]chan tss.Message, signerCount)
|
||
endChs := make([]chan *common.SignatureData, signerCount)
|
||
parties := make([]tss.Party, signerCount)
|
||
|
||
for i := 0; i < signerCount; i++ {
|
||
outChs[i] = make(chan tss.Message, signerCount*10)
|
||
endChs[i] = make(chan *common.SignatureData, 1)
|
||
params := tss.NewParameters(tss.S256(), peerCtx, sortedPartyIDs[i], signerCount, threshold)
|
||
parties[i] = signing.NewLocalParty(msgHash, params, *keygenResults[i].SaveData, outChs[i], endChs[i])
|
||
}
|
||
|
||
// 启动并路由消息
|
||
for i := 0; i < signerCount; i++ {
|
||
go parties[i].Start()
|
||
}
|
||
go routeSignMessages(parties, outChs, sortedPartyIDs)
|
||
|
||
// 收集签名结果
|
||
signData := <-endChs[0]
|
||
return &LocalSigningResult{
|
||
R: new(big.Int).SetBytes(signData.R),
|
||
S: new(big.Int).SetBytes(signData.S),
|
||
RecoveryID: int(signData.SignatureRecovery[0]),
|
||
}, nil
|
||
}
|
||
```
|
||
|
||
### 4.3 签名验证
|
||
|
||
```go
|
||
// 验证签名
|
||
import "crypto/ecdsa"
|
||
|
||
func VerifySignature(publicKey *ecdsa.PublicKey, messageHash []byte, r, s *big.Int) bool {
|
||
return ecdsa.Verify(publicKey, messageHash, r, s)
|
||
}
|
||
|
||
// 示例
|
||
message := []byte("Hello MPC!")
|
||
hash := sha256.Sum256(message)
|
||
valid := ecdsa.Verify(publicKey, hash[:], signResult.R, signResult.S)
|
||
```
|
||
|
||
## 5. 消息路由
|
||
|
||
### 5.1 消息类型
|
||
|
||
| 类型 | 说明 | 方向 |
|
||
|------|------|------|
|
||
| Broadcast | 发送给所有其他方 | 1 → n-1 |
|
||
| P2P | 点对点消息 | 1 → 1 |
|
||
|
||
### 5.2 消息结构
|
||
|
||
```go
|
||
type MPCMessage struct {
|
||
SessionID string // 会话 ID
|
||
FromParty string // 发送方
|
||
ToParties []string // 接收方 (空=广播)
|
||
Round int // 协议轮次
|
||
Payload []byte // 加密的协议消息
|
||
IsBroadcast bool // 是否广播
|
||
Timestamp int64
|
||
}
|
||
```
|
||
|
||
### 5.3 消息路由实现
|
||
|
||
```go
|
||
func routeMessages(
|
||
parties []tss.Party,
|
||
outChs []chan tss.Message,
|
||
sortedPartyIDs []*tss.PartyID,
|
||
) {
|
||
signerCount := len(parties)
|
||
|
||
for idx := 0; idx < signerCount; idx++ {
|
||
go func(i int) {
|
||
for msg := range outChs[i] {
|
||
if msg.IsBroadcast() {
|
||
// 广播给所有其他方
|
||
for j := 0; j < signerCount; j++ {
|
||
if j != i {
|
||
updateParty(parties[j], msg)
|
||
}
|
||
}
|
||
} else {
|
||
// 点对点发送
|
||
for _, dest := range msg.GetTo() {
|
||
for j := 0; j < signerCount; j++ {
|
||
if sortedPartyIDs[j].Id == dest.Id {
|
||
updateParty(parties[j], msg)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}(idx)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. 子集签名 (Subset Signing)
|
||
|
||
### 6.1 原理
|
||
|
||
在 t-of-n 方案中,任意 t+1 个 Party 的子集都可以生成有效签名。关键是使用原始的 Party 索引。
|
||
|
||
### 6.2 示例: 2-of-3 的所有组合
|
||
|
||
```go
|
||
// 3 方生成密钥
|
||
keygenResults, _ := tss.RunLocalKeygen(1, 3) // threshold=1, n=3
|
||
|
||
// 任意 2 方可签名:
|
||
// 组合 1: Party 0 + Party 1
|
||
signers1 := []*tss.LocalKeygenResult{keygenResults[0], keygenResults[1]}
|
||
sig1, _ := tss.RunLocalSigning(1, signers1, messageHash)
|
||
|
||
// 组合 2: Party 0 + Party 2
|
||
signers2 := []*tss.LocalKeygenResult{keygenResults[0], keygenResults[2]}
|
||
sig2, _ := tss.RunLocalSigning(1, signers2, messageHash)
|
||
|
||
// 组合 3: Party 1 + Party 2
|
||
signers3 := []*tss.LocalKeygenResult{keygenResults[1], keygenResults[2]}
|
||
sig3, _ := tss.RunLocalSigning(1, signers3, messageHash)
|
||
|
||
// 所有签名都对同一公钥有效!
|
||
ecdsa.Verify(publicKey, messageHash, sig1.R, sig1.S) // true
|
||
ecdsa.Verify(publicKey, messageHash, sig2.R, sig2.S) // true
|
||
ecdsa.Verify(publicKey, messageHash, sig3.R, sig3.S) // true
|
||
```
|
||
|
||
### 6.3 注意事项
|
||
|
||
1. **Party 索引必须一致**: 签名时使用 keygen 时的原始索引
|
||
2. **不能混用不同 keygen 的分片**: 每个账户对应唯一的一组分片
|
||
3. **阈值验证**: 签名者数量 >= threshold + 1
|
||
|
||
## 7. 性能考虑
|
||
|
||
### 7.1 测试基准
|
||
|
||
| 操作 | 2-of-3 | 3-of-5 | 4-of-7 |
|
||
|------|--------|--------|--------|
|
||
| Keygen | ~93s | ~198s | ~221s |
|
||
| Signing | ~80s | ~120s | ~150s |
|
||
|
||
### 7.2 优化建议
|
||
|
||
1. **预计算**: 部分 Keygen 数据可预计算
|
||
2. **并行执行**: 多个签名请求可并行处理
|
||
3. **消息压缩**: 大消息进行压缩传输
|
||
4. **连接池**: 复用 Party 间的连接
|
||
|
||
## 8. 故障恢复
|
||
|
||
### 8.1 Keygen 失败
|
||
|
||
如果 Keygen 过程中某个 Party 离线:
|
||
- 协议超时失败
|
||
- 需要全部重新开始
|
||
- 建议设置合理的超时时间
|
||
|
||
### 8.2 Signing 失败
|
||
|
||
如果签名过程中 Party 离线:
|
||
- 当前签名失败
|
||
- 可以选择其他 Party 子集重试
|
||
- 密钥分片不受影响
|
||
|
||
### 8.3 密钥分片丢失
|
||
|
||
如果某个 Party 的分片丢失:
|
||
- 如果丢失数量 < n - t: 仍可签名
|
||
- 如果丢失数量 >= n - t: 无法签名,需要重新 Keygen
|
||
- 建议: 加密备份分片到安全存储
|