Strange outcome from running Importpfx.exe to import certificates - powershell

I am writing a Powershell script to automate the setting up of a Windows 2008 R2 server and one thing that is required is the importing of several certificates into different stores. After doing some research on how best to achieve this, I found that Importpfx.exe was the best choice for what I am aiming to do, which is import one .pfx file into the Trusted People store and another .pfx file into the Personal store, both for the Computer account. I then also need to Manage Private keys on the certificate imported into the Personal store once it has been imported.
At first, I thought that Importpfx.exe was doing this correctly, but after researching on how to manage the private keys via Powershell, I learned that this can be done my editing the acl for the file that corresponds to the imported certificate which should be found here "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys". This is where I started to notice that something wasn't quite right with the imported certificate. After searching this folder for a new file after importing the certificates, I noticed that no new files had been added to this folder.
I searched the entire C drive for all files sorted by date modified and found that new files had been added to this folder "C:\Users\'user'\AppData\Roaming\Microsoft\Crypto\RSA\S-1-5-21-2545654756-3424728124-1046164030-4917" instead of the expected folder. Whilst I was able to manually manage private keys for the certificate via the certificate store (as I was user who imported it), no other users were able to log onto the machine and manage the private keys, getting the error message "Cannot find the certificate and private key for decryption" (which would make sense given the folder that the corresponding file exists in).
I use a function to get the thumbprint of the certificates before trying to import the .pfx file. The code I have used to run is:
function GetCertificateThumbprint ( [string]$certPreFix, [string]$certPassword, [string]$certFolder, [string]$domain, [bool]$addIfNotFound, [hashtable]$return)
$storePath = "cert:\LocalMachine"
$storeDir = "My"
$storeName = [System.Security.Cryptography.X509Certificates.StoreName]::My
if($certPreFix -eq "XXX")
{
$storeDir = "TrustedPeople"
$storeName = [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople
}
$storePath = [System.IO.Path]::Combine($storePath, $storeDir)
#Build the certificate file name and get the file
$certFileName = $certPreFix + "." + $domainName + ".*"
$certFile = Get-ChildItem -Path $certFolder -Include $certFileName -Recurse
if ($certFile)
{
# The certificate file exists so get the thumbprint
$Certificate = New-Object system.Security.Cryptography.X509Certificates.X509Certificate2($certFile, $certPassword)
$certThumbprint = $Certificate.Thumbprint
if($addIfNotFound)
{
# Check for the certificate's thumbprint in store and add if it does not exist already
if(-not(Get-ChildItem $storePath | Where-Object {$_.Thumbprint -eq $certThumbprint}))
{
Set-Location "$Env:windir\Tools"
.\importpfx.exe -f $certFile -p $certPassword -t MACHINE -s $storeDir
}
}
}
Can anyone see if I have done anything wrong? Has anyone come across this issue and got around it somehow? This is causing me issues as I cannot automate the Manage Private keys task properly!

I just ran in to the same problem. You must specify the MachineKeySet X509KeyStorageFlag when creating the certificate object:
New-Object system.Security.Cryptography.X509Certificates.X509Certificate2($certFile, $certPassword, "PersistKeySet,MachineKeySet")
Hopefully that helps someone.

Related

Imported pfx certificate is not saved on disk

I wrote a script in PowerShell to import a certificate on Windows Server 2016/2019. The script is added to an Azure DevOps pipeline and the agent is an Environment Agent running as NT AUTHORITY\SYSTEM. It first imports the certificate into the LocalMachine\My store and sets read permissions on the certificate right after that. It comes with quite weird behavior while running as a pipeline or executed manually. I'll write out the different kinds of behavior:
Import certificate on a server (successful)
Check the existence of the file on the physical disk (successful -> found in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys)
Set read permissions for account x (successful)
Import certificate on a server (successful)
Check the existence of the file on the physical disk (failed. File is not stored programdata\machinekeys, but in C:\ProgramData\Microsoft\Crypto\Keys)
Set read permissions for account x (failed --> can't find the file on the proper location)
Import certificate on a server (successful)
Check the existence of the file on the physical disk (successful)
Set read permissions for account x (failed --> can't find the file on the proper location. does not exist on the server )
There is no way to predict which one of the scenario's will occur while running the script.
I monitored server and file behavior with ProcMon (SysInternal Suite) during the import and did see the file being created and saved in while in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys
during both successful and failed attempts.
Import PFX code:
$FilePath = 'e:\folder\certificate.pfx'
$password = 'iLoveChocolateyCookies'
$cert = New-Object system.Security.Cryptography.X509Certificates.X509Certificate2($FilePath, $password, "PersistKeySet,MachineKeySet")
$store = New-object System.Security.Cryptography.X509Certificates.X509Store -argumentlist "My", "LocalMachine"
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::"ReadWrite")
$store.Add($cert)
$store.Close()
Physical disk location check code:
$checkCert = Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.thumbprint -eq '<insertThumbhere>' }
$rsaCertCheck = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($checkCert)
$checkCertDirectory = (Get-ChildItem -Path 'C:\Programdata\Microsoft\Crypto' -Include $rsaCertCheck.Key.UniqueName -File -Recurse).DirectoryName
if ($checkCertDirectory -eq "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys") {
Write-Host "Certificate found on physical disk."
} else {
throw 'nope'
}
Import-PfxCertificate showed the same behaviour. That is why I decided to use the .Net approach after consulting StackOverflow and Google, but ended up with the same issues.
A helpful hand will be very much appreciated :)

