88911562

导航

 

ECDSA签名算法和HDWallet数学原理

golang对于ecdsa算法的实现

简述

椭圆曲线算法,
就是在椭圆曲线上的一系列的离散的有限的点, 并且定义了一个虚拟的0点(原点), 逆元, 加法和乘法二元运算
并且这些二元运算满足加法交换律和结合律.

这些点形成组成了一个有限域, 称为阿贝尔群.

私钥生成:randFieldElement

func randFieldElement(c elliptic.Curve, rand io.Reader) (k *big.Int, err error) {
	params := c.Params()
	b := make([]byte, params.BitSize/8+8)
	_, err = io.ReadFull(rand, b)
	if err != nil {
		return
	}

	k = new(big.Int).SetBytes(b)
	n := new(big.Int).Sub(params.N, one)
	k.Mod(k, n)
	k.Add(k, one)
	return
}

randFieldElement作用是使用 curve paramater 来生成一个新的私钥k.
参数ccurve paramater(或者叫 domain parameters)定义了在有限域中的椭圆曲线的阿贝尔群.

Our elliptic curve algorithms will work in a cyclic subgroup of an elliptic curve over a finite field. Therefore, our algorithms will need the following parameters:

  • The prime p that specifies the size of the finite field.
  • The coefficients a and b of the elliptic curve equation.
  • The base point G that generates our subgroup.
  • The order n of the subgroup.
  • The cofactor h of the subgroup. ($ h = N/n \(,其中\) N $ 是椭圆曲线的阶数)

In conclusion, the domain parameters for our algorithms are the sextuple (p,a,b,G,n,h).

生成签名:signGeneric

func signGeneric(pk *PrivateKey, csprng *cipher.StreamReader, c elliptic.Curve, hash []byte) (r, s *big.Int, err error) {
	N := c.Params().N
	if N.Sign() == 0 {
		return nil, nil, errZeroParam
	}
	var k, kInv *big.Int
	for {
		for {
			k, err = randFieldElement(c, *csprng)
			if err != nil {
				r = nil
				return
			}
			if in, ok := pk.Curve.(invertible); ok {
				kInv = in.Inverse(k)
			} else {
				kInv = fermatInverse(k, N) // N != 0
			}
			r, _ = pk.Curve.ScalarBaseMult(k.Bytes())
			r.Mod(r, N)
			if r.Sign() != 0 {
				break
			}
		}
		e := hashToInt(hash, c)
		s = new(big.Int).Mul(pk.D, r)
		s.Add(s, e)
		s.Mul(s, kInv)
		s.Mod(s, N) // N != 0
		if s.Sign() != 0 {
			break
		}
	}
	return
}

签名函数返回值包含两部分内容, sig = (r, s)

  • k为临时生成的私钥
  • e为签名数据的hash值的整数形式
  • R = k * G, 所以R为临时私钥k的公钥
  • 函数 Inverse, fermatInverse为由doman paramater(secp256k1)定义的椭圆曲线有限域定义的计算逆元的代数实现.
  • 函数 ScalarBaseMult 为有限域上的乘法的代数实现
  • pk 为私钥, P为公钥

代码中的两层for循环,是因为临时生成的私钥不满足条件(具体原因后面在说),for循环会重新再次随机生成临时私钥对于,99.99%的情况是,这个for循环只会执行一次.
所以根据代码可以把计算 rs 的代数表达式简单的写成:

\(\begin{aligned} r = R.x \end{aligned}\)

即 r 的几何意义为:临时私钥k的公钥R[ = k * G 为椭圆曲线上的一个点]的x坐标.

\(\begin{aligned} s = (e + r * pk) / k \end{aligned}\)

验证签名:verifyGeneric

func verifyGeneric(pub *PublicKey, c elliptic.Curve, hash []byte, r, s *big.Int) bool {
	e := hashToInt(hash, c)
	var w *big.Int
	N := c.Params().N
	if in, ok := c.(invertible); ok {
		w = in.Inverse(s)
	} else {
		w = new(big.Int).ModInverse(s, N)
	}
	u1 := e.Mul(e, w)
	u1.Mod(u1, N)
	u2 := w.Mul(r, w)
	u2.Mod(u2, N)
	// Check if implements S1*g + S2*p
	var x, y *big.Int
	if opt, ok := c.(combinedMult); ok {
		x, y = opt.CombinedMult(pub.X, pub.Y, u1.Bytes(), u2.Bytes())
	} else {
		x1, y1 := c.ScalarBaseMult(u1.Bytes())
		x2, y2 := c.ScalarMult(pub.X, pub.Y, u2.Bytes())
		x, y = c.Add(x1, y1, x2, y2)
	}
	if x.Sign() == 0 && y.Sign() == 0 {
		return false
	}
	x.Mod(x, N)
	return x.Cmp(r) == 0
}
  • 函数ScalarBaseMult是椭圆曲线有限域上n * G的乘法, 其中n为参数
  • 函数ScalarMult 是椭圆曲线有限域上定义的n * P的乘法, 第1,2个参数表示P的x和y坐标, 第3个参数为n.

根据函数的实现可以写出验证代数表达式, 并执行推导出如下结果:

