OWSM Custom x509 Assertion – Part 2 – Creating outgoing client assertion

In the previous post I explained how you can access the credential store and keystore using the configurations stored in the jsp-config.xml file. I also explained how you can read assertion properties. I put this code inside my base class CustomAssertion.java. This class has been repeated here below.


package nl.amis.custompolicy.simplex509;

import java.security.cert.X509Certificate;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.xml.namespace.NamespaceContext;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import oracle.security.jps.service.credstore.CredentialStore;

import oracle.wsm.common.sdk.IContext;
import oracle.wsm.common.sdk.IMessageContext;
import oracle.wsm.common.sdk.WSMException;
import oracle.wsm.policy.model.IAssertion;
import oracle.wsm.policy.model.IAssertionBindings;
import oracle.wsm.policy.model.IProperty;
import oracle.wsm.policy.model.impl.Config;
import oracle.wsm.policy.model.impl.SimpleAssertion;
import oracle.wsm.policyengine.IExecutionContext;
import oracle.wsm.policyengine.impl.AssertionExecutor;
import oracle.wsm.security.SecurityException;
import oracle.wsm.security.jps.JpsManager;
import oracle.wsm.security.jps.WsmKeyStore;
import oracle.wsm.security.jps.WsmKeyStoreFactory;
import oracle.wsm.security.policy.scenario.util.ScenarioUtils;
import oracle.wsm.security.policy.scenario.util.ScenarioUtils.Credentials;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