If a particular certificate file .crt or .cer etc has been installed and is active

I am having difficulty trying to get if a particular certificate from a certain path on a drive eg "c:\path\MyCert.crt" has been installed and if it is active.
I have the following:
$crt = New-Object System.Security.Cryptography.X509Certificates.X509Certificate;
$crt.Import("c:\path\MyCert.crt");
write-host "MyCert.crt expires after: " $crt.GetExpirationDateString();
$crt.GetName();
Which displays:
MyCert.crt expires after: 11/12/2021 2:12:31 AM
C=..., O="...", OU=..., OU="...", CN=...
Tried:
[bool](dir cert:\LocalMachine\ | ? { $_.subject -like "cn=MyCert" })
but not sure if this tells me if it is installed and active?
Can someone please help.
Like #TessellatingHeckler suggested, you should compare thumbprints, as this will tell you if the specific certificate is installed. Otherwise you could end up getting but a true value even if the certificate has been renewed or something else has happened to change the cert.
The other problem you'll come across is that, by default, Get-ChildItem (alias DIR) won't search down a path recursively, so you'll only see the folders that a Certificate can live inside by running what you've got in your example:
[1] PS C:\> dir cert:\LocalMachine\
Name : TrustedPublisher
Name : ClientAuthIssuer
Name : Remote Desktop
Name : Root
Name : TrustedDevices
Name : CA
...
You'll also notice that importing a crt file as an 'X509Certificate' object won't show you the thumbprint. You have two options here, look at the file using the Get-PfxCertificate (I'm not sure when this was introduced so I'm not sure if you'll have it available), or you can import the crt file as an 'X509Certificate2' object.
Forgive me for expanding out the alias' in your snippet, but the following will do the trick:
$crt = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$crt.Import('c:\certs\CertFile.crt');
[bool](Get-ChildItem cert:\LocalMachine\ -Recurse | Where { $_.Thumbprint -eq $crt.Thumbprint })

Private keys get deleted unexpectedly in Windows Server 2008 R2

I am facing a strange problem in developing an installation that should in one of the steps install a certificate.
The problem has to do with granting Certificate’s private key access for an account (e.g. IIS_IUSRS) on Windows Server 2008 R2. The private keys are stored in the location C:\Users\All Users\Microsoft\Crypto\RSA\MachineKeys.
A custom C# Setup Project imports a Certificate and gives access for an account on Certificate’s private key during the installation process. After some time (2-3 sec) the private key file is automatically deleted from the MachineKeys folder. Thus the installed Web Application cannot access the specific certificate and displays the following error message:
“System.Security.Cryptography.CryptographicException: Keyset does not exist”. This error occurs only on Windows Server 2008 R2, while for Windows Server 2003 everything is working correctly.
My question is, why the private key gets deleted and which process does this?
Thx
UPDATE 17/05/2012
I have not yet found a solution to the described problem, and no response has been posted on the other forums where I asked (forums.asp.net, social.msdn.microsoft.com). So, can anyone suggest any other resources or advice for further troubleshooting this issue?
Thanks again
This was happening to me too - my setup script would add the cert and grant access to the PK file fine, and the app would work. Then later, after I had closed the PowerShell editor I re-launched the app and it failed with a keyset not found.
Adding the PersistKeySet flag when importing the cert fixed the problem. Here's the PowerShell code for adding the cert and private key with persistence:
param(
[string]$certStore = "LocalMachine\TrustedPeople",
[string]$filename = "sp.pfx",
[string]$password = "password",
[string]$username = "$Env:COMPUTERNAME\WebSiteUser"
)
function getKeyUniqueName($cert) {
return $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
}
function getKeyFilePath($cert) {
return "$ENV:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$(getKeyUniqueName($cert))"
}
$certFromFile = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($filename, $password)
$certFromStore = Get-ChildItem "Cert:\$certStore" | Where-Object {$_.Thumbprint -eq $certFromFile.Thumbprint}
$certExistsInStore = $certFromStore.Count -gt 0
$keyExists = $certExistsInStore -and ($certFromStore.PrivateKey -ne $null) -and (getKeyUniqueName($cert) -ne $null) -and (Test-Path(getKeyFilePath($certFromStore)))
if ((!$certExistsInStore) -or (!$keyExists)) {
$keyFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet
$keyFlags = $keyFlags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
$certFromFile.Import($filename, $password, $keyFlags)
$store = Get-Item "Cert:\$certStore"
$store.Open("ReadWrite")
if ($certExistsInStore) {
#Cert is in the store, but we have no persisted private key
#Remove it so we can add the one we just imported with the key file
$store.Remove($certFromStore)
}
$store.Add($certFromFile)
$store.Close()
$certFromStore = $certFromFile
"Installed x509 certificate"
}
$pkFile = Get-Item(getKeyFilePath($certFromStore))
$pkAcl = $pkFile.GetAccessControl("Access")
$readPermission = $username,"Read","Allow"
$readAccessRule = new-object System.Security.AccessControl.FileSystemAccessRule $readPermission
$pkAcl.AddAccessRule($readAccessRule)
Set-Acl $pkFile.FullName $pkAcl
"Granted read permission on private key to web user"
Is very clear that is a security issue “System.Security.”. and you do not have permissions to do the installation., you need to set the permissions on the private key to allow that service account access it.
Edit later: Go to Start->Run->cmd->type mmc->Select File->Add/Remove->Select Certificates->Add->Computer Account->Local., i attach a screenshot is in spanish but I indicated the fields:
Open->Certificates->Personal->Certificates->Right click Certificate-> All Tasks->Manage Private Keys->Add Network Service.
Also check this entry to see how works this feature in Windows Server 2008., then please after you try it, come back and say if you could solve the issue with what I have told you.
http://referencesource.microsoft.com/#System/security/system/security/cryptography/x509/x509certificate2collection.cs,256 shows where the PersistKeySet flag is tested. The PersistKeySet flag is documented at https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509keystorageflags%28v=vs.110%29.aspx with the phrase "The key associated with a PFX file is persisted when importing a certificate." My techno-babble to English translator tells me this means "You must include the PersistKeySet flag if you call the X509Certificate2 constructor and the certificate might already be installed on the machine." This probably applies to the .Import calls too. It's likely the powershell Import-PfxCertificate cmdlet already does this. But if you are doing what the accepted answer shows or what the OP asked, you need to include the special key. We used a variation of ejegg's script in our solution. We have a process that runs every 3 minutes to check that all configured certs are installed and this seems to work fine now.
The symptom we saw in powershell is the HasPrivateKey property is true but the PrivateKey value itself is null. And the key file for the cert in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys was deleted. The FindPrivateKey utility at https://msdn.microsoft.com/en-us/library/aa717039(v=vs.110).aspx helped us watch file get deleted.
So happy 4th birthday to the question with this very late response.

How to trust a certificate in Windows Powershell

I am using Windows 7, and want to run signed scripts from Powershell, the security-settings of Powershell are set to "all-signed", and my scripts are signed with a valid certificate from my company. I have also added the .pfx-file to my local certificate store (right-clicked the pfx-file and installed).
However, when I start a signed script, I get a message that says:
"Do you want to run software from this untrusted publisher?
File Z:\Powershell Signed Scripts\signed.ps1 is published by CN=[MyCompanyName] and is not trusted on your system. Only run scripts from
trusted publishers.
[V] Never run [D] Do not run [R] Run once [A] Always run [?] Help
(default is "D"):"
Since I want to automatically call these scripts on my systems, I would like to add my imported certificate to the trusted list on my system, so that I do not get a message anymore when I run a signed script for the first time. How can I make my certificate a trusted one?
How to trust a certificate in Windows Powershell
Indeed, you can do this without any mmc :)
First, check the location of your personal certificate named for example "Power" :
Get-ChildItem -Recurse cert:\CurrentUser\ |where {$_ -Match "Power"} | Select PSParentPath,Subject,Issuer,HasPrivateKey |ft -AutoSize
(This one should be empty:)
gci cert:\CurrentUser\TrustedPublisher
Build the command with the path to your certificate:
$cert = Get-ChildItem Certificate::CurrentUser\My\ABLALAH
Next work on certificate store (Here I work on two certificate store : user & computer)
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store "TrustedPublisher","LocalMachine"
$store.Open("ReadWrite")
$store.Add($cert)
$store.Close()
Check, you should find your certificate :
ls cert:\CurrentUser\TrustedPublisher
Sounds like you need to verify that the script is signed properly and that you have the correct certificate installed in the correct certificate store.
Use the Get-AuthenticodeSignature cmdlet to get information about the signed script.
Also review Scott's guide for signing certificates.

