Integration Testing With Certificates

In my post about integration testing of WCF services, I hinted that one compelling reason to perform integration testing of services would be to test authorization managers or message interceptors. When working with message security, certificates tend to be ubiquitous, so you need some automated way to set up certificates for a test suite. Keep in mind that my general principles for integration testing should still apply, which in this case means that any certificates should be installed and configured before the test, and removed again afterwards.

If you look at the WCF samples in the SDK, many of these include a batch file which sets up the certificates. These typically use makecert.exe to create the certificates. Obviously, you could use System.Diagnostics.Process to execute a batch script or makecert.exe, but I prefer not to do this if there's a managed alternative, as managing separate processes tends to be a tad brittle.

Although I have yet to locate a good managed alternative to makecert.exe, it's possible to add existing certificate files to your certificate stores using managed code. As such, the following process can be used to set up certificates in an integration test:

  1. Create the necessary certificates using makecert. The certificates should be exported to .pfx files that can be added to the source control system of your choice. Creating the certificates is a manual process, but is only performed once as part of the development effort to create the integration test.
  2. Before each test is executed, the certificates are automatically installed into their respective certificate stores.
  3. After the test suite has completed, the certificates are automatically removed from the certificate stores.

In the rest of this post, I will describe this process in further detail.

The first step is to create all necessary certificates. For a simple sample application, let's say that you need a certificate for the server, and one for the client. Test certificates can be created with the makecert command line tool:

makecert -sr LocalMachine -ss My -a sha1 -n CN=MyServer -sky exchange -pe
makecert -sr LocalMachine -ss My -a sha1 -n CN=MyClient -sky exchange -pe

These commands create the certificates in the My (aka Personal) folder in the local machine store. The -pe switch is important, since it creates an exportable private key, which we'll need later on. It is also important to create the certificate directly into a certificate store, as this creates and persists the private key. Makecert can also create a certificate directly to a .cer file, but that file will not contain the private key, which will not do for our purposes (remember that the goal of this step is to create a certificate file which can be deployed as part of an automated test, so it needs to contain all necessary information, including the private key).

The test certificates now reside in your local certificate store, so you need to export them (including their private keys) to a file. To do this, open an MMC console with the certificates snap-in (e.g. open a new MMC console and add the Certificates snap-in for the local machine), and expand the Personal folder. You should see the MyServer and MyClient certificates. To export a certificate, right-click it and select All Tasks and Export. Select Yes, export the private key and create a .pfx file. The .pfx files contain both the public and private keys, and can be used for integration testing. These files are part of the integration test project and must be deployed as part of the test (so remember to add them as deployment items to the test configuration).

As I wrote above, this manual step must be performed once. The next step is to write the code which will configure (and remove) the certificates for the test run. Initialization and clean-up code for the test suite should look like this:

 private static CertificateManager certificateManager_;
  
 [ClassInitialize]
 public static void InitializeClass(TestContext ctx)
 {
     MyServiceTest.certificateManager_ = 
         MyServiceTest.CreateCertificateManager();
     MyServiceTest.certificateManager_.RemoveCertificates();
     MyServiceTest.certificateManager_.InstallCertificates();
 }
  
 [ClassCleanup]
 public static void CleanupClass()
     MyServiceTest.certificateManager_.RemoveCertificates();
 }
  
 private static CertificateManager CreateCertificateManager()
{
    List<CertificateInformation> certificates =

        new List<CertificateInformation>();

    certificates.Add(new CertificateInformation(

        "MyServer.pfx", "CN=MyServer", "ploeh", 

        StoreName.My, StoreLocation.LocalMachine));

    certificates.Add(new CertificateInformation(

        "MyClient.pfx", "CN=MyClient", "ploeh",

        StoreName.My, StoreLocation.LocalMachine));

    certificates.Add(new CertificateInformation(

        "MyServer.pfx", "CN=MyServer", "ploeh",

        StoreName.TrustedPeople, StoreLocation.LocalMachine));

    certificates.Add(new CertificateInformation(

        "MyClient.pfx", "CN=MyClient", "ploeh",

        StoreName.TrustedPeople, StoreLocation.LocalMachine));

    return new CertificateManager(certificates);
}

Obviously, the key lies in the implementation of CertificateManager, so let's take a look at that. It's initialized with a list of CertificateInformation objects, which holds the information about each certificate: The file name of the certificate file, the common name of the certificate, the password to the private key and the location where it should be placed. The InstallCertificates and RemoveCertificates methods simply loop through all items in this list and performs the requested action.

Here's how to install a certificate:

 private static void InstallCertificate(CertificateInformation ci)
 {
     X509Certificate2 certificate = new X509Certificate2(
         ci.FilePath, ci.Password,
         X509KeyStorageFlags.PersistKeySet);
  
     X509Store store = new X509Store(ci.StoreName, ci.StoreLocation);
     try
     {
         store.Open(OpenFlags.ReadWrite | OpenFlags.OpenExistingOnly);
         store.Add(certificate);
     }
     finally
     {
         store.Close();
     }
 }

The most important thing to notice here is the use of the PersistKeySet flag in the X509Certificate2 constructor - without it (and the private key password), the private key isn't going to be imported into the certificate store.

Removing the certificates is almost as easy:

 private static void RemoveCertificate(CertificateInformation ci)
 {
     X509Store store = new X509Store(ci.StoreName, ci.StoreLocation);
     try
     {
         store.Open(OpenFlags.ReadWrite | OpenFlags.OpenExistingOnly);
         List<X509Certificate2> cerfificatesToDelete =
             new List<X509Certificate2>();
         foreach (X509Certificate2 c in store.Certificates)
         {
             if (c.Subject == ci.Subject)
             {
                 cerfificatesToDelete.Add(c);
             }
         }
         foreach (X509Certificate2 c in cerfificatesToDelete)
         {
             store.Remove(c);
         }
     }
     finally
     {
         store.Close();
     }
 }

The main caveat here is that all certificates with a matching subject will be deleted, and since you can have more than one certificate with the same subject in the same store, you could accidentally remove too many certificates. If this is ever an issue, you could replace the subject comparison with a comparison of the certificates' thumbprints.

Automatically deploying certificates can be valuable when integration testing WCF services, because you can just xcopy all of your unit test code, test configurations and certificate files to a new machine, compile the project and execute the tests without performing any initial configuration.

As always, when automating configuration steps which involves operating system resources, you must have the rights to perform these actions, and running as administrator is the easiest way to do this.