加密货币安全基石: 从Shamir到Feldman VSS算法, 以币安tss-lib库为例

465 阅读9分钟

背景

在之前的 加密货币安全基石: 从拉格朗日插值到shamir共享密钥算法, 我向大家讲述了sharmir算法是如何做密钥分片和密钥重建的, 其底层数学原理就是拉格朗日算法。

shamir 算法解决了一个密钥如何进行恢复与恢复,但是还有一些实践上的关键的安全性问题无法克服。 比如一个密钥分给3个节点, 再恢复密钥的时候需要有一个中心化节点负责收集每个节点的分片,如果有一个节点被劫持了,那么这个节点的分片就有可能被篡改,导致最终算出来的shared key是无效的。 这个问题可以看作是一个 拜占庭将军问题。 这个问题的存在是很危险的额, 相当于劫持了一个分片就可以篡改私钥, 进而篡改交易的签名.

你可能会这么想,我在中心节点里面把每个节点的分片保存一下,收集各个节点的分片和本地保存的分片比较一下不就行了吗? 这样乍一看很合理,实际上非常的不合理。中心节点既然都把分片保存下来了,那么还进行密钥的分片意义何在呢?之所以要分片就是为了降低密钥泄漏的风险,所以不能有一个节点保存完整的密钥。

有没有什么方法,能够既能够验证密钥分片的合法性,又不保存完整的分片呢? 答案是有的, Feldman - VSS(Verifiable secret sharing) 算法 应运而生

我们以Binance 的 bnb-chain/tss-lib 库为例,源码讲解 feldman vss 的原理和实现。 这大概是全网第一篇解释币安tss-lib的库了hhh

希望你拥有 ECC 椭圆曲线加密算法 和 Shamir 共享密钥算法的基础。 如果没有,可以去我的另外两篇文章提前预习 加密货币安全基石:直观理解ECC椭圆曲线加密算法加密货币安全基石: 从拉格朗日插值到shamir共享密钥算法

辅助函数和变量

为了方便大家后续理解,我将一些辅助函数和辅助的数据类型提取给大家预习一下

crypto/elliptic/params.go

首先是最重要的椭圆曲线参数

type CurveParams struct {
    P       *big.Int // the order of the underlying field
    N       *big.Int // the order of the base point
    B       *big.Int // the constant of the curve equation
    Gx, Gy  *big.Int // (x,y) of the base point
    BitSize int      // the size of the underlying field
    Name    string   // the canonical name of the curve
}

P 为一个大素数, 椭圆曲线上的计算得到一个点的横纵坐标最后需要对P取模, 一来增加随机性,二来约束值的范围, 换句话说也就是椭圆曲线上点的个数。N为基点的阶数,也就是N为乘以基点G的最大数字。 Gx, Gy 为基点G的横纵坐标。 Bitsize 为椭圆曲线所在有限域的位数, 位数越高,加密/签名算法的安全性越高,但计算性能也更低。 Name 为椭圆曲线的名称,一般有 P-224, P-256, P-384, P-512 这几种。

Share tss-lib/crypto/vss/feldman_vss.go

Share 类型也就是分片的类型, threshold 表示分片的最小数量,也就是多项式的次数+1, ID 为分片坐标点的横坐标, Share为分片坐标点的纵坐标

Share struct {
    Threshold int
    ID,       // xi
    Share *big.Int // Sigma i
}

GetRandomPositiveInt,tss-lib/common/random.go

GetRandomPositiveInt 负责生成随机函数,随机数范围数 [1,N], N一般为椭圆曲线参数的N。

func GetRandomPositiveInt(rand io.Reader, lessThan *big.Int) *big.Int {
    if lessThan == nil || zero.Cmp(lessThan) != -1 {
       return nil
    }
    var try *big.Int
    for {
       try = MustGetRandomInt(rand, lessThan.BitLen())
       if try.Cmp(lessThan) < 0 {
          break
       }
    }
    return try
}

CheckIndexes, tss-lib/crypto/vss/feldman_vss.go/CheckIndexes

CheckIndexes 为一个校验随机函数, 常和上面的GetRandomPositiveInt配套使用,用来校验随机数的合法性, 要求随机数的范围必须是 [1,N] 且不能重复。 这里的随机数后面回用作密钥分片的横坐标,所以不能重复。并且不能为0, 如果为0那么多项式只剩下常数了,常数也就是密钥,相当于某个分片直接就拿到的就是共享密钥 secret。

// Check share ids of Shamir's Secret Sharing, return error if duplicate or 0 value found
func CheckIndexes(ec elliptic.Curve, indexes []*big.Int) ([]*big.Int, error) {
    visited := make(map[string]struct{})
    for _, v := range indexes {
       vMod := new(big.Int).Mod(v, ec.Params().N)
       if vMod.Cmp(zero) == 0 {
          return nil, errors.New("party index should not be 0")
       }
       vModStr := vMod.String()
       if _, ok := visited[vModStr]; ok {
          return nil, fmt.Errorf("duplicate indexes %s", vModStr)
       }
       visited[vModStr] = struct{}{}
    }
    return indexes, nil
}

samplePolynomial, tss-lib/crypto/vss/feldman_vss.go

samplePolynomial 用于生成多项式系数, 也就是 拉格朗日多项式的系数,其中第一项为 最终的 shared secret.

func samplePolynomial(ec elliptic.Curve, threshold int, secret *big.Int, rand io.Reader) []*big.Int {
    q := ec.Params().N
    v := make([]*big.Int, threshold+1)
    v[0] = secret
    for i := 1; i <= threshold; i++ {
       ai := common.GetRandomPositiveInt(rand, q)
       v[i] = ai
    }
    return v
}

evaluatePolynomial tss-lib/crypto/vss/feldman_vss.go

evaluatePolynomial 用于计算多项式上的点,也就是给定横坐标,输出纵坐标,香港注释我都写好了

// Evauluates a polynomial with coefficients such that:
// evaluatePolynomial([a, b, c, d], x):
//
//  returns a + bx + cx^2 + dx^3
func evaluatePolynomial(ec elliptic.Curve, threshold int, v []*big.Int, id *big.Int) (result *big.Int) {
    q := ec.Params().N
    modQ := common.ModInt(q)
    result = new(big.Int).Set(v[0])
    X := big.NewInt(int64(1))
    for i := 1; i <= threshold; i++ {
       ai := v[i]                      // 多项式系数
       X = modQ.Mul(X, id)             // 第一次循环计算 x^1, 第二次循环计算 x^2, 第3次循环计算x^3, 以此类推...
       aiXi := new(big.Int).Mul(ai, X) // poly[i]*x^n
       result = modQ.Add(result, aiXi)
    }
    return
}

Feldman VSS 密钥分片

好了,现在直接来看 Fledman 如何做密钥分片, 我们直接从单侧函数入手

TestCreate, tss-lib/crypto/vss/feldman_vss_test.go

TestCreate 为密钥分片的单侧函数,我们以此为入口,开始讲解 feldman VSS 算法。

首先使用 定义了共享密钥 secret 分片的数量和阈值,分别是5,3。 也就是secret 被藏到了一个 2 次多项式里面, 二次多项式, 至少3个点可以恢复出多项式,进而恢复出 secret, 然后取5个点作为secret 的分片。

随后使用 common.GetRandomPositiveInt 生成了 随机数作为 共享密钥 secret

接着 使用 是用来 一个 ids 数组作为分片的横坐标集合.

然后调用 Create 函数生成 密钥分片 和 共享密钥 secret 的 commitments(代码里面为 vs,可以理解为 verify secret, 后续用来验证密钥的合法性 ) 。 vs 本质上是 多项式的系数 乘以 椭圆曲线的基点得到的终点坐标。

最后遍历了一下 vs 里面的坐标,要求不能为0,必须位于椭圆曲线上。

