Windows Communication Foundation (WCF) provides a relatively simple way to implement Certificate-Based Mutual Authentication on distributed clients and services. Additionally, it supports interoperability as it is based on WS-Security and X.509 certificate standards. This blog post briefly summarises mutual authentication and covers the steps to implement it with an IIS hosted WCF service.

Even though WCF’s out-of-the-box functionality removes much of the complexity of Certificate-Based Mutual Authentication in many scenarios, there are cases in which this is not what we need. For example, by default, WCF relies on the Windows Certificate Store for accessing the own private key and the counterpart’s public key when implementing Certificate-Based Mutual Authentication.

Having said so, there are scenarios in which using the Windows Certificate Store is not an option. It can be a deployment restriction or a platform limitation. For example, what if you want to create an Azure WebJob which calls a SOAP Web Service using Certificate-Based Mutual Authentication? (At the time of writing this post) there is no way to store a certificate containing the counterpart’s public key in the underlying certificate store for an Azure WebJob. And just because of that, we cannot enjoy all the built-in benefits of WCF for building our client.

Here, they explain how to create a WCF service that implements custom certificate validation be defining a class derived from X509CertificateValidator and implementing an abstract “Validate” override method. Once defined the derived class, the CertificateValidationMode has to be set to “Custom” and the CustomCertificateValidatorType to be set to the derived class’ type. This can easily be extended to implement mutual authentication on the service side without using the Windows Certificate Store.

My purpose in this post is to describe how to implement a WCF client with Certificate-Based Mutual Authentication without using Windows Certificate Store by compiling the required sources and filling the gaps of the available documentation.

What to consider

Before we start thinking about coding, we need to consider the following:

  • The WCF client must have access to the client’s private key to be able to authenticate with the service.
  • The WCF client must have access to the service’s public key to authenticate the service.
  • Optionally, the WCF client should have access to the service’s certificate issuer’s certificate (Certificate Authority public key) to validate the service’s certificate chain.
  • The WCF client must implement a custom service’s certificate validation, as it cannot rely on the built-in validation.
  • We want to do this, without using the Windows Certificate Store.

Accessing public and private keys without using Windows Certificate Store

First we need to access the client’s private key. This can be achieved without any problem. We could get it from a local or a shared folder, or from a binary resource. For the purpose of this blog, I will be reading it from a local Personal Information Exchange (pfx) file. For reading a pfx file we need to specify a password; thus you might want to consider encrypting or implementing additional security. There are various X509Certificate2 constructor overloads which allow you to load a certificate in different ways. Furthermore, reading a public key is easier, as it does not require a password.

Implementing a custom validator method

On the other hand, implementing the custom validator requires a bit more thought and documentation is not very detailed. The ServicePointManager
class
has a property called “ServerCertificateValidationCallback” of type RemoteCertificateValidationCallback which allows you to specify a custom service certificate validation method. Here is defined the contract for the delegate method.

In order to authenticate the service, once we get its public key, we could do the following:

  • Compare the service certificate against a preconfigured authorised service certificate. They must be the same.
  • Validate that the certificate is not expired.
  • Optionally, validate that the certificate has not been revoked by the issuer (Certificate Authority). This does not apply for self-signed certificates.
  • Validate the certificate chain, using a preconfigured trusted Certificate Authority.

For comparing the received certificate and the preconfigured one we will use the X509Certificate.Equals Method. For validating that the certificate has not expired and not been revoked we will use the X509Chain.Build Method. And finally, to validate that the certificate has been issued by the preconfigured trusted CA, we will make use of the X509Chain.ChainElements Property.

Let’s jump into the code.

To illustrate how to implement the WCF client, what can be better than code itself J? I have implemented the WCF client as a Console Application. Please pay attention to all the comments when reading my code. With the provided background, I hope it is clear and self-explanatory.

using System;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace MutualAuthClient
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("Starting...");

                // Set the ServerCertificateValidationCallback property to a
                // custom method.
                ServicePointManager.ServerCertificateValidationCallback +=
                                                CustomServiceCertificateValidation;

                // We will call a service which expects a string and echoes it
                // as a response.
                var client = new EchoService.EchoServiceClient
                                            ("BasicHttpBinding_IEchoService");

                // Load private key from PFX file.
                // Reading from a PFX file requires specifying the password.
                // You might want to consider adding encryption here.
                Console.WriteLine("Loading Client Certificate (Private Key) from File: "
                                    + ConfigurationManager.AppSettings["ClientPFX"]);
                client.ClientCredentials.ClientCertificate.Certificate =
                                    new X509Certificate2(
                                    ConfigurationManager.AppSettings["ClientPFX"],
                                    ConfigurationManager.AppSettings["ClientPFXPassword"],
                                    X509KeyStorageFlags.MachineKeySet);

                // We are using a custom method for the Server Certificate Validation
                client.ClientCredentials.ServiceCertificate.Authentication.
                                CertificateValidationMode =
                                        X509CertificateValidationMode.None;

                Console.WriteLine();
                Console.WriteLine(String.Format("About to call client.Echo"));
                string response = client.Echo("Test");
                Console.WriteLine();
                Console.WriteLine(String.Format("client.Echo Response: '{0}'", response));
                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine(
                    String.Format("Exception occurred{0}Message:{1}{2}Inner Exception: {3}"
                                   , Environment.NewLine, ex.Message, Environment.NewLine,
                                   ex.InnerException));
            }

        }

        private static bool CustomServiceCertificateValidation(
                object sender, X509Certificate cert, X509Chain chain,
                SslPolicyErrors error)
        {
            Console.WriteLine();
            Console.WriteLine("CustomServiceCertificateValidation has started");

            // Load the authorised and expected service certificate (public key)
            // from file.
            Console.WriteLine("Loading Service Certificate (Public Key) from File: "
                                + ConfigurationManager.AppSettings["ServicePublicKey"]);
            X509Certificate2 authorisedServiceCertificate = new X509Certificate2
                    (ConfigurationManager.AppSettings["ServicePublicKey"]);

            // Load the trusted CA (public key) from file.
            Console.WriteLine("Loading the Trusted CA (Public Key) from File: "
                                + ConfigurationManager.AppSettings["TrustedCAPublicKey"]);
            X509Certificate2 trustedCertificateAuthority = new X509Certificate2
                    (ConfigurationManager.AppSettings["TrustedCAPublicKey"]);

            // Load the received certificate from the service (input parameter) as
            // an X509Certificate2
            X509Certificate2 serviceCert = new X509Certificate2(cert);

            // Compare the received service certificate against the configured
            // authorised service certificate.
            if (!authorisedServiceCertificate.Equals(serviceCert))
            {
                // If they are not the same, throw an exception.
                throw new SecurityTokenValidationException(String.Format(
                    "Service certificate '{0}' does not match that authorised '{1}'"
                    , serviceCert.Thumbprint, authorisedServiceCertificate.Thumbprint));
            }
            else
            {
                Console.WriteLine(String.Format(
                    "Service certificate '{0}' matches the authorised certificate '{1}'."
                    , serviceCert.Thumbprint, authorisedServiceCertificate.Thumbprint));
            }

            // Create a new X509Chain to validate the received service certificate using
            // the trusted CA
            X509Chain chainToValidate = new X509Chain();

            // When working with Self-Signed certificates,
            // there is no need to check revocation.
            // You might want to change this when working with
            // a properly signed certificate.
            chainToValidate.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
            chainToValidate.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
            chainToValidate.ChainPolicy.VerificationFlags =
                                    X509VerificationFlags.AllowUnknownCertificateAuthority;

            chainToValidate.ChainPolicy.VerificationTime = DateTime.Now;
            chainToValidate.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);

            // Add the configured authorised Certificate Authority to the chain.
            chainToValidate.ChainPolicy.ExtraStore.Add(trustedCertificateAuthority);

            // Validate the received service certificate using the trusted CA
            bool isChainValid = chainToValidate.Build(serviceCert);

            if (!isChainValid)
            {
                // If the certificate chain is not valid, get all returned errors.
                string[] errors = chainToValidate.ChainStatus
                    .Select(x => String.Format("{0} ({1})", x.StatusInformation.Trim(),
                            x.Status))
                    .ToArray();
                string serviceCertChainErrors = "No detailed errors are available.";

                if (errors != null && errors.Length > 0)
                    serviceCertChainErrors = String.Join(", ", errors);

                throw new SecurityTokenValidationException(String.Format(
                        "The chain of service certificate '{0}' is not valid. Errors: {1}",
                        serviceCert.Thumbprint, serviceCertChainErrors));
            }

            // Validate that the Service Certificate Chain Root matches the Trusted CA.
            if (!chainToValidate.ChainElements
                .Cast<X509ChainElement>()
                .Any(x => x.Certificate.Thumbprint ==
                                    trustedCertificateAuthority.Thumbprint))
            {
                throw new SecurityTokenValidationException(String.Format(
                        "The chain of Service Certificate '{0}' is not valid. " +
                        " Service Certificate Authority Thumbprint does not match " +
                        "Trusted CA's Thumbprint '{1}'",
                        serviceCert.Thumbprint, trustedCertificateAuthority.Thumbprint));
            }
            else
            {
                Console.WriteLine(String.Format(
                    "Service Certificate Authority '{0}' matches the Trusted CA's '{1}'",
                    serviceCert.IssuerName.Name,
                    trustedCertificateAuthority.SubjectName.Name));
            }
            return true;
        }
    }
}
 


And here is the App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="ClientPFX" value="certificates\ClientPFX.pfx" />
<add key="ClientPFXPassword" value="********" />
<add key="TrustedCAPublicKey" value="certificates\ServiceCAPublicKey.cer" />
<add key="ServicePublicKey" value="certificates\ServicePublicKey.cer" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IEchoService">
<security mode="Transport">
<transport clientCredentialType="Certificate" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="https://server/EchoService.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IEchoService"
contract="EchoService.IEchoService" name="BasicHttpBinding_IEchoService" />
</client>
</system.serviceModel>
</configuration>


In case you find difficult to read my code from WordPress, you can read it from GitHub on the links below:

I hope you have found this post useful, allowing you to implement a WCF client with Mutual Authentication without relying on the Certificate Store, and making your coding easier and happier! : )

Category:
Application Development and Integration, Security
Tags:
, , , ,

Join the conversation! 4 Comments

  1. This post is a god send, thanks for doing it. You explain the problem in a way that is utterly clear (I couldn’t even find a good explanation for the problem anywhere else) and you put the code in there to solve it.

    THANK YOU!

    Reply
  2. Didn’t work for me though 😥 I was so sure it would…
    The request seems to be caught by an IIS layer before it reaches my code.

    Reply
  3. Hi Jorge, thanks for your comments and feedback. What is your requirement? This post is about implementing a WCF client. IIS (or other web server) would be at the other end. If there is an IIS, wouldn’t it be better to make use of the Certificate Store?

    Reply
  4. Reblogged this on Sprouting Bits and commented:

    Reblogging this post to my personal archive 🙂

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: