/**
* DSS - Digital Signature Services
* Copyright (C) 2015 European Commission, provided under the CEF programme
*
* This file is part of the "DSS - Digital Signature Services" project.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package eu.europa.esig.dss.xades.validation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.crypto.dsig.XMLSignature;
import org.bouncycastle.util.encoders.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import eu.europa.esig.dss.DSSDocument;
import eu.europa.esig.dss.DSSException;
import eu.europa.esig.dss.DSSUtils;
import eu.europa.esig.dss.DomUtils;
import eu.europa.esig.dss.InMemoryDocument;
import eu.europa.esig.dss.MimeType;
import eu.europa.esig.dss.utils.Utils;
import eu.europa.esig.dss.validation.AdvancedSignature;
import eu.europa.esig.dss.validation.SignedDocumentValidator;
import eu.europa.esig.dss.xades.DSSXMLUtils;
import eu.europa.esig.dss.xades.XPathQueryHolder;
/**
* Validator of XML Signed document
*
*/
public class XMLDocumentValidator extends SignedDocumentValidator {
private static final byte[] xmlPreamble = new byte[] { '<', '?', 'x', 'm', 'l' };
private static final byte[] xmlUtf8 = new byte[] { -17, -69, -65, '<', '?' };
private static final String BASE64_REGEX = "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$";
/**
* This variable contains the list of {@code XPathQueryHolder} adapted to the specific signature schema.
*/
protected List<XPathQueryHolder> xPathQueryHolders;
protected Document rootElement;
private List<AdvancedSignature> signatures;
/**
* Default constructor used with reflexion (see SignedDocumentValidator)
*/
private XMLDocumentValidator() {
super(null);
}
/**
* The default constructor for XMLDocumentValidator. The created instance is initialised with default
* {@code XPathQueryHolder} and
* {@code XAdES111XPathQueryHolder}.
*
* @param dssDocument
* The instance of {@code DSSDocument} to validate
* @throws DSSException
*/
public XMLDocumentValidator(final DSSDocument dssDocument) throws DSSException {
super(new XAdESSignatureScopeFinder());
this.document = dssDocument;
this.rootElement = DomUtils.buildDOM(dssDocument);
xPathQueryHolders = new ArrayList<XPathQueryHolder>();
final XAdES111XPathQueryHolder xades111xPathQueryHolder = new XAdES111XPathQueryHolder();
xPathQueryHolders.add(xades111xPathQueryHolder);
final XPathQueryHolder xades122XPathQueryHolder = new XAdES122XPathQueryHolder();
xPathQueryHolders.add(xades122XPathQueryHolder);
final XPathQueryHolder xPathQueryHolder = new XPathQueryHolder();
xPathQueryHolders.add(xPathQueryHolder);
}
@Override
public boolean isSupported(DSSDocument dssDocument) {
final String dssDocumentName = dssDocument.getName();
if ((dssDocumentName != null) && MimeType.XML.equals(MimeType.fromFileName(dssDocumentName))) {
return true;
}
int headerLength = 500;
byte[] preamble = new byte[headerLength];
DSSUtils.readToArray(dssDocument, headerLength, preamble);
if (isXmlPreamble(preamble)) {
return true;
}
return false;
}
private boolean isXmlPreamble(byte[] preamble) {
byte[] startOfPramble = Utils.subarray(preamble, 0, xmlPreamble.length);
return Arrays.equals(startOfPramble, xmlPreamble) || Arrays.equals(startOfPramble, xmlUtf8);
}
@Override
public List<AdvancedSignature> getSignatures() {
if (signatures != null) {
return signatures;
}
signatures = new ArrayList<AdvancedSignature>();
final NodeList signatureNodeList = DomUtils.getNodeList(rootElement, "//ds:Signature[not(parent::xades:CounterSignature)]");
for (int ii = 0; ii < signatureNodeList.getLength(); ii++) {
final Element signatureEl = (Element) signatureNodeList.item(ii);
final XAdESSignature xadesSignature = new XAdESSignature(signatureEl, xPathQueryHolders, validationCertPool);
xadesSignature.setSignatureFilename(document.getName());
xadesSignature.setDetachedContents(detachedContents);
xadesSignature.setProvidedSigningCertificateToken(providedSigningCertificateToken);
signatures.add(xadesSignature);
}
return signatures;
}
/**
* Retrieves a signature based on its Id
*
* @param signatureId
* the given Id
* @return the corresponding {@code XAdESSignature}
* @throws DSSException
* in case no Id is provided, or in case no signature was found for the given Id
*/
public AdvancedSignature getSignatureById(final String signatureId) throws DSSException {
if (Utils.isStringBlank(signatureId)) {
throw new NullPointerException("signatureId");
}
final List<AdvancedSignature> advancedSignatures = getSignatures();
for (final AdvancedSignature advancedSignature : advancedSignatures) {
final String advancedSignatureId = advancedSignature.getId();
if (signatureId.equals(advancedSignatureId)) {
return advancedSignature;
}
}
throw new DSSException("The signature with the given id was not found!");
}
@Override
public List<DSSDocument> getOriginalDocuments(final String signatureId) throws DSSException {
if (Utils.isStringBlank(signatureId)) {
throw new NullPointerException("signatureId");
}
List<DSSDocument> result = new ArrayList<DSSDocument>();
final NodeList signatureNodeList = rootElement.getElementsByTagNameNS(XMLSignature.XMLNS, XPathQueryHolder.XMLE_SIGNATURE);
List<AdvancedSignature> signatureList = getSignatures();
for (int ii = 0; ii < signatureNodeList.getLength(); ii++) {
final Element signatureEl = (Element) signatureNodeList.item(ii);
final String idIdentifier = DSSXMLUtils.getIDIdentifier(signatureEl);
if (signatureId.equals(idIdentifier)) {
XAdESSignature signature = (XAdESSignature) signatureList.get(ii);
signature.checkSignatureIntegrity();
if (getSignatureObjects(signatureEl).isEmpty() && signature.getReferences().isEmpty()) {
throw new DSSException("The signature must be enveloped or enveloping!");
} else if (isEnveloping(signatureEl)) {
List<Element> references = getSignatureObjects(signatureEl);
for (Element element : references) {
String content = element.getTextContent();
content = isBase64Encoded(content) ? new String(Base64.decode(content)) : content;
result.add(new InMemoryDocument(content.getBytes()));
}
} else {
signatureEl.getParentNode().removeChild(signatureEl);
final Node documentElement = rootElement.getDocumentElement();
byte[] documentBytes = DSSXMLUtils.serializeNode(documentElement);
documentBytes = isBase64Encoded(documentBytes) ? Base64.decode(documentBytes) : documentBytes;
result.add(new InMemoryDocument(documentBytes));
}
}
}
return result;
}
private boolean isBase64Encoded(byte[] array) {
return isBase64Encoded(new String(array));
}
private boolean isBase64Encoded(String text) {
Pattern pattern = Pattern.compile(BASE64_REGEX);
Matcher matcher = pattern.matcher(text);
return matcher.matches();
}
private boolean isEnveloping(Element signatureEl) {
final NodeList objectNodeList = signatureEl.getChildNodes();
int objectTagNumber = 0;
for (int i = 0; i < objectNodeList.getLength(); i++) {
String nodeName = objectNodeList.item(i).getNodeName();
if ("ds:Object".equals(nodeName)) {
objectTagNumber++;
}
}
return objectTagNumber >= 2;
}
private List<Element> getSignatureObjects(Element signatureEl) {
final NodeList list = DomUtils.getNodeList(signatureEl, XPathQueryHolder.XPATH_OBJECT);
final List<Element> references = new ArrayList<Element>(list.getLength());
for (int ii = 0; ii < list.getLength(); ii++) {
final Node node = list.item(ii);
final Element element = (Element) node;
XPathQueryHolder queryHolder = new XPathQueryHolder();
if (DomUtils.getElement(element, queryHolder.XPATH__QUALIFYING_PROPERTIES_SIGNED_PROPERTIES) != null) {
// ignore signed properties
continue;
}
references.add(element);
}
return references;
}
/**
* This getter returns the {@code XPathQueryHolder}
*
* @return
*/
public List<XPathQueryHolder> getXPathQueryHolder() {
return xPathQueryHolders;
}
/**
* This adds a {@code XPathQueryHolder}. This is useful when the signature follows a particular schema.
*
* @param xPathQueryHolder
*/
public void addXPathQueryHolder(final XPathQueryHolder xPathQueryHolder) {
xPathQueryHolders.add(xPathQueryHolder);
}
/**
* Removes all of the elements from the list of query holders. The list will be empty after this call returns.
*/
public void clearQueryHolders() {
xPathQueryHolders.clear();
}
/**
* @return
*/
public Document getRootElement() {
return rootElement;
}
}