前言
MPC 钱包是一种基于多方计算技术的加密货币钱包,它将私钥拆分为多个部分,并分散存储在不同的地方,只有在用户进行交易时,才会在多方间共同计算私钥。MPC 钱包在多方之间拆分钱包的私钥,从起始阶段,钱包私钥就从未出现过,并且私钥分片由多方在本地独立生成,从根本上消除了单点风险,,以增强隐私性,并降低黑客攻击、泄露和丢失的风险。
拉格朗日插值法
现在已知告诉你两个点: ( x 2 , 0 ) , ( x 1 , 1 ) (x_2,0), (x_1,1) (x2,0),(x1,1),得多项式
y = x − x 1 x 1 − x 2 y={x-x_1 \over x_1-x_2} y=x1−x2x−x1
- 当 x = x 2 x=x_2 x=x2时, y = 0 y=0 y=0;
- 当 x = x 1 x=x_1 x=x1时, y = 1 y=1 y=1;
需求升级告诉三个点:
(
x
1
,
y
红
)
,
(
x
2
,
y
黄
)
,
(
x
3
,
y
蓝
)
(x_1,y_红), (x_2,y_黄), (x_3,y_蓝)
(x1,y红),(x2,y黄),(x3,y蓝),得多项式

已知点的数量大于3时,一个通用模板


基于拉格朗日插值法。直观地:
- 2个点可以确定一条直线;
- 3个点可以确定一条二次曲线;
- ……

Shamir秘密分享
想象有一条“神秘曲线”,我们把秘密藏在这条曲线
x
=
0
x=0
x=0处的值里(也就是曲线的“起点”)。 接着,把这条曲线在若干个不同位置的取值分发给大家——每人一块“拼图”。
当有足够多的拼图块凑在一起时,就能复原整条曲线,从而读出
x
=
0
x=0
x=0的值,也就拿回了秘密。

构造多项式

分发份额

用线性组合恢复秘密

ps: 重组向量,可以理解成向量组,它确定了向量空间, mod 操作则是压缩空间, x=0则指明空间点

SSS的缺点
如果在生产环境中去部署一套密钥分享服务时,我们发现,SSS方案存在诸多风险。我们将该服务中的角色分为dealer和众多的player,dealer负责密钥的分割和聚集恢复,而player作为密钥分片的持有人。

上图是一个阈值结构为2/3的Shamir密钥分享过程,任意两把密钥分片可以恢复出原密钥。可能存在以下风险:
- Dealer作恶,发送给三个Player的密钥分片并不能恢复出一致的密钥,如给Player1和Player2的分片是正常的,而Palyer3的分片是错误的;
- Player作恶,在恢复阶段发送的分片是错误的,这样恢复的密钥也是错误的;
Feldman的可验证密钥分享
FeldmanVSS方案基于离散对数问题,在SSS基础上增加了校验的过程。2/3密钥分享流程如下图所示。

