Mutual authentication with a IIS hosted WCF data service installed in a workgroup environment

This post covers the steps required to secure communication between a WCF client and a WCF data service using mutual certificate authentication.

The client/service topology is depicted below:

image

Both the client and server run on a Windows Server 2008 R2 virtual machine with Windows SDK 7.1, Visual Studio 2010, SQL Server Express 2008 R2 and .NET framework 4.0.

Mutual authentication explained

A detailed discussion of public key cryptography is outside the scope of this article. However, I highly recommend that you read more here to get a better understanding of the underlying principles, so that you can better understand the ‘How-To’ steps that follow.

In essence the client possesses a public key certificate (hereafter known as a client certificate) with associated private key. Similarly, the service has possession of another public key certificate (hereafter known as a service certificate) with associated private key. For a given pair of keys, either the public or private key can be used to encrypt data but ONLY the corresponding key can decrypt the data. The public key is available to the world at large but the private key MUST remain with the owner alone. In this way the owner is able to encrypt data using the private key and a party in receipt of encrypted data can decrypt it using the public key. The fact that the receiving party can successfully decrypt the data using the public key means that the data MUST have been encrypted by the corresponding private key and, as only the owner has access to the private key, the data MUST have originated from the owner; this is known as nonrepudiation as the owner cannot claim that encrypted data did not originate from them (of course, the private key may have been stolen but that is a different matter).

Translating this to the current scenario, in order for the client and service to successfully converse using certificates, the WCF client application must have access to the client’s private key and IIS (the receiving party) must have access to the client’s public key. Conversely, IIS must have access to the service private key and the client must have access to the service public key.

In an Active Directory domain, the public key certificate is ordinarily assigned to a domain account such that IIS can locate the certificate using Active Directory Domain Services (ADDS). However, ADDS does not exist in a workgroup environment and so the client certificate public key list must be maintained within IIS itself.

Methodology

 

1) Create a trusted root certificate

The certificates in this article are issued by a certificate authority, in this case a trusted root certificate created using the makecert tool that ships with the Windows SDK.

  • Create a folder somewhere on the file system and, in a command window, change the directory to match the newly created folder  

  • Run the following command to create a root certificate (do not define a password)

    • "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\makecert" -r -pe -n "CN=Test Root Authority" -ss CA -sr CurrentUser -a sha1 -sky signature -cy authority -sv TestRoot.pvk TestRoot.cer

     

2) Install the client certificate with private key

The client application, as owner of the client certificate, needs access to the private key at runtime. We are going to create an interactive client (e.g. – Console, WinForm, Silverlight, Internet Explorer etc.), and so the certificate will be installed in the Current User – Personal store. If the client was non-interactive (e.g. – NT service) the certificate would be installed in the Local Machine – Personal store.

Additionally, as the client certificate has been issued from a trusted certificate authority (CA), the root certificate created in 1) must also be installed.

  • From the folder where TestRoot.cer is located, create and install a client certificate by running the following in a command window
    • "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\makecert" -n "CN=Test Client" -ic "TestRoot.cer" -iv "TestRoot.pvk" -a sha1 -sky exchange -pe -sr currentuser -ss my "TestClient.cer"
  • Open a snap-in window
    • Start – Run – mmc
    • Add/Remove snap-in
    • Ctrl-M
    • Add a Certificates snap-in for ‘My user account’
  • Expand the (Trusted Root Certification Authorities)/Certificates node
  • Right click the Certificates folder and choose All Tasks – Import
  • Browse to TestRoot.cer
  • Click through the remaining windows and finish

3) Install the service certificate with private key

The service, as owner of a service certificate, needs access to the private key at runtime. As the service is non-interactive (i.e. – it is not running under the context of a logged-on user), the certificate will be installed in the Local Machine – Personal store.

