區塊鏈背後的資訊保安(4)RSA加解密及簽名演算法的技術原理及其Go語言實現

尹成發表於2018-05-21
# RSA加解密及簽名演算法的技術原理及其Go語言實現

對稱加密中,加密和解密使用相同的金鑰,因此必須向解密者配送金鑰,即金鑰配送問題。
而非對稱加密中,由於加密和解密分別使用公鑰和私鑰,而公鑰是公開的,因此可以規避金鑰配送問題。
非對稱加密演算法,也稱公鑰加密演算法。

1977年,Ron Rivest、Adi Shamir、Leonard Adleman三人在美國公佈了一種公鑰加密演算法,即RSA公鑰加密演算法。
RSA是目前最有影響力和最常用的公鑰加密演算法,可以說是公鑰加密演算法的事實標準。

### RSA加密原理

使用M和C分別表示明文和密文,則RSA加密、解密過程如下:



其中e、n的組合(e, n)即為公鑰,d、n的組合(d, n)即為私鑰。
當然e、d、n並非任意取值,需要符合一定條件,如下即為e、d、n的求解過程。

### 生成金鑰對

e、d、n的求解過程,也即生成金鑰對的過程。涉及如下步驟:
* 1、取兩個大質數(也稱素數)p、q,n = pq。
* 2、取正整數e、d,使得ed mod (p-1)(q-1) = 1,也即:ed ≡ 1 mod (p-1)(q-1)。
e和d是模(p-1)(q-1)的乘法逆元,僅當e與(p-1)(q-1)互質時,存在d。

舉例驗證:

* 1、取p、q分別為13、17,n = pq = 221。
* 2、而(p-1)(q-1) = 12x16 = 192,取e、d分別為13、133,有13x133 mod 192 = 1
取明文M = 60,公鑰加密、私鑰解密,加密和解密過程分別如下:



### RSA加密原理證明過程



### 手動求解金鑰對中的d

ed mod (p-1)(q-1) = 1,已知e和(p-1)(q-1)求d,即求e對模(p-1)(q-1)的乘法逆元。
如上面例子中,p、q為13、17,(p-1)(q-1)=192,取e=13,求13d mod 192 = 1中的d。

13d ≡ 1 (mod 192),在右側新增192的倍數,使計算結果可以被13整除。
13d ≡ 1 + 192x9 ≡ 13x133 (mod 192),因此d = 133

其他計算方法有:費馬小定律、擴充套件歐幾里得演算法、尤拉定理。

### RSA安全性

由於公鑰公開,即e、n公開。
因此破解RSA私鑰,即為已知e、n情況下求d。
因ed mod (p-1)(q-1) = 1,且n=pq,因此該問題演變為:對n質因數分解求p、q。

目前已被證明,已知e、n求d和對n質因數分解求p、q兩者是等價的。
實際中n長度為2048位以上,而當n>200位時分解n是非常困難的,因此RSA演算法目前仍被認為是安全實用的。

### RSA計時攻擊和防範

RSA解密的本質是模冪運算,即:



其中C為密文,(d,n)為私鑰,均為超過1024位的大數運算,直接計算並不可行,因此最經典的演算法為蒙哥馬利演算法。
而這種計算是比較是耗時的,因此攻擊者可以觀察不同的輸入對應的解密時間,通過分析推斷私鑰,稱為計時攻擊。
而防範RSA計時攻擊的辦法,即在解密時加入隨機因素,使得攻擊者無法準確獲取解密時間。

具體實現步驟如下:



### go標準庫中的RSA加解密實現

go標準庫中解密即實現了對計時攻擊的防範,程式碼如下:

```go
//加密
//m為明文
//(pub.E, pub.N)為公鑰
//c為密文
func encrypt(c *big.Int, pub *PublicKey, m *big.Int) *big.Int {
    e := big.NewInt(int64(pub.E))
    c.Exp(m, e, pub.N)
    return c
}

//解密
//傳入random支援防範計時攻擊
func decrypt(random io.Reader, priv *PrivateKey, c *big.Int) (m *big.Int, err error) {
    if c.Cmp(priv.N) > 0 {
        err = ErrDecryption
        return
    }
    if priv.N.Sign() == 0 {
        return nil, ErrDecryption
    }

    var ir *big.Int
    if random != nil {
        var r *big.Int

        for {
            //步驟1產生0至n-1之間隨機數r
            r, err = rand.Int(random, priv.N)
            if err != nil {
                return
            }
            if r.Cmp(bigZero) == 0 {
                r = bigOne
            }
            var ok bool
            //r的模n的乘法逆元ir,步驟4中使用
            ir, ok = modInverse(r, priv.N)
            if ok {
                break
            }
        }
        bigE := big.NewInt(int64(priv.E))
        //計算步驟2中C'
        rpowe := new(big.Int).Exp(r, bigE, priv.N) // N != 0
        cCopy := new(big.Int).Set(c)
        cCopy.Mul(cCopy, rpowe)
        cCopy.Mod(cCopy, priv.N)
        c = cCopy
    }

    if priv.Precomputed.Dp == nil {
        //步驟3,使用C'計算對應的M'
        m = new(big.Int).Exp(c, priv.D, priv.N)
    } else {
        //略
    }

    if ir != nil {
        //步驟4計算實際的M
        m.Mul(m, ir)
        m.Mod(m, priv.N)
    }

    return
}
//程式碼位置src/crypto/rsa/rsa.go
```

### RSA簽名和驗籤的原理

非對稱加密演算法,除支援加密外,還可以實現簽名。原理如下:

簽名:
* 1、提取訊息摘要,使用傳送方私鑰對訊息摘要加密,生成訊息簽名。
* 2、將訊息簽名和訊息一起,使用接收方公鑰加密,獲得密文。

驗籤:
* 1、使用接收方私鑰對密文解密,獲得訊息和訊息簽名。
* 2、使用傳送方公鑰解密訊息簽名,獲得訊息摘要。
* 3、使用相同辦法重新提取訊息摘要,與上一步中訊息摘要對比,如相同則驗籤成功。

附示意圖如下:



### go標準庫中的RSA簽名和驗籤實現

程式碼如下:

```go
//簽名
func SignPKCS1v15(rand io.Reader, priv *PrivateKey, hash crypto.Hash, hashed []byte) ([]byte, error) {
    //雜湊提取訊息摘要
    hashLen, prefix, err := pkcs1v15HashInfo(hash, len(hashed))
    if err != nil {
        return nil, err
    }

    tLen := len(prefix) + hashLen
    k := (priv.N.BitLen() + 7) / 8
    if k < tLen+11 {
        return nil, ErrMessageTooLong
    }

    // EM = 0x00 || 0x01 || PS || 0x00 || T
    em := make([]byte, k)
    em[1] = 1
    for i := 2; i < k-tLen-1; i++ {
        em[i] = 0xff
    }
    //整合訊息摘要和訊息體
    copy(em[k-tLen:k-hashLen], prefix)
    copy(em[k-hashLen:k], hashed)

    m := new(big.Int).SetBytes(em)
    //使用傳送方私鑰加密訊息摘要和訊息體,即為簽名
    c, err := decryptAndCheck(rand, priv, m)
    if err != nil {
        return nil, err
    }

    copyWithLeftPad(em, c.Bytes())
    return em, nil
}

//驗證簽名
func VerifyPKCS1v15(pub *PublicKey, hash crypto.Hash, hashed []byte, sig []byte) error {
    //雜湊提取訊息摘要
    hashLen, prefix, err := pkcs1v15HashInfo(hash, len(hashed))
    if err != nil {
        return err
    }

    tLen := len(prefix) + hashLen
    k := (pub.N.BitLen() + 7) / 8
    if k < tLen+11 {
        return ErrVerification
    }

    c := new(big.Int).SetBytes(sig)
    //使用傳送方公鑰解密,提取訊息體和訊息簽名
    m := encrypt(new(big.Int), pub, c)
    em := leftPad(m.Bytes(), k)
    // EM = 0x00 || 0x01 || PS || 0x00 || T

    //對比傳送方和接收方訊息體、以及訊息簽名
    ok := subtle.ConstantTimeByteEq(em[0], 0)
    ok &= subtle.ConstantTimeByteEq(em[1], 1)
    ok &= subtle.ConstantTimeCompare(em[k-hashLen:k], hashed)
    ok &= subtle.ConstantTimeCompare(em[k-tLen:k-hashLen], prefix)
    ok &= subtle.ConstantTimeByteEq(em[k-tLen-1], 0)

    for i := 2; i < k-tLen-1; i++ {
        ok &= subtle.ConstantTimeByteEq(em[i], 0xff)
    }

    if ok != 1 {
        return ErrVerification
    }

    return nil
}
//程式碼位置src/crypto/rsa/pkcs1v15.go
```

### 後記

RSA演算法中使用了大量數論知識,有關數論知識還有待學習。
待續。




網址:http://www.qukuailianxueyuan.io/



欲領取造幣技術與全套虛擬機器資料

區塊鏈技術交流QQ群:756146052  備註:CSDN

尹成學院微信:備註:CSDN















相關文章