WSE Security

Protect Your Web Services Through The Extensible Policy Framework In WSE 3.0

Tomasz Janczuk

This article discusses:

  • Policy framework architecture in WSE 3.0
  • Using the policy framework to secure Web services
  • Wire format of secure SOAP messages
  • Writing custom extensions to the policy framework
This article uses the following technologies:
Web Services Enhancements 3.0

Code download available at: WSE.exe(119 KB)

Contents

WSE Policy Framework
The Policy Object Model
Applying Policy
Security Assertions
Username/Password Authentication
Certificate Authentication
Kerberos Authentication
Adding Replay Detection
Conclusion

Web Services Enhancements (WSE) 3.0 is an add-on to Visual Studio® 2005 and the Microsoft® .NET Framework 2.0. It provides advanced Web services capabilities, helping to keep pace with the evolving Web services protocol specifications.

The WSE policy framework provides a mechanism to describe the constraints and requirements that need to be enforced for a Web service. In this article I will describe how the policy framework works in WSE. I will discuss security scenarios supported by WSE 3.0, particularly the wire formats of secure SOAP messages generated and consumed by WSE. I will then explore the mechanisms that can be used to extend the framework, allowing you to apply and enforce custom constraints and requirements. My example code will use this capability to add a replay detection feature to protect recipients of SOAP messages.

WSE Policy Framework

The WSE policy framework describes constraints and requirements for communicating with an ASMX (ASP.NET Web services framework) or WSE Web service. Policy is a deployment-time concept. An instance of a policy is translated into run-time components that apply the requirements at the sender or enforce them at the receiver. This process of translation, referred to as policy compilation, takes place before any messages are exchanged between the client and the service.

The atomic run-time components are called SOAP filters and operate at the level of a single SOAP envelope. Their primary responsibility is to inspect or transform the SOAP envelope. For example, a SOAP filter on the sender may digitally sign the message and add a Security header to it, as defined in the WS-Security specification. Similarly, a SOAP filter on the receiver may extract the Security header from the SOAP envelope and require that the digital signature it contains be successfully verified before dispatching the message to the application code.

A single policy contains an ordered list of policy assertions. Each policy assertion defines a single requirement of a Web service. Two corresponding run-time components enforce these requirements. In Figure 1, the policy and policy assertion deployment-time concepts correspond to the pipeline and SOAP filter run-time concepts, respectively. For example, the ordering of requirements in a policy allows an assertion that describes authentication requirements to take effect prior to an assertion that describes authorization requirements—this way the identity of the caller, which is determined in the authentication process, can be used during authorization enforcement. The order of policy assertions within a policy controls the order in which related requirements will be applied or enforced at run time. Figure 1 shows how policy compilation preserves the ordering of requirements expressed as policy assertions in the order of SOAP filters in the pipeline.

Figure 1 Policy Implemented Through Assertion of SOAP Filters

Figure 1** Policy Implemented Through Assertion of SOAP Filters **

During the policy compilation process, each policy assertion generates up to four SOAP filters. Each SOAP filter executes at a different stage of the request/response message exchange. Two distinct SOAP filters process the input and output messages on the client, and two filters process the input and output messages on the service. SOAP filters generated by policy assertions follow the order of assertions in the policy. This collection of input and output filters is represented by a pipeline.

Even though a single policy assertion may create up to four SOAP filters, not all requirements expressed as policy assertions need all four hook points in the request/response message pattern. For example, a replay detection requirement would only need two SOAP filters for incoming messages on the client and service. No special processing of outgoing messages is necessary in this case.

The Policy Object Model

A look at the object model behind the policy framework helps to illustrate these concepts. The PolicyAssertion abstract base class declares four methods for creating SoapFilters. These methods execute to process a request or response on the client or service. At run time, two of these methods will be called (which two will depend on whether a policy assertion is applied to the client or the service) to contribute SoapFilters to the pipeline. Any of these methods may return null, which indicates the assertion does not require any processing at that point in the message exchange.

public abstract class PolicyAssertion{
    public abstract SoapFilter CreateClientInputFilter(
        FilterCreationContext context);
    public abstract SoapFilter CreateClientOutputFilter(
        FilterCreationContext context);
    public abstract SoapFilter CreateServiceInputFilter(
        FilterCreationContext context);
    public abstract SoapFilter CreateServiceOutputFilter(
        FilterCreationContext context);
    ...
}

SoapFilter is a run-time class responsible for an atomic transformation of a SOAP message. Its contract is quite straightforward: the only method that must be implemented accepts a SoapEnvelope to be inspected or transformed and returns a SoapFilterResult. The SoapFilterResult indicates whether the pipeline should either continue processing or terminate. This termination feature is only useful in advanced scenarios involving handling of infrastructure messages (for example, WS-Reliability protocol messages), that should not be dispatched directly to the Web service class:

public abstract class SoapFilter
{
    public abstract SoapFilterResult ProcessMessage(
        SoapEnvelope envelope);
    ...
}

A PolicyAssertion is ordered into an instance of the Policy class to capture the desired set of requirements for a service or a proxy. The Policy can then act as a factory for creating client or service pipelines, as shown in the following:

public class Policy : IPipelineProvider
{
    public Collection<PolicyAssertion> Assertions { get; }
    public Policy(params PolicyAssertion[] assertions) { }
    public Pipeline CreateClientPipeline(
        PipelineCreationContext context) { }
    public Pipeline CreateServicePipeline(
        PipelineCreationContext context) { }
}

The Pipeline class represents an ordered collection of SoapFilter objects for processing incoming or outgoing messages. As such, Pipeline can be used to customize the run-time behavior of a service or service proxy.

Applying Policy

Policies in WSE can be defined imperatively in code or declaratively in an external XML file. Both approaches address specific scenarios. When defining policy imperatively in code, you take full control over defining the requirements that need to be satisfied in order to use the service. When the compiled application is distributed, the administrator cannot change these requirements.

Declaring policies in an XML file addresses the opposite scenario: the developer only provides the application logic in code and delegates the responsibility of specifying policy requirements to the administrator. This approach makes more sense when the policy requirements depend on the deployment environment and cannot be decided upon at development time. For example, the same Web service can be exposed for users of a Windows domain and the Internet. In the Windows domain, the security requirements could use Kerberos. Meanwhile, on the Internet, username/password and X.509 certificates can be used.

WSE defines a PolicyAttribute, which can be added to a class representing the ASMX Web service or a service proxy to apply a specific policy:

[WebService]
[Policy("HelloWorldPolicy")]
public class HelloWorld : WebService
{
    [WebMethod]
    public string Hello(string request) 
    { 
        return String.Format("Hello, {0}", request);
    }
}

In this example, a policy named HelloWorldPolicy is applied to the HelloWorld Web service. When the service class is instantiated for the first time in an application domain, the WSE runtime will resolve this policy name to a Policy instance declared in an XML file referenced in the configuration file. Afterwards, the Policy instance will be compiled into a pipeline with SoapFilters for incoming and outgoing messages. All SOAP requests and responses directed to this ASMX Web service will be processed through this pipeline. PolicyAttribute is not supported at the Web method scope.

The name of the XML file containing policy declarations is provided in the configuration file as shown in the following:

<configuration>
    <microsoft.web.services3>
        <policy fileName="policies.config"/>
    </microsoft.web.services3>    
</configuration>

The schema of the XML file containing the policies allows you to declare any number of named policies. This enables different services that are running in the same application domain (and therefore sharing a single configuration file) to use different policies. For example, this policies.config file contains definitions of two policies—HelloWorldPolicy and Policy17, as shown here:

<policies xmlns="https://schemas.microsoft.com/wse/2005/06/policy">
    <policy name="HelloWorldPolicy">
        <usernameForCertificateSecurity/>
    </policy>
    <policy name="Policy17">
        <kerberosSecurity/>
    </policy>
</policies>

Both policies have only one policy assertion each—one is using a usernameForCertificateSecurity assertion and the other a kerberosSecurity assertion. The assertions are built into WSE 3.0 and offer additional configuration options.

PolicyAttribute can also be used to apply policy to a service proxy on the client side. This can be done by adding the attribute on the class representing the ASMX proxy. However, the ASMX proxy class is typically autogenerated using the Add Web Reference feature of the Visual Studio environment or the wsdl.exe tool. PolicyAttribute would therefore have to be added to this class each time it is regenerated, which is not convenient. Thus, the recommended way to assign policy to a client proxy is to call the SetPolicy method on it as follows:

HelloWorld serviceProxy = new HelloWorld();
serviceProxy.SetPolicy("HelloWorldPolicy");

SetPolicy is provided by the WebServicesClientProtocol class, which the ASMX proxy class (HelloWorld in this example) must derive from. WSE integrates closely with the Add Web Reference feature of the Visual Studio environment to ensure all newly generated ASMX proxy classes derive from WebServicesClientProtocol for WSE-enabled projects.

Referencing policy by name in your code is the best approach when you want to leave the decision about the specifics of the policy to the administrator. However, if you want to control the policy entirely in code, you can achieve this in two ways: you can indicate a common language runtime (CLR) type of the policy class using PolicyAttribute, or you can provide an instance of a Policy object using the SetPolicy method.

In Figure 2, the MyPolicy class derives from a Policy base class and populates the Assertions collection with an instance of KerberosAssertion in the constructor, thereby creating a fully specified policy in code. Next, PolicyAttribute is applied to the ASMX service class to indicate the CLR type of a class complying with the Policy contract. This class will be instantiated by the WSE runtime by the default constructor when the service instance is created. The resulting Policy instance is compiled into a pipeline. Figure 3 summarizes recommended and available methods of setting policy on the ASMX and SoapClient/SoapService services and proxies.

Figure 3 Methods for Setting Policy

Service/Proxy PolicyAttribute with Policy Name PolicyAttribute with Policy CLR Type SetPolicy Method with Policy Name SetPolicy Method with Policy Instance
ASMX Service
ASMX Proxy
SoapSender
SoapReceiver
Recommended Available Not Applicable

Figure 2 Specifying a Policy Type

class MyPolicy : Policy
{
    public MyPolicy() : base()
    {
        this.Assertions.Add(new KerberosAssertion()); 
    }
}

[WebService]
[Policy(typeof(MyPolicy))]
public class HelloWorld : WebService
{
    [WebMethod]
    public string Hello(string request) { 
    {            
        return String.Format("Hello, {0}", request);
    }
}

Security Assertions

The policy framework in WSE 3.0 provides six ready-to-use assertions that address common scenarios, helping to secure Web services. In general, these assertions use WS-Security and WS-SecureConversation mechanisms to provide message integrity and confidentiality as well as authentication. Figure 4 summarizes the features offered by the six assertions.

Figure 4 Security Assertions in WSE 3.0

Policy Assertion Integrity Confidentiality Secure Conversation Client Authentication Service Authentication
UsernameForCertificateAssertion Username/password X.509
MutualCertificate10Assertion X.509 X.509
MutualCertificate11Assertion X.509 X.509
AnonymousForCertificateAssertion   X.509
UsernameOverTransportAssertion       Username/password  
KerberosAssertion Kerberos  

Using any of the six security assertions offered by WSE facilitates future interoperability with Windows Communication Foundation. Currently, messages secured using any of the WSE 3.0 assertions are wire-level compatible with the Windows Communication Foundation Beta 2 release. If you are going to write a WSE client application that needs to call a service written on another platform, or your WSE Web service will be consumed by clients created on other platforms, it is important to understand the features of the WS-* specifications that these assertions use, as well as the actual layout of elements within the Security SOAP header.

Except for UsernameOverTransportAssertion, all security assertions support integrity, confidentiality, and secure conversation. Integrity features allow the recipient of a message to check whether the message was modified in transit. This is achieved by digitally signing the message and the recipient validating this signature. The WS-Security standard requires that the digital signature follow the XML Digital Signature standard. This standard supports signing of selected parts of an XML document (a SOAP envelope in this case). Security assertions in WSE allow you to decide which parts of the SOAP envelope need to be signed. By default, all WS-Addressing SOAP headers, the SOAP body containing the application payload, and the timestamp within the Security header are signed by the sender and required to be signed by the recipient.

Confidentiality features allow the sender of the message to ensure that the content of the message will only be available to the intended recipient. This is achieved by encrypting the message. WS-Security requires that encryption of the message follow the XML Encryption standard, which supports encrypting only selected elements of an XML document. Standard assertions in WSE only support encryption of the body of the SOAP envelope, which is the most common scenario. This is also the default behavior if you decide not to leave the body in plain text.

All assertions in WSE except UsernameOverTransportAssertion allow you to customize the parts of the message that are signed and to decide whether the body of the message should be encrypted. The APIs for all assertions are the same because these features are handled by a common base SecurityPolicyAssertion class.

These protection requirements will be uniformly applied to all Web methods of a service after the policy containing this assertion is applied to the service class. More advanced scenarios may require that protection requirements differ between operations exposed by a service. The two most common scenarios are related to performance and interoperability. WSE allows you to differentiate the protection requirements for individual operations handled by a single policy. For this purpose, operations of a service are uniquely identified using the value of the Action header of the request message as defined in the WS-Addressing specification. (The WSE SDK contains a sample that demonstrates how to use the first child element of the SOAP body as a key on which to pivot your protection requirements.) For example, if you want to encrypt the body of the SOAP message for all operations except one identified by the uri:hello action, you could write code like this:

KerberosAssertion assertion = new KerberosAssertion();
assertion.Protection.DefaultOperation.Request.EncryptBody = true;
assertion.Protection.DefaultOperation.Response.EncryptBody = true;
OperationProtectionRequirements operationRequirements = 
    new OperationProtectionRequirements();
operationRequirements.Request.EncryptBody = false;
operationRequirements.Response.EncryptBody = false;
assertion.Protection.Operations["uri:hello"] = operationRequirements;

Whenever you add a custom SOAP header to the message, you may want to define protection requirements for it with a policy. WSE security assertions support specifying integrity requirements for custom SOAP headers. If your header has namespace uri:example and local name myHeader, and is only present on requests, you can require it to be signed with the following code:

assertion.Protection.DefaultOperation.Request.CustomSignedHeaders.Add(
    new XmlQualifiedName("myHeader", "uri:example"));

You can control all of these integrity and confidentiality requirements by using the external XML policy file. For example, the policy file shown in Figure 5 defines a policy that uniformly signs and encrypts all operations except for those with a request action equal to uri:hello.

Figure 5 Signing Operations that Don't Request uri:hello

<policies>
  <policy name="HelloWorldPolicy">
    <kerberosSecurity>
      <protection>
        <request encryptBody="True" />
        <response encryptBody="True" />
        <fault encryptBody="False" />
      </protection>
      <protection requestAction="uri:hello">
        <request signatureOptions="IncludeAddressing, IncludeSoapBody" 
                 encryptBody="False">
          <signedHeader name="myHeader" namespace="uri:example"/>
        </request>
        <response signatureOptions="IncludeAddressing, IncludeSoapBody" 
                  encryptBody="False" />
        <fault signatureOptions="IncludeNone " encryptBody="False" />
      </protection>
    </kerberosSecurity>
  </policy>
</policies>

All security assertions in WSE except for UsernameOverTransportAssertion provide support for establishing secure conversations following the WS-SecureConversation 1.1 specification. The idea behind a secure conversation is that a session is established between the client and the service. The handshake protocol defined by WS-SecureConversation is secured using credentials specific to the selected security assertion. For example, in the case of UsernameForCertificateAssertion, the client authenticates to the service during the handshake using username and password credentials, and the service authenticates to the client using its X.509 certificate. At the end of the handshake, the service issues a SecurityContextToken to the client. This token contains a symmetric key that is later used by both the client and service when exchanging subsequent messages.

The use of a symmetric cryptographic key over multiple message exchanges is, in most cases, more efficient than using the credentials that were used to establish the secure conversation. If you plan to use a single client proxy instance for multiple calls to the service and performance is important, then enabling secure conversation in the policy will be beneficial.

Establishing a secure conversation is handled entirely by the pipeline generated by the security assertions. The application code does not have to deal with the handshake messages defined by WS-SecureConversation at any point. Therefore, enabling secure conversation in a security assertion is as easy as simply turning it on:

KerberosAssertion assertion = new KerberosAssertion();
assertion.EstablishSecurityContext = true;

The security context token established between the client and the service is associated with the service proxy instance and is used for all messages sent on that proxy. However, the token may expire during the lifetime of the proxy. In that case, the proxy will, by default, throw an exception when you try to send new requests. (If it didn't, the service would likely reject the message anyway.)

There is another feature of security assertions in WSE that allows secure conversations to be reestablished once they expire, which provides a seamless experience for the user of the proxy. However, the new secure conversation bears no cryptographic relationship to the original one. (In the process of reestablishing the secure conversation, the security context token is not renewed; a new one is obtained.) Since the application utilizing the proxy may depend on the session semantics of the secure conversation, the reestablishment behavior is turned off by default. To enable it, all you need to do is set one more property on the assertion instance:

assertion.RenewExpiredSecurityContext = true;

The settings controlling the secure conversation behavior are fully accessible from the XML policy file as well:

<policies>
  <policy name="HelloWorldPolicy">
    <kerberosSecurity establishSecurityContext="True"
                      renewExpiredSecurityContext="True" />
  </policy>
</policies>

Username/Password Authentication

WSE provides two assertions that support client authentication using a username/password credential: UsernameForCertificateAssertion and UsernameOverTransportAssertion. UsernameForCertificateAssertion offers both client and server authentication using WS-Security protocols. UsernameOverTransportAssertion only provides client authentication using WS-Security, and relies on a secure transport (such as HTTPS) to provide message protection and server authentication.

When using UsernameForCertificateAssertion, the service must be configured with an X.509 certificate and its associated private key. The client needs to be configured with its own username/password credential as well as with the X.509 certificate of the service (public key only). It is assumed that the client knows the X.509 certificate of the service out of band. Note that WSE does not support obtaining a service's certificate at run time using protocols like Transport Layer Security (TLS) and secure sockets layer (SSL).

Configuration of the client or server credentials can be done declaratively in the XML policy file:

<usernameForCertificateSecurity>
  <clientToken>
    <username username="tomasz" password="zsamot"/>
  </clientToken>
  <serviceToken>
    <x509 storeLocation="LocalMachine" storeName="My" 
          findValue="CN=example.com" 
          findType="FindBySubjectDistinguishedName" />
  </serviceToken>
</usernameForCertificateSecurity>

The service's certificate is referenced from the certificate store, and the username/password credential is directly embedded in the XML policy file. Please note the client's username/password credential is only required on the client side. Even though it is possible, you should avoid storing passwords in an XML file. I recommend that you don't include the clientToken element in the policy configuration file at all, but instead use the imperative method of providing the username/password credential in the client code by calling the SetClientCredential method on a proxy:

string username = ..., password = ...;
HelloWorld helloWorld = new HelloWorld();
helloWorld.SetClientCredential(new UsernameToken(username, password));

This allows you to query the user of your application for credentials at run time and pass them on to the proxy, without the need to store them in an external XML file. You can also provide X.509 credentials in code using a similar mechanism, which may come in handy if you want to use the same proxy class and policy to talk to services with different X.509 certificates:

X509Certificate2 serviceCertificate;
helloWorld.SetServiceCredential(
    new X509SecurityToken(serviceCertificate));

If you provide both client and service credentials in code, your policy can be reduced to the following:

<policies>
  <policy name="HelloWorldPolicy">
    <usernameForCertificateSecurity/>
  </policy>
</policies>

So how does the UsernameForCertificateAssertion secure the message exchange? Figure 6 shows a sample layout of the Security header for the request and response message.

Figure 6 Messages Secured UsernameForCertificateAssertion

Figure 6** Messages Secured UsernameForCertificateAssertion **

To secure the request message, WSE creates a random symmetric key, wraps this key using the public key from the service's X.509 certificate, and uses this symmetric key to sign and encrypt the parts of the message that require integrity and confidentiality. The symmetric key is serialized into the Security header in the form of an xenc:EncryptedKey element. The reference list inside this element points to the xenc:EncryptedData element containing a username token representing the client's credentials. This way the client's password does not appear in the clear on the wire. The last element in the Security header of the request is dsig:Signature, which signs all message parts that require integrity, the timestamp, and the plain text wsse:UsernameToken element encrypted within xenc:EncryptedData.

Note that the service's X.509 certificate does not appear in the Security header of the request message. It is not required since both the client and the service have access to it. Instead, an external reference describing the properties of this certificate is used.

The response message uses the symmetric key passed by the client to the service in the request to sign and encrypt the message. The Security header of the response contains the timestamp, an xenc:ReferenceList pointing to the encrypted message parts, and a signature signing the timestamp and parts of the message that require integrity protection.

UsernameForCertificateAssertion verifies that a response is signed and encrypted with the same symmetric key that was used on the request. This proves that the response was sent by the owner of the X.509 certificate for which the symmetric key was encrypted in the request, since only the holder of the associated private key could decrypt the symmetric key. This mechanism allows the assertion to provide server authentication assurances on the client.

The wire format may be different depending on the integrity and confidentiality requirements as well as the assertion configuration. In particular, the layout will differ if you change the order in which the signature and encryption is applied to the message. By default, the message is first signed and then encrypted. Using the MessageProtectionOrder property, you can also configure the assertion to first encrypt and then sign the message, or sign the message, encrypt it, and then encrypt the message signature.

Another configurable aspect of the assertion is related to key derivation. By default, derived keys are not used, but you can change this using the RequireDerivedKeys property. When this feature is turned on, each time a message is signed or encrypted using a symmetric key, a derived key is first computed based on this root symmetric key, and the signature or encryption uses the newly derived key. In that case, you would see two additional elements in the Security header representing the derived key tokens used for signatures and encryption.

One last feature supported by security assertions in WSE is signature confirmation as defined in WS-Security 1.1. By default this behavior is turned off, but you can use the RequireSignatureonfirmation property on the assertion to require that the recipient of the request confirms all signatures from the request in the response. In this case, you would see additional wsse:SignatureConfirmation elements in the Security header of the response.

The second assertion in WSE that offers client authentication using username/password credentials is UsernameOverTransportAssertion. This assertion relies on a secure transport protocol like HTTPS to provide integrity, confidentiality, and server authentication assurances. WS-Security features are only used to carry the username/password credentials from the client to the service in the Security header of the SOAP message. WSE does not enforce that the transport is actually secure when this assertion is applied, so you must exercise extra caution when using it. If the transport is not secure, the password will appear in plain text on the wire.

Configuration of UsernameOverTransportAssertion is minimal. On the server, no credential configuration is necessary, as server authentication is not performed at the SOAP level. Your XML policy file can be as simple as the following:

<policies>
  <policy name="HelloWorldPolicy">
    <usernameForCertificateSecurity/>
  </policy>
</policies>

On the client side, you have the option of specifying the client's username/password credential in the XML policy file or in code. Again, for security reasons it is recommended you provide the credentials in code. Since server authentication is handled at the transport layer, there is no need to provide the service's X.509 certificate on the client side. Given all that, your client-side XML policy can look identical to the one on the server side.

Note that, when UsernameOverTransportAssertion is applied, the username and associated password are sent over the wire in plain text within the Security header of the request message. It is therefore crucial that confidentiality is provided for the entire message at the transport level.

Certificate Authentication

WSE provides three security assertions supporting authentication based on X.509 certificates. AnonymousForCertificateAssertion allows the client to remain anonymous while authenticating the service using its X.509 certificate. MutualCertificate10Assertion and MutualCertificate11Assertion both perform mutual authentication using the client and service's X.509 certificates. The difference between them is in the WS-Security specification features they use. MutualCertificate10Assertion does not require any WS-Security 1.1 features and is therefore compliant with a broader range of interoperability targets. MutualCertificate-11Assertion uses the WS-Security 1.1 feature of referencing the xenc:EncryptedKey element as a token to achieve better performance in securing request/response exchanges. However, this requires both communicating endpoints to support the newer WS-Security specification.

To use AnonymousForCertificateAssertion, both the client and service must be configured with the service's X.509 certificate. This certificate can be provided either through the XML policy file or through code. The mechanism is identical to what I already described for UsernameForCertificateAssertion. The wire format, shown in Figure 7, is even simpler.

Figure 7 Messages Secured with AnonymousForCertificateAssertion

Figure 7** Messages Secured with AnonymousForCertificateAssertion **

Notice that the layout of the Security header of the request message is similar to that of UsernameForCertificateAssertion and the layout of the Security header of the response message is identical. What's missing in the request's Security header is an xenc:EncryptedData element containing the client's username/password credentials in the form of wsse:UsernameToken. This way the client remains anonymous, while the service is authenticated with its X.509 certificate, and both request and response can be signed and encrypted using the encrypted key token. Because the response message contains a reference to the encrypted key token from the request, AnonymousForCertificateAssertion therefore requires that both of the communicating parties support the WS-Security 1.1 specification.

MutualCertificate11Assertion builds on AnonymousForCertificateAssertion by adding client authentication using an X.509 certificate. The client's certificate can be provided in the XML policy file (see Figure 8).

Figure 8 Client Certificate Specified by Policy

<mutualCertificate11Security>
  <clientToken>
    <x509 storeLocation="CurrentUser" storeName="My" 
          findValue="CN=client.com" 
          findType="FindBySubjectDistinguishedName" />
  </clientToken>
  <serviceToken>
    <x509 storeLocation="LocalMachine" storeName="TrustedPeople" 
          findValue="CN=service.com" 
          findType="FindBySubjectDistinguishedName" />
  </serviceToken>
</mutualCertificate11Security>

You can also omit the specification of the client's certificate in the XML file and provide it directly in code using the same SetClientCredential method I described earlier:

X509Certificate2 clientCertificate = ...;
HelloWorld helloWorld = new HelloWorld();
helloWorld.SetClientCredential(
    new X509SecurityToken(clientCertificate));

Figure 9 shows the effect of applying MutualCertificate11Assertion to the exchange. The response message is identical to that generated by AnonymousForCertificateAssertion, which requires support for WS-Security 1.1.

Figure 9 Messages Secured with MutualCertificate11Assertion

Figure 9** Messages Secured with MutualCertificate11Assertion **

Compared with AnonymousForCertificateAssertion, there are two additional elements when using MutualCertificate11Assertion: a binary security token representing client's X.509 certificate, and a second signature that uses this token to sign the primary message signature (the signature that signs all the parts of the message that require integrity). The effect of this second signature is to operate as if the client's certificate was used to sign the same parts of the message the primary signature signs. This is possible because the serialized form of the primary signature follows the XML Digital Signature specification, which means it already contains digested values representing the message parts that require integrity protection. The second signature will therefore be called an endorsing signature. Computing the endorsing signature is typically less expensive than computing the primary signature simply because the XML representing the primary signature is often smaller than the XML of all the message parts it covers.

MutualCertificate10Assertion achieves the same authentication and message protection effect as MutualCertificate11Assertion. The difference is that it can do its job using only WS-Security 1.0 features. The benefit is a greater number of products this assertion will interoperate with. This comes at a cost of degraded performance: securing the full request/response exchange requires four public key cryptographic operations as opposed to the three required by MutualCertificate11Assertion.

MutualCertificate10Assertion has the same configuration requirements as MutualCertificate11Assertion. But notice that the wire format shown in Figure 10 is very different.

Figure 10 Messages Secured with MutualCertificate10Assertion

Figure 10** Messages Secured with MutualCertificate10Assertion **

The request contains a binary security token with the client's X.509 certificate. This token is used directly to sign all the parts of the message that require integrity, including the timestamp. Encryption is done the same way as in AnonymousForCertificateAssertion and MutualCertificate11Assertion: using a symmetric key token encrypted for the service's X.509 certificate.

The response message follows a pattern similar to the request. The message is signed with the service's X.509 certificate and encrypted with a symmetric key token encrypted for the public key of the client's X.509 certificate. However, the response does not contain a binary security token with the service's X.509 certificate. This is because the client already has this certificate (it was used to help encrypt the request), so it is sufficient to provide an external reference to it.

Kerberos Authentication

When both the client and service are within the same Kerberos trust domain (for example, they are joined to the same Windows domain), Kerberos authentication can be used. It does not require as much deployment effort as any of the certificate-based authentication assertions supported by WSE, since certificates do not need to be distributed to all participants. In addition, Kerberos is based on symmetric cryptography, which is faster than public key cryptography. WSE provides KerberosAssertion to help you secure message exchanges with Kerberos tickets.

KerberosAssertion does not require any configuration on the service side. On the client, the Service Principal Name (SPN) of the targeted service needs to be specified. The SPN is used by the client to obtain a Kerberos ticket for the target service. You can specify the SPN in the XML policy file like this:

<policies>
  <policy name="HelloWorldPolicy">
    <kerberosSecurity>
      <token>
        <kerberos targetPrincipal="HOST/myServer"/>
      </token>
    </kerberosSecurity>
  </policy>
</policies>

Alternatively, you can provide the KerberosToken directly to your proxy in code using the SetClientCredential method:

HelloWorld helloWorld = new HelloWorld();
helloWorld.SetClientCredential(
    new KerberosToken("HOST/myServer"));

There is one limitation of using the SetClientCredential method with Kerberos tokens—the Windows operating system prevents a given instance of a Kerberos ticket from being used more than once in order to prevent replay attacks. This means before making each call, you have to set a new instance of a KerberosToken on the proxy using the SetClientCredential method as shown previously. Alternatively, if you specify your entire policy in code, you can set a Kerberos token provider on the KerberosAssertion directly. The token provider acts as a factory of KerberosTokens. Whenever the WSE runtime needs to secure a new request, it calls into this provider to ask for a fresh instance of a KerberosToken. This is how you could write code to set the Kerberos token provider:

KerberosAssertion assertion = new KerberosAssertion();
assertion.KerberosTokenProvider = 
    new KerberosTokenProvider("HOST/myServer");
HelloWorld helloWorld = new HelloWorld();
helloWorld.SetPolicy(new Policy(assertion));

Figure 11 shows how a message exchange is secured with a KerberosAssertion.

Figure 11 Messages Secured with KerberosAssertion

Figure 11** Messages Secured with KerberosAssertion **

Adding Replay Detection

Security assertions in WSE provide support for integrity, confidentiality, and client and service authentication. But these assertions do not mitigate a replay attack where an attacker intercepts an acceptable message sent by a valid client and then sends a copy of that message to the service at a later time. Such attacks are typically mitigated with replay detection, where the recipient can detect and reject duplicate messages. A replay detection feature can be added to WSE by building on the extensibility capabilities of the policy framework.

You first need to make some design decisions. How are you going to determine message uniqueness? Obviously you need to compare some parts of incoming messages. The parts of the SOAP envelope for which you have ensured uniqueness between messages should be signed by an authenticated sender of the message; otherwise, an attacker can circumvent your replay comparison check by modifying the messages, which would pass undetected on the receiver. Ideally, you would be able to compare all the parts that were signed by the sender. However, this is impractical because XML comparison is computationally expensive, and you would need to store substantial sections of incoming messages in order to prevent future replay attacks.

However, if you can assume the parts you care about are digitally signed with the XML Digital Signature required by the WS-Security specification, you have a very concise part of the SOAP envelope that carries enough entropy (or randomness) for replay detection purposes: the signature value itself.

Using the signature value has three advantages: it carries enough entropy to represent the parts of the message the recipient cares about, it's cheap and easy to compare, and it's compact to store. An ideal implementation of a replay detection mechanism would leverage signature value for that purpose. However, since I want to highlight the extensibility capabilities of the WSE policy framework, I will take an even simpler route by using MessageId, a WS-Addressing SOAP header that is recommended to carry a globally unique Uniform Resource Identifier (URI) identifying the message. Every self-respecting service will require this header to be signed, so it contributes to the entropy of the message signature value.

The replay detection mechanism needs to store the value of the MessageId header of past messages in order to compare them to new incoming messages. This process would obviously lead to ever increasing memory allocation. You could solve this problem by introducing control over the memory consumption and the assurances provided by the replay detection mechanism. Let's assume you want to limit the maximum number of past messages our replay detection mechanism will remember, therefore putting a cap on memory allocation. At the same time you can introduce control over the minimum quality of service, which in this case means the minimum time span during which replayed messages are guaranteed to be detected. Finally, you must consider the desired behavior of the system when a new message arrives at the time the memory consumption is at its limit. To live up to the quality of service assurances and the memory quota at the same time, you don't have many options other than rejecting this message. The core of the replay detection mechanism is implemented by a custom SoapFilter (see Figure 12).

Figure 12 Replay Detection Based on wsa:MessageId

class ReplayDetectionSoapFilter : SoapFilter
{
    Dictionary<string, DateTime> replayCache =
        new Dictionary<string, DateTime>();
    int maximumCacheSize = 60 * 60 * 10; // 60 msg/sec for 10 minutes
    TimeSpan minimumReplayProtectionTime = new TimeSpan(0, 10, 0);

    public override SoapFilterResult ProcessMessage(SoapEnvelope envelope)
    {
        if (envelope == null) throw new ArgumentNullException("envelope");

        string uniqueMessageIdentifier = 
            GetUniqueMessageIdentifier(envelope);
        lock (replayCache)
        {
            EnsureMessageIdentifierUniqueness(uniqueMessageIdentifier);
            AddMessageIdentifierToCache(uniqueMessageIdentifier);
        }
        return SoapFilterResult.Continue;
    }

    string GetUniqueMessageIdentifier(SoapEnvelope envelope)
    {
        return envelope.Context.Addressing.MessageID.Value.ToString();
    }

    void EnsureMessageIdentifierUniqueness(string identifier)
    {
        if (replayCache.ContainsKey(identifier))
        {
            replayCache[identifier] = DateTime.Now;
            throw new InvalidOperationException(
                «A duplicate message was received»);
        }
    }

    void AddMessageIdentifierToCache(string identifier)
    {
        if (replayCache.Count >= maximumCacheSize)
        {
            // purge the cache from entries we no longer need to keep
            DateTime retireTime = DateTime.Now.Subtract(
                minimumReplayProtectionTime);
            List<string> entriesToRetire = new List<string>();
            foreach (KeyValuePair<string, DateTime> entry in replayCache)
            {
                if (entry.Value < retireTime)
                    entriesToRetire.Add(entry.Key);
            }
            foreach (string key in entriesToRetire)
            {
                replayCache.Remove(key);
            }

            // if the cache size is still over quota, can't process
            if (this.replayCache.Count >= this.maximumCacheSize)
                throw new InvalidOperationException(
                    «Replay detection cache is full»);
        }

        replayCache[identifier] = DateTime.Now;
    }
}

The ReplayDetectionSoapFilter constructor assumes default values for the maximum cache size for MessageId header values as well as the minimum duration for which replays are guaranteed to be detected. The data structure to keep track of MessageId values is a dictionary that maps the MessageId value to the time the message with that value was last received.

The core of the implementation is in the ProcessMessage method. It first extracts the MessageId value from the incoming message. Then the EnsureMessageIdentifierUniqueness method is called to make sure another message with the same MessageId hasn't been registered within the minimum replay detection time span. If the new message happens to have a MessageId value that matches one already in the cache, the code assumes it is a replayed message and rejects it by throwing an appropriate exception from the EnsureMessageIdentifierUniqueness method. Otherwise, AddMessageIdentifierToCache is called to store the MessageId value along with the current time in the cache.

If the cache size has reached its quota, the application first tries to purge the cache by removing all entries not required to maintain quality of service as determined by the minimum replay detection time span. If the cache is still full after the attempted purging, the code will throw an exception because it cannot guarantee the quality of service and keep the memory consumption within the quota at the exact same time.

Pay close attention to the locking around the operations on the cache, which takes place in the ProcessMessage method. The ProcessMessage method on the SoapFilter must be designed to be thread safe because it may be called from multiple threads to process different messages.

The next step is to develop a policy assertion that will create the ReplayDetectionSoapFilter and allow this functionality to be added to a policy. Figure 13 shows the minimal implementation of ReplayDetectionPolicyAssertion. Note that ReplayDetectionSoapFilter is only returned to process incoming messages on the service. Theoretically, replay detection also makes sense when processing incoming messages on the client, but for the simplicity of this example, I have decided to provide this feature only on the service side.

Figure 13 Implementation of ReplayDetectionPolicyAssertion

public class ReplayDetectionPolicyAssertion : PolicyAssertion
{
    public override SoapFilter 
        CreateClientInputFilter(FilterCreationContext context)
    {
        return null;
    }

    public override SoapFilter 
        CreateClientOutputFilter(FilterCreationContext context)
    {
        return null;
    }

    public override SoapFilter 
        CreateServiceInputFilter(FilterCreationContext context)
    {
        return new ReplayDetectionSoapFilter();
    }

    public override SoapFilter 
        CreateServiceOutputFilter(FilterCreationContext context)
    {
        return null;
    }
}

ReplayDetectionPolicyAssertion is now ready to be used imperatively. This is how you add this assertion to a policy and install it on a service:

class MyPolicy : Policy {
    public MyPolicy() : base() {
        this.Assertions.Add(new KerberosAssertion()); 
        this.Assertions.Add(new ReplayDetectionPolicyAsertion());
    }
}

[WebService]
[Policy(typeof(MyPolicy))]
public class HelloWorld : WebService {
    ...
}

Note how the ReplayDetectionPolicyAssertion is installed as a second assertion in MyPolicy, after KerberosAssertion. If you look back at Figure 1, it will become clear that such ordering will cause ReplayDetectionSoapFilter to execute before the filter created by KerberosAssertion when processing incoming messages on the service. In this case it makes perfect sense: if the message appears to be replayed, you want to reject it before investing time and memory in validating security.

To make the ReplayDetectionPolicyAssertion usable declaratively when specifying policy in the XML file, you need to add serialization capability. PolicyAssertion defines a three-method contract that each serializable policy assertion needs to follow. The three methods are ReadXml, WriteXml, and GetExtensions. Their implementation is shown in Figure 14.

Figure 14 ReplayDetectionPolicyAssertion

public class ReplayDetectionPolicyAssertion : PolicyAssertion
{
    public override IEnumerable<KeyValuePair<string, Type>> 
        GetExtensions()
    {
        return new KeyValuePair<string, Type>[] {
            new KeyValuePair<string, Type>(
                "replayDetection", 
                typeof(ReplayDetectionPolicyAssertion)) };
    }

    public override void WriteXml(XmlWriter writer)
    {
        if (writer == null) throw new ArgumentNullException("writer");

        writer.WriteStartElement("replayDetection");
        writer.WriteEndElement();
    }

    public override void ReadXml(XmlReader reader, 
        IDictionary<string, Type> extensions)
    {
        if (reader == null) throw new ArgumentNullException("reader");

        bool isEmpty = reader.IsEmptyElement;
        reader.ReadStartElement("replayDetection");
        if (!isEmpty) reader.ReadEndElement();
    }

    ...
}

The ReadXml and WriteXml methods are responsible for reading the policy assertion from and writing it to the XML policy file, respectively. The concept of an extension is used to provide a mapping between a local element name in the XML policy file with a CLR type representing this element. When a policy specified in an XML file contains a custom assertion (such as ReplayDetectionPolicyAssertion), this assertion type must be declared as an extension to the policy framework in a special extensions section at the beginning of the file, like this:

<policies>
  <extensions>
    <extension name="replayDetection" 
       type="WSE3ReplayDetection.ReplayDetectionPolicyAssertion, 
             WSE3ReplayDetection" />
  </extensions>
  <policy name="HelloWorldPolicy">
    <kerberosSecurity/>
    <replayDetection/>
  </policy>
</policies>

The extensions section may contain many extension elements, each of which defines and associates a local element name with a fully qualified CLR type name. When the policy parser comes across one of the names declared in the extensions section whenever an assertion is expected, an instance of the associated CLR type is created and the ReadXml method is called on it.

The extensions section can also be leveraged when your custom assertion is designed to expose an extensibility point. One deficiency of the ReplayDetectionPolicyAssertion described in this article is that the cache of MessageId values representing past messages is held in memory. When the application domain is recycled or a new pipeline instance is created in any other way, this state is lost. Holding the replay detection state in memory also does not allow you to detect replays across server farm nodes.

One way to solve this problem is to make the implementation of the replay detection cache an extensibility point. In scenarios that require persistency, an implementation can use a database instead of memory. Once you have a CLR type that defines a contract for such an extensibility point, you need to expose a property of this type on the ReplayDetectionPolicyAssertion as well as provide serialization and deserialization code for its parameters (such as a database connection string). The assertion deserialization code only requires that a class complies with the contract, but is not interested in its configuration details. The extensions mechanism can be used to register a mapping between a specific local element name and a CLR type implementing this contract, therefore providing a uniform mechanism for capturing extensibility points in the XML policy file. I have provided a full implementation of the extensible ReplayDetectionPolicyAssertion with the code download for this article.

Conclusion

As you've seen in this article, the policy framework in WSE 3.0 provides a general mechanism for transforming SOAP messages sent to and from Web services. Extensions to the policy framework implemented by WSE itself offer support for a broad range of security scenarios. Using the security policy assertion implementations included in WSE 3.0 can help you create apps that will interoperate with services created using Windows Communication Foundation. The policy framework is extensible and allows custom features to be implemented when processing SOAP messages while taking advantage of imperative and declarative policy specifications.

Tomasz Janczuk has worked on security for the Windows Communication Foundation since 2001. He also works on development for WSE 3.0. Tomasz's focus has been on SOAP-level security protocols and standards, most notably WS-SecurityPolicy 1.0 and 1.1.