C# 将私有/公共 RSA 密钥从 RSACryptoServiceProvider 导出到 PEM 字符串

Posted

技术标签:

【中文标题】C# 将私有/公共 RSA 密钥从 RSACryptoServiceProvider 导出到 PEM 字符串【英文标题】:C# Export Private/Public RSA key from RSACryptoServiceProvider to PEM string 【发布时间】:2014-07-07 05:44:07 【问题描述】:

我有一个 System.Security.Cryptography.RSACryptoServiceProvider 实例,我需要将它的密钥导出到 PEM 字符串 - 像这样:

-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDUNPB6Lvx+tlP5QhSikADl71AjZf9KN31qrDpXNDNHEI0OTVJ1
OaP2l56bSKNo8trFne1NK/B4JzCuNP8x6oGCAG+7bFgkbTMzV2PCoDCRjNH957Q4
Gxgx1VoS6PjD3OigZnx5b9Hebbp3OrTuqNZaK/oLPGr5swxHILFVeHKupQIDAQAB
AoGAQk3MOZEGyZy0fjQ8eFKgRTfSBU1wR8Mwx6zKicbAotq0CBz2v7Pj3D+higlX
LYp7+rUOmUc6WoB8QGJEvlb0YZVxUg1yDLMWYPE7ddsHsOkBIs7zIyS6cqhn0yZD
VTRFjVST/EduvpUOL5hbyLSwuq+rbv0iPwGW5hkCHNEhx2ECQQDfLS5549wjiFXF
gcio8g715eMT+20we3YmgMJDcviMGwN/mArvnBgBQsFtCTsMoOxm68SfIrBYlKYy
BsFxn+19AkEA82q83pmcbGJRJ3ZMC/Pv+/+/XNFOvMkfT9qbuA6Lv69Z1yk7I1ie
FTH6tOmPUu4WsIOFtDuYbfV2pvpqx7GuSQJAK3SnvRIyNjUAxoF76fGgGh9WNPjb
DPqtSdf+e5Wycc18w+Z+EqPpRK2T7kBC4DWhcnTsBzSA8+6V4d3Q4ugKHQJATRhw
a3xxm65kD8CbA2omh0UQQgCVFJwKy8rsaRZKUtLh/JC1h1No9kOXKTeUSmrYSt3N
OjFp7OHCy84ihc8T6QJBANe+9xkN9hJYNK1pL1kSwXNuebzcgk3AMwHh7ThvjLgO
jruxbM2NyMM5tl9NZCgh1vKc2v5VaonqM1NBQPDeTTw=
-----END RSA PRIVATE KEY-----

但是根据 MSDN 文档没有这样的选项,只有某种 XML 导出。我不能使用像 BouncyCastle 这样的第三方库。 有没有办法生成这个字符串?

【问题讨论】:

该类的实例如何以及在何处拥有密钥? 痛点是由于 .Net 及其使用来自RFC 3275 的 XML 编码。 .Net 不使用 ASN.1/DER 或 PEM 编码的密钥。我认为它是唯一一个以这种方式做事的加密库。 【参考方案1】:

请注意:以下代码用于导出 private 密钥。如果您希望导出 public 密钥,请参考我给出的答案here。

PEM 格式只是将密钥的ASN.1DER 编码(每个PKCS#1)转换为 Base64。鉴于表示密钥所需的字段数量有限,创建快速而肮脏的 DER 编码器以输出适当的格式然后进行 Base64 编码非常简单。因此,下面的代码不是特别优雅,但可以完成工作:

private static void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream)

    if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp");
    var parameters = csp.ExportParameters(true);
    using (var stream = new MemoryStream())
    
        var writer = new BinaryWriter(stream);
        writer.Write((byte)0x30); // SEQUENCE
        using (var innerStream = new MemoryStream())
        
            var innerWriter = new BinaryWriter(innerStream);
            EncodeIntegerBigEndian(innerWriter, new byte[]  0x00 ); // Version
            EncodeIntegerBigEndian(innerWriter, parameters.Modulus);
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent);
            EncodeIntegerBigEndian(innerWriter, parameters.D);
            EncodeIntegerBigEndian(innerWriter, parameters.P);
            EncodeIntegerBigEndian(innerWriter, parameters.Q);
            EncodeIntegerBigEndian(innerWriter, parameters.DP);
            EncodeIntegerBigEndian(innerWriter, parameters.DQ);
            EncodeIntegerBigEndian(innerWriter, parameters.InverseQ);
            var length = (int)innerStream.Length;
            EncodeLength(writer, length);
            writer.Write(innerStream.GetBuffer(), 0, length);
        

        var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray();
        outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----");
        // Output as Base64 with lines chopped at 64 characters
        for (var i = 0; i < base64.Length; i += 64)
        
            outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i));
        
        outputStream.WriteLine("-----END RSA PRIVATE KEY-----");
    


private static void EncodeLength(BinaryWriter stream, int length)

    if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative");
    if (length < 0x80)
    
        // Short form
        stream.Write((byte)length);
    
    else
    
        // Long form
        var temp = length;
        var bytesRequired = 0;
        while (temp > 0)
        
            temp >>= 8;
            bytesRequired++;
        
        stream.Write((byte)(bytesRequired | 0x80));
        for (var i = bytesRequired - 1; i >= 0; i--)
        
            stream.Write((byte)(length >> (8 * i) & 0xff));
        
    


private static void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true)

    stream.Write((byte)0x02); // INTEGER
    var prefixZeros = 0;
    for (var i = 0; i < value.Length; i++)
    
        if (value[i] != 0) break;
        prefixZeros++;
    
    if (value.Length - prefixZeros == 0)
    
        EncodeLength(stream, 1);
        stream.Write((byte)0);
    
    else
    
        if (forceUnsigned && value[prefixZeros] > 0x7f)
        
            // Add a prefix zero to force unsigned if the MSB is 1
            EncodeLength(stream, value.Length - prefixZeros + 1);
            stream.Write((byte)0);
        
        else
        
            EncodeLength(stream, value.Length - prefixZeros);
        
        for (var i = prefixZeros; i < value.Length; i++)
        
            stream.Write(value[i]);
        
    

【讨论】:

很好,这很完美,但是我如何导出公钥。有没有类似的结构。我尝试仅使用指数和模数(公钥的内容)来执行此操作,但它没有返回有效结果。如何获取公钥字符串? 没关系。我得到它的工作,我忘了删除版本部分。现在它还导出公钥。 对于那些感兴趣的人,可以通过我的答案中的代码来正确导出公钥:***.com/questions/28406888/…,它重复使用了这个答案中的一些方法。 这非常适合将私钥和公钥导出到pem 文件 我已经编译并稍微修改了 Iridium 的两个出色的导出函数,并将它们与导入函数相结合以获得完整的解决方案(导入和导出公钥和私钥):Import and export RSA Keys between C# and PEM format using BouncyCastle【参考方案2】:

要导出PublicKey,请使用以下代码:

public static String ExportPublicKeyToPEMFormat(RSACryptoServiceProvider csp)

    TextWriter outputStream = new StringWriter();

    var parameters = csp.ExportParameters(false);
    using (var stream = new MemoryStream())
    
        var writer = new BinaryWriter(stream);
        writer.Write((byte)0x30); // SEQUENCE
        using (var innerStream = new MemoryStream())
        
            var innerWriter = new BinaryWriter(innerStream);
            EncodeIntegerBigEndian(innerWriter, new byte[]  0x00 ); // Version
            EncodeIntegerBigEndian(innerWriter, parameters.Modulus);
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent);

            //All Parameter Must Have Value so Set Other Parameter Value Whit Invalid Data  (for keeping Key Structure  use "parameters.Exponent" value for invalid data)
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent); // instead of parameters.D
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent); // instead of parameters.P
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent); // instead of parameters.Q
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent); // instead of parameters.DP
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent); // instead of parameters.DQ
            EncodeIntegerBigEndian(innerWriter, parameters.Exponent); // instead of parameters.InverseQ

            var length = (int)innerStream.Length;
            EncodeLength(writer, length);
            writer.Write(innerStream.GetBuffer(), 0, length);
        

        var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray();
        outputStream.WriteLine("-----BEGIN PUBLIC KEY-----");
        // Output as Base64 with lines chopped at 64 characters
        for (var i = 0; i < base64.Length; i += 64)
        
            outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i));
        
        outputStream.WriteLine("-----END PUBLIC KEY-----");

        return outputStream.ToString();

    


private static void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true)

    stream.Write((byte)0x02); // INTEGER
    var prefixZeros = 0;
    for (var i = 0; i < value.Length; i++)
    
        if (value[i] != 0) break;
        prefixZeros++;
    
    if (value.Length - prefixZeros == 0)
    
        EncodeLength(stream, 1);
        stream.Write((byte)0);
    
    else
    
        if (forceUnsigned && value[prefixZeros] > 0x7f)
        
            // Add a prefix zero to force unsigned if the MSB is 1
            EncodeLength(stream, value.Length - prefixZeros + 1);
            stream.Write((byte)0);
        
        else
        
            EncodeLength(stream, value.Length - prefixZeros);
        
        for (var i = prefixZeros; i < value.Length; i++)
        
            stream.Write(value[i]);
        
    


private static void EncodeLength(BinaryWriter stream, int length)

    if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative");
    if (length < 0x80)
    
        // Short form
        stream.Write((byte)length);
    
    else
    
        // Long form
        var temp = length;
        var bytesRequired = 0;
        while (temp > 0)
        
            temp >>= 8;
            bytesRequired++;
        
        stream.Write((byte)(bytesRequired | 0x80));
        for (var i = bytesRequired - 1; i >= 0; i--)
        
            stream.Write((byte)(length >> (8 * i) & 0xff));
        
    

【讨论】:

我必须删除 EncodeIntegerBigEndian(innerWriter, new byte[] 0x00 ); // Version 行才能将生成的 PEM 文件加载到 Poco 库的 Crypto::RSAKey 中。 这对我不起作用。相反,我使用了@Iridium 的另一篇帖子***.com/questions/28406888/…,根据他的答案下方的cmets。 这六个额外的写入到底是什么... RSA 公钥是n,e 对。您应该检查 PKCS #1 以了解网络上的正确格式,而不是像上面那样分享黑客。【参考方案3】:

对于那些对原始答案的明显复杂性犹豫不决的其他人(这非常有帮助,不要误会我的意思),我想我会发布我的解决方案,它更简单一点 IMO(但仍基于原始答案):

public class RsaCsp2DerConverter 
   private const int MaximumLineLength = 64;

   // Based roughly on: http://***.com/a/23739932/1254575

   public RsaCsp2DerConverter() 

   

   public byte[] ExportPrivateKey(String cspBase64Blob) 
      if (String.IsNullOrEmpty(cspBase64Blob) == true)
         throw new ArgumentNullException(nameof(cspBase64Blob));

      var csp = new RSACryptoServiceProvider();

      csp.ImportCspBlob(Convert.FromBase64String(cspBase64Blob));

      if (csp.PublicOnly)
         throw new ArgumentException("CSP does not contain a private key!", nameof(csp));

      var parameters = csp.ExportParameters(true);

      var list = new List<byte[]> 
         new byte[] 0x00,
         parameters.Modulus,
         parameters.Exponent,
         parameters.D,
         parameters.P,
         parameters.Q,
         parameters.DP,
         parameters.DQ,
         parameters.InverseQ
      ;

      return SerializeList(list);
   

   private byte[] Encode(byte[] inBytes, bool useTypeOctet = true) 
      int length = inBytes.Length;
      var bytes = new List<byte>();

      if (useTypeOctet == true)
         bytes.Add(0x02); // INTEGER

      bytes.Add(0x84); // Long format, 4 bytes
      bytes.AddRange(BitConverter.GetBytes(length).Reverse());
      bytes.AddRange(inBytes);

      return bytes.ToArray();
   

   public String PemEncode(byte[] bytes) 
      if (bytes == null)
         throw new ArgumentNullException(nameof(bytes));

      var base64 = Convert.ToBase64String(bytes);

      StringBuilder b = new StringBuilder();
      b.Append("-----BEGIN RSA PRIVATE KEY-----\n");

      for (int i = 0; i < base64.Length; i += MaximumLineLength)
         b.Append($" base64.Substring(i, Math.Min(MaximumLineLength, base64.Length - i)) \n");

      b.Append("-----END RSA PRIVATE KEY-----\n");

      return b.ToString();
   

   private byte[] SerializeList(List<byte[]> list) 
      if (list == null)
         throw new ArgumentNullException(nameof(list));

      var keyBytes = list.Select(e => Encode(e)).SelectMany(e => e).ToArray();

      var binaryWriter = new BinaryWriter(new MemoryStream());
      binaryWriter.Write((byte) 0x30); // SEQUENCE
      binaryWriter.Write(Encode(keyBytes, false));
      binaryWriter.Flush();

      var result = ((MemoryStream) binaryWriter.BaseStream).ToArray();

      binaryWriter.BaseStream.Dispose();
      binaryWriter.Dispose();

      return result;
   

【讨论】:

【参考方案4】:
 public static Func<string, string> ToBase64PemFromKeyXMLString= (xmlPrivateKey) =>
        
            if (string.IsNullOrEmpty(xmlPrivateKey))
                throw new ArgumentNullException("RSA key must contains value!");
            var keyContent = new PemReader(new StringReader(xmlPrivateKey));
            if (keyContent == null)
                throw new ArgumentNullException("private key is not valid!");
            var ciphrPrivateKey = (AsymmetricCipherKeyPair)keyContent.ReadObject();
            var asymmetricKey = new AsymmetricKeyEntry(ciphrPrivateKey.Private);

            PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(asymmetricKey.Key);
            var serializedPrivateKey = privateKeyInfo.ToAsn1Object().GetDerEncoded();
            return Convert.ToBase64String(serializedPrivateKey);
        ;

【讨论】:

【参考方案5】:

如果您使用的是 .NET Core 3.0,并且您不需要依赖于 Windows 平台的RSACryptoServiceProvider,这已经实现了开箱即用

    public string ExportPrivateKey(RSA rsa)
    
        var privateKeyBytes = rsa.ExportRSAPrivateKey();
        var builder = new StringBuilder("-----BEGIN RSA PRIVATE KEY");
        builder.AppendLine("-----");

        var base64PrivateKeyString = Convert.ToBase64String(privateKeyBytes);
        var offset = 0;
        const int LINE_LENGTH = 64;

        while (offset < base64PrivateKeyString.Length)
        
            var lineEnd = Math.Min(offset + LINE_LENGTH, base64PrivateKeyString.Length);
            builder.AppendLine(base64PrivateKeyString.Substring(offset, lineEnd - offset));
            offset = lineEnd;
        

        builder.Append("-----END RSA PRIVATE KEY");
        builder.AppendLine("-----");
        return builder.ToString();
    

注意:在 .NET Core 中,您不会使用 RSACryptoServiceProvider,因为它更特定于 Windows。

【讨论】:

这段代码对我来说工作得很好-但有一个小错误:开头行声明“BEGIN RSA PRIVATE KEY”(这是正确的),但结尾行省略了“RSA”->结束行应该是“END RSA PRIVATE KEY” 嗨@Prasanth,您能否分享一下 .NET Core 中的 RSACryptoServiceProvider 是特定于 Windows 的地方的详细信息?【参考方案6】:

使用当前版本的 .NET CORE,无需在所有其他响应中提供所有方便的东西即可完成此操作。


  RSA rsa = RSA.Create();
  rsa.KeySize = 4096;

  string hdr = "-----BEGIN RSA PRIVATE KEY-----";
  string ftr = "-----END RSA PRIVATE KEY-----";

  string priv = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());

  // To export the public key, update hdr and ftr with `PUBLIC`.
  // string pub = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo());

  string PEM = $"hdr\npriv\nftr";
  // Distribute PEM.

【讨论】:

在您对导出公钥的评论中,您说要使用 hdr 和 ftr,但这些值特定于私钥....应该是 hdr="-----BEGIN RSA PUBLIC KEY- ----" and ftr="-----END RSA PUBLIC KEY-----" 也许我写得不好,但说update hdr and ftr in 的意思正是你所说的。 :)

以上是关于C# 将私有/公共 RSA 密钥从 RSACryptoServiceProvider 导出到 PEM 字符串的主要内容,如果未能解决你的问题,请参考以下文章

使用RSACryptoServiceProvider从XML导入RSA公钥会将属性PublicOnly设置为false

使用标准库的 Python 中的私有/公共加密

C# RSA 公钥输出不正确

如何将公共RSA密钥上传到HSM(使用PKCS#11库)?

如何在 C# 中加载私有 ecdsa 密钥 es256?

.NET 中的 RSA 密钥指数有限制吗?