Skip to content
Security & Identity

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

8 min read

Deprecation notice: IdentityServer v3 reached end of life in 2020. The current supported product is Duende IdentityServer. The CNG key provider incompatibility described here is a .NET Framework behaviour that still affects certificate handling in .NET applications today, so this post remains relevant beyond IdentityServer v3.


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

Signing certificate has no 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 you haven't seen this error message but have encountered an issue with IdentityServer signing its tokens. If so, I would highly recommend you stop and enable logging before going any further. Restart your application and check your logs for the error message above. Logging is essential — 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 read about what the error is all about.


Key Takeaways

  • The root cause is a CNG vs CSP key provider mismatch: New-SelfSignedCertificate in PowerShell generates CNG keys, which the .NET Framework's X509Certificate2.PrivateKey property cannot access directly.
  • cert.HasPrivateKey returns true even when the key is inaccessible, making this problem deceptive to diagnose without logging.
  • The CryptographicException: Invalid provider type specified exception is the real signal — enable logging to surface it.
  • The fix is to convert the certificate's private key from CNG to RSA using OpenSSL, producing a new PFX that .NET can consume without any code changes.

Assumptions

This article assumes you have already checked the obvious things. Run through the following list — if the answer is "Yes" for every question, you may proceed.

  • Have you confirmed 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 confirmed that the account your application is running under has permission to access the private key?

If all three are true, it is increasingly likely that you are running into a CNG key issue.

What Is a CNG Key?

Certificates in Windows are stored using cryptographic storage providers. Windows has two providers that are not mutually compatible:

  • Cryptographic Service Providers (CSP) — the legacy provider, compatible with the classic .NET Framework cryptography APIs.
  • Cryptography API: Next Generation (CNG) — introduced in Windows Vista, more secure and flexible, but not compatible with X509Certificate2.PrivateKey in the .NET Framework.

Although CNG has been available for many years, large parts of the .NET Framework still do not support it natively. A possible workaround is to call the CryptoAPI or CNG API directly. If you want a pure .NET solution that understands CNG keys, you can use the CLR Security open-source library — but if you can avoid that dependency, converting the key format is cleaner.

How was my PFX file generated?

In this case, I used the PowerShell cmdlet New-SelfSignedCertificate to generate a self-signed certificate and then exported it to a PFX file via the Certificate Management Console. New-SelfSignedCertificate generates CNG keys by default, which is why this problem arises. See HowTo: Create Self-Signed Certificates with PowerShell for the full certificate generation walkthrough.

How Do You Prove the Issue?

To investigate the "Signing certificate has no private key or private key is not accessible" error, I created the following test stub. It loads a certificate from an embedded resource and attempts to access the private key.

C#
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 produces the following log entries:

text
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

Notice that HasPrivateKey returns True, yet access is denied. Closer inspection of the exception thrown when accessing the private key reveals the following:

System.Security.Cryptography.CryptographicException: Invalid provider type specified. at System.Security.Cryptography.Utils.CreateProvHandle(CspParameters parameters, Boolean randomKeyContainer) at System.Security.Cryptography.RSACryptoServiceProvider.GetKeyPair() at System.Security.Cryptography.X509Certificates.X509Certificate2.get_PrivateKey()

What Causes This Error?

The MSDN post by Alejandro Magencio describes in detail why this exception is thrown. In short, X509Certificate2.PrivateKey in the .NET Framework only understands CSP keys. When the certificate uses a CNG key, it throws Invalid provider type specified.

To access a CNG key in code, you need either the CryptoAPI/CNG API directly or the CLR Security open-source library. Adding the CLR Security library lets you interrogate the key type:

C#
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;
        }
    }
}

This now produces:

text
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

The certificate is confirmed as a CNG key. Rather than adding a CLR Security dependency and special-casing the key type in application code, the cleaner solution is to convert the certificate's private key to RSA format.

Converting Your Certificate Key from CNG to RSA

The conversion process uses OpenSSL for Windows and involves four 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 the public certificate chain with the RSA private key into a new PFX file

Step 1 — Extract the public key and certificate chain:

text
openssl pkcs12 -in "yourcertificate.pfx" -nokeys -out "yourcertificate.cer" -passin "pass:myreallystrongpassword"

Step 2 — Extract the CNG private key:

text
openssl pkcs12 -in "yourcertificate.pfx" -nocerts -out "yourcertificate.pem" -passin "pass:myreallystrongpassword" -passout "pass:myreallystrongpassword"

Step 3 — Convert the private key to RSA format:

text
openssl rsa -inform PEM -in "yourcertificate.pem" -out "yourcertificate.rsa" -passin "pass:myreallystrongpassword" -passout "pass:myreallystrongpassword"

Step 4 — Merge the certificate chain with the RSA private key:

text
openssl pkcs12 -export -in "yourcertificate.cer" -inkey "yourcertificate.rsa" -out "yourcertificate-converted.pfx" -passin "pass:myreallystrongpassword" -passout "pass:myreallystrongpassword"

Update your application to reference yourcertificate-converted.pfx and the error will be resolved.

Enabling Logging in Identity Server

Identity Server v3 provides detailed logging through two mechanisms. Development-time logging exposes low-level details useful during development and testing. Production-time logging raises events to assist with system monitoring and fault diagnosis. Sensitive information may be exposed through development logging — it must always be disabled in production.

To enable logging with Identity Server v3, follow these three steps.

Step 1 — Configure your logging framework.

For example, configure Serilog as follows:

C#
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Trace()
    .CreateLogger();

Step 2 — Configure Identity Server logging settings.

C#
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,
    }
};

Step 3 — Enable diagnostic trace logging in your application configuration.

xml
<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>

The BareTail log viewer is excellent for tailing the log file in real time.

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

Once logging is configured, rebuild and restart your application, confirm a log file is being written, and check it for the certificate error described at the top of this post.

David Christiansen
David Christiansen

Solution Architect with 30 years in cloud infrastructure, security, identity, and .NET engineering.

Related Posts