All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.component.xmlsecurity.api.DefaultXmlSignature2Message Maven / Gradle / Ivy

/*
 * 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.camel.component.xmlsecurity.api;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.Manifest;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;

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

import org.apache.camel.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Maps the XML signature to a camel message. A output node is determined from the XML signature document via a node
 * search and then serialized and set to the output message body.
 * 

* There are three output node search types supported: "Default", "ElementName", and "XPath". All these search types * support enveloped XML signature or enveloping XML signature. *

*

    *
  • The "ElementName" search uses the local name and namespace specified in the search value to determine the output * element from the XML signature document. With the input parameter 'RemoveSignatureElements", you can specify whether * the signature elements should be removed from the resulting output document. This flag shall be used for enveloped * XML signatures. *
  • The "XPath" search uses an XPath expression to evaluate the output node. In this case the output node can be of * type Element, TextNode, or Document. With the input parameter 'RemoveSignatureElements", you can specify whether the * signature elements should be removed from the resulting output document. This flag shall be used for enveloped XML * signatures. *
  • The "Default" search is explained in more detail below. *
*

* Default Output Node Search: *

    * In the enveloped XML signature case, the XML document without the signature part is returned in the message body. *

    * In the enveloping XML signature case, the message body is determined from a referenced Object element in the * following way: *

      *
    • Only same document references are taken into account (URI must start with '#'). *
    • Also indirect same document references to an object via manifest are taken into account. *
    • The resulting number of object references must be 1. *
    • The referenced object must contain exactly 1 {@link DOMStructure}. *
    • The node of the DOMStructure is serialized to a byte array and added as body to the message. *
    * This does mean that the enveloping XML signature must have either the structure * *
     *     {@code
     *     
     *         
     *            
     *            
     *            ...
     *         
     *
     *         
     *              
     *         
     *         
     *         ...
     *         ()?
     *     
     *     }
     * 
     *
     * or the structure
     *
     * 
     *     {@code
     *     
     *         
     *            
     *            
     *            ...
     *         
     *
     *         
     *            
     *               
     *            
     *         
     *         
     *             
     *         
     *          
     *         ...
     *         ()?
     *     
     *     }
     * 
     *
     * 
     */
    public class DefaultXmlSignature2Message implements XmlSignature2Message {
    
        /**
         * Search type 'Default' for determining the output node.
         *
         */
        public static final String OUTPUT_NODE_SEARCH_TYPE_DEFAULT = "Default";
    
        /**
         * Search type 'ElementName' for determining the output element.
         *
         */
        public static final String OUTPUT_NODE_SEARCH_TYPE_ELEMENT_NAME = "ElementName";
    
        /**
         * Search type 'XPath' for determining the output node. Search value must be of type
         * {@link XPathFilterParameterSpec}.
         *
         */
        public static final String OUTPUT_NODE_SEARCH_TYPE_XPATH = "XPath";
    
        private static final Logger LOG = LoggerFactory.getLogger(DefaultXmlSignature2Message.class);
    
        @Override
        public void mapToMessage(Input input, Message output) throws Exception {
    
            Node node;
            boolean removeSignatureElements = false;
            if (OUTPUT_NODE_SEARCH_TYPE_DEFAULT.equals(input.getOutputNodeSearchType())) {
                LOG.debug("Searching for output node via default search");
                if (isEnveloping(input)) {
                    node = getNodeForMessageBodyInEnvelopingCase(input);
                } else {
                    // enveloped or detached XML signature  --> remove signature element
                    node = input.getMessageBodyDocument().getDocumentElement();
                    removeSignatureElements = true;
                }
            } else if (OUTPUT_NODE_SEARCH_TYPE_ELEMENT_NAME.equals(input.getOutputNodeSearchType())) {
                node = getOutputElementViaLocalNameAndNamespace(input);
            } else if (OUTPUT_NODE_SEARCH_TYPE_XPATH.equals(input.getOutputNodeSearchType())) {
                node = getOutputNodeViaXPath(input);
            } else {
                throw new XmlSignatureException(
                        String.format("Wrong configuration: The output node search type %s is not supported.",
                                input.getOutputNodeSearchType()));
            }
    
            LOG.debug("Output node with local name {} and namespace {} found", node.getLocalName(), node.getNamespaceURI());
    
            if (!removeSignatureElements) {
                removeSignatureElements = input.getRemoveSignatureElements() != null && input.getRemoveSignatureElements();
            }
    
            if (removeSignatureElements) {
                removeSignatureElements(node);
            }
    
            transformNodeToByteArrayAndSetToOutputMessage(input, output, node);
        }
    
        protected void transformNodeToByteArrayAndSetToOutputMessage(Input input, Message output, Node node)
                throws Exception {
    
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            XmlSignatureHelper.transformToOutputStream(node, os, omitXmlDeclaration(output, input), input.getOutputXmlEncoding());
            output.setBody(os.toByteArray());
            if (input.getOutputXmlEncoding() != null) {
                output.setHeader(XmlSignatureConstants.CHARSET_NAME, input.getOutputXmlEncoding());
            }
        }
    
        protected Node getOutputNodeViaXPath(Input input) throws Exception {
            checkSearchValueNotNull(input);
            checkSearchValueOfType(XPathFilterParameterSpec.class, input);
            XPathFilterParameterSpec xpathFilter = (XPathFilterParameterSpec) input.getOutputNodeSearch();
            XPathExpression expr = XmlSignatureHelper.getXPathExpression(xpathFilter);
            NodeList nodes = (NodeList) expr.evaluate(input.getMessageBodyDocument(), XPathConstants.NODESET);
            if (nodes == null || nodes.getLength() == 0) {
                throw new XmlSignatureException(
                        String.format(
                                "Cannot extract root node for the output document from the XML signature document. No node found for XPATH %s as specified in the output node search.",
                                xpathFilter.getXPath()));
            }
            if (nodes.getLength() > 1) {
                throw new XmlSignatureException(
                        String.format(
                                "Cannot extract root node for the output document from the XML signature document. XPATH %s as specified in the output node search results into more than one child.",
                                xpathFilter.getXPath()));
    
            }
            Node result = nodes.item(0);
            if (Node.ELEMENT_NODE == result.getNodeType() || Node.TEXT_NODE == result.getNodeType()
                    || Node.DOCUMENT_NODE == result.getNodeType()) {
                return result;
            }
            throw new XmlSignatureException(
                    String.format("Cannot extract root node for the output document from the XML signature document. "
                                  + "XPATH %s as specified in the output node search results into a node which has the wrong type.",
                            xpathFilter.getXPath()));
        }
    
        protected Node getOutputElementViaLocalNameAndNamespace(Input input) throws Exception {
            String search = getNonEmptyStringSearchValue(input);
            String namespace;
            String localName;
            if ('{' == search.charAt(0)) {
                // namespace
                int index = search.indexOf('}');
                if (index < 1) {
                    throw new XmlSignatureException(
                            String.format(
                                    "Wrong configuration: Value %s for the output node search %s has wrong format. "
                                          + "Value must have the form '{}' or '' if no the element has no namespace.",
                                    search, input.getOutputNodeSearchType()));
                }
                namespace = search.substring(1, index);
                if (search.length() < index + 1) {
                    throw new XmlSignatureException(
                            String.format(
                                    "Wrong configuration: Value %s for the output node search %s has wrong format. "
                                          + "Value must have the form '{}' or '' if no the element has no namespace.",
                                    search, input.getOutputNodeSearchType()));
                }
                localName = search.substring(index + 1);
            } else {
                namespace = null;
                localName = search;
            }
            NodeList nodeList = input.getMessageBodyDocument().getElementsByTagNameNS(namespace, localName);
            if (nodeList.getLength() == 0) {
                throw new XmlSignatureException(
                        String.format(
                                "Cannot extract root element for the output document from the XML signature document. Element with local name %s and namespace %s does not exist.",
                                namespace, localName));
            }
            if (nodeList.getLength() > 1) {
                throw new XmlSignatureException(
                        String.format(
                                "Cannot extract root element for the output document from the XML signature document. More than one element found with local name %s and namespace %s.",
                                namespace, localName));
            }
            return nodeList.item(0);
        }
    
        protected String getNonEmptyStringSearchValue(Input input) throws Exception {
            checkSearchValueNotNull(input);
            checkSearchValueOfType(String.class, input);
            String search = (String) input.getOutputNodeSearch();
            checkStringSarchValueNotEmpty(search, input.getOutputNodeSearchType());
            return search;
        }
    
        protected void checkSearchValueOfType(Class cl, Input input) throws Exception {
            if (!cl.isAssignableFrom(input.getOutputNodeSearch().getClass())) {
                throw new XMLSignatureException(
                        String.format(
                                "Wrong configuration: Search value is of class %s, the output node search %s requires class %s.",
                                input
                                        .getOutputNodeSearch().getClass().getName(),
                                input.getOutputNodeSearchType(), cl.getName()));
            }
    
        }
    
        protected void checkStringSarchValueNotEmpty(String searchValue, String outputNodeSearchType) throws Exception {
            if (searchValue.isEmpty()) {
                throw new XMLSignatureException(
                        String.format("Wrong configuration: Value for output node search %s is empty.",
                                outputNodeSearchType));
            }
        }
    
        protected void checkSearchValueNotNull(Input input) throws Exception {
            LOG.debug("Searching for output element with search value '{}' and sarch type {}", input.getOutputNodeSearch(),
                    input.getOutputNodeSearchType());
            if (input.getOutputNodeSearch() == null) {
                throw new XMLSignatureException(
                        String.format("Wrong configuration: Value is missing for output node search %s.",
                                input.getOutputNodeSearchType()));
            }
        }
    
        protected Node getNodeForMessageBodyInEnvelopingCase(Input input) throws Exception {
            Node node;
            List relevantReferences = getReferencesForMessageMapping(input);
    
            List relevantObjects = getObjectsForMessageMapping(input);
    
            DOMStructure domStruc = getDomStructureForMessageBody(relevantReferences, relevantObjects);
            node = domStruc.getNode();
            return node;
        }
    
        /**
         * Removes the Signature elements from the document.
         */
        protected void removeSignatureElements(Node node) {
            Document doc = XmlSignatureHelper.getDocument(node);
            NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
            List nodesToBeRemoved = new ArrayList<>(nl.getLength());
            for (int i = 0; i < nl.getLength(); i++) {
                // you cannot remove the nodes within this loop, because nl list would change
                nodesToBeRemoved.add(nl.item(i));
            }
            for (Node n : nodesToBeRemoved) {
                Node parent = n.getParentNode();
                if (parent != null) {
                    parent.removeChild(n);
                }
            }
        }
    
        /**
         * Checks whether the XML document has as root element the signature element.
         *
         * @param  input     XML signature input
         * @return           true if the root element of the xml signature document is the signature element;
         *                   otherwise false
         * @throws Exception
         */
        protected boolean isEnveloping(Input input) {
            Element el = input.getMessageBodyDocument().getDocumentElement();
            if ("Signature".equals(el.getLocalName()) && XMLSignature.XMLNS.equals(el.getNamespaceURI())) {
                return true;
            }
            return false;
        }
    
        protected Boolean omitXmlDeclaration(Message message, Input input) {
            Boolean omitXmlDeclaration = message.getHeader(XmlSignatureConstants.HEADER_OMIT_XML_DECLARATION, Boolean.class);
            if (omitXmlDeclaration == null) {
                omitXmlDeclaration = input.omitXmlDeclaration();
            }
            if (omitXmlDeclaration == null) {
                omitXmlDeclaration = Boolean.FALSE;
            }
            return omitXmlDeclaration;
        }
    
        /**
         * Returns the references whose referenced objects are taken into account for the message body. This message you can
         * use to filter the relevant references from the references provided by the input parameter.
         *
         *
         * @param  input     references and objects
         * @return           relevant references for the mapping to the camel message
         * @throws Exception if an error occurs
         */
        protected List getReferencesForMessageMapping(Input input) throws Exception {
            return input.getReferences();
        }
    
        /**
         * Returns the objects which must be taken into account for the mapping to the camel message.
         *
         * @param  input     references and objects
         * @return           relevant objects for the mapping to camel message
         * @throws Exception if an error occurs
         */
        protected List getObjectsForMessageMapping(Input input) throws Exception {
            return input.getObjects();
        }
    
        /**
         * Returns the DOM structure which is transformed to a byte array and set to the camel message body.
         *
         * @param  relevantReferences input from method {@link #getReferencesForMessageMapping(ReferencesAndObjects)}
         * @param  relevantObjects    input from method {@link #getObjectsForMessageMapping(ReferencesAndObjects)}
         * @return                    dom structure
         * @throws Exception          if an error occurs
         */
        protected DOMStructure getDomStructureForMessageBody(List relevantReferences, List relevantObjects)
                throws Exception {
    
            List referencedObjects = getReferencedSameDocumentObjects(relevantReferences, relevantObjects);
    
            if (referencedObjects.isEmpty()) {
                throw new XmlSignatureException(
                        "Unsupported XML signature document: Content object not found in the enveloping XML signature.");
            }
    
            if (referencedObjects.size() > 1) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < referencedObjects.size(); i++) {
                    XMLObject xmlOb = referencedObjects.get(i);
                    sb.append(xmlOb.getId());
                    if (i < referencedObjects.size() - 1) {
                        sb.append(", ");
                    }
                }
                throw new XmlSignatureException(
                        String.format(
                                "Unsupported XML signature document: More than one content objects found. Object IDs: %s",
                                sb.toString()));
            }
    
            @SuppressWarnings("unchecked")
            List structures = referencedObjects.get(0).getContent();
            if (structures.isEmpty()) {
                throw new XmlSignatureException(
                        "Unsupported XML signature: XML signature is not enveloping; content not found in XML signature: structure list is empty.");
            }
            if (structures.size() > 1) {
                throw new XmlSignatureException(
                        "Unsupported XML signature: more than one structure elements in referenced content object.");
            }
            XMLStructure structure = structures.get(0);
            // only dom currently supported
            DOMStructure domStruc = (DOMStructure) structure;
            return domStruc;
        }
    
        protected List getReferencedSameDocumentObjects(
                List relevantReferences, List relevantObjects) {
            List referencedObjects = new ArrayList<>(1);
    
            for (Reference ref : relevantReferences) {
                String refUri = getSameDocumentReferenceUri(ref);
                if (refUri == null) {
                    continue;
                }
                XMLObject referencedOb = getReferencedObject(relevantObjects, refUri);
                if (referencedOb != null) {
                    referencedObjects.add(referencedOb);
                    continue;
                }
                // content could also be indirectly referenced via manifest
                addManifestReferencedObjects(relevantObjects, referencedObjects, refUri);
            }
            return referencedObjects;
        }
    
        @SuppressWarnings("unchecked")
        protected void addManifestReferencedObjects(
                List allObjects, List referencedObjects, String manifestId) {
            Manifest manifest = getReferencedManifest(allObjects, manifestId);
            if (manifest == null) {
                return;
            }
            for (Reference manifestRef : manifest.getReferences()) {
                String manifestRefUri = getSameDocumentReferenceUri(manifestRef);
                if (manifestRefUri == null) {
                    continue;
                }
                XMLObject manifestReferencedOb = getReferencedObject(allObjects, manifestRefUri);
                if (manifestReferencedOb != null) {
                    referencedObjects.add(manifestReferencedOb);
                }
            }
        }
    
        protected String getSameDocumentReferenceUri(Reference ref) {
            String refUri = ref.getURI();
            if (refUri == null) {
                LOG.warn("Ignoring reference {} which has no URI", ref);
                return null;
            }
            if (!refUri.startsWith("#")) {
                LOG.warn("Ignoring non-same document reference {}", refUri);
                return null;
            }
            return refUri.substring(1);
        }
    
        protected Manifest getReferencedManifest(List objects, String id) {
            for (XMLObject xo : objects) {
                @SuppressWarnings("unchecked")
                List content = xo.getContent();
                for (XMLStructure xs : content) {
                    if (xs instanceof Manifest) {
                        Manifest man = (Manifest) xs;
                        if (id.equals(man.getId())) {
                            return man;
                        }
                    }
                }
            }
            return null;
        }
    
        protected XMLObject getReferencedObject(List objects, String id) {
            for (XMLObject ob : objects) {
                if (id.equals(ob.getId())) {
                    return ob;
                }
            }
            return null;
        }
    }