public abstract class CustomAssertion extends AssertionExecutor {

protected IAssertion mAssertion = null;
protected IExecutionContext mEcontext = null;
protected IContext mIcontext = null;
private JpsManager jpsManager;
private WsmKeyStore wsmKeyStore;
private Properties configProps;

public CustomAssertion(String tag) {
jpsManager = null;
wsmKeyStore = null;
configProps = new Properties();
}

public void destroy() {
}

public JpsManager getJpsManager() {
return jpsManager;
}

public WsmKeyStore getWsmKeyStore() {
return wsmKeyStore;
}

public Properties getConfigProperties() {
return configProps;
}

public void init(IAssertion iAssertion,
IExecutionContext iExecutionContext,
IContext iContext) throws WSMException {
mAssertion = iAssertion;
mEcontext = iExecutionContext;
mIcontext = iContext;
try {
if (ScenarioUtils.isJpsEnv()) {
jpsManager = new JpsManager();
jpsManager.setAuthenticationMode("anonymous");
}
} catch (SecurityException e) {
throw new WSMException(e);
}
IAssertionBindings bindings =
((SimpleAssertion)(this.mAssertion)).getBindings();
if (bindings != null) {
List cfgl = bindings.getConfigs();
if (!cfgl.isEmpty()) {
Config cfg = (Config)cfgl.get(0);
List<IProperty> configProperties = cfg.getProperties();
if (configProperties != null) {
for (IProperty configProperty : configProperties) {
String propName = configProperty.getName();
String propValue = configProperty.getValue();
if (propValue == null || propValue.trim().isEmpty())
propValue = configProperty.getDefaultValue();
if (propValue != null)
configProps.setProperty(propName, propValue);
}
}
}
}
}

protected boolean setWsmKeyStore(IMessageContext msgContext) throws SecurityException {
// Controleren of keystore service er is.
if (jpsManager != null && !jpsManager.isKeyStoreServiceAvailable()) {
throw new SecurityException("keystore not available Error");
}
// OPHALEN CREDENTIALSTORE
CredentialStore credentialStore =
jpsManager.getKeyStoreLevelCredentialStore();
if (credentialStore == null) {
throw new SecurityException("credentialstore not available Error");
}
// OPHALEN KeyStoreConfig
Map<String, String> keyStoreConfig = jpsManager.getKeyStoreConfig();
if (keyStoreConfig == null) {
throw new SecurityException("keystore configuration not available Error");
}
// OPHALEN KEYSTORE TYPE
String keystoreType = keyStoreConfig.get("keystore.type");
if (keystoreType != null && keystoreType.trim().isEmpty()) {
throw new SecurityException("keystore type not set Error");
}
if (!WsmKeyStore.KEYSTORE_TYPES_ENUM.JKS.toString().equalsIgnoreCase(keystoreType)) {
throw new SecurityException("Only keystore of type JKS is supported");
}
// OPHALEN KEYSTORE PATH
String location = keyStoreConfig.get("location");
// OPHALEN KEYSTORE CSF MAP
String keystoreCSFMap = keyStoreConfig.get("keystore.csf.map");
// OPHALEN KEYSTORE PASSWORD UIT CREDENTIAL STORE
String keyStorePassword = null;
String keyStorePassCSFKey =
keyStoreConfig.get("keystore.pass.csf.key");
if (keyStorePassCSFKey != null) {
Credentials keystorePassCreds =
ScenarioUtils.getKeyStoreCredsFromCSF(keystoreCSFMap,
keyStorePassCSFKey,
credentialStore);
if (keystorePassCreds != null)
keyStorePassword = new String(keystorePassCreds.getPassword());
}
// Ophalen SIGNATURE CSF KEY
String keystoreSigCSFKey =
ScenarioUtils.getConfigPropertyValue("keystore.sig.csf.key",
msgContext,
getConfigProperties(),
keyStoreConfig);
if (keystoreSigCSFKey != null && keystoreSigCSFKey.trim().isEmpty()) {
throw new SecurityException("signature csf key is empty");
}
// Ophalen SIGNATURE ALIAS AND PASSWORD
String signAlias = null;
String signPassword = null;
Credentials signCreds =
ScenarioUtils.getKeyStoreCredsFromCSF(keystoreCSFMap,
keystoreSigCSFKey,
credentialStore);
if (signCreds != null) {
signPassword = new String(signCreds.getPassword());
signAlias = signCreds.getUsername();
}
// Ophalen ENCRYPTION CSF KEY
String keystoreEncCSFKey =
ScenarioUtils.getConfigPropertyValue("keystore.enc.csf.key",
msgContext,
getConfigProperties(),
keyStoreConfig);
if (keystoreEncCSFKey != null && keystoreEncCSFKey.trim().isEmpty()) {
throw new SecurityException("encryption csf key is empty");
}
// Ophalen ENCRYPTION ALIAS AND PASSWORD
String cryptAlias = null;
String cryptPassword = null;
Credentials cryptCreds =
ScenarioUtils.getKeyStoreCredsFromCSF(keystoreCSFMap,
keystoreEncCSFKey,
credentialStore);
if (null != cryptCreds) {
cryptPassword = new String(cryptCreds.getPassword());
cryptAlias = cryptCreds.getUsername();
}
X509Certificate recipientCert =
ScenarioUtils.getConfigPropertyRecipientCert(msgContext,
getConfigProperties(),
null);
String keystoreRecipientAlias =
ScenarioUtils.getConfigPropertyValue("keystore.recipient.alias",
msgContext,
getConfigProperties(), null);
if (keystoreRecipientAlias != null &&
keystoreRecipientAlias.trim().isEmpty()) {
throw new SecurityException("recipient alias is empty");
}

wsmKeyStore =
WsmKeyStoreFactory.getKeyStore(location, keystoreType, "keystore",
keyStorePassword, signAlias,
signPassword, cryptAlias,
cryptPassword,
keystoreRecipientAlias,
recipientCert);
return wsmKeyStore != null;
}

public static Node getDataNode(Element payload,
final HashMap<String, String> namespaces,
String xpathStr) {
Node node = null;

try {
NamespaceContext ctx = new NamespaceContext() {
public String getNamespaceURI(String prefix) {
return namespaces.get(prefix);
}

public Iterator getPrefixes(String val) {
return null;
}

public String getPrefix(String uri) {
return null;
}
};
XPathFactory xpathFact = XPathFactory.newInstance();
XPath xpath = xpathFact.newXPath();
xpath.setNamespaceContext(ctx);
node =
(Node)xpath.evaluate(xpathStr, payload, XPathConstants.NODE);
} catch (XPathExpressionException ex) {
ex.printStackTrace();
return null;
}
return node;
}
}

