1. 程式人生 > >【我的區塊鏈之路】- go實現區塊鏈中常見的各類演算法

【我的區塊鏈之路】- go實現區塊鏈中常見的各類演算法

咳咳,為什麼要出這一篇文章呢?首先,這段時間本人在找工作,然後被問到了各類演算法的底層細節,有些確實很懵逼。這裡做個總結,也順便給大家歸納歸納一下!

上主題:

橢圓曲線加密

我們先來說一說最常用的 ECC 吧,ECC 就是 Elliptic Curve Cryptography 的縮寫。那麼,在說橢圓曲線加密之前,我們來說一說什麼是橢圓曲線?

中學的時候我們學過圓錐曲線,比如橢圓、雙曲線和拋物線。因為描述這些曲線的方程都是二次方程,圓錐曲線又被稱為二次曲線。而橢圓曲線是則是由三次方程描述的一些曲線。更準確地說,橢圓曲線是由下面的方程描述的曲線

                                                                                 


需要注意的是,橢圓曲線之所以叫“橢圓曲線”,是因為其曲線方程跟利用微積分計算橢圓周長的公式相似。實際上它的影象跟橢圓完全不搭邊。

如橢圓曲線 y^2=x^3−x+1 的影象:

【注意】橢圓曲線有這樣的兩個性質:

  1. 關於X軸對稱
  2. 畫一條直線跟橢圓曲線相交,它們最多有三個交點

橢圓曲線上的運算

由於橢圓曲線加密進行的運算實際上都是在橢圓曲線上進行的,必須注意的是,這裡把這些運算稱為“加法”和“乘法”僅僅是方便描述,他們跟平時認知的加法和乘法完全是兩碼事,完全可以給他們取其它名字(比如”乘法“和”冪運算“等)。總之就是規定,規定,規定(重要的事說三遍)

  • 首先定義座標系中距離X軸無窮遠點為橢圓曲線上的一個特殊點,稱為0點


    那麼此時上述第二條性質可以加強為:過曲線上任意兩點(可重合)的直線必定與曲線相交於第三點。

  • 然後定義橢圓曲線上點的加法。設橢圓曲線上有兩點,A和B點,那麼作過這兩點的直線與該曲線相交於第三點(C點),然後關於X軸對稱得到D點,則D為這兩個點的和,記作D=A+B (注意: 這僅僅是規定)。很明顯,D點也在該曲線上。所以橢圓曲線上兩點之和也是曲線上的點

特別地,如果兩點重合,則作橢圓曲線在A點處的切線,與曲線相交於第二點(B點),然後關於X軸對稱得到C點,則C點為A點與自身的和,記作 C =  2A

加法,我們可以得到以下結論:

  • A+B = B+A
    也就是橢圓曲線上的加法滿足交換律。

  • A+0 = A
    因為0點是無窮遠點,所以過A點與0點的直線是垂直於X軸的,它與曲線相交於另一點B點,那麼B點關於X軸對稱的點就是A點,即A點為A點和0點之和。

然後在加法的基礎上,定義橢圓曲線上點的乘法。

乘法:(下列的也是一種規定,是規定)

P是橢圓曲線上的一個點,那麼正整數k乘以點P的結果由下面的式子定義,注意式子中的加法是上面提到的橢圓曲線上點的加法:

1∗P=P1
2∗P=P+P
3∗P=2∗P+P
… 
k∗P=(k−1)∗P+P

乘法滿足以下性質:

對於任意正整數k和j,有 
k∗(j∗P) = (kj)∗P = (jk)∗P = j∗(k∗P)

知道公鑰反推私鑰:

k 為正整數,P 是橢圓曲線上的點(稱為基點),已知 k∗P P,計算 k

進一步為:

k 為正整數,P 是橢圓曲線上的點,已知 P^kP,計算 k = logP P^k

以上,是複雜度很高的操作,公鑰反推私鑰很難求 (在橢圓曲線演算法中很難求)

【注意】:

密碼學中,並不能使用上面介紹的實數域上的橢圓曲線。因為:
1. 實數域上的橢圓曲線是連續的,有無限個點,密碼學要求有限點
2. 實數域上的橢圓曲線的運算有誤差,不精確。密碼學要求精確

所以我們需要引入有限域上的橢圓曲線

有限域上的橢圓曲線:

所謂有限域上的橢圓曲線,簡單來說就是滿足下面式子要求的曲線(x, y, a, b都是小於素數【也即是質數】p的 非負整數):

                                                                

對比一下原先的橢圓曲線的方程:

                                                               

可以看到這個只是對原式進行了簡單的取模處理而已

按數論定義,有限域GF(p)指給定某個質數p,由0、1、2......p-1共p個元素組成的整數集合。且方程的座標點滿足了在有限域中定義的加減乘除運算的結果也應該是在域中。

假設橢圓曲線為 y² = x³ + x + 1,其在有限域GF(23)上時,寫作:
                   

                            y² ≡ x³ + x + 1 (mod 23)

此時,橢圓曲線不再是一條光滑曲線,而是一些不連續的點,如以點(1,7)為例,7² ≡ 1³ + 1 + 1 ≡ 3 (mod 23)。如此還有如下點:
 
  (0,1) (0,22)
  (1,7) (1,16)
  (3,10) (3,13)
  (4,0)
  (5,4) (5,19)
  (6,4) (6,19)
  (7,11) (7,12)
  (9,7) (9,16)
  (11,3) (11,20)
  等等。
 
另外,如果P(x,y)為橢圓曲線上的點,則-P即(x,-y)也為橢圓曲線上的點。如點P(0,1),-P=(0,-1)=(0,22)也為橢圓曲線上的點。看圖:(該圖的點全部以 y = 23/2 對稱)並不代表著關於某水平線對稱哦,參考:https://www.cnblogs.com/X-knight/p/9153209.html

                                

又如下圖是橢圓曲線 y² = x³−x+1 對素數97取模後的影象:

原本連續光滑的曲線變成了離散的點,基本已經面目全非了,但是依然可以看到它是關於某條水平直線(y= 97/2)對稱的。而且上面定義的橢圓曲線的加法仍然可用(當然乘法也可以)

【注意】:密碼學中有限域上的橢圓曲線一般有兩種,一種是定義在以素數p為模的整數域GF(p),也就是上面介紹的;另一種則是定義在特徵為 2 的伽羅瓦域 GF(2^m)上。
 

好了我們下面來看看,逼逼了這麼久我們到底是要做什麼?

用生成的私鑰 + 橢圓曲線公鑰的過程就是計算xG的座標的過程

計算 xG 就是 【私鑰 * 基點 = 公鑰】

 
  相關公式如下:
  有限域GF(p)上的橢圓曲線 y² = x³ + ax + b,若P(Xp, Yp), Q(Xq, Yq),且P≠-Q,則R(Xr,Yr) = P+Q 由如下規則確定:
 
  Xr = (λ² - Xp - Xq) mod p
  Yr = (λ(Xp - Xr) - Yp) mod p


  其中【公式 1】 λ = (Yq - Yp)/(Xq - Xp) mod p(若P≠Q), 【公式 2】 λ = (3Xp² + a)/2Yp mod p(若P=Q)
 
  因此,有限域GF(23)上的橢圓曲線 y² ≡ x³ + x + 1 (mod 23),假設以(0,1)為G點,計算2G、3G、4G...xG等等,方法如下:
 
  計算2G:

       2G = G + G ,所以用 【公式 2】求 λ
  λ = (3x0² + 1)/2x1 mod 23 = (1/2) mod 23 = 12
  Xr = (12² - 0 - 0) mod 23 = 6
  Yr = (12(0 - 6) - 1) mod 23 = 19
  即2G為點(6,19)
 
  計算3G:
  3G = G + 2G,即(0,1) + (6,19),用【公式 1】求 λ
  λ = (19 - 1)/(6 - 0) mod 23 = 3
  Xr = (3² - 0 - 6) mod 23 = 3
  Yr = (3(0 - 3) - 1) mod 23 = 13
  即3G為點(3, 13)
 
  同理計算4G、5G...xG,分佈如下圖:
 