\(\begin{aligned} e*G/s + r*Pub/s &= e*G/s + r*Pk*G/s \\ &= (e+r*Pk)*G/s \\ &= ((e + r * Pk) * G)\ /\ ((e + r * Pk) / k) \\ &= k * G \\ &= R \end{aligned}\)

  • e为签名数据hash值的整数形式

  • G为Domain parameters的中定义的椭圆曲线的生成点

  • k 为在signGeneric函数中生成的临时私钥

  • 传入的参数r, 为k的公钥的x坐标

  • R(代数表达式最后推出的结果), 就是k的公钥

函数verifyGeneric最后x.Cmp(r)==0就是比较上面的代数表达式推算出的的R.x(k的公钥的x坐标)和signGeneric(签名函数)返回的r(临时私钥的公钥的x坐标)是否相等来判断签名是否验证成功的.

分层确定钱包Hierarchical Deterministic Wallet

分层确定钱包的详细描述及相关细节在<<master bitcoin-HD Wallets (BIP-32/BIP-44)>><<BIP32>>中已经有非常详细的说明.

这里不再重复这些内容, 其中, 分层确定确定钱包有一个非常重要的特性:

A very useful characteristic of HD wallets is the ability to derive public child keys from public parent keys, without having the private keys.
HD wallets 一个非常有用的特性是:不需要知道父私钥,就能够通过父公钥派生出子公钥.

这个特性是分层确定钱包最奇妙的地方, 这一章节就是来讲清楚HD wallet这个特性背后的数学原理.

首先定义代表某些计算的符号如下:

  • point(p): returns the coordinate pair resulting from EC point multiplication (repeated application of the EC group operation) of the secp256k1 base point with the integer p.
  • ser32(i): serialize a 32-bit unsigned integer i as a 4-byte sequence, most significant byte first.
  • ser256(p): serializes the integer p as a 32-byte sequence, most significant byte first.
  • serP(P): serializes the coordinate pair P = (x,y) as a byte sequence using SEC1's compressed form: (0x02 or 0x03) || ser256(x), where the header byte depends on the parity of the omitted y coordinate.
  • parse256(p): interprets a 32-byte sequence as a 256-bit number, most significant byte first.

父扩展私钥派生子扩展私钥

The function CKDpriv((kpar, cpar), i) → (ki, ci) computes a child extended private key from the parent extended private key:

  • Check whether i ≥ 231 (whether the child is a hardened key).
    • If so (hardened child): let I = HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i)). (Note: The 0x00 pads the private key to make it 33 bytes long.)
    • If not (normal child): let I = HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i)).
  • Split I into two 32-byte sequences, IL and IR.
  • The returned child key ki is parse256(IL) + kpar (mod n).
  • The returned chain code ci is IR.
  • In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.)

函数CKDpriv为派生子私钥的函数, 参数和返回值解释为:Kpar(父私钥),Cpar(父链码),Ki子私钥, Ci(子链码)

其中point(kpar) = kpar * G = 父公钥, 记为k_pubpar

为了突出重点, 这里把上面的计算过程精简一为下面的过程:

  1. HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) 得到64个字节的数组
  2. 64个字节前32位作为子链码
  3. 后32位作为临时私钥kephemeral
  4. kephemeral + kpar 作为子私钥记为:kchild
  5. 根据子私钥可以通过( kchild * G)计算出子公钥记为k_pubchild

父扩展公钥派生子扩展公钥

The function CKDpub((Kpar, cpar), i) → (Ki, ci) computes a child extended public key from the parent extended public key. It is only defined for non-hardened child keys.

  • Check whether i ≥ 231 (whether the child is a hardened key).
    ** If so (hardened child): return failure
    ** If not (normal child): let I = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i)).
  • Split I into two 32-byte sequences, IL and IR.
  • The returned child key Ki is point(parse256(IL)) + Kpar.
  • The returned chain code ci is IR.
  • In case parse256(IL) ≥ n or Ki is the point at infinity, the resulting key is invalid, and one should proceed with the next value for i.

同样, 为了突出重点, 把上面描述的过程精简为下面的过程:

  1. HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) 得到64个字节的数组
  2. 64个字节的前32个字节作为子链码
  3. 后32位作为临时私钥kephemeral
  4. 然后计算kephemeral * G = k_pubephemeral 为临时公钥
  5. 然后计算 k_pubephemeral + k_pubpar 作为子公钥 = k_pubchild

又由于椭圆曲线上的点是一个阿贝尔群, 满足加法交换律和结合律, 可以有下面的推导过程:

\(\begin{aligned} k\_pub_{ephemeral} + k\_pub_{par} &= k_{ephemeral} * G + k_{par} * G \\ &= (k_{ephemeral} + k_{par}) * G \\ &= k_{child} * G \\ &= k\_pub_{child} \\ \end{aligned}\)

这就是为什么HD Wallet只需要暴露扩展公钥就能推测出子私钥地址的原因.

分层确定钱包的风险

分层确定钱包的风险请参考这篇文章:Private Key Recovery Combination Attacks

参考引用

Elliptic Curve Cryptography: a gentle introduction
ecdsa math
What is modular arithmetic
模运算
椭圆曲线加密算法
ECC椭圆曲线详解
bitcoin extendedkey 源码

posted on 2021-06-03 17:10  88911562  阅读(628)  评论(0编辑  收藏  举报