Additionally, as the service certificate has been issued from a trusted certificate authority (CA), the root certificate for the CA must also be installed so that the chain of trust can be verified.

  • From the folder where TestRoot.cer is located, install the service certificate by running the following in a command window
    • "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\makecert" -n "CN=www.mydataservice.com" -ic "TestRoot.cer" -iv "TestRoot.pvk" -a sha1 -sky exchange -eku 1.3.6.1.5.5.7.3.1 -pe -sr LocalMachine -ss my TestDataService.cer
  • Open a snap-in window
    • Start – Run – mmc
    • Add/Remove snap-in
    • Ctrl-M
    • Add a Certificates snap-in for ‘Computer account’
  • Expand the (Trusted Root Certification Authorities)/Certificates node
  • Right click the Certificates folder and choose All Tasks – Import
  • Browse to TestRoot.cer
  • Click through the remaining windows and finish

4) Export the client public key certificate

IIS needs access to the client’s public key certificate. In this section the certificate is exported for later use.

  • Open a snap-in window (or use the same window as 2))
    • Start – Run – mmc
    • Add/Remove snap-in
    • Ctrl-M
    • Add a Certificates snap-in for ‘My user account’
  • Expand the (Certificates – Current User)/Personal/Certificates node
  • Right click the Test Client certificate and choose All Tasks – Export (Do not export the primary key)
    • N.B. - Exporting the private key will not cause problems but remember that the private key is for the client ALONE
  • Save the certificate as TestClient_Public.cer (Base-64 encoded X.509)

5) Export the service public key certificate

The WCF client needs access to the service public key certificate. In this section the certificate is exported for later use.

  • Open a snap-in window (or use the same window as 3))

    • Start – Run – mmc
    • Add/Remove snap-in
    • Ctrl-M
    • Add a Certificates snap-in for ‘Computer account’
  • Expand the (Certificates – Local Computer)/Personal/Certificates node

  • Right click the www.testdataservice.com certificate and choose All Tasks – Export (Do not export the primary key)

    • Exporting the private key will not cause problems but remember that the private key is for the service ALONE
  • Save the certificate as TestDataService_Public.cer (Base-64 encoded X.509)

 

6) Create a WCF data service

In this section a basic WCF data service is created in order to secure it using mutual authentication.

  • In Visual Studio 2010, create a new empty solution called MyDataServices

image

  • Add a new ASP.NET empty web application called MyDataService

image

  • Add a new empty ADO.NET Entity Data Model item to MyDataService called MyDataServiceModel.edmx (empty model)

image

  • Add a new WCF Data Service item to MyDataService called MyDataServiceImp.svc

image

  • Open the code behind file for MyDataServiceImp.svc and add MyDataServiceModelContainer into the class definition

image 

  • Open MyDataServiceModel.edmx
  • Right click anywhere on the screen and choose to Generate Database from Model
  • Create a new data connection to SQL Server if one does not already exist
  • Build the solution

7) Host the data service in IIS

  • Open IIS manager
  • Start – Administrative Tools – Internet Information Services (IIS) Manager
  • Create a new website called ‘My Data Service’
  • Point the physical path to the data service created in 6)
  • Choose a binding type of https and select the www.mydataservice.com SSL certificate

image

  • Modify the app pool under which the website is running so that the .NET framework version is v4.0.30319

image

  • Add a hosts file entry for ‘127.0.0.1 www.mydataservice.com’
  • Via Windows Explorer, give ‘Everyone’ full control of the MyDataService folder
    • N.B. – this is a shortcut for keeping the blog post succinct. Such permissions should not be an option for a production deployment

image

 

8) Create a WCF client

In order to make life a little easier, we will create and configure the WCF client before locking down the service.

  • Add a new Console Application project called MyDataServiceClient to MyDataServices
  • Right click the project and choose to ‘Add Service Reference’
  • Enter an address of https://www.mydataservice.com/mydataserviceimp.svc
  • Click Go
  • Once the service has been discovered give it the namespace MyDataServiceEntities

9) Enable client certificate authentication in IIS

Both IIS and the WCF data service must be configured for client certificate authentication, otherwise there is a mismatch and a service activation failure similar to ‘The SSL settings for the service 'SslRequireCert' does not match those of the IIS 'Ssl' ’ will occur.

  • Open IIS manager
    • Start – Administrative Tools – Internet Information Services (IIS) Manager
  • Highlight the WCF data service website and double click ‘SSL Settings’

image

  • Configure IIS as below

image

 

10) Register the client public key certificate with the service

In this section the public key certificate from 4) is registered with the data service website.

Client certificates can be mapped in a ‘one to one’ or ‘many to one’ relationship with Windows accounts. ‘one to one’ means that a client certificate is linked to a single Windows account. ‘many to one’ means that many client certificates are assigned to a single Windows account.

In this post we will use the ‘one to one’ relationship but the ‘many to one’ relationship does not differ greatly

  • Highlight the MyDataService website and double click ‘Configuration Editor’

image

  • Choose the ‘oneToOneMappings’ section

image

  • Click Add
  • Set the entry as ‘enabled’ = true
  • Set the username with associated password as that of the local administrator
  • In production this would most likely be a service account with reduced privileges
  • Right click the TestClient_Public.cer file from 4) and choose to edit in Notepad
  • Ensure that the block between, but not including, ‘-----BEGIN CERTIFICATE-----‘ and ‘-----END CERTIFICATE-----‘, is all on a single line (it is not by default)
  • Copy and paste the block into the ‘certificate’ field so that the resulting entry is as below

image

  • Close the window
  • Click Apply

11) Mandate client certificate authentication for the data service

The data service must also be configured to mandate client certificate authentication.

In web.config, overwrite the <system.serviceModel> element with the following

Code Snippet

  1. <system.serviceModel>
  2.   <bindings>
  3.     <webHttpBinding>
  4.       <binding name="mutual">
  5.         <security mode="Transport">
  6.           <transport clientCredentialType="Certificate" />
  7.         </security>
  8.       </binding>
  9.     </webHttpBinding>
  10.   </bindings>
  11.   <behaviors>
  12.     <endpointBehaviors>
  13.       <behavior name="mutual">
  14.         <webHttp/>
  15.       </behavior>
  16.     </endpointBehaviors>
  17.   </behaviors>
  18.   <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
  19.   <services>
  20.     <service name="MyDataService.MyDataServiceImp">
  21.       <endpoint contract="System.Data.Services.IRequestHandler"
  22.                 binding="webHttpBinding"
  23.                 bindingConfiguration="mutual"
  24.                 behaviorConfiguration="mutual">
  25.       </endpoint>
  26.     </service>
  27.   </services>
  28. </system.serviceModel>

The contents of the <webHttpBinding> element mandate that client certificate authentication at the transport level is required for the binding. The <services>/<service>/<endpoint> element describes the contract that all WCF data services must adhere to.

12) Register the service public key certificate with the client

As this article is concerned with mutual authentication then it is not sufficient for only the service to authenticate the client. In addition, the client must be certain that it is calling the right endpoint. One way to achieve this is to store the service public key certificate with the client so that during runtime, the SSL certificate presented by the service can be compared against the locally stored version. In this way the client can be sure that it is calling the correct SSL endpoint located at www.mydataservice.com and that it has not been phished into calling another endpoint with the same URL but different SSL certificate.

  • Open a snap-in window
    • Start – Run – mmc
    • Add/Remove snap-in
    • Ctrl-M
    • Add a Certificates snap-in for ‘My user account’
  • Expand the (Certificates – Current User)/Personal/Certificates node
  • Right click the Certificates folder and choose All Tasks – Import
  • Browse to TestDataService_Public.cer that was created in 6)
  • Click through the remaining windows and finish

13) Update the WCF client to verify the service certificate

As the service is now locked down, the client will not be able to use it without undergoing some modification.

  • Overwrite Program.cs with the following

Code Snippet

  1. namespace MyDataServiceClient
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Net;
  6.     using System.Net.Security;
  7.     using System.Security.Cryptography.X509Certificates;
  8.  
  9.     class Program
  10.     {
  11.         private static MyDataServiceEntities.MyDataServiceModelContainer entities = new MyDataServiceEntities.MyDataServiceModelContainer(new Uri(@"https://www.mydataservice.com/mydataserviceimp.svc"));
  12.  
  13.         static void Main(string[] args)
  14.         {
  15.             X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
  16.  
  17.             store.Open(OpenFlags.ReadOnly);
  18.  
  19.             X509Certificate2Collection certColl = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, "CN=Test Client", false);
  20.  
  21.             entities.ClientCertificate = certColl[0];
  22.  
  23.             store.Close();
  24.  
  25.             ServicePointManager.ServerCertificateValidationCallback +=
  26.                 delegate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslError)
  27.                 {
  28.                     // Validate the certificate
  29.                     store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
  30.  
  31.                     store.Open(OpenFlags.ReadOnly);
  32.  
  33.                     X509Certificate2Collection serverCertColl = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, "CN=www.mydataservice.com", false);
  34.  
  35.                     X509Certificate2 serverCert = serverCertColl[0];
  36.  
  37.                     store.Close();
  38.  
  39.                     // Make this as rigorous as necessary
  40.                     return serverCert.Thumbprint == (new X509Certificate2(cert)).Thumbprint;
  41.                 };
  42.  
  43.             // Send the query to the data service context
  44.             IEnumerable<object> result = entities.Execute<object>(entities.BaseUri);
  45.  
  46.             Console.WriteLine();
  47.             Console.WriteLine("**********");
  48.             Console.WriteLine("Success!!!");
  49.             Console.WriteLine("**********");
  50.  
  51.             Console.ReadKey();
  52.         }
  53.     }
  54. }
 Note that the ClientCertificate property of the proxy is not recognised; this represents the last piece of the puzzle. As there is no declarative configuration for the WCF client (i.e. - no <system.serviceModel> element was created in the client config file, unlike for, say, a standard SOAP based web service), the client certificate is hooked in programmatically by intercepting the underlying HttpWebRequest instance.
  •  Overwrite Reference.cs contained within the MyDataServiceEntities service reference, with the following
    

Code Snippet

  1. namespace MyDataServiceClient.MyDataServiceEntities
  2. {
  3.     using System.Data.Services.Client;
  4.     using System.Net;
  5.     using System.Security.Cryptography.X509Certificates;
  6.  
  7.     public partial class MyDataServiceModelContainer : global::System.Data.Services.Client.DataServiceContext
  8.     {
  9.         private X509Certificate clientCertificate = null;
  10.  
  11.         public MyDataServiceModelContainer(global::System.Uri serviceRoot) :
  12.             base(serviceRoot)
  13.         {
  14.             this.OnContextCreated();
  15.         }
  16.  
  17.         partial void OnContextCreated();
  18.  
  19.         public X509Certificate ClientCertificate
  20.         {
  21.             get
  22.             {
  23.                 return clientCertificate;
  24.             }
  25.             set
  26.             {
  27.                 if (value == null)
  28.                 {
  29.                     // if the event has been hooked up before, we should remove it
  30.                     if (clientCertificate != null)
  31.                         this.SendingRequest -= this.OnSendingRequest_AddCertificate;
  32.                 }
  33.                 else
  34.                 {
  35.                     // hook up the event if its being set to something non-null
  36.                     if (clientCertificate == null)
  37.                         this.SendingRequest += this.OnSendingRequest_AddCertificate;
  38.                 }
  39.  
  40.                 clientCertificate = value;
  41.             }
  42.         }
  43.  
  44.         private void OnSendingRequest_AddCertificate(object sender, SendingRequestEventArgs args)
  45.         {
  46.             if (null != ClientCertificate)
  47.                 (args.Request as HttpWebRequest).ClientCertificates.Add(ClientCertificate);
  48.         }
  49.     }
  50. }
  •  
  • Set MyDataServiceClient as the startup project
  • Press F5

image

Conclusion

In this article I have demonstrated a methodology for securing communication between a WCF client and a IIS hosted WCF data service, installed on a workgroup server, using mutual certificate authentication. While in many ways similar to mutual authentication scenarios described elsewhere, securing a WCF data service exhibits some unique characteristics that warranted documenting.

I hope you find this useful.

Written by [BradleyCotier]