28 April, 2010

WCF: Restrict which Clients can call your WCF Service methods via X.509 Certificates

Today a colleague asked me how you can prevent anybody but a known client from being able to call your WCF service/service-methods. It goes without saying that we are aiming at controlling the calling machine in this scenario and not so much interested in the calling user. A quite common scenario when doing server-to-server communication in e.g. a B2B situation. Well – it can be accomplished quite securely and robust by using X.509 certificates which identifies the calling machine.

I should mean that it is preferred if the solution to this problem is a completely configured solution instead of a coded solution. No code – just configuration. In this way, the compiled assemblies remain the same and no need for recompilation is needed when you decide to change certificates or other “external” things. This I did not completely succeed with, as the below solution requires a single security-attribute (PrincipalPermission) on the methods that you want protected. As seen in the figure – this service is to be called by 2 different clients; one with a valid certificate and one without a certificate. The last should fail and be thrown away when trying to call the service methods!

Running sample:
You can download a running sample (Proof-Of-Concept) here: X509.POC.zip

Capture

How do we accomplish this scenario?

First – lets focus on the WCF Service that is to be protected.

SERVICE SIDE:
Now – to insist that the Client should identity himself via X.509 certificates to the Service, we need to configure this in the service configuration in the <bindings> configuration section. In the below, a custom configuration is seen called “messageAndCertificateClient”. This mandates that the calling Client should present a Certificate (set in the clientCredentialsType attribute) to identity/authenticate himself to the service.

<configuration>
<
system.serviceModel>
<
services>
<
service name="Server.CalcService" behaviorConfiguration="serviceCredentialsBehavior">
<
endpoint name=""
address="http://localhost/Calculator"
binding="wsHttpBinding"
bindingConfiguration="messageAndCertificateClient"
contract="X509.SharedLib.ICalcService" />
</
service>
</
services>

<
bindings>
<
wsHttpBinding>
<
binding name="messageAndCertificateClient">
<
security mode="Message">
<
message clientCredentialType="Certificate" />
</
security>
</
binding>
</
wsHttpBinding>
</
bindings>

<
behaviors>
<
serviceBehaviors>
<
behavior name="serviceCredentialsBehavior">
<
serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost/Calculator/mex"/>
<
serviceCredentials>
<!--
Service certificate (used for initial handshake and negotiation between C/S)-->
<
serviceCertificate findValue="HabaneroServer"
x509FindType="FindBySubjectName"
storeLocation="LocalMachine"
storeName ="My"/>
<
clientCertificate>
<!--
How should Client certificates be authenticated?-->
<
authentication certificateValidationMode="None"/>
</
clientCertificate>
</
serviceCredentials>
<!--
use AspNetRoles to concatentate CN and thumbPrint -->
<
serviceAuthorization principalPermissionMode="UseAspNetRoles"/>
</
behavior>
</
serviceBehaviors>
</
behaviors>
</
system.serviceModel>
</
configuration>


The server side has a simple method implementation/signature seen below. As seen, the service method (‘Add’) has been attributed with a PrincipalPermissionAttribute (system.security.permissions). This attribute is set with the Name property to “CN=<subject>; <thumbprint>”. This is quite important as the later tag <serviceAuthorization principalPermissionMode=”UseAspNetRoles”/> makes WCF concatenate the incoming Clients certificate data (subject and certificate unique thumbprint) into a semicolon separated string. This string is used for authorization later on by means of simple string comparison. It is important to mention that this PrincipalPermissionAttribute can be applied to the service method level only and does not apply to the service level. You are however in this way able to control access to a very granular level and do not span the service in it’s entirety.

[ServiceContract(Namespace="http://services.ckit.dk/2010/1")]
public interface ICalcService
{
[OperationContract]
int Add(int a, int b);

[OperationContract]
int Subtract(int a, int b);
}



class CalcService : ICalcService
{
#region ICalcService Members

//can only be called if cert = <thumb>
[PrincipalPermission(SecurityAction.Demand, Name = "CN=HabaneroClient; 6cc9da1b64592d1f0ce46824674c833a30993bba")]
public int Add(int a, int b)
{
Console.WriteLine("Service called...");
return a + b;
}

public int Subtract(int a, int b)
{
Console.WriteLine("Service called...");
return a - b;
}

#endregion
}


Now – you might be wondering where did this thumbprint stuff come from? The subject and thumbprint that we want to allow to use our service methods are both coming from the Clients certificate. It is retrieved by opening the Clients certificate in the Certificate Manager (MMC-snapin) and copying them into notepad (this is done on the Client machine!).


image


CLIENT SIDE: We need to setup the client to present certificates to the service. How is this accomplished? The same way anything else is setup when dealing with WCF. It is configured in the Clients configuration file:

 
CLIENT SIDE:
The client service is set up like this, mandating that a Certificate is presented to the (server) service. In addition, an expected identity (HabaneroServer) is set in the dns value setting. The Certificate presented by the client is called HabaneroClient and found in the local Certificate Storage.
<?xml version="1.0" encoding="utf-8"?>
<
configuration>
<
system.serviceModel>
<
client>
<
endpoint name="localEP"
address="http://localhost/Calculator"
binding="wsHttpBinding"
behaviorConfiguration="endpointCredentialsBehavior"
bindingConfiguration="certBehave"
contract="X509.SharedLib.ICalcService">
<
identity>
<!--
Expected identity of server (certificate)-->
<
dns value="HabaneroServer" />
</
identity>
</
endpoint>
</
client>

<
behaviors>
<
endpointBehaviors>
<
behavior name="endpointCredentialsBehavior">
<
clientCredentials>
<!--
Identity to present to service-->
<
clientCertificate findValue="HabaneroClient"
storeLocation="LocalMachine"
x509FindType="FindBySubjectName"/>
<
serviceCertificate>
<!--
How will the Client validate the Servers certificate?-->
<
authentication certificateValidationMode="None"/>
</
serviceCertificate>
</
clientCredentials>
</
behavior>
</
endpointBehaviors>
</
behaviors>

<
bindings>
<
wsHttpBinding>
<
binding name="certBehave" >
<
security mode="Message">
<
message clientCredentialType="Certificate" />
</
security>
</
binding>
</
wsHttpBinding>
</
bindings>
</
system.serviceModel>
</
configuration>

As seen above, the Client configuration is more or less a copy of the Server side configuration. It mandates that the Client presents a certificate as authentication to the service.

2 comments:

Khuda Gawah said...

In my mind this is a very weak strategy.

Defining security in config files for which there is no or relatively less security, is awful. You are defining the cert search criteria in the config file. This search criteria can be easily changed.

If this was done from within code and the cert properties were picked from the signed client, it would have added more strength to the authorization technique.

Claus Konrad said...

First of all - thanks for the nice comments.
In respect to your suggestions, the premise here was to use as much "out-of-the-box" functionality as possible and not "invent the wheel" yourself. This is accomplished to full satisfaction in this presentation, as no custom code is involved here - it is all just configuration using standard Microsoft/WCF settings.

Now - regarding your suggestions that the signed value should be taken from the calling client, you are right.
And in fact that IS THE CASE HERE! The thumbprint added to the method (in code ;-)) is the thumbprint of the certificate the client exposes to the server. If the server receives a call from anybody other than the accepted client, the call is rejected at the gate (the caller can not present an accepted thumbprint).
And that this should be easy to change? This is no easier to change than any other web.config setting in a webservice.

If you are still hanging on to it being easy to change, you are free to push this value into a backend database record and retrieve it during the authentication. This does however require you to implement a custom authentication policy, which as what I wanted to avoid in this particular scenario.