Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assigning Permission to Certificate's Private Key via Powershell (Win 2012 R2)

On a Windows Server 2012 R2 machine attached to a domain, I am running the following statements:

$target_machine_fqdn = [System.Net.Dns]::GetHostByName($env:computerName)

$certificate_request = Get-Certificate `
    -Template 'AcmeComputer' `
    -DnsName $target_machine_fqdn `
    -CertStoreLocation 'Cert:\LocalMachine\My'

I'm requesting a certificate for the host from the domain's CA. The statement returns with no error. A certificate is generated for the machine and placed in it's "Cert:\LocalMachine\My" as requested.

Problem: I can't figure out how to grant a service account rights to the certificate's private key.

Now, there are about 1,000 articles instructing people how to grant permission by retrieving the UniqueKeyContainerName with code that starts like the following:

$certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName

That won't work here. While the certificate has a private key, the private key data member is null:

enter image description here

In the cases where the solution I just eluded to works, the private key is on the file system. However, in this case, the private key is in the registry at the following:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SystemCertificates\MY\Keys

When using the Certificates MMC-snapin, I can see the certificate. I can manage permissions on the private key. So, I know it's there. Unfortunately, I need to automate the permission assignment so using the Certificates MMC-snapin isn't an option. I need to do this through Powershell some how.

like image 591
Adam Avatar asked Sep 03 '25 09:09

Adam


2 Answers

I recently went through automating access to certificate private key myself. I too found a number of places telling me to modify the ACLs of the key data on the hard drive, but that was not satisfying since when I checked the permissions to the private key using PowerShell the user I added wasn't listed. So much web searching, a few articles, and a fair bit of trial and error led me to this.

I start by defining the user object, and the access that I want to grant them:

# Create NTAccount object to represent the account
$AccountName = 'Domain\UserName'
$User = New-Object System.Security.Principal.NTAccount($AccountName)
# Define AccessRule to be added to the private key, could use 'GenericRead' if all you need is read access
$AccessRule = New-Object System.Security.AccessControl.CryptoKeyAccessRule($User, 'FullControl', 'Allow')

Then I open the local machine certificate store as Read/Write, and find the certificate that I'm looking for:

# Define the thumbprint of the certificate we are interested in
$Thumb = '63CFDDE9A748345CD77C106DAA09B805B33951BF'
# Open Certificate store as read/write
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
$store.Open("ReadWrite")
# Look up the certificate's reference object in the store
$RWcert = $store.Certificates | where {$_.Thumbprint -eq $Thumb}

Then I make a new CSP (Crypto Service Provider) parameter set, based off the existing certificate, add the new access rule to the parameter set.

# Create new CSP parameter object based on existing certificate provider and key name
$csp = New-Object System.Security.Cryptography.CspParameters($RWcert.PrivateKey.CspKeyContainerInfo.ProviderType, $RWcert.PrivateKey.CspKeyContainerInfo.ProviderName, $RWcert.PrivateKey.CspKeyContainerInfo.KeyContainerName)

# Set flags and key security based on existing cert
$csp.Flags = "UseExistingKey","UseMachineKeyStore"
$csp.CryptoKeySecurity = $RWcert.PrivateKey.CspKeyContainerInfo.CryptoKeySecurity
$csp.KeyNumber = $RWcert.PrivateKey.CspKeyContainerInfo.KeyNumber

# Add access rule to CSP object
$csp.CryptoKeySecurity.AddAccessRule($AccessRule)

Then we instantiate a new CSP, with those parameters, which will apply the new access rule to the existing certificate based off the flags we defined, and the key info we gave it.

# Create new CryptoServiceProvider object which updates Key with CSP information created/modified above
$rsa2 = New-Object System.Security.Cryptography.RSACryptoServiceProvider($csp)

Then we just close the certificate store, and we're all done.

# Close certificate store
$store.Close() 

Edit: After looking around I realized that I had a couple certs that are the same. I believe that this is due to a non-RSA cipher being used to encrypt the private key. I used some of the info from this answer that explains how to work with a third party CNG crypto provider. I didn't like having to download an assembly to do the things in that answer, but I used a little bit of the code to get the path to the key (yes, there is a key on the drive), and added an ACL to the file, which did work for delegating rights to the private key. So here's what I did...

First, we verify that the certificate has a CNG based key:

[Security.Cryptography.X509Certificates.X509CertificateExtensionMethods]::HasCngKey($Certificate)

If that returns True then we're a go to move on past that. Mine did, I'm guessing yours will too. Then we find that key on the hard drive by reading the PrivateKey data (which is missing from $Certificate), and getting the UniqueName for it, and then searching the Crypto folder for that file.

$privateKey = [Security.Cryptography.X509Certificates.X509Certificate2ExtensionMethods]::GetCngPrivateKey($Certificate)
$keyContainerName = $privateKey.UniqueName
$keyMaterialFile = gci $env:ALLUSERSPROFILE\Microsoft\Crypto\*Keys\$keyContainerName

Then I grabbed the current ACLs for the file, made up a new AccessRule to give the desired user access to the file, added the rule to the ACLs I just grabbed, and applied the updated ACL back to the file.

$ACL = Get-Acl $keyMaterialFile
$AccountName = 'Domain\User'
$User = New-Object System.Security.Principal.NTAccount($AccountName)
$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($User,'FullControl','None','None','Allow')
$ACL.AddAccessRule($AccessRule)
Set-Acl -Path $keyMaterialFile -AclObject $ACL

After that I was able to look in certlm.msc and verify that the user had rights to the private key.

Dependency Update: Looks like Microsoft now publishes Security.Cryptography.dll, so you don't have to compile it off GitHub. If you have the AzureRM module installed you can find the DLL in a number of the component modules (I grabbed it from AzureRM.SiteRecovery).

like image 124
TheMadTechnician Avatar answered Sep 05 '25 00:09

TheMadTechnician


The TheMadTechnician answered this question like a mf'n champ. Should he ever need me to help him bury a body, he need only call. I am adding to his answer with details for folks that cannot build/deploy the assembly

NOTE: The clrsecurity project had been posted to CodePlex, which was shutdown in 2017. The project was moved to github where it can be downloaded. The clrsecurity assembly referenced in the post is no longer supported.

Also, credit to Vadims Podāns (Crypt32) who wrote the article Retrieve CNG key container name and unique name, which helps readers access the CNG private key using unmanaged code in Powershell.

If you are like me and cannot use the clrsecurity assembly, the .NET 4.5.6 framework introduced a elements we can leverage. Consider the following:

## Identify the certificate who's private key you want to grant
## permission to
$certificate = $(ls 'cert:\LocalMachine\My\C51280CE3AD1FEA848308B764DDCFA7F43D4AB1A')

## Identify the user you'll be granting permission to
$grantee_name = 'foo\lordAdam'
$grantee = New-Object System.Security.Principal.NTAccount($grantee_name)

## Get the location and permission-of the cert's private key
$privatekey_rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate)
$privatekey_file_name = $privatekey_rsa.key.UniqueName
$privatekey_path = "${env:ALLUSERSPROFILE}\Microsoft\Crypto\Keys\${privatekey_file_name}"
$privatekey_file_permissions = Get-Acl -Path $privatekey_path

## Grant the user 'read' access to the key
$access_rule = New-Object System.Security.AccessControl.FileSystemAccessRule($grantee, 'Read', 'None', 'None', 'Allow')
$privatekey_file_permissions.AddAccessRule($access_rule)
Set-Acl -Path $privatekey_path -AclObject $privatekey_file_permissions

We're using the GetRSAPrivateKey() method of the System.Security.Cryptography.X509Certificates.RSACertificateExtensions static class to return us the private key. We then grab the UniqueName (Key property has a UniqueName property). We use that to deduce the key file's location. The rest is granting file permissions.

If you want to see where private keys are stored, check out Key Storage and Retrieval.

like image 43
Adam Avatar answered Sep 05 '25 01:09

Adam