Mike Caron
Mike Caron

Reputation: 14561

Why is Powershell deleting my private key after I import it?

I have a Powershell script that imports a PFX certificate into the Machine store. This script is intended to be invoked by chef. When I run it manually, it works great. However, when chef runs it, it seems to only complete half its operation.

echo "Importing as PFX" >> $log

$rule = new-object security.accesscontrol.filesystemaccessrule ".\IIS_IUSRS", "Read", allow

$pfx = Get-PfxCertificate -FilePath $file
$certThumbprint = $pfx.Thumbprint

echo "Thumbprint is $certThumbprint" >> $log

"$password" | certutil -ImportPFX "$certStoreLocation" "$file" >> $log

$keyPath = "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\" + (((Get-ChildItem -Path cert:\LocalMachine\My | Where-Object { $_.thumbprint -eq $certThumbprint }).PrivateKey).CspKeyContainerInfo).UniqueKeyContainerName 

echo "Updating private key permissions: $keyPath" >> $log

$acl = get-acl -path $keyPath

$acl.AddAccessRule($rule)

# Allow IIS to read this private key
set-acl $keyPath $acl

In my log, I see successful output:

Examining certificate for import: c:\www\certificates\xyz.pfx
Importing as PFX
Thumbprint is 206E30F2694C8627C18EAE97729C643A3CC1317E
Enter PFX password: 
Certificate "xyz" added to store.

CertUtil: -importPFX command completed successfully.
Updating private key permissions: C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\aee87c90ef6606b733130f970131e498_051677f6-7959-49c2-afb8-d2fad091409a

And, in MMC, I can see my certificate. However, the path listed in the log is missing, and when I try and examine the private key, it says it's missing. Again, when I run this as myself, it imports fine.

Thinking that I'm going insane, I used Process Monitor to see what's going on:

Process Monitor output showing that the file is marked for deletion

I don't know why, but it seems that Powershell marks the private key for deletion when it closes the file. What?? Why???

Notes:

Upvotes: 5

Views: 806

Answers (3)

Filip Cornelissen
Filip Cornelissen

Reputation: 3742

For all future .NET users who have disappearing private keys, this is @decoyjoe his answer in c#:

var certFileName = "c:/somedir/cert.pfx";
using (var tempCert = new X509Certificate2(certFileName, "pw", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable))
using (var realCert = new X509Certificate2(certFileName, "pw", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable))
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
    store.Open(OpenFlags.ReadWrite);
    store.Add(realCert);
}

Upvotes: 0

decoyjoe
decoyjoe

Reputation: 41

I ran into this issue myself and discovered that this is due to a bug with how Windows manages certificate private keys when imported as the "NT AUTHORITY\SYSTEM" account, which is the account that Chef runs as on the system.

From a bug report I submitted I learned that for the NT AUTHORITY\SYSTEM account, the user keystore and the machine keystore are the same directory, C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys.

In your script, Get-PfxCertificate first loads your PFX. Looking at the source for Get-PfxCertificate we see that it loads the certificate with just the "DefaultKeySet" flag and does not include the PersistKeySet flag. Meaning when Get-PfxCertificate loads the PFX, the key is stored in the user keystore, temporarily. This results in the key file being created under C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys (which remember is the user keystore for the "SYSTEM" account) but only temporarily, meaning the key file will be removed from disk when the certificate object is disposed or when the process closes.

Then the PFX is loaded with certutil which imports the private key "persisted" (i.e. permanently) to the machine keystore, which happens to be the same path C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys as the "SYSTEM" user keystore.

This is where the Windows bug appears. When the PFX is imported by certutil, Windows should be creating a new distinct key file under C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys since the derived key file name has already been taken by the Get-PfxCertificate load. But since that was a user keystore load and this is a machine keystore load, Windows doesn't see the key name as already being in-use. This results in Windows giving certutil -importPFX the same key file on disk as the Get-PfxCertificate load.

Now we see the problem.

PowerShell exits and Windows deletes the private key file from disk that was created when the PFX was loaded with Get-PfxCertificate, which is what we would expect it to do (this is what you're seeing in the Process Monitor output). But since certutil -importPFX was given the same key file on disk, Windows has inadvertently deleted the "persisted" private key file from disk. Your certificate's private key is now missing, i.e. Keyset does not exist.

Workaround

Import the PFX in a way that avoids the duplicate key name entirely by loading the PFX twice into the same keystore context in the same process. This plays off the fact that the key file name on disk is derived from the name given to the key when the PFX is loaded.

First, load the PFX into the machine keystore without the PersistKeySet flag. This will load the key with the preferred name from the PFX.

Now load the PFX a second time, again into the machine keystore but now with the PersistKeySet key storage flag. Since we previously already loaded the same PFX into the same machine keystore, Windows will properly detect that the preferred key name from the PFX is already taken and will generate a new key name. This new key name will be used to derive the key file name on disk. This results in a key file name on disk that is completely unique from all other loads of the same PFX.

Now when the process closes and Windows deletes the key file created from the first PFX load, it properly deletes only the key file associated with the initial non-persisted load.

Here is some PowerShell code you can use that implements that workaround:


$pfxPath = 'PATH_TO_CERT.pfx'
$pfxPasswordFilePath = 'PATH_TO_PFX_PASSWORD_FILE'

$pfxPassword = ConvertTo-SecureString -String (Get-Content -Path $pfxPasswordFilePath -Raw) -AsPlainText -Force

# Initial load of the PFX to ensure the preferred key name is detected as already taken.
$tempCertLoad = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
    $pfxPath,
    $pfxPassword,
    [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet
)

# Now do the real PFX import with a persisted private key.
$certToImport = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
    $pfxPath,
    $pfxPassword,
    [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor
        [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
)

$privateKey = [Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certToImport)
$keyFilename = $privateKey.Key.UniqueName

$store = [Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
$store.Open('ReadWrite')
$store.Add($certToImport)
$store.Close()
$store.Dispose()

# Dispose both certificate objects
$tempCertLoad.Dispose()
$certToImport.Dispose()

# Now confirm the persisted private key still exists on disk
$privateKeyFilePath = Join-Path -Path $env:ProgramData -ChildPath "Microsoft/Crypto/RSA/MachineKeys/${keyFilename}"
$keyExists = Test-Path -Path $privateKeyFilePath
Write-Information "Private key file path: ${privateKeyFilePath}" -InformationAction Continue
Write-Information "               Exists: ${keyExists}" -InformationAction Continue

Upvotes: 4

Jordan Craig
Jordan Craig

Reputation: 126

Is there a reason for not using the existing Chef resources for managing certificates in Windows?

https://github.com/chef-cookbooks/windows#windows_certificate

Your rceipe would then simply be a case of:

windows_certificate 'c:\www\certificates\xyz.pfx' do
   pfx_password    "my_password"
   private_key_acl [".\IIS_USRS"]
   store_name      "MY"
end

Upvotes: -1

Related Questions