PowerShell MVP, Adam Bertram with his recent tweet inspired me to write this blog post. I realized that I see a lot of articles which describe how to delete certificate from certificate store, but never saw article that would describe how to do it properly.
Let’s recall what is wrong here. Years ago I wrote a blog post: The case of accidentally deleted user certificates that describes steps how to restore deleted certificate with private key by having a backup certificate only with public key. I explained why you can use certificate with just public key to restore bindings to private key. The answer was: when you delete certificate by using standard means (certificate store management functions in CryptoAPI), the private key is not deleted! Standard tools includes: Certificates MMC snap-in, X509Store class in .NET, certutil -delstore, etc., all they use Certificate and Certificate Store Functions. Our goal now is to fill the gap and provide an ability to remove private key along with certificate when you work in PowerShell.
The solution consist of some work with unmanaged CryptoAPI functions.
Main function is: CryptAcquireContext. This function accepts four parameters:
Where we can get values for first three parameters? For certificates that use legacy CSP to store private keys, this information can be retrieved directly from X509Certificate2 object:
[↓] [vPodans] (dir cert:\CurrentUser\My)[0].privatekey.cspkeycontainerinfo MachineKeyStore : False ProviderName : Microsoft Enhanced Cryptographic Provider v1.0 ProviderType : 1 KeyContainerName : efc3deed-fc95-44a6-8e04-ac480ab0fed3 UniqueKeyContainerName : 1d5c79da24200a19f42fa77a50cb5aa3_3ac92f0d-f753-422d-9fa3-f769bd32b511 KeyNumber : Exchange Exportable : True HardwareDevice : False Removable : False Accessible : True Protected : False CryptoKeySecurity : RandomlyGenerated : False [↓] [vPodans]
I selected values in the output that will fill the function parameters. So far, so good.
However, we have to support CNG (that uses modern CAPI2 key storage providers instead of legacy CSP) and quickly run into the problem:
PS C:\> (dir Cert:\LocalMachine\My)[0] | fl *priv* HasPrivateKey : True PrivateKey : PS C:\>
The HasPrivateKey property is set to True, but PrivateKey property is NULL, as the result, we cannot access private key object in .NET. In fact, it is a known limitation of .NET which very poorly support CNG. Fortunately, there is a way to get this information. Some time ago, I posted a great article about accessing CNG private key information: Retrieve CNG key container name and unique name. We can reuse that article, by calling NCryptOpenStorageProvider and NCryptOpenKey and then call NCryptDeleteKey function. However, there is a shortcut: we can call CryptAcquireCertificatePrivateKey function with CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG value in the dwFlags parameter and pass resulted key handle (in the phCryptProvOrNCryptKey parameter) to NCryptDeleteKey function.
Now we are ready to write the final code. The code will be implemented as a helper function which you can embed in your certificate deletion scripts.
function Remove-PrivateKey { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Security.Cryptography.X509Certificates.X509Certificate2[]]$Certificate ) begin { # define unmanaged functions to retrieve private key information and delete the key. $signature = @" [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern bool CryptAcquireContext( ref IntPtr phProv, string pszContainer, string pszProvider, uint dwProvType, long dwFlags ); [DllImport("Crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CryptAcquireCertificatePrivateKey( IntPtr pCert, uint dwFlags, IntPtr pvReserved, ref IntPtr phCryptProv, ref uint pdwKeySpec, ref bool pfCallerFreeProv ); [DllImport("NCrypt.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern int NCryptDeleteKey( IntPtr hKey, uint dwFlags ); [DllImport("Crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CertGetCertificateContextProperty( IntPtr pCertContext, uint dwPropId, IntPtr pvData, ref uint pcbData ); [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] public struct CRYPT_KEY_PROV_INFO { [MarshalAs(UnmanagedType.LPWStr)] public string pwszContainerName; [MarshalAs(UnmanagedType.LPWStr)] public string pwszProvName; public uint dwProvType; public uint dwFlags; public uint cProvParam; public IntPtr rgProvParam; public uint dwKeySpec; } "@ Add-Type -MemberDefinition $signature -Namespace PKI -Name PfxTools $CERT_KEY_PROV_INFO_PROP_ID = 0x2 $CRYPT_DELETEKEYSET = 0x10 $CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG = 0x40000 } process { foreach ($cert in $Certificate) { # if the current certificate do not have private key -- skip if (!$cert.HasPrivateKey) {continue} # if we reach this far and PrivateKey is $null -- the key is CNG key. if ($cert.PrivateKey -eq $null) { $phCryptProv = [IntPtr]::Zero $dwFlags = $CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG $pdwKeySpec = 0 $pfCallerFreeProv = $false if (![PKI.PfxTools]::CryptAcquireCertificatePrivateKey( $cert.Handle, $dwFlags, 0, [ref]$phCryptProv, [ref]$pdwKeySpec, [ref]$pfCallerFreeProv)) {continue} $hresult = [PKI.PfxTools]::NCryptDeleteKey($phCryptProv,0) if ($hresult -ne 0) { $message = "Cert '{0}' failed: {1}" -f $cert.Thumbprint, (New-Object ComponentModel.Win32Exception $hresult).Message Write-Warning $message } } else { # if the key is legacy, then just read the PrivateKey object of the X509Certificate2 object $phProv = [IntPtr]::Zero if (![PKI.PfxTools]::CryptAcquireContext( [ref]$phProv, $cert.PrivateKey.CspKeyContainerInfo.KeyContainerName, $cert.PrivateKey.CspKeyContainerInfo.ProviderName, $cert.PrivateKey.CspKeyContainerInfo.ProviderType, $CRYPT_DELETEKEYSET)) { $hresult = [Runtime.InteropServices.Marshal]::GetLastWin32Error() $message = "Cert '{0}' failed: {1}" -f $cert.Thumbprint, (New-Object ComponentModel.Win32Exception $hresult).Message Write-Warning $message } } } } }
The usage is pretty simple: just pass a certificate object, and the function will delete private key when necessary. The function performs all required checks to properly handle various scenarios. Few considerations should be taken into account prior to using this function:
HTH
The function accepts an array of certificates and should be able to delete all their keys. But then in the line
if (!$cert.HasPrivateKey) {return}
shouldn't it be "continue" instead of "return"? Otherwise it stops after first certificate which doesn't have private key.
Also, the statement that certutil doesn't delete private key is not completely true. If you delete a certificate using "certutil.exe -delstore" command then yes, private key remains in the system. But there is also "certutil.exe -delkey" which deletes private key according to passed container name. Note that this command is a kind of hidden - it is not show in "certutil.exe /?".
Thanks, fixed the code.
The usage is pretty simple: just pass a
And what is accepted as certificate object? has, serial, subject?
None seems to work
> And what is accepted as certificate object? has, serial, subject?
pass entire certificate object. Not serial number or thumbprint. Roughly, an instance of "X509Certificate2" class. The object that is returned when you call Get-ChildItem against certificate store.
This function is unessecary.
Remove-Item with Optione -Deletekey does the trick
> Remove-Item with Optione -Deletekey does the trick
agree. But this switch doesn't exist on previous PowerShell versions. For compatibility reasons I have to support the lowest PS version whihc is in use.
Post your comment:
Comments: