Monday, October 23, 2006

Implementing Web Service Security: Verifying the Message Signature

In my last post I sketched out some of the implementation details for a simple web service security scheme. Next I wanted to flesh out the implementation and incorporate it into my sample web service.

The process of verifying a message from the client should look something like this:

  1. Receive the message and parse it into a DOM.
  2. Retrieve the security element from the header.
  3. Retrieve the signature submitted by the client from the security element.
  4. Recreate the signature based on the submitted access ID, timestamp and canonicalized text of the message body (along with the shared secret key).
  5. If the signatures match, allow processing to continue. Otherwise generate a fault and abort processing.

The verification process should happen transparently from the standpoint of any particular web service operation, such as the getMembers method I wrote about earlier.

JAX-WS provides handlers to support this type of transparent pre- or post-processing. A handler that has access to the entire SOAP message (including the header) must implement the javax.xml.ws.handler.soap.SOAPHandler interface.

The SOAPHandler interface honestly seems a bit odd to me. The same method handleMessage is invoked on each message both before and after processing (See the Tech Tip for some good diagrams). Except in the seemingly unlikely event that you want the same behavior in both these situations, you need to test a context property and split the implementation of handleMessage into two branches. There is probably an explanation for this, but for the moment it is lost on me.

I started by implementing a generic starting point for this and other handlers I might need to write. The base handler simplifies handling of either the inbound or outbound processing phase:

package member.common;

import java.util.Iterator;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.*;

import javax.xml.ws.handler.soap.SOAPMessageContext;

