6

I used following PowerShell function to import PFX to my Windows 2008 R2 server's certificate store

function Import-PfxCertificate ([String]$certPath,[String]$certificateStoreLocation = "CurrentUser",[String]$certificateStoreName = "My",$pfxPassword = $null)
{
    $pfx = new-object System.Security.Cryptography.X509Certificates.X509Certificate2    

    $pfx.Import($certPath, $pfxPassword, "Exportable,PersistKeySet")    

    $store = new-object System.Security.Cryptography.X509Certificates.X509Store($certificateStoreName,$certificateStoreLocation)    
    $store.open("MaxAllowed")    
    $store.add($pfx)    
    $store.close()
    return $pfx
}

The caller of the function looks like $importedPfxCert = Import-PfxCertificate $pfxFile "LocalMachine" "My" $password I installed it to local machine's My store. Then I granted read permission to my IIS Application pool.

I have a WCF service which needs to use it

<behaviors>
  <serviceBehaviors>
    <behavior>
      <serviceCredentials>
        <serviceCertificate findValue="MyCertName" x509FindType="FindBySubjectName" />
        <userNameAuthentication userNamePasswordValidationMode="Custom"
          customUserNamePasswordValidatorType="MyValidator" />
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>

When I use a client to call the service, I got exception from WCF It is likely that certificate 'CN=MyCertName' may not have a private key that is capable of key exchange or the process may not have access rights for the private key.

If I remove it from MMC, and manually import the same PFX file from Certificate MMC, to same store and grant same permission, my client can call the service without problem.

So it leads me to think, for some reason if I use PowerShell the private key is screwed somehow.

The funny thing is in either way, I go to MMC and double click on my installed certificate I can see You have a private key that corresponds to the certificate. so it looks like private key is loaded even in PowerShell. permission settings are identical.

Any clue or experience?

hardywang
  • 4,864
  • 11
  • 65
  • 101

5 Answers5

5

Have same issue. Next script work:

function InstallCert ($certPath, [System.Security.Cryptography.X509Certificates.StoreName] $storeName)
{
    [Reflection.Assembly]::Load("System.Security, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a")

    $flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet

    $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, "", $flags)

    $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($storeName, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)

    $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite);

    $store.Add($cert);

    $store.Close();
}
Sergey Azarkevich
  • 2,641
  • 2
  • 22
  • 38
  • 4
    For anyone trying to parse out what actually fixed things, it's the `MachineKeySet` flag that ensures the private keys are stored at the machine level rather than the current user. – TLS Jun 20 '17 at 14:47
  • I was able to use this function to import a cert+key p12 file into the "My" store, but was unable to get it to accept "Remote Desktop" as a StoreName. I ended up removing the type definition `System.Security.Cryptography.X509Certificates.StoreName` from the function signature and it worked. I knew "Remote Desktop" was valid because of `dir 'Cert:\localmachine\Remote Desktop'` – Bruno Bronosky Sep 08 '17 at 13:57
  • In other words, change the first line to `function InstallCert ($certPath, $storeName)` – Bruno Bronosky Sep 08 '17 at 13:57
2

I updated Sergey's answer to the following. Note that the using namespace ... syntax is only valid for PS 5.0 and later. If you need this for an earlier version, you will have to add the full namespace, System.Security.Cryptography.X509Certificates, as needed.

using namespace System.Security

[CmdletBinding()]
param (
    [parameter(mandatory=$true)] [string] $CertificateFile,
    [parameter(mandatory=$true)] [securestring] $PrivateKeyPassword,
    [parameter(mandatory=$true)] [string] $AllowedUsername
)

# Setup certificate
$Flags = [Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet `
    -bor [Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet `
    -bor [Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
$Certificate = New-Object Cryptography.X509Certificates.X509Certificate2($CertificateFile, $PrivateKeyPassword, $Flags)

# Install certificate into machine store
$Store = New-Object Cryptography.X509Certificates.X509Store(
    [Cryptography.X509Certificates.StoreName]::My, 
    [Cryptography.X509Certificates.StoreLocation]::LocalMachine)
$Store.Open([Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$Store.Add($Certificate)
$Store.Close()

# Allow read permission of private key by user
$PKFile = Get-ChildItem "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$($Certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName)"
$PKAcl = $PKFile.GetAccessControl("Access")
$ReadAccessRule = New-Object AccessControl.FileSystemAccessRule(
    $AllowedUsername,
    [AccessControl.FileSystemRights]::Read,
    [AccessControl.AccessControlType]::Allow
)
$PKAcl.AddAccessRule($ReadAccessRule)
Set-Acl $PKFile.FullName $PKAcl

Save this script to InstallCertificate.ps1, then run it as Administrator:

PS C:\Users\me> .\InstallCertificate.ps1

cmdlet InstallCertificate.ps1 at command pipeline position 1
Supply values for the following parameters:
CertificateFile: c:\my\path\mycert.pfx
PrivateKeyPassword: *********************
AllowedUsername: me
PS C:\Users\me> ls Cert:\LocalMachine\My
<Observe that your cert is now listed here.  Get the thumbprint>
PS C:\Users\me> (ls Cert:\LocalMachine\My | ? { $_.Thumbprint -eq $Thumbprint }).PrivateKey

After rebooting, the last line should show that the private key is still installed even as non-Administrator.

Edited to add the ACL step as described in https://stackoverflow.com/a/37402173/7864889.

powerdude
  • 21
  • 2
0

I had a similar issue on one of our dev servers when importing a certificate through the MMC. My problem was that the Administrators group did not have any permissions on the MachineKeys folder.

C:\Users\All Users\Microsoft\Crypto\RSA\MachineKeys

I added full control on the MachineKeys folder to Administrators and it was able to successfully create the private key when importing the certificate.

Make sure the user you're running Powershell under has access to write to the MachineKeys folder.

mobese46
  • 241
  • 1
  • 7
0

The following code referenced below, by Sergey Azarkevich, is what did the trick for me:

$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
Fares
  • 669
  • 6
  • 11
0

I landed on this SO thread as the built in Import-PfxCertificate was not importing CAPI certificates correctly. Unfortunately powerdude's answer cmdlet didnt work for me as it threw The property 'CspKeyContainerInfo' cannot be found on this object. Verify that the property exists. when setting the permissions.

After combining it with this wonderful gist from milesgratz it worked.

Here is the final modified version made to look like a Import-PfxCertificate substitution.

# Import-CapiPfxCertificate.ps1
# for CNG certificates use built in Import-PfxCertificate

using namespace System.Security

[CmdletBinding()]
param (
    [parameter(mandatory=$true)] [string] $FilePath,
    [parameter(mandatory=$true)] [securestring] $Password,
    [parameter(mandatory=$true)] [string] $CertStoreLocation,
    [parameter(mandatory=$false)] [string] $AllowedUsername
)

if (-not ($CertStoreLocation -match '^Cert:\\([A-Z]+)\\([A-Z]+)$')) {
    Write-Host "Incorrect CertStoreLocation. See usage in the Import-PfxCertificate documentation" -ForegroundColor Red
    exit 1;
}

$StoreName = $Matches.2
$StoreLocation = $Matches.1

# Setup certificate
$Flags = [Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet `
    -bor [Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet `
    -bor [Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
$Certificate = New-Object Cryptography.X509Certificates.X509Certificate2($FilePath, $Password, $Flags)

# Install certificate into the specified store
$Store = New-Object Cryptography.X509Certificates.X509Store(
    $StoreName, 
    $StoreLocation)
$Store.Open([Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$Store.Add($Certificate)
$Store.Close()

if (-not ([string]::IsNullOrEmpty($AllowedUsername))) {
    # Allow read permission of private key by user
    $PKUniqueName = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)).key.UniqueName
    $PKFile = Get-Item "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$PKUniqueName"
    $PKAcl = Get-Acl $PKFile
    $PKAcl.AddAccessRule((New-Object AccessControl.FileSystemAccessRule($AllowedUsername, "Read", "Allow")))
    Set-Acl $PKFile.FullName $PKAcl
}
Angel Yordanov
  • 3,112
  • 1
  • 22
  • 19