package helpers; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.StringReader; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.xml.crypto.MarshalException; import javax.xml.crypto.XMLStructure; import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.DigestMethod; import javax.xml.crypto.dsig.Reference; import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.XMLSignatureException; import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.dom.DOMValidateContext; import javax.xml.crypto.dsig.keyinfo.KeyInfo; import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; import javax.xml.crypto.dsig.keyinfo.X509Data; import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.apache.xml.security.Init; import org.apache.xml.security.signature.XMLSignature; import org.apache.xml.security.transforms.Transforms; import org.apache.xml.serialize.OutputFormat; import org.apache.xml.serialize.XMLSerializer; public class XMLHelpers { /** * Returns a namespace aware document builder factory. * * @return DocumentBuilderFactory NamespaceAware */ public DocumentBuilderFactory getDBF() { try { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); documentBuilderFactory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING , true); documentBuilderFactory.setNamespaceAware(true); return documentBuilderFactory; } catch (ParserConfigurationException e) { e.printStackTrace(); } return null; } /** * Returns a string serialization of a string * * @param document * document which should be converted to a string * @throws IOException * If an error in serialization occurred * @return string of document */ public String getString(Document document) throws IOException { return getString(document, false, 0); } public String getString(Document document, boolean indenting, int indent) throws IOException{ OutputFormat format = new OutputFormat(document); format.setLineWidth(200); format.setIndenting(indenting); format.setIndent(indent); format.setPreserveEmptyAttributes(true); format.setEncoding("UTF-8"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); XMLSerializer serializer = new XMLSerializer(baos, format); serializer.asDOMSerializer(); serializer.serialize(document); return baos.toString("UTF-8"); } /** * Returns a string serialization of a string, use indent and linebreaks = * true to pretty print a document * * @param document * document which should be converted to a string * @param indent * amount of indent * @param linebreaks * if line breaks should be inserted * @return string of document, pretty or linearized * @throws IOException if an Serializer error occures */ public String getStringOfDocument(Document document, int indent, boolean linebreaks) throws IOException{ document.normalize(); removeEmptyTags(document); return getString(document, linebreaks, indent); } /** * Converts a string representation of a XML document in a document Object * * @param message * String representation of a XML document * @throws SAXException * If any parse errors occur. * @return Document of XML string */ public Document getXMLDocumentOfSAMLMessage(String message) throws SAXException { try { DocumentBuilderFactory documentBuilderFactory = getDBF(); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); Document document = documentBuilder.parse(new InputSource(new StringReader(message))); return document; } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * Returns all Signatures of the given Document * * @param document * document with signatures * @return NodeList with signatures */ public NodeList getSignatures(Document document) { NodeList nl = document.getElementsByTagNameNS("*", "Signature"); return nl; } /** * Removes empty tags, spaces between XML tags * * @param document * document in which the empty tags should be removed */ public void removeEmptyTags(Document document) { NodeList nl = null; try { if(Thread.currentThread().getContextClassLoader() == null){ Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } XPath xPath = XPathFactory.newInstance().newXPath(); nl = (NodeList) xPath.evaluate("//text()[normalize-space(.)='']", document, XPathConstants.NODESET); for (int i = 0; i < nl.getLength(); ++i) { Node node = nl.item(i); node.getParentNode().removeChild(node); } } catch (XPathExpressionException e) { e.printStackTrace(); } } /** * Removes all signatures in a given XML document * * @param document * document in which the signature should be removed * @return number of removed signatures */ public int removeAllSignatures(Document document) { NodeList nl = getSignatures(document); int nrSig = nl.getLength(); for (int i = 0; i < nrSig; i++) { Node parent = nl.item(0).getParentNode(); parent.removeChild(nl.item(0)); } removeEmptyTags(document); document.normalize(); return nrSig; } /** * Removes a signature in a given XML document * * @param document * document in which the signature should be removed * @return number of removed signatures */ public int removeOnlyMessageSignature(Document document) { try { if(Thread.currentThread().getContextClassLoader() == null){ Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } setIDAttribute(document); XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expr = xpath.compile("//*[local-name()='Response']/*[local-name()='Signature']"); NodeList nl = (NodeList) expr.evaluate(document, XPathConstants.NODESET); int nrSig = nl.getLength(); for (int i = 0; i < nrSig; i++) { Node parent = nl.item(0).getParentNode(); parent.removeChild(nl.item(0)); } removeEmptyTags(document); document.normalize(); return nrSig; } catch (XPathExpressionException e) { e.printStackTrace(); } return 0; } /** * Returns a NodeList with assertions of the given XML document * * @param document * document with the assertions * @return NodeList with assertions */ public NodeList getAssertions(Document document) { return document.getElementsByTagNameNS("*", "Assertion"); } /** * Returns a NodeList with encrypted assertions of the given XML document * * @param document * document with the encrypted assertions * @return NodeList with encrypted assertions */ public NodeList getEncryptedAssertions(Document document) { return document.getElementsByTagNameNS("*", "EncryptedAssertion"); } /** * Returns SOAP Body as an Element * * @param document * document with SOAP body * @return Element SOAP Body Element or null if no body found */ public Element getSOAPBody(Document document) { try { if(Thread.currentThread().getContextClassLoader() == null){ Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expr = xpath.compile("//*[local-name()='Envelope']/*[local-name()='Body']"); NodeList elements = (NodeList) expr.evaluate(document, XPathConstants.NODESET); if(elements.getLength()>0){ return (Element) elements.item(0); } } catch (XPathExpressionException e) { e.printStackTrace(); } return null; } /** * Returns SAML Response out of SOAP Body as an Element * * @param document * document with SOAP envelope * @return Document SAML Response */ public Document getSAMLResponseOfSOAP(Document document) throws ParserConfigurationException{ Element body = getSOAPBody(document); DocumentBuilderFactory documentBuilderFactory = getDBF(); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); Document documentSAML = documentBuilder.newDocument(); Element SAMLresponseOld = (Element) body.getFirstChild(); Element SAMLresponse = (Element) documentSAML.adoptNode(SAMLresponseOld); documentSAML.appendChild(SAMLresponse); return documentSAML; } /** * Returns a NodeList response Element in it * * @param document * document with the response * @return NodeList with response element */ public NodeList getResponse(Document document) { return document.getElementsByTagNameNS("*", "Response"); } /** * Returns the attribute value of a XML tag * * @param element * DOM element which contains the attribute * @param attributeName * name of the Attribute * @return attribute if found attribute value otherwise an empty string */ private String getAttributeValueByName(Element element, String attributeName) { if (element == null) { return ""; } Attr attribute = (Attr) element.getAttributes().getNamedItem(attributeName); if (attribute != null) { return attribute.getNodeValue(); } return ""; } /** * Returns the issuer of an SAML Message * * @param document * Document which contains the issuer * @return Issuer of message / first Assertion if found, else empty string */ public String getIssuer(Document document) { NodeList nl = document.getElementsByTagNameNS("*", "Issuer"); if (nl.getLength() > 0) { return nl.item(0).getTextContent(); } return ""; } /** * Returns NotBefore Date Attribute of Condition Element * * @param assertion * Assertion with Condition tag * @return NotBefore date Attribute of Condition Element if found, else * empty string */ public String getConditionNotBefore(Node assertion) { if (assertion == null || !assertion.getLocalName().equals("Assertion")) { return "no assertion"; } Element conditions = (Element) ((Element) assertion).getElementsByTagNameNS("*", "Conditions").item(0); return getAttributeValueByName(conditions, "NotBefore"); } /** * Returns NotOnOrAfter Date Attribute of Condition Element * * @param assertion * Assertion with Condition tag * @return NotOnOrAfter Date Attribute of Condition Element if found, else * empty string */ public String getConditionNotAfter(Node assertion) { if (assertion == null || !assertion.getLocalName().equals("Assertion")) { return "no assertion"; } Element conditions = (Element) ((Element) assertion).getElementsByTagNameNS("*", "Conditions").item(0); return getAttributeValueByName(conditions, "NotOnOrAfter"); } /** * Returns NotBefore Date Attribute of SubjectConfirmation Element * * @param assertion * Assertion with SubjectConfirmation tag * @return NotBefore Date Attribute of SubjectConfirmation Element if found, * else empty string */ public String getSubjectConfNotBefore(Node assertion) { if (assertion == null || !assertion.getLocalName().equals("Assertion")) { return "no assertion"; } Element subjConfirmation = (Element) ((Element) assertion).getElementsByTagNameNS("*", "SubjectConfirmationData").item(0); return getAttributeValueByName(subjConfirmation, "NotBefore"); } /** * Returns NotOnOrAfter Date Attribute of SubjectConfirmation Element * * @param assertion * Assertion with SubjectConfirmation tag * @return NotOnOrAfter Date Attribute of SubjectConfirmation Element if * found, else empty string */ public String getSubjectConfNotAfter(Node assertion) { if (assertion == null || !assertion.getLocalName().equals("Assertion")) { return "no assertion"; } Element subjConfirmation = (Element) ((Element) assertion).getElementsByTagNameNS("*", "SubjectConfirmationData").item(0); return getAttributeValueByName(subjConfirmation, "NotOnOrAfter"); } /** * Returns Signature Algorithm of Node which is signed * * @param node * node with Signature * @return Signature Algorithm of Node which is signed */ public String getSignatureAlgorithm(Node node) { if (node == null) { return "no element"; } Element signatureMethod = (Element) ((Element) node).getElementsByTagNameNS("*", "SignatureMethod").item(0); return getAttributeValueByName(signatureMethod, "Algorithm"); } /** * Returns Digest Algorithm of Node which is signed * * @param node * node with Signature * @return Digest Algorithm of Node which is signed */ public String getDigestAlgorithm(Node node) { if (node == null) { return "no element"; } Element digestMethod = (Element) ((Element) node).getElementsByTagNameNS("*", "DigestMethod").item(0); return getAttributeValueByName(digestMethod, "Algorithm"); } /** * Returns encryption algorithm of encrypted assertion * * @param assertion * encrypted assertion node * @return encryption algorithm of encrypted assertion */ public String getEncryptionMethod(Node assertion) { if (assertion == null || !assertion.getLocalName().equals("EncryptedAssertion")) { return "no encryption"; } Element encryptionMethod = (Element) ((Element) assertion).getElementsByTagNameNS("*", "EncryptionMethod") .item(0); return getAttributeValueByName(encryptionMethod, "Algorithm"); } /** * Returns embedded x509 certificate of signature * * @param node * node with embedded x509 certificate, no matter how deeply nested * @return first embedded x509 certificate of signature or null if not found */ public String getCertificate(Node node) { NodeList certificates = ((Element) node).getElementsByTagNameNS("*", "X509Certificate"); if(certificates.getLength() > 0){ Element certificate = (Element) certificates.item(0); return certificate.getTextContent(); } return null; } /** * Set the ID Attribute in an XML Document so that java recognises the ID * Attribute as a real id * * @param document * Document to set the ids */ public void setIDAttribute(Document document) { try { if(Thread.currentThread().getContextClassLoader() == null){ Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expr = xpath.compile("//*[@ID]"); NodeList nodeList = (NodeList) expr.evaluate(document, XPathConstants.NODESET); for (int i = 0; i < nodeList.getLength(); i++) { Element elem = (Element) nodeList.item(i); Attr attr = (Attr) elem.getAttributes().getNamedItem("ID"); elem.setIdAttributeNode(attr, true); } } catch (XPathExpressionException e) { e.printStackTrace(); } } /** * Sign assertions in SAML message * * @param document * Document in assertions should be signed * @param signAlgorithm * Signature algorithm in uri form, default if an unknown * algorithm is provided: * http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 * @param digestAlgorithm * Digest algorithm in uri form, default if an unknown algorithm * is provided: http://www.w3.org/2001/04/xmlenc#sha256 */ public void signAssertion(Document document, String signAlgorithm, String digestAlgorithm, X509Certificate cert, PrivateKey key) throws CertificateException, FileNotFoundException, NoSuchAlgorithmException, InvalidKeySpecException, MarshalException, XMLSignatureException, IOException { try { if(Thread.currentThread().getContextClassLoader() == null){ Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } setIDAttribute(document); XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expr = xpath.compile("//*[local-name()='Assertion']/@ID"); NodeList nlURIs = (NodeList) expr.evaluate(document, XPathConstants.NODESET); String[] sigIDs = new String[nlURIs.getLength()]; for (int i = 0; i < nlURIs.getLength(); i++) { sigIDs[i] = nlURIs.item(i).getNodeValue(); } Init.init(); for (String id : sigIDs) { signElement(document, id, cert, key, signAlgorithm, digestAlgorithm); } } catch (XPathExpressionException e) { e.printStackTrace(); } } /** * Sign whole SAML Message * * @param document * Document with the response to sign * @param signAlgorithm * Signature algorithm in uri form, default if an unknown * algorithm is provided: * http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 * @param digestAlgorithm * Digest algorithm in uri form, default if an unknown algorithm * is provided: http://www.w3.org/2001/04/xmlenc#sha256 */ public void signMessage(Document document, String signAlgorithm, String digestAlgorithm, X509Certificate cert, PrivateKey key) throws CertificateException, FileNotFoundException, NoSuchAlgorithmException, InvalidKeySpecException, MarshalException, XMLSignatureException, IOException { try { if(Thread.currentThread().getContextClassLoader() == null){ Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } setIDAttribute(document); XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expr = xpath.compile("//*[local-name()='Response']/@ID"); NodeList nlURIs = (NodeList) expr.evaluate(document, XPathConstants.NODESET); String[] sigIDs = new String[nlURIs.getLength()]; for (int i = 0; i < nlURIs.getLength(); i++) { sigIDs[i] = nlURIs.item(i).getNodeValue(); } Init.init(); for (String id : sigIDs) { signElement(document, id, cert, key, signAlgorithm, digestAlgorithm); } } catch (XPathExpressionException e) { e.printStackTrace(); } } /** * Sign the assertion with the given id * * @param document * Document in which the assertion with the given id should be * signed * @param id * the signature algorithm * @param key * the private key to sign the assertion * @param cert * the certificate which should be included in the assertions * signed info * @param signAlgorithm * Signature algorithm in uri form, default if an unknown * algorithm is provided: * http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 * @param digestAlgorithm * Digest algorithm in uri form, default if an unknown algorithm * is provided: http://www.w3.org/2001/04/xmlenc#sha256 */ private Document signElement(Document doc, String id, X509Certificate cert, PrivateKey key, String signAlgorithm, String digestAlgorithm) throws MarshalException, XMLSignatureException { try { XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance("DOM", new org.jcp.xml.dsig.internal.dom.XMLDSigRI()); List<Transform> transforms = new ArrayList<Transform>(); Transform enveloped = xmlSignatureFactory.newTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE, (XMLStructure) null); transforms.add(enveloped); Transform c14n = xmlSignatureFactory.newTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS, (XMLStructure) null); transforms.add(c14n); Reference ref; try { ref = xmlSignatureFactory.newReference("#" + id, xmlSignatureFactory.newDigestMethod(digestAlgorithm, null), transforms, null, null); } catch (NoSuchAlgorithmException e) { ref = xmlSignatureFactory.newReference("#" + id, xmlSignatureFactory.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); } SignedInfo signedInfo; try { signedInfo = xmlSignatureFactory.newSignedInfo(xmlSignatureFactory.newCanonicalizationMethod( CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null), xmlSignatureFactory .newSignatureMethod(signAlgorithm, null), Collections.singletonList(ref)); } catch (NoSuchAlgorithmException e) { signedInfo = xmlSignatureFactory.newSignedInfo(xmlSignatureFactory.newCanonicalizationMethod( CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null), xmlSignatureFactory .newSignatureMethod(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256, null), Collections .singletonList(ref)); } KeyInfoFactory keyInfoFactory = xmlSignatureFactory.getKeyInfoFactory(); List<X509Certificate> x509Content = new ArrayList<>(); x509Content.add(cert); X509Data x509Data = keyInfoFactory.newX509Data(x509Content); KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(x509Data)); Element elementToSign = doc.getElementById(id); NodeList issuerList = elementToSign.getElementsByTagNameNS("*", "Issuer"); Element elementBeforeSignature; if (issuerList.getLength() > 0) { elementBeforeSignature = (Element) issuerList.item(0); } else { elementBeforeSignature = elementToSign; } // find next sibling node of Element type Node nextNodeAfterIssuer = elementBeforeSignature.getNextSibling(); while (nextNodeAfterIssuer != null && nextNodeAfterIssuer.getNodeType() != Node.ELEMENT_NODE) { nextNodeAfterIssuer = nextNodeAfterIssuer.getNextSibling(); } Element nextElementAfterIssuer = (Element) nextNodeAfterIssuer; DOMSignContext domSignContext = new DOMSignContext(key, elementToSign); domSignContext.setDefaultNamespacePrefix("ds"); domSignContext.setNextSibling(nextElementAfterIssuer); javax.xml.crypto.dsig.XMLSignature signature = xmlSignatureFactory.newXMLSignature(signedInfo, keyInfo); signature.sign(domSignContext); return doc; } catch (InvalidAlgorithmParameterException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e1) { e1.printStackTrace(); } return null; } /*------------ //Source: http://www.oracle.com/technetwork/articles/javase/dig-signature-api-140772.html ------------*/ /** * Validates if the first XML Signature of the given document is valid * Only used for test purposes * * @param document * Document with signature to validate * @return true if valid, else false */ public boolean validateSignature(Document document) throws Exception { setIDAttribute(document); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); // Find Signature element. NodeList nl = document.getElementsByTagNameNS(javax.xml.crypto.dsig.XMLSignature.XMLNS, "Signature"); if (nl.getLength() == 0) { throw new Exception("Cannot find Signature element"); } // Create a DOMValidateContext and specify a KeySelector // and document context. DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), nl.item(0)); // Unmarshal the XMLSignature javax.xml.crypto.dsig.XMLSignature signature = fac.unmarshalXMLSignature(valContext); // Validate the XMLSignature. boolean coreValidity = signature.validate(valContext); // Check core validation status. if (coreValidity == false) { boolean sv = signature.getSignatureValue().validate(valContext); if (sv == false) { if(Flags.DEBUG){ // Check the validation status of each Reference. @SuppressWarnings("rawtypes") Iterator i = signature.getSignedInfo().getReferences().iterator(); for (int j = 0; i.hasNext(); j++) { boolean refValid = ((Reference) i.next()).validate(valContext); System.out.println("ref[" + j + "] validity status: " + refValid); } } } } return coreValidity; } }