分割密钥及生成承诺
分割密钥的过程和SSS类似,同时生成多项式系数的承诺
c
i
=
g
a
i
c_i=g^{a_i}
ci=gai
- g为循环群的生成元,即椭圆曲线中的G点
- a i a_i ai为SSS创建阶段生成多项式中的系数
Dealer 按上述步骤完成了之后
- 将密钥分片 s h a r e ( i ) = ( x i , y i ) share(i) = (x_i,y_i) share(i)=(xi,yi)分别发送给Player;
- 将承诺 c o m m i t s = c 0 , c 1 , c 2 , . . , c k commits = {c_0,c_1,c_2,..,c_k} commits=c0,c1,c2,..,ck公开广播给所有结点
验证分片
收到分片和承诺后,每个Player可执行下面的计算
- 根据多项式: y = a 0 + a 1 x + . . . + a k x k y=a_0 + a_1x+ ... + a_kx^k y=a0+a1x+...+akxk
- 带入分片share: y i = a 0 + a 1 x i + . . . + a k x i k y_i=a_0 + a_1x_i+ ... + a_kx_i^k yi=a0+a1xi+...+akxik
- 再以g为底: g y i = g a 0 + a 1 x i + . . . + a k x i k = c 0 c 1 x i 1 . . . c k x i k g^{y_i}=g^{a_0 + a_1x_i+ ... + a_kx_i^k}=c_0c_1^{x_i^1}...c_k^{x_i^k} gyi=ga0+a1xi+...+akxik=c0c1xi1...ckxik
根据上面公式 g , x i , y i , k g,x_i,y_i, k g,xi,yi,k均已知,Player可验收验证该等式左右两边成立,如果Dealer作恶,则收到错误分片的Player本地将无法通过这个测试
恢复密钥及验证正确性:恢复密钥的过程和SSS类似,Dealer计算出a0密钥后,后还需要验证 c 0 = g a 0 c_0 = g^{a_0} c0=ga0是否成立,避免Player发送错误的分片导致恢复出错误的密钥。
MPC
多方安全技术(MPC, security multi-party computing) 越来越多的被应用在加密货币钱包/交易所; 其主要应用在3个方面:
- 多方共同生成一个钱包的私钥, 并对这个私钥进行分片。使得不同分片支持门限恢复操作。如将一个完整的密钥分片分成5份,需要两份即可恢复出完整密钥。 5 记做分片数量, 2记做门限数量。
- 多方协同签名, 使用多方的密钥分片对交易进行签名。具体来说, 每一个密钥分片对交易进行签名 并 拼凑起来的 签名 等价于 完整私钥对交易进行签名。
- 多方协同密钥重置, 密钥参与方经过协商, 将原本的私钥分片打散然后重新分配, 最终的完整私钥和公钥不变, 但是每个参与方持有的私钥分片发生改变, 整个协商过程中完整私钥都不会出现。
这里的多方(一般被称作party)是比较广义的, 可能有多重意义, 比如一个用户登录一台设备, 这台设备可以被称作一个 party。 比如加密币平台的服务器为客户端party生成一个临时的 进程, 这个进程与客户端的party一起协商出一把私钥, 这个进程也可以称作一个 party。 在密码学上面, 只要参与多方安全计算的实体都统称为 party。 生成的密钥分片也会被保存到多个地方, 通常是两到三个: 客户端, 服务端, 用户方(用户自己记在脑袋或者小本本炒好都可以)
通过上面的两个应用可以实现下面两个非常吸引人的性质:
- 分布式完成密钥生成(密钥重置), 永远不会出现完整的 密钥, 无论是内存, 硬盘, 还是cpu, 服务器的任何硬件永远都不会出现完整的密钥
- 签名过程, 无论是内存, 硬盘, 还是cpu, 服务器的任何硬件永远都不会出现使用完整的密钥对交易进行签名 但实现效果和使用一把完整密钥进行签名的效果一致
这两个性质就非常诱人, 即使钱包/交易所里面有内鬼, 内鬼也无法知道钱包的完整私钥, 因为完整的私钥压根从来就没有展示出来过。配合上零日志记录+端到端加密, 可以实现神仙来了都无法撼动的资产安全效果。
GG18
GG18 讲解了如何去中心化,分布式地生成密钥分片。
假设有4个用户Alice, Bob, Chalize, David 想要协商出一把共享密钥, 每个用户保存一个共享密钥的分片, 3个分片可以恢复出完整密钥。可以通过下几个步骤实现:
-
Alice 选定自己的私钥10, Bob选定自己的私钥20, Chalize选定自己的私钥30, David选定自己的私钥40

