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).