Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| /** | |
| * 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.Exchange; | |
| 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. | |
| * <p> | |
| * There are three output node search types supported: "Default", "ElementName", | |
| * and "XPath". All these search types support enveloped XML signature or | |
| * enveloping XML signature. | |
| * <p> | |
| * <ul> | |
| * <li>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. | |
| * <li>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. | |
| * <li>The "Default" search is explained in more detail below. | |
| * </ul> | |
| * <p> | |
| * Default Output Node Search: | |
| * <ul> | |
| * In the enveloped XML signature case, the XML document without the signature | |
| * part is returned in the message body. | |
| * <p> | |
| * In the enveloping XML signature case, the message body is determined from a | |
| * referenced Object element in the following way: | |
| * <ul> | |
| * <li>Only same document references are taken into account (URI must start with | |
| * '#'). | |
| * <li>Also indirect same document references to an object via manifest are | |
| * taken into account. | |
| * <li>The resulting number of object references must be 1. | |
| * <li>The referenced object must contain exactly 1 {@link DOMStructure}. | |
| * <li>The node of the DOMStructure is serialized to a byte array and added as | |
| * body to the message. | |
| * </ul> | |
| * This does mean that the enveloping XML signature must have either the | |
| * structure | |
| * | |
| * <pre> | |
| * {@code | |
| * <Signature> | |
| * <SignedInfo> | |
| * <Reference URI="#object"/> | |
| * <!-- further references possible but they must not point to an Object or Manifest containing an object reference --> | |
| * ... | |
| * </SignedInfo> | |
| * | |
| * <Object Id="object"> | |
| * <!-- contains the DOM node which should be extracted to the message body --> | |
| * <Object> | |
| * <!-- further object elements possible which are not referenced--> | |
| * ... | |
| * (<KeyInfo>)? | |
| * </Signature> | |
| * } | |
| * </pre> | |
| * | |
| * or the structure | |
| * | |
| * <pre> | |
| * {@code | |
| * <Signature> | |
| * <SignedInfo> | |
| * <Reference URI="#manifest"/> | |
| * <!-- further references are possible but they must not point to an Object or other manifest containing an object reference --> | |
| * ... | |
| * </SignedInfo> | |
| * | |
| * <Object > | |
| * <Manifest Id="manifest"> | |
| * <Reference URI=#object/> | |
| * </Manifest> | |
| * </Objet> | |
| * <Object Id="object"> | |
| * <!-- contains the DOM node which should be extracted to the message body --> | |
| * </Object> | |
| * <!-- further object elements possible which are not referenced --> | |
| * ... | |
| * (<KeyInfo>)? | |
| * </Signature> | |
| * } | |
| * </pre> | |
| * | |
| * </ul> | |
| */ | |
| 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 { //NOPMD | |
| 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(Exchange.CHARSET_NAME, input.getOutputXmlEncoding()); | |
| } | |
| } | |
| protected Node getOutputNodeViaXPath(Input input) throws Exception { //NOPMD | |
| 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 { //NOPMD | |
| 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 '{<namespace>}<element local name>' or '<element local name>' 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 '{<namespace>}<element local name>' or '<element local name>' 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 { //NOPMD | |
| 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 { //NOPMD | |
| if (!cl.isAssignableFrom(input.getOutputNodeSearch().getClass())) { | |
| throw new XMLSignatureException(String.format( | |
| "Wrong configruation: 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 { //NOPMD | |
| if (searchValue.isEmpty()) { | |
| throw new XMLSignatureException(String.format("Wrong configruation: Value for output node search %s is empty.", | |
| outputNodeSearchType)); | |
| } | |
| } | |
| protected void checkSearchValueNotNull(Input input) throws Exception { //NOPMD | |
| 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 configruation: Value is missing for output node search %s.", | |
| input.getOutputNodeSearchType())); | |
| } | |
| } | |
| protected Node getNodeForMessageBodyInEnvelopingCase(Input input) throws Exception { //NOPMD | |
| Node node; | |
| List<Reference> relevantReferences = getReferencesForMessageMapping(input); | |
| List<XMLObject> relevantObjects = getObjectsForMessageMapping(input); | |
| DOMStructure domStruc = getDomStructureForMessageBody(relevantReferences, relevantObjects); | |
| node = domStruc.getNode(); | |
| return node; | |
| } | |
| /** | |
| * Removes the Signature elements from the document. | |
| * | |
| * @param doc | |
| * document | |
| */ | |
| protected void removeSignatureElements(Node node) { | |
| Document doc = XmlSignatureHelper.getDocument(node); | |
| NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); | |
| List<Node> 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 <code>true</code> if the root element of the xml signature | |
| * document is the signature element; otherwise <code>false</code> | |
| * @throws Exception | |
| */ | |
| protected boolean isEnveloping(Input input) throws Exception { //NOPMD | |
| 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<Reference> getReferencesForMessageMapping(Input input) throws Exception { //NOPMD | |
| 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<XMLObject> getObjectsForMessageMapping(Input input) throws Exception { //NOPMD | |
| 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<Reference> relevantReferences, List<XMLObject> relevantObjects) | |
| throws Exception { //NOPMD | |
| List<XMLObject> referencedObjects = getReferencedSameDocumentObjects(relevantReferences, relevantObjects); | |
| if (referencedObjects.isEmpty()) { | |
| throw new XmlSignatureException( | |
| String.format("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<XMLStructure> structures = referencedObjects.get(0).getContent(); | |
| if (structures.size() == 0) { | |
| 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<XMLObject> getReferencedSameDocumentObjects(List<Reference> relevantReferences, List<XMLObject> relevantObjects) { | |
| List<XMLObject> 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<XMLObject> allObjects, List<XMLObject> referencedObjects, String manifestId) { | |
| Manifest manifest = getReferencedManifest(allObjects, manifestId); | |
| if (manifest == null) { | |
| return; | |
| } | |
| for (Reference manifestRef : (List<Reference>) 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<XMLObject> objects, String id) { | |
| for (XMLObject xo : objects) { | |
| @SuppressWarnings("unchecked") | |
| List<XMLStructure> 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<XMLObject> objects, String id) { | |
| for (XMLObject ob : objects) { | |
| if (id.equals(ob.getId())) { | |
| return ob; | |
| } | |
| } | |
| return null; | |
| } | |
| } |