Where to begin creating a Powershell script for this?

If I needed to give a certificate an additional permission as described below, where do I even begin with in Powershell? There must be a Windows commandline executable that I have to start with from commandline. Only then I could think about writing it in a powershell script, correct?
Click Start, type mmc in the Search programs and files box, and then press ENTER.
On the File menu, click Add/Remove Snap-in.
Under Available snap-ins, double-click Certificates.
Select Computer account, and then click Next.
Click Local computer, and then click Finish. Click OK.
Under Console Root, Certificates (Local Computer), in the Personal store, click Certificates.
Right-click xxAzurehost1 certificate that has been created earlier. Choose All Tasks > Manage Private Keys. Click Add and then Advanced.
Click Locations and choose your local computer. Click Find Now. Select
NETWORK SERVICE in the search results and click OK. Click OK. In the
Permissions for xxxazurehost1 private keys window, select NETWORK
SERVICE and give Read permissions. Click OK.
To control the ACL for the private key all you have to do is edit a file ACL. The trick is to find which file.
Private keys are stored in:
%ProgramData%\Microsoft\Crypto
On XP:
C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto
Under here you'll see keys organized by algorithm e.g. DSS, RSA.
The User Store will be subfolder with a user SID. The Local Machine store will be in subfolder:
MachineKeys
Here's a way to get the file which stores the private key information for your cert.
First go into the local machine personal certificate store:
PS> cd cert:\LocalMachine\My
Now you have to get a handle to your cert. There's more than one way to do this, here's one using the thumbprint:
$cert = dir | ? {$_.Thumbprint -eq "232820EEBF7DBFA01EE68A28BA0450671F862AE1"}
Now you can find the private key file name like this:
$fileName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
$keyFile = dir -Path "C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto" -Recurse | ? {$_.Name -eq $fileName}
$keyFile will be the FileInfo object you can change the ACL on with either Set-ACL or icacls.exe