Solved: Identity Server v3 and ‘Invalid provider type specified’ CngKey private key errors

11 May 2016 4 By David

In the scenario described in this post, you have encountered an error in your log that says the following:

Signing certificate has not private key or private key is not accessible. Make sure the account running your application has access to the private key

TL;DR; See how to convert your certificate key from CNG to RSA

Or maybe your haven’t seen this error message but have encountered an issue with Identity Server signing it tokens.  If this is the case, I would highly recommend you stop and enable logging before you go any further, restart your application and check your logs for the error message above.  Logging is key, we need to see what is going on in order to know how to fix it! Jump ahead to Enabling Logging in Identity Server V3 then come back to find out what the error is all about.

Assumptions

Now, this article assumes you have checked some of the obvious things.  Run through the following list and if the answer is “Yes!” for every question then you may proceed 🙂

  • Have you checked that the certificate you are using actually has a private key?
  • Have you checked that the password you are using to access the private key is correct?
  • Have you checked that the account your application is running under has permissions to access to the private key?

OK, so you have answered yes to all of these questions – so it is maybe beginning to be more likely that you are falling foul of a Cng Key issue.

What is a CNG Key?

Certificates in Windows are stored using Storage Providers. Windows has two of these providers, that are not compatible. The old style “Cryptographic Service Providers” or CSP in short and the new style “Cryptography API: Next Generation” or CNG. The CNG providers have been around since Windows Vista, and although it is more secure and easier to use many facets of software are still not compatible with CNG providers.  This appears to also include the .NET Framework.

A possible workaround to this may be to use CryptoAPI/CNG API directly to deal with CNG keys.  But if we want an easier and pure .NET solution which understands CNG, we need to find another solution (details to follow!).

How was my PFX file generated?

In this case, I used the powershell module New-SelfSignedCertificate to generate a self-signed certificate and then exported that certificate to a PFX file via the Management Console.

Proving out the issue

So let’s investigate what’s causing the “Signing certificate has not private key or private key is not accessible” error.

To help me figure this out, I created the following test stub which I used to load a certificate (from an embedded resource in this case) and attempt to access the private key.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using IdentityServer3.Core.Logging;
using Security.Cryptography.X509Certificates;

public class Certificate
{
    private readonly static ILog Logger = LogProvider.For<Certificate>();
    public static X509Certificate2 Load()
    {
        var assembly = typeof(Certificate).Assembly;
        using (var stream = assembly.GetManifestResourceStream("TestApp.MyCert.pfx"))
        {
            var file = Path.Combine(Path.GetTempPath(), "TestApp-" + Guid.NewGuid());
            Logger.DebugFormat("Loading certificate from {0}", file);
            try
            {
                File.WriteAllBytes(file, ReadStream(stream));
                var cert = new X509Certificate2(file, "MyReallyStrongPassword", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.Exportable);

                Logger.DebugFormat("Certificate Has Private Key: {0}", cert.HasPrivateKey);
                Logger.DebugFormat("Is Private Access Allowed: {0}", IsPrivateAccessAllowed(cert));

                return cert;
            }
            finally
            {
                File.Delete(file);
            }
        }
    }

    private static byte[] ReadStream(Stream input)
    {
        var buffer = new byte[16 * 1024];
        using (var ms = new MemoryStream())
        {
            int read;
            while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
            {
                ms.Write(buffer, 0, read);
            }
            return ms.ToArray();
        }
    }

    public static bool IsPrivateAccessAllowed(X509Certificate2 cert)
    {
        try
        {
            var privateKey = cert.PrivateKey;
            return true;
        }
        catch (CryptographicException ex)
        {
            Logger.ErrorException("Error accessing private key", ex);
            return false;
        }
    }
}

Running this code results in the following entries in my log file

2016-05-11 15:56:45.473 +01:00 [Debug] Loading certificate from "C:\Windows\TEMP\TestApp-845cfe9c-a0d3-4bc7-bde1-53727c3c81c9"
2016-05-11 15:56:45.495 +01:00 [Debug] Certificate Has Private Key: True
2016-05-11 15:56:45.497 +01:00 [Debug] Is Private Access Allowed: False

Closer inspection of the error thrown when accessing the private key revealed the following exception being thrown.

System.Security.Cryptography.CryptographicException: Invalid provider type specified.
at System.Security.Cryptography.Utils.CreateProvHandle(CspParameters parameters, Boolean randomKeyContainer)
at System.Security.Cryptography.Utils.GetKeyPairHelper(CspAlgorithmType keyType, CspParameters parameters, Boolean randomKeyContainer, Int32 dwKeySize, SafeProvHandle& safeProvHandle, SafeKeyHandle& safeKeyHandle)
at System.Security.Cryptography.RSACryptoServiceProvider.GetKeyPair()
at System.Security.Cryptography.RSACryptoServiceProvider..ctor(Int32 dwKeySize, CspParameters parameters, Boolean useDefaultKeySize)
at System.Security.Cryptography.X509Certificates.X509Certificate2.get_PrivateKey()

Now we seem to be getting somewhere!

GoogleFoo resulted in me discovering Alejandro Magencio’s blog post on this very exception.  His post describes in detail the background to what is going on underneath the covers and why we are getting this exception.

In short, the .NET Framework being used does not like CNG keys.  In order access them we need to either talk directly to the CryptoAPI/CNG API or we could use the CLR Security open source library.

Adding in usage of the CLR Security library allows me to interogate the type of private key that my certificate has.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using IdentityServer3.Core.Logging;
using Security.Cryptography.X509Certificates;

public class Certificate
{
    private readonly static ILog Logger = LogProvider.For<Certificate>();
    public static X509Certificate2 Load()
    {
        var assembly = typeof(Certificate).Assembly;
        using (var stream = assembly.GetManifestResourceStream("TestApp.MyCert.pfx"))
        {
            var file = Path.Combine(Path.GetTempPath(), "TestApp-" + Guid.NewGuid());
            Logger.DebugFormat("Loading certificate from {0}", file);
            try
            {
                File.WriteAllBytes(file, ReadStream(stream));
                var cert = new X509Certificate2(file, "MyReallyStrongPassword", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.Exportable);

                Logger.DebugFormat("Certificate Has Private Key: {0}", cert.HasPrivateKey);
                Logger.DebugFormat("HasCngKey: {0}", cert.HasCngKey());
                Logger.DebugFormat("Is Private Access Allowed: {0}", IsPrivateAccessAllowed(cert));

                return cert;
            }
            finally
            {
                File.Delete(file);
            }
        }
    }

    private static byte[] ReadStream(Stream input)
    {
        var buffer = new byte[16 * 1024];
        using (var ms = new MemoryStream())
        {
            int read;
            while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
            {
                ms.Write(buffer, 0, read);
            }
            return ms.ToArray();
        }
    }

    public static bool IsPrivateAccessAllowed(X509Certificate2 cert)
    {
        try
        {
            if (cert.HasCngKey())
            {
                var privateKey = cert.GetCngPrivateKey();
            }
            else
            {
                var privateKey = cert.PrivateKey;
            }
            return true;
        }
        catch (CryptographicException ex)
        {
            Logger.ErrorException("Error accessing private key", ex);
            return false;
        }
    }
}

Which results in the following entries in my log file.

2016-05-11 15:56:45.473 +01:00 [Debug] Loading certificate from "C:\Windows\TEMP\TestApp-845cfe9c-a0d3-4bc7-bde1-53727c3c81c9"
2016-05-11 15:56:45.495 +01:00 [Debug] Certificate Has Private Key: True
2016-05-11 15:56:45.495 +01:00 [Debug] HasCngKey: True
2016-05-11 15:56:45.497 +01:00 [Debug] Is Private Access Allowed: True

This is great, but I would prefer to not have to code my way out of this.  I would rather have a certificate in a format that can be readily accessed by my .NET code.  So, let’s convert my certificate private key from CNG to RSA.

Converting your certificate key from CNG to RSA

Steps:

  1. Extract your public key and full certificate chain from your PFX file
  2. Extract the CNG private key
  3. Convert the private key to RSA format
  4. Merge public keys with RSA private key to a new PFX file

After changing your application to use the new PFX you just created, you should find that your issues have been resolved.

Now let’s see how to carry out these steps using OpenSSL (Get OpenSSL for Windows from here)

1. Extract your public key and full certificate chain from your PFX file

OpenSSL pkcs12 -in “yourcertificate.pfx” -nokeys -out “yourcertificate.cer” -passin “pass:myreallystrongpassword”

2. Extract the CNG private key

OpenSSL pkcs12 -in “yourcertificate.pfx” -nocerts –out “yourcertificate.pem” -passin “pass:myreallystrongpassword” -passout “pass:myreallystrongpassword”

3. Convert the private key to RSA format

OpenSSL rsa -inform PEM -in “yourcertificate.pem” -out “yourcertificate.rsa” -passin “pass:myreallystrongpassword” -passout “pass:myreallystrongpassword”

4. Merge public keys with RSA private key to a new PFX file

OpenSSL pkcs12 -export -in “yourcertificate.cer” -inkey “yourcertificate.rsa” -out “yourcertificate-converted.pfx” -passin “pass:myreallystrongpassword” -passout “pass:myreallystrongpassword”

Enabling Logging in Identity Server

Identity Server v3 provides a wealth of information to developers through it’s two logging features.  Development time logging provides really low level details to assist developers diagnose issues during development and testing.  Production-time logging comes in the form of a number of events being raised to assist in system monitoring and fault diagnosis.   The reason there are two levels of logging is that it is possible that sensitive information may be exposed through development logging and this facility should therefor always be disabled when the system is in production.

To enable logging with Identity Server v3,

  1. Configure the logging framework of choice. 

For example, you could configure Serilog via the following statement:

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Trace()
    .CreateLogger();
  1. Configure Identity Server Logging settings

Here is a full list of the logging settings available for Identity Server V3.

var options = new IdentityServerOptions {
                    LoggingOptions = new LoggingOptions()
                    {
                        EnableHttpLogging = true,
                        EnableKatanaLogging = true,
                        EnableWebApiDiagnostics = true,
                        WebApiDiagnosticsIsVerbose = true
                    },
                    EventsOptions = new EventsOptions()
                    {
                        RaiseInformationEvents = true,
                        RaiseErrorEvents = true,
                        RaiseFailureEvents = true,
                        RaiseSuccessEvents = true,
                    }
                };
  1. Enable Diagnostic Logging in your application configuration file

For example

<system.diagnostics>
  <trace autoflush="true" indentsize="4">
    <listeners>
      <add name="myTraceListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="Trace.log" />
      <remove name="Default" />
    </listeners>
  </trace>
</system.diagnostics>

As is also recommended by the developers of Identity Server, I use BareTail to provide me with a great view of my log files.

For more details on Identity Server logging configuration, see the official Identity Server v3 documentation

Now that you have configured logging, build and rerun your application and confirm that a log file is being created.  Then check the log file for error.  You may now wish to return to reading about the certificate error upon which this post is based.