RSA加密解密原理(二)

PKCS

PKCS(Public Key Cryptography Standards, PKCS)公鑰加密標準,是美國RSA信息安全公司旗下的RSA實驗室開發的一系列編譯標準,非對稱密鑰一般都包含其他信息,所以PKCS通過ASN.1的格式標準定義密鑰展示

一個PKCS#1 公鑰用asn.1表示格式如下:

      RSAPublicKey ::= SEQUENCE {
        modulus           INTEGER,  -- n
        publicExponent    INTEGER   -- e
      }

modulus就是n,publicExponent就是e,n和e就代表了公鑰。上面asn.1格式的標準
一個PKCS#1私鑰用asn.1表示格式如下:

      RSAPrivateKey ::= SEQUENCE {
        version           Version,
        modulus           INTEGER,  -- n
        publicExponent    INTEGER,  -- e
        privateExponent   INTEGER,  -- d
        prime1            INTEGER,  -- p
        prime2            INTEGER,  -- q
        exponent1         INTEGER,  -- d mod (p-1)
        exponent2         INTEGER,  -- d mod (q-1)
        coefficient       INTEGER,  -- (inverse of q) mod p
        otherPrimeInfos   OtherPrimeInfos OPTIONAL
      }

openssl默認使用的是PKCS#1,但這個已經非常舊了,openssl主要是為了兼容,推進使用PKCS#8
PKCS#8是一個專門用于編碼私鑰的標準,可用于編碼 DSA/RSA/ECC 私鑰。它通常被編碼成 PEM 格式存儲。相比較PKCS#1,它比較安全可以兼容任何格式的私鑰,因此建議用PKCS#8來代替

X.509

X.509是密碼學里公鑰證書的格式標準。比如ssl用的就是它

x.509是公鑰標準,基本上現在的庫公鑰都使用x.509,私鑰標準符合pkcs。pkcs#8相比較在pkcs#1的標準上增加了一些頭部信息,比pkcs#1安全性高
X.509的RSA公鑰格式:

      RSAPublicKey ::= SEQUENCE {
         algorithm AlgorithmIdentifier , // 這就是增加的頭信息
         publicKey RSAPublicKey  // 這就是PKCS#1的RSA公鑰的內容
      }

PKCS#8的RSA私鑰格式:

      PrivateKey ::= SEQUENCE {
          version Version ,  // 這就是增加的頭信息
          privateKeyAlgorithm PrivateKeyAlgorithmIdentifier , // 這也是增加的頭信息,表示使用的什么算法,可以是 RSA,也可以是其它的算法,比如 DES、AES 等對稱加密算法等。
          privateKey RSAPrivateKey // 這就是PKCS#1的RSA私鑰的內容
      }

上面的公鑰用der編碼得到二進制格式,而為了方便看再用base64編碼就是pem格式的字符串了。

PEM 和 DER編碼

ASN.1通過DER編碼把公鑰和私鑰編碼成二進制格式以便于網絡上傳輸而PEM則是為了方便,對DER進行base64編碼同時在頭和尾處加上一行字符串進行標記PEM格式,這樣字符串就比較方便復制查看

pkcs#1的例子用pem編碼后的格式如下:

      // 公鑰
      -----BEGIN RSA PUBLIC KEY-----
      BASE64編碼的DER密鑰文本
      -----END RSA PUBLIC KEY-----
      
      // 私鑰
      -----BEGIN RSA PRIVATE KEY-----
      BASE64編碼的DER密鑰文本
      -----END RSA PRIVATE KEY-----

pkcs#8編碼后的未加密的私鑰格式:

      -----BEGIN PRIVATE KEY-----
      BASE64編碼的DER密鑰文本
      -----END PRIVATE KEY-----
      
      -----BEGIN ENCRYPTED PRIVATE KEY-----
      BASE64編碼的DER密鑰文本
      -----END ENCRYPTED PRIVATE KEY-----

x.509的公鑰編碼后的格式:

      -----BEGIN PUBLIC KEY-----
      BASE64編碼的DER密鑰文本
      -----END PUBLIC KEY-----

相比較pkcs#1,就少了個rsa字符
通常以DER格式存儲的證書,大都使用 .cer .crt .der 拓展名,在 Windows 系統比較常見,而PEM 格式的數據通常以 .pem .key .crt .cer 等拓展名存儲,打開查看就是一堆字符串,openssl 默認使用的就是pem格式。

pkcs填充規則

在rsa加密的過程中,密文的長度不能大于密鑰的長度,也就是必須滿足0 < m < n,如果長了則需要對數據進行分段加密,但是如果m太短則需要對m進行填充

rsa加密的密文m是不能超過密鑰的長度的,如果m>n,該公式就不能成立 m=pow(y, d) % n 無法解密,運算就會出錯。
填充規則常用的標準有NoPPadding,OAEPPadding,PKCS1Padding這幾種,go 在crypto/rsa庫中用的是PKCS #1 v1.5 padding,PKCS1Padding的填充總共占用11個字節,對于1024位長度的密鑰占用128個字節,減去11個字節,那明文最長的長度就是128-11=117個字節。1024長度的被破解過已經不建議使用了,至少使用2048或以上長度的密鑰比較安全。PKCS1Padding 8.1 Encryption-block formatting填充規則如下:

      // M為明文
      // BT 代表block type塊類型,有0x00,0x01,0x02, 如是是私鑰則BT=00x0或01x0。如果是公鑰操作,BT=0x02。
      // PS為填充的字節,BT=0x00則PS=0x00,BT=0x01則PS=0xFF,BT=0x02則PS=非0偽隨機數
      EM = 0x00 || BT || PS || 0x00 || M
      
      // 假設密鑰長度是2048,也就是256個字節,BT=0x02,M = 100個字節,則PS = 256 - 100 - 3 字節,填充的結構如下:
      em = 0x00 + 0x02 + (256 - 100 - 3)字節的隨機數 + 0x00 + m

go 在crypto/rsa中的公鑰填充加密代碼示例

      // https://pkg.go.dev/crypto/rsa#EncryptPKCS1v15
      func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
        randutil.MaybeReadByte(rand)
      
        if err := checkPub(pub); err != nil {
            return nil, err
        }
        k := pub.Size()
        if len(msg) > k-11 {
            return nil, ErrMessageTooLong
        }
      
        // EM = 0x00 || 0x02 || PS || 0x00 || M
        em := make([]byte, k)
        em[1] = 2
        ps, mm := em[2:len(em)-len(msg)-1], em[len(em)-len(msg):]
        err := nonZeroRandomBytes(ps, rand)
        if err != nil {
            return nil, err
        }
        em[len(em)-len(msg)-1] = 0
        copy(mm, msg)
      
        m := new(big.Int).SetBytes(em)
        c := encrypt(new(big.Int), pub, m)
      
        return c.FillBytes(em), nil
      }
      
      func encrypt(c *big.Int, pub *PublicKey, m *big.Int) *big.Int {
        e := big.NewInt(int64(pub.E))
          // 這里就是加密的過程了,就是上面我們說的公式pow(m,e)%n,不寫第三個參數,可以單獨調用Mod取模算出最終加密結果
        c.Exp(m, e, pub.N)
        return c
      }
      
      func (x *Int) FillBytes(buf []byte) []byte {
        // Clear whole buffer. (This gets optimized into a memclr.)
        for i := range buf {
            buf[i] = 0
        }
        x.abs.bytes(buf)
        return buf
      }

簽名

簽名就是用私鑰加密,而驗簽是用公鑰解密。簽名的目的是為了證明發出消息的人以及消息是否完整,擁有私有簽名的數據,則只有持有公鑰的人才可以解開

