Introduction
Security has an important role in any distributed application and Windows Communication Foundation (known as WCF or Indigo), the new Microsoft communication framework, implements many security standards and has a wide range of features available.
One of the most important aspects of security is authentication. WCF can be configured to use many authentication methods:
- Anonymous caller
- User name and password
- Certificate
- Windows
- CardSpace
In this article I will show you how to configure WCF with certificates to authenticate service clients and server using an alternative approach.
If you want to exactly understand my implementation, continue reading the next section. If you simply want to understand how to configure WCF using certificates jump directly to the Quick start tutorial section.
Background concepts
The next sections assume that you are familiar with many WCF and security concepts. See the External resources section if you want to review some of these concepts or for more information.
The problem
The use of certificates for authentication is not new, but is still one of the most common way to authenticate a subject. WCF has a built-in support for certificates that conform to the Web Services Security (WS-Security) standards.
The problem with the default configurations and examples available is that all the certificates must be installed in the Certificate Store, which basically is a central location where Windows saves all the certificates (used also for other applications: Internet Explorer, ...).
Why this solution causes some problems? The easy answer is because it is not easy to correctly configure all the certificates. For more details:
- When you deploy your service to the server you must install in the Certificate Store all the certificates used (in different locations based on the use of the certificate).
This operation must be executed using an installation program, a script file or a batch file. For this reason, it is difficult to deploy the application using an xcopy/ClickOnce installation. - Each client must also install the certificate used to authenticate itself always in the Certificate Store. This is easy if you have a small number of clients but very difficult if you must manually configure each computer (in addition, for the client, you can't use an xcopy/ClickOnce installation).
- You must give to the running process (like ASP.NET) the permissions to read the certificate private key. This step usually requires changing the file system permission. This again requires a script file or an installation which is not always easy.
- If you are using a shared hosting probably you can't install certificates or change certificate permissions.
- As a developer I like to have each project isolated from the others. I want to be able to easy test different configurations or applications, I like to simply download the latest version from the code repository and run it, without any special configuration. Using the Certificate Store I must always remember to install or uninstall the certificates each time.
At the following MSDN page you can see an example of a configuration using certificates and a description of how to install certificates using the classic solution: MSDN: Message Security Certificate.
These are the reasons behind my decision to try a different approach that I will describe in the following sections.
The solution
My goal is to find an easy way to use certificates without using Certificate Store. WCF can be easily extended; in this article I will show you how to extend WCF to load the certificates from files.
I known that storing certificates on the file system is less secure, but I think that with some attention this can be a useful alternative. See the Disadvantages section for a discussion of the possible problems of my approach.
Consider that with my solution, I simply change how the certificates are loaded, all the advantages of using WCF (standards, proved code, ...) are still valid. Most important you must still use most of the settings required to use certificates. For a complete and working example, I suggest to look at the sample project in the zip file or follow the Quick start tutorial section to implement this solution on your own project.
To understand how this solution works, continue reading the next section.
Implementation details
Loading a certificate from a file is quite easy, you must simply use the System.Security.Cryptography.X509Certificates.X509Certificate2
class:
X509Certificate2 certificate =
new X509Certificate2(fullpath, password);
The first parameter is the path of the certificate file, the second parameter is the password used to encrypt the private key (if present).
With X509Certificate2
class you can load 2 kinds of files:
- .cer file - Used to store a public key
- .pfx file - Used to store a public+private key (optional encrypted with a password)
You can obtain these files from a public certification authority or create your self-signed certificates using makecert.exe
and pvk2pfx.exe
, both available as Visual Studio Tools. Here an example on how to create a certificate:
makecert -r -pe -n "CN=CompanyXYZ Server" -b 01/01/2007 -e 01/01/2010
-sky exchange Server.cer -sv Server.pvk
pvk2pfx.exe -pvk Server.pvk -spc Server.cer -pfx Server.pfx
The first command (makecert
) generates a public key (in this case Server.cer) and a private key (in this case Server.pvk). The second command (pvk2pfx
) merges the 2 files on a single .pfx file (in this case Server.pfx).
When executing makecert
and pvk2pfx
, you can insert a password used to encrypt the private key.
Service authentication
This section describes how to configure the server with the certificate used to authenticate the service.
First you must add a reference to the DevAge.ServiceModel.dll. This assembly (included in the sample project with the full code) contains some classes used to load the certificate from a file that I will soon describe.
Then you must generate the service certificate and put it in a secure directory. For ASP.NET web site, I think that a good solution is to use the App_Data directory, which is automatically configured to not allow public access. I have generated in the App_Data directory 2 files: Server.cer and Server.pfx.
Usually you can specify the certificate for the service inside the system.serviceModel
section of the .config file like this:
<behaviors>
<serviceBehaviors>
<behavior name="serviceCredentialBehavior">
<serviceCredentials>
<serviceCertificate findValue="Contoso.com"
storeLocation="LocalMachine"
storeName="My"
x509FindType="FindBySubjectName" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
This configuration basically tells WCF where to find the certificate inside the Certificate Store.
To load the certificate from file, unfortunately you can't simply change the configuration, but you must set the ServiceHost.Credentials.ServiceCertificate.Certificate
property.
I have created a new configuration section where you can specify where to find the server certificate:
<devage.serviceModel>
<services>
<add name="MathService"
serverCertificate="App_Data\Server.pfx"
/>
</services>
</devage.serviceModel>
Then I have created a new ServiceHost
derived class, DevAge.ComponentModel.CertificateServiceHost
, which automatically reads the new configuration section and sets the ServiceHost.Credentials.ServiceCertificate.Certificate
property.
If you directly create the ServiceHost
class you can simply change your code to create the DevAge.ComponentModel.CertificateServiceHost
instead.
If you use ASP.NET (that automatically creates the ServiceHost
class) you must configure the .svc file in this way:
<% @ServiceHost Language=C# Debug="true"
Service="MathService"
CodeBehind="~/App_Code/MathService.cs"
Factory="DevAge.ServiceModel.CertificateServiceHostFactory" %>
Note the Factory
property that uses a specific factory class to create my DevAge.ComponentModel.CertificateServiceHost
.
This configuration replaces the standard serviceCertificate
section.
NOTE: When you will create the proxy client with svcutil
you will see an identity
section inside the endpoint
section:
<identity>
<certificate encodedValue="...." />
</identity>
This section must be recreated each time you change the service certificate, because it contains the public identification of the certificate generated at design time. This value is used by the client to be sure to speak with the expected service.
Client authentication
This section describes how to use certificates to authenticate each client.
You must create a certificate for each client (or share the same certificate for more than one client). You can use the same command to generate self-signed certificates or obtain it from a certification authority.
In my example I have generated in the client directory 2 files: Client.cer and Client.pfx.
Normally each client can configure the application with the certificate using the configuration below:
<behaviors>
<endpointBehaviors>
<behavior name="ClientCredentialsBehavior">
<clientCredentials>
<clientCertificate findValue="Cohowinery.com"
storeLocation="CurrentUser"
storeName="My"
x509FindType="FindBySubjectName" />
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
This configuration always uses the Certificate Store to locate the right certificate to use and like in the previous example, there isn't a way to directly use a certificate file. So I have again created a new configuration section to specify the certificate file name:
<devage.serviceModel>
<endPoints>
<add contract="Client.MathService.IMathService"
clientCertificate="Client.pfx" />
</endPoints>
</devage.serviceModel>
To manually set the certificate for the client you can set the ClientProxy.ClientCredentials.ClientCertificate.Certificate
property.
Unfortunately in this case, there isn't a way to automatically set this property by reading the new section (or at least I cannot find a way...) so you must configure each proxy with code like this:
MathService.MathServiceClient service =
new Client.MathService.MathServiceClient();
DevAge.ServiceModel.Proxy<MathService.IMathService>.Configure(service);
The DevAge.ServiceModel.Proxy
class reads the configuration section and sets the ClientProxy.ClientCredentials.ClientCertificate.Certificate
property.
If the certificate used by the client is trusted by the server (usually must be added in the Trusted people of the Certificate Store) your configuration is complete. If otherwise the certificate is self-signed or anyway is not trusted by the server you must configure the server to accept the certificate.
To solve this issue, I have implemented a custom X509CertificateValidator
class, DevAge.ServiceModel.CustomCertificateValidator
, which can be configured for each service. You must extend the devage.serviceModel
section of the .config file of the service (on the server) like this:
<devage.serviceModel>
<services>
<add name="MathService"
serverCertificate="App_Data\Server.pfx"
clientCertificates="App_Data\Client1.cer,App_Data\Client2.cer"
/>
</services>
</devage.serviceModel>
Note the new clientCertificates
property which contains a list of client certificates to consider trusted.
You must copy inside the App_Data server directory all the client public key files (in this case Client1.cer and Client2.cer files).
The CertificateServiceHost
class automatically reads this new section and sets the X509CertificateValidator
using code like this:
X509ClientCertificateAuthentication authentication =
serviceHost.Credentials.ClientCertificate.Authentication;
authentication.CertificateValidationMode =
System.ServiceModel.Security.X509CertificateValidationMode.Custom;
authentication.CustomCertificateValidator =
new CustomCertificateValidator(clientCertificates);
If the certificate file has a password, you can specify it within the file name using this format: filename|password. For example you can specify the serverCertificate
attribute of the web.config with this value: App_Data\Server.pfx|yourpassword
.
Remember also that you can specify 2 different password for the *.pvk and for the *.pfx files, the password to use is the password specified for the *.pfx file. See the makecert
and pvk2pfx
documentation for more details on how to set passwords.
Storing the certificates on files is easier but is less secure because a malicious user can steal the private key. The public key can be shared without problems but if someone steals the private key your security is compromised. With the private key a malicious user can decrypt the messages or impersonate your service or a client.
I think anyway that usually if someone can access your file system you probably have many other security problems. Consider also that if someone has access to your file system, he or she can probably (depends on the type of the attack) also access the Certificate Store.
My suggestion is to correctly evaluate your application and your security requirements. In many cases, I think that saving certificate on a file is quite secure and can be useful.
Finally consider that you can use a mixed configuration, for example reading the server certificate inside the Certificate Store and the client certificates on file system.
Here a simple tutorial to use certificates with a service hosted on IIS/ASP.NET and a console client application. I will create a simple Math service with a single Sum
method.
Creating the service
Create a new web site project; add a reference to the DevAge.ServiceModel.dll assembly (that you can find in the sample project).
Add these files:
- App_Code/MathService.cs
using System;
using System.ServiceModel;
using System.Runtime.Serialization;
[ServiceContract()]
public interface IMathService
{
[OperationContract]
double Sum(double a, double b);
}
public class MathService : IMathService
{
public double Sum(double a, double b)
{
return a + b;
}
}
- MathService.svc
<% @ServiceHost Language=C# Debug="true"
Service="MathService"
CodeBehind="~/App_Code/MathService.cs"
Factory="DevAge.ServiceModel.CertificateServiceHostFactory" %>
- Web.config
="1.0"
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<configSections>
<section name="devage.serviceModel"
type="DevAge.ServiceModel.Configuration.Section,
DevAge.ServiceModel" />
</configSections>
<system.serviceModel>
<services>
<service name="MathService" behaviorConfiguration="behavior1">
<endpoint contract="IMathService"
binding="wsHttpBinding" bindingConfiguration="binding1"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="behavior1">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<wsHttpBinding>
<binding name="binding1">
<security mode="Message">
<message clientCredentialType="Certificate" />
</security>
</binding>
</wsHttpBinding>
</bindings>
</system.serviceModel>
<devage.serviceModel>
<services>
<add name="MathService"
serverCertificate="App_Data\Server.pfx"
clientCertificates=""
/>
</services>
</devage.serviceModel>
</configuration>
Now you must obtain a server certificate, you can buy it from a certification authority or create a self-signed certificate with the following procedure:
Open a Visual Studio Command Prompt, go to the App_Data directory and execute these commands (don't insert the password when asked):
makecert -r -pe -n "CN=CompanyXYZ Server" -b 01/01/2007 -e 01/01/2010
-sky exchange Server.cer -sv Server.pvk
pvk2pfx.exe -pvk Server.pvk -spc Server.cer -pfx Server.pfx
Run the project on Visual Studio to start the ASP.NET Development Server and try to navigate to the MathService.svc page.
Creating the client
Create a new Console Application project using "Client
" as the project name; add a reference to the DevAge.ServiceModel.dll assembly (that you can find in the sample project). Using Visual Studio you must right click on the project and select "Add Service Reference". Insert the URL of your service (probably something like http://localhost:1281/Server/MathService.svc) and the name MathService
.
Create a new Program.cs file to test your service like this:
using System;
using System.Collections.Generic;
using System.Text;
namespace Client
{
class Program
{
static void Main(string[] args)
{
MathService.MathServiceClient service =
new MathService.MathServiceClient();
DevAge.ServiceModel.Proxy<MathService.IMathService>.Configure(service);
try
{
double val = service.Sum(10, 20);
Console.WriteLine("OK");
}
catch (Exception ex)
{
service.Abort();
Console.WriteLine(ex.ToString());
}
Console.WriteLine("Press Enter key to exit...");
Console.ReadLine();
}
}
}
Open the app.config file, save in a temp file the encodedValue
attribute in the certificate
tag. Replace the app.config file with the configuration below, but remember to restore the previous encodedValue
attribute and check the address
attribute to point to the right service URL:
="1.0"="utf-8"
<configuration>
<configSections>
<section name="devage.serviceModel"
type="DevAge.ServiceModel.Configuration.Section, DevAge.ServiceModel" />
</configSections>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="binding1" >
<security mode="Message">
<message clientCredentialType="Certificate" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="behavior1">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="None" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="http://localhost:1281/Server/MathService.svc"
binding="wsHttpBinding" bindingConfiguration="binding1"
contract="Client.MathService.IMathService"
behaviorConfiguration="behavior1"
name="endpoint1" >
<identity>
<certificate encodedValue="TODO Here use your specific encodedValue" />
</identity>
</endpoint>
</client>
</system.serviceModel>
<devage.serviceModel>
<endPoints>
<add contract="Client.MathService.IMathService"
clientCertificate="..\..\Client.pfx" />
</endPoints>
</devage.serviceModel>
</configuration>
Now we must generate the client certificate. Again this certificate can be obtained from a certification authority or created with the procedure below:
Open a Visual Studio Command Prompt, go to the client project directory and execute these commands (don't insert the password when asked):
makecert -r -pe -n "CN=CompanyXYZ Client" -b 01/01/2007 -e 01/01/2010
-sky exchange Client.cer -sv Client.pvk
pvk2pfx.exe -pvk Client.pvk -spc Client.cer -pfx Client.pfx
Finally copy the Client.cer file in the server App_Data directory and modify the web.config server file adding the client certificate like this:
<devage.serviceModel>
<services>
<add name="MathService"
serverCertificate="App_Data\Server.pfx"
clientCertificates="App_Data\Client.cer"
/>
</services>
</devage.serviceModel>
Note the clientCertificates
attribute that now contains the Client.cer file.
Now you can test your client application.
Sample project
In the sample project, I have created a full working example composed of:
Server
- a service hosted by a web site project Client
- the service client implemented as a console application DevAge.ServiceModel
- the library that contains the new implementation used to load the certificates from a file
You can simply open the solution with Visual Studio 2005 and run the Client project to test the code.
Conclusion
I think that this article show the advantages and disadvantages of loading the certificates from files without using the Certificate Store. Security is important and I know that this solution is not perfect, but can be a useful alternative. Consider anyway that it is always easy to change the configuration files to use the classic approach.
Write to me if you find errors (on code or documentation) or if you have problems, questions or suggestions. I will appreciate any feedback.
History
- 30 Apr 2007: First release
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.