/**
* 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.io.IOException;
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.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import org.w3c.dom.Document;
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.
* <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 (isEnveloped(input)) {
// enveloped XML signature --> remove signature element
node = input.getMessageBodyDocument().getDocumentElement();
removeSignatureElements = true;
} else {
node = getNodeForMessageBodyInNonEnvelopedCase(input);
}
} 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 TransformerFactoryConfigurationError, TransformerConfigurationException, TransformerException, IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
XmlSignatureHelper.transformToOutputStream(node, os, omitXmlDeclaration(output, input));
output.setBody(os.toByteArray());
}
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 getNodeForMessageBodyInNonEnvelopedCase(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("http://www.w3.org/2000/09/xmldsig#", "Signature");
for (int i = 0; i < nl.getLength(); i++) {
Node n = nl.item(i);
Node parent = n.getParentNode();
if (parent != null) {
parent.removeChild(n);
}
}
}
/**
* Returns the enveloped data in case of an enveloped XML signature.
*
* @param input
* references of signed info and objects
* @return <code>true</code> if there exists a reference with URI = "" and
* with {@link Transform#ENVELOPED} transform; otherwise
* <code>false</code>
* @throws Exception
*/
@SuppressWarnings("unchecked")
protected boolean isEnveloped(Input input) throws Exception { //NOPMD
for (Reference ref : input.getReferences()) {
if ("".equals(ref.getURI())) {
for (Transform t : (List<Transform>)ref.getTransforms()) {
if (Transform.ENVELOPED.equals(t.getAlgorithm())) {
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 XML signature. Detached or enveloped signatures are not supported."));
}
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<XMLObject>(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;
}
}