Skip to content

Deleting certificates from Windows Certificate Store programmatically (PowerShell and C#)

Yesterday I went through one thread on Reddit: New to PS and want to create a script to clear all personal certificates from a local machine and something was suspicious to me. Then I went further and asked google for similar question and examined first page:

These searches were for PowerShell. Um? Let’s look at C# results:

And they walk around same code fragment. If you look closely to all answers, they provide same solution: raw Remove-Item cmdlet in PowerShell and X509Store.Remove(X509Certificate2) in .NET applications.

Fair enough, all these solutions are correct, they do their work, what is wrong with them? Answer: they are not complete. Years ago I wrote a blog post about the case of accidentally deleted user certificates. Neither of provided solution removes private key associated with certificate. Key pair is still on a boat and is perfectly usable. And if we get a copy of public certificate, we can reconstruct the association between public and private parts of certificate and even export them to PFX. You should follow private key hygiene and take additional actions to remove the private key material from key storage whenever you remove certificate (with associated private key).

PowerShell solution

If you are using PowerShell, then take a look at dynamic parameter called –DeleteKey for Remove-Item cmdlet: Deleting Certificates and Private Keys:

Remove-Item `
  -Path cert:\LocalMachine\My\D2D38EBA60CAA1C12055A2E1C83B15AD450110C2 `
  -DeleteKey

It is a very tiny switch, easy to miss, but extremely valuable when talking about key material removal from store. Essentially, this is a complete solution. There is one pitfall: don’t do this in remote sessions! If key is stored on hardware device (smart card, HSM), a PIN prompt popup may appear and there is no one to enter the PIN or close the dialog in remote session.  Do it only locally.

.NET solution

If you are removing certificates from .NET code, you will have to do a bit more of work and use p/invoke or use 3rd party solutions. For example, a PSPKI supporting library implements an extension method: X509Certificate2Extensions.DeletePrivateKey Method. Reference the SysadminsLV.PKI.dll in your project and add SysadminsLV.PKI.Utils.CLRExtensions namespace in usings.

If you don’t like 3rd party solutions, you have to  go hard way: p/invoke. Many programmers refuse p/invoke because of various reasons, but it is not that bad since about a half of .NET Framework uses p/invoke. Even .NET Core.

If you are using .NET Core, this solution will work only on Windows platform

If your key is stored in legacy CSP, call CryptAcquireContext function and pass CRYPT_DELETEKEYSET flag in dwFlags parameter.

If your key is stored in CNG Key Sotrage Provider, call NCryptDeleteKey function.

Bear in mind, that when calling CryptAcquireContext, you must specify NCRYPT_MACHINE_KEY_FLAG flag if private key is stored in local machine store (opposite to current user store). Best way is to create an extension method that will handle all this. Here is sample code:

using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Win32.SafeHandles;

namespace CertTools {
    public static class CertExtensions {
        // some CryptoAPI constants
        const UInt32 NCRYPT_MACHINE_KEY_FLAG = 0x00000020;
        const UInt32 CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG = 0x00010000;
        const UInt32 CRYPT_DELETEKEYSET = 0x00000010;

        // some CryptoAPI function
        [DllImport("ncrypt.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern Int32 NCryptDeleteKey(
            [In] SafeNCryptKeyHandle hKey,
            [In] UInt32 dwFlags
        );
        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern Boolean CryptAcquireContext(
           ref IntPtr phProv,
           String pszContainer,
           String pszProvider,
           UInt32 dwProvType,
           Int64 dwFlags
        );
        [DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern Boolean CryptAcquireCertificatePrivateKey(
            [In]            IntPtr pCert,
            [In]            UInt32 dwFlags,
            [In, Optional]  IntPtr pvReserved,
            [Out] out SafeNCryptKeyHandle phCryptProv,
            [Out] out UInt32 pdwKeySpec,
            [Out] out Boolean pfCallerFreeProv
        );

        public static Boolean DeletePrivateKey(this X509Certificate2 cert) {
            // acquire private key handle either, from CSP or KSP
            if (!CryptAcquireCertificatePrivateKey(
                cert.Handle,
                CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG,
                IntPtr.Zero,
                out SafeNCryptKeyHandle phCryptProvOrNCryptKey,
                out UInt32 pdwKeySpec,
                out Boolean _)) { return false; }
            // if pdwKeySpec return 0xffffffff, the key is stored in KSP, otherwise, it is legacy CSP. Call appropriate method
            // depending on key storage
            return pdwKeySpec == UInt32.MaxValue
                ? deleteCngKey(phCryptProvOrNCryptKey)
                : deleteLegacyKey(cert.PrivateKey);
        }
        // this method removes key from legacy CSP
        static Boolean deleteLegacyKey(AsymmetricAlgorithm privateKey) {
            if (privateKey == null) { return false; }
            String keyContainer;
            String provName;
            UInt32 provType;
            // depending on key algorithm (RSA/DSA) acquire key container information to use for CryptAcquireContext call
            switch (privateKey) {
                case RSACryptoServiceProvider _:
                    keyContainer = ((RSACryptoServiceProvider)privateKey).CspKeyContainerInfo.KeyContainerName;
                    provName = ((RSACryptoServiceProvider)privateKey).CspKeyContainerInfo.ProviderName;
                    provType = (UInt32)((RSACryptoServiceProvider)privateKey).CspKeyContainerInfo.ProviderType;
                    break;
                case DSACryptoServiceProvider _:
                    keyContainer = ((DSACryptoServiceProvider)privateKey).CspKeyContainerInfo.KeyContainerName;
                    provName = ((DSACryptoServiceProvider)privateKey).CspKeyContainerInfo.ProviderName;
                    provType = (UInt32)((DSACryptoServiceProvider)privateKey).CspKeyContainerInfo.ProviderType;
                    break;
                default:
                    privateKey.Dispose();
                    return false;
            }
            // we can't tell if key is stored in machine context or current user context. 
            IntPtr phProv = IntPtr.Zero;
            Boolean status1, status2 = false;
            // try to delete from local machine
            status1 = CryptAcquireContext(
                ref phProv,
                keyContainer,
                provName,
                provType,
                CRYPT_DELETEKEYSET | NCRYPT_MACHINE_KEY_FLAG);
            // if method call against local machine fails, attempt to call against current user store.
            if (!status1) {
                status2 = CryptAcquireContext(
                    ref phProv,
                    keyContainer,
                    provName,
                    provType,
                    CRYPT_DELETEKEYSET);
            }
            privateKey.Dispose();
            // if any method succeeded, then key was found and successfully deleted. If both failed, then it may indicate that
            // private key was not found and there is nothing to delete. Either way, true or false are legitimate results. Don't
            // throw exceptions.
            return status1 || status2;
        }
        static Boolean deleteCngKey(SafeNCryptKeyHandle phKey) {
            // with CNG keys everything is simple, just call NCryptDeleteKey
            Int32 hresult = NCryptDeleteKey(phKey, 0);
            phKey.Dispose();
            return hresult == 0;
        }
    }
}

I added comments that explain the logic of the code. Refer to Microsoft Docs for unmanaged function description. The code is exception free.

Leave a Comment





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

Scroll To Top