public abstract class BaseHandler 
  implements SOAPHandler {

  protected static final String SECURITY_ELEMENT = "Security";
  protected static final String ACCESS_ID_ELEMENT = "AccessID";
  protected static final String TIMESTAMP_ELEMENT = "Timestamp";
  protected static final String SIGNATURE_ELEMENT = "Signature";

  private static SOAPFactory factory;

  public Set getHeaders() {
    return null;
  }

  public boolean handleMessage(SOAPMessageContext smc) {

    try {
      return isOutbound(smc) ? handleOutbound(smc) : handleInbound(smc);
    } catch (SOAPException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Handles pre-processing of messages before being sent by a client
   * or processed by a server.
   **/
  protected boolean handleInbound(SOAPMessageContext smc) 
    throws SOAPException {
    return true;
  }

  /**
   * Handles post-processing of messages after being received by a client
   * or processed by a server.
   **/
  protected boolean handleOutbound(SOAPMessageContext smc) 
    throws SOAPException {
    return true;
  }

  public boolean handleFault(SOAPMessageContext smc) {
    return true;
  }
    
  public void close(MessageContext messageContext) {
  }
    
  /**
   * Tests whether the message is being sent or received.
   **/
  private boolean isOutbound(SOAPMessageContext smc) {

    Boolean outboundProperty = (Boolean)
      smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        
    return outboundProperty.booleanValue();
  }

  /**
   * Returns a factory required for generating SOAP faults.
   **/
  protected SOAPFactory getFactory() throws SOAPException {

    if (factory == null) {
      factory = SOAPFactory.newInstance();
    }

    return factory;
  }

  /**
   * Returns a qualified name based on the namespace declared by the
   * root element in the SOAP body.
   **/
  protected QName getQName(SOAPBody body, String localName) {

    Iterator i = body.getChildElements();
    if  (! i.hasNext()) { return null; }

    SOAPElement methodElement = (SOAPElement) i.next();
    String namespaceURI = methodElement.getNamespaceURI();
    String prefix = methodElement.getPrefix();

    return new QName(namespaceURI, localName, prefix);    
  }
}

Once I had the base handler in hand, I could write a signature verification handler focused exclusively on pre-processing of each incoming message. The handleInbound method does the actual work:

package member.server;

import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import java.util.logging.Logger;

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;

import javax.xml.ws.soap.SOAPFaultException;

import javax.xml.ws.handler.soap.SOAPMessageContext;

import member.common.Canonicalizer;
import member.common.Signature;
import member.common.BaseHandler;
import member.common.Timestamp;

/*
 * Verifies the signature on inbound messages.
 */
public class SignatureVerificationHandler extends BaseHandler {

  private static final Logger log = Logger.getLogger("member");

  private static final long MAX_TIME_RANGE_MILLIS = 1000 * 60 * 5;

  public boolean handleInbound(SOAPMessageContext smc) throws SOAPException {

    SOAPMessage message = smc.getMessage();
    SOAPPart part = message.getSOAPPart();

    SOAPEnvelope envelope = part.getEnvelope();
    
    SOAPBody body = envelope.getBody();
    if (body == null) { 
      throw new RuntimeException("Missing required message body.");
    }
    
    SOAPElement securityElement = getSecurityElement(envelope);
    String accessID = getChildText(securityElement, ACCESS_ID_ELEMENT);
    String timestamp = getChildText(securityElement, TIMESTAMP_ELEMENT);
    Date submittedDate = Timestamp.parse(timestamp);
    
    long timeRangeMillis = 
      Math.abs(System.currentTimeMillis() - submittedDate.getTime());
    if (timeRangeMillis > MAX_TIME_RANGE_MILLIS) {
      SOAPFault fault = 
        getFactory().createFault("Timestamp outside allowed range",
     getQName(body, "InvalidTimestamp"));
      throw new SOAPFaultException(fault);
    }
    
    String submittedSignature = 
      getChildText(securityElement, SIGNATURE_ELEMENT);
    
    String bodyText = Canonicalizer.toText(body);
    String signedText = timestamp + ":" + accessID + ":" + bodyText;
    String secretKey = getSecretKey(accessID);
    String verifiedSignature = Signature.create(secretKey, signedText);
    
    if (! submittedSignature.equals(verifiedSignature)) {
      SOAPFault fault = 
        getFactory().createFault("Failed to verify the message signature",
     getQName(body, "InvalidSignature"));
      throw new SOAPFaultException(fault);
    }

    return true;
  }

  private SOAPElement getSecurityElement(SOAPEnvelope envelope) 
    throws SOAPException {

    SOAPHeader header = envelope.getHeader();

    if (header == null) {
      throw new RuntimeException("Missing required message header");
    }

    QName qName = getQName(envelope.getBody(), SECURITY_ELEMENT);
    if (qName == null) {
      throw new RuntimeException("Missing required method element.");
    }

    Iterator i = header.getChildElements(qName);
    if (! i.hasNext()) {
      throw new RuntimeException("Missing required security element.");
    }

    SOAPElement securityElement = (SOAPElement) i.next();

    return securityElement;
  }

  private String getChildText(SOAPElement element, String name) {
    
    Iterator i = element.getChildElements(new QName("", name, ""));
    if (! i.hasNext()) {
      return "";
    }
    
    SOAPElement child = (SOAPElement) i.next();

    Node textNode = child.getFirstChild();
    if (textNode == null) { 
      return ""; 
    }

    if (textNode.getNodeType() != Node.TEXT_NODE) {
      return "";
    }

    return ((CharacterData) textNode).getData();
  }

  /**
    * Placeholder method.
    **/
  private String getSecretKey(String accessID) {
    return "secret";
  }
}

The handler throws a fault if the timestamp or the signature is invalid. A real implementation of the getSecretKey method would also throw a fault if the access ID was invalid.

To add the handler to the message processing chain, I created a handlers.xml file in the member.server package directory:

<?xml version="1.0" encoding="UTF-8"?>

<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
  <handler-chain>
    <handler>
      <handler-class>member.server.SignatureVerificationHandler</handler-class>
    </handler>
  </handler-chain>
</handler-chains>

This apparently gets picked up by the apt tool and applied to all the web services in that package.

Before I could actually test the security scheme, I had to create a a client that would supply the correct security element in the message header. Keep reading for the shocking finale.

No comments: