WS-Security Authentication and Digital Signatures with Web Services Enhancements

 

Matt Powell
Microsoft Corporation

December 2002

Applies to:
   Microsoft&174; ASP.NET
   Microsoft .NET Framework
   Microsoft Visual Studio&174; projects
   SOAP
   Web Services Enhancements 1.0 for Microsoft .NET
   WS-Security Specification

Summary: How to use Web Services Enhancements 1.0 for Microsoft .NET to take advantage of the WS-Security specification for authenticating and signing data for your Web services. (30 printed pages)

Download Web Services Enhancements 1.0 for Microsoft .NET.

Contents

Introduction
The WSE Environment
Basic UsernameToken Authentication
Sending X.509 Certificates as WS-Security Tokens
Digital Signatures
Conclusion
References

Introduction

With the advent of the Web Services Enhancements 1.0 for Microsoft .NET (WSE), Microsoft has provided its first toolset for implementing security within a SOAP message. No longer are Web services tied strictly to using the security capabilities of the underlying transport. Now a SOAP message on its own can be authenticated, its integrity verified, and can even be encrypted all within the SOAP envelope using the mechanisms defined by the WS-Security specification. In this article, we will be looking at how you can use WSE to take advantage of WS-Security for authenticating and signing data for your Web services.

The WSE Environment

WSE sits on top of the .NET Framework support for writing and consuming Web services. At the heart of the WSE support is the Microsoft.Web.Services.SoapContext class that provides an interface for inspecting the WS-Security header and other headers for incoming SOAP messages, and adding WS-Security and other headers for outgoing SOAP messages. For developers writing code to consume Web services, a wrapper class was created to enhance the framework capabilities by adding a SoapContext for the SOAP request and response. On the server, a Microsoft ASP.NET SOAP extension was created that validates incoming SOAP messages and creates a request and response SoapContext that can be accessed from within WebMethods.

Setting up the basic WSE environment involves configuring your ASP.NET application to use the WSE SOAP extension. It is possible to do this on a machine-wide basis by adding an entry in the Machine.config, but there may be cases where you do not need or want WSE support. We will be adding WSE support for a particular virtual directory. To do this, add a /configuration/system.web/webServices/soapExtensionTypes/Add element to the Web.config for your virtual directory. The added element should look something like the entry below with the exception that the type attribute should all be on one line (it was broken up here for readability).

<webServices>
  <soapExtensionTypes>
    <add type="Microsoft.Web.Services.WebServicesExtension, 
        Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral, 
        PublicKeyToken=31bf3856ad364e35" priority="1" group="0" />
  </soapExtensionTypes>
</webServices>

From the point of WSE, we are now ready to go. There will be additional configuration settings that we will need to make for certain aspects of WS-Security, and we will discuss those later. Now all we need to do is add a reference to Microsoft.Web.Services.dll in our Visual Studio .NET projects and we are ready to go.

Basic UsernameToken Authentication

Before we can attempt to digitally sign our SOAP messages, we first need to have the ability to figure out who is doing the signing. It doesn't do any good to validate the integrity of a message if the message came from Evil Chuck when we expected it to come from Bob. Therefore, we will begin our exploration of WS-Security by looking at the concept of UsernameTokens and how WSE allows us to validate them.

The UsernameToken element is defined in WS-Security to provide a means for doing basic username/password validation. If you have experience with HTTP, then you can consider this to be similar to Basic Authentication. WS-Security does provide some variation through three forms of the UsernameToken element. The different forms are shown here:

<!-- No Password -->
<UsernameToken>
  <Username>Bob</Username>
</UsernameToken>

<!-- Clear Text Password -->
<UsernameToken>
  <Username>Bob</Username>
  <Password Type="wsse:PasswordText">Opensezme</Password>
</UsernameToken>

<!-- Digest: SHA1 hash of base64-encoded Password -->
<UsernameToken>
  <Username>Bob</Username>
  <Password Type="wsse:PasswordDigest">
    QSMAKo67+vzYnU9TcMSqOFXy14U=
  </Password>
</UsernameToken>

The first form does not include a password, so consider this an option for situations where some other mechanism authenticates the individual and the username token is used only for identification. The second option includes the clear text password. As you might expect, the authentication process on the other end would involve checking some sort of database of valid usernames and passwords to see if there is a match. The third option sends a digest of the password instead of a clear text password. The good news about this approach is that the password is not sent on the wire and thus would be impossible for some evil intermediary to figure out. The bad news is that an evil intermediary could send the hashed password and then could be authenticated as the original sender.

To avoid this problem, the Web Services Security Addendum has added an additional safeguard. Instead of sending just a hash of the password, the addendum specifies that a digest version of the password should be sent. This digest contains a hash that is a combination of the password, a Nonce (functionally a unique string that identifies this request), and the creation time. Therefore no two password hashes would be the same. A version of the Revised UsernameToken is shown below.

<!-- Revised UsernameToken -->
<wsse:UsernameToken 
    xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" 
    wsu:Id="SecurityToken-58564463-5bdc-4a6b-a7fb-94a0d7357a20">
  <wsse:Username>Joe</wsse:Username>
  <wsse:Password Type="wsse:PasswordDigest">
    gpBDXjx79eutcXdtlULIlcrSiRs=
  </wsse:Password>
  <wsse:Nonce>
    h52sI9pKV0BVRPUolQC7Cg==
  </wsse:Nonce>
  <wsu:Created>2002-11-04T19:16:50Z</wsu:Created>
</wsse:UsernameToken>