In this post I will explore how complicated it is to create a WS Signing policy. This policy is pretty basic. It will sign the ws-addressing headers and the SOAP body of the request. It also adds and signs a timestamp. The timestamp and signatures of the response message are verified.

There is unfortunately not much information on how to use the WS Security packages supplied by Oracle. There is a very high level document I used as a starting point. This document describes the use of cryptographic building blocks Oracle provides to implement security. These APIs are part of the Oracle Security Developer Tools (OSDT).

I create a java project and added the following packages:

${MIDDLEWARE_HOME}\oracle_common\modules\oracle.osdt_11.1.1\osdt_xmlsec.jar
${MIDDLEWARE_HOME}\oracle_common\modules\oracle.osdt_11.1.1\osdt_wss.jar
${MIDDLEWARE_HOME}\oracle_common\modules\oracle.wsm.agent.common_11.1.1\wsm-agent-core.jar
${MIDDLEWARE_HOME}\oracle_common\modules\oracle.wsm.common_11.1.1\wsm-policy-core.jar
${MIDDLEWARE_HOME}\oracle_common\modules\oracle.jps_11.1.1\jps-api.jar

The main class used to sign my request is oracle.security.xmlsec.wss.WSSecurity. This class will sign the SOAP request. If you provide an array of ids of elements to sign (header and/or body elements) together with a binary securitytoken the signing happens automagically after calling the sign method.

The final result was not completely to my satisfaction:

  • I could not find an easy way of adding the mustUnderstand attribute to my security header. So I used the DOM Element method setAttributeNS to add the attribute directly.
  • The sample code used a method oracle.security.xmlsec.wss.util.WSSUtils.addWsuIdToElement to add a wsuId. When I used this method the wsu prefix was lost. So I added the wsuId attribute using the setAttributeNS method again.
  • The BinarySecurityToken is the last node within the WS-security SOAP header by default. To make it the first child I had to move it. I also wanted the WS security header itself as the first SOAP header so I had to move this one also.

Verifying the response message is also not very complicated. I first check the timestamp in the security header. If no timestamp is found an error is raised as my policy expects one.  Then I loop over all signatures inside the security and verify them. I expect a security header with at least one or more signed elements inside. You can image more complicated checks whether the SOAP response complies to a predefined security policy.

This resulted in the following assertion executer class:

package nl.amis.custompolicy.simplex509;

import java.security.cert.X509Certificate;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.namespace.QName;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;

import nl.amis.custompolicy.simplex509.util.Utilities;

import oracle.security.xmlsec.dsig.XSSignature;
import oracle.security.xmlsec.wss.WSSURI;
import oracle.security.xmlsec.wss.WSSecurity;
import oracle.security.xmlsec.wss.WSSecurityTokenReference;
import oracle.security.xmlsec.wss.util.WSSUtils;
import oracle.security.xmlsec.wss.util.WSSignatureParams;
import oracle.security.xmlsec.wss.x509.X509BinarySecurityToken;

import oracle.wsm.common.sdk.IContext;
import oracle.wsm.common.sdk.IMessageContext;
import oracle.wsm.common.sdk.IResult;
import oracle.wsm.common.sdk.Result;
import oracle.wsm.common.sdk.SOAPBindingMessageContext;
import oracle.wsm.common.sdk.WSMException;
import oracle.wsm.security.SecurityException;

public class SimpleX509AssertionExecutor extends CustomAssertion {

private static final String ns_wsa = "http://www.w3.org/2005/08/addressing";
private static final Logger TRACE = Logger.getLogger(SimpleX509AssertionExecutor.class.getName());

public SimpleX509AssertionExecutor() {
super("[SimpleX509AssertionExecutor] ");
}

public void destroy() {
}

public oracle.wsm.policyengine.IExecutionContext getExecutionContext() {
return this.econtext;
}

public String getAssertionName() {
return this.mAssertion.getQName().toString();
}

public IResult execute(IContext context) throws WSMException {
IResult result = new Result();
IMessageContext.STAGE stage = ((IMessageContext)context).getStage();
if (stage == IMessageContext.STAGE.request) {
try {
SOAPBindingMessageContext smc = ((SOAPBindingMessageContext)context);
// create WmsKeyStore
setWsmKeyStore((IMessageContext)context);
SOAPMessage msg = smc.getRequestMessage();
SOAPEnvelope env = msg.getSOAPPart().getEnvelope();
// You need to explicitly add the wsu prefix and namespace to the envelope.
env.addNamespaceDeclaration("wsu", WSSURI.ns_wsu);
// Now create a new <wsse:Security> Header
// newInstance will internally use SOAPHeader.addHeaderElement
WSSecurity ws = WSSecurity.newInstance(env);
// add mustUnderstand.
String prefix = env.getPrefix();
String ns_soap = env.getNamespaceURI();
ws.getElement().setAttributeNS(ns_soap, prefix + ":mustUnderstand", "1");

X509Certificate cert = getWsmKeyStore().getSignCert();
X509BinarySecurityToken x509Token = ws.createBST_X509(cert);
// remember to put this inside your WSSecurity header.
// addX509CertificateToken puts it at the beginning, you can also
// use a regular DOM method appendChild or insertChild to put it in.
ws.addX509CertificateToken(x509Token);
// optionally add an wsu:Id, so you can refer to it
x509Token.setWsuId("Cert");

// Create some security token references to this token
WSSecurityTokenReference str = ws.createSTR_X509_Ref("#Cert");
WSSignatureParams wsp = new WSSignatureParams(null, getWsmKeyStore().getSignKey());
// You need to set the STR that you have created earlier into this object;
wsp.setKeyInfoData(str);

// add timestamp
String wsuId = Utilities.addTimeStamp(ws);
// uris is an array of IDs to be signed.
ArrayList<String> uris = new ArrayList<String>();
uris.add(wsuId);

// signing addressing headers
SOAPHeader sHeader = msg.getSOAPHeader();
Iterator itr = sHeader.examineAllHeaderElements();
SOAPHeaderElement he = null;
do {
if (!itr.hasNext())
break;
he = (SOAPHeaderElement)itr.next();
if (he.getNamespaceURI().equals(ns_wsa)) {
wsuId = (new StringBuilder()).append("id-").append(oracle.security.xmlsec.util.XMLUtils.randomName()).toString();
he.setAttributeNS(WSSURI.ns_wsu, "wsu:Id", wsuId);
uris.add((new StringBuilder()).append("#").append(wsuId).toString());
}
} while (true);

// Signen van de body.
SOAPBody sBody = msg.getSOAPBody();
wsuId = (new StringBuilder()).append("id-").append(oracle.security.xmlsec.util.XMLUtils.randomName()).toString();
sBody.setAttributeNS(WSSURI.ns_wsu, "wsu:Id", wsuId);
uris.add((new StringBuilder()).append("#").append(wsuId).toString());

wsp.setSOAPMessage(msg);
// Now sign or encrypt some data (refer to following sections)
// These should use the above STRs
String urisArr[] = uris.toArray(new String[uris.size()]);
ws.sign(urisArr, wsp, null);

// put BinaryToken to front. Assume it only contains one BinaryToken
List btokens = ws.getBinaryTokens();
if (btokens != null && btokens.size() > 0) {
Iterator it = btokens.iterator();
do {
if (!it.hasNext())
break;
Object bo = it.next();
if ((bo instanceof X509BinarySecurityToken) &&
((X509BinarySecurityToken)bo).getWsuId().equals(x509Token.getWsuId())) {
X509BinarySecurityToken existingToken = (X509BinarySecurityToken)bo;
ws.removeChild(existingToken.getNode());
break;
}
} while (true);
}
WSSUtils.prependChild(ws, x509Token.getNode());

// Put the WS Security header in front of all other SOAP Headers
itr = sHeader.examineAllHeaderElements();
do {
if (!itr.hasNext())
break;
he = (SOAPHeaderElement)itr.next();
if (he.getNamespaceURI().equals(WSSURI.ns_wsu)) {
sHeader.removeChild(he);
}
} while (true);
Utilities.prependChild(sHeader, ws.getNode());

TRACE.fine("Finished");
result.setStatus(IResult.SUCCEEDED);
return result;
} catch (Exception e) {
throw new WSMException("Fault", e);
}
} else if (stage == IMessageContext.STAGE.response) {
try {
SOAPBindingMessageContext smc = ((SOAPBindingMessageContext)context);
SOAPMessage message = smc.getResponseMessage();
SOAPEnvelope soapenv = message.getSOAPPart().getEnvelope();
WSSecurity sec = Utilities.getSecurityHeader(soapenv);
if (sec==null) {
throw new SecurityException("WS Security header expected.");
}
if (!Utilities.validateTimestamp(sec.getTimestamp())) {
throw new SecurityException("Timestamp invalid");
}
List<XSSignature> sigs = sec.getSignatures();
if (sigs==null || sigs.isEmpty()) {
throw new SecurityException("Signed elements expected");
}
for (XSSignature signature : sigs) {
if (!sec.verify(signature)) {
throw new SecurityException("Signature invalid");
}
}
} catch (Exception e) {
throw new WSMException("Fault",e);
}
result.setStatus(IResult.SUCCEEDED);
return result;
}
result.setStatus(IResult.SUCCEEDED);
return result;
}

}

