Accessing and using certificate private keys in .NET Framework/.NET Core

PKI Solutions Logo

This blog post is about programming and its purpose is to have a link to direct developers for explanation. Inspired from this list:

A bit of history

Disclaimer: TL;DR.

Microsoft has a long history of their proprietary cryptography subsystem called CryptoAPI. It was first invented in Windows NT 4.0 in the way it is  widely used these days. *nix-based platforms use standard key storage format defined in a number of RFCs, often in an unencrypted format, PKCS#8 or PKCS#1 (which is a subset of PKCS#8). Operations with keys (encryption, signing, etc.) were implemented in separate libraries such as OpenSSL. Any library that implements PKCS#1/8 could work with such keys, thus providing a way to use custom cryptographic libraries on a single platform.

Microsoft invented and implemented their own philosophy around their cryptography subsystem with Cryptographic Service Provider (CSP) concept and proprietary key format. CSP is simply a box with named encrypted keys inside. Each CSP is responsible for key stored inside and provides an abstraction layer between client (key consumer) and certificate keys. CSP stores keys in an encrypted form, thus access to private key raw file doesn’t give you anything useful. This is how Microsoft provides a kind of key security. Instead of raw access to key material (that prevents from key leak in some degree), you use standard CryptoAPI calls and ask particular CSP to use named key to perform cryptographic operation (encryption, signing, whatever else). In some cases, you can export key material in standard format, such as PKCS#12, sometimes not. This behavior is governed by key export policy and this is another story. In theory, this concept was intended to protect keys from leaks, make developer’s life easier by abstracting cryptographic operations via a set of CryptoAPI functions (most of them are defined in crypt32.dll library) to operate with keys.

After years, with the release of Windows Vista and Windows Server 2008, Microsoft rebuilt entire cryptography stack from scratch and improved it all around, by:

  • providing key isolation
  • moving cryptography operations to kernel memory (in legacy CSP cryptographic operations were executed in user memory for software keys)
  • added built-in support and implementation for NSA Suite B algorithms (SHA2 hashing family, ECC-based asymmetric keys, AES)
  • added an ability to use custom algorithm implementations (such as GOST)
  • unified abstract functions set
  • much more

This new stack was named a Cryptography Next Generation or CNG, or CAPI2 and backed by ncrypt.dll library. Providers within this stack got new name to distinguish from legacy CSPs – Key Storage Provider or KSP. CSP –> legacy crypto, KSP –> modern crypto. Plain and simple.

Built-in Windows components and services got native support for CNG: ADCS, ADDS, EFS, IIS, RDS, Internet Explorer, etc. Almost all what was shipped with Windows OS and what wasn’t based on .NET was compatible with CNG in 2006. A limited number of external products got support for CNG. Most popular was Microsoft Office 2007 which natively supports CNG when installed on Windows Vista and newer OSes. But most external products that were either, .NET-based or had .NET interface were not compatible with CNG, because .NET didn’t support CNG at that time. And its support came many and many years after CNG become native in Windows. M(B)illions of developers and IT administrators crushed their heads while battling with keys in attempt to get the right one for their application.

It was .NET 4.6 when .NET-based life become different. Cryptography stack in .NET can be divided to two eras: before 4.6 and after.

Dark Ages (before .NET 4.6)

Before .NET Framework version 4.6, cryptography support in .NET was Windows-only and sticks to legacy CryptoAPI library calls. Easiest (and, possibly, the only) way to access the certificate’s private key was:

public class Class1 {
     public Class1() {
         var cert = new X509Certificate2(...);
         var privateKey = (RSACryptoServiceProvider)cert.PrivateKey;
         privateKey.Decrypt(...);
         // or
         privateKey.SignData(...);
     }
}

An X509Certificate2 class has a PrivateKey property of AsymmetricAlgorithm type. AsymmetricAlgorithm class is abstract class for any asymmetric algorithm and defines only few relevant methods. Access to actual algorithm implementations is done via explicit algorithm groups: RSA, DSA, ECDsa, ECDiffieHellman. These classes had only one platform-specific implementation. In case of RSA it was RSACryptoServiceProvider. So, no doubt, previous example worked in 99.99%. In very rare cases you could get InvalidCastException: in 0.009% it was ECC key and in 0.001% it was DSA key.

Note: this example throws exception, when you access PrivateKey property and private key is stored in KSP. This means that KSP keys explicitly not supported in X509Certificate2 ecosystem before .NET 4.6.

Bright Ages (after .NET 4.6)

.NET team was criticized for poor to no CNG support for a long period after CNG release. As the result, major .NET-based Microsoft products still (as of early 2020) don’t support CNG keys. Entire System Center product line, Exchange Server, ADFS and many other products still doesn’t support CNG storage, while CNG is about 14 years around us. Cool story. .NET team added a very basic CngKey class in v3.5 to access CNG storages and keys. Not so much and this class wasn’t integrated with X509Certificate2 class in any way.

Only with the release of v4.6, things become real and CNG support was greatly improved by adding CNG implementations for asymmetric key algorithms: RSACng, DSACng, ECDsaCng, ECDiffieHellmanCng. Using these classes, you can fully access CNG keys in your .NET applications. And here is a little puzzle: .NET has two implementations for RSA keys: legacy RSACryptoServiceProvider and new RSACng. You can’t know at runtime where the key is stored: in CSP or KSP and depending on key storage, a cast to different types is required. And this cast must be done at compile time, because RSA abstract class didn’t have methods to perform cryptographic operations. Kudos to .NET team since they found a quite elegant way to solve this puzzle: They added cryptographic methods to abstract base classes and now you can use them without knowing exact implementation: legacy CSP or modern KSP.

Compare them:

Instead or fixing X509Certificate.PrivateKey property, .NET team added extension methods to X509Certificate2 class:

Since 4.6, this is the recommended way to access public and private keys. What you need to know in advance – which method to call: RSA, DSA, ECDsa? You can’t know this at compile time, but you can check it at runtime by checking the algorithm OID in public key: X509Certificate2.PublicKey has Oid that identifies the asymmetric key algorithm. Drop this property to switch statement and call appropriate extension methods to get the right key.

While first example still works in 4.6, direct access to X509Certificate2.PrivateKey and X509Certificate2.PublicKey.Key properties is now discouraged. You shall access keys only via mentioned extension methods. No discussions, no exceptions. Otherwise, you are a candidate for new thread on StackOverflow as I referenced in the beginning of this post. Don’t believe? Check next section!

These changes not only solved the support of CNG in X509Certificate2 class, but helped to move this class to .NET Core and other platforms with very different cryptography subsystems and add platform-specific implementations for asymmetric algorithms. So far, so good.

Weird Ages (.NET 4.7)

In 4.7 (and all versions of .NET Core on Windows), .NET team made things even worse, by changing the default type returned by X509Certificate2.PrivateKey from RSACryptoServiceProvider to RSACng. I already mentioned that this access is strictly discouraged starting with v4.6. Applications now crash at a runtime when you attempt to use example I posted in “Dark Ages” section. That example uses compile-time explicit cast to RSACryptoServiceProvider, but SURPRISE, the type is changed to RSACng in 4.7! Check and mate.

Summary

The whole point of this post was to explain why:

public class Class1 {
     public Class1() {
         var cert = new X509Certificate2(...);
         var privateKey = (RSACryptoServiceProvider)cert.PrivateKey;
         // use key
     } }

is bad! And:

public class Class1 {
     public Class1() {
         var cert = new X509Certificate2(...);
         RSA privateKey = cert.GetRSAPrivateKey();
         // use the key
     } }

or

public class Class1 {
     public Class1() {
         const String RSA = "1.2.840.113549.1.1.1";
         const String DSA = "1.2.840.10040.4.1";
         const String ECC = "1.2.840.10045.2.1";
         var cert = new X509Certificate2(...);
         switch (cert.PublicKey.Oid.Value) {
             case RSA:
                 RSA rsa = cert.GetRSAPrivateKey(); // or cert.GetRSAPublicKey() when need public key
                 // use the key
                 break;
             case DSA:
                 DSA dsa = cert.GetDSAPrivateKey(); // or cert.GetDSAPublicKey() when need public key
                 // use the key
                 break;
             case ECC:
                 ECDsa ecc = cert.GetECDsaPrivateKey(); // or cert.GetECDsaPublicKey() when need public key
                 // use the key
                 break;
         }
     } }

is the right way to access the private key in .NET Framework 4.6+ and .NET Core (all versions). Happy programming with .NET and X509Certificate2!

About Vadims Podāns

Senior PKI Developer

19 Comments

  1. Elena on August 14, 2020 at 11:41 am

    Good day.

    Is it possible make request like this “curl –cert test.crt –key test.key” on .net framework 4.7?

    • Vadims Podāns on August 15, 2020 at 12:52 am

      Sorry, I don’t know what that command does.

  2. Abhinav on August 31, 2020 at 1:01 am

    Any idea if GetRSAPrivateKey throws Invalid provider type specified with .Net framework 4.6.1 and of course the oid value is of RSA .

    • Vadims Podāns on August 31, 2020 at 1:12 am

      Do the certificate contain the private key? What “certutil -store my ” says?

      • Abhinav on August 31, 2020 at 1:32 am

        Got he issue from another blog of yours. Certificate private key is deleted. Used crtutil to find the diff between the 2 RSACng keys. Thanks.

        • Vadims Podāns on August 31, 2020 at 1:34 am

          Good you were able to narrow the issue root.

  3. stmu on December 1, 2020 at 2:17 am

    Hi,

    how to set the wrigth cardReader (eg. “Microsoft Virtual Smart Card 0”) if there are more than one card reader in system.

    If i call “var privateKey = (RSACryptoServiceProvider)cert.PrivateKey;” than the first Card Reader in System is used (Private key of certificate was imported into “Microsoft Base Smart Card Crypto Provider” wit certutil -importPFX

    during access to the private key a pin is required

    • Vadims Podāns on December 1, 2020 at 8:15 am

      I don’t think it is possible to specify reader name when acquiring private key handle.

  4. David Mohr on March 12, 2021 at 10:01 am

    What is the correct way to use CNG keys/containers to access the private key if it is only available on a smart card? I want to try to sign some hashed data using a smart card private key using CNG code with C# if possible. I already have the pin from user input to a textbox on my form, so I don’t want the pin dialog to appear. I have this working with some restrictions (one smartcard only – with two it only works on the first one found) using CSP code but want to change to the newer format.

    • Vadims Podāns on March 12, 2021 at 10:18 am

      You need to use CngKey.SetProperty API to set PIN for your key.

      1. Acquire private key handle for certificate (must be RSACng)
      2. Acquire CngKey from RSACng
      3. Call CngKey.SetProperty and specify “SmartCardPin” as property name.

      • Dave on March 15, 2021 at 1:28 pm

        Thanks. I will try and get that working. I am trying: var pvtHandle = cert2.GetRSAPrivateKey(); Which gets me seemingly valid data. (I can see it in the debugger) But I am not sure yet how to extract the handle correctly. I appreciate the help.

  5. Rama on March 23, 2021 at 9:55 pm

    Is there any way we can export the private key obtained using the above methods to a file in base64 format ?

    • Vadims Podāns on March 23, 2021 at 11:29 pm

      it depends on whether private key is exportable or not. But this question is outside the scope of the post. I would recommend to ask the question on professional forums such as Stack Overflow.

  6. Pdesai on June 16, 2021 at 6:25 pm

    what is the best alternative if we want to allow few users to allow read permission to private keys. In the past we used the CspKeyContainerInfo that was available inside the RSACryptoServiceProvider

  7. Peter M on September 23, 2021 at 6:48 am

    Thanks for the informative article. It explains well how to export all the private keys of existing certificates and there encryption methods.

    I am currently trying to use [System.Security.Cryptography.X509Certificates.X509Certificate2] and CNG to create a cert on Powershell 5.1 / .net 4.8 so it signs the private key in RSA. It always uses the old CAPI method. I was wondering if you knew a way of forcing it .net 4.8 to use CNG?

    Problem is described in 2 links below:
    https://stackoverflow.com/questions/69196724/difference-between-powershell-5-1-and-powershell-7-certificate-encyption-using-s?noredirect=1#comment122330957_69196724

    https://github.com/PowerShell/PowerShell/issues/10833

Leave a Comment





This site uses Akismet to reduce spam. Learn how your comment data is processed.