Loading RSA key pair from PEM files in .NET Core with C#

BouncyCastle wrapper for loading RSA keys from PEM files instead of XML files

Recently I wrote and article about using asymmetric keys for token based authentication in ASP.NET Core. As I was setting up the RSA keys to test the demo appliation I kept running to some challanges that did not make thigs going so smooth

Challenge number 1 - OpenSSL

To start with everthing, first I had to generate RSA key pair since I had to use an external tool which is not natively a part of Windows since I am using Windows as my development machine. For this porpose I could either spin up a Linix VM or, as an easier and less heavy solution, I used Cygwin tool to run OpenSSL commands and generate private/public RSA key value pair.

Challenge number 2 - PEM format not supported

Next challange was that .NET framework does not load RSA keys natively from PEM which is the output format of the OpenSSL tool. Instead it needs XML folrmat of the private/public keys. I had to take my RSA keys and re-format it from PEM to XML using online too (https://superdry.apphb.com/tools/online-rsa-key-converter).

This is fine for setting up demo project, but let's say we want to automate key generation. With using and external website, it is a bit dodgy. There has to be a better way to do this. Ideally, I would like to get away from the XML format of RSA key pairs.

Challenge number 2 - .NET Core 2.1 does not support FromXmlString

Since I decided to write my whole code for ASP.NET Core, there was another challenge. Apparently .NET Core 2.1 still does not support FromXmlString in System.Security.Cryptography.RSA class. As soon as I tried to invoke FromXmlString method to load my RSA key from XML file, I got System.PlatformNotSupportedException.

Rsa Xml Core Not Supported By Platform

For that purpose I had to write an extension method to do pretty much the same thing this method is doing in full .NET framework 4.x (https://github.com/dotnet/corefx/issues/23686).

using System;
using System.IO;
using System.Security.Cryptography;
using System.Xml;

namespace Sample.Core.Common.Extensions
{
    public static class Encryption
    {
        public static void FromXmlFile(this RSA rsa, string xmlFilePath)
        {
            RSAParameters parameters = new RSAParameters();

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(File.ReadAllText(xmlFilePath));

            if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue"))
            {
                foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes)
                {
                    switch (node.Name)
                    {
                        case "Modulus": parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "Exponent": parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "P": parameters.P = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "Q": parameters.Q = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "DP": parameters.DP = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "DQ": parameters.DQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "InverseQ": parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "D": parameters.D = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                    }
                }
            }
            else
            {
                throw new Exception("Invalid XML RSA key.");
            }

            rsa.ImportParameters(parameters);
        }

        public static void ToXmlFile(this RSA rsa, bool includePrivateParameters, string xmlFilePath)
        {
            RSAParameters parameters = rsa.ExportParameters(includePrivateParameters);

            File.WriteAllText(xmlFilePath,
                string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
                  parameters.Modulus != null ? Convert.ToBase64String(parameters.Modulus) : null,
                  parameters.Exponent != null ? Convert.ToBase64String(parameters.Exponent) : null,
                  parameters.P != null ? Convert.ToBase64String(parameters.P) : null,
                  parameters.Q != null ? Convert.ToBase64String(parameters.Q) : null,
                  parameters.DP != null ? Convert.ToBase64String(parameters.DP) : null,
                  parameters.DQ != null ? Convert.ToBase64String(parameters.DQ) : null,
                  parameters.InverseQ != null ? Convert.ToBase64String(parameters.InverseQ) : null,
                  parameters.D != null ? Convert.ToBase64String(parameters.D) : null)
                  );
        }
    }
}

    

Taking different path

Obviously whole story about storing and loading RSA key pair from and to XML files does not look good, so I decided to see what can I do with PEM format files. After some time Googling around I found that BouncyCastle nuget package is handling cryptography a lot better than native .NET library for cryptography. It supports direct reading and writing RSA with PEM files and I found that there is official nuget package available for .NET Core

Install-Package BouncyCastle.NetCore -Version 1.8.2
    

It was just what I needed. First methods I wrote were the ones to generate RSA key pairs from code and save them to files

        public static void GenerateRsaKeyPair(String privateKeyFilePath, String publicKeyFilePath)
        {
            RsaKeyPairGenerator rsaGenerator = new RsaKeyPairGenerator();
            rsaGenerator.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
            var keyPair = rsaGenerator.GenerateKeyPair();


            using (TextWriter privateKeyTextWriter = new StringWriter())
            {

                PemWriter pemWriter = new PemWriter(privateKeyTextWriter);
                pemWriter.WriteObject(keyPair.Private);
                pemWriter.Writer.Flush();
                File.WriteAllText(privateKeyFilePath, privateKeyTextWriter.ToString());
            }


            using (TextWriter publicKeyTextWriter = new StringWriter())
            {

                PemWriter pemWriter = new PemWriter(publicKeyTextWriter);
                pemWriter.WriteObject(keyPair.Public);
                pemWriter.Writer.Flush();

                File.WriteAllText(publicKeyFilePath, publicKeyTextWriter.ToString());
            }
        }
        
    

Next thing is to read private and public key from those files. There are already methods for this in BouncyCastle package, but there is a catch, as always.

ASP.NET Core token based authentication is working with .NET built in classes, so I had to either wrap BouncyCastle classes or find the way to build .NET RSA cryptography class instances from BouncyCastle. I decided to generate new System.Security.Cryptography.RSACryptoServiceProvider class instances by assigning the property from BouncyCastle instances and end up with the following two methods

        public static RSACryptoServiceProvider PrivateKeyFromPemFile(String filePath)
        {
            using (TextReader privateKeyTextReader = new StringReader(File.ReadAllText(filePath)))
            {
                AsymmetricCipherKeyPair readKeyPair = (AsymmetricCipherKeyPair)new PemReader(privateKeyTextReader).ReadObject();


                RsaPrivateCrtKeyParameters privateKeyParams = ((RsaPrivateCrtKeyParameters)readKeyPair.Private);
                RSACryptoServiceProvider cryptoServiceProvider = new RSACryptoServiceProvider();
                RSAParameters parms = new RSAParameters();

                parms.Modulus = privateKeyParams.Modulus.ToByteArrayUnsigned();
                parms.P = privateKeyParams.P.ToByteArrayUnsigned();
                parms.Q = privateKeyParams.Q.ToByteArrayUnsigned();
                parms.DP = privateKeyParams.DP.ToByteArrayUnsigned();
                parms.DQ = privateKeyParams.DQ.ToByteArrayUnsigned();
                parms.InverseQ = privateKeyParams.QInv.ToByteArrayUnsigned();
                parms.D = privateKeyParams.Exponent.ToByteArrayUnsigned();
                parms.Exponent = privateKeyParams.PublicExponent.ToByteArrayUnsigned();

                cryptoServiceProvider.ImportParameters(parms);

                return cryptoServiceProvider;
            }
        }

        public static RSACryptoServiceProvider PublicKeyFromPemFile(String filePath)
        {
            using (TextReader publicKeyTextReader = new StringReader(File.ReadAllText(filePath)))
            {
                RsaKeyParameters publicKeyParam = (RsaKeyParameters)new PemReader(publicKeyTextReader).ReadObject();

                RSACryptoServiceProvider cryptoServiceProvider = new RSACryptoServiceProvider();
                RSAParameters parms = new RSAParameters();



                parms.Modulus = publicKeyParam.Modulus.ToByteArrayUnsigned();
                parms.Exponent = publicKeyParam.Exponent.ToByteArrayUnsigned();


                cryptoServiceProvider.ImportParameters(parms);

               return cryptoServiceProvider;
            }
        }

    

Now it is a lot easier to setup the token authentication in ASP.NET Core in Startup.cs. The following is modified authentication confutation from the project in article Token based authentication and Identity framework in ASP.NET Core - Part 3

 #region Add Authentication

            RSA publicRsa = RsaHelper.PublicKeyFromPemFile(Path.Combine(Directory.GetCurrentDirectory(),
                                "Keys",
                                 this.Configuration.GetValue<String>("Tokens:PublicKey")
                                 ));
            
                
                RsaSecurityKey signingKey = new RsaSecurityKey(publicRsa);


                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                }).AddJwtBearer(config =>
                {
                    config.RequireHttpsMetadata = false;
                    config.SaveToken = true;
                    config.TokenValidationParameters = new TokenValidationParameters()
                    {
                        IssuerSigningKey = signingKey,
                        ValidateAudience = true,
                        ValidAudience = this.Configuration["Tokens:Audience"],
                        ValidateIssuer = true,
                        ValidIssuer = this.Configuration["Tokens:Issuer"],
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    };
                });
            
            #endregion
    

Full code for this can be found on github https://github.com/dejanstojanovic/dotnetcore-token-authentication/blob/asymmetric_rsahelper/Sample.Core.Resource.Asymetric.Api/Startup.cs

You can also clone/fork the whole solution branch https://github.com/dejanstojanovic/dotnetcore-token-authentication/tree/asymmetric_rsahelper

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article