I created a Utility class that contains a few utility functions. Most of them are pretty much self describing. The timestamp methods are pretty basic. They do not take into account any time skew between systems. You can add this (5 minutes is generally excepted) to make sure timestamps are not being wrongly rejected.

package nl.amis.custompolicy.simplex509.util;

import java.util.Date;

import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;

import oracle.security.xmlsec.wss.WSSecurity;
import oracle.security.xmlsec.wss.WSUCreated;
import oracle.security.xmlsec.wss.WSUExpires;
import oracle.security.xmlsec.wss.WSUTimestamp;

import oracle.wsm.security.SecurityException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;

public class Utilities {

public static String addTimeStamp(WSSecurity sec) {
Document doc = sec.getOwnerDocument();
WSUTimestamp ts = new WSUTimestamp(doc);
ts.setId((new StringBuilder()).append("Timestamp-").append(oracle.security.xmlsec.util.XMLUtils.randomName()).toString());
WSUCreated wsuCreated = new WSUCreated(doc);
Date now = new Date();
wsuCreated.setValue(now);
ts.setCreated(wsuCreated);
String expiryTime = "300";
WSUExpires expiry = new WSUExpires(doc);
expiry.setValue(new Date(now.getTime() +
(long)(Integer.parseInt(expiryTime) * 1000)));
ts.setExpires(expiry);
sec.setTimestamp(ts);
return (new StringBuilder()).append("#").append(ts.getWsuId()).toString();
}

public static void prependChild(Node parent, Node newChild) {
Node firstChild = parent.getFirstChild();
if (firstChild != null)
parent.insertBefore(newChild, firstChild);
else
parent.appendChild(newChild);
}

public static WSSecurity getSecurityHeader(SOAPEnvelope soapenv) throws SecurityException {
try {
WSSecurity wsSecs[] = WSSecurity.getAllSecurityHeaders(soapenv);
if (wsSecs == null || wsSecs.length == 0)
return null;
else
return wsSecs[0];
} catch (SOAPException e) {
throw new SecurityException(e);
}
}

public static boolean validateTimestamp(WSUTimestamp timestamp) {
WSUCreated created = timestamp.getCreated();
WSUExpires expires = timestamp.getExpires();
Date currentDate = new Date();
if (currentDate.after(expires.getValue()) ||
currentDate.before(created.getValue())) {
return false;
}
return true;
}

}

How did I test the working of this code? First of all you need to put your assertion jar in the lib directory of the application server and restart the server again every time you want to test any changes. This makes your development cycle somewhat long and tedious. I wanted to use some logging to trace my program. Adding this up to the long development cycles I skipped this approach and figured out remote debugging in jdeveloper (I am used to Eclipse) so I could step through my code.

When I got a decent signed request I used SOAPUI to create a MockService that processes this request and returns a signed response my assertion could verify. This works like a charm. The documentation explain this nicely.

OWSM Custom x509 Assertion - Part 2 - Creating outgoing client assertion soapui