Is it possible to determine the file name of a certificate's private key on a remote Windows server?
Asked Answered
A

1

8

I'm trying to determine the file name of a certificate's private key stored on a remote Windows (2k3/2k8) machine and am having some difficulty. I'm also not that familiar with Microsoft's CryptAPI, so I'm looking for any help you can provide.

The purpose of this exercise is to find certificates with private keys installed on a remote computer that meet a specific criteria and ensure the correct rights are assigned to their private key files. Although I could assign rights at the folder level, I'd prefer to only assign rights at the private key file level where it's necessary (for obvious reasons).

Here's the scenario, assume a service account with administrative-like permissions is accessing the certificate store:

  1. I retreive the remote certificate store using the following call from C# using p/invoke:

    [DllImport("CRYPT32", EntryPoint = "CertOpenStore", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr CertOpenStore(int storeProvider, int encodingType, int hcryptProv, int flags, string pvPara);

    IntPtr storeHandle = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_LOCAL_MACHINE, string.Format(@"\{0}{1}", serverName, name));

  2. I then use CertEnumCertificatesInStore to retreive certificates that I want to evaluate.

    [DllImport("CRYPT32", EntryPoint = "CertEnumCertificatesInStore", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr CertEnumCertificatesInStore(IntPtr storeProvider, IntPtr prevCertContext); IntPtr certCtx = IntPtr.Zero; certCtx = CertEnumCertificatesInStore(storeHandle, certCtx);

  3. If a certificate matches my criteria, I create an X509Certificate2 instance from the IntPtr returned from the CertEnumCertificatesInStore call like:

    X509Certificate2 current = new X509Certificate2(certCtx);

  4. Once I have the X509Certificate2 instances for certificates I'm interested in, I call CryptAcquireCertificatePrivateKey to get the private key provider:

    [DllImport("crypt32", CharSet = CharSet.Unicode, SetLastError = true)] internal extern static bool CryptAcquireCertificatePrivateKey(IntPtr pCert, uint dwFlags, IntPtr pvReserved, ref IntPtr phCryptProv, ref int pdwKeySpec, ref bool pfCallerFreeProv);

    //cert is an X509Certificate2

    CryptAcquireCertificatePrivateKey(cert.Handle, 0, IntPtr.Zero, ref hProvider, ref _keyNumber, ref freeProvider);

  5. To retreive the private key file name, I try to request the Unique Container Name from the hProvider as pData like:

    [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] internal extern static bool CryptGetProvParam(IntPtr hCryptProv, CryptGetProvParamType dwParam, IntPtr pvData, ref int pcbData, uint dwFlags);

    IntPtr pData = IntPtr.Zero; CryptGetProvParam(hProvider, PP_UNIQUE_CONTAINER, pData, ref cbBytes, 0));

So far all of the above steps work great locally (servername == local machine name); however, the unique container name (private key filename) that's returned for a certificate stored in a remote computer's local machine certificate store doesn't render as the actual private key filename I'm seeing under:

w2k3: \Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys

ws08: \ProgramData\Microsoft\Crypto\RSA\MachineKeys

For example, if I run the above steps directly on the remote machine, I get a private key file name AAAAAAA-111111, but if I run them remotely, I get a private key BBBBBBBB-2222222. Also, if I install the remote certificate locally and run the steps against my local machine I get the same private key name BBBBBBBB-2222222.

Most likely I feel that I may be missing a caveat in step 4, calling CryptAcquireCertificatePrivateKey. It may be that this call relies on the local machine's identity to generate the name of the unique container that would be used to store the private key blob.

Updated

After some further research, I found a blog that details out exactly how the filenames for private key containers are created here.

Instead of using CryptAcquireCertificatePrivateKey, you can use the methods described on that blog to get the private key container name on any machine once you have the name of the container obtained by CertGetCertificateContextProperty. The code here shows how to get the private key container name so you can generate the private key filename. * disclaimer - I'm pretty sure this is subject to change and may not even be complete, but am posting it in case it helps someone else in the future *

Structs and P/Invoke:

[StructLayout(LayoutKind.Sequential)]
public struct CryptKeyProviderInfo
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public String pwszContainerName;
    [MarshalAs(UnmanagedType.LPWStr)]
    public String pwszProvName;
    public uint dwProvType;
    public uint dwFlags;
    public uint cProvParam;
    public IntPtr rgProvParam;
    public uint dwKeySpec;
}

public const uint CERT_KEY_PROV_INFO_PROP_ID = 0x00000002;

[DllImport("crypt32.dll", SetLastError = true)]
internal extern static bool CertGetCertificateContextProperty(IntPtr pCertContext, uint dwPropId, IntPtr pvData, ref uint pcbData);

IntPtr providerInfo = IntPtr.Zero;
string containerName = string.Empty;
try
{

    //Win32 call w/IntPtr.Zero will get the size of our Cert_Key_Prov_Info_Prop_ID struct
    uint pcbProviderInfo = 0;
    if (!Win32.CertGetCertificateContextProperty(certificate.Handle, Win32.CERT_KEY_PROV_INFO_PROP_ID, IntPtr.Zero, ref pcbProviderInfo))
    {
        //if we can't get the certificate context, return string.empty
        return string.Empty;
    }

    //Allocate heap for Cert_Key_Prov_Info_Prop_ID struct
    providerInfo = Marshal.AllocHGlobal((int)pcbProviderInfo);

    //Request actual Cert_Key_Prov_Info_Prop_ID struct with populated data using our allocated heap
    if (Win32.CertGetCertificateContextProperty(certificate.Handle, Win32.CERT_KEY_PROV_INFO_PROP_ID, providerInfo, ref pcbProviderInfo))
    {
        //Cast returned pointer into managed structure so we can refer to it by it's structure layout
        Win32.CryptKeyProviderInfo keyInfo = (Win32.CryptKeyProviderInfo)Marshal.PtrToStructure(providerInfo, typeof(Win32.CryptKeyProviderInfo));

        //Get the container name
        containerName = keyInfo.pwszContainerName;
    }

    //Do clean-up immediately if possible
    if (providerInfo != IntPtr.Zero)
    {
        Marshal.FreeHGlobal(providerInfo);
        providerInfo = IntPtr.Zero;
    }
}
finally
{
    //Do clean-up on finalizer if an exception cause early terminiation of try - after alloc, before cleanup
    if (providerInfo != IntPtr.Zero)
        Marshal.FreeHGlobal(providerInfo);
}
Abie answered 7/3, 2012 at 23:21 Comment(2)
You may want to create an answer from the answer part and accept it so the question is marked as solved.Klump
Thanks @ivan_pozdeev, I've added my update as an answer below.Abie
A
6

Using the CertGetCertificateContextProperty above, I was able to solve this question. So it is possible to determine the file name of a certificate's private key on a remote computer using the steps mentioned in the update.

Abie answered 12/3, 2012 at 22:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.