35

Certificate is already installed on machine. Now I want to give read permission on PrivateKey of Certificate to application user.

Balpreet Patil
  • 1,644
  • 2
  • 16
  • 16

6 Answers6

44

Here is the Answer.

Created a powershell script file AddUserToCertificate.ps1

Here is the content for script file.

param(
    [string]$userName,
    [string]$permission,
    [string]$certStoreLocation,
    [string]$certThumbprint
);
# check if certificate is already installed
$certificateInstalled = Get-ChildItem cert:$certStoreLocation | Where thumbprint -eq $certThumbprint

# download & install only if certificate is not already installed on machine
if ($certificateInstalled -eq $null)
{
    $message="Certificate with thumbprint:"+$certThumbprint+" does not exist at "+$certStoreLocation
    Write-Host $message -ForegroundColor Red
    exit 1;
}else
{
    try
    {
        $rule = new-object security.accesscontrol.filesystemaccessrule $userName, $permission, allow
        $root = "c:\programdata\microsoft\crypto\rsa\machinekeys"
        $l = ls Cert:$certStoreLocation
        $l = $l |? {$_.thumbprint -like $certThumbprint}
        $l |%{
            $keyname = $_.privatekey.cspkeycontainerinfo.uniquekeycontainername
            $p = [io.path]::combine($root, $keyname)
            if ([io.file]::exists($p))
            {
                $acl = get-acl -path $p
                $acl.addaccessrule($rule)
                echo $p
                set-acl $p $acl
            }
        }
    }
    catch 
    {
        Write-Host "Caught an exception:" -ForegroundColor Red
        Write-Host "$($_.Exception)" -ForegroundColor Red
        exit 1;
    }    
}

exit $LASTEXITCODE

Now run it as part of deployment. Example to running above script in powershell console window.

C:\>.\AddUserToCertificate.ps1 -userName testuser1 -permission read -certStoreLocation \LocalMachine\My -certThumbprint 1fb7603985a8a11d3e85abee194697e9784a253

this example give read permission to user testuser1 on certificate that in installed in \LocalMachine\My and has thumb print 1fb7603985a8a11d3e85abee194697e9784a253

If you are using ApplicationPoolIdentity then you username will be 'IIS AppPool\AppPoolNameHere'

Note: You will need to use ' ' as there is a space between IIS and AppPool.

Balpreet Patil
  • 1,644
  • 2
  • 16
  • 16
  • 2
    thanks a lot, great script. if anyone needs to give the full control permission, it is really - _fullcontrol_ – iBobb Nov 22 '17 at 14:41
  • I have an old Win2008 machine which has PowerShell V2. I had to use `$certificateInstalled = Get-ChildItem cert:$certStoreLocation | Where {$_.thumbprint -eq $certThumbprint}` in line 8 (the first statement) – Tony May 09 '18 at 05:35
  • 3
    This no longer works. $_.privatekey returns null now. – Quark Soup Apr 02 '19 at 00:44
  • Thanks this really saved my week, I combine this with this answer https://stackoverflow.com/questions/7334216/iis7-permissions-overview-applicationpoolidentity and was able to set the application pool to read the certificate. – Juan May 18 '20 at 03:07
36

The accepted answer did not work for me as the $_.privatekey returned null. I managed to get access to the private key and assign 'Read' permissions for my Application Pool as follows:

param (
[string]$certStorePath  = "Cert:\LocalMachine\My",
[string]$AppPoolName,
[string]$certThumbprint
)

Import-Module WebAdministration
    
$certificate = Get-ChildItem $certStorePath | Where thumbprint -eq $certThumbprint

if ($certificate -eq $null)
{
    $message="Certificate with thumbprint:"+$certThumbprint+" does not exist at "+$certStorePath
    Write-Host $message -ForegroundColor Red
    exit 1;
}else
{
    $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate)
    $fileName = $rsaCert.key.UniqueName
    $path = "$env:ALLUSERSPROFILE\Microsoft\Crypto\Keys\$fileName"
    $permissions = Get-Acl -Path $path

    $access_rule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS AppPool\$AppPoolName", 'Read', 'None', 'None', 'Allow')
    $permissions.AddAccessRule($access_rule)
    Set-Acl -Path $path -AclObject $permissions
}
Michael Armitage
  • 1,502
  • 19
  • 17
  • Yes. The accepted answer use to work for me, but it's been a while since I ran it. Had the same problem, the $_.privatekey returned null. Thank you. This seems to have fixed it. – Quark Soup Apr 02 '19 at 00:15
  • This doesn't seem to work right anymore either. The $rsaCert and $fileName variables don't seem to contain the correct information, pulling a UniqueName value that simply doesn't exist in a way that the $path variable can use. – nightsurfer Dec 01 '21 at 14:45
6

It came to my attention a few weeks ago that something changed (I suspect a Windows update) and broke the ability for some certificates to use the CspKeyContainerInfo.UniqueKeyContainerName property referenced in Michael Armitage's script. Some sleuthing uncovered that Windows decided to start using CNG instead of Crypto Service Provider to protect the key. The following script fixed my issue and should correctly support CNG vs CSP use case scenarios:

$serviceUser = "DOMAIN\Service User"
$certificate = Get-ChildItem Cert:\LocalMachine\My | Where-Object Thumbprint -eq "certificatethumbprint"

$privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate)
$containerName = ""
if ($privateKey.GetType().Name -ieq "RSACng")
{
    $containerName = $privateKey.Key.UniqueName
}
else
{
    $containerName = $privateKey.CspKeyContainerInfo.UniqueKeyContainerName
}

$keyFullPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + $containerName;
if (-Not (Test-Path -Path $keyFullPath -PathType Leaf))
{
    throw "Unable to get the private key container to set permissions."
}

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

# Add the new ACE to the ACL of the private key
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($serviceUser, "Read", "Allow")
$acl.AddAccessRule($accessRule);

# Write back the new ACL
Set-Acl -Path $keyFullPath -AclObject $acl;

You would, of course, want to adapt/enhance this to meet your specific needs.

anxkha
  • 61
  • 1
  • 1
  • 2
    Nicely done, keep in mind fewer and fewer certs are being stored in the RSA\MachineKeys folder, so you may be better off doing something like `$keyFullPath = Get-ChildItem -Path $env:AllUsersProfile\Microsoft\Crypto -Recurse -Filter $containerName | Select -Expand FullName` – TheMadTechnician Feb 10 '22 at 23:27
  • Ahah, good to know, thanks! I was curious which folders they are transitioning to and came across this document: https://learn.microsoft.com/en-us/windows/win32/seccng/key-storage-and-retrieval I'll update my answer once I have a chance to test your proposed modification! – anxkha Feb 12 '22 at 18:41
  • @anxkha The suggestion of @TheMadTechnician does return the exact same result as your current `$keyFullPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + $containerName;` line above. However, on my Win2k19 Server machine using PS 7.2.4, there's no `.GetAccessControl()` method available: `InvalidOperation: Method invocation failed because [System.IO.FileInfo] does not contain a method named 'GetAccessControl'.` - any help? – Yoda Jun 01 '22 at 15:46
  • 1
    If I remember correctly, PowerShell Core does not have the ability to modify ACLs and one of the symptoms of this is GetAccessControl not existing. Someone else may have had better luck/found a way, but I have not found a way to convince PowerShell Core to know how to modify ACLs and so only the built-in PowerShell 5.4 works for this. Sorry :( – anxkha Jun 03 '22 at 02:11
4

Adding on Michael Armitage script, This will work for both the cases where PrivateKey value is present and when it is blank

function setCertificatePermission {
    param($accountName, $certificate)
    if([string]::IsNullOrEmpty($certificate.PrivateKey))
    {
        $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate)
        $fileName = $rsaCert.key.UniqueName
        $path = "$env:ALLUSERSPROFILE\Microsoft\Crypto\Keys\$fileName"
        $permissions = Get-Acl -Path $path
        $access_rule = New-Object System.Security.AccessControl.FileSystemAccessRule($accountName, 'FullControl', 'None', 'None', 'Allow')
        $permissions.AddAccessRule($access_rule)
        Set-Acl -Path $path -AclObject $permissions
    } else{
            $user = New-Object System.Security.Principal.NTAccount($accountName)
            $accessRule = New-Object System.Security.AccessControl.CryptoKeyAccessRule($user, 'FullControl', 'Allow')
            $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
            $store.Open("ReadWrite")
            $rwCert = $store.Certificates | where {$_.Thumbprint -eq $certificate.Thumbprint}
            $csp = New-Object System.Security.Cryptography.CspParameters($rwCert.PrivateKey.CspKeyContainerInfo.ProviderType, $rwCert.PrivateKey.CspKeyContainerInfo.ProviderName, $rwCert.PrivateKey.CspKeyContainerInfo.KeyContainerName)
            $csp.Flags = "UseExistingKey","UseMachineKeyStore"
            $csp.CryptoKeySecurity = $rwCert.PrivateKey.CspKeyContainerInfo.CryptoKeySecurity
            $csp.KeyNumber = $rwCert.PrivateKey.CspKeyContainerInfo.KeyNumber
            $csp.CryptoKeySecurity.AddAccessRule($AccessRule)
            $rsa2 = New-Object System.Security.Cryptography.RSACryptoServiceProvider($csp)
            $store.close()
        }
}
Ankit Patel
  • 321
  • 2
  • 8
2

As an alternate to above script. You can use PowerShell module. I have not tried it myself but module looks good. http://get-carbon.org/index.html

Here is command to set permissions http://get-carbon.org/Grant-Permission.html

Balpreet Patil
  • 1,644
  • 2
  • 16
  • 16
2

You can use WinHttpCertCfg.exe, a Certificate Configuration Tool Link: https://learn.microsoft.com/en-us/windows/desktop/winhttp/winhttpcertcfg-exe--a-certificate-configuration-tool

Some code example:

Set privatekeyAcces to Svc-LocalAgent$@mydomain.local
*.\WinHttpCertCfg.exe -g -c LOCAL_MACHINE\MY -s *.d365.mydomain.com  -a "Svc-LocalAgent$@mydomain.com"*
Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
Barreto
  • 29
  • 1
  • Take the time to learn the Powershell version of this from Michael Armitage. – Quark Soup Apr 02 '19 at 00:44
  • 1
    @DonaldAirey why? This is much simpler, if you have the tool and it supports your scenario. – Ohad Schneider Apr 16 '19 at 14:07
  • 2
    1. Because Powershell is a general purpose language. 2. Powershell will be around long after WinHttpCertCfg.exe is retired. 3. You solution involves setting the path to include the directory where WinHttpCertCfg.exe lives and, possibly, downloading it. I don't know where this tool is on my machine and I don't have time to go looking for it. – Quark Soup Apr 16 '19 at 15:26
  • 1
    setting permission on certificate where `$_.privatekey` is null, this will throw error of **access denied** – Ankit Patel Jun 23 '20 at 14:48