3

Here's what i'm trying to achieve. We're developing a microservices app on Kubernetes. One of the microservices is IdentityServer instance. Initially i want to test the solution locally on Docker to make sure it works. For this purpose, i want to paste the certificate to appsettings.json. Eventually this value will be replaced by a Kubernetes secret. In my startup class this is how i'm trying to load my certificate:

 services.AddIdentityServer()
         .AddSigningCredential(GetIdentityServerCertificate())
         .AddConfigurationStore(...

    private X509Certificate2 GetIdentityServerCertificate()
    {
        var clientSecret = Configuration["Certificate"];
        var pfxBytes = Convert.FromBase64String(clientSecret);
        var certificate = new X509Certificate2(pfxBytes);
        return certificate;
    }

The certificate is generated by me using openssl:

openssl req –newkey rsa:2048 –nodes –keyout XXXXX.key –x509 –days 365 –out XXXXX.cer

openssl pkcs12 –export –in XXXX.cer –inkey XXXX.key –out XXXX.pfx

Then i get the certificate by using:

openssl pkcs12 -in XXXX.pfx -info -nokeys

-----BEGIN CERTIFICATE-----
i take this content and paste into appconfig.json
-----END CERTIFICATE-----

When i debug it, the result is: System.InvalidOperationException: 'X509 certificate does not have a private key.'

Nick Rak
  • 2,629
  • 13
  • 19
skyrunner
  • 460
  • 1
  • 7
  • 18
  • 1
    You took only the certificate content from pkcs#12 (in base64 format). You should take whole pkcs#12 content in DER encoding as `pfxBytes`. – pepo Nov 27 '18 at 20:16
  • And when loading PKCS#12 into X509Certificate2 constructor you will need to provide also the password as the second parameter of the constructor. – pepo Nov 27 '18 at 20:22
  • I've tried to load entire certificate initially but i got different error then. I've created another question regarding this issue: https://stackoverflow.com/questions/53513317/error23076071pkcs12-routinespkcs12-parsemac-verify-failure-when-generating Not sure how to handle DER encoding. Should i somehow encode the certificate to DER string and then in my C# class i should use something different than FromBase64String method to read it from json? – skyrunner Nov 28 '18 at 06:26
  • You created it using openssl. If I remember correctly it should already be in DER encoding (raw bytes). – pepo Nov 28 '18 at 12:33
  • Did you solve it? – Charlie Jan 09 '19 at 05:47
  • It is as described in the first comment. In the end, I decided not to load the certificate from a string value but instead i'm loading .pfx file and i had no issues with it since then. – skyrunner Jan 10 '19 at 01:35

1 Answers1

2

Here is an example that works in .net core:

Use openssl tool. Open your terminal and type the following commands:

openssl genrsa -out private.pem 2048 
openssl rsa -in private.pem -outform PEM -pubout -out public.pem

It will result in creating 2 files.

There’s one more thing before we can actually use our RSA keys within .NET Core application. We need to convert them into XML. Use a "RSA PEM to XML Converter". It can be done here: Online rsa key converter Before copying XML into private-rsa-key.xml and public-rsa-key.xml files, format them using: XML Formatter

The private key is required only for the service responsible for generating the token. This must not be shared outside of this project.

Startup.cs :

public class Startup
{
    public IConfiguration Configuration { get; }
    private SigningCredentials _signingCredentials;

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // other code
        services
                .AddIdentityServer()
                .AddSigningCredential(_signingCredentials)
                .AddInMemoryApiResources(_identityConfig.GetApiResources())
                .AddInMemoryClients(_identityConfig.GetClients());
        // other code
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseMvc();
        app.UseIdentityServer();
    }

    private void InitializeRsaKey()
    {
        try
        {
            RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(2048);
            var rsaParametersPrivate = RSAExtensions.RSAParametersFromXmlFile(Configuration.GetSection("JwtSettings:rsaPrivateKeyXml").Value);
            rsaProvider.ImportParameters(rsaParametersPrivate);
            var securityKey = new RsaSecurityKey(rsaProvider);
            _signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);
        }
        catch(Exception ex)
        {
            throw new Exception("Identity Server RSA Key initialization failed. " + ex.ToString());
        }
    }
}

The RSAExtensions class:

public static class RSAExtensions
{
    /// <summary>
    /// Gets RSA Parameters from XML file.
    /// </summary>
    /// <param name="xmlFilePath">The XML file path.</param>
    /// <returns>RSAParameters.</returns>
    /// <exception cref="Exception">Invalid XML RSA key.</exception>
    public static RSAParameters RSAParametersFromXmlFile(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.");
        }

        return parameters;
    }
}

In appsettings.json add the following:

"JwtSettings": {
    "rsaPrivateKeyXml": "RSAKeys/private-rsa-key.xml",
},

I don't know if you can store raw XML in appsettings.json. If not you can write a helper method to convert pem to xml and store the raw pem content inside a configuration variable.

I found a sample project in GitHub that may help you on the conversion: Rsa-Dotnet-Core

Tarik Tutuncu
  • 790
  • 4
  • 12