/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.cxf.ws.security.wss4j; import java.lang.reflect.Method; import java.security.Provider; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.security.auth.callback.CallbackHandler; import javax.xml.namespace.QName; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.dom.DOMSource; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.apache.cxf.attachment.AttachmentUtil; import org.apache.cxf.binding.soap.SoapFault; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.binding.soap.SoapVersion; import org.apache.cxf.binding.soap.saaj.SAAJInInterceptor; import org.apache.cxf.binding.soap.saaj.SAAJUtils; import org.apache.cxf.common.i18n.Message; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.endpoint.Endpoint; import org.apache.cxf.helpers.CastUtils; import org.apache.cxf.helpers.DOMUtils; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.message.MessageUtils; import org.apache.cxf.phase.Phase; import org.apache.cxf.rt.security.utils.SecurityUtils; import org.apache.cxf.security.transport.TLSSessionInfo; import org.apache.cxf.staxutils.StaxUtils; import org.apache.cxf.ws.security.SecurityConstants; import org.apache.cxf.ws.security.tokenstore.TokenStore; import org.apache.cxf.ws.security.tokenstore.TokenStoreUtils; import org.apache.wss4j.common.cache.ReplayCache; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.crypto.ThreadLocalSecurityProvider; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.dom.WSConstants; import org.apache.wss4j.dom.engine.WSSConfig; import org.apache.wss4j.dom.engine.WSSecurityEngine; import org.apache.wss4j.dom.engine.WSSecurityEngineResult; import org.apache.wss4j.dom.handler.RequestData; import org.apache.wss4j.dom.handler.WSHandlerConstants; import org.apache.wss4j.dom.handler.WSHandlerResult; import org.apache.wss4j.dom.processor.Processor; import org.apache.wss4j.dom.util.WSSecurityUtil; import org.apache.wss4j.dom.validate.NoOpValidator; import org.apache.wss4j.dom.validate.Validator; /** * Performs WS-Security inbound actions. */ public class WSS4JInInterceptor extends AbstractWSS4JInterceptor { /** * This configuration tag specifies the default attribute name where the roles are present * The default is "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role". */ public static final String SAML_ROLE_ATTRIBUTENAME_DEFAULT = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role"; public static final String PROCESSOR_MAP = "wss4j.processor.map"; public static final String VALIDATOR_MAP = "wss4j.validator.map"; public static final String SECURITY_PROCESSED = WSS4JInInterceptor.class.getName() + ".DONE"; private static final Logger LOG = LogUtils.getL7dLogger(WSS4JInInterceptor.class); private boolean ignoreActions; /** * */ private WSSecurityEngine secEngineOverride; public WSS4JInInterceptor() { super(); setPhase(Phase.PRE_PROTOCOL); getAfter().add(SAAJInInterceptor.class.getName()); getAfter().add("org.apache.cxf.ws.addressing.soap.MAPCodec"); } public WSS4JInInterceptor(boolean ignore) { this(); ignoreActions = ignore; } public WSS4JInInterceptor(Map<String, Object> properties) { this(); setProperties(properties); final Map<QName, Object> processorMap = CastUtils.cast( (Map<?, ?>)properties.get(PROCESSOR_MAP)); final Map<QName, Object> validatorMap = CastUtils.cast( (Map<?, ?>)properties.get(VALIDATOR_MAP)); if (processorMap != null) { if (validatorMap != null) { processorMap.putAll(validatorMap); } secEngineOverride = createSecurityEngine(processorMap); } else if (validatorMap != null) { secEngineOverride = createSecurityEngine(validatorMap); } } public void setIgnoreActions(boolean i) { ignoreActions = i; } private SOAPMessage getSOAPMessage(SoapMessage msg) { SAAJInInterceptor.INSTANCE.handleMessage(msg); return msg.getContent(SOAPMessage.class); } @Override public Object getProperty(Object msgContext, String key) { // use the superclass first Object result = super.getProperty(msgContext, key); // handle the special case of the SEND_SIGV if (result == null && WSHandlerConstants.SEND_SIGV.equals(key) && this.isRequestor((SoapMessage)msgContext)) { result = ((SoapMessage)msgContext).getExchange().getOutMessage().get(key); } return result; } public final boolean isGET(SoapMessage message) { String method = (String)message.get(SoapMessage.HTTP_REQUEST_METHOD); return "GET".equals(method) && message.getContent(XMLStreamReader.class) == null; } public void handleMessage(SoapMessage msg) throws Fault { if (msg.containsKey(SECURITY_PROCESSED) || isGET(msg) || msg.getExchange() == null) { return; } Object provider = msg.getExchange().get(Provider.class); final boolean useCustomProvider = provider != null && ThreadLocalSecurityProvider.isInstalled(); try { if (useCustomProvider) { ThreadLocalSecurityProvider.setProvider((Provider)provider); } handleMessageInternal(msg); } finally { if (useCustomProvider) { ThreadLocalSecurityProvider.unsetProvider(); } } } @SuppressWarnings("deprecation") private void handleMessageInternal(SoapMessage msg) throws Fault { boolean utWithCallbacks = MessageUtils.getContextualBoolean(msg, SecurityConstants.VALIDATE_TOKEN, true); translateProperties(msg); RequestData reqData = new CXFRequestData(); WSSConfig config = (WSSConfig)msg.getContextualProperty(WSSConfig.class.getName()); WSSecurityEngine engine; if (config != null) { engine = new WSSecurityEngine(); engine.setWssConfig(config); } else { engine = getSecurityEngine(utWithCallbacks); if (engine == null) { engine = new WSSecurityEngine(); } config = engine.getWssConfig(); } reqData.setWssConfig(config); reqData.setEncryptionSerializer(new StaxSerializer()); // Add Audience Restrictions for SAML configureAudienceRestriction(msg, reqData); SOAPMessage doc = getSOAPMessage(msg); boolean doDebug = LOG.isLoggable(Level.FINE); SoapVersion version = msg.getVersion(); if (doDebug) { LOG.fine("WSS4JInInterceptor: enter handleMessage()"); } /* * The overall try, just to have a finally at the end to perform some * housekeeping. */ try { reqData.setMsgContext(msg); reqData.setAttachmentCallbackHandler(new AttachmentCallbackHandler(msg)); setAlgorithmSuites(msg, reqData); reqData.setCallbackHandler(getCallback(reqData, utWithCallbacks)); computeAction(msg, reqData); String action = getAction(msg, version); List<Integer> actions = WSSecurityUtil.decodeAction(action); String actor = (String)getOption(WSHandlerConstants.ACTOR); if (actor == null) { actor = (String)msg.getContextualProperty(SecurityConstants.ACTOR); } reqData.setActor(actor); // Configure replay caching configureReplayCaches(reqData, actions, msg); TLSSessionInfo tlsInfo = msg.get(TLSSessionInfo.class); if (tlsInfo != null) { Certificate[] tlsCerts = tlsInfo.getPeerCertificates(); reqData.setTlsCerts(tlsCerts); } /* * Get and check the Signature specific parameters first because * they may be used for encryption too. */ doReceiverAction(actions, reqData); // Only search for and expand (Signed) XOP Elements if MTOM is enabled (and not // explicitly specified by the user) if (getString(WSHandlerConstants.EXPAND_XOP_INCLUDE_FOR_SIGNATURE, msg) == null && getString(WSHandlerConstants.EXPAND_XOP_INCLUDE, msg) == null) { reqData.setExpandXopInclude(AttachmentUtil.isMtomEnabled(msg)); } /*get chance to check msg context enableRevocation setting *when use policy based ws-security where the WSHandler configuration *isn't available */ boolean enableRevocation = reqData.isRevocationEnabled() || MessageUtils.isTrue(SecurityUtils.getSecurityPropertyValue(SecurityConstants.ENABLE_REVOCATION, msg)); reqData.setEnableRevocation(enableRevocation); Element soapBody = SAAJUtils.getBody(doc); if (soapBody != null) { engine.setCallbackLookup(new CXFCallbackLookup(soapBody.getOwnerDocument(), soapBody)); } Element elem = WSSecurityUtil.getSecurityHeader(doc.getSOAPHeader(), actor, version.getVersion() != 1.1); elem = (Element)DOMUtils.getDomElement(elem); Node originalNode = null; if (elem != null) { originalNode = elem.cloneNode(true); } WSHandlerResult wsResult = engine.processSecurityHeader(elem, reqData); importNewDomToSAAJ(doc, elem, originalNode); Element header = SAAJUtils.getHeader(doc); Element body = SAAJUtils.getBody(doc); header = (Element)DOMUtils.getDomElement(header); body = (Element)DOMUtils.getDomElement(body); if (!(wsResult.getResults() == null || wsResult.getResults().isEmpty())) { // security header found if (reqData.isEnableSignatureConfirmation()) { checkSignatureConfirmation(reqData, wsResult); } checkActions(msg, reqData, wsResult.getResults(), actions, SAAJUtils.getBody(doc)); doResults( msg, actor, header, body, wsResult, utWithCallbacks ); } else { // no security header found if (doc.getSOAPPart().getEnvelope().getBody().hasFault() && isRequestor(msg)) { LOG.warning("The request is a SOAP Fault, but it is not secured"); // We allow lax action matching here for backwards compatibility // with manually configured WSS4JInInterceptors that previously // allowed faults to pass through even if their actions aren't // a strict match against those configured. In the WS-SP case, // we will want to still call doResults as it handles asserting // certain assertions that do not require a WS-S header such as // a sp:TransportBinding assertion. In the case of WS-SP, // the unasserted assertions will provide confirmation that // security was not sufficient. // checkActions(msg, reqData, wsResult, actions); doResults(msg, actor, header, body, wsResult, utWithCallbacks); } else { checkActions(msg, reqData, wsResult.getResults(), actions, SAAJUtils.getBody(doc)); doResults(msg, actor, header, body, wsResult, utWithCallbacks); } } if (SAAJUtils.getBody(doc) != null) { advanceBody(msg, body); } SAAJInInterceptor.replaceHeaders(doc, msg); if (doDebug) { LOG.fine("WSS4JInInterceptor: exit handleMessage()"); } msg.put(SECURITY_PROCESSED, Boolean.TRUE); } catch (WSSecurityException e) { throw WSS4JUtils.createSoapFault(msg, version, e); } catch (XMLStreamException e) { throw new SoapFault(new Message("STAX_EX", LOG), e, version.getSender()); } catch (SOAPException e) { throw new SoapFault(new Message("SAAJ_EX", LOG), e, version.getSender()); } finally { reqData = null; } } private void importNewDomToSAAJ(SOAPMessage doc, Element elem, Node originalNode) throws SOAPException { if (DOMUtils.isJava9SAAJ() && originalNode != null && !originalNode.isEqualNode(elem)) { //ensure the new decrypted dom element could be imported into the SAAJ Node node = null; Document document = null; Element body = SAAJUtils.getBody(doc); if (body != null) { document = body.getOwnerDocument(); } if (elem != null && elem.getOwnerDocument() != null && elem.getOwnerDocument().getDocumentElement() != null) { node = elem.getOwnerDocument(). getDocumentElement().getFirstChild().getNextSibling().getFirstChild(); } if (document != null && node != null) { Node newNode = null; try { newNode = document.importNode(node, true); if (newNode != null) { try { Method method = newNode.getClass().getMethod("getDomElement"); newNode = (Element)method.invoke(newNode); } catch (java.lang.NoSuchMethodException ex) { // do nothing; } } elem.getOwnerDocument().getDocumentElement().getFirstChild(). getNextSibling().replaceChild(newNode, node); } catch (Exception ex) { //just to the best try } } } } private void configureAudienceRestriction(SoapMessage msg, RequestData reqData) { // Add Audience Restrictions for SAML boolean enableAudienceRestriction = SecurityUtils.getSecurityPropertyBoolean(SecurityConstants.AUDIENCE_RESTRICTION_VALIDATION, msg, true); if (enableAudienceRestriction) { List<String> audiences = new ArrayList<>(); if (msg.get(org.apache.cxf.message.Message.REQUEST_URL) != null) { audiences.add((String)msg.get(org.apache.cxf.message.Message.REQUEST_URL)); } else if (msg.get(org.apache.cxf.message.Message.REQUEST_URI) != null) { audiences.add((String)msg.get(org.apache.cxf.message.Message.REQUEST_URI)); } if (msg.getContextualProperty("javax.xml.ws.wsdl.service") != null) { audiences.add(msg.getContextualProperty("javax.xml.ws.wsdl.service").toString()); } reqData.setAudienceRestrictions(audiences); } } private void checkActions( SoapMessage msg, RequestData reqData, List<WSSecurityEngineResult> wsResult, List<Integer> actions, Element body ) throws WSSecurityException { if (ignoreActions) { // Not applicable for the WS-SecurityPolicy case return; } // now check the security actions: do they match, in any order? if (!checkReceiverResultsAnyOrder(wsResult, actions)) { LOG.warning("Security processing failed (actions mismatch)"); throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY); } // Now check to see if SIGNATURE_PARTS are specified String signatureParts = (String)getProperty(msg, WSHandlerConstants.SIGNATURE_PARTS); if (signatureParts != null) { String warning = "To enforce that particular elements were signed you must either " + "use WS-SecurityPolicy, or else use the CryptoCoverageChecker or " + "SignatureCoverageChecker"; LOG.warning(warning); } } /** * Do whatever is necessary to determine the action for the incoming message and * do whatever other setup work is necessary. * * @param msg * @param reqData */ protected void computeAction(SoapMessage msg, RequestData reqData) throws WSSecurityException { // // Try to get Crypto Provider from message context properties. // It gives a possibility to use external Crypto Provider // Crypto encCrypto = (Crypto)SecurityUtils.getSecurityPropertyValue(SecurityConstants.ENCRYPT_CRYPTO, msg); if (encCrypto != null) { reqData.setDecCrypto(encCrypto); } Crypto sigCrypto = (Crypto)SecurityUtils.getSecurityPropertyValue(SecurityConstants.SIGNATURE_CRYPTO, msg); if (sigCrypto != null) { reqData.setSigVerCrypto(sigCrypto); } } protected void configureReplayCaches(RequestData reqData, List<Integer> actions, SoapMessage msg) throws WSSecurityException { if (isNonceCacheRequired(actions, msg)) { ReplayCache nonceCache = getReplayCache( msg, SecurityConstants.ENABLE_NONCE_CACHE, SecurityConstants.NONCE_CACHE_INSTANCE ); reqData.setNonceReplayCache(nonceCache); } if (isTimestampCacheRequired(actions, msg)) { ReplayCache timestampCache = getReplayCache( msg, SecurityConstants.ENABLE_TIMESTAMP_CACHE, SecurityConstants.TIMESTAMP_CACHE_INSTANCE ); reqData.setTimestampReplayCache(timestampCache); } if (isSamlCacheRequired(actions, msg)) { ReplayCache samlCache = getReplayCache( msg, SecurityConstants.ENABLE_SAML_ONE_TIME_USE_CACHE, SecurityConstants.SAML_ONE_TIME_USE_CACHE_INSTANCE ); reqData.setSamlOneTimeUseReplayCache(samlCache); } } /** * Is a Nonce Cache required, i.e. are we expecting a UsernameToken */ protected boolean isNonceCacheRequired(List<Integer> actions, SoapMessage msg) { return actions.contains(WSConstants.UT) || actions.contains(WSConstants.UT_NOPASSWORD); } /** * Is a Timestamp cache required, i.e. are we expecting a Timestamp */ protected boolean isTimestampCacheRequired(List<Integer> actions, SoapMessage msg) { return actions.contains(WSConstants.TS); } /** * Is a SAML Cache required, i.e. are we expecting a SAML Token */ protected boolean isSamlCacheRequired(List<Integer> actions, SoapMessage msg) { return actions.contains(WSConstants.ST_UNSIGNED) || actions.contains(WSConstants.ST_SIGNED); } /** * Set a WSS4J AlgorithmSuite object on the RequestData context, to restrict the * algorithms that are allowed for encryption, signature, etc. */ protected void setAlgorithmSuites(SoapMessage message, RequestData data) throws WSSecurityException { super.decodeAlgorithmSuite(data); } protected void doResults( SoapMessage msg, String actor, Element soapHeader, Element soapBody, WSHandlerResult wsResult, boolean utWithCallbacks ) throws SOAPException, XMLStreamException, WSSecurityException { /* * All ok up to this point. Now construct and setup the security result * structure. The service may fetch this and check it. */ List<WSHandlerResult> results = CastUtils.cast((List<?>)msg.get(WSHandlerConstants.RECV_RESULTS)); if (results == null) { results = new LinkedList<>(); msg.put(WSHandlerConstants.RECV_RESULTS, results); } results.add(0, wsResult); WSS4JSecurityContextCreator contextCreator = (WSS4JSecurityContextCreator)SecurityUtils.getSecurityPropertyValue( SecurityConstants.SECURITY_CONTEXT_CREATOR, msg); if (contextCreator != null) { contextCreator.createSecurityContext(msg, wsResult); } else { new DefaultWSS4JSecurityContextCreator().createSecurityContext(msg, wsResult); } } protected void advanceBody( SoapMessage msg, Node body ) throws SOAPException, XMLStreamException, WSSecurityException { XMLStreamReader reader = StaxUtils.createXMLStreamReader(new DOMSource(body)); // advance just past body int evt = reader.next(); if (reader.hasNext() && evt != XMLStreamConstants.END_ELEMENT) { reader.next(); } msg.setContent(XMLStreamReader.class, reader); } private String getAction(SoapMessage msg, SoapVersion version) { String action = (String)getOption(WSHandlerConstants.ACTION); if (action == null) { action = (String)msg.get(WSHandlerConstants.ACTION); } if (action == null) { LOG.warning("No security action was defined!"); throw new SoapFault("No security action was defined!", version.getReceiver()); } return action; } protected CallbackHandler getCallback(RequestData reqData, boolean utWithCallbacks) throws WSSecurityException { if (!utWithCallbacks) { CallbackHandler pwdCallback = null; try { pwdCallback = getCallback(reqData); } catch (Exception ex) { // ignore } return new DelegatingCallbackHandler(pwdCallback); } else { return getCallback(reqData); } } protected CallbackHandler getCallback(RequestData reqData) throws WSSecurityException { Object o = SecurityUtils.getSecurityPropertyValue(SecurityConstants.CALLBACK_HANDLER, (SoapMessage)reqData.getMsgContext()); CallbackHandler cbHandler = null; try { cbHandler = SecurityUtils.getCallbackHandler(o); } catch (Exception ex) { throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, ex); } if (cbHandler == null) { try { cbHandler = getPasswordCallbackHandler(reqData); } catch (WSSecurityException sec) { Endpoint ep = ((SoapMessage)reqData.getMsgContext()).getExchange().getEndpoint(); if (ep != null && ep.getEndpointInfo() != null) { TokenStore store = TokenStoreUtils.getTokenStore((SoapMessage)reqData.getMsgContext()); return new TokenStoreCallbackHandler(null, store); } throw sec; } } Endpoint ep = ((SoapMessage)reqData.getMsgContext()).getExchange().getEndpoint(); if (ep != null && ep.getEndpointInfo() != null) { TokenStore store = TokenStoreUtils.getTokenStore((SoapMessage)reqData.getMsgContext()); return new TokenStoreCallbackHandler(cbHandler, store); } return cbHandler; } /** * @return the WSSecurityEngine in use by this interceptor. * This engine is defined to be the secEngineOverride * instance, if defined in this class (and supplied through * construction); otherwise, it is taken to be the default * WSSecEngine instance (currently defined in the WSHandler * base class). */ protected WSSecurityEngine getSecurityEngine(boolean utWithCallbacks) { if (secEngineOverride != null) { return secEngineOverride; } if (!utWithCallbacks) { Map<QName, Object> profiles = new HashMap<>(1); Validator validator = new NoOpValidator(); profiles.put(WSConstants.USERNAME_TOKEN, validator); return createSecurityEngine(profiles); } return null; } /** * @return a freshly minted WSSecurityEngine instance, using the * (non-null) processor map, to be used to initialize the * WSSecurityEngine instance. */ protected static WSSecurityEngine createSecurityEngine(final Map<QName, Object> map) { assert map != null; final WSSConfig config = WSSConfig.getNewInstance(); for (Map.Entry<QName, Object> entry : map.entrySet()) { final QName key = entry.getKey(); Object val = entry.getValue(); if (val instanceof Class<?>) { config.setProcessor(key, (Class<?>)val); } else if (val instanceof Processor) { config.setProcessor(key, (Processor)val); } else if (val instanceof Validator) { config.setValidator(key, (Validator)val); } else if (val == null) { config.setProcessor(key, (Class<?>)null); } } final WSSecurityEngine ret = new WSSecurityEngine(); ret.setWssConfig(config); return ret; } /** * Get a ReplayCache instance. It first checks to see whether caching has been explicitly * enabled or disabled via the booleanKey argument. If it has been set to false then no * replay caching is done (for this booleanKey). If it has not been specified, then caching * is enabled only if we are not the initiator of the exchange. If it has been specified, then * caching is enabled. * * It tries to get an instance of ReplayCache via the instanceKey argument from a * contextual property, and failing that the message exchange. If it can't find any, then it * defaults to using an EH-Cache instance and stores that on the message exchange. */ protected ReplayCache getReplayCache( SoapMessage message, String booleanKey, String instanceKey ) { return WSS4JUtils.getReplayCache(message, booleanKey, instanceKey); } }