Wednesday, 11 February 2009

Reading X509 Certificates from remote machines

Today, I got to write some code to read X509 certificates remotely from machines on local network. The requirement was to read certificates installed in certificate stores on remote machines and verify whether or not they have expired.

It may sound trivial but I found that it was not possible to do it using classes in the System.Security.Cryptography.X509Certificates namespace. For some reason, Microsoft have not included support to read remote certificate stores from the X509Store class, which otherwise makes working with X509 really easy.


Reading Certificates from local machine
If you want to read certificates locally on your machine, the X509Store class is very useful and you can read and manipulate certificates with minimal code, as shown below:

X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
try
{
store.Open(OpenFlags.ReadOnly);
}
catch (Exception ex)
{
throw new Exception("Error opening certificate store", ex);
}
foreach (X509Certificate cert in store.Certificates)
{
/// do whatever you want to read with certificate here
}


The X509Store can open with OpenFlags.Readonly and OpenFlags.ReadWrite option. The StoreLocation can be StoreLocation.LocalMachine or StoreLocation.CurrentUser and the StoreName can be any of the 8 different options.
(StoreName.My is equivalent to Personal certificate store that you will see when you open Certificates from MMC Console)



Reading Certificates from remote machine

One way to do it is to use directory services and find out the certificates of remote machine using DirectorySearcher and DirectoryEntry class in the System.DirectoryServices namespaces. This is described in the blogpost Get X509Certificate2 from a LDAP Server or Remote Machine. I found it a bit of overkill.

Another way to do it is to use the old Microsoft CAPICOM COM APIs. This requires that the CAPICOM ActiveX controls be installed along with the application and the CAPICOM APIs be p/invoked from your managed code. The most obvious downside of this is that the whole CAPICOM SDK would needs to be installed and distributed with your application.

The best option that I found was to use the ever reliable CertOpenStore windows API. The API is very powerful and if you have some experience with Windows programming and using P/Invoke to call windows API, the code would be most simple and straightforward for you to work with. Of course, the best practice is to encapsulate it in a class and make the P/Invoke hidden from the rest of the application

The following class would do it for you. For illustration purposes, I have used the classes from the System.Security.Cryptography.X509Certificates namespace. You can do the same using only the APIs and not given computer name at all in the last parameter.


using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;

public enum CertStoreName
{
MY,
ROOT,
TRUST,
CA
}
public class CertStoreReader
{

#region P/Invoke Interop
private static int CERT_STORE_PROV_SYSTEM = 10;
private static int CERT_SYSTEM_STORE_CURRENT_USER = (1 << 16);
private static int CERT_SYSTEM_STORE_LOCAL_MACHINE = (2 << 16);

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

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

[DllImport("CRYPT32", EntryPoint = "CertCloseStore", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CertCloseStore(IntPtr storeProvider, int flags);

#endregion


public string ComputerName { get; set; }


private readonly bool isLocalMachine;
public CertStoreReader(string machineName)
{
ComputerName = machineName;
if (machineName == string.Empty)
{
isLocalMachine = true;
}
else
{
isLocalMachine = string.Compare(ComputerName, Environment.MachineName, true) == 0 ? true : false;
}
}

public X509Certificate2Collection GetCertificates(CertStoreName storeName)
{
X509Certificate2Collection collectionToReturn = null;
string givenStoreName = GetStoreName(storeName);

if (givenStoreName == string.Empty)
{
throw new Exception("Invalid Store Name");
}

if (isLocalMachine)
{
X509Store store = new X509Store(givenStoreName, StoreLocation.LocalMachine);
try
{
store.Open(OpenFlags.ReadOnly);
}
catch (Exception ex)
{
throw new Exception("Error opening certificate store", ex);
}
collectionToReturn = store.Certificates;
}
else
{
try
{
IntPtr storeHandle = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_LOCAL_MACHINE, string.Format(@"\\{0}\{1}", ComputerName, givenStoreName));
if (storeHandle == IntPtr.Zero)
{
throw new Exception(string.Format("Cannot connect to remote machine: {0}", ComputerName));
}

IntPtr currentCertContext = IntPtr.Zero;
collectionToReturn = new X509Certificate2Collection();
do
{
currentCertContext = CertEnumCertificatesInStore(storeHandle, currentCertContext);
if (currentCertContext != IntPtr.Zero)
{
collectionToReturn.Add(new X509Certificate2(currentCertContext));
}
}
while (currentCertContext != (IntPtr)0);

CertCloseStore(storeHandle, 0);
}
catch (Exception ex)
{
throw new Exception("Error opening Certificate Store", ex);
}
}

return collectionToReturn;
}

private static string GetStoreName(CertStoreName certStoreName)
{
string storeName = string.Empty;
switch (certStoreName)
{
case CertStoreName.MY:
storeName = "My";
break;

case CertStoreName.ROOT:
storeName = "Root";
break;

case CertStoreName.CA:
storeName = "CA";
break;

case CertStoreName.TRUST:
storeName = "Trust";
break;
}
return storeName;
}
}

10 comments:

Trashkid2000 said...

Very great Article! Thank you very much for written down this.

One thing, the declatation in the Api- Function isnot correct. It must been:

private static int CERT_SYSTEM_STORE_CURRENT_USER = (1 << 16);

private static int CERT_SYSTEM_STORE_LOCAL_MACHINE = (2 << 16);

Marko

Hamid said...

Thanks Thrashkid2000!
I must have introduce the error when I reformatted the post. It is corrected now.

-Hamid

Lou said...

This is great, but I am trying to connect to a remote MS Certificate Server and I am not able to pass in the certificate store? Any ideas how I can collect the certificates that have issued on my own CA?

Gregor said...

Hi,

how can i read certificate for specified user on remote computer.

Thanx for help,

Gregor

Steve said...

can you create a p/invoke example for installing a certificate on a remote machine

Omair said...

Quite similarly i need to code an installation of certificate in to Windows CE hand held device. It will not be a remote installation rather an application residing inside the hand held will execute. Can you be of some help in this re ? Thanks in advance.

Fábio said...

Hello Hamid,

Could disponibiliozar the project?

Thanks,

Tech Geek said...

Using "System.Security.Cryptography.X509Certificates" namespace itself, we can open certificate store of a remote machine as below:

X509Store store = new X509Store(@"\\RemoteMachineName\My", StoreLocation.LocalMachine);


Check the blog - http://blogs.technet.com/b/heyscriptingguy/archive/2011/02/16/use-powershell-and-net-to-find-expired-certificates.aspx

Fábio said...

Hello,

Convert to vb.net, but I do not know how to call?

thank you

C# Dev said...

CertEnumCertificatesInStore function returns zero value to currentCertContext. What could be the reason..? I am trying to access USB token certificate connected to remote server...