10

I have a PowerShell script that installs pfx certificate into the LocalMachine certificate store. The function looks like this:

function Add-Certificate {
param
(
    [Parameter(Position=1, Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$pfxPath,

    [Parameter(Position=2, Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$pfxPassword
)

    Write-Host "Installing certificate" -ForegroundColor Yellow

    try 
    {
        $pfxcert = new-object system.security.cryptography.x509certificates.x509certificate2
        $pfxcert.Import($pfxPath, $pfxPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]"PersistKeySet")

        $store = new-object system.security.cryptography.X509Certificates.X509Store -argumentlist "MY", LocalMachine
        $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]"ReadWrite");
        $store.Add($pfxcert);
        $store.Close();

        return $pfxcert
    }
    catch 
    {
        throw
    }
}

When I open the Certificate Manager to verify the installation I can see that it has installed correctly.

The next step in my process is to assign permissions to the certificate to a service account.

function Set-CertificatePermission
{
    param
    (
        [Parameter(Position=1, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$pfxThumbPrint,

        [Parameter(Position=2, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$serviceAccount
    )

    $cert = Get-ChildItem -Path cert:\LocalMachine\My | Where-Object -FilterScript { $PSItem.ThumbPrint -eq $pfxThumbPrint; };

    # Specify the user, the permissions and the permission type
    $permission = "$($serviceAccount)","Read,FullControl","Allow"
    $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission;

    # Location of the machine related keys
    $keyPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\";
    $keyName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName;
    $keyFullPath = $keyPath + $keyName;

    try
    {
        # Get the current acl of the private key
        # This is the line that fails!
        $acl = Get-Acl -Path $keyFullPath;

        # Add the new ace to the acl of the private key
        $acl.AddAccessRule($accessRule);

        # Write back the new acl
        Set-Acl -Path $keyFullPath -AclObject $acl;
    }
    catch
    {
        throw $_;
    }
}

This function fails. Specifically, this function fails when trying to evaluate the Get-Acl command, with the following error: Get-Acl : Cannot find path 'C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\59f1e969a4f7e5de90224f68bc9be536_1d508f5e-0cbc-4eca-a402-3e55947faa3b'

As it turns out the key file has been installed into my roaming profile C:\Users\MyUserName\AppData\Roaming\Microsoft\Crypto\RSA\S-1-5-21-1259098847-1967870486-1845911597-155499

I'm sure there is something wrong with the Add-Certificate function, but I cannot figure out what it is. How do I force it to install the key file in the C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys directory?

stephenl
  • 3,119
  • 4
  • 22
  • 22
  • FYI, I know this doesn't address the main topic, but `try .. catch` statements will only catch terminating errors in PowerShell. Most cmdlets generate non-terminating errors, so to change them into terminating errors, use the `-ErrorAction` parameter on `Get-Acl` and set the value to `Stop`. –  Dec 31 '13 at 06:25
  • Thanks @TrevorSullivan, I'll make that change immediately. – stephenl Dec 31 '13 at 06:31
  • You're quite welcome. Is there a reason you aren't using the `Import-PfxCertificate` cmdlet, by the way? –  Dec 31 '13 at 06:31
  • @TrevorSullivan As far as I am aware this is only available in Windows 2012. – stephenl Dec 31 '13 at 06:34
  • What version of Windows and PowerShell are you running? –  Dec 31 '13 at 06:36
  • @TrevorSullivan I'm using PowerShell 4 on Windows 2008R2. The PKI module isn't available as a module on 2008. – stephenl Dec 31 '13 at 06:39
  • One addition only: GetAccessControl in try-catch is not a cmdlet, needs to be in parentheses. – Arman Feb 06 '17 at 16:58

2 Answers2

14

The problem is the when the X509Certificate2 was getting imported via the Import() method, the X509KeyStorageFlags were not configured to write the private key to the computer's private key store. I've updated the function to include the appropriate X509KeyStorageFlags.

function Add-Certificate {
    [CmdletBinding()]
    param
    (
        [Parameter(Position=1, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(Position=2, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Password
    )

    Write-Verbose -Message ('Installing certificate from path: {0}' -f $Path);

    try 
    {
        # Create the certificate
        $pfxcert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ErrorAction Stop;
        $KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bxor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bxor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet;
        Write-Verbose ('Key storage flags is: {0}' -f $KeyStorageFlags);
        $pfxcert.Import($Path, $Password, $KeyStorageFlags);

        # Create the X509 store and import the certificate
        $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList My, LocalMachine -ErrorAction Stop;
        $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite);
        $store.Add($pfxcert);
        $store.Close();

        Write-Output -InputObject $pfxcert;
    }
    catch 
    {
        throw $_;
    }
}
  • 4
    I'm looking at the same sort of thing. The problem I have is this sets the permissions on the file system, but not permissions on the private key itself (e.g. inside certificate manager right click the cert->all tasks-> manage private keys). Any idea how to modify that? – LDJ Feb 21 '14 at 13:36
4

The script provided worked for me (thanks!) with one change, due to a windows bug (see Why does Set-Acl on the drive root try to set ownership of the "object"?)

Here is the updated script:

function Set-CertificatePermission
{
 param
 (
    [Parameter(Position=1, Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$pfxThumbPrint,

    [Parameter(Position=2, Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$serviceAccount
 )

 $cert = Get-ChildItem -Path cert:\LocalMachine\My | Where-Object -FilterScript { $PSItem.ThumbPrint -eq $pfxThumbPrint; };

 # Specify the user, the permissions and the permission type
 $permission = "$($serviceAccount)","Read,FullControl","Allow"
 $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission;

 # Location of the machine related keys
 $keyPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\";
 $keyName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName;
 $keyFullPath = $keyPath + $keyName;

 try
 {
    # Get the current acl of the private key
    $acl = (Get-Item $keyFullPath).GetAccessControl()

    # Add the new ace to the acl of the private key
    $acl.AddAccessRule($accessRule);

    # Write back the new acl
    Set-Acl -Path $keyFullPath -AclObject $acl;
 }
 catch
 {
    throw $_;
 }
}
Daniel Fisher lennybacon
  • 3,865
  • 1
  • 30
  • 38
  • One addition only: GetAccessControl in try-catch is not a cmdlet, needs to be in parentheses. Otherwise, looks cool :) – Arman Feb 06 '17 at 16:58
  • 1
    For anyone who comes along and tries to use this answer, the line with $acl in the try-catch should look like this: $acl = (Get-Item $keyFullPath).GetAccessControl('Access') – Brett Bim Feb 16 '17 at 20:53
  • I notice this is granting "Read,FullControl". When does "FullControl" need to be included there, and when would just "Read" be enough? – John Rusk - MSFT Aug 07 '17 at 04:56