mpc门限钱包

前言

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=x1x2xx1

  • 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个方面:

  1. 多方共同生成一个钱包的私钥, 并对这个私钥进行分片。使得不同分片支持门限恢复操作。如将一个完整的密钥分片分成5份,需要两份即可恢复出完整密钥。 5 记做分片数量, 2记做门限数量。
  2. 多方协同签名, 使用多方的密钥分片对交易进行签名。具体来说, 每一个密钥分片对交易进行签名 并 拼凑起来的 签名 等价于 完整私钥对交易进行签名。
  3. 多方协同密钥重置, 密钥参与方经过协商, 将原本的私钥分片打散然后重新分配, 最终的完整私钥和公钥不变, 但是每个参与方持有的私钥分片发生改变, 整个协商过程中完整私钥都不会出现。

这里的多方(一般被称作party)是比较广义的, 可能有多重意义, 比如一个用户登录一台设备, 这台设备可以被称作一个 party。 比如加密币平台的服务器为客户端party生成一个临时的 进程, 这个进程与客户端的party一起协商出一把私钥, 这个进程也可以称作一个 party。 在密码学上面, 只要参与多方安全计算的实体都统称为 party。 生成的密钥分片也会被保存到多个地方, 通常是两到三个: 客户端, 服务端, 用户方(用户自己记在脑袋或者小本本炒好都可以)

通过上面的两个应用可以实现下面两个非常吸引人的性质:

  • 分布式完成密钥生成(密钥重置), 永远不会出现完整的 密钥, 无论是内存, 硬盘, 还是cpu, 服务器的任何硬件永远都不会出现完整的密钥
  • 签名过程, 无论是内存, 硬盘, 还是cpu, 服务器的任何硬件永远都不会出现使用完整的密钥对交易进行签名 但实现效果和使用一把完整密钥进行签名的效果一致

这两个性质就非常诱人, 即使钱包/交易所里面有内鬼, 内鬼也无法知道钱包的完整私钥, 因为完整的私钥压根从来就没有展示出来过。配合上零日志记录+端到端加密, 可以实现神仙来了都无法撼动的资产安全效果。

GG18

GG18 讲解了如何去中心化,分布式地生成密钥分片。
假设有4个用户Alice, Bob, Chalize, David 想要协商出一把共享密钥, 每个用户保存一个共享密钥的分片, 3个分片可以恢复出完整密钥。可以通过下几个步骤实现:

  1. Alice 选定自己的私钥10, Bob选定自己的私钥20, Chalize选定自己的私钥30, David选定自己的私钥40在这里插入图片描述

  2. 随后四个人各自计算自己的密钥分片

    • 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=dGQ=f(0)GQ=f1(0)G+f2(0)G+f3(0)G+f4(0)GQ=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

在这里插入图片描述
下面来看看 MTA 协议是如何通过 Paillier 同态加密实现的

在这里插入图片描述

  1. MTA 协议需要两个参与方, 命名为 Alice 和 Bob。 其中Alice 拥有 Paillier 公私钥对, 拥有 秘密值 a. Bob 拥有秘密值 b
  2. 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 进行加密。
  3. 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=bcA+EA(β)=bEA(a)+EA(β)=EA(ab+β)
  4. Bob 向 Alice 发送 c B = E A ( a b + β ′ ) cB= E_A(ab+ \beta') cB=EA(ab+β)
  5. Alice 解密 cB, 令 α = D A ( c B ) = a b + β ′ \alpha=D_A(cB)=ab+ \beta' α=DA(cB)=ab+β
  6. 然后我们计算: α + β = 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_1​w_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: 承诺和预参数广播

    1. 生成原始私钥ui
    2. 根据原始私钥和threshold使用 feldman vss 生成 threshold+1 次方的曲线, 然后返回曲线的系数*G(G为椭圆曲线基点) 和曲线上的点, 也就是 shares 和 vs
    3. 广播承诺消息 - 发送vss承诺
  • Round 2: 分享发送和去承诺

    1. 向每个参与方发送其对应的秘密分享(基于本地多项式算出来给其它节点的秘密)
    2. 广播: VSS多项式, 例如 g i a g^a_i gia(去承诺)
  • Round 3: 验证和密钥计算

    1. 计算最终私钥分享 - 将所有收到的分享相加
    2. 验证VSS承诺 - 验证去承诺的有效性和分享正确性
    3. 计算公钥分享 - 为每个参与方计算对应的公钥(分片公钥)
    4. 生成ECDSA总公钥 - 计算最终的椭圆曲线公钥(将初广播过来的分片公钥相加)
  • Round 4: 最终验证和完成

    1. 完成协议 - 将最终结果发送到完成通道

主要参考

线性代数(三) 线性方程组&向量空间
【MPC精讲】Shamir秘密分享
三分钟入门拉格朗日插值法
如何分享秘密1:可验证密钥分享
加密货币安全基石: 分布式密钥生成协议
MTA协议计算签名中的s
MTA协议计算签名中的r

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值