Although every legitimate request will have a different hash, you do have to be careful about malicious users pulling the entire UsernameToken from someone else's legitimate request and placing it in their own illegitimate requests. You can minimize risk by setting the Timestamp expiration (another feature of WSE) to a sufficiently small period of time and logically enforcing the expiration on the server. For instance, you might want to indicate that a message expires after 30 seconds so that it will not propagate in a delayed fashion, and then on the server not accept UsernameTokens 30 seconds after the time entered in the Created element. Be aware that clock synchronization issues between machines may cause problems with rejecting valid requests and under certain situations allowing a window of aged requests. So, using the creation time is not a complete solution. To further eliminate the risk, a Web service could keep a table of the Nonce values from the recently received UsernameTokens and not allow a request if the Nonce had been used before. The table would only have to hold nonce entries until their expiration time was hit. If you did receive multiple requests with the same Nonce, you should consider throwing out both requests since it is possible the illegitimate request was received first. Also be aware that Nonce-checking does not protect against the scenario where an evil intermediary stops an incoming message from reaching its target and then replaces the message with its own using the UsernameToken in the original message. You would need to add a digital signature to the message to protect against this kind of attack. We discuss digital signatures later in this article.

Of course all the hashing protection in the world doesn't preclude the fact that both the sender and the receiver need to have knowledge of the user's password. On the client side, it can be expected that users may be prompted for their passwords. On the server side, however, there needs to be some sort of table in which to look up valid username and password pairs. The way WSE allows for this capability is through an extension mechanism called a Password Provider.

The Password Provider

WSE has defined an interface, Microsoft.Web.Services.Security.IPasswordProvider, for a class to implement in order to register as a Password Provider. The interface has one function, GetPassword, and it takes a Microsoft.Web.Services.Security.UsernameToken as input. The GetPassword function returns the password for the given user. The idea is that you can use whatever mechanism you want to store valid username/password pairs and then provide a class that implements the IPasswordProvider interface to allow WSE to access your particular password-storing mechanism. You even have the option of performing your own personal combination of digests and hashes on the UsernameToken, possibly with a shared secret, in order to further control your authentication infrastructure.

In order to inform WSE of your particular Password Provider, you must make an appropriate WSE configuration setting. This involves adding a microsoft.web.services element inside the configuration element for your application's configuration file. It also involves specifying the WSE class that can understand this particular configuration information. The following configSections can be added in the Machine.config or individual Web.configs on your machine. Again the type attribute must be entirely on one line but has been broken up here for readability. This node is located at /configuration/configSections in your .config file.

<configSections>
  <section name="microsoft.web.services"
    type="Microsoft.Web.Services.Configuration.WebServicesConfiguration,
    Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral, 
    PublicKeyToken=31bf3856ad364e35" />
</configSections>

For an ASP.NET application, the following entry could be added as a child of the parent configuration element in the Web.config in order to inform WSE of your particular Password Provider:

<microsoft.web.services>        
  <security>
    <!-- This is the class which will provide the password hashes
               for the UsernameToken signatures. -->
    <passwordProvider 
        type="WSE_Security.PasswordProvider, WSE-Security" />
  </security>
</microsoft.web.services>

The type attribute of the passwordProvider element indicates the class that you have written that implements the IPasswordProvider interface. In this case, my class is called PasswordProvider, is defined in the WSE_Security namespace, and is located in the WSE-Security.dll assembly. For the sake of simplicity my class returns a password of "opensezme" for every username. In most scenarios this is where you would implement logic that reads the appropriate password from a SQL database or some other storage location. The code for my trivial class is shown below:

namespace WSE_Security
{
    public class PasswordProvider : IPasswordProvider
    {
        public string GetPassword(UsernameToken token)
        {
            return "opensezme";
        }
    }
}

Creating a WebMethod That Uses WS-Security

Now we are actually ready to create a Web service and call it. The Web service I want to create will be a simple "Hello World" type of Web service, except I want to access the SoapContext for the class so that I can personalize the response. The code for my WebMethod is shown below.

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using Microsoft.Web.Services.Security;
using Microsoft.Web.Services;


namespace WSE_Security
{
    [WebService]
    public class Hello : System.Web.Services.WebService
    {
        [WebMethod]
        public string PersonalHello()
        {
            string response = "";

            // Only accept SOAP requests
            SoapContext requestContext = HttpSoapContext.RequestContext;
            if (requestContext == null)
            {
                throw new ApplicationException("Non-SOAP request.");
            }

            // Look through all the tokens in the Tokens collection
            // for a UsernameToken.
            foreach (SecurityToken tok in requestContext.Security.Tokens)
            {
                if (tok is UsernameToken)
                {
                    response += "Hello " + ((UsernameToken)tok).Username;
                }
            }
            return response;
        }

    }
}

Before I could do anything with WSE in my WebMethod, I had to add a reference to Microsoft.Web.Services.dll in my project. I then added two using clauses to the code for Microsoft.Web.Services and Microsoft.Web.Services.Security. I was then able to create my PersonalHello WebMethod.

The first thing my PersonalHello function does is fetch the SoapContext object for the request using the static HttpSoapContext.RequestContext member. Now I can access all the information in the WS-Security headers. If there is no context, my Web method returns a Fault otherwise I enumerate all the security tokens that have been sent with my request and check to see if any of them are UsernameTokens. If I find a UsernameToken, I build a response string that includes a greeting based off of the username.

Creating a Client That Uses WS-Security

