Sunday, September 24, 2006

Web Service Security

In my last two posts I used the JAX-WS 2.0 API to implement a simple web service that I can access with either a SOAP or REST request.

The next thing I wanted to do was to add security to the service:

  1. Authentication. The sender and recipient should both be able to verify each other's identiy.
  2. Confidentiality. The data exchanged between the sender and recipient should be kept confidential from anyone watching network traffic.
  3. Data integrity. The sender and recipient should both have a way to verify that no one has tampered with the data they exchange.

I wanted to meet these security requirements without compromising interoperability, including access via REST. Consumers should be still able to access the service with a minimal toolkit consisting of an HTTP client, an XML parser and commonly used cryptographic functions.

My initial thought was to use the WS-Security standard as implemented by the XWSS project. This project builds in turn on the XML Digital Signature and XML Encryption APIs.

The focus of WS-Security seems to be on message-based security. The goal is to provide a standard for signing and encrypting all or part of a SOAP message regardless of the underlying transport mechanism (HTTP, SMTP, JMS, etc.). It avoids any reliance on transport-based security, notably SSL.

The complexity of the WS-Security standard seemed to pose an obstacle to ensuring universal access to my service. I looked at the public web services offered by a number of major companies and found that none of them used WS-Security. Instead they all used a mix of more established protocols and algorithms, along with their own simple conventions. Most of their approaches could be expressed using the WS-Security schema, but I have not seen a major public service that uses it.

Common security schemes for public web services seem to take one of two forms:

  1. Signature Verification. Consumers generate a unique signature for each request based on the submitted data, a timestamp (to prevent a replay attack) and a secret key (typically using the HMAC_SHA1 algorithm). The service re-generates the signature for each request and verifies a match.
  2. Session Authentication. A consumer always starts by submitting a user identifier a key or password to an authentication service. The service responds with a short-lived session id or token for authenticating subsequent requests.

Session authentication not guarantee data integrity by itself, but may be combined with signature verification to achieve that goal. Such hybrid schemes generate signatures with the temporary session token rather than with a permanent key.

Both schemes rely on SSL to encrypt requests and responses and therefore ensure confidentiality.

One drawback to the simple signature verification scheme using HMAC seems to be that it requires the server to permanently store a retrievable secret key for each consumer. That could be avoided by generating an RSA public and private key pair for each consumer and using that to sign messages instead. With RSA the server would only have to store the public key for each consumer. Despite this advantage the major services seem to use HMAC, perhaps because RSA implementations are not as widely available.

Hybrid schemes avoid the problem of storing a secret key because login passwords can be stored in hashed form. However, I am not sure how inconvenient it would be to require the initial login request as part of each service invocation.

After researching this I decided to start by implementing signature verification. Using WS-Security, requests to my example web service would look something like this:

<S:Envelope xmlns:S="http://www.w3.org/2001/12/soap-envelope"
                 xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
  <S:Header>
    <wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/04/secext">
      <wsse:UsernameToken Id="AccessKey">
        <wsse:Username>karl</wsse:Username>
      </wsse:UsernameToken>
      <ds:Signature>
        <ds:SignedInfo>
          <ds:CanonicalizationMethod 
            Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <ds:SignatureMethod 
            Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
          <ds:Reference URI="#SoapBody">
            <ds:DigestMethod 
              Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>LyLsF0Pi4wPU...</ds:DigestValue>
          </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>DJbchm5gK...</ds:SignatureValue>
      </ds:Signature>
    </wsse:Security>
  </S:Header>
  <S:Body Id="SoapBody">
    <ns1:MemberGroups>
      <MemberID>2</MemberID>
    </ns1:MemberGroups>
  </S:Body>
</S:Envelope>

I would have to fit the timestamp in there somewhere as well. If I were just specifying my own notation, the request would look like this:

<S:Envelope xmlns:S="http://www.w3.org/2001/12/soap-envelope"
                 xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
  <S:Header>
    <ns1:Security>
      <AccessID>karl</AccessID>
      <Timestamp>2005-01-31T23:59:59.183Z</Timestamp>
      <Signature>DJbchm5gK...</Signature>
    </ns1:Security>
  </S:Header>
  <S:Body Id="SoapBody">
    <ns1:MemberGroups>
      <MemberID>2</MemberID>
    </ns1:MemberGroups>
  </S:Body>
</S:Envelope>

The only significant piece of information missing from my simpler notation is a "canonicalization method." Standardization of XML attributes and elements does not seem necessary as long as the service mandates a few simple conventions:

  1. All requests use UTF-8 character encoding.
  2. Request elements such as MemberGroups never have attributes.
  3. Request elements do not use namespace prefixes.
  4. No white space between elements.

The extra verbosity of WS-Standard seems warranted if it makes it easier for most consumers to use the service. I am not sure this is the case.

No comments: