http://git-wip-us.apache.org/repos/asf/camel/blob/a8265a2c/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java new file mode 100644 index 0000000..120b81c --- /dev/null +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java @@ -0,0 +1,680 @@ +/** + * 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.processor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import javax.xml.crypto.AlgorithmMethod; +import javax.xml.crypto.KeySelector; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureException; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.component.xmlsecurity.api.KeyAccessor; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureConstants; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureException; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureFormatException; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureHelper; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureInvalidKeyException; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureNoKeyException; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureProperties; +import org.apache.camel.util.IOHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Creates from the message body a XML signature element which is returned in + * the message body of the output message. Enveloped and enveloping XML + * signatures are supported. + * <p> + * In the enveloped XML signature case, the method + * {@link XmlSignerConfiguration#getParentLocalName()} must not return + * <code>null</code>. In this case the parent element must be contained in the + * XML document provided by the message body and the signature element is added + * as last child element of the parent element. If a KeyInfo instance is + * provided by the {@link KeyAccessor} and + * {@link XmlSignerConfiguration#getAddKeyInfoReference()} is <code>true</code>, + * then also a reference to the KeyInfo element is added. The generated XML + * signature has the following structure: + * + * <pre> + * {@code + * <[parent element]> + * ... + * <Signature Id="[signature_id]"> + * <SignedInfo> + * <Reference URI=""> + * <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> + * (<Transform>)* + * <DigestMethod> + * <DigestValue> + * </Reference> + * (<Reference URI="#[keyinfo_Id]"> + * <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> + * <DigestMethod> + * <DigestValue> + * </Reference>)? + * <!-- further references possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) --> + * </SignedInfo> + * <SignatureValue> + * (<KeyInfo Id="[keyinfo_id]">)? + * <!-- Object elements possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) --> + * </Signature> + * </[parent element]> + * } + * </pre> + * <p> + * In the enveloping XML signature case, the generated XML signature has the + * following structure: + * + * <pre> + * {@code + * <Signature Id="[signature_id]"> + * <SignedInfo> + * <Reference URI="#[object_id]" type="[optional_type_value]"> + * (<Transform>)* + * <DigestMethod> + * <DigestValue> + * </Reference> + * (<Reference URI="#[keyinfo_id]"> + * <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> + * <DigestMethod> + * <DigestValue> + * </Reference>)? + * <!-- further references possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) --> + * </SignedInfo> + * <SignatureValue> + * (<KeyInfo Id="[keyinfo_id]">)? + * <Object Id="[object_id]"/> + * <!-- further Object elements possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) --> + * </Signature> + * } + * </pre> + * + * In the enveloping XML signature case, also message bodies containing plain + * text are supported. This must be indicated via the header + * {@link XmlSignatureConstants#HEADER_MESSAGE_IS_PLAIN_TEXT} or via the + * configuration {@link XmlSignerConfiguration#getPlainText()}. + * + * <p> + * In both cases, the digest algorithm is either read from the configuration + * method {@link XmlSignerConfiguration#getDigestAlgorithm()} or calculated from + * the signature algorithm ( + * {@link XmlSignerConfiguration#getSignatureAlgorithm()}. The optional + * transforms are read from {@link XmlSignerConfiguration#getTransformMethods()} + * . + * <p> + * In both cases, you can add additional references and objects which contain + * properties for the XML signature, see + * {@link XmlSignerConfiguration#setProperties(XmlSignatureProperties)}. + */ + +public class XmlSignerProcessor extends XmlSignatureProcessor { + + private static final Logger LOG = LoggerFactory.getLogger(XmlSignerProcessor.class); + + private static final String SHA512 = "sha512"; + + private static final String SHA384 = "sha384"; + + private static final String SHA256 = "sha256"; + + private static final String SHA1 = "sha1"; + + private static final String HTTP_WWW_W3_ORG_2001_04_XMLDSIG_MORE_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"; + + private final XmlSignerConfiguration config; + + public XmlSignerProcessor(XmlSignerConfiguration config) { + super(); + this.config = config; + } + + @Override + public XmlSignerConfiguration getConfiguration() { + return config; + } + + @Override + public void process(Exchange exchange) throws Exception { //NOPMD + + try { + LOG.debug("XML signature generation started using algorithm {} and canonicalization method {}", getConfiguration() + .getSignatureAlgorithm(), getConfiguration().getCanonicalizationMethod().getAlgorithm()); + + // lets setup the out message before we invoke the signing + // so that it can mutate it if necessary + Message out = exchange.getOut(); + out.copyFrom(exchange.getIn()); + + Document outputDoc = sign(out); + + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + XmlSignatureHelper.transformNonTextNodeToOutputStream(outputDoc, outStream, omitXmlDeclaration(out)); + byte[] data = outStream.toByteArray(); + out.setBody(data); + clearMessageHeaders(out); + LOG.debug("XML signature generation finished"); + } catch (Exception e) { + // remove OUT message, as an exception occurred + exchange.setOut(null); + throw e; + } + } + + protected Document sign(final Message out) throws Exception { //NOPMD + + try { + XMLSignatureFactory fac; + // Try to install the Santuario Provider - fall back to the JDK provider if this does + // not work + try { + fac = XMLSignatureFactory.getInstance("DOM", "ApacheXMLDSig"); + } catch (NoSuchProviderException ex) { + fac = XMLSignatureFactory.getInstance("DOM"); + } + + final Node node = getMessageBodyNode(out); + + Node parent = getParentOfSignature(out, node); + + final KeySelector keySelector = getConfiguration().getKeyAccessor().getKeySelector(out); + if (keySelector == null) { + throw new XmlSignatureNoKeyException( + "Key selector is missing for XML signature generation. Specify a key selector in the configuration."); + } + + // the method KeyAccessor.getKeyInfo must be called after the method KeyAccessor.getKeySelector, this is part of the interface contract! + final KeyInfo keyInfo = getConfiguration().getKeyAccessor().getKeyInfo(out, node, fac.getKeyInfoFactory()); + + final String signatureId = "_" + UUID.randomUUID().toString(); + LOG.debug("Signature Id {}", signatureId); + + XmlSignatureProperties.Input input = new InputBuilder().contentDigestAlgorithm(getDigestAlgorithmUri()).keyInfo(keyInfo) + .message(out).messageBodyNode(node).parent(parent).signatureAlgorithm(getConfiguration().getSignatureAlgorithm()) + .signatureFactory(fac).signatureId(signatureId).build(); + + XmlSignatureProperties.Output properties = getSignatureProperties(input); + + List<? extends XMLObject> objects = getObjects(input, properties); + List<? extends Reference> refs = getReferences(input, properties, getKeyInfoId(keyInfo)); + + SignedInfo si = createSignedInfo(fac, refs); + + if (parent == null) { + // for enveloping signature, create new document + parent = XmlSignatureHelper.newDocumentBuilder(Boolean.TRUE).newDocument(); + } + + DOMSignContext dsc = createAndConfigureSignContext(parent, keySelector); + + XMLSignature signature = fac.newXMLSignature(si, keyInfo, objects, signatureId, null); + // generate the signature + signature.sign(dsc); + + return XmlSignatureHelper.getDocument(parent); + + } catch (XMLSignatureException se) { + if (se.getCause() instanceof InvalidKeyException) { + throw new XmlSignatureInvalidKeyException(se.getMessage(), se); + } else { + throw new XmlSignatureException(se); + } + } catch (GeneralSecurityException e) { + // like NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException + throw new XmlSignatureException(e); + } + + } + + protected XmlSignatureProperties.Output getSignatureProperties(XmlSignatureProperties.Input input) throws Exception { //NOPMD + XmlSignatureProperties propGetter = getConfiguration().getProperties(); + XmlSignatureProperties.Output propsOutput = null; + if (propGetter != null) { + propsOutput = propGetter.get(input); + } + return propsOutput; + } + + private DOMSignContext createAndConfigureSignContext(Node parent, KeySelector keySelector) { + DOMSignContext dsc = new DOMSignContext(keySelector, parent); + // set namespace prefix for "http://www.w3.org/2000/09/xmldsig#" according to best practice described in http://www.w3.org/TR/xmldsig-bestpractices/#signing-xml-without-namespaces + if (getConfiguration().getPrefixForXmlSignatureNamespace() != null + && !getConfiguration().getPrefixForXmlSignatureNamespace().isEmpty()) { + dsc.putNamespacePrefix("http://www.w3.org/2000/09/xmldsig#", getConfiguration().getPrefixForXmlSignatureNamespace()); + } + setCryptoContextProperties(dsc); + setUriDereferencerAndBaseUri(dsc); + return dsc; + } + + protected Boolean omitXmlDeclaration(Message message) { + Boolean omitXmlDeclaration = message.getHeader(XmlSignatureConstants.HEADER_OMIT_XML_DECLARATION, Boolean.class); + if (omitXmlDeclaration == null) { + omitXmlDeclaration = getConfiguration().getOmitXmlDeclaration(); + } + if (omitXmlDeclaration == null) { + omitXmlDeclaration = Boolean.FALSE; + } + LOG.debug("Omit XML declaration: {}", omitXmlDeclaration); + return omitXmlDeclaration; + } + + protected SignedInfo createSignedInfo(XMLSignatureFactory fac, List<? extends Reference> refs) throws Exception { //NOPMD + return fac.newSignedInfo(fac.newCanonicalizationMethod(getConfiguration().getCanonicalizationMethod().getAlgorithm(), + (C14NMethodParameterSpec) getConfiguration().getCanonicalizationMethod().getParameterSpec()), + getSignatureMethod(getConfiguration().getSignatureAlgorithm(), fac), refs); + } + + private SignatureMethod getSignatureMethod(String signatureAlgorithm, XMLSignatureFactory fac) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + return fac.newSignatureMethod(signatureAlgorithm, null); + } + + protected Node getMessageBodyNode(Message message) throws Exception { //NOPMD + InputStream is = message.getMandatoryBody(InputStream.class); + + Boolean isPlainText = isPlainText(message); + + Node node; + if (isPlainText != null && isPlainText) { + node = getTextNode(message, is); + } else { + Document doc = parseInput(is, getConfiguration().getDisallowDoctypeDecl()); + node = doc.getDocumentElement(); + LOG.debug("Root element of document to be signed: {}", node); + } + return node; + } + + protected Boolean isPlainText(Message message) { + Boolean isPlainText = message.getHeader(XmlSignatureConstants.HEADER_MESSAGE_IS_PLAIN_TEXT, Boolean.class); + if (isPlainText == null) { + isPlainText = getConfiguration().getPlainText(); + } + LOG.debug("Is plain text: {}", isPlainText); + return isPlainText; + } + + protected Element getParentOfSignature(Message inMessage, Node messageBodyNode) throws Exception { //NOPMD + if (getConfiguration().getParentLocalName() == null) { + return null; + } + if (messageBodyNode.getParentNode() == null || messageBodyNode.getParentNode().getNodeType() != Node.DOCUMENT_NODE) { + throw new XmlSignatureFormatException( + "Incomming message has wrong format: It is not an XML document. Cannot create an enveloped XML signature."); + } + + Document doc = (Document) messageBodyNode.getParentNode(); + NodeList parents = doc.getElementsByTagNameNS(getConfiguration().getParentNamespace(), getConfiguration().getParentLocalName()); + + if (parents == null || parents.getLength() == 0) { + throw new XmlSignatureFormatException( + String.format( + "Incoming message has wrong format: The parent element with the local name %s and the namespace %s was not found in the message to build an enveloped XML signature.", + getConfiguration().getParentLocalName(), getConfiguration().getParentNamespace())); + } + // return the first element + return (Element) parents.item(0); + + } + + protected List<? extends Reference> getReferences(XmlSignatureProperties.Input input, XmlSignatureProperties.Output properties, + String keyInfoId) throws Exception { //NOPMD + + // Create Reference with URI="#<objectId>" for enveloping signature or URI="" for enveloped signature and the transforms + Reference ref = createReference(input.getSignatureFactory(), getContentReferenceUri(input.getMessage()), + getContentReferenceType(input.getMessage())); + Reference keyInfoRef = createKeyInfoReference(input.getSignatureFactory(), keyInfoId, input.getContentDigestAlgorithm()); + + int propsRefsSize = properties == null || properties.getReferences() == null || properties.getReferences().isEmpty() ? 0 + : properties.getReferences().size(); + int size = keyInfoRef == null ? propsRefsSize + 1 : propsRefsSize + 2; + List<Reference> referenceList = new ArrayList<Reference>(size); + referenceList.add(ref); + if (keyInfoRef != null) { + referenceList.add(keyInfoRef); + } + if (properties != null && properties.getReferences() != null && !properties.getReferences().isEmpty()) { + referenceList.addAll(properties.getReferences()); + } + return referenceList; + } + + protected List<? extends XMLObject> getObjects(XmlSignatureProperties.Input input, XmlSignatureProperties.Output properties) + throws Exception { //NOPMD + if (isEnveloped()) { + if (properties == null || properties.getObjects() == null) { + return Collections.emptyList(); + } + return properties.getObjects(); + } + + final String objectId = getConfiguration().getContentObjectId(); + LOG.debug("Object Content Id {}", objectId); + + XMLObject obj = createXMLObject(input.getSignatureFactory(), input.getMessageBodyNode(), objectId); + if (properties == null || properties.getObjects() == null || properties.getObjects().isEmpty()) { + return Collections.singletonList(obj); + } + List<XMLObject> result = new ArrayList<XMLObject>(properties.getObjects().size() + 1); + result.add(obj); + result.addAll(properties.getObjects()); + return result; + } + + private Node getTextNode(Message inMessage, InputStream is) throws IOException, ParserConfigurationException, XmlSignatureException { + LOG.debug("Message body to be signed is plain text"); + String encoding = getMessageEncoding(inMessage); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOHelper.copyAndCloseInput(is, bos); + try { + String text = new String(bos.toByteArray(), encoding); + return XmlSignatureHelper.newDocumentBuilder(true).newDocument().createTextNode(text); + } catch (UnsupportedEncodingException e) { + throw new XmlSignatureException(String.format("The message encoding %s is not supported.", encoding), e); + } + } + + protected String getMessageEncoding(Message inMessage) { + String encoding = inMessage.getHeader(XmlSignatureConstants.HEADER_PLAIN_TEXT_ENCODING, String.class); + if (encoding == null) { + encoding = getConfiguration().getPlainTextEncoding(); + } + LOG.debug("Messge encoding: {}", encoding); + return encoding; + } + + protected Document parseInput(InputStream is, Boolean disallowDoctypeDecl) throws XmlSignatureFormatException, + ParserConfigurationException, IOException { + try { + return XmlSignatureHelper.newDocumentBuilder(disallowDoctypeDecl).parse(is); + } catch (SAXException e) { + throw new XmlSignatureFormatException( + "XML signature generation not possible. Sent message is not an XML document. Check the sent message.", e); + } finally { + IOHelper.close(is, "input stream"); + } + } + + protected Reference createReference(XMLSignatureFactory fac, String uri, String type) throws InvalidAlgorithmParameterException, + XmlSignatureException { + try { + List<Transform> transforms = getTransforms(fac); + Reference ref = fac.newReference(uri, fac.newDigestMethod(getDigestAlgorithmUri(), null), transforms, type, null); + return ref; + } catch (NoSuchAlgorithmException e) { + throw new XmlSignatureException("Wrong algorithm specified in the configuration.", e); + } + } + + protected String getContentReferenceType(Message message) { + String type = message.getHeader(XmlSignatureConstants.HEADER_CONTENT_REFERENCE_TYPE, String.class); + if (type == null) { + type = getConfiguration().getContentReferenceType(); + } + LOG.debug("Content reference type: {}", type); + return type; + } + + protected String getContentReferenceUri(Message message) { + String uri = message.getHeader(XmlSignatureConstants.HEADER_CONTENT_REFERENCE_URI, String.class); + if (uri == null) { + uri = getConfiguration().getContentReferenceUri(); + } + if (uri == null) { + uri = isEnveloped() ? "" : "#" + getConfiguration().getContentObjectId(); + } + LOG.debug("Content reference uri: {}", uri); + return uri; + } + + protected XMLObject createXMLObject(XMLSignatureFactory fac, Node node, String id) { + return fac.newXMLObject(Collections.singletonList(new DOMStructure(node)), id, null, null); + } + + private List<Transform> getTransforms(XMLSignatureFactory fac) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + boolean isEnveloped = isEnveloped(); + List<AlgorithmMethod> configuredTrafos = getConfiguration().getTransformMethods(); + if (isEnveloped) { + // add enveloped transform if necessary + if (configuredTrafos.size() > 0) { + if (!containsEnvelopedTransform(configuredTrafos)) { + configuredTrafos = new ArrayList<AlgorithmMethod>(configuredTrafos.size() + 1); + configuredTrafos.add(XmlSignatureHelper.getEnvelopedTransform()); + configuredTrafos.addAll(getConfiguration().getTransformMethods()); + } + } else { + // add enveloped and C14N trafo + configuredTrafos = new ArrayList<AlgorithmMethod>(2); + configuredTrafos.add(XmlSignatureHelper.getEnvelopedTransform()); + configuredTrafos.add(XmlSignatureHelper.getCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE)); + } + } + + List<Transform> transforms = new ArrayList<Transform>(configuredTrafos.size()); + for (AlgorithmMethod trafo : configuredTrafos) { + Transform transform = fac.newTransform(trafo.getAlgorithm(), (TransformParameterSpec) trafo.getParameterSpec()); + transforms.add(transform); + LOG.debug("Transform method: {}", trafo.getAlgorithm()); + } + return transforms; + } + + protected boolean isEnveloped() { + return getConfiguration().getParentLocalName() != null; + } + + private boolean containsEnvelopedTransform(List<AlgorithmMethod> configuredTrafos) { + for (AlgorithmMethod m : configuredTrafos) { + if (Transform.ENVELOPED.equals(m.getAlgorithm())) { + return true; + } + } + return false; + } + + protected String getDigestAlgorithmUri() throws XmlSignatureException { + + String result = getConfiguration().getDigestAlgorithm(); + if (result == null) { + String signatureAlgorithm = getConfiguration().getSignatureAlgorithm(); + if (signatureAlgorithm != null) { + if (signatureAlgorithm.contains(SHA1)) { + result = DigestMethod.SHA1; + } else if (signatureAlgorithm.contains(SHA256)) { + result = DigestMethod.SHA256; + } else if (signatureAlgorithm.contains(SHA384)) { + result = HTTP_WWW_W3_ORG_2001_04_XMLDSIG_MORE_SHA384; + } else if (signatureAlgorithm.contains(SHA512)) { + result = DigestMethod.SHA512; + } + } + } + if (result != null) { + LOG.debug("Digest algorithm: {}", result); + return result; + } + throw new XmlSignatureException( + "Digest algorithm missing for XML signature generation. Specify the digest algorithm in the configuration."); + } + + protected Reference createKeyInfoReference(XMLSignatureFactory fac, String keyInfoId, String digestAlgorithm) throws Exception { //NOPMD + + if (keyInfoId == null) { + return null; + } + if (getConfiguration().getAddKeyInfoReference() == null) { + return null; + } + + if (!getConfiguration().getAddKeyInfoReference()) { + return null; + } + + LOG.debug("Creating reference to key info element with Id: {}", keyInfoId); + List<Transform> transforms = new ArrayList<Transform>(1); + Transform transform = fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null); + transforms.add(transform); + return fac.newReference("#" + keyInfoId, fac.newDigestMethod(digestAlgorithm, null), transforms, null, null); + } + + private String getKeyInfoId(KeyInfo keyInfo) throws Exception { //NOPMD + if (keyInfo == null) { + return null; + } + return keyInfo.getId(); + } + + private static class InputBuilder { + + private XMLSignatureFactory signatureFactory; + + private String signatureAlgorithm; + + private Node parent; + + private Node messageBodyNode; + + private Message message; + + private KeyInfo keyInfo; + + private String contentDigestAlgorithm; + + private String signatureId; + + public InputBuilder signatureFactory(XMLSignatureFactory signatureFactory) { + this.signatureFactory = signatureFactory; + return this; + } + + public InputBuilder signatureAlgorithm(String signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + public InputBuilder parent(Node parent) { + this.parent = parent; + return this; + } + + public InputBuilder messageBodyNode(Node messageBodyNode) { + this.messageBodyNode = messageBodyNode; + return this; + } + + public InputBuilder message(Message message) { + this.message = message; + return this; + } + + public InputBuilder keyInfo(KeyInfo keyInfo) { + this.keyInfo = keyInfo; + return this; + } + + public InputBuilder contentDigestAlgorithm(String contentDigestAlgorithm) { + this.contentDigestAlgorithm = contentDigestAlgorithm; + return this; + } + + public InputBuilder signatureId(String signatureId) { + this.signatureId = signatureId; + return this; + } + + public XmlSignatureProperties.Input build() { + return new XmlSignatureProperties.Input() { + + @Override + public XMLSignatureFactory getSignatureFactory() { + return signatureFactory; + } + + @Override + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + @Override + public Node getParent() { + return parent; + } + + @Override + public Node getMessageBodyNode() { + return messageBodyNode; + } + + @Override + public Message getMessage() { + return message; + } + + @Override + public KeyInfo getKeyInfo() { + return keyInfo; + } + + @Override + public String getContentDigestAlgorithm() { + return contentDigestAlgorithm; + } + + @Override + public String getSignatureId() { + return signatureId; + } + + }; + } + + } +}
http://git-wip-us.apache.org/repos/asf/camel/blob/a8265a2c/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierConfiguration.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierConfiguration.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierConfiguration.java new file mode 100644 index 0000000..869abad --- /dev/null +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierConfiguration.java @@ -0,0 +1,232 @@ +/** + * 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.processor; + +import javax.xml.crypto.KeySelector; + +import org.apache.camel.CamelContext; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.component.xmlsecurity.api.DefaultXmlSignature2Message; +import org.apache.camel.component.xmlsecurity.api.DefaultValidationFailedHandler; +import org.apache.camel.component.xmlsecurity.api.XmlSignature2Message; +import org.apache.camel.component.xmlsecurity.api.ValidationFailedHandler; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureChecker; + +public class XmlVerifierConfiguration extends XmlSignatureConfiguration { + + private KeySelector keySelector; + + private String keySelectorName; + + private XmlSignatureChecker xmlSignatureChecker; + + private String xmlSignatureCheckerName; + + private XmlSignature2Message xmlSignature2Message = new DefaultXmlSignature2Message(); + + private String xmlSignature2MessageName; + + private ValidationFailedHandler validationFailedHandler = new DefaultValidationFailedHandler(); + + private String validationFailedHandlerName; + + private Object outputNodeSearch; + + private String outputNodeSearchType = DefaultXmlSignature2Message.OUTPUT_NODE_SEARCH_TYPE_DEFAULT; + + private Boolean removeSignatureElements = Boolean.FALSE; + + private Boolean secureValidation = Boolean.TRUE; + + public XmlVerifierConfiguration() { + super(); + } + + public XmlVerifierConfiguration copy() { + try { + return (XmlVerifierConfiguration) clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeCamelException(e); + } + } + + public void setCamelContext(CamelContext camelContext) { + super.setCamelContext(camelContext); + setKeySelector(keySelectorName); + setXmlSignatureChecker(xmlSignatureCheckerName); + setXmlSignature2Message(xmlSignature2MessageName); + setValidationFailedHandler(validationFailedHandlerName); + } + + public void setKeySelector(KeySelector keySelector) { + this.keySelector = keySelector; + } + + public KeySelector getKeySelector() { + return keySelector; + } + + /** + * Sets the reference name for a KeySelector that can be found in the + * registry. + */ + public void setKeySelector(String keySelectorName) { + if (getCamelContext() != null && keySelectorName != null) { + KeySelector selector = getCamelContext().getRegistry() + .lookupByNameAndType(keySelectorName, KeySelector.class); + if (selector != null) { + setKeySelector(selector); + } + } + if (keySelectorName != null) { + this.keySelectorName = keySelectorName; + } + } + + public XmlSignatureChecker getXmlSignatureChecker() { + return xmlSignatureChecker; + } + + public void setXmlSignatureChecker(XmlSignatureChecker xmlSignatureChecker) { + this.xmlSignatureChecker = xmlSignatureChecker; + } + + /** + * Sets the reference name for a application checker that can be found in + * the registry. + */ + public void setXmlSignatureChecker(String xmlSignatureCheckerName) { + if (getCamelContext() != null && xmlSignatureCheckerName != null) { + XmlSignatureChecker checker = getCamelContext().getRegistry() + .lookupByNameAndType(xmlSignatureCheckerName, + XmlSignatureChecker.class); + if (checker != null) { + setXmlSignatureChecker(checker); + } + } + if (xmlSignatureCheckerName != null) { + this.xmlSignatureCheckerName = xmlSignatureCheckerName; + } + } + + public XmlSignature2Message getXmlSignature2Message() { + return xmlSignature2Message; + } + + public void setXmlSignature2Message(XmlSignature2Message xmlSignature2Message) { + this.xmlSignature2Message = xmlSignature2Message; + } + + /** + * Sets the reference name for the to-message instance that can be found in + * the registry. + */ + public void setXmlSignature2Message(String xmlSignature2Message) { + if (getCamelContext() != null && xmlSignature2Message != null) { + XmlSignature2Message maper = getCamelContext().getRegistry() + .lookupByNameAndType(xmlSignature2Message, + XmlSignature2Message.class); + if (maper != null) { + setXmlSignature2Message(maper); + } + } + if (xmlSignature2Message != null) { + this.xmlSignature2MessageName = xmlSignature2Message; + } + } + + public ValidationFailedHandler getValidationFailedHandler() { + return validationFailedHandler; + } + + public void setValidationFailedHandler(ValidationFailedHandler validationFailedHandler) { + this.validationFailedHandler = validationFailedHandler; + } + + public void setValidationFailedHandler(String validationFailedHandlerName) { + if (getCamelContext() != null && validationFailedHandlerName != null) { + ValidationFailedHandler vailFailedHandler = getCamelContext() + .getRegistry().lookupByNameAndType(validationFailedHandlerName, + ValidationFailedHandler.class); + if (vailFailedHandler != null) { + setValidationFailedHandler(vailFailedHandler); + } + } + if (validationFailedHandlerName != null) { + this.validationFailedHandlerName = validationFailedHandlerName; + } + } + + public Object getOutputNodeSearch() { + return outputNodeSearch; + } + + /** + * Sets the output node search value for determining the node from the XML + * signature document which shall be set to the output message body. The + * class of the value depends on the type of the output node search. The + * output node search is forwarded to {@link XmlSignature2Message}. + * + */ + public void setOutputNodeSearch(Object outputNodeSearch) { + this.outputNodeSearch = outputNodeSearch; + } + + public String getOutputNodeSearchType() { + return outputNodeSearchType; + } + + /** + * Determines the search type for determining the output node which is + * serialized into the output message bodyF. See + * {@link #setOutputNodeSearch(String)}. The supported default search types + * you can find in {@link DefaultXmlSignature2Message}. + * + * @param outputNodeSearchType + */ + public void setOutputNodeSearchType(String outputNodeSearchType) { + this.outputNodeSearchType = outputNodeSearchType; + } + + public Boolean getRemoveSignatureElements() { + return removeSignatureElements; + } + + /** + * Indicator whether the XML signature elements (elements with local name + * "Signature" and namesapce ""http://www.w3.org/2000/09/xmldsig#"") shall + * be removed from the document set to the output message. Normally, this is + * only necessary, if the XML signature is enveloped. The default value is + * {@link Boolean#FALSE}. This parameter is forwarded to + * {@link XmlSignature2Message}. + * <p> + * This indicator has no effect if the output node search is of type + * {@link DefaultXmlSignature2Message#OUTPUT_NODE_SEARCH_TYPE_DEFAULT}.F + */ + public void setRemoveSignatureElements(Boolean removeSignatureElements) { + this.removeSignatureElements = removeSignatureElements; + } + + public Boolean getSecureValidation() { + return secureValidation; + } + + public void setSecureValidation(Boolean secureValidation) { + this.secureValidation = secureValidation; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/a8265a2c/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierProcessor.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierProcessor.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierProcessor.java new file mode 100644 index 0000000..1fd40ee --- /dev/null +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlVerifierProcessor.java @@ -0,0 +1,324 @@ +/** + * 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.processor; + +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchProviderException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.crypto.KeySelector; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dsig.Manifest; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignature.SignatureValue; +import javax.xml.crypto.dsig.XMLSignatureException; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.component.xmlsecurity.api.ValidationFailedHandler; +import org.apache.camel.component.xmlsecurity.api.XmlSignature2Message; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureChecker; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureFormatException; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureHelper; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureInvalidException; +import org.apache.camel.util.IOHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * XML signature verifier. Assumes that the input XML contains exactly one + * Signature element. + */ +public class XmlVerifierProcessor extends XmlSignatureProcessor { + + private static final Logger LOG = LoggerFactory + .getLogger(XmlVerifierProcessor.class); + + private final XmlVerifierConfiguration config; + + public XmlVerifierProcessor(XmlVerifierConfiguration config) { + super(); + this.config = config; + } + + @Override + public XmlVerifierConfiguration getConfiguration() { + return config; + } + + @Override + public void process(Exchange exchange) throws Exception { + + InputStream stream = exchange.getIn().getMandatoryBody(InputStream.class); + try { + // lets setup the out message before we invoke the signing + // so that it can mutate it if necessary + Message out = exchange.getOut(); + out.copyFrom(exchange.getIn()); + verify(stream, out); + clearMessageHeaders(out); + } catch (Exception e) { + // remove OUT message, as an exception occurred + exchange.setOut(null); + throw e; + } finally { + IOHelper.close(stream, "input stream"); + } + } + + protected void verify(InputStream input, final Message out) + throws Exception { + + LOG.debug("Verification of XML signature document started"); + final Document doc = parseInput(input); + + Node signatureNode = getSignatureNode(doc); + + XMLSignatureFactory fac; + // Try to install the Santuario Provider - fall back to the JDK provider if this does + // not work + try { + fac = XMLSignatureFactory.getInstance("DOM", "ApacheXMLDSig"); + } catch (NoSuchProviderException ex) { + fac = XMLSignatureFactory.getInstance("DOM"); + } + + KeySelector selector = getConfiguration().getKeySelector(); + if (selector == null) { + throw new IllegalStateException("Wrong configuration. Key selector is missing."); + } + + DOMValidateContext valContext = new DOMValidateContext(selector, signatureNode); + valContext.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE); + valContext.setProperty("org.jcp.xml.dsig.validateManifests", Boolean.TRUE); + + if (getConfiguration().getSecureValidation() == Boolean.TRUE) { + valContext.setProperty("org.apache.jcp.xml.dsig.secureValidation", Boolean.TRUE); + valContext.setProperty("org.jcp.xml.dsig.secureValidation", Boolean.TRUE); + } + setUriDereferencerAndBaseUri(valContext); + + setCryptoContextProperties(valContext); + + final XMLSignature signature = fac.unmarshalXMLSignature(valContext); + + executeApplicationCheck(out, doc, signature); + + boolean coreValidity; + try { + coreValidity = signature.validate(valContext); + } catch (XMLSignatureException se) { + throw getConfiguration().getValidationFailedHandler().onXMLSignatureException(se); + } + // Check core validation status + boolean goon = coreValidity; + if (coreValidity == false) { + goon = handleSignatureValidationFailed(valContext, signature); + } + if (goon) { + LOG.debug("XML signature verified"); + map2Message(signature, out, doc); + } else { + throw new XmlSignatureInvalidException(""); + } + } + + private void executeApplicationCheck(final Message out, final Document doc, + final XMLSignature signature) throws Exception { + if (getConfiguration().getXmlSignatureChecker() != null) { + XmlSignatureChecker.Input checkerInput = new XmlSignatureChecker.Input() { + + @Override + public SignedInfo getSignedInfo() { + return signature.getSignedInfo(); + } + + @Override + public SignatureValue getSignatureValue() { + return signature.getSignatureValue(); + } + + @SuppressWarnings("unchecked") + @Override + public List<? extends XMLObject> getObjects() { + return (List<? extends XMLObject>) signature.getObjects(); + } + + @Override + public Document getMessageBodyDocument() { + return doc; + } + + @Override + public Message getMessage() { + return out; + } + + @Override + public KeyInfo getKeyInfo() { + return signature.getKeyInfo(); + } + }; + getConfiguration().getXmlSignatureChecker().checkBeforeCoreValidation(checkerInput); + } + } + + private void map2Message(XMLSignature signature, Message out, + final Document messageBodyDocument) throws Exception { + @SuppressWarnings("unchecked") + final List<Reference> refs = new ArrayList<Reference>(signature.getSignedInfo().getReferences()); + @SuppressWarnings("unchecked") + final List<XMLObject> objs = new ArrayList<XMLObject>(signature.getObjects()); + XmlSignature2Message.Input refsAndObjects = new XmlSignature2Message.Input() { + + @Override + public List<Reference> getReferences() { + return refs; + } + + @Override + public List<XMLObject> getObjects() { + return objs; + } + + @Override + public Document getMessageBodyDocument() { + return messageBodyDocument; + } + + @Override + public Boolean omitXmlDeclaration() { + return getConfiguration().getOmitXmlDeclaration(); + } + + @Override + public Object getOutputNodeSearch() { + return getConfiguration().getOutputNodeSearch(); + } + + @Override + public String getOutputNodeSearchType() { + return getConfiguration().getOutputNodeSearchType(); + } + + public Boolean getRemoveSignatureElements() { + return getConfiguration().getRemoveSignatureElements(); + } + + }; + getConfiguration().getXmlSignature2Message().mapToMessage(refsAndObjects, out); + } + + private Node getSignatureNode(Document doc) throws IOException, + ParserConfigurationException, XmlSignatureFormatException { + + // Find Signature element + NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + if (nl.getLength() == 0) { + throw new XmlSignatureFormatException( + "Message is not a correct XML signature document: 'Signature' element is missing. Check the sent message."); + } + + if (nl.getLength() != 1) { + throw new XmlSignatureFormatException( + "XML signature document is not supported; it contains more than one signature element. Check the sent message."); + } + Node signatureNode = nl.item(0); + LOG.debug("Signature element found"); + return signatureNode; + } + + @SuppressWarnings("unchecked") + protected boolean handleSignatureValidationFailed(DOMValidateContext valContext, + XMLSignature signature) throws Exception { + ValidationFailedHandler handler = getConfiguration().getValidationFailedHandler(); + LOG.debug("handleSignatureValidationFailed called"); + try { + handler.start(); + + // first check signature value, see + // https://www.isecpartners.com/media/12012/XMLDSIG_Command_Injection.pdf + SignatureValue sigValue = signature.getSignatureValue(); + boolean sv = sigValue.validate(valContext); + if (!sv) { + handler.signatureValueValidationFailed(sigValue); + } + + // then the references! + // check the validation status of each Reference + for (Reference ref : (List<Reference>) signature.getSignedInfo().getReferences()) { + boolean refValid = ref.validate(valContext); + if (!refValid) { + handler.referenceValidationFailed(ref); + } + } + + // validate Manifests, if property set + if (Boolean.TRUE.equals(valContext.getProperty("org.jcp.xml.dsig.validateManifests"))) { + for (XMLObject xo : (List<XMLObject>) signature.getObjects()) { + List<XMLStructure> content = xo.getContent(); + for (XMLStructure xs : content) { + if (xs instanceof Manifest) { + Manifest man = (Manifest) xs; + for (Reference ref : (List<Reference>) man + .getReferences()) { + boolean refValid = ref.validate(valContext); + if (!refValid) { + handler.manifestReferenceValidationFailed(ref); + } + } + } + } + } + } + boolean goon = handler.ignoreCoreValidationFailure(); + LOG.debug("Ignore Core Validation failure: {}", goon); + return goon; + } finally { + handler.end(); + } + + } + + protected Document parseInput(InputStream is) + throws XmlSignatureFormatException, ParserConfigurationException, + IOException { + try { + Document doc = + XmlSignatureHelper.newDocumentBuilder(getConfiguration().getDisallowDoctypeDecl()).parse(is); + return doc; + } catch (SAXException e) { + throw new XmlSignatureFormatException( + "Message has wrong format, it is not a XML signature document. Check the sent message.", + e + ); + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/a8265a2c/components/camel-xmlsecurity/src/main/resources/META-INF/services/org/apache/camel/component/xmlsecurity ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/resources/META-INF/services/org/apache/camel/component/xmlsecurity b/components/camel-xmlsecurity/src/main/resources/META-INF/services/org/apache/camel/component/xmlsecurity new file mode 100644 index 0000000..44f51fc --- /dev/null +++ b/components/camel-xmlsecurity/src/main/resources/META-INF/services/org/apache/camel/component/xmlsecurity @@ -0,0 +1,18 @@ +# +# 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. +# + +class=org.apache.camel.component.xmlsecurity.XmlSignatureComponent \ No newline at end of file http://git-wip-us.apache.org/repos/asf/camel/blob/a8265a2c/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java b/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java new file mode 100644 index 0000000..aef1fba --- /dev/null +++ b/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java @@ -0,0 +1,64 @@ +/** + * 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; + +import java.security.KeyPair; + +import javax.xml.crypto.KeySelector; + +import org.apache.camel.CamelContext; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.xmlsecurity.api.KeyAccessor; +import org.apache.camel.impl.JndiRegistry; +import org.apache.camel.spring.SpringCamelContext; + +public class SpringXmlSignatureTest extends XmlSignatureTest { + + private static KeyPair rsaPair; + + protected CamelContext createCamelContext() throws Exception { + rsaPair = getKeyPair("RSA", 1024); + return SpringCamelContext.springCamelContext("/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTests.xml"); + } + + public static KeyAccessor getDsaKeyAccessor() { + return getKeyAccessor(getKeyPair("DSA", 1024).getPrivate()); + } + + public static KeyAccessor getRsaKeyAccessor() { + return getKeyAccessor(rsaPair.getPrivate()); + } + + public static KeySelector getDsaKeySelector() { + return KeySelector.singletonKeySelector(getKeyPair("DSA", 1024).getPublic()); + } + + public static KeySelector getRsaKeySelector() { + return KeySelector.singletonKeySelector(rsaPair.getPublic()); + } + + @Override + protected JndiRegistry createRegistry() throws Exception { + return super.createRegistry(); + } + + @Override + protected RouteBuilder[] createRouteBuilders() throws Exception { + return new RouteBuilder[] {}; + } + +} \ No newline at end of file