/** * 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.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.xml.XMLConstants; import javax.xml.crypto.AlgorithmMethod; import javax.xml.crypto.dom.DOMStructure; import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.spec.ExcC14NParameterSpec; import javax.xml.crypto.dsig.spec.XPathFilter2ParameterSpec; import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec; import javax.xml.crypto.dsig.spec.XPathType; import javax.xml.crypto.dsig.spec.XSLTTransformParameterSpec; import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.validation.Schema; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.bootstrap.DOMImplementationRegistry; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSOutput; import org.w3c.dom.ls.LSSerializer; import org.xml.sax.SAXException; import org.apache.camel.util.IOHelper; /** * Helps to construct the transformations and the canonicalization methods for * the XML Signature generator. */ public final class XmlSignatureHelper { private XmlSignatureHelper() { // Helper class } /** * Returns a configuration for a canonicalization algorithm. * * @param algorithm * algorithm URI * @return canonicalization * @throws IllegalArgumentException * if <tt>algorithm</tt> is <code>null</code> */ public static AlgorithmMethod getCanonicalizationMethod(String algorithm) { return getCanonicalizationMethod(algorithm, null); } /** * Returns a configuration for a canonicalization algorithm. * * @param algorithm * algorithm URI * @param inclusiveNamespacePrefixes * namespace prefixes which should be treated like in the * inclusive canonicalization, only relevant if the algorithm is * exclusive * @return canonicalization * @throws IllegalArgumentException * if <tt>algorithm</tt> is <code>null</code> */ public static AlgorithmMethod getCanonicalizationMethod(String algorithm, List<String> inclusiveNamespacePrefixes) { if (algorithm == null) { throw new IllegalArgumentException("algorithm is null"); } XmlSignatureTransform canonicalizationMethod = new XmlSignatureTransform(algorithm); if (inclusiveNamespacePrefixes != null) { ExcC14NParameterSpec parameters = new ExcC14NParameterSpec(inclusiveNamespacePrefixes); canonicalizationMethod.setParameterSpec(parameters); } return canonicalizationMethod; } public static AlgorithmMethod getEnvelopedTransform() { return new XmlSignatureTransform(Transform.ENVELOPED); } /** * Returns a configuration for a base64 transformation. * * @return Base64 transformation */ public static AlgorithmMethod getBase64Transform() { return new XmlSignatureTransform(Transform.BASE64); } /** * Returns a configuration for an XPATH transformation. * * @param xpath * XPATH expression * @return XPATH transformation * @throws IllegalArgumentException * if <tt>xpath</tt> is <code>null</code> */ public static AlgorithmMethod getXPathTransform(String xpath) { return getXPathTransform(xpath, null); } /** * Returns a configuration for an XPATH transformation which needs a * namespace map. * * @param xpath * XPATH expression * @param namespaceMap * namespace map, key is the prefix, value is the namespace, can * be <code>null</code> * @throws IllegalArgumentException * if <tt>xpath</tt> is <code>null</code> * @return XPATH transformation */ public static AlgorithmMethod getXPathTransform(String xpath, Map<String, String> namespaceMap) { if (xpath == null) { throw new IllegalArgumentException("xpath is null"); } XmlSignatureTransform transformXPath = new XmlSignatureTransform(); transformXPath.setAlgorithm(Transform.XPATH); XPathFilterParameterSpec params = getXpathFilter(xpath, namespaceMap); transformXPath.setParameterSpec(params); return transformXPath; } public static XPathFilterParameterSpec getXpathFilter(String xpath, Map<String, String> namespaceMap) { XPathFilterParameterSpec params = namespaceMap == null ? new XPathFilterParameterSpec(xpath) : new XPathFilterParameterSpec(xpath, namespaceMap); return params; } public static XPathFilterParameterSpec getXpathFilter(String xpath) { return getXpathFilter(xpath, null); } @SuppressWarnings("unchecked") public static XPathExpression getXPathExpression(XPathFilterParameterSpec xpathFilter) throws XPathExpressionException { XPathFactory factory = XPathFactory.newInstance(); XPath xpath = factory.newXPath(); if (xpathFilter.getNamespaceMap() != null) { xpath.setNamespaceContext(new XPathNamespaceContext(xpathFilter.getNamespaceMap())); } return xpath.compile(xpathFilter.getXPath()); } private static class XPathNamespaceContext implements NamespaceContext { private final Map<String, String> prefix2Namespace; XPathNamespaceContext(Map<String, String> prefix2Namespace) { this.prefix2Namespace = prefix2Namespace; } public String getNamespaceURI(String prefix) { if (prefix == null) { throw new NullPointerException("Null prefix"); } if ("xml".equals(prefix)) { return XMLConstants.XML_NS_URI; } String ns = prefix2Namespace.get(prefix); if (ns != null) { return ns; } return XMLConstants.NULL_NS_URI; } // This method isn't necessary for XPath processing. public String getPrefix(String uri) { throw new UnsupportedOperationException(); } // This method isn't necessary for XPath processing either. @SuppressWarnings("rawtypes") public Iterator getPrefixes(String uri) { throw new UnsupportedOperationException(); } } /** * Returns a configuration for an XPATH2 transformation. * * @param xpath * XPATH expression * @param filter * possible values are "intersect", "subtract", "union" * @throws IllegalArgumentException * if <tt>xpath</tt> or <tt>filter</tt> is <code>null</code>, or * is neither "intersect", nor "subtract", nor "union" * @return XPATH transformation */ public static AlgorithmMethod getXPath2Transform(String xpath, String filter) { return getXPath2Transform(xpath, filter, null); } /** * Returns a configuration for an XPATH2 transformation which consists of * several XPATH expressions. * * @param xpathAndFilterList * list of XPATH expressions with their filters * @param namespaceMap * namespace map, key is the prefix, value is the namespace, can * be <code>null</code> * @throws IllegalArgumentException * if <tt>xpathAndFilterList</tt> is <code>null</code> or empty, * or the specified filter values are neither "intersect", nor * "subtract", nor "union" * @return XPATH transformation */ public static AlgorithmMethod getXPath2Transform(String xpath, String filter, Map<String, String> namespaceMap) { XPathAndFilter xpathAndFilter = new XPathAndFilter(); xpathAndFilter.setXpath(xpath); xpathAndFilter.setFilter(filter); List<XPathAndFilter> list = new ArrayList<XmlSignatureHelper.XPathAndFilter>(1); list.add(xpathAndFilter); return getXPath2Transform(list, namespaceMap); } /** * Returns a configuration for an XPATH2 transformation which consists of * several XPATH expressions. * * @param xpathAndFilterList * list of XPATH expressions with their filters * @param namespaceMap * namespace map, key is the prefix, value is the namespace, can * be <code>null</code> * @throws IllegalArgumentException * if <tt>xpathAndFilterList</tt> is <code>null</code> or empty, * or the specified filter values are neither "intersect", nor * "subtract", nor "union" * @return XPATH transformation */ public static AlgorithmMethod getXPath2Transform(List<XPathAndFilter> xpathAndFilterList, Map<String, String> namespaceMap) { if (xpathAndFilterList == null) { throw new IllegalArgumentException("xpathAndFilterList is null"); } if (xpathAndFilterList.isEmpty()) { throw new IllegalArgumentException("XPath and filter list is empty"); } List<XPathType> list = getXPathTypeList(xpathAndFilterList, namespaceMap); XmlSignatureTransform transformXPath = new XmlSignatureTransform(Transform.XPATH2); transformXPath.setParameterSpec(new XPathFilter2ParameterSpec(list)); return transformXPath; } private static List<XPathType> getXPathTypeList(List<XPathAndFilter> xpathAndFilterList, Map<String, String> namespaceMap) { List<XPathType> list = new ArrayList<XPathType>(xpathAndFilterList.size()); for (XPathAndFilter xpathAndFilter : xpathAndFilterList) { XPathType.Filter xpathFilter; if (XPathType.Filter.INTERSECT.toString().equals(xpathAndFilter.getFilter())) { xpathFilter = XPathType.Filter.INTERSECT; } else if (XPathType.Filter.SUBTRACT.toString().equals(xpathAndFilter.getFilter())) { xpathFilter = XPathType.Filter.SUBTRACT; } else if (XPathType.Filter.UNION.toString().equals(xpathAndFilter.getFilter())) { xpathFilter = XPathType.Filter.UNION; } else { throw new IllegalStateException(String.format("XPATH %s has a filter %s not supported", xpathAndFilter.getXpath(), xpathAndFilter.getFilter())); } XPathType xpathtype = namespaceMap == null ? new XPathType(xpathAndFilter.getXpath(), xpathFilter) : new XPathType( xpathAndFilter.getXpath(), xpathFilter, namespaceMap); list.add(xpathtype); } return list; } /** * Returns a configuration for an XPATH2 transformation which consists of * several XPATH expressions. * * @param xpathAndFilterList * list of XPATH expressions with their filters * @throws IllegalArgumentException * if <tt>xpathAndFilterList</tt> is <code>null</code> or empty, * or the specified filte values are neither "intersect", nor * "subtract", nor "union" * @return XPATH transformation */ public static AlgorithmMethod getXPath2Transform(List<XPathAndFilter> xpathAndFilterList) { return getXPath2Transform(xpathAndFilterList, null); } /** * Returns a configuration for an XSL transformation. * * @param path * path to the XSL file in the classpath * @return XSL transform * @throws IllegalArgumentException * if <tt>path</tt> is <code>null</code> * @throws IllegalStateException * if the XSL file cannot be found * @throws Exception * if an error during the reading of the XSL file occurs */ public static AlgorithmMethod getXslTransform(String path) throws Exception { //NOPMD InputStream is = readXslTransform(path); if (is == null) { throw new IllegalStateException(String.format("XSL file %s not found", path)); } try { return getXslTranform(is); } finally { IOHelper.close(is); } } /** * Returns a configuration for an XSL transformation. * * @param is * input stream of the XSL * @return XSL transform * @throws IllegalArgumentException * if <tt>is</tt> is <code>null</code> * @throws Exception * if an error during the reading of the XSL file occurs */ public static AlgorithmMethod getXslTranform(InputStream is) throws SAXException, IOException, ParserConfigurationException { if (is == null) { throw new IllegalArgumentException("is must not be null"); } Document doc = parseInput(is); DOMStructure stylesheet = new DOMStructure(doc.getDocumentElement()); XSLTTransformParameterSpec spec = new XSLTTransformParameterSpec(stylesheet); XmlSignatureTransform transformXslt = new XmlSignatureTransform(); transformXslt.setAlgorithm(Transform.XSLT); transformXslt.setParameterSpec(spec); return transformXslt; } protected static InputStream readXslTransform(String path) throws Exception { //NOPMD if (path == null) { throw new IllegalArgumentException("path is null"); } return XmlSignatureHelper.class.getResourceAsStream(path); } public static List<AlgorithmMethod> getTransforms(List<AlgorithmMethod> list) { return list; } private static Document parseInput(InputStream is) throws SAXException, IOException, ParserConfigurationException { return newDocumentBuilder(Boolean.TRUE).parse(is); } public static List<Node> getTextAndElementChildren(Node node) { List<Node> result = new LinkedList<Node>(); NodeList children = node.getChildNodes(); if (children == null) { return result; } for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (Node.ELEMENT_NODE == child.getNodeType() || Node.TEXT_NODE == child.getNodeType()) { result.add(child); } } return result; } public static DocumentBuilder newDocumentBuilder(Boolean disallowDoctypeDecl) throws ParserConfigurationException { return newDocumentBuilder(disallowDoctypeDecl, null); } public static DocumentBuilder newDocumentBuilder(Boolean disallowDoctypeDecl, Schema schema) throws ParserConfigurationException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); dbf.setValidating(false); // avoid external entity attacks dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); boolean isDissalowDoctypeDecl = disallowDoctypeDecl == null ? true : disallowDoctypeDecl; dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", isDissalowDoctypeDecl); // avoid overflow attacks dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); if (schema != null) { dbf.setSchema(schema); } return dbf.newDocumentBuilder(); } public static void transformToOutputStream(Node node, OutputStream os, boolean omitXmlDeclaration, String encoding) throws Exception { //NOPMD if (node.getNodeType() == Node.TEXT_NODE) { byte[] bytes = tranformTextNodeToByteArray(node, encoding); os.write(bytes); } else { transformNonTextNodeToOutputStream(node, os, omitXmlDeclaration, encoding); } } /** * Use {@link #transformToOutputStream(Node, OutputStream, boolean, String)} * instead. */ @Deprecated public static void transformToOutputStream(Node node, OutputStream os, boolean omitXmlDeclaration) throws Exception { //NOPMD if (node.getNodeType() == Node.TEXT_NODE) { byte[] bytes = tranformTextNodeToByteArray(node); os.write(bytes); } else { transformNonTextNodeToOutputStream(node, os, omitXmlDeclaration); } } /** * Use * {@link #transformNonTextNodeToOutputStream(Node, OutputStream, boolean, String)} * instead. */ @Deprecated public static void transformNonTextNodeToOutputStream(Node node, OutputStream os, boolean omitXmlDeclaration) throws Exception { //NOPMD transformNonTextNodeToOutputStream(node, os, omitXmlDeclaration, null); } /** * Serializes a node using a certain character encoding. * * @param node * DOM node to serialize * @param os * output stream, to which the node is serialized * @param omitXmlDeclaration * indicator whether to omit the XML declaration or not * @param encoding * character encoding, can be <code>null</code>, if * <code>null</code> then "UTF-8" is used * @throws Exception */ public static void transformNonTextNodeToOutputStream(Node node, OutputStream os, boolean omitXmlDeclaration, String encoding) throws Exception { //NOPMD // previously we used javax.xml.transform.Transformer, however the JDK xalan implementation did not work correctly with a specified encoding // therefore we switched to DOMImplementationLS if (encoding == null) { encoding = "UTF-8"; } DOMImplementationRegistry domImplementationRegistry = DOMImplementationRegistry.newInstance(); DOMImplementationLS domImplementationLS = (DOMImplementationLS) domImplementationRegistry.getDOMImplementation("LS"); LSOutput lsOutput = domImplementationLS.createLSOutput(); lsOutput.setEncoding(encoding); lsOutput.setByteStream(os); LSSerializer lss = domImplementationLS.createLSSerializer(); lss.getDomConfig().setParameter("xml-declaration", !omitXmlDeclaration); lss.write(node, lsOutput); } /** use {@link #tranformTextNodeToByteArray(Node, String)} instead. */ @Deprecated public static byte[] tranformTextNodeToByteArray(Node node) { return tranformTextNodeToByteArray(node, null); } /** * Trannsforms a text node to byte array using a certain character encoding. * * @param node * text node * @param encoding * character encoding, can be <code>null</code>, if * <code>null</code> then UTF-8 is used * @return byte array, <code>null</code> if the node has not text content * @throws IllegalStateException * if the encoding is not supported */ public static byte[] tranformTextNodeToByteArray(Node node, String encoding) { if (encoding == null) { encoding = "UTF-8"; } String text = node.getTextContent(); if (text != null) { try { return text.getBytes(encoding); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } else { return null; } } public static Document getDocument(Node node) { if (node.getNodeType() == Node.DOCUMENT_NODE) { return (Document) node; } return node.getOwnerDocument(); } public static class XPathAndFilter { private String xpath; private String filter; public XPathAndFilter(String xpath, String filter) { this.xpath = xpath; this.filter = filter; } public XPathAndFilter() { } public String getXpath() { return xpath; } public void setXpath(String xpath) { this.xpath = xpath; } public String getFilter() { return filter; } public void setFilter(String filter) { this.filter = filter; } } }