-
随后四个人各自计算自己的密钥分片
- Alice private key = 12 + 25 +30 +40 = 107
- Bob private key = 16 + 34+28+38 = 116
- Charlize private key = 22+47+24+34 = 127
- David share key = 30 + 64 + 18 +28 = 140
至此密钥分片的计算已经完成, 最终的私钥通过 (1,107), (2,116), (3,127), (4,140) 使用拉格朗日插值计算得到二次曲线 f ( x ) = x 2 + 6 x + 100 f(x) = x^2 + 6x +100 f(x)=x2+6x+100, x= 0 时 100就是完整的密钥。
各方不泄漏自己的私钥(10,20,30,40)情况下将自己的私钥分享出去,从而可以恢复出最终的共享密钥 100.
仔细观察
f
(
x
)
=
x
2
+
6
x
+
100
f(x) = x^2 + 6x +100
f(x)=x2+6x+100 刚好等于四个人选择的曲线相加,这也是密钥生成的正确性的来源 四个人通过点对点传输的方式将自己的曲线间接的暴露给四个人。 对于公钥Q则有
Q
=
d
G
⇒
Q
=
f
(
0
)
G
⇒
Q
=
f
1
(
0
)
G
+
f
2
(
0
)
G
+
f
3
(
0
)
G
+
f
4
(
0
)
G
⇒
Q
=
Q
1
+
Q
2
+
Q
3
+
Q
4
Q=dG \\ \Rightarrow Q= f(0)G \\ \Rightarrow Q= f_1(0)G+f_2(0)G+f_3(0)G+f_4(0)G \\ \Rightarrow Q=Q_1+Q_2+Q_3+Q_4
Q=dG⇒Q=f(0)G⇒Q=f1(0)G+f2(0)G+f3(0)G+f4(0)G⇒Q=Q1+Q2+Q3+Q4
通过广播,通知各节点
ATM
利用Paillier 同态加密公私钥对进行门线签名
根据椭圆曲线可知签名 s i g n = ( r , s ) sign=(r,s) sign=(r,s),如果要想去中心化地签名, 就需要去中心化地计算 r 和 s。
注意: G d G^d Gd 和 d G dG dG 是一样的操作, 这里有点累比, 因为有限域上面只有 一个数字乘一个点,点和点是无法的直接相乘的只能相加,但因为多个点相加是杂乱无序的,所以可以类比成数字中的求高次方
计算签名中的r

我们观察上式, γ i \gamma_i γi 由每一个 party 私自持有, 而 g γ i g^{\gamma_i} gγi公开, 剩下的问题上如何去中心化地协商出 k γ k\gamma kγ

下面来看看 MTA 协议是如何通过 Paillier 同态加密实现的

- MTA 协议需要两个参与方, 命名为 Alice 和 Bob。 其中Alice 拥有 Paillier 公私钥对, 拥有 秘密值 a. Bob 拥有秘密值 b
- Alice 像 Bob 发送 c A = E A ( a ) cA= E_A(a) cA=EA(a), E A ( a ) E_A(a) EA(a)表示Alice使用 Paillier公钥对其秘密值 a 进行加密。
- Bob 准备一个随机数 β ′ , 令 β = − β ′ \beta', 令 \beta= -\beta' β′,令β=−β′,然后计算 c B = b ⋅ c A + E A ( β ′ ) = b ⋅ E A ( a ) + E A ( β ′ ) = E A ( a b + β ′ ) cB=b⋅cA+E_A(\beta')= b⋅E_A(a) +E_A(\beta')= E_A(ab+ \beta') cB=b⋅cA+EA(β′)=b⋅EA(a)+EA(β′)=EA(ab+β′)
- Bob 向 Alice 发送 c B = E A ( a b + β ′ ) cB= E_A(ab+ \beta') cB=EA(ab+β′)
- Alice 解密 cB, 令 α = D A ( c B ) = a b + β ′ \alpha=D_A(cB)=ab+ \beta' α=DA(cB)=ab+β′
- 然后我们计算: α + β = a b \alpha + \beta = ab α+β=ab

也就是说每一个私钥分片持有方两两进行两次MTA就可以完成对ECDSA签名中R的拆分。
计算签名中的s
s = k ( z + r d ) = k z + k r d s=k(z+rd)=kz+krd s=k(z+rd)=kz+krd
- z: 被签名的哈希, 对于每一个party都是已知的
- d:为完整私钥
假设有两个参与方 k = k 0 + k 1 , d = d 0 + d 1 k=k_0+k_1, d=d_0+d_1 k=k0+k1,d=d0+d1
s = k ( z + r d ) = k 1 z + k 2 z + ( k 0 + k 1 ) ( d 0 + d 1 ) r s=k(z+rd)=k_1z+k_2z+(k_0+k_1)(d_0+d_1)r s=k(z+rd)=k1z+k2z+(k0+k1)(d0+d1)r
- k 0 z , k 1 z k_0z, k_1z k0z,k1z 可以有 party0 和 party1 各自计算
- r:在上面已经计算出结果
完整私钥d也就是拉格朗日曲线
d = L ( x ) = y 0 l o ( x ) + y 1 l 1 ( x ) = w 0 + w 1 d=L(x)=y_0l_o(x)+y_1l_1(x) = w_0+w_1 d=L(x)=y0lo(x)+y1l1(x)=w0+w1
则
k
d
=
(
k
0
+
k
1
)
∗
(
w
0
+
w
1
)
=
k
0
w
0
+
k
1
w
1
+
k
0
w
1
+
k
1
w
0
kd=(k_0+k_1)*(w_0+w_1)=k_0w_0 + k_1w_1 + k_0w_1 + k_1w_0
kd=(k0+k1)∗(w0+w1)=k0w0+k1w1+k0w1+k1w0
其中
k
0
w
0
k_0w_0
k0w0 可以由party0独自计算,
k
1
w
1
k_1w_1
k1w1可以由party1独自计算
最终计算s的问题只剩下 party0不泄露自己的
k
0
,
w
0
k_0,w_0
k0,w0, party1不泄露自己的
k
1
,
w
1
k_1,w_1
k1,w1 ,计算出
k
0
w
1
+
k
1
w
0
k_0w_1 + k_1w_0
k0w1+k1w0
可以使用两次MTA协议进行秘密交换, 将乘法秘密转化成加法秘密从而得到 k 0 w 1 , k 1 w 0 k_0w_1 , k_1w_0 k0w1,k1w0
币安tss-lib
bnb-chain/tss-lib 是这是一个基于Gennaro和Goldfeder CCS 2018论文实现的 { t , n } \{t,n\} {t,n}-门限ECDSA和EdDSA签名库。该库允许多个参与方协作生成密钥和签名,而无需信任的第三方。
-
密钥生成(Keygen): 创建密钥份额,无需可信第三方
-
签名(Signing): 使用密钥份额生成签名
-
动态组重构(Resharing): 在保持密钥不变的情况下更改参与方组
每个参与方本地存储一个密钥份额,永不向他人透露,无可信第三方参与密钥分发
支持的曲线:
- ECDSA: 支持secp256k1(比特币、以太坊)和NIST P-256(NEO)等曲线
-EdDSA: 支持Edwards曲线,用于Cardano、Stellar等加密货币
来到 tss-lib/ecdsa/keygen /local_party_test.go 中的 TestE2EConcurrentAndSaveFixtures 函数, 我们从单测入手, 宏观看看如何进行分布式密钥生成
func TestE2EConcurrentAndSaveFixtures(t *testing.T) {
// 设置日志级别为 info
setUp("info")
// 门限值 = testParticipants / 2
threshold := testThreshold
// 导入参与方数据, 一些提前生成的本地文件, 加速单测, 主要是安全大素数safe prime的生成
// 目标数据存在:tss/_ecdsa_fixtures
fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants)
if err != nil {
// 没有固定文件,生成新的参与方数据
common.Logger.Info("No test fixtures were found, so the safe primes will be generated from scratch. This may take a while...")
pIDs = tss.GenerateTestPartyIDs(testParticipants)
}
// 一个中心化的熟悉彼此的contex, 参与方进程ID上下文
p2pCtx := tss.NewPeerContext(pIDs)
// 参与方数组
parties := make([]*LocalParty, 0, len(pIDs))
// 进程间通信管道初始化
errCh := make(chan *tss.Error, len(pIDs))
outCh := make(chan tss.Message, len(pIDs))
endCh := make(chan *LocalPartySaveData, len(pIDs))
updater := test.SharedPartyUpdater
startGR := runtime.NumGoroutine()
// 批量创建参与方
for i := 0; i < len(pIDs); i++ {
var P *LocalParty
params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[i], len(pIDs), threshold)
...
if i < len(fixtures) {
P = NewLocalParty(params, outCh, endCh, fixtures[i].LocalPreParams).(*LocalParty)
} else {
P = NewLocalParty(params, outCh, endCh).(*LocalParty)
}
parties = append(parties, P)
// 启动参与方, 每个参与方会执行round1-round4
go func(P *LocalParty) {
if err := P.Start(); err != nil {
errCh <- err
}
}(P)
}
// PHASE: keygen
var ended int32
// 主事件循环 利用事件模型nio
keygen:
for {
fmt.Printf("ACTIVE GOROUTINES: %d\n", runtime.NumGoroutine())
select {
case err := <-errCh: // 处理错误事件
common.Logger.Errorf("Error: %s", err)
assert.FailNow(t, err.Error())
break keygen
case msg := <-outCh: // 处理协议消息事件
dest := msg.GetTo()
if dest == nil { // 实现进程间广播
for _, P := range parties {
if P.PartyID().Index == msg.GetFrom().Index {
continue
}
go updater(P, msg, errCh)
}
} else { // 实现进程间点对点
if dest[0].Index == msg.GetFrom().Index {
t.Fatalf("party %d tried to send a message to itself (%d)", dest[0].Index, msg.GetFrom().Index)
return
}
go updater(parties[dest[0].Index], msg, errCh)
}
case save := <-endCh: // 处理完成事件
// 保存测试固定文件
index, err := save.OriginalIndex()
assert.NoErrorf(t, err, "should not be an error getting a party's index from save data")
tryWriteTestFixtureFile(t, index, *save)
atomic.AddInt32(&ended, 1)
// 所有参与方都完成了,在上帝视角,开始验证阶段
if atomic.LoadInt32(&ended) == int32(len(pIDs)) {
t.Logf("Done. Received save data from %d participants", ended)
// 对每一个 party 算出来的分片数据进行合法性校验
u := new(big.Int)
for j, Pj := range parties {
pShares := make(vss.Shares, 0)
// 收集 Pj 在 round 2 协议中收到的分片(share)
for _, P := range parties {
vssMsgs := P.temp.kgRound2Message1s
share := vssMsgs[j].Content().(*KGRound2Message1).Share
shareStruct := &vss.Share{
Threshold: threshold,
ID: P.PartyID().KeyInt(),
Share: new(big.Int).SetBytes(share),
}
pShares = append(pShares, shareStruct)
}
// 通过拉格朗日插值恢复出曲线的常数项, 也就是 Pj 的原始私钥 uj
uj, err := pShares[:threshold+1].ReConstruct(tss.S256())
assert.NoError(t, err, "vss.ReConstruct should not throw error")
assert.Equal(t, uj, Pj.temp.ui)
// uG为原始私钥*G
uG := crypto.ScalarBaseMult(tss.EC(), uj)
assert.True(t, uG.Equals(Pj.temp.vs[0]), "ensure u*G[j] == V_0")
// Pj.data.Xi 为 round3 阶段算出来的私钥, 也是最终保存的私钥(savedata中的 Xi), 相当于 Alice 最终得到的分片 107
xj := Pj.data.Xi
gXj := crypto.ScalarBaseMult(tss.EC(), xj)
// gXj Pj 的最终私钥*G 也就是 Pj 的公钥, 也就是savedata 中的BigXj
BigXj := Pj.data.BigXj[j]
assert.True(t, BigXj.Equals(gXj), "ensure BigX_j == g^x_j")
// 污染一个分片, 然后恢复出私钥 uj,再乘*G得到假的公钥 应该不等于 Pj.temp.vs[0]
{
badShares := pShares[:threshold]
badShares[len(badShares)-1].Share.Set(big.NewInt(0))
uj, err := pShares[:threshold].ReConstruct(tss.S256())
assert.NoError(t, err)
assert.NotEqual(t, parties[j].temp.ui, uj)
BigXjX, BigXjY := tss.EC().ScalarBaseMult(uj.Bytes())
assert.NotEqual(t, BigXjX, Pj.temp.vs[0].X())
assert.NotEqual(t, BigXjY, Pj.temp.vs[0].Y())
}
u = new(big.Int).Add(u, uj)
}
// 从 最终计算结果 savedata 中取出最终的公钥
pkX, pkY := save.ECDSAPub.X(), save.ECDSAPub.Y()
pk := ecdsa.PublicKey{
Curve: tss.EC(),
X: pkX,
Y: pkY,
}
sk := ecdsa.PrivateKey{
PublicKey: pk,
D: u,
}
// 确保公钥位于 离散椭圆曲线上
assert.True(t, sk.IsOnCurve(pkX, pkY), "public key must be on curve")
// 使用私钥签名, 公钥验签名验证
assert.NotZero(t, u, "u should not be zero")
ourPkX, ourPkY := tss.EC().ScalarBaseMult(u.Bytes())
assert.Equal(t, pkX, ourPkX, "pkX should match expected pk derived from u")
assert.Equal(t, pkY, ourPkY, "pkY should match expected pk derived from u")
t.Log("Public key tests done.")
// 确认每一个 party 都计算得到同一把公钥
for _, Pj := range parties {
assert.Equal(t, pkX, Pj.data.ECDSAPub.X())
assert.Equal(t, pkY, Pj.data.ECDSAPub.Y())
}
t.Log("Public key distribution test done.")
// ecdsa 签名与验签
data := make([]byte, 32)
for i := range data {
data[i] = byte(i)
}
r, s, err := ecdsa.Sign(rand.Reader, &sk, data)
assert.NoError(t, err, "sign should not throw an error")
ok := ecdsa.Verify(&pk, data, r, s)
assert.True(t, ok, "signature should be ok")
t.Log("ECDSA signing test done.")
t.Logf("Start goroutines: %d, End goroutines: %d", startGR, runtime.NumGoroutine())
break keygen
}
}
}
}
-
Round 1: 承诺和预参数广播
- 生成原始私钥ui
- 根据原始私钥和threshold使用 feldman vss 生成 threshold+1 次方的曲线, 然后返回曲线的系数*G(G为椭圆曲线基点) 和曲线上的点, 也就是 shares 和 vs
- 广播承诺消息 - 发送vss承诺
-
Round 2: 分享发送和去承诺
- 向每个参与方发送其对应的秘密分享(基于本地多项式算出来给其它节点的秘密)
- 广播: VSS多项式, 例如 g i a g^a_i gia(去承诺)
-
Round 3: 验证和密钥计算
- 计算最终私钥分享 - 将所有收到的分享相加
- 验证VSS承诺 - 验证去承诺的有效性和分享正确性
- 计算公钥分享 - 为每个参与方计算对应的公钥(分片公钥)
- 生成ECDSA总公钥 - 计算最终的椭圆曲线公钥(将初广播过来的分片公钥相加)
-
Round 4: 最终验证和完成
- 完成协议 - 将最终结果发送到完成通道
主要参考
《线性代数(三) 线性方程组&向量空间》
《【MPC精讲】Shamir秘密分享》
《三分钟入门拉格朗日插值法》
《如何分享秘密1:可验证密钥分享》
《加密货币安全基石: 分布式密钥生成协议》
《MTA协议计算签名中的s》
《MTA协议计算签名中的r》
561

被折叠的 条评论
为什么被折叠?



