#threshold-signature #secret-key #mpc #tss

cggmp21

基于CGGMP21论文的TSS ECDSA实现

6个版本 (3个破坏性)

0.4.0 2024年7月18日
0.2.1 2024年4月2日
0.2.0 2024年3月12日
0.1.1 2024年1月8日
0.0.0 2022年10月6日

#241 in 密码学

Download history 64/week @ 2024-04-19 50/week @ 2024-04-26 58/week @ 2024-05-03 50/week @ 2024-05-10 10/week @ 2024-05-17 34/week @ 2024-05-24 110/week @ 2024-05-31 162/week @ 2024-06-07 118/week @ 2024-06-14 18/week @ 2024-06-21 10/week @ 2024-06-28 42/week @ 2024-07-05 88/week @ 2024-07-12 59/week @ 2024-07-19 86/week @ 2024-07-26 9/week @ 2024-08-02

每月245次下载

MIT/ApacheLGPL-3.0+

1MB
6.5K SLoC

License Docs Crates io

基于CGGMP21论文的阈值ECDSA

CGGMP21 是一种最先进的ECDSA TSS协议,支持1轮签名(需要预处理),可识别中止,提供两种签名协议(3+1轮和5+1轮,中止识别复杂度不同)以及自带的密钥刷新协议。

此包实现了

  • 阈值(即t-out-of-n)和非阈值(即n-out-of-n)密钥生成
  • (3+1)轮通用阈值和非阈值签名
  • 辅助信息生成协议
  • 非阈值密钥的密钥刷新
  • 基于 slip10 标准(兼容 bip32)的HD钱包支持
    需要 hd-wallets 功能

关于我们实现协议的详细描述可在 此处 获取。

我们还提供辅助工具,如

  • 秘密密钥重建(从TSS导出密钥)
  • 可信经销商(将密钥导入TSS)

此包 (目前)支持

  • 阈值密钥的密钥刷新(即t-out-of-n)
  • 可识别中止
  • (5+1)轮签名协议

我们的实现已由Kudelski审核。报告可在 此处 获取。

关于阈值和非阈值密钥的概念:最初,CGGMP21论文不支持任意的 t,仅与非阈值n-out-of-n密钥一起工作。我们添加了对任意阈值 $2 \le t \le n$ 的支持,但我们使其可以选择退出阈值性,以便如果需要,可以执行原始的CGGMP21协议。

运行协议

网络

运行交互式协议最重要的部分是定义各方如何相互通信。我们的 cggmp21 库对网络层无感知,只需要你提供两件事:一条传入消息的流和一个输出消息的接收器,即

let incoming: impl Stream<Item = Result<Incoming<Msg>>>;
let outgoing: impl Sink<Outgoing<Msg>>;

其中

  • Msg 是一个协议消息(例如,signing::msg::Msg
  • round_based::Incominground_based::Outgoing 包装 Msg 并提供额外的数据(例如,发送者/接收者)
  • futures::Streamfutures::Sink 是众所周知的前端原语。

一旦你有了这些,你就可以构建一个 MpcParty

let delivery = (incoming, outgoing);
let party = round_based::MpcParty::connected(delivery);

要使用的具体网络实现将严重依赖于具体的应用。一些应用可能使用 libp2p;其他应用可能更喜欢有一个中央交付服务器或数据库(如 Redis 或 Postgres);一些特定应用可能希望通过公共区块链进行通信,等等。

无论你使用何种网络实现,请记住

  • 所有消息都必须经过身份验证
    每当一方从另一方收到消息时,接收方应通过密码学验证消息确实来自所声明的发送者。
  • 所有 p2p 消息都必须加密
    只有指定的接收者才能阅读消息

签名者索引

我们的库使用索引来唯一引用共享密钥的特定签名者。每个索引 i 是一个无符号整数 u16,其中 $0 \le i < n$,其中 n 是各方总数。

所有签名者都应对彼此的索引有相同的看法。例如,如果签名者 A 持有索引 2,则所有其他签名者必须同意 i=2 对应签名者 A。

假设某种类型的 PKI(这很可能像上面描述的那样用于确保安全通信),每个签名者都有一个公钥,该公钥唯一标识该签名者。然后,可以通过对签名者的公钥进行字典序排序,并让签名者的索引是该签名者公钥在排序列表中的位置,为签名者分配唯一的索引。

执行ID

执行我们的协议要求所有参与者就唯一的执行 ID(也称为会话标识符)达成一致,该 ID 假设永远不会重复。该字符串为不同执行的协议提供上下文分离,以确保攻击者不能将一条执行的消息重放至另一条执行。

一旦签名者可以相互交谈并共享一个执行 ID,他们就可以开始进行 MPC 了!

辅助信息生成

在常规流程中,签名者在运行分布式密钥生成之前运行一个用于生成辅助数据的协议。此协议设置某些参数(特别是,每个签名者的 Paillier 模数),这些参数将在签名协议期间使用。此协议可以按如下方式运行

// Prime generation can take a while
let pregenerated_primes = cggmp21::PregeneratedPrimes::generate(&mut OsRng);

let eid = cggmp21::ExecutionId::new(b"execution id, unique per protocol execution");
let i = /* signer index, same as at keygen */;
let n = /* number of signers */;

let aux_info = cggmp21::aux_info_gen(eid, i, n, pregenerated_primes)
    .start(&mut OsRng, party)
    .await?;

辅助数据生成协议计算量大,因为它需要生成安全的素数,并涉及多个零知识(ZK)证明。

关于辅助数据的可重用性

CGGMP21 论文假设为共享的每个密钥生成新的辅助数据。然而,对证明的审查表明这并非必要,一组固定的签名者可以使用相同的辅助数据来安全地共享/使用多个密钥。

分布式密钥生成(DKG)

DKG 协议涉及所有将共享密钥的签名者。所有签名者需要就一些基本参数达成一致,包括参与者索引、执行 ID 和阈值值(即 t)。协议可以按如下方式执行

use cggmp21::supported_curves::Secp256k1;

let eid = cggmp21::ExecutionId::new(b"execution id, unique per protocol execution");
let i = /* signer index (0 <= i < n) */;
let n = /* number of signers taking part in key generation */;
let t = /* threshold */;

let incomplete_key_share = cggmp21::keygen::<Secp256k1>(eid, i, n)
    .set_threshold(t)
    .start(&mut OsRng, party)
    .await?;

上述操作将生成一个 IncompleteKeyShare。不完整的关键共享可以通过使用 serde crate 进行序列化来保存在磁盘上。妥善处理这些材料,因为它们包含敏感信息。

假设辅助数据生成已经完成(见上文),您可以使用以下方式“完成”关键共享:

let key_share = cggmp21::KeyShare::from_parts((incomplete_key_share, aux_info))?;

签名

一旦签署者拥有一组“完成”的关键共享,他们可以签名或生成预签名。在两种情况下,都需要恰好达到阈值数量的签署者参与协议。在DKG协议中一样,每个签署者都需要分配一个唯一的索引,现在是在0到t-1的范围内。但签署者还需要知道在密钥生成时每个签署者所占的索引。

以下示例中,我们进行了完整的签名

let eid = cggmp21::ExecutionId::new(b"execution id, unique per protocol execution");

let i = /* signer index (0 <= i < min_signers) */;
let parties_indexes_at_keygen: [u16; MIN_SIGNERS] =
    /* parties_indexes_at_keygen[i] is the index the i-th party had at keygen */;
let key_share = /* completed key share */;

let data_to_sign = cggmp21::DataToSign::digest::<Sha256>(b"data to be signed");

let signature = cggmp21::signing(eid, i, &parties_indexes_at_keygen, &key_share)
    .sign(&mut OsRng, party, data_to_sign)
    .await?;

或者,您可以生成一个预签名,稍后用它来签名

  1. 使用 SigningBuilder::generate_presignature 来运行预签名生成协议
  2. 稍后,当收到签名请求时,每个签署者都会使用 Presignature::issue_partial_signature 发布部分签名
  3. 可以使用 PartialSignature::combine 将部分签名组合起来,以获得完整的签名

切勿重复使用预签名! 如果使用相同的预签名来签名两个不同的消息,可能会泄露私钥。

同步API

每个协议都被定义为异步函数。如果您需要在非异步环境中运行协议,库提供了一个包装器,允许您仅使用同步API来执行协议。

要使用它,您需要启用 state-machine 功能。然后,对于每个协议定义,您可以找到一个伴随函数,该函数返回 StateMachine,它可以用来执行协议。例如,如果您进行预签名生成,请使用 signing::SigningBuilder::generate_presignature_sync

支持HD钱包

库支持基于 slip10 标准的非硬化确定性密钥派生(与 bip32 兼容)。它允许签署者一次性生成主密钥,然后使用它即时派生所需的所有子密钥。子密钥派生在签名协议中几乎不产生成本。

为了使用HD钱包,必须启用 hd-wallets 功能。然后,需要通过使用 hd_wallet 设置为 true 的常规密钥生成协议来生成主密钥。

主密钥生成后,您可以在签名时设置派生路径以发布子密钥的签名。

SPOF代码:密钥导入和导出

CGGMP21协议旨在通过保证攻击者需要破坏一定数量的节点才能获得密钥来避免单点故障。然而,某些用例可能需要您创建单点故障,例如,将现有密钥导入TSS和从TSS导出密钥。

此类用例与MPC的本质相矛盾,因此我们默认不包含这些原语。但是,您可以通过启用 spof 功能来选择它们,然后您可以使用 trusted_dealer 进行密钥导入,以及使用 key_share::reconstruct_secret_key 进行密钥导出。

实现与CGGMP21的不同之处

CGGMP21 仅定义了一个非阈协议。为了支持一般的阈,我们定义了我们自己的类似于CGGMP21的密钥生成和阈签协议。然而,我们在crate中保留了协议的阈和非阈版本,所以如果你选择非阈协议,你将运行论文中定义的原始协议。

与原始论文相比,在实现中有其他(少量)差异(主要是错别字修正);它们都在规范中进行了记录。

时间攻击

时间攻击是一种通过执行持续时间泄露敏感信息的侧信道攻击。我们将时间攻击视为范围之外,因为它们在如此复杂的协议如CGGMP21中几乎不可能执行,在我们的特定部署中也不可能执行。因此,我们有意不执行常量时间操作,这给我们带来了显著的性能提升。

依赖项

~30MB
~668K SLoC