C# экспорт закрытого / открытого ключа RSA из RSACryptoServiceProvider в строку PEM

У меня есть экземпляр System.Безопасность.Криптография.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. Есть ли способ сгенерировать эту строку?

4 ответов


обратите внимание: код ниже предназначен для экспорта частная ключ. Если вы хотите экспортировать общественные ключ, пожалуйста, обратитесь к моему ответу, данному здесь.

формат PEM-это просто ASN.1 DER кодировка ключа (per 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]);
        }
    }
}

для экспорта 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));
        }
    }
}

для тех, кто отказался от кажущейся сложности исходного ответа (что очень полезно, не поймите меня неправильно), я думал, что опубликую свое решение, которое немного более простое IMO (но все же основано на оригинальном ответе):

public class RsaCsp2DerConverter {
   private const int MaximumLineLength = 64;

   // Based roughly on: http://stackoverflow.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;
   }
}

 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);
        };