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.
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
- https://www.pcwdld.com/cygwin-cheat-sheet
- https://github.com/dejanstojanovic/dotnetcore-token-authentication/tree/asymmetric_rsahelper
- https://github.com/dejanstojanovic/dotnetcore-token-authentication/blob/asymmetric_rsahelper/Sample.Core.Resource.Asymetric.Api/Startup.cs
- https://www.nuget.org/packages/BouncyCastle.NetCore/
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.
Comments for this article