RSA是一种非对称加解密算法,用于实现签名/认证等。在.Net框架中提供了System.Security.Cryptography.RSACryptoServiceProvider类,用于封装实现RSA算法,但这个类使用的公钥/私钥格式为XML,这是.Net特有的格式,而其它语言如Java编程中一般使用PEM或DER等格式,OpenSSL规范中也特荐为PEM格式。项目中经常会遇到XML与PEM格式的密钥相互转换的需求,我将分4篇文章,总结C#中PEM与XML格式密钥的相互转换。今天先从分析PEM公钥开始,后面再分析私钥。

一、PEM公钥的使用

PEM公钥文件格式

PEM文件用Notepad打开,可以看到如下,图一:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArEEtglN1RQ0mNnd7qZko
z4BIpR01NnWw6V98Neqne+NLIcKvimlF8lYfYH+mtbFev4/yFzPIP9DhB18eMbCv
VpqoqoukYFWJkt9hAmLISshrxqc71/1IVV+R6oP8+v3o8hbqeyHv5XaRYVv5kOwy
Om11lqT5P7V9iRCMq7vrTiXeCuCNQ+kse+XJoNzFxJtz9gE/7Q21xmgMuCxHWewn
3hWePBT5hClcmMktqG4NT+iynMO06zNf8+Ay4mp+t3XGwJ03NLVEREctLsFKtpC9
GX5K9CWt9h85eOGzqYu94RLrSVnFP/NwnWzA3P5xD88gbHtEx34lsdeBTUbSoGhs
uQIDAQAB
-----END PUBLIC KEY-----

分三部分:头部标识(第一行),结束标识(最后一行),中间部分是数据,用Base64编码解码,图二:
pubKey

数据存储的格式见下表总结,图三:
数据格式

图三表格的“内容”列,红字是文件格式要求的固定值,与图二中红字相对应。浅兰字为示例内容,不同的Key文件,值不同。
图二中绿字字节是长度值,对应图三内容列的长度值,表示后续数据块占用字节长度。其规则为:

  • 当长度<128时,用1Byte表示,最高位=0,低7位表示长度值。
  • 当长度>=128时,用多Byte表示,其中第1Byte的最高位=1,低7位的值=x,表示后续x字节的值存储长度值,低字节高有效位,BigEndian方式。

表格中各数据段说明:

  • 第1部分:PEM头,由标识(0x30),长度区组成,后续Inner数据块。
  • 第2部分:Inner块,前16字节为固定标识,长度区,后续bitString数据块
  • 第3部分:BitString块,由标识,长度区组成,后续params块。长度区指示后续的参数块总长度。后续的参数块有2个,依次排列分别为:模(modulus)和指数(exponent)。
  • 第4部分:模参数值
  • 第5部分:指数值

C#实现

理解了PEM公钥文件的格式,如何在C#中使用就比较方便了:

  • 定义RSAParameters变量,读入PEM的模数、指数值。
  • 使用RSACryptoServiceProvider.ImportParameters方法,创建RSA实例。
  • 若需要转换成XML格式,使用RSACryptoServiceProvider.ToXmlString(false),生成XML格式的公钥。

读入PEM文件的代码如下:
其中GetPemBytes方法是把PEM文件的数据部分,按Base64解码,返回字节数组。

        internal static RSAParameters GetRSAParametersPub(string fn,delegateShowMsg show)
        {
            RSAParameters p = new RSAParameters();
            byte[] keyBytes = GetPemBytes(fn);
            if (keyBytes[0] != 0x30) throw new Exception($"sequence wrong {keyBytes[0]:x2}");
            int len = 0;
            int id = 1;
            len = DecodeLenth(keyBytes, ref id);
            int inner0 = id + 1;
            int inner1 = inner0 + len - 1;
            id = inner0 + 16;
            len = DecodeLenth(keyBytes, ref id);
            int bitString0 = id + 1;
            int bitString1 = bitString0 + len - 1;
            id = bitString0 + 2;
            len = DecodeLenth(keyBytes, ref id);
            int params0 = id + 1;
            int params1 = params0 + len - 1;
            id = params0;
            p.Modulus = DecodeIntegerBigEndian(keyBytes, ref id);
            p.Exponent = DecodeIntegerBigEndian(keyBytes, ref id);
            return p;
        }

        private static int DecodeLenth(byte[] stream, ref int id)
        {
            int len = stream[id];
            if (len == 0) throw new ArgumentOutOfRangeException("length", "length should be not 0");
            if (len < 0x80)
            {
                return len;
            }
            else
            {
                len = len & 0x7f;
                int temp = 0;
                for (var i = len - 1; i >= 0; i--)
                {
                    id += 1;
                    temp += stream[id] << i * 8;
                }
                return temp;
            }
        }

        private static byte[] DecodeIntegerBigEndian(byte[] stm, ref int id)
        {
            if (stm[id] != 0x02) throw new Exception("params id invalid");
            id  +=1;
            var len = DecodeLenth(stm, ref id);
            int end0 = id + 1;
            int end1 = end0 + len - 1;
            //删除补零,>7f时补0
            id = end0;
            while (stm[id] == 0)
            {
                id++;
            }
            if (stm[id] > 0x7f)
            {
                end0 = id;
            }
            byte[] result= stm.Where((key, index) => index >= end0 & index <= end1).ToArray();
            id = end1 + 1;
            return result;
        }

这是头一次在CSDN上发表文章,这个MarkDown编辑器使用不顺手,很多功能不知如何实现,比如字体颜色、居中等,写作过程插入代码时还导致全篇显示错误,折腾了几次才明白似乎代码中不能有空行,是这样吗?无论如何,如果这篇文章能对大家有所帮助,也不枉这番功夫哈,欢迎点赞打赏鼓励-,下一篇将介绍PEM私钥的使用,敬请期待~

更多推荐