背景: 密钥分片算法被广泛应用在于各类加密货币钱包/交易所当中。有相当多的钱包/交易所都不再将钱包地址的私钥单独存储, 而是采取了分布式存储的方案。通常情况下至上分为两片,一片存储在用户的设备上,一片存储在钱包/交易所的服务器上(有的平台甚至提供更多分片方案)。这样的话,即使加密货币钱包/交易所有内鬼甚至平台跑路了, 用户的资金安全依然有保障。这篇文章致力于用最简单的数学原理讲述大名鼎鼎的 shamir secret sharing 共享密钥算法(也称密钥分片算法),从拉格朗日插值公式,推广到shamir算法,再到golang中使用 shamir 算法。
什么是拉格朗日插值法
说人话, 给定k个点, 拉格朗日插值法可以拟合一个多项式把这k个点穿起来。 更进一步, 给定k+1个点, 拉格朗日插值法可以唯一确定一个 k 次多项式穿过这k个点。
我们知道两点(不重合)唯一确定一条直线, 3点(不重合)确定一条抛物线,以此类推,k+1个点唯一确定一个 k次多项式。 拉格朗日插值法就是可以唯一确定这 k-1 个多项式。
拉格朗日插值数学表达式
给定二维坐标上 k+1 个点:
(x0,yo),(x1,y1),...,(xk,yk)
可以唯一确定的 k 次项表达式为:
L(x)=j=0∑kyjlj(x)
lj(x)=i=0,i=j∏kxj−xix−xi
举个例子
令 k =2 , 给定3个点可以唯一确定二次多项式
(x0,yo)=(4,10)(x0,yo)=(5,5.25)(x0,yo)=(6,1)
此时 有
L(x)=j=0∑kyjlj(x)=j=0∑2yjlj(x)
其中:
当j=0,l1(x)=i=0,i=0∏kxj−xix−xi=x0−x1x−x1⋅x0−x2x−x2=4−5x−5⋅4−6x−6当j=1,l1(x)=i=0,i=1∏kxj−xix−xi=x1−x0x−x0⋅x1−x2x−x2=5−4x−4⋅5−6x−6当j=2,l1(x)=i=0,i=2∏kxj−xix−xi=x2−x0x−x0⋅x2−x1x−x1=6−4x−4⋅6−5x−5
我们将上述的 三个式子相加, 就得到了:
L(x)=l0(x)+l1(x)+l2(x)=y0⋅x0−x1x−x1⋅x0−x2x−x2+y1⋅x1−x0x−x0⋅x1−x2x−x2+y2⋅x2−x0x−x0⋅x2−x1x−x1,(1)式=10⋅4−5x−5⋅4−6x−6+5.25⋅5−4x−4⋅5−6x−6+1⋅6−4x−4⋅6−5x−5=(5x2−55x+150)+(−5.25x2+52.5x−126)+(0.5x−4.5x+60),(2)式=0.25x2−7x+34
如何理解啦哥朗日插值法
我们来看上面的 (1式)
L(x)=y0⋅x0−x1x−x1⋅x0−x2x−x2+y1⋅x1−x0x−x0⋅x1−x2x−x2+y2⋅x2−x0x−x0⋅x2−x1x−x1,(1)式
我们将 x = x_0 , x = x_1, x=x_2 代入上式, 得到:
L(x0)=y0⋅x0−x1x0−x1⋅x0−x2x0−x2+0+0=y0⋅1=y0,L(x1)=0+y1⋅x1−x0x−x0⋅x1−x2x−x2+0=y1⋅1=y1,L(xd)=0+0+y2⋅x2−x0x−x0⋅x2−x1x−x1=y2⋅1=y0,
可以观察得到 当 x=x_0 时, 只有 y_0 的系数(也就是了l0(x))为1, 其他 y_n 的系数全部为0
同理 当 x=x_1 时, 只有 y_1 的系数(也就是了l1(x))为1, 其他 y_n 的系数全部为0;
当 x=x_2 时, 只有 y_2 的系数(也就是了l2(x))为1, 其他 y_n 的系数全部为0。
这就有意思了, 我们完全把 ln(x) 看作是一个开关或者说是二极管。
ln(x) 这个开关或者二极管, 只对 y=yn 打开且系数为1, 对其他 ym,m=n 则关闭 也就是系数为0.
那么 L(x) 就可以看成 yi 与其开关的 相乘 的累加, 直观理解, 拉格朗日插值公式可以写成这样:
L(x)=j=0∑kyi⋅yi的开关(也就是x的k次项)
上面这种直观的写法也就是上面 的 (2) 式:
L(x)=(5x2−55x+150)+(−5.25x2+52.5x−126)+(0.5x−4.5x+10),(2)式
这个式子可以看作是3个开关函数的相加。 x0,y0 经过第一个开关函数, x1,y1 经过第二个开关函数, x2,y2 经过第二个开关函数。
我们把这三个函数求出来, 然后相加 就得到了一条经过这3个点的二次多项式。
如果用图形画出来, 就是下面这样:

最终拉格朗日插值的公式就是红色的曲线, 其来源于 绿蓝黄 三条曲线的叠加。
这里点名表扬 Geogebra 提供的在线作图工具
拉格朗日推广到 shamir 密钥分片算法
回想一下拉格朗日插值解决了什么问题: 给定 k+1 个不同的点, 可以唯一确定 k 次多项式。 反过来, k 次多项式需要至少 k+1 个不同的点。如果少了个点, 只有k个点, 那么满足经过 k个点的 k次多项式有无限条。
我们把这个性质应用在密钥分片上面就非常有意思了: 随机生成 k+1 个不同的分片, 这 k+1 个不同的分片可以唯一确定一条 k次多项式的 曲线, 这条曲线的某一部分(比如常数项)就可以看作是最终的 密钥.
或者反过来说, 我们也可以 先确定一个 k次多项式, k次多项数的常数部分作为最终的密钥, 然后在这个k次多项式上面取 很多个点作为分片密钥, 至少需要 k+1 个分片密钥就能恢复出这个 k次多项式, 进而恢复出最终的密钥。
写成公式就是这样:
f(x)≡ao+a1x+a2x2+a3x3+a4x4+......+akxk(modp)secrete=a0
特别说明一下, 在实际的运算中用到了 对一个 大质数(素数) p 取模 运算是因为以下几个原因:
- 约束系数和点纵坐标点大小. 如果不进行取模运算,多项式的系数可能会变得非常大,这会增加信息泄露的风险。通过对大质数进行取模,可以限制多项式系数的大小,减少了信息泄露的可能性。
- 隐藏点的横坐标和纵坐标的关系,因为对一个大质数(素数)取模,相当于给输入和输出之间加了个哈希函数。因为是大质数(素数),所以出来的结果的防碰撞性会很好。这样即使是攻击者只要获取不到恢复密钥所需的最小阈值数量的分片密钥,他是无法通过观测已经获取到的分片(也就是横纵坐标的关系)来尝试恢复出完整密钥(完整多项式系数的)
- 增加计算复杂度,取模运算涉及到有限域的运算,这些运算在计算上是复杂和耗时的。
在这条曲线上去不同的点作为密钥分片, 需要 k+1 个分片即可恢复出 f(x) 这条曲线,进而恢复出常数部分的密钥。
这就是 shamir 密钥分片的底层数学原理
在 Golang 中使用 shamir 进行密钥分片创建与恢复
我们对一把 ECC(加密货币安全基石:直观理解ECC椭圆曲线加密算法)的私钥进行分片测试. 其中 testCase 的 Minimum 是需要恢复密钥的最小阈值, Shares 是分片的数量。
package shamir_demo
import (
shamir "github.com/SSSaaS/sssa-golang"
"testing"
)
func Test_a(t *testing.T) {
key := `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILgdGKSnnmbJqKeRvFIBkKSijI6A9PN0zQcnDTJWbENHoAoGCCqGSM49
AwEHoUQDQgAEEeO1ycZJC32ABXt9QZ8BbwIV+71Ai3OffrFslCotLEicb0zmWOe+
41snB5/pDJiX/m3VnA6C6B4yaXR0+iRMdg==
-----END EC PRIVATE KEY-----`
type testCase struct {
Minimum int
Shares int
}
tests := []testCase{
{2, 3},
{3, 5},
{5, 10},
{19, 20},
}
for i, tt := range tests {
created, err := shamir.Create(tt.Minimum, tt.Shares, key)
if err != nil {
t.Fatalf("Test case no.%d created err:%v \n", i, err)
}
recovered, err := shamir.Combine(created)
if err != nil {
t.Fatalf("Test case no.%d recovered failed,err:%v \n", i, err)
}
if recovered != key {
t.Fatalf("Test case no.%d recoverd an invalied key:%s \n", i, recovered)
}
}
t.Log("shamir test succeeded")
}
运行单测文件输出结果:
shamir_test.go:44: shamir test succeeded
PASS
巨人的肩膀:
- en.wikipedia.org/wiki/Shamir…
- zh.wikipedia.org/zh-tw/%E6%8…
- github.com/SSSaaS/sssa…
- www.geogebra.org/graphing
- juejin.cn/post/726388…