所以,上述就是在說,我們如何根據 提前生成好的私鑰 X (私鑰 可以是有某種隨機演算法求出來的一個 數字) 和 選定的橢圓曲線 (如:Curve25519,prime256v1,secp256k1)(以上曲線均為有限質數域下的橢圓曲線,表現為 離散的點,根據 y = 質數P/2 水平線對稱),求出 公式  XG 點的 座標 (x ,y) 既是組成公鑰的 x 和 y。(其中 G 為選好的橢圓曲線上的基點,即 (x,y) == (0, 0)求出來的曲線上的座標點,XG 也是曲線上的一點。

實際應用中,我們並不需要關心橢圓曲線的眾多引數如何選取(要選對引數 a, b 對於普通使用者來說並不現實),只要從密碼學家們精心挑選的一堆曲線中選擇一個就行了。一般來說曲線Curve25519,prime256v1是比較常用的,比特幣選擇secp256k1則是有自己的考量


建立基於橢圓曲線的加密機制,需要找到類似RSA質因子分解或其他求離散對數這樣的難題。而橢圓曲線上的已知G和xG求x,是非常困難的(為什麼呢?請看計算出XG的步驟就知道的,每一步的 λ 都是不一樣的),此即為橢圓曲線上的的離散對數問題。此處x即為私鑰,xG即為公鑰


 
橢圓曲線加密演算法原理如下:


 
  設私鑰、公鑰分別為k、Y,即Y = kG,其中G為基點
 
  公鑰加密
  選擇隨機數r,將訊息M生成密文C,該密文是一個點對,即:
  C = {rG, M+rY},其中Y為公鑰
 
  私鑰解密
  M + rY - k(rG) = M + r(kG) - k(rG) = M
  其中k、Y分別為私鑰、公鑰。

橢圓曲線簽名演算法原理

 
  橢圓曲線簽名演算法,即 ECDSA
  設私鑰、公鑰分別為k、Y,即 Y = kG,其中G為G點。
 
  私鑰簽名:
  1、選擇隨機數 r,計算點 rG(x, y)
  2、根據隨機數r、訊息M的雜湊h、私鑰 k,計算 s = (h + kx)/r
  3、將訊息M、和 簽名{rG, s}發給接收方。
 
  公鑰驗證簽名:
  1、接收方收到訊息M、以及簽名{rG=(x,y), s}。
  2、根據訊息求雜湊h。
  3、使用傳送方公鑰K計算:hG/s + xY/s,並與rG比較,如相等即驗籤成功。
 
  原理如下:
  hG/s + xY/s = hG/s + x(kG)/s = (h+xk)G/s
  = r(h+xk)G / (h+kx) =  rG

Diffie–Hellman金鑰交換 (DH)

Diffie–Hellman金鑰交換(以下簡稱DH)是用於雙方在可能被竊聽環境下安全交換金鑰的一種方法。 
演算法的安全性是由上面提到的離散對數難題保證。

具體演算法流程如下:

  • 小紅和小明約定 p 和 g 的值
  • 小紅生成私鑰 x,計算 g^x mod p 作為 公鑰 公佈出去
  • 小明生成私鑰 y,計算 g^y mod p 作為 公鑰 公佈出去
  •     小紅得知 g^y mod p後,計算 

            s = (g^y mod p)^x mod p = (g^y)^x mod p = g^xy mod p

  • 小明得到gxmodpgxmodp後,計算 

            s = (g^x mod p)^y mod p = (g^x)^y mod p = g^xy mod p

  • 雙方都得到了相同的金鑰的ss,交換完畢

上面的流程中,x 和 y 始終由兩人自行保管的,第三方竊聽得到的只有 p、g、g^x mod p和 g^y mod p這幾個值。
上面說過,離散對數是很難算的,所以第三方不能由這些資訊計算出 x 或 y,也就沒辦法計算出金鑰 s 了

基於橢圓曲線的DH金鑰交換(ECDH)

ECDH跟DH的流程基本是一致的。

  • 小紅和小明約定使用某條橢圓曲線(包括曲線引數,有限域引數以及基點P等)
  • 小紅生成私鑰 x,計算 x∗P 作為公鑰公佈出去
  • 小明生成私鑰 y,計算 y∗P 作為公鑰公佈出去
  • 小紅得知 y∗P 後,計算 

       s = x∗(y∗P) = xy∗P

  • 小明得到x∗Px∗P後,計算 

       s = y∗(x∗P) = yx∗P

  • 雙方都得到了相同的金鑰的ss,交換完畢

由於計算橢圓曲線上的離散對數是很難的,所以第三方沒辦法在只知道 x∗P 和 y∗P 的情況下計算出 x 或 y 的值。好了,下面我們來看看橢圓曲線的 go 程式碼實現吧。【主要為 go 原生的crypto/elliptic和crypto/ecdsa包下的程式碼分析】

如程式碼:

/**
曲線介面
庫引用路徑: crypto/elliptic
程式碼所在檔案路徑:src/crypto/elliptic/elliptic.go
*/
type Curve interface {
	// 獲取橢圓曲線引數
	Params() *CurveParams
	// 某點是否在曲線上
	IsOnCurve(x, y *big.Int) bool
	// 加法 (x1,y1) + (x2,y2)
	Add(x1, y1, x2, y2 *big.Int) (x, y *big.Int)
	// 二倍運算 2*(x,y)
	Double(x1, y1 *big.Int) (x, y *big.Int)
	// 即乘法 k*(Bx,By) 
	ScalarMult(x1, y1 *big.Int, k []byte) (x, y *big.Int)
	// 即 k*G 其中G 為基點.
	ScalarBaseMult(k []byte) (x, y *big.Int)
}

上述程式碼是操作曲線 實現物件的,主要定義了判斷曲線上的點,返回曲線的基本引數 (P, n, b,Gx,Gy,BitSize, Name),及曲線上點的加法和乘法運算。

我們再看看,CurveParams 曲線的主要組成引數都是些什麼:【其實CurveParams也就是 Curve 的實現】

/**
曲線的實現結構體
程式碼引用路徑:crypto/elliptic
程式碼所在檔案路徑:src/crypto/elliptic/elliptic.go
*/
type CurveParams struct {
    //有限域GF(p)中質數p
    P       *big.Int
    //G點的階
    //如果存在最小正整數n,使得nG=O∞,則n為G點的階
    N       *big.Int
    //橢圓曲線方程y²= x³-3x+b中常數b
    B       *big.Int
    //G點(x,y)
    Gx, Gy  *big.Int
    //金鑰長度
    BitSize int
    //橢圓曲線名稱
    Name    string
}

func (curve *CurveParams) Params() *CurveParams {
    //獲取橢圓曲線引數,即curve
    return curve
}

func (curve *CurveParams) IsOnCurve(x, y *big.Int) bool {
    //是否在曲線y²=x³-3x+b上
    // y² = x³ - 3x + b
	y2 := new(big.Int).Mul(y, y)
	y2.Mod(y2, curve.P)

	x3 := new(big.Int).Mul(x, x)
	x3.Mul(x3, x)

	threeX := new(big.Int).Lsh(x, 1)
	threeX.Add(threeX, x)

	x3.Sub(x3, threeX)
	x3.Add(x3, curve.B)
	x3.Mod(x3, curve.P)

	return x3.Cmp(y2) == 0
}

func (curve *CurveParams) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) {
    //加法運算,程式碼略
}

