Java tutorial
/** * Copyright (C) 2014 The Holodeck B2B Team, Sander Fieten * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.holodeckb2b.security.handlers; import java.util.List; import java.util.Properties; import javax.security.auth.callback.CallbackHandler; import org.apache.axiom.soap.SOAPEnvelope; import org.apache.axis2.AxisFault; import org.apache.axis2.context.MessageContext; import org.apache.axis2.engine.Handler.InvocationResponse; import org.apache.commons.logging.Log; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.dom.WSConstants; import org.apache.wss4j.dom.WSSConfig; import org.apache.wss4j.dom.handler.HandlerAction; import org.apache.wss4j.dom.handler.RequestData; import org.apache.wss4j.dom.handler.WSHandler; import org.apache.wss4j.dom.handler.WSHandlerConstants; import org.apache.wss4j.dom.util.WSSecurityUtil; import org.holodeckb2b.axis2.Axis2Utils; import org.holodeckb2b.ebms.axis2.MessageContextUtils; import org.holodeckb2b.common.handler.BaseHandler; import org.holodeckb2b.common.util.Utils; import org.holodeckb2b.ebms3.constants.DefaultSecurityAlgorithm; import org.holodeckb2b.ebms3.constants.SecurityConstants; import org.holodeckb2b.ebms3.persistency.entities.MessageUnit; import org.holodeckb2b.interfaces.general.EbMSConstants; import org.holodeckb2b.interfaces.pmode.security.IEncryptionConfiguration; import org.holodeckb2b.interfaces.pmode.security.IKeyTransport; import org.holodeckb2b.interfaces.pmode.security.ISigningConfiguration; import org.holodeckb2b.interfaces.pmode.security.IUsernameTokenConfiguration; import org.holodeckb2b.interfaces.pmode.security.X509ReferenceType; import org.holodeckb2b.security.callbackhandlers.AttachmentCallbackHandler; import org.holodeckb2b.security.callbackhandlers.PasswordCallbackHandler; import org.holodeckb2b.security.util.SecurityUtils; import org.w3c.dom.Document; /** * Is the <i>OUT_FLOW</i> handler that creates the WS-Security headers in the outgoing message. It uses the WSS4J * library for the actual work of adding the headers. * <p>The handler can add two security headers, one default and one targeted to the "ebms" role/actor. The latter can * only contain a <code>wsse:UsernameToken</code> element for authentication and authorization purposes (see section * 7.10 of the ebMS v3 Core Specification). The default can also contain the signature and encrypted data of the message * <p>Whether security headers must be create should be indicated by the message context property {@link * SecurityConstants#ADD_SECURITY_HEADERS}. If it contains the <code>Boolean.TRUE</code> the headers will be created. * The configuration for the information to be included in the headers should also be specified in message properties * as shown in the table below: * <table border="1"> * <tr><td>Property key</td><td>Configuration interface</td><td>Contains configuration for</td></tr> * <tr><td>{@link SecurityConstants#EBMS_USERNAMETOKEN}</td> * <td>{@link IUsernameTokenConfiguration}</td> * <td>The <code>UsernameToken</code> targeted at the <i>ebms</i> role</td></tr> * <tr><td>{@link SecurityConstants#DEFAULT_USERNAMETOKEN}</td * <td>{@link IUsernameTokenConfiguration}</td> * <td>The <code>UsernameToken</code> targeted at the <i>default</i> role</td></tr> * <tr><td>{@link SecurityConstants#SIGNATURE}</td> * <td>{@link ISigningConfiguration}</td> * <td>The <code>Signature</code> to create in the <i>default</i> header</td></tr> * </table> * * @author Sander Fieten <sander at holodeck-b2b.org> */ public class CreateWSSHeaders extends BaseHandler { protected static final String WSS4J_PART_EBMS_HEADER = "{}{" + EbMSConstants.EBMS3_NS_URI + "}Messaging;"; protected static final String WSS4J_PART_S11_BODY = "{}{http://schemas.xmlsoap.org/soap/envelope/}Body;"; protected static final String WSS4J_PART_S12_BODY = "{}{http://www.w3.org/2003/05/soap-envelope}Body;"; protected static final String WSS4J_PART_UT = "{}{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}UsernameToken;"; protected static final String WSS4J_PART_ATTACHMENTS = "{}cid:Attachments;"; @Override protected byte inFlows() { return OUT_FLOW | OUT_FAULT_FLOW; } @Override protected InvocationResponse doProcessing(MessageContext mc) throws AxisFault { CreateWSSHeaders.WSSSendHandler processor = null; // Check if security headers must be added Boolean addHeaders = (Boolean) mc.getProperty(SecurityConstants.ADD_SECURITY_HEADERS); if (addHeaders == null || !addHeaders) { log.debug("No security headers to add, skip processing"); return InvocationResponse.CONTINUE; } // Convert the SOAP Envelope to standard DOM representation as this is required by the security processing // libraries Document domEnvelope = Axis2Utils.convertToDOM(mc); if (domEnvelope == null) { logError(mc, null, "Converting the SOAP envelope to DOM representation failed"); return InvocationResponse.ABORT; } try { // Create security header processor processor = new CreateWSSHeaders.WSSSendHandler(mc, domEnvelope, log); } catch (WSSecurityException ex) { logError(mc, null, "Setting up the security processor failed (" + ex.getMessage() + ")"); return InvocationResponse.ABORT; } // Set up the message context properties specific for the header targeted to ebms role. This can only contain // a username token IUsernameTokenConfiguration utConfig = (IUsernameTokenConfiguration) mc .getProperty(SecurityConstants.EBMS_USERNAMETOKEN); if (utConfig != null) { log.debug("A UsernameToken element must be added to the security header targeted to ebms role"); // Create a password callback handler to hand over the password PasswordCallbackHandler pwdCBHandler = new PasswordCallbackHandler(); mc.setProperty(WSHandlerConstants.PW_CALLBACK_REF, pwdCBHandler); setupUsernameToken(mc, utConfig, pwdCBHandler); try { // The security header targeted to the ebms actor should only contain a Username token. log.debug("Add the WSS header targeted to ebms role"); processor.createSecurityHeader(SecurityConstants.EBMS_WSS_HEADER, WSHandlerConstants.USERNAME_TOKEN); log.debug("Added the WSS header targeted to ebms role"); } catch (WSSecurityException wse) { logError(mc, "ebms", wse); return InvocationResponse.ABORT; } utConfig = null; // reset usernametoken config } // Set up the message context properties for the default WSS header. This header can also include signing and // encryption // The actions that need to be executed String actions = ""; // Create a password callback handler to hand over the password PasswordCallbackHandler pwdCBHandler = new PasswordCallbackHandler(); mc.setProperty(WSHandlerConstants.PW_CALLBACK_REF, pwdCBHandler); // Check if a UsernameToken element should be added utConfig = (IUsernameTokenConfiguration) mc.getProperty(SecurityConstants.DEFAULT_USERNAMETOKEN); if (utConfig != null) { log.debug("A UsernameToken element must be added to the security header targeted to ebms role"); // Add UT action actions = WSHandlerConstants.USERNAME_TOKEN; // Set up message context setupUsernameToken(mc, utConfig, pwdCBHandler); } ISigningConfiguration signatureCfg = (ISigningConfiguration) mc.getProperty(SecurityConstants.SIGNATURE); if (signatureCfg != null) { log.debug("The message must be signed, set up signature configuration"); // Add Signature action actions += " " + WSHandlerConstants.SIGNATURE; // Set up message context setupSignature(mc, signatureCfg, pwdCBHandler); } IEncryptionConfiguration encryptCfg = (IEncryptionConfiguration) mc .getProperty(SecurityConstants.ENCRYPTION); if (encryptCfg != null) { log.debug("The message must be encrypted, set up encryption configuration"); // Add encryption action actions += " " + WSHandlerConstants.ENCRYPT; // Set up message context setupEncryption(mc, encryptCfg, pwdCBHandler); } try { log.debug("Add the default WSS header"); processor.createSecurityHeader(null, actions); log.debug("Added the default WSS header"); } catch (WSSecurityException wse) { logError(mc, "default", wse); return InvocationResponse.ABORT; } // Convert the processed SOAP envelope back to the Axiom representation for further processing SOAPEnvelope SOAPenv = Axis2Utils.convertToAxiom(domEnvelope); if (SOAPenv == null) { logError(mc, null, "Converting the SOAP envelope to Axiom representation failed"); return InvocationResponse.ABORT; } else { mc.setEnvelope(SOAPenv); log.debug("Security header(s) successfully added"); } return InvocationResponse.CONTINUE; } /** * Sets the message context properties for adding a UsernameToken to the security header. * <p>Because other elements that need to be added to the header may also require a password the password callback * handler is not created in this method, but shared for the header. * * @param mc The {@link MessageContext} to set up * @param utConfig The configuration for the username token * @param pwdCBHandler The {@link PasswordCallbackHandler} to use for handing over the password to WSS4J library */ private void setupUsernameToken(MessageContext mc, IUsernameTokenConfiguration utConfig, PasswordCallbackHandler pwdCBHandler) { mc.setProperty(WSHandlerConstants.USER, utConfig.getUsername()); mc.setProperty(WSHandlerConstants.ADD_USERNAMETOKEN_CREATED, Boolean.toString(utConfig.includeCreated())); mc.setProperty(WSHandlerConstants.ADD_USERNAMETOKEN_NONCE, Boolean.toString(utConfig.includeNonce())); mc.setProperty(WSHandlerConstants.PASSWORD_TYPE, utConfig.getPasswordType() == IUsernameTokenConfiguration.PasswordType.DIGEST ? WSConstants.PW_DIGEST : WSConstants.PW_TEXT); pwdCBHandler.addUser(utConfig.getUsername(), utConfig.getPassword()); } /** * Sets the message context properties for adding a Signature to the security header. * <p>Because other elements that need to be added to the header may also require a password the password callback * handler is not created in this method, but shared for the header. * * @param mc The {@link MessageContext} to set up * @param sigConfig The configuration for creating the signature * @param pwdCBHandler The {@link PasswordCallbackHandler} to use for handing over the password to WSS4J library */ private void setupSignature(MessageContext mc, ISigningConfiguration sigCfg, PasswordCallbackHandler pwdCBHandler) { // Set up crypto engine Properties sigProperties = SecurityUtils.createCryptoConfig(SecurityUtils.CertType.priv); mc.setProperty(WSHandlerConstants.SIG_PROP_REF_ID, "" + sigProperties.hashCode()); mc.setProperty("" + sigProperties.hashCode(), sigProperties); // Set up signing config // AS4 requires that the ebMS message header (eb:Messaging element) and SOAP Body are signed mc.setProperty(WSHandlerConstants.SIGNATURE_PARTS, WSS4J_PART_EBMS_HEADER + (mc.isSOAP11() ? WSS4J_PART_S11_BODY : WSS4J_PART_S12_BODY)); // And if there are attachments also the attachments. Whether UsernameToken elements in the security header // should be signed is not specified. But to prevent manipulation Holodeck B2B includes them in the signature mc.setProperty(WSHandlerConstants.OPTIONAL_SIGNATURE_PARTS, WSS4J_PART_UT + WSS4J_PART_ATTACHMENTS); // The alias of the certificate to use for signing, converted to lower case because JKS aliasses are case // insensitive mc.setProperty(WSHandlerConstants.SIGNATURE_USER, sigCfg.getKeystoreAlias().toLowerCase()); // The password to access the certificate in the keystore pwdCBHandler.addUser(sigCfg.getKeystoreAlias().toLowerCase(), sigCfg.getCertificatePassword()); // How should certificate be referenced in header? mc.setProperty(WSHandlerConstants.SIG_KEY_ID, SecurityUtils .getWSS4JX509KeyId((sigCfg.getKeyReferenceMethod() != null ? sigCfg.getKeyReferenceMethod() : DefaultSecurityAlgorithm.KEY_REFERENCE))); // If BST is included, should complete cert path be included? if (sigCfg.getKeyReferenceMethod() == X509ReferenceType.BSTReference && (sigCfg.includeCertificatePath() != null ? sigCfg.includeCertificatePath() : false)) mc.setProperty(WSHandlerConstants.USE_SINGLE_CERTIFICATE, "false"); else mc.setProperty(WSHandlerConstants.USE_SINGLE_CERTIFICATE, "true"); // Algorithms to use mc.setProperty(WSHandlerConstants.SIG_DIGEST_ALGO, Utils.getValue(sigCfg.getHashFunction(), DefaultSecurityAlgorithm.MESSAGE_DIGEST)); mc.setProperty(WSHandlerConstants.SIG_ALGO, Utils.getValue(sigCfg.getSignatureAlgorithm(), DefaultSecurityAlgorithm.SIGNATURE)); } /** * Sets the message context properties for adding encryption to the security header. * <p>Because other elements that need to be added to the header may also require a password the password callback * handler is not created in this method, but shared for the header. * * @param mc The {@link MessageContext} to set up * @param sigConfig The configuration for creating the signature * @param pwdCBHandler The {@link PasswordCallbackHandler} to use for handing over the password to WSS4J library */ private void setupEncryption(MessageContext mc, IEncryptionConfiguration encCfg, PasswordCallbackHandler pwdCBHandler) { // Set up crypto engine Properties encProperties = SecurityUtils.createCryptoConfig(SecurityUtils.CertType.pub); mc.setProperty(WSHandlerConstants.ENC_PROP_REF_ID, "" + encProperties.hashCode()); mc.setProperty("" + encProperties.hashCode(), encProperties); // Set up encryption config // AS4 requires that only the payloads are encrypted, so we encrypt the Body only when it contains a payload Boolean includesBodyPayload = (Boolean) mc.getProperty(SecurityConstants.ENCRYPT_BODY); if (includesBodyPayload != null && includesBodyPayload) mc.setProperty(WSHandlerConstants.ENCRYPTION_PARTS, (mc.isSOAP11() ? WSS4J_PART_S11_BODY : WSS4J_PART_S12_BODY)); // And if there are attachments also the attachments must be encrypted. mc.setProperty(WSHandlerConstants.OPTIONAL_ENCRYPTION_PARTS, WSS4J_PART_ATTACHMENTS); // Symmetric encryption algorithms to use mc.setProperty(WSHandlerConstants.ENC_SYM_ALGO, Utils.getValue(encCfg.getAlgorithm(), DefaultSecurityAlgorithm.ENCRYPTION)); // The alias of the certificate to use for encryption mc.setProperty(WSHandlerConstants.ENCRYPTION_USER, encCfg.getKeystoreAlias()); // KeyTransport configuration defines settings for constructing the xenc:EncryptedKey // Set defaults String ktAlgorithm = DefaultSecurityAlgorithm.KEY_TRANSPORT; X509ReferenceType ktKeyReference = DefaultSecurityAlgorithm.KEY_REFERENCE; String ktDigest = DefaultSecurityAlgorithm.MESSAGE_DIGEST; IKeyTransport ktConfig = encCfg.getKeyTransport(); if (ktConfig != null) { // Key encryption algorithm ktAlgorithm = Utils.getValue(ktConfig.getAlgorithm(), DefaultSecurityAlgorithm.KEY_TRANSPORT); // If key transport algorithm is RSA-OAEP also the MGF must be set if (WSConstants.KEYTRANSPORT_RSAOEP_XENC11.equalsIgnoreCase(ktAlgorithm)) mc.setProperty(WSHandlerConstants.ENC_MGF_ALGO, ktConfig.getMGFAlgorithm()); // Message digest ktDigest = Utils.getValue(ktConfig.getDigestAlgorithm(), DefaultSecurityAlgorithm.MESSAGE_DIGEST); // Key refence method if (ktConfig.getKeyReferenceMethod() != null) ktKeyReference = ktConfig.getKeyReferenceMethod(); } // Set the relevant message context properties mc.setProperty(WSHandlerConstants.ENC_KEY_ID, SecurityUtils.getWSS4JX509KeyId(ktKeyReference)); mc.setProperty(WSHandlerConstants.ENC_DIGEST_ALGO, ktDigest); mc.setProperty(WSHandlerConstants.ENC_KEY_TRANSPORT, ktAlgorithm); } /** * Helper method to log error about failure to create the security headers to the message. * * @param mc The message context (used to get info about message being processed) * @param role The role of the WS-Security that was being created, can be null for general error * @param message Specific error message to log */ private void logError(final MessageContext mc, final String role, final String message) { StringBuffer logMsg = new StringBuffer(); // Use primary message unit to log error MessageUnit pMU = MessageContextUtils.getPrimaryMessageUnit(mc).entity; logMsg.append("Could not create WS-Security header"); if (Utils.isNullOrEmpty(role)) logMsg.append("(s)"); else logMsg.append(" targeted to ").append(role); logMsg.append(" for message with primary message unit ["); logMsg.append(pMU.getClass().getSimpleName()).append(";msgID=").append(pMU.getMessageId()).append(']'); logMsg.append("\n\tError details: ").append(message); log.error(logMsg.toString()); } /** * Helper method to log error about failure to create the security headers to the message * * @param mc The message context (used to get info about message being processed) * @param role The role of the WS-Security that was being created, can be null for general error * @param e The exception that occurred */ private void logError(final MessageContext mc, final String role, final Exception e) { // Create a error message that drills down to root cause StringBuffer exMsg = new StringBuffer(); exMsg.append(e.getClass().getSimpleName()).append(" : ").append(e.getMessage()); Throwable cause = e.getCause(); if (cause != null) { do { exMsg.append('\n').append('\t').append("Caused by: ").append(cause.getClass().getSimpleName()); exMsg.append(" : ").append(cause.getMessage()); cause = cause.getCause(); } while (cause != null); } logError(mc, role, exMsg.toString()); } /** * Is the inner class that does the actual processing of the WS Security header. It is based on {@link WSHandler} * provided by the Apache WSS4J library. Because the overall class already extends {@link BaseHandler} the inner * class construct is used. */ class WSSSendHandler extends WSHandler { /** * Logging facility */ private Log log; /** * The current message context */ private MessageContext msgCtx; /** * The WSS4J security engine configuration */ private WSSConfig wssConfig; /** * The DOM representation of the SOAP envelope */ private Document domEnvelope; /** * Callback handler that provides access to the SOAP attachments */ private CallbackHandler attachmentCBHandler; /** * Creates a WSSSendHandler for creating the security headers in the given message. * * @param mc The {@link MessageContext} for the message * @param doc The standard DOM representation of the SOAP Envelope of the message * @param log The log to use. We use the log of the handler to hide this class. */ WSSSendHandler(MessageContext mc, Document doc, Log handlerLog) throws WSSecurityException { this.msgCtx = mc; this.log = handlerLog; this.domEnvelope = doc; log.debug("Set up security engine configuration"); wssConfig = WSSConfig.getNewInstance(); attachmentCBHandler = new AttachmentCallbackHandler(msgCtx); } @Override public Object getOption(String string) { return msgCtx.getProperty(string); } @Override public Object getProperty(Object o, String string) { return getOption(string); } @Override public void setProperty(Object o, String string, Object o1) { msgCtx.setProperty(string, o1); } @Override public String getPassword(Object o) { return null; } @Override public void setPassword(Object o, String string) { } void createSecurityHeader(String actor, String actions) throws WSSecurityException { log.debug("Check actions to perform"); if (actions == null || actions.isEmpty()) { log.info("No security actions specified!"); return; } List<HandlerAction> actionList = WSSecurityUtil.decodeHandlerAction(actions.trim(), wssConfig); if (actionList == null || actionList.isEmpty()) { log.info("No security actions specified!"); return; } log.debug("Configure security processing environment"); msgCtx.setProperty(WSHandlerConstants.ACTOR, actor); RequestData reqData = new RequestData(); reqData.setWssConfig(wssConfig); reqData.setMsgContext(msgCtx); // Register callback handler for attachments reqData.setAttachmentCallbackHandler(this.attachmentCBHandler); // Check if we need a username (for UsernameToken or Signature) boolean userNameRequired = false; for (HandlerAction handlerAction : actionList) { if ((handlerAction.getAction() == WSConstants.SIGN || handlerAction.getAction() == WSConstants.UT || handlerAction.getAction() == WSConstants.UT_SIGN) && (handlerAction.getActionToken() == null || handlerAction.getActionToken().getUser() == null)) { userNameRequired = true; break; } } if (userNameRequired) { log.debug("A user name is needed because a username token or signature has to be inserted"); String userName = (String) getOption(WSHandlerConstants.USER); if (userName == null || userName.isEmpty()) userName = (String) getOption(WSHandlerConstants.SIGNATURE_USER); log.debug("Username for creating the " + actor + " WSS header is set to " + userName); if (userName == null || userName.isEmpty()) { // We need a username but don't have one :-( log.error("Required username for creating the WSS header is missing!"); //throw new AxisFault("NO_USERNAME"); } else reqData.setUsername(userName); } // Are we processing a request or response? boolean isRequest = isInFlow(INITIATOR); log.debug( "Create the \"" + actor + "\" WSS headers for this " + (isRequest ? "request." : "response.")); doSenderAction(domEnvelope, reqData, actionList, isRequest); log.debug("WSS header created."); } } }