/* DigiDoc4J library
*
* This software is released under either the GNU Library General Public
* License (see LICENSE.LGPL).
*
* Note that the only valid version of the LGPL license as far as this
* project is concerned is the original GNU Library General Public License
* Version 2.1, February 1999
*/
package org.digidoc4j.impl.bdoc.manifest;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.xml.security.signature.Reference;
import org.digidoc4j.Signature;
import org.digidoc4j.exceptions.DigiDoc4JException;
import org.digidoc4j.impl.bdoc.BDocSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import eu.europa.esig.dss.DSSDocument;
import eu.europa.esig.dss.xades.DSSXMLUtils;
/**
* For validating meta data within the manifest file and signature files.
*/
public class ManifestValidator {
private static final Logger logger = LoggerFactory.getLogger(ManifestValidator.class);
public static final String MANIFEST_PATH = "META-INF/manifest.xml";
public static final String MIMETYPE_PATH = "mimetype";
private List<DSSDocument> detachedContents;
private ManifestParser manifestParser;
private Collection<Signature> signatures;
public ManifestValidator(ManifestParser manifestParser, List<DSSDocument> detachedContents, Collection<Signature> signatures) {
this.manifestParser = manifestParser;
this.detachedContents = detachedContents;
this.signatures = signatures;
}
/**
* Validate the container.
*
* @return list of error messages
*/
public List<String> validateDocument() {
logger.debug("");
if (!manifestParser.containsManifestFile()) {
String errorMessage = "Container does not contain manifest file.";
logger.error(errorMessage);
throw new DigiDoc4JException(errorMessage);
}
List<String> errorMessages = new ArrayList<>();
Map<String, ManifestEntry> manifestEntries = manifestParser.getManifestFileItems();
Set<ManifestEntry> signatureEntries = new HashSet<>();
for (Signature signature : signatures) {
signatureEntries = getSignatureEntries((BDocSignature) signature);
errorMessages.addAll(validateEntries(manifestEntries, signatureEntries, signature.getId()));
}
errorMessages.addAll(validateFilesInContainer(signatureEntries));
logger.info("Validation of meta data within the manifest file and signature files error count: " + errorMessages.size());
return errorMessages;
}
private List<String> validateFilesInContainer(Set<ManifestEntry> signatureEntries) {
logger.debug("");
ArrayList<String> errorMessages = new ArrayList<>();
if (signatureEntries.size() == 0)
return errorMessages;
Set<String> signatureEntriesFileNames = getFileNamesFromManifestEntrySet(signatureEntries);
List<String> filesInContainer = getFilesInContainer();
for(String fileInContainer: filesInContainer) {
if(!signatureEntriesFileNames.contains(fileInContainer)) {
logger.error("Container contains unsigned data file '" + fileInContainer + "'");
errorMessages.add("Container contains a file named " + fileInContainer + " which is not found in the signature file");
}
}
return errorMessages;
}
private Set<String> getFileNamesFromManifestEntrySet(Set<ManifestEntry> signatureEntries) {
Set<String> signatureEntriesFileNames = new HashSet<>(signatureEntries.size());
for (ManifestEntry entry : signatureEntries) {
signatureEntriesFileNames.add(entry.getFileName());
}
return signatureEntriesFileNames;
}
@SuppressWarnings("unchecked")
static List<String> validateEntries(Map<String, ManifestEntry> manifestEntries, Set<ManifestEntry> signatureEntries,
String signatureId) {
logger.debug("");
ArrayList<String> errorMessages = new ArrayList<>();
if (signatureEntries.size() == 0)
return errorMessages;
Set<ManifestEntry> one = new HashSet(manifestEntries.values());
Set<ManifestEntry> two = new HashSet(signatureEntries);
one.removeAll(signatureEntries);
two.removeAll(manifestEntries.values());
for (ManifestEntry manifestEntry : one) {
String fileName = manifestEntry.getFileName();
ManifestEntry signatureEntry = signatureEntryForFile(fileName, signatureEntries);
if (signatureEntry != null) {
errorMessages.add("Manifest file has an entry for file " + fileName + " with mimetype " +
manifestEntry.getMimeType() + " but the signature file for signature " + signatureId +
" indicates the mimetype is " + signatureEntry.getMimeType());
two.remove(signatureEntry);
} else {
errorMessages.add("Manifest file has an entry for file " + fileName + " with mimetype "
+ manifestEntry.getMimeType() + " but the signature file for signature " + signatureId +
" does not have an entry for this file");
}
}
for (ManifestEntry manifestEntry : two) {
errorMessages.add("The signature file for signature " + signatureId + " has an entry for file "
+ manifestEntry.getFileName() + " with mimetype " + manifestEntry.getMimeType()
+ " but the manifest file does not have an entry for this file");
}
return errorMessages;
}
private static ManifestEntry signatureEntryForFile(String fileName, Set<ManifestEntry> signatureEntries) {
logger.debug("File name: " + fileName);
for (ManifestEntry signatureEntry : signatureEntries) {
if (fileName.equals(signatureEntry.getFileName())) {
return signatureEntry;
}
}
return null;
}
private Set<ManifestEntry> getSignatureEntries(BDocSignature signature) {
Set<ManifestEntry> signatureEntries = new HashSet<>();
List<Reference> references = signature.getOrigin().getReferences();
for (Reference reference : references) {
if (reference.getType().equals("")) {
String mimeTypeString = null;
Node signatureNode = signature.getOrigin().getDssSignature().getSignatureElement();
Node node = DSSXMLUtils.getNode(signatureNode, "./ds:SignedInfo/ds:Reference[@URI=\"" + reference.getURI() + "\"]");
if (node != null) {
String referenceId = node.getAttributes().getNamedItem("Id").getNodeValue();
mimeTypeString = DSSXMLUtils.getValue(signatureNode,
"./ds:Object/xades:QualifyingProperties/xades:SignedProperties/" +
"xades:SignedDataObjectProperties/xades:DataObjectFormat" +
"[@ObjectReference=\"#" + referenceId + "\"]/xades:MimeType");
}
// TODO: mimeTypeString == null ? node == null?
String uri = getFileURI(reference);
signatureEntries.add(new ManifestEntry(uri, mimeTypeString));
}
}
return signatureEntries;
}
private String getFileURI(Reference reference) {
String uri = reference.getURI();
try {
uri = new URI(uri).getPath();
} catch (URISyntaxException e) {
logger.warn("Does not parse as an URI, therefore assuming it's not encoded: '" + uri + "'");
}
return uri;
}
private List<String> getFilesInContainer() {
List<String> fileEntries = new ArrayList<>();
List<String> signatureFileNames = getSignatureFileNames();
for (DSSDocument detachedContent : detachedContents) {
String name = detachedContent.getName();
if (!(MANIFEST_PATH.equals(name) || ("META-INF/".equals(name)) || (MIMETYPE_PATH.equals(name)
|| signatureFileNames.contains(name)))) {
fileEntries.add(name);
}
}
return fileEntries;
}
private List<String> getSignatureFileNames() {
List<String> signatureFileNames = new ArrayList<>();
for (Signature signature :signatures) {
String signatureFileName = "META-INF/signature" + signature.getId().toLowerCase() + ".xml";
if (signatureFileNames.contains(signatureFileName)) {
String errorMessage = "Duplicate signature file: " + signatureFileName;
logger.error(errorMessage);
throw new DigiDoc4JException(errorMessage);
}
signatureFileNames.add(signatureFileName);
}
return signatureFileNames;
}
}