Schedule a Demo
Blog October 2, 2019 Certificates, Cryptographic Keys, PowerShell

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

by Vadims Podāns

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

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.

Related Resources

  • Blog
    March 7, 2024

    Why you are getting it wrong with Certificate Lifecycle Management

    Certificate Management, Certificates, CLM
  • Blog
    March 7, 2024

    PKI Insights – Avoiding PenTest Pitfalls

    Certificates, PKI, PKI Insights
  • Blog
    February 6, 2024

    PKI Insights Recap – Microsoft Intune Cloud PKI

    BYOD, Certificates, Cloud, Enrollment, NDES

Vadims Podāns

PKI Software Architect

View All Posts by Vadims Podāns

Comments

  • Hi,
    Thanks for this article
    do you have any suggestion how to revoked multiple user certificate based on user name and certificate tmplate?

    please suggest if you have any power shell script

    • Sorry, your question isn’t related to blog post’s topic. You may want to address your question to scripting or security forums.

  • I am pretty new to PS and working to learn. Right now we need to try and clear the personal certs found in internet options for all users remotely to see if that clears a problem we are having. Any suggestions since so far everything I have seen and tried doesn’t work for that. These are kiosk systems and we think all the certs in all the profiles may be the cause of the issues locking their CACs.

    • Unfortunately, I’m not providing scripting support in comments. You may need to ask your question on PowerShell-oriented forums.

  • Do you have a script to remove certificates from a certificate authority? We had a script glitch that generated thousands of certs and not there is alot of bloat and I want to remove them and not just revoke them, I would like to do it with a script and thumbprint.

    • If you are using ADCS you can use certutil.exe

      CertUtil [Options] -deleterow RowId | Date [Request | Cert | Ext | Attrib | CRL]

Leave a Reply

Your email address will not be published. Required fields are marked *