簽名分為以下幾步:

  1. 對數據進行哈希運算得到一個短的哈希值,因為rsa加密有長度限制。h= hash(m)
  2. 對哈希值和摘要算法標識符OID進行asn.1編碼
              DigestInfo ::= SEQUENCE {
                   digestAlgorithm DigestAlgorithmIdentifier, // 消息摘要算法
                   digest Digest  // 就是哈希運算的結果 h
              }
    
  3. der編碼后對數據進行填充然后利用私鑰進行加密,和上面加密的填充過程一樣,區別是這次是用私鑰,填充的塊類型BT和PS有些區別
              EM = 0x00 || 0x01 || PS || 0x00 || T
              
              // 以上面的例子為參考,2048長度的密鑰,密文的長度最多256個字節,假設len(m)=100,m為der編碼后的數據
              em = 0x00 + 0x01 + (256 - 100 - 3)字節的0xff + 0x00 + m
    
  4. 私鑰加密
    go 在crypto/rsa中的簽名,驗簽代碼示例
      // 簽名
      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.Size()
        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
        }
      
        return c.FillBytes(em), nil
      }
      
      // 驗簽
      // VerifyPKCS1v15 verifies an RSA PKCS #1 v1.5 signature.
      // hashed is the result of hashing the input message using the given hash
      // function and sig is the signature. A valid signature is indicated by
      // returning a nil error. If hash is zero then hashed is used directly. This
      // isn't advisable except for interoperability.
      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.Size()
        if k < tLen+11 {
            return ErrVerification
        }
      
        // RFC 8017 Section 8.2.2: If the length of the signature S is not k
        // octets (where k is the length in octets of the RSA modulus n), output
        // "invalid signature" and stop.
        if k != len(sig) {
            return ErrVerification
        }
      
        c := new(big.Int).SetBytes(sig)
        m := encrypt(new(big.Int), pub, c)
        em := m.FillBytes(make([]byte, 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
      }
      
      func pkcs1v15HashInfo(hash crypto.Hash, inLen int) (hashLen int, prefix []byte, err error) {
        // Special case: crypto.Hash(0) is used to indicate that the data is
        // signed directly.
        if hash == 0 {
            return inLen, nil, nil
        }
      
        hashLen = hash.Size()
        if inLen != hashLen {
            return 0, nil, errors.New("crypto/rsa: input must be hashed message")
        }
        prefix, ok := hashPrefixes[hash]
        if !ok {
            return 0, nil, errors.New("crypto/rsa: unsupported hash function")
        }
        return
      }
      
      // For performance, we don't use the generic ASN1 encoder. Rather, we
      // precompute a prefix of the digest value that makes a valid ASN1 DER string
      // with the correct contents.
      
      var hashPrefixes = map[crypto.Hash][]byte{
        crypto.MD5:       {0x30, 0x20, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x05, 0x00, 0x04, 0x10},
        crypto.SHA1:      {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14},
        crypto.SHA224:    {0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0x04, 0x1c},
        crypto.SHA256:    {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20},
        crypto.SHA384:    {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30},
        crypto.SHA512:    {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40},
        crypto.MD5SHA1:   {}, // A special TLS case which doesn't use an ASN1 prefix.
        crypto.RIPEMD160: {0x30, 0x20, 0x30, 0x08, 0x06, 0x06, 0x28, 0xcf, 0x06, 0x03, 0x00, 0x31, 0x04, 0x14},
      }

go在hashPrefixes里提前計算好了沒個哈希算法標識符的der編碼后的值,注釋里說是為了提升性能。我以sha256為例,實現如下,發現除了首尾4個字節,中間部分一致:

      package main
      
      import (
        "crypto/x509/pkix"
        "encoding/asn1"
        "encoding/hex"
        "fmt"
      )
      
      func main() {
        // hash256算法標識oid
        oidSHA256 := asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
      
        mgf1Params := pkix.AlgorithmIdentifier{
            Algorithm:  oidSHA256,
            Parameters: asn1.NullRawValue,
        }
        d, err := asn1.Marshal(mgf1Params)
        if err != nil {
            fmt.Println(err)
        }
        oid := hex.EncodeToString(d)
        fmt.Println(oid) 
          // 輸出如下:
          // 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00
          // {0x30, 0x31, 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}
      }

可以看到輸出的和上面hashPrefixes里sha256的值除了首尾4個字節不同,首部2個字節分別是0x30,0x31,尾部2個字節分別是 0x04, 0x20,至于這4個分別代表什么暫時不清楚,??♂?。

經查閱文檔發現是我構造的簽名數據結構問題,然后我們加上簽名的數據m,按照文檔定義數據結構(參考RFC 2313 10.1.2),發現得到的der編碼前半部分剛好與hashPrefixes里sha256的值是一樣的,后半部分剛好是哈希值編碼后的值,如果對編碼后的數據進行填充,然后私鑰加密,其實就是實現了一次簽名的完整過程。

        /* ASN1 DER structures
        DigestInfo ::= SEQUENCE {
            digestAlgorithm AlgorithmIdentifier,
            digest OCTET STRING
        }
        */

            // 算法標識符
        type AlgorithmIdentifier struct {
            Algorithm  asn1.ObjectIdentifier
            Parameters asn1.RawValue `asn1:"optional"`
        }

        // 簽名的數據結構
        type DigestInfo struct {
            DigestAlgorithm AlgorithmIdentifier
            Digest          []byte
        }

        sha := sha256.New()
        m := []byte{50}
        sha.Write(m)
        h := sha.Sum(nil)

        var digestInfo = DigestInfo{
            DigestAlgorithm: AlgorithmIdentifier{
                Algorithm: oidSHA256,
                Parameters: asn1.RawValue{
                    Tag: asn1.TagNull,
                },
            },
            Digest: h,
        }

        d, err := asn1.Marshal(digestInfo)
        if err != nil {
            fmt.Println(err)
            return
        }
        oid := hex.EncodeToString(d)
        fmt.Println(oid)

          // 輸出
          // 3031300d060960864801650304020105000420 d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35

但是上面代碼如果我們去掉生成哈希值的部分,然后 Digest 字段的值定義為空或者不填寫則生成的值和hashPrefixes[sha256]是不一樣,這種區別剛好區分在首部2個字節,那么問題來了這2個字節分別代表什么意思呢?這里牽扯到ans.1的der編碼規則,不是很懂,后邊這塊的知識需要再補補,簡單來說0x30指的是類型,代表著一個sequence結構,0x31指的是后邊數據的長度。

asn.1的der編碼規則是遵循了type-length-value 規則,由幾個部分組成

Identifier octets Type Length octets Contents octets End-of-Contents octets
Type Length Value (only if indefinite form)

Type用高2位表示Tag class,高位第3位表示是否是復合數據類型P/C,后邊則是 TagNumber

0x30就指的是Type,0x30轉換成二進制 0011 0000,可以看到前面2個位是0。可以看到 00是tag class,代表asn.1的原生數據類型,1是 P/C C指的是復合數據類型,由于簽名是 SEQUENCE 結構體所以這里是復合數據類型,所以是1,后邊的 1 0000 轉換成10進制是16,而16所在的tagNumber剛好代表 SEQUENCE,參考x.690 BER encoding Identifier octets
0x31指的是數據長度,長度又分定長和不定長等,說起來就比較多了,詳細的另寫一篇記錄。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容