背景
在之前的 加密货币安全基石: 从拉格朗日插值到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, 但是可以验证每一个分片的合法性。
其中
因为 secret 和 q 都非常大,所以 根据 v[0]无法反推出 secret。
这就回到了椭圆曲线上的离散对数难题 : 给定一个 椭圆曲线起点G, 终点 Q, 大质数p, 要求找到满足下列条件的私钥 d 是非常困难的。
这就叫做椭圆曲线上的离散对数问题。 离散对数问题目前并无有效的攻击方法, 被认为是非常难以计算的数学问题, 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次多项式为例):
我们把share展开来, 可以继续往下写:
是不是发现什么有趣的东西, 分片和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 如何实现。