In order to see how to take advantage of WS-Security with WSE on the client side, I created a simple Windows Form application that would call my Web service when I click a button. As I did for my Web service class, I added a reference to the Microsoft.Web.Services.dll and included the same using statements that we added in our server code.

For the client side, WSE provides the Microsoft.Web.Services.WebServicesClientProtocol class that inherits from the System.Web.Services.Protocols.SoapHttpClientProtocol class. The SoapHttpClientProtocol class is used when you select the Add Web Reference option from within Visual Studio .NET or you use the WSDL.exe utility to create client code based on a WSDL. What you can do is use either the Add Web Reference option from Visual Studio .NET or the WSDL.exe utility to generate proxy classes for your client, then change the generated proxy class from inheriting from SoapHttpClientProtocol to WebServicesClientProtocol. The proxy class will now have a RequestSoapContext and ResponseSoapContext property that you can use to access the WS-Security headers that you send and receive. If you use the Add Web Reference option, you will find the code for the generated proxy class under the Web References directory. For C# projects, you find a file called Reference.cs under the directory whose name matches the hostname where the WSDL resides. For Microsoft® Visual Basic® .NET projects, the file is called Reference.vb. I changed the class declaration from

public class Service1 :
    System.Web.Services.Protocols.SoapHttpClientProtocol {

to

public class Service1 : 
    Microsoft.Web.Services.WebServicesClientProtocol {

If you use the Visual Studio .NET Add Web Reference option, you have to be careful about making changes to the file where it places the generated code. If you choose the Update Web Reference option, then Visual Studio .NET regenerates the code and overwrites your changes.

In order to create a UsernameToken for the request, my client code looked like the following.

localhost.Hello proxy = new localhost.Hello();
proxy.Url = endpointInput.Text;
UsernameToken TextToken 
    = new UsernameToken(usernameInput.Text, 
                        passwordInput.Text,
                        PasswordOption.SendHashed);
proxy.RequestSoapContext.Security.Tokens.Add(TextToken);
string result;
try
{
    result = proxy.PersonalHello();
}
catch (Exception ex)
{
    result = ex.Message;
}
MessageBox.Show(result);

The only difference between this code and the code that would call a normal Web service is that I have created a UsernameToken object and added it to the Tokens collection for the request. The constructor for the UsernameToken object takes three parameters: the username, the password, and the PasswordOption. In this case I am sending the password hashed. A condensed version of the SOAP request generated from this code is shown below. You will notice that a Security header is included with the request that includes a UsernameToken child element that contains the username, hashed password, a random Nonce that we can use to identify this particular request, and the creation time. The entire message is included at the end of this article in the reference section.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Header>
       ...
    <wsse:Security 
        soap:mustUnderstand="1"
        xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
      <wsse:UsernameToken 
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" 
          wsu:Id="SecurityToken-58564463-5bdc-4a6b-a7fb-94a0d7357a20">
        <wsse:Username>Joe</wsse:Username>
        <wsse:Password Type="wsse:PasswordDigest">
          gpBDXjx79eutcXdtlULIlcrSiRs=
        </wsse:Password>
        <wsse:Nonce>
          h52sI9pKV0BVRPUolQC7Cg==
        </wsse:Nonce>
        <wsu:Created>2002-11-04T19:16:50Z</wsu:Created>
      </wsse:UsernameToken>
    </wsse:Security>
  </soap:Header>
  <soap:Body>
    <PersonalHello xmlns="http://tempuri.org/" />
  </soap:Body>
</soap:Envelope>

The message can now be sent to the Web service where the WSE SOAP extension will validate the general format of the request, checks the password hash against the password retrieved from the Password Provider, and then if all goes well, the request makes it to our Web method. Our Web method receives the incoming request with the populated SoapContext, looks for a UsernameToken in the Security header, and builds the response string based off the name indicated. The response comes back as a normal WebMethod response and our client application displays the string returned. We have successfully run our first WS-Security application!

Validating the UsernameToken on the Server

Although WSE verifies the Security header syntax and checks the password hash against the password from the Password Provider, there is some extra verification that must be performed on the request. For instance, there may be more than one UsernameToken element included with the request. WS-Security provides support for including any number of tokens with a request that may be used for different purposes. The fact that our client code added a UsernameToken to the Tokens collection should have been a clear warning that more than one token could be included with a single message. In fact, if you modified the client code to create a second UsernameToken and add it to the collection, our current code would return a string similar to the following:

Hello Bob.Hello Alice.

I modified the code for our Web method to verify that the UsernameToken includes a hashed password and to only accept incoming requests with a single UsernameToken. My modified code is listed below.

[WebMethod]
public string PersonalHello()
{
    SoapContext requestContext = HttpSoapContext.RequestContext;
    if (requestContext == null)
    {
        throw new ApplicationException("Non-SOAP request.");
    }
    // We only allow requests with one security token
    if (requestContext.Security.Tokens.Count == 1)
    {
        foreach (SecurityToken tok 
                     in requestContext.Security.Tokens)
        {
            // Only accept UsernameTokens
            if (tok is UsernameToken)
            {
                UsernameToken UserToken = (UsernameToken)tok;
                // Only accept UsernameTokens with a 
                // hashed password.
                if (UserToken.PasswordOption 
                    == PasswordOption.SendHashed)
                {
                    return "Hello " + UserToken.Username;
                }
                else
                {
                    throw new SoapException(
                        "Invalid UsernameToken password type.", 
                        SoapException.ClientFaultCode);
                }
            }
            else
            {
                throw new SoapException(
                    "UsernameToken security token required.", 
                    SoapException.ClientFaultCode);
            }
        }
    }
    else
    {
        throw new SoapException(
            "Request must have exactly one security token.", 
            SoapException.ClientFaultCode);
    }
    return null;
}

We already mentioned issues of malicious users that might try to use someone else's UsernameToken element. This is a form of a replay attack that would require quite a bit more code than is shown here to protect against it. Instead, let's look at another more secure option.

Sending X.509 Certificates as WS-Security Tokens

Besides UsernameTokens, WS-Security also defines a BinarySecurityToken element for holding a couple of well-known types of tokens that have their own proprietary formats. The specification defines BinarySecurityToken types for X.509 v3 certificates and Kerberos v5 tickets. WSE supports X.509 certificates, and you will find that in a lot of ways they are treated the same as the UsernameToken.

Certificate Stores

Before we can include a certificate with our request, we have to figure out where to find it. In traditional Microsoft® Windows software development, certificates were kept in a central location called a certificate store. Each user has a private store of certificates to use to do things such as digitally sign e-mail messages or to authenticate with Web servers over SSL. The certificates themselves are not private since they are just a digitally signed way of handing out public keys. What is important is that the private key that corresponds to the public key in the certificate is in the secure key store of the individual whose certificate is used. This will allow users to digitally sign an entity with their private key and then anyone would be able to verify their signature with the public key in the certificate.

The initial release of the .NET Framework did not have classes for accessing the Windows certificate stores although it did have classes for handling X.509 certificates. However, WSE has provided classes that allow you to open the Windows certificate stores and use the certificates from this easily managed location. The following code uses the Microsoft.Web.Services.Security.X509 class to fill a listBox with the common names of all the certificates in a user's personal certificate store.

  ...
using Microsoft.Web.Services;
using Microsoft.Web.Services.Security;
using Microsoft.Web.Services.Security.X509;
  ...
private X509CertificateStore store;
  ...
private void Form1_Load(object sender, System.EventArgs e)
{
    store = X509CertificateStore.CurrentUserStore(
        X509CertificateStore.MyStore);
    store.OpenRead();
    foreach(X509Certificate cert in store.Certificates)
    {
        listBox1.Items.Add(cert.GetName());
    }
}

For an example of a full-featured dialog for selecting an X.509 certificate from a certificate store, see X509CertificateStoreDialog.cs in the WSE sample code directory.

Adding the X.509 Certificate Token to a SOAP Message

Now that we can find our personal certificates, adding them to our SOAP request is very similar to adding the UsernameToken that we used before. The following code gets the certificate from the already opened certificate store based off their selection from the listBox that we populated earlier, and then adds the binary token created from the certificate to the Tokens collection.

X509Certificate cert =
    (X509Certificate)store.Certificates[listBox1.SelectedIndex];
proxy.RequestSoapContext.Security.Tokens.Add(
    new X509SecurityToken(cert));

Accessing the certificate on the server side is also very similar to accessing the UsernameToken. A modified version of our previous Web method logic that uses the certificate's common name to build the personalized response is shown below.

[WebMethod]
public string PersonalHello()
{
    // Only accept SOAP requests
    SoapContext requestContext = HttpSoapContext.RequestContext;
    if (requestContext == null)
    {
        throw new ApplicationException("Non-SOAP request.");
    }
    // We only allow requests with one security token
    if (requestContext.Security.Tokens.Count == 1)
    {
        foreach (SecurityToken tok in requestContext.Security.Tokens)
        {
            // Only accept X.509 Certificates
            if (tok is X509SecurityToken)
            {
                X509SecurityToken certToken = (X509SecurityToken)tok;
                return "Hello " + certToken.Certificate.GetName();
            }
            else
            {
                throw new SoapException(
                    "X.509 security token required.", 
                    SoapException.ClientFaultCode);
            }
        }
    }
    else
    {
        throw new SoapException(
            "Request must have exactly one security token.", 
            SoapException.ClientFaultCode);
    }
    return null;
}

Digital Signatures

Simply sending X.509 certificates with a request is actually not much of a way to authenticate anything. The certificates are considered public knowledge so anyone could include anyone else's certificate with their request. The mechanism for using certificates for authentication is based off the idea we mentioned earlier in regards to signing some entity with the private key that corresponds to the public key in the certificate. In the case of sending a SOAP message, if Bob creates a digital signature of the SOAP body element with his private key, he can include the corresponding certificate along with the signature in the headers of the request so that all who receive the message can verify that the request came from Bob and that it was unaltered since he signed it. Including the certificate with the request is not required but it does make it convenient for those trying to validate the signature.

WSE supports creating digital signatures in a very straightforward manner. Along with the Tokens collection there is a SoapContext.Security.Elements collection that will allow you to add various WS-Security elements including a Signature element. Building from our previous client code that included a digital certificate, we now use that same certificate to sign the request. The code to do this is shown below.

X509Certificate cert = 
    (X509Certificate)store.Certificates[listBox1.SelectedIndex];
X509SecurityToken certToken = new X509SecurityToken(cert);
proxy.RequestSoapContext.Security.Tokens.Add(certToken);
proxy.RequestSoapContext.Security.Elements.Add(
    new Signature(certToken));

Just as we did before, we pull the certificate from the previously opened certificate store and add it to the Tokens collection. We then create a new Signature object using the X509SecurityToken as a parameter for the Signature constructor and add the Signature object to the Elements collection. An abbreviated view of the SOAP message generated from this code is shown below. The complete SOAP message is included at the end of this article in the References section.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Header>
    <wsrp:path 
        soap:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soap:mustUnderstand="1" 
        xmlns:wsrp="http://schemas.xmlsoap.org/rp">
      <wsrp:action 
          wsu:Id="Id-b856ae70-7a1b-4895-a05c-5f6596ca4429" 
             ...
    </wsrp:path>
             ...
    <wsse:Security 
        soap:mustUnderstand="1" 
        xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
      <wsse:BinarySecurityToken 
          ValueType="wsse:X509v3" 
          EncodingType="wsse:Base64Binary" 
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" 
          wsu:Id="SecurityToken-f6f96b4b-23c5-421e-92ff-f1050d531e82">
        MIIGkzCCBXugAwIBAgIK  . . . 39Vmjd20Lw==
      </wsse:BinarySecurityToken>
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod 
              Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
          <SignatureMethod 
              Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
          <Reference URI="#Id-24cc3660-6f1a-41fe-a949-71d7ed9fc636">
            <Transforms>
              <Transform 
                  Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>
              /8iL3OP9mfzuixI/ilkhHMbatV0=
            </DigestValue>
          </Reference>
          <Reference URI="#Id-b856ae70-7a1b-4895-a05c-5f6596ca4429">
            <Transforms>
               ...
        </SignedInfo>
        <SignatureValue>
ZY4MhHzBYz+CBdAz1LhAFjy6QxQoKJoA7l2eG45QV0hDIJrmXwLEGrPnpX+uPan5+MS6hm+oL
/sGTbKJ/DJMp/t5ZyqY1qvngGQLcYXRy538zemwFfeGN5R2wmOoUSeCBUqprQVUbnkz+qlVp/
5f7t7VGW2Ee55Q3ol+ApVoFQE=
        </SignatureValue>
        <KeyInfo>
          <wsse:SecurityTokenReference>
            <wsse:Reference 
    URI="#SecurityToken-f6f96b4b-23c5-421e-92ff-f1050d531e82" />
          </wsse:SecurityTokenReference>
        </KeyInfo>
      </Signature>
    </wsse:Security>
  </soap:Header>
  <soap:Body 
      wsu:Id="Id-24cc3660-6f1a-41fe-a949-71d7ed9fc636"
      xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
    <PersonalHello xmlns="http://tempuri.org/" />
  </soap:Body>
</soap:Envelope>

For a thorough understanding of the various header elements included in this request, see some of the other specifications like WS-Routing and the Web Services Security Addendum. For our discussion, we will look at the specifics of how WSE used WS-Security to sign our request.

The first thing to note about this message is that there is a BinarySecurityToken element inside the Security header. The attributes for this element indicate that it is a X.509 v3 certificate that is Base 64 encoded. This is the X509SecurityToken that we added to the Tokens collection in our last two examples. Again, it is not required to be included with the message, but it makes validating the signature convenient.

The next thing to notice is that that there is now a Signature element in the Security header. There are three child elements of the Signature: the SignedInfo element, the SignatureValue element, and the KeyInfo element. The SignedInfo element defines the exact data that is being signed in this request, how it is being canonicalized, and what algorithm is being used for computing the signature. The important item to understand is the list of reference elements that define what in this message is actually being signed. I removed a number of reference elements for brevity, but you will notice that for the two that are listed, there is a URI attribute associated with each. That URI attribute corresponds to the ID attribute of the element being included in the signature. For instance, the first reference indicates a URI that begins with "#Id-24cc3660-". If you search through the message you will see that this URI corresponds to the ID for the Body element within our message. Therefore the Body is part of the signed information for this request. The second reference refers to the action element in the Path header that is part of the WS-Routing specification. WSE automatically includes the Path header and Timestamp header, and much of the additional information in these headers is included in the digital signature. The following is a list of the elements that WSE will reference in the SignedInfo header and thus be included in the digital signature.

/soap:Envelope/soap:Header/wsrp:path/wsrp:action
/soap:Envelope/soap:Header/wsrp:path/wsrp:to
/soap:Envelope/soap:Header/wsrp:path/wsrp:from
/soap:Envelope/soap:Header/wsrp:path/wsrp:id
/soap:Envelope/soap:Header/wsu:Timestamp/wsu:Created
/soap:Envelope/soap:Header/wsu:Timestamp/wsu:Expires
/soap:Envelope/soap:Body

The reason subelements of the path and Timestamp headers are signed instead of the entire path and Timestamp elements is that it is expected in WS-Routing scenarios that intermediaries would add elements within the path and Timestamp headers as the message propagates through SOAP level routers. Therefore, a digital signature would break if anything in those headers were modified.

The last thing to notice about the Signature element in this request is that the KeyInfo has a reference back to the BinarySecurityToken that contains our certificate. This indicates where the key came from that was used to sign this request. Of course, for a certificate it is the private key that corresponds to the public key in the certificate that was used to create the digital signature. The public key can then be used to verify that the data could only come from someone who knows the corresponding private key.

Verifying the Digital Signature

The WSE SOAP extension will validate the syntax of a digital signature, but simply knowing that a valid signature exists in a message is not enough to know that the message came from a particular individual. WS-Security and the XML Digital Signature specification, (Extensible Markup Language) XML-Signature Syntax and Processing, are very flexible in the way that they allow signatures to be included within XML. For instance, you may be expecting a message from Alice, you might see that the message has a signature from Alice, but that signature could conceivably only be signing the routing information in the path header. There is no guarantee that the thing you are probably most concerned with (usually the body of the message) came from Alice unless you do some of your own verification.

The following code looks for the presence of a Signature with the request, verifies that the body of the message is signed using the SignatureOptions property, and then, since we know exactly who sent the message and that it arrived just as it was sent, constructs a personal hello based off the certificate used to sign the request.

[WebMethod]
public string PersonalHello()
{
    // Only accept SOAP requests
    SoapContext requestContext = HttpSoapContext.RequestContext;
    if (requestContext == null)
    {
        throw new ApplicationException("Non-SOAP request.");
    }
    // Look for Signatures in the Elements collection
    foreach (Object elem in requestContext.Security.Elements)
    {
        if (elem is Signature)
        {
            Signature sign = (Signature)elem;
            // Verify that signature signs the body of the request
            if (sign != null 
                && (sign.SignatureOptions &
                    SignatureOptions.IncludeSoapBody) != 0)
            {
                // Determine what kind of token is used 
                // with the signature.
                if (sign.SecurityToken is UsernameToken)
                {
                    return "Hello " +
                        ((UsernameToken)sign.SecurityToken).Username;
                }
                else if (sign.SecurityToken is X509SecurityToken)
                {
                    return "Hello " +
                       ((X509SecurityToken)sign.SecurityToken)
                       .Certificate.GetName();
                }
           }
        }
    }
    // No approriate signature found
    throw new SoapException("No valid signature found", 
        SoapException.ClientFaultCode);
}

Signing a Partial Message

In the previous example we created a WSE default signature on the SOAP message. The signature encompasses the SOAP body and various portions of the SOAP header that make sense in most cases. There are scenarios where you may not care to sign the default portions of a message or even the entire SOAP body. For instance, it might make sense for an intermediary to modify portions of the SOAP body which would invalidate the signature. Therefore it would be nice if we had control over exactly what is signed and what is not.

You have two options for determining what portions of a message are covered by the digital signature. We used the SignatureOptions property of the Signature class to determine if the signature included the SOAP body in the previous example. You can also use the SignatureOptions property to specify what portions of the SOAP message to be covered by the signature. You can use any combination of the following flags defined by the SignatureOptions enumeration:

IncludePath
IncludePathAction
IncludePathFrom
IncludePathId
IncludePathTo
IncludeSoapBody
IncludeTimestamp
IncludeTimestampCreated
IncludeTimestampExpires

There is one more value that you can set the SignatureOptions to and that is IncludeNone. This indicates that none of the options above should be included in the signature. So why would you bother to sign a message if you were going to specify the IncludeNone SignatureOption? The answer is found by using the other mechanism for determining what portions of the message we want to send.

WS-Security gives you two options for determining what is being signed and what is not. One option is to specify a transform on the message and then to create the signature based on the results. However, WSE does not support this option. The other possibility is to specify an Id attribute on the element that you want to sign and then include a reference to that element in the SignedInfo portion of the Signature element. This is how WSE creates its signatures for the various options that are part of the SignatureOptions enumeration. But WSE also allows you to create the Id attribute yourself and then to manually add the reference to the SignedInfo portion of the signature. This allows you to sign custom SOAP headers as well as any particular portions of the body that you might want to sign. You just need to make sure that the Id attribute is set and that the href for it is included in the references. We are going to look at an example where we will sign only a portion of the SOAP body.

ASP.NET Web services use the XmlSerializer from the .NET Framework to create and parse the SOAP messages that it sends and receives. You can control many of the details of exactly how the serialization occurs by flagging your content with attributes that control how the XmlSerializer works. Therefore it is not difficult to add an Id attribute to part of the XML, set its value and then add that value to the signature's references.

To illustrate how to do this, I created an ASP.NET WebMethod that returns a complex type that includes a purchase order. I want to digitally sign only the actual purchase order element of the XML so that the client will know for certain that it came from me. First we created the complex class that will be returned from our WebMethod. In this class, it is the actual purchase order number, but there is also an ID property that will be serialized as an attribute. This will be the ID that will be referenced in our signature. It is critical that we include the right namespace for this attribute that matches the namespace in the WS-Security specification so we use the Namespace parameter of the XmlAttribute attribute to set it to http://schemas.xmlsoap.org.ws/2002/07/utility.

public class PONum
{
    [XmlAttribute("Id", 
      Namespace="http://schemas.xmlsoap.org/ws/2002/07/utility")]
    public string ID;
    [XmlText]
    public string PONumber;
} 

Our WebMethod will have to do a number of things. First it will need to generate a GUID that will be used as the unique ID. It will have to create an instance of the class above and set the ID property so that it is in the proper format with the GUID value included. It also sets the value of the actual purchase order number that, for our particular example, is always the same. Next, it will create a security token based off the X.509 certificate that will be used to sign the purchase order number.

In order to use a certificate from ASP.NET, it must be in a certificate store that is accessible from within ASP.NET working process. Similarly the private key that corresponds to the public key in the certificate must be in a key store that is accessible from ASP.NET. For my example, I used a certificate in the machine's certificate store. In order to get this to work I gave permissions to the "ASPNET user" (the account that the ASP.NET worker process runs as) to the machine's physical key store. This potentially opens up access for other applications to the machine's key store so you should make changes like this with a great deal of caution. See information in the WSE documentation entitled "Managing X.509 Certificates" for more information on issues around storing and accessing certificates and keys on your machine.

After we have the security token, we can create an instance of the Signature passing our token in the constructor. We immediately set the SignatureOptions property to SignatureOptions.IncludeNone so that it won't include all the default portions of the message that it would normally include. Now we call the Signature object's AddReference method, specifying the same string that we set the ID property to earlier except that we prepend a '#' symbol in order to indicate that it is a local reference. The code for the Web method is shown below:

[WebMethod]
public PONum GetPONumber()
{
    SoapContext responseContext 
        = HttpSoapContext.ResponseContext;
    PONum PO = new PONum();
    Guid referenceID = Guid.NewGuid();
    PO.ID = "Id:" + referenceID.ToString();
    PO.PONumber = "PO10025";
    X509CertificateStore store = 
        X509CertificateStore.LocalMachineStore(
            X509CertificateStore.MyStore);
    store.OpenRead();
    X509Certificate cert 
        = store.FindCertificateBySubjectName(
            "CN = mattpo.redmond.corp.microsoft.com")[0];
    X509SecurityToken token = new X509SecurityToken(cert);
    responseContext.Security.Tokens.Add(token);
    Signature sig = new Signature(token);
    sig.SignatureOptions = SignatureOptions.IncludeNone;
    sig.AddReference(new Reference("#" + PO.ID));
    responseContext.Security.Elements.Add(sig);
    return PO;
}

The last step is for the client to check for the appropriate signature on the returned purchase order. We do that by verifying that the ID returned is in the references for the signature, and that the certificate that was used for that signature is the one we expect. The code for calling the Web service and verifying the certificate used is shown below.

localhost.Service1Wse proxy = new localhost.Service1Wse();
localhost.PONum po = proxy.GetPONumber();
foreach (Object element in proxy.ResponseSoapContext.Security.Elements)
{
    if (element is Signature)
    {
        Signature sig = (Signature)element;
        foreach (Reference reference in sig.SignedInfo.References)
        {
            if (reference.Uri == "#" + po.Id)
            {
                X509Certificate signatureCert 
                    = ((X509SecurityToken)
                            sig.SecurityToken).Certificate;
                if (signatureCert.Equals(poCert))
                    MessageBox.Show("It's signed!");
            }
        }
    }
}

Conclusion

This article illustrates how Web Services Enhancements for Microsoft .NET offers you a glimpse into the capabilities of the WS-Security specification by looking at how you can use WSE to do UsernameToken authentication and by using X.509 authentication to digitally sign portions of the SOAP message. WSE lets you inspect and verify the kind of WS-Security support that is included with a SOAP message. There are a number of other WS-Security capabilities WSE provides that are not discussed here, such as using various forms of encryption. I encourage you to install WSE, write some code, and inspect the messages created and the messages included in the References section to get a feel for how WS-Security can enhance the functionality of your Web services.

References

SOAP Message with Hashed UsernameToken

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Header>
    <wsrp:path 
        soap:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soap:mustUnderstand="1" 
        xmlns:wsrp="http://schemas.xmlsoap.org/rp">
      <wsrp:action>
        http://tempuri.org/PersonalHello
      </wsrp:action>
      <wsrp:to>
        http://localhost:8080/wsdk-security/verifiedhello.asmx
      </wsrp:to>
      <wsrp:id>
        uuid:14f61c50-586c-42ec-8286-c5c9fa8bfce1
      </wsrp:id>
    </wsrp:path>
    <wsu:Timestamp 
        xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
      <wsu:Created>2002-11-04T19:16:50Z</wsu:Created>
      <wsu:Expires>2002-11-04T19:21:50Z</wsu:Expires>
    </wsu:Timestamp>
    <wsse:Security 
        soap:mustUnderstand="1"
        xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
      <wsse:UsernameToken 
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" 
          wsu:Id="SecurityToken-58564463-5bdc-4a6b-a7fb-94a0d7357a20">
        <wsse:Username>Joe</wsse:Username>
        <wsse:Password Type="wsse:PasswordDigest">
          gpBDXjx79eutcXdtlULIlcrSiRs=
        </wsse:Password>
        <wsse:Nonce>
          h52sI9pKV0BVRPUolQC7Cg==
        </wsse:Nonce>
        <wsu:Created>2002-11-04T19:16:50Z</wsu:Created>
      </wsse:UsernameToken>
    </wsse:Security>
  </soap:Header>
  <soap:Body>
    <PersonalHello xmlns="http://tempuri.org/" />
  </soap:Body>
</soap:Envelope>

SOAP Message with X.509 Certificate Token Used to Digitally Sign the Message

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Header>
    <wsrp:path 
        soap:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soap:mustUnderstand="1" 
        xmlns:wsrp="http://schemas.xmlsoap.org/rp">
      <wsrp:action 
          wsu:Id="Id-b856ae70-7a1b-4895-a05c-5f6596ca4429" 
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
        http://tempuri.org/PersonalHello
      </wsrp:action>
      <wsrp:to 
          wsu:Id="Id-e3fa8752-df7d-4a16-a883-f98800bf24f7"
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
        http://localhost:8080/wsdk-security/signedhello.asmx
      </wsrp:to>
      <wsrp:id 
          wsu:Id="Id-64bdd986-1d54-4176-bbc3-c38255fcfedf" 
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
        uuid:15810d11-91d9-44cb-b3c8-016ff9d93b78
      </wsrp:id>
    </wsrp:path>
    <wsu:Timestamp 
        xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
      <wsu:Created 
          wsu:Id="Id-134651fa-1791-4679-abf7-cbbc100fbeb9">
        2002-11-05T23:35:59Z
      </wsu:Created>
      <wsu:Expires 
          wsu:Id="Id-9c106440-3955-4f62-903d-c30c6c9e8d27">
        2002-11-05T23:40:59Z
      </wsu:Expires>
    </wsu:Timestamp>
    <wsse:Security 
        soap:mustUnderstand="1" 
        xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
      <wsse:BinarySecurityToken 
          ValueType="wsse:X509v3" 
          EncodingType="wsse:Base64Binary" 
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" 
          wsu:Id="SecurityToken-f6f96b4b-23c5-421e-92ff-f1050d531e82">
        MIIGkzCCBXugAwIBAgIK  . . . 39Vmjd20Lw==
      </wsse:BinarySecurityToken>
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod 
              Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
          <SignatureMethod 
              Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
          <Reference URI="#Id-24cc3660-6f1a-41fe-a949-71d7ed9fc636">
            <Transforms>
              <Transform 
                  Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>
              /8iL3OP9mfzuixI/ilkhHMbatV0=
            </DigestValue>
          </Reference>
          <Reference URI="#Id-b856ae70-7a1b-4895-a05c-5f6596ca4429">
            <Transforms>
              <Transform 
                  Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>
              rH3m6W9zglaAMMzP7sD9yvwzEdA=
            </DigestValue>
          </Reference>
          <Reference URI="#Id-e3fa8752-df7d-4a16-a883-f98800bf24f7">
            <Transforms>
              <Transform 
                  Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>
              tzgO0kc7HSomAxAokgHw3/hkL+E=
            </DigestValue>
          </Reference>
          <Reference URI="#Id-64bdd986-1d54-4176-bbc3-c38255fcfedf">
            <Transforms>
              <Transform 
                  Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>
              Ls05OCaMfqqaIFj2vV8a/aHQQBo=
            </DigestValue>
          </Reference>
          <Reference URI="#Id-134651fa-1791-4679-abf7-cbbc100fbeb9">
            <Transforms>
              <Transform 
                  Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>
              VqOIop+9EeJXGIwB3TqAqXgiKUI=
            </DigestValue>
          </Reference>
          <Reference URI="#Id-9c106440-3955-4f62-903d-c30c6c9e8d27">
            <Transforms>
              <Transform 
                  Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>
              3d9WMNwuLIrRfSyaWCsl63d+wDA=
            </DigestValue>
          </Reference>
        </SignedInfo>
        <SignatureValue>
ZY4MhHzBYz+CBdAz1LhAFjy6QxQoKJoA7l2eG45QV0hDIJrmXwLEGrPnpX+uPan5+MS6hm+oL/sGTbKJ/DJMp/t5ZyqY1qvngGQLcYXRy538zemwFfeGN5R2wmOoUSeCBUqprQVUbnkz+qlVp/5f7t7VGW2Ee55Q3ol+ApVoFQE=
        </SignatureValue>
        <KeyInfo>
          <wsse:SecurityTokenReference>
            <wsse:Reference 
    URI="#SecurityToken-f6f96b4b-23c5-421e-92ff-f1050d531e82" />
          </wsse:SecurityTokenReference>
        </KeyInfo>
      </Signature>
    </wsse:Security>
  </soap:Header>
  <soap:Body 
      wsu:Id="Id-24cc3660-6f1a-41fe-a949-71d7ed9fc636"
      xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
    <PersonalHello xmlns="http://tempuri.org/" />
  </soap:Body>
</soap:Envelope>

SOAP Message with X.509 Certificate Token Used to Digitally Sign a Portion of the Message

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Header>
    <wsu:Timestamp
        xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
      <wsu:Created>2002-11-07T21:51:51Z</wsu:Created>
      <wsu:Expires>2002-11-07T21:56:51Z</wsu:Expires>
    </wsu:Timestamp>
    <wsse:Security 
        soap:mustUnderstand="1"
        xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
      <wsse:BinarySecurityToken 
          ValueType="wsse:X509v3" 
          EncodingType="wsse:Base64Binary"
          xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
          wsu:Id="SecurityToken-547309ee-532f-40ce-a370-a64be85e977e">
        MIIHRjC ... HVUjaoy
      </wsse:BinarySecurityToken>
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod 
              Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
          <SignatureMethod 
              Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
          <Reference URI="#Id:ce249a29-aa9a-427a-b0c4-830cdc7f481a">
            <DigestMethod 
                Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>7yhtDGpxNtkFGT9+1vWHI7sQL1c=</DigestValue>
          </Reference>
        </SignedInfo>
        <SignatureValue>
tNj18ILnxAyc/3AoNCRb+ZBcYcIp5KCKTFLCTNhAzuokk5m1S8FOBvFYTUdy1qCU2i655/KCcIzZ7lzLSqY57iaoWgdpQBAWvEEhxkSNuGGl/qoknNhc4B2SN24t1AniB4UwNFvo2u6rHiBr3nSfAv0rSPuGa32c3Ri8LRcqZ5M=
        </SignatureValue>
        <KeyInfo>
          <wsse:SecurityTokenReference>
            <wsse:Reference 
                URI=
                "#SecurityToken-547309ee-532f-40ce-a370-a64be85e977e" />
          </wsse:SecurityTokenReference>
        </KeyInfo>
      </Signature>
    </wsse:Security>
  </soap:Header>
  <soap:Body>
    <GetPONumberResponse xmlns="http://tempuri.org/">
      <GetPONumberResult 
          d4p1:Id="Id:ce249a29-aa9a-427a-b0c4-830cdc7f481a" 
          xmlns:d4p1="http://schemas.xmlsoap.org/ws/2002/07/utility">
        PO10025
      </GetPONumberResult>
    </GetPONumberResponse>
  </soap:Body>
</soap:Envelope>

Show: