美文网首页
RSA加密解密原理(二)

RSA加密解密原理(二)

作者: 小东班吉 | 来源:发表于2022-12-05 17:30 被阅读0次

    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指的是数据长度,长度又分定长和不定长等,说起来就比较多了,详细的另写一篇记录。

    相关文章

      网友评论

          本文标题:RSA加密解密原理(二)

          本文链接:https://www.haomeiwen.com/subject/ugdifdtx.html