func (curve *CurveParams) Double(x1, y1 *big.Int) (*big.Int, *big.Int) {
    //二倍運算,程式碼略
}

func (curve *CurveParams) ScalarMult(Bx, By *big.Int, k []byte) (*big.Int, *big.Int) {
    //k*(Bx,By),程式碼略
}

func (curve *CurveParams) ScalarBaseMult(k []byte) (*big.Int, *big.Int) {
    //k*G, G為基點,程式碼略
}

我們再往下看看公私鑰對,及簽名驗籤等操作的程式碼:

//程式碼位置src/crypto/ecdsa/ecdsa.go

// 公鑰
type PublicKey struct {
    // 曲線例項
	elliptic.Curve
    // 公鑰對應去上線的一點座標 (即:kG的座標,其中名k為私鑰,G為基點)
	X, Y *big.Int
}

// 私鑰
type PrivateKey struct {
    // 公鑰例項
	PublicKey
    // 私鑰的數字,即kG中的k
	D *big.Int
}


// 簽名
// rand 隨機寫入流
// priv 私鑰
// 待簽名Hash
// @return r 簽名的 {r, s}中的r
// @return s 簽名的 {r, s}中的s
// @return err
func Sign(rand io.Reader, priv *PrivateKey, hash []byte) (r, s *big.Int, err error) {
    entropylen := (priv.Curve.Params().BitSize + 7) / 16
    if entropylen > 32 {
        entropylen = 32
    }
    entropy := make([]byte, entropylen)
    _, err = io.ReadFull(rand, entropy)
    if err != nil {
        return
    }

    md := sha512.New()
    md.Write(priv.D.Bytes()) //私鑰
    md.Write(entropy)
    md.Write(hash)
    key := md.Sum(nil)[:32]

    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, nil, err
    }

    csprng := cipher.StreamReader{
        R: zeroReader,
        S: cipher.NewCTR(block, []byte(aesIV)),
    }

    c := priv.PublicKey.Curve //橢圓曲線
    N := c.Params().N //G點的階
    if N.Sign() == 0 {
        return nil, nil, errZeroParam
    }
    var k, kInv *big.Int
    for {
        for {
            //取隨機數k
            k, err = randFieldElement(c, csprng)
            if err != nil {
                r = nil
                return
            }

            //求k在有限域GF(P)的逆,即1/k
            if in, ok := priv.Curve.(invertible); ok {
                kInv = in.Inverse(k)
            } else {
                kInv = fermatInverse(k, N) // N != 0
            }

            //求r = kG
            r, _ = priv.Curve.ScalarBaseMult(k.Bytes())
            r.Mod(r, N)
            if r.Sign() != 0 {
                break
            }
        }

        e := hashToInt(hash, c) //e即雜湊
        s = new(big.Int).Mul(priv.D, r) //Dr,即DkG
        s.Add(s, e) //e+DkG
        s.Mul(s, kInv) //(e+DkG)/k
        s.Mod(s, N) // N != 0
        if s.Sign() != 0 {
            break
        }

        //簽名為{r, s},即{kG, (e+DkG)/k}
    }

    return
}

// 驗籤
// pub 公鑰
// hash 等待和簽名做對比的被簽名的原文Hash (和 簽名方法入參中的待簽名Hash)
// r 簽名的 {r, s}中的r
// s 簽名的 {r, s}中的s
// @return bool 是否通過校驗 true 是 false 否
func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool {
    c := pub.Curve //橢圓曲線
    N := c.Params().N //G點的階

    if r.Sign() <= 0 || s.Sign() <= 0 {
        return false
    }
    if r.Cmp(N) >= 0 || s.Cmp(N) >= 0 {
        return false
    }
    e := hashToInt(hash, c) //e即雜湊

    var w *big.Int
    //求s在有限域GF(P)的逆,即1/s
    if in, ok := c.(invertible); ok {
        w = in.Inverse(s)
    } else {
        w = new(big.Int).ModInverse(s, N)
    }

    u1 := e.Mul(e, w) //即e/s
    u1.Mod(u1, N)
    u2 := w.Mul(r, w) //即r/s
    u2.Mod(u2, N)

    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()) //即eG/s
        x2, y2 := c.ScalarMult(pub.X, pub.Y, u2.Bytes()) //即DGr/s
        //即eG/s + DGr/s = (e + Dr)G/s
        //= (e + Dr)kG / (e + DkG) = (e + Dr)r / (e + Dr) = r
        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
}

使用如下:

package main
import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"log"
	"fmt"
	"crypto/sha256"
)
func main() {
	// 先獲取一個橢圓例項
	curve := elliptic.P256()
	//得到私鑰
	privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
	if err != nil {
		log.Panic(err)
	}
	//產生公鑰
	publicKey := privateKey.PublicKey
	fmt.Println("priKey", privateKey, "\npubKey", publicKey)

	strHash := sha256.New().Sum([]byte( "我是學生"))
	// 簽名
	r, s, err := ecdsa.Sign(rand.Reader, privateKey, strHash)
	if nil != err {
		log.Panic(err)
	}
	fmt.Println("r", r, "\ns", s)
	strHash2 := sha256.New().Sum([]byte("我是個程式設計師"))
	// 驗籤
	fmt.Println(ecdsa.Verify(&publicKey, strHash2, r, s))  	// false
	fmt.Println(ecdsa.Verify(&publicKey, strHash, r, s))	// true
}

好了,以上就是對橢圓曲線加密的講解,其實以太坊中不是直接用go的原生庫crypto中的ecdsa哦,而是用了比特幣所使用的一個C++的庫 libsecp256k1,在目錄:crypto/secp256k1 路徑中。

【本文還未寫完,國慶這幾天會完善完.......】