WSO2 7 minutes

WS-Security: Message signing with a secure callback handler

Rutger van Iperen
Integration Consultant
WS-Security: Message signing with a secure callback handler
Scroll

blog-20201127-ws-security-mesage-signing-callback-handler-600x350pxIn a previous blog, the signing of outgoing messages was discussed. This is the promised follow-up where we show you how to secure this callback handler. Like mentioned in the previous blog, WSO2 supports this standard through a third party library called Apache Rampart (co-created by the developers of WSO2). In that blog we discussed an alternative option for this where a default callbackhandler always sends back one password/passphrase. There are two problems with this, most importantly we don’t want this passphrase in our code (even less our code repository), secondly we can only deal with one passphrase which would require a callbackhandler for every certificate we use.

In this blog we will show you a more secure and versatile way to create a callbackhandler when signing or encrypting messages on the WSO2 Enterprise Integrator.

First of we created a simple config file looking like this. The config file contains a signature element for each keypair/alias. For now we save the configuration as callback.xml in the <EI_HOME>/conf folder

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

<conf xmlns_svns="http://org.wso2.securevault/configuration">

    <signature alias="demo-pass">

        <password svns_secretAlias="demo.signing.password">password</password>

    </signature>

    <signature alias="demo-pass-plain">

        <password>verySuperS3cret</password>

    </signature>

</conf>

callback.xml

The first change to the default callbackhandler is that we stop returning a default password. We will use the alias attribute (in the callback) to lookup the proper password. The alias in the callback is obtained from the userCertAlias in the WSPolicy. This is sent to the callbackhandler as part of the callback, we then match that with the alias attribute of a signature element in the callback.xml configuration file.

 import org.apache.axiom.om.OMElement; import org.apache.axiom.om.OMNode; import org.apache.axiom.om.impl.builder.StAXOMBuilder; import org.apache.axiom.om.xpath.AXIOMXPath; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.ws.security.WSPasswordCallback; import org.wso2.carbon.utils.CarbonUtils; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class YenloSimpleCallbackHandler implements CallbackHandler { private static final Log log = LogFactory.getLog(YenloSimpleCallbackHandler.class); public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback pc = (WSPasswordCallback) callbacks[0]; String alias = pc.getIdentifier(); InputStream fileInputStream = null; String configPath = CarbonUtils.getCarbonHome()+ File.separator + "conf" + File.separator + "callback.xml"; File configXML = new File(configPath); try { fileInputStream = new FileInputStream(configXML); StAXOMBuilder builder = new StAXOMBuilder(fileInputStream); OMNode configElement = builder.getDocumentElement(); AXIOMXPath path = new AXIOMXPath("signature[@alias='" + alias + "']"); OMNode signature = (OMNode) path.selectSingleNode((OMNode) configElement); if (signature != null) { String password = ((OMElement) signature).getFirstChildWithName(new QName("password")).getText(); log.debug("Retrieved password for alias: " + alias) ; pc.setPassword(password); } else { log.error("No password found for alias: " + alias); } } catch (XMLStreamException e) { log.error("Unable to parse callback.xml configuration file", e); } catch (IOException e) { log.error("Unable to read callback.xml configuration file", e); } catch ( Exception e) { log.error("Unexpected error retrieving password : ", e ); }finally { if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { log.error("Failed to close the FileInputStream, file : " + configPath); } } } } } 

Obviously you can rename and restructure the configuration file to your liking, as long as you make the appropriate changes to the lookup part of the callbackhandler. Now that we can dynamically find the passphrase for a keypair used to sign messages we would like to get rid of the plain text passphrase in our configuration file. For this we will use the Ciphertool that comes with the WSO2 product.

Using the Ciphertool to encrypt the passphrases requires adding the passphrase alias and location to the cipher-tool.properties and the alias and original passphrase to the cipher-text.properties as follows.

In the cipher-text.properties file add the following line:

Signing.demo.password=password’

This will have you current password which will be encrypted when you run the Ciphertool.

In the cipher-tool.properties add the following line.

‘Signing.demo.password=conf/callback.xml//conf/signature[@alias=’demo.signing.password’]/password,false’

This line references our config file, and the location where the password should be used (using the alias property on the element). And revers to the (to be encrypted) password in the cipher-text.properties by name.

Run the Ciphertool as described in the WSO2 documentation to encrypt the passwords.

All we need to do now is adapt the callbackhandler that we’ve created to be able to resolve password encrypted with the Ciphertool. To do so we to make a few changes to our code as listed below.

Import the following classes to deal with the securevault:

import org.wso2.securevault.SecretResolver; import org.wso2.securevault.SecretResolverFactory;

Initialize a new SecretResolver using the contents of our config file:

SecretResolver secretResolver = 
SecretResolverFactory.create((OMElement)configElement, false);


Retrieve the alias attribute from the password entry we want the value for:

OMAttribute secretAlias = ((OMElement) signature).getFirstChildWithName(new QName("password")).getAttribute(new QName("http://org.wso2.securevault/configuration","secretAlias"));

Use the secret resolver to retrieve and unencrypt the password from the cipher-text.properties file:

if (secretResolver != null && secretResolver.isInitialized()) { if (secretAlias != null && secretResolver.isTokenProtected(secretAlias.getAttributeValue())) { password = secretResolver.resolve(secretAlias.getAttributeValue()); } else { password = ((OMElement) signature).getFirstChildWithName(new QName("password")).getText(); } }

As you can see in the above snippet there are a few checks before it resolves the password. Including a check if the attribute actually refers to a token that is known. If any of these fail we will not be able to find and or decrypt the password.

Here is the full code for the callbackhandler.

package nl.yenlo.integratie.security.signing; import org.apache.axiom.om.OMAttribute; import org.apache.axiom.om.OMNode; import org.apache.axiom.om.xpath.AXIOMXPath; import org.apache.ws.security.WSPasswordCallback; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.IOException; import org.apache.axiom.om.OMElement; import org.apache.axiom.om.impl.builder.StAXOMBuilder; import org.wso2.carbon.utils.CarbonUtils; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import org.wso2.securevault.SecretResolver; import org.wso2.securevault.SecretResolverFactory; public class YenloCallbackHandler implements CallbackHandler { private static final Log log = LogFactory.getLog(YenloCallbackHandler.class); public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback pc = (WSPasswordCallback) callbacks[0]; String alias = pc.getIdentifier(); InputStream fileInputStream = null; String configPath = CarbonUtils.getCarbonHome()+ File.separator + "conf" + File.separator + "callback.xml"; File configXML = new File(configPath); try { fileInputStream = new FileInputStream(configXML); StAXOMBuilder builder = new StAXOMBuilder(fileInputStream); OMNode configElement = builder.getDocumentElement(); SecretResolver secretResolver = SecretResolverFactory.create((OMElement)configElement, false); AXIOMXPath path = new AXIOMXPath("signature[@alias='" + alias + "']"); OMNode signature = (OMNode) path.selectSingleNode((OMNode) configElement); if (signature != null) { log.debug("Retreiving password for alias: " + alias); OMAttribute secretAlias = ((OMElement) signature).getFirstChildWithName(new QName("password")). getAttribute(new QName("http://org.wso2.securevault/configuration","secretAlias")); if(secretAlias != null){ log.debug("Alias: " + secretAlias.getAttributeValue()); } String password = ""; if (secretResolver != null && secretResolver.isInitialized()) { if (secretAlias != null && secretResolver.isTokenProtected(secretAlias.getAttributeValue())) { password = secretResolver.resolve(secretAlias.getAttributeValue()); } else { password = ((OMElement) signature).getFirstChildWithName(new QName("password")).getText(); } } log.debug("Retrieved password for alias: " + alias) ; pc.setPassword(password); } else { log.error("No password found for alias: " + alias); } } catch (XMLStreamException e) { log.error("Unable to parse callback.xml configuration file", e); } catch (IOException e) { log.error("Unable to read callback.xml configuration file", e); } catch ( Exception e) { log.error("Unexpected error retrieving password : ", e ); }finally { if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { log.error("Failed to close the FileInputStream, file : " + configPath); } } } } } 

Now you should be able to sign messages while the passphrase is properly encrypted and stored in a config file.

It is important to note that our callbackhandler does not require encryption, it checks for it and will handle either encrypted or unencrypted passphrases. This last bit is really only for demo/testing purposes. If you can encrypt your passwords you always should. Of course you can change this and remove the option of retrieving plain text passwords.

Do you have any questions regarding this blog? Feel free to leave a comment. In the need of WSO2 Support? Have a look at our WSO2 Support services!

 

{{cta(‘ac5750e0-36c6-4c0f-9f41-624f8794a17e’,’justifycenter’)}}