Threshold Signature Scheme, for ECDSA and EDDSA
多方阈值签名方案
根据 MIT 许可证授权使用。
注意!这是一个面向开发者的库。你可以在这里找到与 Binance Chain CLI 配合使用的 TSS 工具:https://docs.binance.org/tss.html。
简介
这是一个基于 Gennaro 和 Goldfeder 在 2018 年 CCS 的研究(1)和类似方法的 EdDSA(Edwards 曲线数字签名算法)的多方 {t,n} 阈值 ECDSA(椭圆曲线数字签名算法)实现。
该库包括三个协议:
- 密钥生成 - 创建无需信任的经销商的密钥份额("keygen")。
- 签名 - 使用密钥份额生成签名("signing")。
- 动态组 - 改变参与者组并保持秘密("resharing")。
💡 不要错过这些关于安全使用的重要提示
原则
ECDSA 被广泛用于比特币、以太坊(secp256k1 曲线)、NEO(NIST P-256 曲线)等加密货币。
EdDSA 则被广泛应用于卡尔达诺、永恒链、恒星币等加密货币。
对于这些货币,这种技术可用于创建多签钱包,需要多个参与者共同合作才能签署交易。查看多签用例
每个参与者本地存储一个密钥/地址的密钥份额,并通过协议保护其安全——任何时候都不会泄露给其他人。此外,没有密钥份额的中央分发者。
与多签解决方案相比,由 TSS 产生的交易通过不透露哪些 t+1 参与者参与了签名,来保持签名人隐私。
还有性能上的优势,即区块链节点可以在没有额外多签逻辑或处理的情况下检查签名的有效性。
使用
你应该首先创建一个 LocalParty 实例并为其提供所需参数。
你使用的 LocalParty 应来自 keygen、signing 或 resharing 包,取决于你的需求。
设置
// 当使用 keygen 协议时,建议预先计算“安全素数”和 Paillier 私密信息,因为这可能需要一些时间。
// 此代码将使用等于可用 CPU 核心数的并发限制来生成这些参数。
preParams, _ := keygen.GeneratePreParams(1 * time.Minute)
// 为网络上的每个参与对等节点创建一个 *PartyID(你应该为每个节点调用 tss.NewPartyID)
parties := tss.SortPartyIDs(getParticipantPartyIDs())
// 配置参数
// 注意:id 和 moniker 字段是为了方便跟踪参与者,id 应该是网络中代表该派对的唯一字符串,moniker 可以为任意内容(甚至为空)。
// uniqueKey 是此对等节点的唯一标识密钥(如其 p2p 公钥),作为大整数。
thisParty := tss.NewPartyID(id, moniker, uniqueKey)
ctx := tss.NewPeerContext(parties)
// 选择椭圆曲线
// 使用 ECDSA
curve := tss.S256()
// 或使用 EdDSA
// curve := tss.Edwards()
params := tss.NewParameters(curve, ctx, thisParty, len(parties), threshold)
// 你应该保留一个 id 字符串到 *PartyID 实例的本地映射,以便从字节中恢复发件人的 *PartyID 以传递给 UpdateFromBytes(见下文)
partyIDMap := make(map[string]*PartyID)
for _, id := range parties {
partyIDMap[id.Id] = id
}
密钥生成
使用 keygen.LocalParty 进行密钥生成协议。完成协议后,通过 endCh 接收的保存数据应持久化到安全存储中。
party := keygen.NewLocalParty(params, outCh, endCh, preParams) // 如果省略最后一个参数,则在第一轮中计算预参数
go func() {
err := party.Start()
// 处理 err...
}()
签名
使用 signing.LocalParty 进行签名,传入待签名的 message。它需要从密钥生成协议获得的密钥数据。完成时,签名会通过 endCh 发送。
请注意,t+1 个签名者被要求签署一条消息,为了最佳使用,不应涉及超过这个数量的参与者。每个签名者都应该有相同的观点,知道 t+1 个签名者是谁。
party := signing.NewLocalParty(message, params, ourKeyData, outCh, endCh)
go func() {
err := party.Start()
// 处理 err...
}()
再共享
使用 resharing.LocalParty 来重新分布密钥份额。通过 endCh 接收到的保存数据应覆盖存储中的现有密钥数据,或者如果派对接收新份额,则写入新数据。
请注意,使用 ReSharingParameters 为该党提供更多有关应该执行的再共享的上下文。
party := resharing.NewLocalParty(params, ourKeyData, outCh, endCh)
go func() {
err := party.Start()
// 处理 err...
}()
💡 在再共享过程中,密钥数据可能会在回合中修改。在通过 end 通道接收到最终结构之前,永远不要覆盖磁盘上已保存的任何数据。
消息传输
在这些示例中,outCh 将收集派对发出的消息,而 endCh 将在协议完成后接收保存数据或签名。
在协议期间,你应该向派对提供从网络上的其他参与派对接收到的更新。
一个 Party 有两个线程安全的方法用于从网线上更新其状态:
// 更新派对状态的主要入口点,从网络接收
UpdateFromBytes(wireBytes []byte, from *tss.PartyID, isBroadcast bool) (ok bool, err *tss.Error)
// 可用于更新本地运行或测试中的派对状态
Update(msg tss.ParsedMessage) (ok bool, err *tss.Error)
而一个 tss.Message 有以下两个方法将消息转换为用于线路的数据:
// 返回编码后的消息字节以发送到指定的目标,以及路由信息
WireBytes() ([]byte, *tss.MessageRouting, error)
// 返回 protobuf 封装消息结构,仅在某些特殊情况(例如移动应用)中使用
WireMsg() *tss.MessageWrapper
在典型的用例中,预期是传输实现将通过本地 Party 的 out 通道消费消息字节,将其发送到 msg.GetTo() 结果指定的目的地,并在其接收端传递给 UpdateFromBytes。
这样就不需要处理实现传输所需的 Protocol Buffers 的序列化/反序列化。
v2.0中的ECDSA预参数变化
在v2.0版本中,增添了PaillierSK的两个字段P和Q。这些字段用于生成Paillier密钥证明。来自2.0版本之前生成的密钥保管库需要重新生成(重共享)以更新预参数,并填写必要的字段。
如何安全使用
⚠️ 本节至关重要,请务必阅读!
消息传输由应用层负责,本库不提供此项功能。下文每一段都需仔细阅读并遵循,确保实施安全的传输机制是保护协议安全的关键。
构建传输时,应提供广播频道以及连接每对参与方的点对点频道。你的传输系统还应当采用端到端加密(推荐使用支持关联数据认证加密(AEAD)的TLS),确保各参与方仅能读取发给自己的消息。
在传输内,每个消息应嵌套一个会话ID,该ID对于键生成、签名或重共享轮次唯一。此会话ID需事先通过非公开渠道商定,并仅由参与方知晓。接收任何消息时,程序应验证接收到的会话ID与初始约定的一致性。
此外,你的传输机制应包含允许“可靠的广播”的方式,即保证参与者广播的消息能让每个人都能确切接收到同样的信息。网络上有多种算法示例,通过共享和比对收到消息的散列值来实现这一点。
应用程序应处理超时和错误情况。可以通过调用Party上的WaitingFor方法获取仍在等待其消息的其他参与方集合。也可以从tss.Error的指针获取导致错误的参与方集合。
安全审计
此库已由Kudelski Security进行全面审查,最终报告于2019年10月发布。可在本存储库的v1.0.0版本说明中找到这份报告的副本【audit-binance-tss-lib-final-20191018.pdf】。