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.
No comments:
Post a Comment