Skip to content

Instantly share code, notes, and snippets.

@therightstuff
Last active November 3, 2023 16:34
Show Gist options
  • Star 47 You must be signed in to star a gist
  • Fork 15 You must be signed in to fork a gist
  • Save therightstuff/aa65356e95f8d0aae888e9f61aa29414 to your computer and use it in GitHub Desktop.
Save therightstuff/aa65356e95f8d0aae888e9f61aa29414 to your computer and use it in GitHub Desktop.
Import and export RSA Keys between C# and PEM format using BouncyCastle
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.IO;
using System.Security.Cryptography;
namespace MyProject.Data.Encryption
{
public class RSAKeys
{
/// <summary>
/// Import OpenSSH PEM private key string into MS RSACryptoServiceProvider
/// </summary>
/// <param name="pem"></param>
/// <returns></returns>
public static RSACryptoServiceProvider ImportPrivateKey(string pem) {
PemReader pr = new PemReader(new StringReader(pem));
AsymmetricCipherKeyPair KeyPair = (AsymmetricCipherKeyPair)pr.ReadObject();
RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)KeyPair.Private);
RSACryptoServiceProvider csp = new RSACryptoServiceProvider();// cspParams);
csp.ImportParameters(rsaParams);
return csp;
}
/// <summary>
/// Import OpenSSH PEM public key string into MS RSACryptoServiceProvider
/// </summary>
/// <param name="pem"></param>
/// <returns></returns>
public static RSACryptoServiceProvider ImportPublicKey(string pem) {
PemReader pr = new PemReader(new StringReader(pem));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();
RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);
RSACryptoServiceProvider csp = new RSACryptoServiceProvider();// cspParams);
csp.ImportParameters(rsaParams);
return csp;
}
/// <summary>
/// Export private (including public) key from MS RSACryptoServiceProvider into OpenSSH PEM string
/// slightly modified from https://stackoverflow.com/a/23739932/2860309
/// </summary>
/// <param name="csp"></param>
/// <returns></returns>
public static string ExportPrivateKey(RSACryptoServiceProvider csp)
{
StringWriter outputStream = new StringWriter();
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();
// WriteLine terminates with \r\n, we want only \n
outputStream.Write("-----BEGIN RSA PRIVATE KEY-----\n");
// Output as Base64 with lines chopped at 64 characters
for (var i = 0; i < base64.Length; i += 64)
{
outputStream.Write(base64, i, Math.Min(64, base64.Length - i));
outputStream.Write("\n");
}
outputStream.Write("-----END RSA PRIVATE KEY-----");
}
return outputStream.ToString();
}
/// <summary>
/// Export public key from MS RSACryptoServiceProvider into OpenSSH PEM string
/// slightly modified from https://stackoverflow.com/a/28407693
/// </summary>
/// <param name="csp"></param>
/// <returns></returns>
public static string ExportPublicKey(RSACryptoServiceProvider csp)
{
StringWriter 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);
innerWriter.Write((byte)0x30); // SEQUENCE
EncodeLength(innerWriter, 13);
innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER
var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 };
EncodeLength(innerWriter, rsaEncryptionOid.Length);
innerWriter.Write(rsaEncryptionOid);
innerWriter.Write((byte)0x05); // NULL
EncodeLength(innerWriter, 0);
innerWriter.Write((byte)0x03); // BIT STRING
using (var bitStringStream = new MemoryStream())
{
var bitStringWriter = new BinaryWriter(bitStringStream);
bitStringWriter.Write((byte)0x00); // # of unused bits
bitStringWriter.Write((byte)0x30); // SEQUENCE
using (var paramsStream = new MemoryStream())
{
var paramsWriter = new BinaryWriter(paramsStream);
EncodeIntegerBigEndian(paramsWriter, parameters.Modulus); // Modulus
EncodeIntegerBigEndian(paramsWriter, parameters.Exponent); // Exponent
var paramsLength = (int)paramsStream.Length;
EncodeLength(bitStringWriter, paramsLength);
bitStringWriter.Write(paramsStream.GetBuffer(), 0, paramsLength);
}
var bitStringLength = (int)bitStringStream.Length;
EncodeLength(innerWriter, bitStringLength);
innerWriter.Write(bitStringStream.GetBuffer(), 0, bitStringLength);
}
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();
// WriteLine terminates with \r\n, we want only \n
outputStream.Write("-----BEGIN PUBLIC KEY-----\n");
for (var i = 0; i < base64.Length; i += 64)
{
outputStream.Write(base64, i, Math.Min(64, base64.Length - i));
outputStream.Write("\n");
}
outputStream.Write("-----END PUBLIC KEY-----");
}
return outputStream.ToString();
}
/// <summary>
/// https://stackoverflow.com/a/23739932/2860309
/// </summary>
/// <param name="stream"></param>
/// <param name="length"></param>
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));
}
}
}
/// <summary>
/// https://stackoverflow.com/a/23739932/2860309
/// </summary>
/// <param name="stream"></param>
/// <param name="value"></param>
/// <param name="forceUnsigned"></param>
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]);
}
}
}
public static void runTests(){
Console.WriteLine("Starting RSAKeys tests");
RSACryptoServiceProvider initialProvider = new RSACryptoServiceProvider(2048);
String privateKey = RSAKeys.ExportPrivateKey(initialProvider);
String publicKey = RSAKeys.ExportPublicKey(initialProvider);
Console.WriteLine("-----------------------");
Console.Write("Private Key exported: ");
Console.WriteLine(privateKey);
Console.WriteLine("-----------------------");
Console.Write("Public Key exported: ");
Console.WriteLine(publicKey);
RSACryptoServiceProvider importedProvider = RSAKeys.ImportPrivateKey(privateKey);
privateKey = RSAKeys.ExportPrivateKey(importedProvider);
publicKey = RSAKeys.ExportPublicKey(importedProvider);
Console.WriteLine("-----------------------");
Console.Write("Private Key imported/exported: ");
Console.WriteLine(privateKey);
Console.WriteLine("-----------------------");
Console.Write("Public Key imported/exported: ");
Console.WriteLine(publicKey);
Console.WriteLine("RSAKeys tests completed");
}
}
}
@therightstuff
Copy link
Author

Method "RSACryptoServiceProvider ImportPublicKey", Doesn't work
It will be easier, and this way and this option works.
public static RSACryptoServiceProvider ImportPublicKey(string pub) { PemReader pr2 = new PemReader(new StringReader(pub)); AsymmetricCipherKeyPair KeyPair = (AsymmetricCipherKeyPair)pr2.ReadObject(); RSAParameters rsaParam = DotNetUtilities.ToRSAParameters((RsaKeyParameters)KeyPair.Public); RSACryptoServiceProvider csp = new RSACryptoServiceProvider();// cspParams); csp.ImportParameters(rsaParam); Console.WriteLine(csp.ToXmlString(**false**)); return csp; }

I've just added a couple of tests (added to this gist) and run it on VSCode on a Mac using the latest BouncyCastle version from NuGet, works perfectly for me. Could you please provide more context? Also, your code block doesn't let me know which packages you're including, it would be great if you created a gist with the complete class file and posted a link here.

If there's an easier way to do this, I'm all for it!

@soondook
Copy link

soondook commented Nov 9, 2019

I apologize for my English:))
In my project, I use the following components:
Microsoft Visual Studio Community 2019 / VisualStudio.16.Release/16.2.5+29306.81
Microsoft.CSharp: v4.0.30319
BouncyCastle.Crypto: 1.8.1.0 / v1.1.4322 (latest version from NuGet)
When I try use your codeblock, this leads to an unhandled exception:
System.InvalidCastException: "Failed to cast object type "Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair" to type "Org.BouncyCastle.Crypto.AsymmetricKeyParameter"."

Thank you for the good job! Hope our efforts help someone else.. ))
Best Regards!

@therightstuff
Copy link
Author

Your English is just fine :)

The error you're receiving is because you're providing the ImportPrivateKey method a public key, and it must receive a private key. If you are trying to import a public key, you need to use ImportPublicKey.

@sebagomez
Copy link

you sir, are a lifesaver

@therightstuff
Copy link
Author

you sir, are a lifesaver

Glad to be able to help!

@ridhoq
Copy link

ridhoq commented Feb 26, 2020

FYI, for anyone who is stuck by not finding the DotNetUtilities: see this comment: bcgit/bc-csharp#113 (comment). You are probably using the wrong NuGet package.

@ledunguit
Copy link

work like a charm !! 2021 :v Lifesaver!

@therightstuff
Copy link
Author

@ridhoq - thank you!
@ledunguit - great stuff!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment