Saturday 27 October 2012

Loading a Certificate from the Certificate Store via a Custom Configuration Section

I have recently been doing a fair amount of work with Windows Identity Foundation (WIF). In doing so I have had to load up certificates so in order to make the application flexible enough to deploy to different environments, use different certificates and follow certain standards I wanted to load the certificates from the Windows Certificate Store.

I knew that some of the other frameworks in the core libraries, such as WCF, load certificates out of the certificate stores via configuration so I wanted to emulate how they did that. After looking through some of the classes in the libraries and reflecting over the code and borrowing code generated when creating a WIF STS reference website I came up with the following that uses the CertificateReferenceElement.

interface IMySecurityConfiguration
{
    X509Certificate2 RequiredCertificate { get; } 
    X509Certificate2 OptionalCertificate { get; }
}

class MySecurityConfigurationSection : ConfigurationSection, IMySecurityConfiguration
{
    private const string ELEMENT_OPTIONALCERTIFICATE = "optionalCertificate";
    private const string ELEMENT_REQUIREDCERTIFICATE = "requiredCertificate";

    private ConfigurationPropertyCollection properties;

    protected override ConfigurationPropertyCollection Properties
    {
        get
        {
            return properties ?? (properties = new ConfigurationPropertyCollection
                                {
                                    new ConfigurationProperty(ELEMENT_REQUIREDCERTIFICATE,
                                                  typeof (CertificateReferenceElement), null,
                                                  ConfigurationPropertyOptions.IsRequired),
                                    new ConfigurationProperty(ELEMENT_OPTIONALCERTIFICATE,
                                                  typeof (CertificateReferenceElement), null,
                                                  ConfigurationPropertyOptions.None)
                                });
        }
    }

    private CertificateReferenceElement RequiredCertificateReference
    {
        get { return (CertificateReferenceElement) this[ELEMENT_REQUIREDCERTIFICATE]; }
    }

    private CertificateReferenceElement OptionalCertificateReference
    {
        get { return (CertificateReferenceElement) this[ELEMENT_OPTIONALCERTIFICATE]; }
    }

    public X509Certificate2 RequiredCertificate
    {
        get { return RequiredCertificateReference.LocateCertificate(); }
    }

    public X509Certificate2 OptionalCertificate
    {
        get
        {
            return OptionalCertificateReference.ElementInformation.IsPresent ?
                   OptionalCertificateReference.LocateCertificate() : null;
        }
    }
}

static class CertificateConfigurationExtensions
{
    public static X509Certificate2 LocateCertificate(this CertificateReferenceElement element)
    {
        return CertificateUtil.GetCertificate(element.StoreName, element.StoreLocation, element.X509FindType, element.FindValue, true);
    }
}

static class CertificateUtil
{
    public static X509Certificate2 GetCertificate(
        StoreName storeName, StoreLocation storeLocation, X509FindType findType, object findValue, bool throwIfMultipleOrNoMatch)
    {
        var certificateStore = new X509Store(storeName, storeLocation);
        X509Certificate2Collection certificates = null;
        try
        {
            certificateStore.Open(OpenFlags.ReadOnly);
            certificates = certificateStore.Certificates.Find(findType, findValue, false);
            if (certificates.Count == 1)
            {
                return new X509Certificate2(certificates[0]);
            }
            if (throwIfMultipleOrNoMatch)
            {
                if (certificates.Count == 0)
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "Cannot find certificate: StoreName = '{0}', StoreLocation = '{1}', FindType = '{2}', FindValue - '{3}'",
                            storeName, storeLocation, findType, findValue));
                }
                else
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "Found multiple certificates for:  StoreName = '{0}', StoreLocation = '{1}', FindType = '{2}', FindValue - '{3}'",
                            storeName, storeLocation, findType, findValue));
                }
            }
            else return null;
        }
        finally
        {
            if (certificates != null)
            {
                foreach (X509Certificate2 certificate in certificates)
                {
                    certificate.Reset();
                }
            }
            certificateStore.Close();
        }
    }
}

Using the configuration section

Then you would load up the config as follows:

<mysecurityconfiguration>
    <requiredcertificate findvalue="CN=foo" />
</mysecurityconfiguration>

For a full list of the properties have a look at the certificateReference element.

A note about loading a Private Key from the Certificate Store

In order to load a certificate with a private key you need to give the user the application is going to run as permission to load the private key. For more details see this blog post How to give IIS access to private keys.

No comments:

Post a Comment