/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.core.util.xml; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.nio.file.Files; import java.security.GeneralSecurityException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.xml.crypto.AlgorithmMethod; import javax.xml.crypto.Data; import javax.xml.crypto.KeySelector; import javax.xml.crypto.KeySelectorException; import javax.xml.crypto.KeySelectorResult; import javax.xml.crypto.MarshalException; import javax.xml.crypto.OctetStreamData; import javax.xml.crypto.URIDereferencer; import javax.xml.crypto.URIReference; import javax.xml.crypto.URIReferenceException; import javax.xml.crypto.XMLCryptoContext; 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.SignatureMethod; import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.Transform; 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.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.crypto.dsig.spec.TransformParameterSpec; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.SAXException; /** * * Initial date: 16 févr. 2017<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class XMLDigitalSignatureUtil { private static final OLog log = Tracing.createLoggerFor(XMLDigitalSignatureUtil.class); /** * Validate a XML file with a XML Digital Signature saved in an extenral file. * * * @param xmlFile * @param xmlSignatureFile * @param publicKey * @return * @throws ParserConfigurationException * @throws SAXException * @throws IOException * @throws MarshalException * @throws XMLSignatureException */ public static boolean validate(String uri, File xmlFile, File xmlSignatureFile, PublicKey publicKey) throws ParserConfigurationException, SAXException, IOException, MarshalException, XMLSignatureException { Document doc = getDocument(xmlSignatureFile); NodeList nl = doc.getElementsByTagName("Signature"); if (nl.getLength() == 0) { return false; } DOMValidateContext validContext = new DOMValidateContext(publicKey, nl.item(0)); validContext.setBaseURI(uri); validContext.setURIDereferencer(new FileURIDereferencer(uri, xmlFile)); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignature signature = fac.unmarshalXMLSignature(validContext); boolean validFlag = signature.validate(validContext); if(!validFlag) { // log and throw if not valid boolean sv = signature.getSignatureValue().validate(validContext); String msg = "signature validation status: " + sv; int numOfReferences = signature.getSignedInfo().getReferences().size(); for (int j=0; j<numOfReferences; j++) { Reference ref = (Reference)signature.getSignedInfo().getReferences().get(j); boolean refValid = ref.validate(validContext); msg += " ref["+j+"] validity status: " + refValid; } log.warn(msg); } return validFlag; } /** * * @param uri * @param xmlFile * @param xmlSignatureFile * @return * @throws ParserConfigurationException * @throws SAXException * @throws IOException * @throws MarshalException * @throws XMLSignatureException */ public static boolean validate(String uri, File xmlFile, File xmlSignatureFile) throws ParserConfigurationException, SAXException, IOException, MarshalException, XMLSignatureException { Document doc = getDocument(xmlSignatureFile); NodeList nl = doc.getElementsByTagName("Signature"); if (nl.getLength() == 0) { return false; } DOMValidateContext validContext = new DOMValidateContext(new X509KeySelector(), nl.item(0)); validContext.setBaseURI(uri); validContext.setURIDereferencer(new FileURIDereferencer(uri, xmlFile)); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignature signature = fac.unmarshalXMLSignature(validContext); boolean validFlag = signature.validate(validContext); if(!validFlag) { // log and throw if not valid boolean sv = signature.getSignatureValue().validate(validContext); String msg = "signature validation status: " + sv; int numOfReferences = signature.getSignedInfo().getReferences().size(); for (int j=0; j<numOfReferences; j++) { Reference ref = (Reference)signature.getSignedInfo().getReferences().get(j); boolean refValid = ref.validate(validContext); msg += " ref["+j+"] validity status: " + refValid; } log.warn(msg); } return validFlag; } public static boolean validate(File signedXmlFile, PublicKey publicKey) throws ParserConfigurationException, SAXException, IOException, MarshalException, XMLSignatureException { Document doc = getDocument(signedXmlFile); NodeList nl = doc.getElementsByTagName("Signature"); if (nl.getLength() == 0) { return false; } DOMValidateContext validContext = new DOMValidateContext(publicKey, nl.item(0)); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignature signature = fac.unmarshalXMLSignature(validContext); boolean validFlag = signature.validate(validContext); if(!validFlag) { // log and throw if not valid boolean sv = signature.getSignatureValue().validate(validContext); String msg = "signature validation status: " + sv; int numOfReferences = signature.getSignedInfo().getReferences().size(); for (int j=0; j<numOfReferences; j++) { Reference ref = (Reference)signature.getSignedInfo().getReferences().get(j); boolean refValid = ref.validate(validContext); msg += " ref["+j+"] validity status: " + refValid; } log.warn(msg); } return validFlag; } /** * Produce a signed a XML file. The signature is added in the XML file. * * @param xmlFile The original XML file * @param xmlSignedFile The signed XML file * @param x509Cert * @param privateKey * @throws IOException * @throws SAXException * @throws ParserConfigurationException * @throws NoSuchAlgorithmException * @throws GeneralSecurityException * @throws MarshalException * @throws XMLSignatureException * @throws TransformerException */ public static void signEmbedded(File xmlFile, File xmlSignedFile, X509Certificate x509Cert, PrivateKey privateKey) throws IOException, SAXException, ParserConfigurationException, NoSuchAlgorithmException, GeneralSecurityException, MarshalException, XMLSignatureException, TransformerException { Document doc = getDocument(xmlFile); // Create the signature factory for creating the signature. XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM"); List<Transform> transforms = new ArrayList<Transform>(); Transform envelopped = sigFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null); transforms.add(envelopped); // Create the canonicalization transform to be applied after the XSLT. CanonicalizationMethod c14n = sigFactory.newCanonicalizationMethod( CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null); transforms.add(c14n); // Create the Reference to the XML to be signed specifying the hash algorithm to be used // and the list of transforms to apply. Also specify the XML to be signed as the current // document (specified by the first parameter being an empty string). Reference reference = sigFactory.newReference( "", sigFactory.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); // Create the Signed Info node of the signature by specifying the canonicalization method // to use (INCLUSIVE), the signing method (RSA_SHA1), and the Reference node to be signed. SignedInfo si = sigFactory.newSignedInfo(c14n, sigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null), Collections.singletonList(reference)); // Create the KeyInfo node containing the public key information to include in the signature. KeyInfoFactory kif = sigFactory.getKeyInfoFactory(); X509Data xd = kif.newX509Data(Collections.singletonList(x509Cert)); KeyInfo ki = kif.newKeyInfo(Collections.singletonList(xd)); // Get the node to attach the signature. Node signatureInfoNode = doc.getDocumentElement(); // Create a signing context using the private key. DOMSignContext dsc = new DOMSignContext(privateKey, signatureInfoNode); // Create the signature from the signing context and key info XMLSignature signature = sigFactory.newXMLSignature(si, ki); signature.sign(dsc); write(doc, xmlSignedFile); } /** * Create a separate XML file with the XML Digital Signature. * * of the specified XML file. * @param xmlFile The XML File to sign * @param outputSignatureFile Where the Digital Signature is saved * @param signatureDoc A DOM which hold the signature (optional but if you give one, the root element must exists) * @throws ParserConfigurationException * @throws GeneralSecurityException * @throws NoSuchAlgorithmException * @throws XMLSignatureException * @throws MarshalException * @throws TransformerException */ public static void signDetached(String uri, File xmlFile, File outputSignatureFile, Document signatureDoc, String keyName, X509Certificate x509Cert, PrivateKey privateKey) throws IOException, SAXException, ParserConfigurationException, NoSuchAlgorithmException, GeneralSecurityException, MarshalException, XMLSignatureException, TransformerException { Document doc = getDocument(xmlFile); // Create the signature factory for creating the signature. XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM"); List<Transform> transforms = new ArrayList<Transform>(); //Transform envelopped = sigFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null); //transforms.add(envelopped); // Create the canonicalization transform to be applied after the XSLT. CanonicalizationMethod c14n = sigFactory.newCanonicalizationMethod( CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null); transforms.add(c14n); // Create the Reference to the XML to be signed specifying the hash algorithm to be used // and the list of transforms to apply. Also specify the XML to be signed as the current // document (specified by the first parameter being an empty string). Reference reference = sigFactory.newReference( uri, sigFactory.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); // Create the Signed Info node of the signature by specifying the canonicalization method // to use (INCLUSIVE), the signing method (RSA_SHA1), and the Reference node to be signed. SignedInfo si = sigFactory.newSignedInfo(c14n, sigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null), Collections.singletonList(reference)); // Create the KeyInfo node containing the public key information to include in the signature. KeyInfoFactory kif = sigFactory.getKeyInfoFactory(); X509Data xd = kif.newX509Data(Collections.singletonList(x509Cert)); List<Object> keyInfoList = new ArrayList<>(); if(StringHelper.containsNonWhitespace(keyName)) { keyInfoList.add(kif.newKeyName(keyName)); } keyInfoList.add(xd); KeyInfo ki = kif.newKeyInfo(keyInfoList); // Get the node to attach the signature. Node signatureInfoNode = doc.getDocumentElement(); // Create a signing context using the private key. DOMSignContext dsc = new DOMSignContext(privateKey, signatureInfoNode); dsc.setBaseURI(uri); dsc.setURIDereferencer(new FileURIDereferencer(uri, xmlFile)); // Create the signature from the signing context and key info XMLSignature signature = sigFactory.newXMLSignature(si, ki); signature.sign(dsc); NodeList nl = doc.getElementsByTagName("Signature"); if (nl.getLength() == 1) { if(signatureDoc != null && signatureDoc.getDocumentElement() != null) { Element rootEl = signatureDoc.getDocumentElement(); rootEl.appendChild(signatureDoc.importNode(nl.item(0), true)); write(rootEl, outputSignatureFile); } else { write(nl.item(0), outputSignatureFile); } } } private static void write(Node node, File outputFile) throws IOException, TransformerException, TransformerFactoryConfigurationError, IllegalArgumentException { try (Writer ssw = new FileWriter(outputFile)) { TransformerFactory tf = TransformerFactory.newInstance(); Transformer trans = tf.newTransformer(); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); trans.transform(new DOMSource(node), new StreamResult(ssw)); } catch (TransformerException | TransformerFactoryConfigurationError | IllegalArgumentException e) { throw e; } catch (IOException e) { throw e; } } public static Document getDocument(File xmlFile) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); dbFactory.setNamespaceAware(true); return dbFactory.newDocumentBuilder().parse(xmlFile); } public static Document createDocument() throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); dbFactory.setNamespaceAware(true); return dbFactory.newDocumentBuilder().newDocument(); } public static String getReferenceURI(Document doc) { NodeList nl = doc.getElementsByTagName("Reference"); for(int i=nl.getLength(); i-->0; ) { Element referenceEl = (Element)nl.item(i); Node uriNode = referenceEl.getAttributes().getNamedItem("URI"); if(uriNode != null) { return uriNode.getNodeValue(); } } return null; } public static String getKeyName(Document doc) { NodeList nl = doc.getElementsByTagName("KeyName"); if(nl.getLength() == 1) { return getElementText(doc, "KeyName"); } return null; } public static String getElementText(Document doc, String elementName) { StringBuilder sb = new StringBuilder(); if(doc != null) { NodeList nl = doc.getElementsByTagName(elementName); if(nl.getLength() == 1) { Node element = nl.item(0); for(Node child=element.getFirstChild(); child != null; child = child.getNextSibling()) { if(child instanceof Text) { Text text = (Text)child; sb.append(text.getTextContent()); } } } } return sb.toString(); } private static class FileURIDereferencer implements URIDereferencer { private final String uri; private final File xmlFile; public FileURIDereferencer(String uri, File xmlFile) { this.uri = uri; this.xmlFile = xmlFile; } @Override public Data dereference(URIReference uriReference, XMLCryptoContext context) throws URIReferenceException { try { if(uri.equals(uriReference.getURI())) { byte[] bytes = Files.readAllBytes(xmlFile.toPath()); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); return new OctetStreamData(inputStream); } return null; } catch (Exception e) { throw new URIReferenceException(e); } } } private static class X509KeySelector extends KeySelector { public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException { @SuppressWarnings("unchecked") Iterator<Object> ki = keyInfo.getContent().iterator(); while (ki.hasNext()) { XMLStructure info = (XMLStructure) ki.next(); if (!(info instanceof X509Data)) { continue; } X509Data x509Data = (X509Data) info; @SuppressWarnings("unchecked") Iterator<Object> xi = x509Data.getContent().iterator(); while (xi.hasNext()) { Object o = xi.next(); if (!(o instanceof X509Certificate)) { continue; } final PublicKey key = ((X509Certificate)o).getPublicKey(); if (algEquals(method.getAlgorithm(), key.getAlgorithm())) { return new KeySelectorResult() { public Key getKey() { return key; } }; } } } throw new KeySelectorException("No key found!"); } static boolean algEquals(String algURI, String algName) { if ((algName.equalsIgnoreCase("DSA") && algURI.equalsIgnoreCase(SignatureMethod.DSA_SHA1)) || (algName.equalsIgnoreCase("RSA") && algURI.equalsIgnoreCase(SignatureMethod.RSA_SHA1))) { return true; } else { return false; } } } }