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("").append(tag).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.