C# Export Private/Public RSA key from RSACryptoServiceProvider to PEM string
Asked Answered
C

6

57

I have an instance of System.Security.Cryptography.RSACryptoServiceProvider, i need to export it's key to a PEM string - like this:

-----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-----

But there is no such option according to the MSDN documentation, there is only some kind of XML export. I can't use any third party libraries like BouncyCastle. Is there any way to generate this string?

Comply answered 19/5, 2014 at 9:57 Comment(2)
How and where does an instance of that class have a key?Iodous
Tthe pain point is due to .Net and their use of XML encoding from RFC 3275. .Net does not use ASN.1/DER or PEM encoded keys. I think its the only crypto library that does things this way.Kolosick
C
94

Please note: The code below is for exporting a private key. If you are looking to export the public key, please refer to my answer given here.

The PEM format is simply the ASN.1 DER encoding of the key (per PKCS#1) converted to Base64. Given the limited number of fields needed to represent the key, it's pretty straightforward to create quick-and-dirty DER encoder to output the appropriate format then Base64 encode it. As such, the code that follows is not particularly elegant, but does the job:

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]);
        }
    }
}
Constitutionally answered 19/5, 2014 at 14:6 Comment(9)
Great, this works perfectly, but how do I export the public key. Does it have similar structure. I've tried doing this with just the exponent and modulus (the contents of the public key) but it doesn't return a valid result. How to get the public key string?Comply
Never mind. I got it working, I forgot to remove the version part. Now it also exports public keys.Comply
For those interested, correctly exporting a public key can be done via the code in my answer here: #28407388 which re-uses some of the methods from this answer.Constitutionally
This works great for exporting both private key and public key to pem fileNaxos
@RijulSudhir if you're exporting the public key, you should use the code given in #28407388Constitutionally
@Constitutionally I tried the code in that link first. But it didn't work.File is created. But decryption using that key file fails. But decryption using public key exported with ExportPrivateKey function worked fine for me.Naxos
@RijulSudhir if you're decrypting a file, you need the private key (exported by the code above), not a public key.Constitutionally
@Constitutionally I think you misunderstood what I wrote. Any way, for me export function ExportPrivateKey worked perfectly for both private and public key. I wanted anyone who may read this answer to know about that. That is why I commented in the first place. So that they can also try it if everything else fails in case of exporting public key.Naxos
I've compiled and slightly modified Iridium's two excellent export functions and combined them with import functions for a full solution (import and export both public and private keys): Import and export RSA Keys between C# and PEM format using BouncyCastleDomiciliate
T
19

With the current version of .NET, this can be done in a simple way.


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

  // Private key export.
  string hdrPrv = "-----BEGIN RSA PRIVATE KEY-----";
  string ftrPrv = "-----END RSA PRIVATE KEY-----";
  string keyPrv = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());
  string PEMPrv = $"{hdrPrv}\n{keyPrv}\n{ftrPrv}";//concatenate header and footer to actual key


  // Public key export (with a correction for more accuracy from RobSiklos's comment to have the key in PKCS#1 RSAPublicKey format)
  string hdrPub = "-----BEGIN RSA PUBLIC KEY-----";
  string ftrPub = "-----END RSA PUBLIC KEY-----";
  string keyPub = Convert.ToBase64String(rsa.ExportRSAPublicKey());
  string PEMPub = $"{hdrPub}\n{keyPub}\n{ftrPub}";//concatenate header and footer to actual key
  
  // Distribute PEMs.

Note: To have the nicely formatted file with new lines, you can write a little function to do it for you. With the solution given above, you will have a file with only three lines.

Tigre answered 10/9, 2020 at 5:42 Comment(3)
In your comment for exporting public key you say to use hdr and ftr but those values are specific to the private key.... should be hdr="-----BEGIN RSA PUBLIC KEY-----" and ftr="-----END RSA PUBLIC KEY-----"Enchant
perhaps I wrote this bad, but saying update hdr and ftr in meant exactly what you said. :)Tigre
This is slightly incorrect. When using ExportSubjectPublicKeyInfo() the header/footer should contain "PUBLIC KEY" not "RSA PUBLIC KEY". If you want to use the latter, then use ExportRSAPublicKey() instead of ExportSubjectPublicKeyInfo()Grefe
S
12

If you're using .NET Core 3.0 this is already implemented out of the box

    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();
    }
Sink answered 12/2, 2020 at 18:14 Comment(3)
This code worked just fine for me - BUT is has a small bug: The opening line states "BEGIN RSA PRIVATE KEY" (which is correct), but the closing line omits the "RSA" -> closing line should be "END RSA PRIVATE KEY"Ludendorff
Hi @Prasanth, can you please share the details about where it is pointed that RSACryptoServiceProvider in .NET Core is Windows specific?Allgood
I think, RSACryptoServiceProvider is not windows specific. Windows CSP (CAPI) is windows specific. learn.microsoft.com/en-us/dotnet/api/…Conversationalist
S
10

Here is a GREAT NEWS, with .NET 7, Microsoft has added a new method to export RSA keys directly to PEM format.

Refer RSA.ExportRSAPrivateKeyPem Method

Below is how you can use it.

using (var rsa = new RSACryptoServiceProvider(2048)) // Generate a new 2048 bit RSA key
{
    // RSA keys in PKCS#1 format, PEM encoded
    string publicPrivateKeyPEM = rsa.ExportRSAPrivateKeyPem();
    string publicOnlyKeyPEM = rsa.ExportRSAPublicKeyPem();

    // RSA keys in XML format
    string publicPrivateKeyXML = rsa.ToXmlString(true);
    string publicOnlyKeyXML = rsa.ToXmlString(false);

    // RSA keys in byte array
    byte[] publicPrivateKey = rsa.ExportRSAPrivateKey();
    byte[] publicOnlyKey = rsa.ExportRSAPublicKey();
}
Scouting answered 19/1, 2023 at 8:55 Comment(0)
S
3

For anyone else who balked at the original answer's apparent complexity (which is very helpful, don't get me wrong), I thought I'd post my solution which is a little more straightforward IMO (but still based on the original answer):

public class RsaCsp2DerConverter {
   private const int MaximumLineLength = 64;

   // Based roughly on: https://mcmap.net/q/535536/-c-export-private-public-rsa-key-from-rsacryptoserviceprovider-to-pem-string

   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;
   }
}
Succubus answered 28/10, 2016 at 13:54 Comment(0)
P
-1
 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);
        };
Posset answered 12/3, 2018 at 6:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.