C#的RSA加密解密签名,就为了支持PEM PKCS#8格式密钥对的导入导出

差点造了一整个轮子

.Net Framework 4.5 里面的RSA功能,并未提供简单对PEM密钥格式的支持(.Net Core有咩?),差点(还远着)造了一整个轮子,就为了支持PEM PKCS#8、PKCS#1格式密钥对的导入导出。

Github: https://github.com/xiangyuecn/RSA-csharp

本文内容来自README,主要介绍了PEM PKCS#8、PKCS#1公钥和私钥格式,并以此为基础写的C#函数方法。

前言、自述、还有啥

在写一个小转换工具时加入了RSA加密解密支持(见图RSA工具),秘钥输入框支持填写XML和PEM格式,操作类型里面支持XML->PEM、PEM->XML的转换。

实现相应功能发现原有RSA操作类不能良好工作,PEM->XML没问题,只要能通过PEM创建RSA,就能用 RSACryptoServiceProvider 自带方法导出XML。但XML->PEM没有找到相应的简单实现方法,大部分博客写的用BouncyCastle库来操作,代码是少,但BouncyCastle就有好几兆大小,我的小工具啊才100K;所以自己实现了一个支持导出 PKCS#1 PKCS#8 格式PEM密钥的方法 RSA_PEM.ToPEM

操作过程中发现原有RSA操作类不支持用 PKCS#8 格式PEM密钥来创建RSA对象(用的 RSACryptoServiceProviderExtension 的扩展方法来支持PEM密钥),仅支持 PKCS#1 ,所以又自己实现了一个从PEM密钥来创建 RSACryptoServiceProvider 的方法 RSA_PEM.FromPEM

在实现导入导出PEM密钥过程中,对 PKCS#1 PKCS#8 格式的PEM密钥有了一定的了解,主要参考了:

RSA公钥文件(PEM)解析 》:公钥字节码分解。

RSA私钥文件(PEM)解析 》:私钥字节码分解。

iOS安全相关 - RSA中公钥的DER格式组成 》:1字节和2字节长度表述方法,和为什么有些字段前面要加0x00。

clone 下来用vs应该能够直接打开,经目测看起来没什么卵用的文件都svn:ignore掉了(svn滑稽。

  • 通过 XML格式 密钥对创建RSA
  • 通过 PEM格式 密钥对创建RSA
  • RSA加密、解密
  • RSA签名、验证
  • 导出 XML格式 公钥、私钥
  • 导出 PEM格式 公钥、私钥
  • PEM格式 秘钥对和 XML格式 秘钥对互转

    PEM密钥编码格式

    长度表述方法

    PEM格式中,每段数据基本上都是 flag+长度数据占用位数+长度数值+数据 这种格式。

    长度数据占用位数有0x81和0x82两个值,分别代表长度数值占用了1字节和2字节。

    但长度数据占用位数不一定存在,如果长度数值<0x80时(理由应该和下面这个加0x00一致),长度数值直接在flag后面用1位来表述,变成了 flag+长度数值(<0x80)+数据

    什么情况下内容前面要加0x00

    如果内容的bit流的前4 bit十六进制值>=8就要在内容前面加0x00,其他不用加。

    一个大整数,最高位为符号位,其为1时,就是负数,所以要在最高位填充0x00以保证不为负。

    PEM公钥编码格式

    PKCS#1 PKCS#8 公钥编码都是统一的格式。

    /*****1024字节公钥*****/
    -----BEGIN PUBLIC KEY-----
    MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYw9+M3+REzDtYqwBrs/as/Oy8
    GRE5OmnqOV0EfkEiCIjiczbVEFnZ3qRjLbDATfmBxNQ6c6Fga8nX28glEH/aL/RG
    2KdpI8KMUqKAszNydsHSeh9MSKcd5zgS4NuE0u+eR7CB8kOWipiLDQmY38kpM36p
    RWdNQlpIaTDo5IhJJwIDAQAB
    -----END PUBLIC KEY-----
    /*****二进制表述*****/
    30819F300D06092A864886F70D010101050003818D003081890281810098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E48849270203010001
    /*****二进制分解*****/
    后续(到结尾)内容长度为0x9F字节,相当于整个文件长度-当前这4字节。
    格式:tag[+长度数据占用位数(可选)]+后续长度数值,
        此处tag=0x30,
        长度数据占用1位(参考前面长度表述方法),
        后续长度数值=0x9F个字节
    30 81 9F
    固定内容 encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"(其实不懂是啥玩意)
    30 0D 06092A864886F70D0101010500
    /*后续内容长度,后面内容长度为0x8D字节,和开头格式一致*/
    03 81 8D
    /*固定内容*/
    /*后续内容长度,后面内容长度为0x89字节,和开头格式一致*/
    30 81 89
    RSA Modulus内容
    格式:tag[+长度数据占用位数(可选)]+内容长度数值+内容,
        此处tag=0x02,所有RSA相关参数都是用02,
        长度数据占用位数为1字节,
        内容长度数值=0x81个字节,
        内容为0x00-0x27这一段(参考前面什么情况下要加0x00)。
    02 81 81
    0098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E4884927
    /*RSA Exponent内容,和Modulus一样,但此处长度数据占用位数不存在*/
    02 03 010001
    

    PEM PKCS#1私钥编码格式

    /*****1024字节PKCS#1私钥*****/
    -----BEGIN RSA PRIVATE KEY-----
    MIICXAIBAAKBgQCYw9+M3+REzDtYqwBrs/as/Oy8GRE5OmnqOV0EfkEiCIjiczbV
    EFnZ3qRjLbDATfmBxNQ6c6Fga8nX28glEH/aL/RG2KdpI8KMUqKAszNydsHSeh9M
    SKcd5zgS4NuE0u+eR7CB8kOWipiLDQmY38kpM36pRWdNQlpIaTDo5IhJJwIDAQAB
    AoGAcGNSWRrynia+1onf4lzg8v2U0QGEKV0vRNF0/HRCSN6MjkUDJxdDc0UYHZsk
    uSXklTMQi/w70msacQNRqOsNk32O6vVPxr4NfTVaIV59Jv9Z5SXGiRmRZXeRw0ks
    KYdOwaDJJu9zETNHZoMFJm8sq/tGJPQCPNesoZRZssL7mjkCQQDOI6jKt60bvu6V
    XvtQoyUUbyMj9eCOBatS49jRvv326TMc951e9TcbnD0cxJrV1N6yIi+++ejwfagb
    eYf++N61AkEAvbc8KTlBbI9TMwnVkQpst+ckgm3gpRDhAfQ/Lt7r8g2KAHsJv+wb
    AJCgu8PgqM9mQjVxZ+78+aLEQ+h5rvMV6wJAY1c9/ct8ihV+Zs+qL1cgBHP2rFrO
    x8KlqMGS+KmhPD9v2XLfDScBUrX9oYKB17DJTXE6Lz/CaTs1K2BrEI4gzQJBAJIQ
    s9chaAfHSc1v8uha2F23Ltrk8iLknfi9LrBNneedGPVJxbXoeNm0gKxQIXaXSCoN
    r6TP0iH5eZa3NIjIS8UCQAbw+d2WJIon+vuUsKk2dtZTqZx8e53NreZUFMaIkoS5
    JPJqI6/6hq8/2ARFO3P9/qkxDMkJv8mSjV91cZixB10=
    -----END RSA PRIVATE KEY-----
    /*****二进制表述*****/
    3082025C0201000281810098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E48849270203010001028180706352591AF29E26BED689DFE25CE0F2FD94D10184295D2F44D174FC744248DE8C8E45032717437345181D9B24B925E49533108BFC3BD26B1A710351A8EB0D937D8EEAF54FC6BE0D7D355A215E7D26FF59E525C6891991657791C3492C29874EC1A0C926EF73113347668305266F2CABFB4624F4023CD7ACA19459B2C2FB9A39024100CE23A8CAB7AD1BBEEE955EFB50A325146F2323F5E08E05AB52E3D8D1BEFDF6E9331CF79D5EF5371B9C3D1CC49AD5D4DEB2222FBEF9E8F07DA81B7987FEF8DEB5024100BDB73C2939416C8F533309D5910A6CB7E724826DE0A510E101F43F2EDEEBF20D8A007B09BFEC1B0090A0BBC3E0A8CF6642357167EEFCF9A2C443E879AEF315EB024063573DFDCB7C8A157E66CFAA2F57200473F6AC5ACEC7C2A5A8C192F8A9A13C3F6FD972DF0D270152B5FDA18281D7B0C94D713A2F3FC2693B352B606B108E20CD0241009210B3D7216807C749CD6FF2E85AD85DB72EDAE4F222E49DF8BD2EB04D9DE79D18F549C5B5E878D9B480AC50217697482A0DAFA4CFD221F97996B73488C84BC5024006F0F9DD96248A27FAFB94B0A93676D653A99C7C7B9DCDADE65414C6889284B924F26A23AFFA86AF3FD804453B73FDFEA9310CC909BFC9928D5F757198B1075D
    /*****二进制分解(大部分和公钥格式相同)*****/
    /*后续内容长度,后面内容长度为0x025C个字节,和公钥开头格式一致,参考公钥部分*/
    30 82 025C
    /*固定版本号*/
    02 01 00
    /*#####从这里开始后面就是内容了 注:KCS#8仅仅是在此处插入部分内容#####*/
    /*RSA Modulus内容,和公钥开头格式一致,参考公钥部分*/
    02 81 81 
    0098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E4884927
    /*RSA Exponent*/
    02 03 010001
    /*RSA D*/
    02 81 80
    706352591AF29E26BED689DFE25CE0F2FD94D10184295D2F44D174FC744248DE8C8E45032717437345181D9B24B925E49533108BFC3BD26B1A710351A8EB0D937D8EEAF54FC6BE0D7D355A215E7D26FF59E525C6891991657791C3492C29874EC1A0C926EF73113347668305266F2CABFB4624F4023CD7ACA19459B2C2FB9A39
    /*RSA P*/
    02 41
    00CE23A8CAB7AD1BBEEE955EFB50A325146F2323F5E08E05AB52E3D8D1BEFDF6E9331CF79D5EF5371B9C3D1CC49AD5D4DEB2222FBEF9E8F07DA81B7987FEF8DEB5
    /*RSA Q*/
    02 41
    00BDB73C2939416C8F533309D5910A6CB7E724826DE0A510E101F43F2EDEEBF20D8A007B09BFEC1B0090A0BBC3E0A8CF6642357167EEFCF9A2C443E879AEF315EB
    /*RSA DP*/
    02 40 63573DFDCB7C8A157E66CFAA2F57200473F6AC5ACEC7C2A5A8C192F8A9A13C3F6FD972DF0D270152B5FDA18281D7B0C94D713A2F3FC2693B352B606B108E20CD
    /*RSA DQ*/
    02 41
    009210B3D7216807C749CD6FF2E85AD85DB72EDAE4F222E49DF8BD2EB04D9DE79D18F549C5B5E878D9B480AC50217697482A0DAFA4CFD221F97996B73488C84BC5
    /*RSA InverseQ*/
    02 40
    06F0F9DD96248A27FAFB94B0A93676D653A99C7C7B9DCDADE65414C6889284B924F26A23AFFA86AF3FD804453B73FDFEA9310CC909BFC9928D5F757198B1075D
    

    PEM PKCS#8私钥编码格式

    /*****1024字节PKCS#8私钥*****/
    -----BEGIN PRIVATE KEY-----
    MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJjD34zf5ETMO1ir
    AGuz9qz87LwZETk6aeo5XQR+QSIIiOJzNtUQWdnepGMtsMBN+YHE1DpzoWBrydfb
    yCUQf9ov9EbYp2kjwoxSooCzM3J2wdJ6H0xIpx3nOBLg24TS755HsIHyQ5aKmIsN
    CZjfySkzfqlFZ01CWkhpMOjkiEknAgMBAAECgYBwY1JZGvKeJr7Wid/iXODy/ZTR
    AYQpXS9E0XT8dEJI3oyORQMnF0NzRRgdmyS5JeSVMxCL/DvSaxpxA1Go6w2TfY7q
    9U/Gvg19NVohXn0m/1nlJcaJGZFld5HDSSwph07BoMkm73MRM0dmgwUmbyyr+0Yk
    9AI816yhlFmywvuaOQJBAM4jqMq3rRu+7pVe+1CjJRRvIyP14I4Fq1Lj2NG+/fbp
    Mxz3nV71NxucPRzEmtXU3rIiL7756PB9qBt5h/743rUCQQC9tzwpOUFsj1MzCdWR
    Cmy35ySCbeClEOEB9D8u3uvyDYoAewm/7BsAkKC7w+Coz2ZCNXFn7vz5osRD6Hmu
    8xXrAkBjVz39y3yKFX5mz6ovVyAEc/asWs7HwqWowZL4qaE8P2/Zct8NJwFStf2h
    goHXsMlNcTovP8JpOzUrYGsQjiDNAkEAkhCz1yFoB8dJzW/y6FrYXbcu2uTyIuSd
    +L0usE2d550Y9UnFteh42bSArFAhdpdIKg2vpM/SIfl5lrc0iMhLxQJABvD53ZYk
    iif6+5SwqTZ21lOpnHx7nc2t5lQUxoiShLkk8mojr/qGrz/YBEU7c/3+qTEMyQm/
    yZKNX3VxmLEHXQ==
    -----END PRIVATE KEY-----
    /*****二进制表述*****/
    30820276020100300D06092A864886F70D0101010500048202603082025C0201000281810098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E48849270203010001028180706352591AF29E26BED689DFE25CE0F2FD94D10184295D2F44D174FC744248DE8C8E45032717437345181D9B24B925E49533108BFC3BD26B1A710351A8EB0D937D8EEAF54FC6BE0D7D355A215E7D26FF59E525C6891991657791C3492C29874EC1A0C926EF73113347668305266F2CABFB4624F4023CD7ACA19459B2C2FB9A39024100CE23A8CAB7AD1BBEEE955EFB50A325146F2323F5E08E05AB52E3D8D1BEFDF6E9331CF79D5EF5371B9C3D1CC49AD5D4DEB2222FBEF9E8F07DA81B7987FEF8DEB5024100BDB73C2939416C8F533309D5910A6CB7E724826DE0A510E101F43F2EDEEBF20D8A007B09BFEC1B0090A0BBC3E0A8CF6642357167EEFCF9A2C443E879AEF315EB024063573DFDCB7C8A157E66CFAA2F57200473F6AC5ACEC7C2A5A8C192F8A9A13C3F6FD972DF0D270152B5FDA18281D7B0C94D713A2F3FC2693B352B606B108E20CD0241009210B3D7216807C749CD6FF2E85AD85DB72EDAE4F222E49DF8BD2EB04D9DE79D18F549C5B5E878D9B480AC50217697482A0DAFA4CFD221F97996B73488C84BC5024006F0F9DD96248A27FAFB94B0A93676D653A99C7C7B9DCDADE65414C6889284B924F26A23AFFA86AF3FD804453B73FDFEA9310CC909BFC9928D5F757198B1075D
    /*****二进制分解(和PKCS#1只是多了一段数据,详细结构参考PKCS#1的)*****/
    /*后续内容长度*/
    30 82 0276
    /*固定版本号*/
    02 01 00
    /*#####相对于KCS#1仅仅是在此处开始插入部分数据 Begin#####*/
    固定内容 encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"(其实不懂是啥玩意)
    30 0D 06092A864886F70D0101010500
    /*后续内容长度,后面内容长度为0x0260字节,和开头格式一致*/
    04 82 0260
    /*后续内容长度,后面内容长度为0x025C字节,和开头格式一致*/
    30 82 025C
    /*固定版本号*/
    02 01 00
    /*#####相对于KCS#1仅仅是在此处结束插入部分数据 End#####*/
    /*RSA Modulus内容*/
    02 81 81 
    0098C3DF8CDFE444CC3B58A...
    ...后续内容省略...
    

    C# RSA操作类

    RSA.cs

    此文件依赖RSA_PEM.cs,用于进行加密、解密、签名、验证、秘钥导入导出操作。

    [构造函数] new RSA(1024)

    通过指定密钥长度来创建RSA,会生成新密钥。

    [构造函数] new RSA("<xml>")

    通过XML格式密钥对创建RSA,xml可以是公钥或私钥,XML格式如:

    <RSAKeyValue><Modulus>mMPfjN/kRMw7WKsAa7P2rPzsvBkROTpp6jldBH5BIgiI4nM21RBZ2d6kYy2wwE35gcTUOnOhYGvJ19vIJRB/2i/0RtinaSPCjFKigLMzcnbB0nofTEinHec4EuDbhNLvnkewgfJDloqYiw0JmN/JKTN+qUVnTUJaSGkw6OSISSc=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>
    

    [构造函数] new RSA("PEM", any)

    通过PEM格式密钥对创建RSA,PEM可以是公钥或私钥,支持PKCS#1PKCS#8格式,PEM格式如:

    -----BEGIN PUBLIC KEY-----
    MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYw9+M3+REzDtYqwBrs/as/Oy8
    GRE5OmnqOV0EfkEiCIjiczbVEFnZ3qRjLbDATfmBxNQ6c6Fga8nX28glEH/aL/RG
    2KdpI8KMUqKAszNydsHSeh9MSKcd5zgS4NuE0u+eR7CB8kOWipiLDQmY38kpM36p
    RWdNQlpIaTDo5IhJJwIDAQAB
    -----END PUBLIC KEY-----
    

    [方法] .ToXML([false]是否仅仅导出公钥)

    导出XML格式秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。

    [方法] .ToPEM_PKCS1|.ToPEM_PKCS8([false]是否仅仅导出公钥)

    导出PEM格式秘钥对,两个方法分别导出PKCS#1PKCS#8格式。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。

    [方法] .Encode("字符串"|bytes)

    加密操作,支持任意长度数据。

    [方法] .DecodeOrNull("Base64字符串"|bytes)

    解密操作,解密失败返回null,支持任意长度数据。

    [方法] .Sign("hash", "字符串"|bytes)

    通过hash算法(MD5、SHA1等)来对数据进行签名。

    [方法] .Verify("hash", "sign", "字符串")

    通过hash算法(MD5、SHA1等)来验证字符串是否和sign签名一致。

    RSA_PEM.cs

    此文件不依赖任何文件,可以单独copy来用(RSA_Unit里面的方法可以忽略)

    [静态方法] .FromPEM("PEM")

    通过PEM格式秘钥对来创建RSACryptoServiceProvider,PEM可以是公钥或私钥。

    [静态方法] .ToPEM(RSACryptoServiceProvider, exportPublicOnly, usePKCS8)

    将RSA中的密钥对导出成PEM格式usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。

    RSA_Unit.cs

    封装的一些通用方法,如:base64。没有此文件也可以,引用的地方用别的代码实现。

    Program.cs

    控制台入口文件,用来测试的,里面包含了主要的使用用例。

    RSA工具: