How to set read permission on the private key file of X.509 certificate from .NET

42,247

Solution 1

To do it programmatically, you have to do three things:

  1. Get the path of the private key folder.

  2. Get the file name of the private key within that folder.

  3. Add the permission to that file.

See this post for some example code that does all three (specifically look at the "AddAccessToCertificate" method).

Solution 2

This answer is late but I wanted to post it for anybody else that comes searching in here:

I found an MSDN blog article that gave a solution using CryptoKeySecurity here, and here is an example of a solution in C#:

var rsa = certificate.PrivateKey as RSACryptoServiceProvider;
if (rsa != null)
{
    // Modifying the CryptoKeySecurity of a new CspParameters and then instantiating
    // a new RSACryptoServiceProvider seems to be the trick to persist the access rule.
    // cf. http://blogs.msdn.com/b/cagatay/archive/2009/02/08/removing-acls-from-csp-key-containers.aspx
    var cspParams = new CspParameters(rsa.CspKeyContainerInfo.ProviderType, rsa.CspKeyContainerInfo.ProviderName, rsa.CspKeyContainerInfo.KeyContainerName)
    {
        Flags = CspProviderFlags.UseExistingKey | CspProviderFlags.UseMachineKeyStore,
        CryptoKeySecurity = rsa.CspKeyContainerInfo.CryptoKeySecurity
    };

    cspParams.CryptoKeySecurity.AddAccessRule(new CryptoKeyAccessRule(sid, CryptoKeyRights.GenericRead, AccessControlType.Allow));

    using (var rsa2 = new RSACryptoServiceProvider(cspParams))
    {
        // Only created to persist the rule change in the CryptoKeySecurity
    }
}

I'm using a SecurityIdentifier to identify the account but an NTAccount would work just as well.

Solution 3

In case this helps anyone else out, I wrote Jim Flood's answer in Powershell

function Set-PrivateKeyPermissions {
param(
[Parameter(Mandatory=$true)][string]$thumbprint,
[Parameter(Mandatory=$false)][string]$account = "NT AUTHORITY\NETWORK SERVICE"
)
#Open Certificate store and locate certificate based on provided thumbprint
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
$store.Open("ReadWrite")
$cert = $store.Certificates | where {$_.Thumbprint -eq $thumbprint}

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

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

# Create new access rule - could use parameters for permissions, but I only needed GenericRead
$access = New-Object System.Security.AccessControl.CryptoKeyAccessRule($account,"GenericRead","Allow")
# Add access rule to CSP object
$csp.CryptoKeySecurity.AddAccessRule($access)

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

#Close certificate store
$store.Close()

}

Note that the account parameter can be in the form of "DOMAIN\USER" as well (not just built in names) - I tested this in my environment and it automatically converted it to the appropriate SID

Solution 4

You can use the WinHttpCertCfg.exe tool that ships as part of the Windows Server 2003 Resource Kit Tools.

Example:

winhttpcertcfg -g -c LOCAL_MACHINE\My -s test -a NetworkService


Alternatively, you could use the Find Private Key tool that ships with the WCF SDK, to find the location on disk of the certificate's private key file. Then you can simply use ACL to set the right privileges on the file.

Example:

FindPrivateKey My LocalMachine -n "CN=test"

Solution 5

Based on @russ's answer,

This version copes with both Key Storage Provider & the Legacy Crypto Service Provider.

function Set-PrivateKeyPermissions {
    param(
        [Parameter(Mandatory=$true)]
        [string]$thumbprint,
        [Parameter(Mandatory=$true)]
        [string]$account
    )

    #Open Certificate store and locate certificate based on provided thumbprint
    $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
    $store.Open("ReadWrite")
    $cert = $store.Certificates | where {$_.Thumbprint -eq $thumbprint}

    if ($cert.PrivateKey -Eq $null) {
        # Probably using Key Storage Provider rather than crypto service provider
        $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
        if ($rsaCert -Eq $null) {
            throw "Private key on certificate $($cert.Subject) not available"
        }

        $fileName = $rsaCert.key.UniqueName
        $path = "$env:ALLUSERSPROFILE\Microsoft\Crypto\Keys\$fileName"
        $permissions = Get-Acl -Path $path

        $access_rule = New-Object System.Security.AccessControl.FileSystemAccessRule($account, "FullControl", "Allow")
        $permissions.AddAccessRule($access_rule)
        Set-Acl -Path $path -AclObject $permissions
    } else {
        #Create new CSP object based on existing certificate provider and key name
        $csp = New-Object System.Security.Cryptography.CspParameters($cert.PrivateKey.CspKeyContainerInfo.ProviderType, $cert.PrivateKey.CspKeyContainerInfo.ProviderName, $cert.PrivateKey.CspKeyContainerInfo.KeyContainerName)

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

        # Create new access rule - could use parameters for permissions, but I only needed GenericRead
        $access = New-Object System.Security.AccessControl.CryptoKeyAccessRule($account,"GenericRead","Allow")
        # Add access rule to CSP object
        $csp.CryptoKeySecurity.AddAccessRule($access)

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

    #Close certificate store
    $store.Close()
}
Share:
42,247
Ray Lu
Author by

Ray Lu

...AI & Blockchain...

Updated on November 18, 2021

Comments

  • Ray Lu
    Ray Lu over 2 years

    Here is the code to add a pfx to the Cert store.

    X509Store store = new X509Store( StoreName.My, StoreLocation.LocalMachine );
    store.Open( OpenFlags.ReadWrite );
    X509Certificate2 cert = new X509Certificate2( "test.pfx", "password" );
    store.Add( cert );
    store.Close();
    

    However, I couldn't find a way to set permission for NetworkService to access the private key.

    Can anyone shed some light? Thanks in advance.

  • Bronumski
    Bronumski over 11 years
    I prefer this solution as there is less code and no need to mess with file paths.
  • Mike Cheel
    Mike Cheel almost 11 years
    Have you gotten this to work when using remote desktop? I can see the key when I access it but the second my program finishes it disappears.
  • Mark
    Mark over 8 years
    In my case my key already existed and I wanted to re-add it with permissions. This failed silently because the private key file in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys became "detached" from the certificate. The fix was to create the certificate with flags as: X509Certificate2 cert = new X509Certificate2(pathToCert, "password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet); See this answer.
  • John Rusk - MSFT
    John Rusk - MSFT almost 7 years
    For some certificates, I think those made with Crytography Next Gen (CNG) providers, the above code can't find the private key. In that case, may this might help: stackoverflow.com/a/22146915
  • Adam
    Adam about 6 years
    When I tried this, it died at the "..CryptoKeySecurity.AddAccessRule($access)" call. It looks like the CryptoKeySecurity property is null for me. This might only work if the private key is stored on the file system (versus in the registry).
  • eddiewould
    eddiewould over 4 years
    I added a check right after loading the $cert to make sure the .PrivateKey property was not null
  • Mike Rosoft
    Mike Rosoft over 3 years
    I am getting UnauthorizedAccessException in the var rsa2 = new RSACryptoServiceProvider(cspParams) line when debugging in Visual Studio. And that's weird, because the process is running as an administrator (I have added an app.manifest file to ensure that).
  • Mike Rosoft
    Mike Rosoft over 3 years
    I have confirmed that I get the same exception when the application is executed from an elevated console.
  • Stian Standahl
    Stian Standahl over 2 years
    I have tried this. But i was unable to find the keyPath. What i did was to add the certificate to the store programatically then right after check if the certificate exists and then afterwards check if that path exists. Do you have a indication it might be some other place?
  • Stian Standahl
    Stian Standahl over 2 years
    After alot of googling i found this SO answer stackoverflow.com/a/49924679/582061 that lead me to this microsoft page that documented all of the places a certificate is hiding docs.microsoft.com/nb-no/windows/win32/seccng/… . And as you said in the last comment there, the certificate can be found under the path %APPDATA%\Microsoft\Crypto\RSA\User SID . The SID can be found like this: using System.DirectoryServices.AccountManagement; UserPrincipal.Current?.Sid?.Value
  • Stian Standahl
    Stian Standahl over 2 years
    Another note: I tried using the unique name and search through the folders, but I never got a hit. This is probably because these files and folders are hidden. So you have to manually go look for the files.
  • LunicLynx
    LunicLynx over 2 years
    @StianStandahl I was able to find it in both ways. Have you iterated over all sids? For me it was in roaming. How did you install the certificate?
  • Stian Standahl
    Stian Standahl over 2 years
    Hi. They are installed in C# code using the X509Store code api. From the second comment i made i found the SIDs by adding breakpoint in code and looking it up through debug window. Then i looked manually in the places from the provided microsoft link (where it made sense). So problem is solved! :)