Sunday, October 22, 2006

Implementing Web Service Security

In my last post I looked into adding a security scheme to the example web service I implemented with JAX-WS 2.0.

I shied away from WS-Security because I wanted my service to be easy to access even with a minimal toolkit, without compromising authentication, confidentiality or data integrity. I opted to rely on SSL as the transport mechanism, and to define my own concise notation for signing each message with a shared secret key:

<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 elements of the scheme are:

AccessID A unique identifier assigned to each user who is authorized to access the web service.
Timestamp The current time to millisecond precision. The timestamp submitted by the user must be expressed in the UTC time zone, and must be within 5 minutes of the server clock for the message to be authenticated successfully.
Signature A message authentication code (MAC) generated using the HMAC_SHA1 algorithm, based on a shared secret key and a text string.

The text string used to generate the MAC has the following form:

Timestamp:AccessID:CanonicalizedMessage

The canonicalized message is the content of the Body element from the SOAP message. All spaces between elements must be stripped, and the top-level element must only have a namespace prefix. For example:

2006-09-28T07:05:07.484Z:karl:<ns1:MemberGroups><MemberID>2</MemberID></ns1:MemberGroups>

To simplify the creation of the security element for outbound messages from the client, as well as the authentication of inbound messages to the server, I wrote three small utility classes described below.

The simplest utility to handle the timestamp element of the security header. The timestamp string must conform to the standard Internet time/date format, and must furthermore be in the UTC time zone:

package member.common;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;

import java.util.Date;
import java.util.TimeZone;

/**
 * Creates and parses timestamps in IETF RFC 3339 format.
 **/
public class Timestamp {

  private static DateFormat getFormatter() {

    DateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
    f.setTimeZone(TimeZone.getTimeZone("UTC"));

    return f;
  }

  public static String create() {
    return getFormatter().format(new Date());
  }

  public static Date parse(String timestamp) {
    try {
      return getFormatter().parse(timestamp);
    } catch (ParseException e) {
      throw new RuntimeException("Invalid timestamp " + timestamp);
    }
  }
}

Generating a canonicalized version of the message is simple as long our schema consists of simple elements without attributes:

package member.common;

import java.util.Iterator;

import org.w3c.dom.CharacterData;
import org.w3c.dom.Node;
import javax.xml.soap.SOAPElement;

/**
 * Transforms the contents of a SOAP element into a standardized text
 * string.
 **/
public class Canonicalizer {

  public static String toText(SOAPElement element) {
    
    StringBuilder buffer = new StringBuilder();
    appendElement(buffer, element.getChildElements());

    return buffer.toString();
  }

  private static void appendElement(StringBuilder buffer, Iterator i) {
    
    while (i.hasNext()) {
      
      Node node = (Node) i.next();
      if (node.getNodeType() == Node.TEXT_NODE) {

        buffer.append(((CharacterData) node).getData());

      } else if (node instanceof SOAPElement) {

        SOAPElement element = (SOAPElement) node;
        String tag = element.getTagName();

        buffer.append("<").append(tag).append(">");
        appendElement(buffer, element.getChildElements());
        buffer.append("");
      }
    }
  }
}

Creating the signature is also a straightforward application of the HMAC_SHA1 algorithm:

package member.common;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Signature {

  private static final String MAC_ALG = "HmacSHA1";
  private static final String DIGEST_ALG = "SHA-1";

  /**
   * Creates a messsage authentication code for a text string.
   *
   * @param key A shared key
   * @param message A text message
   * @return A hex string representing the message authentication code
   **/
  public static String create(String key, String message) {

    try {

      byte[] keyBytes = key.getBytes("UTF-8");
      byte[] messageBytes = message.getBytes("UTF-8");

      Mac mac = Mac.getInstance(MAC_ALG);
      mac.init(new SecretKeySpec(keyBytes, MAC_ALG));

      byte[] signature = mac.doFinal(messageBytes);

      return toHex(signature);

    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Converts a binary messsage authentication code into a hex string.
   **/
  public static String toHex(byte[] bytes) {
    
    StringBuffer buffer = new StringBuffer(bytes.length * 2);
    for (int i = 0; i < bytes.length; i++) {

      int b = bytes[i];
      int hi = (b & 0xF0) >>> 4;
      buffer.append(Character.forDigit(hi, 16));
      int lo = b & 0x0F;
      buffer.append(Character.forDigit(lo, 16));
    }

    return buffer.toString();
  }
}

My next post will focus on using these utility classes to add a security header to outbound messages from a client.

No comments: