skyrunner
skyrunner

Reputation: 500

"X509 certificate does not have a private key" when generating self signed certificate for Identity Server 4

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

Upvotes: 3

Views: 6088

Answers (1)

Tarik Tutuncu
Tarik Tutuncu

Reputation: 818

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

Upvotes: 2

Related Questions