func TestCreate(t *testing.T) {
    num, threshold := 5, 3

    secret := common.GetRandomPositiveInt(rand.Reader, tss.EC().Params().N)

    ids := make([]*big.Int, 0)
    for i := 0; i < num; i++ {
       ids = append(ids, common.GetRandomPositiveInt(rand.Reader, tss.EC().Params().N))
    }

    vs, _, err := Create(tss.EC(), threshold, secret, ids, rand.Reader)
    assert.Nil(t, err)

    assert.Equal(t, threshold+1, len(vs))
    // assert.Equal(t, num, params.NumShares)

    assert.Equal(t, threshold+1, len(vs))

    // ensure that each vs has two points on the curve
    for i, pg := range vs {
       assert.NotZero(t, pg.X())
       assert.NotZero(t, pg.Y())
       assert.True(t, pg.IsOnCurve())
       assert.NotZero(t, vs[i].X())
       assert.NotZero(t, vs[i].Y())
    }
}

Create, tss-lib/crypto/vss/feldman_vss.go

接下来深入核心,看看Create 函数里面做了什么

func Create(ec elliptic.Curve, threshold int, secret *big.Int, indexes []*big.Int, rand io.Reader) (Vs, Shares, error) {
    if secret == nil || indexes == nil {
       return nil, nil, fmt.Errorf("vss secret or indexes == nil: %v %v", secret, indexes)
    }
    if threshold < 1 {
       return nil, nil, errors.New("vss threshold < 1")
    }

    ids, err := CheckIndexes(ec, indexes)
    if err != nil {
       return nil, nil, err
    }

    num := len(indexes)
    if num < threshold {
       return nil, nil, ErrNumSharesBelowThreshold
    }

    poly := samplePolynomial(ec, threshold, secret, rand)

    v := make(Vs, len(poly))
    for i, ai := range poly {
       v[i] = crypto.ScalarBaseMult(ec, ai)
    }

    shares := make(Shares, num)
    for i := 0; i < num; i++ {
       share := evaluatePolynomial(ec, threshold, poly, ids[i])
       shares[i] = &Share{Threshold: threshold, ID: ids[i], Share: share}
    }
    return v, shares, nil
}

首先校验了一下 secret 和 分片的横坐标不能为 nil, 其次校验了最小分片数量 threshold 不能小于1, 校验了分片的横坐标合法性(不能为0 也不能重复),校验了分片的个数不能小于阈值 threshold。

poly 也就是多项式的系数, poly[0] 也就是共享密钥 secret .

接下来是 VSS 的核心,计算 commitments V.

v[i] 通过多项式的系数去乘以椭圆曲线的基点G得到。 以后中心节点虽然不能通过 v 计算出 secret, 但是可以验证每一个分片的合法性。

其中

v[0]=(poly[0]G)modq=(secretG)modq,G为椭圆曲线基点v[0] = (poly[0]*G ) mod q= (secret * G) mod q , G 为椭圆曲线基点

因为 secret 和 q 都非常大,所以 根据 v[0]无法反推出 secret。

这就回到了椭圆曲线上的离散对数难题 : 给定一个 椭圆曲线起点G, 终点 Q, 大质数p, 要求找到满足下列条件的私钥 d 是非常困难的。

GdQ(modp)G^d≡Q(modp)

这就叫做椭圆曲线上的离散对数问题。 离散对数问题目前并无有效的攻击方法, 被认为是非常难以计算的数学问题, ECC 加密 , ECDSA 签名的安全性都来源于这个 特性。 同样的 tss-lib 中的 Feldman VSS 中的 commitment 安全性也来源于此。

最后使用 evaluatePolynomial 计算分片的纵坐标,也就是中多项式上的纵坐标。

Feldman VSS 密钥分片验证

TestVerify, tss-lib/crypto/vss/feldman_vss.go

生成密钥分片和 commitments的地方和 Create 中的一样,我们只需要关注 shares[i].Verify 是如何验证分片的合法性就好了

func TestVerify(t *testing.T) {
    num, threshold := 5, 3

    secret := common.GetRandomPositiveInt(rand.Reader, tss.EC().Params().N)

    ids := make([]*big.Int, 0)
    for i := 0; i < num; i++ {
       ids = append(ids, common.GetRandomPositiveInt(rand.Reader, tss.EC().Params().N))
    }

    vs, shares, err := Create(tss.EC(), threshold, secret, ids, rand.Reader)
    assert.NoError(t, err)

    for i := 0; i < num; i++ {
       assert.True(t, shares[i].Verify(tss.EC(), threshold, vs))
    }
}

Share.Verify, tss-lib/crypto/vss/feldman_vss.go

func (share *Share) Verify(ec elliptic.Curve, threshold int, vs Vs) bool {
    if share.Threshold != threshold || vs == nil || len(vs) != threshold+1 {
       return false
    }
    var err error
    modQ := common.ModInt(ec.Params().N)
    v, t := vs[0], one // YRO : we need to have our accumulator outside of the loop
    for j := 1; j <= threshold; j++ {
       // t = k_i^j
       t = modQ.Mul(t, share.ID)
       // v = v * v_j^t
       vjt := vs[j].SetCurve(ec).ScalarMult(t)
       v, err = v.SetCurve(ec).Add(vjt)
       if err != nil {
          return false
       }
    }
    sigmaGi := crypto.ScalarBaseMult(ec, share.Share)
    return sigmaGi.Equals(v)
}

Verify 首先校验了了 分配最小个数 threshold 和 多项式系数直接关系的合法性。

随后的计算过程比较负责,我们先从最下面的 sigmaGi 看起

sigmaGi := crypto.ScalarBaseMult(ec, share.Share)

上面这行代码翻译成书序公式就是这样(以3次多项式为例):

sigmaGi=(shareGmodqsigmaGi = (share * G) modq

我们把share展开来, 可以继续往下写:

sigmaGi=(shareGmodq=((a+bx+cx2+dx3)G)modq=(aG)modq+(bxG)modq+(cx2G)modq+(dx3G)modq=v[0]+v[1]x+v[2]x2+v[3]x3sigmaGi = (share * G) modq \newline = ((a+bx+cx^2+dx^3)* G ) mod q \newline = (a * G) mod q + (bx * G) mod q +(cx^2 * G) mod q +(dx^3 * G) mod q \newline = v[0] + v[1]x +v[2]x^2 +v[3]x^3

是不是发现什么有趣的东西, 分片和commitents 扯上关系了,可以利用v校验 share的合法性

再来看这几行代码:

v, t := vs[0], one // YRO : we need to have our accumulator outside of the loop
    for j := 1; j <= threshold; j++ {
       // t = k_i^j
       t = modQ.Mul(t, share.ID)
       // v = v * v_j^t
       vjt := vs[j].SetCurve(ec).ScalarMult(t)
       v, err = v.SetCurve(ec).Add(vjt)
       if err != nil {
          return false
       }
    }

这几行代码老实话写得有点绕,我们把for循环每一轮拆开来来看:

j=1 时:
    t = 1*x modq
    vjt= = v[1]*x *G mod q
    v= (v[0] + v[1]*x)*G modq 
     = (a * G) mod q + (bx*G ) modq

这样看是不是一下就懂了,这个循环就是在计算最后的v, 从而和 sigmaGi 进行比较。

总结

我们简要复习了 sharmir 算法, 解释了 sharmir 算法在工程上实现的可能风险, 即中心节点无法验证分片的合法性, 存在分片节点被篡改导致计算出来的 共享密钥 被篡改. 于是 Feldman VSS 应运而生, 其提供了一种无需保存 密钥明文却可以验证 分配合法性的机制。 最后我们以 币安的 tss-lib 为例, 详解了 Feldman VSS 如何实现。

巨人的肩膀

  1. github.com/bnb-chain/t…
  2. zh.wikipedia.org/zh-tw/%E6%8…