Reputation: 14561
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:
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
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
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
.
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
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