/* * DSS - Digital Signature Services * * Copyright (C) 2013 European Commission, Directorate-General Internal Market and Services (DG MARKT), B-1049 Bruxelles/Brussel * * Developed by: 2013 ARHS Developments S.A. (rue Nicolas Bové 2B, L-1253 Luxembourg) http://www.arhs-developments.com * * This file is part of the "DSS - Digital Signature Services" project. * * "DSS - Digital Signature Services" 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. * * DSS 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 * "DSS - Digital Signature Services". If not, see <http://www.gnu.org/licenses/>. */ package eu.europa.ec.markt.dss.validation102853.asic; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import eu.europa.ec.markt.dss.ASiCNamespaces; import eu.europa.ec.markt.dss.DSSUtils; import eu.europa.ec.markt.dss.DSSXMLUtils; import eu.europa.ec.markt.dss.exception.DSSException; import eu.europa.ec.markt.dss.exception.DSSNotETSICompliantException; import eu.europa.ec.markt.dss.exception.DSSNullException; import eu.europa.ec.markt.dss.signature.AsicManifestDocument; import eu.europa.ec.markt.dss.signature.DSSDocument; import eu.europa.ec.markt.dss.signature.InMemoryDocument; import eu.europa.ec.markt.dss.signature.MimeType; import eu.europa.ec.markt.dss.signature.asic.ASiCService; import eu.europa.ec.markt.dss.validation102853.AdvancedSignature; import eu.europa.ec.markt.dss.validation102853.DocumentValidator; import eu.europa.ec.markt.dss.validation102853.SignedDocumentValidator; import eu.europa.ec.markt.dss.validation102853.policy.ValidationPolicy; import eu.europa.ec.markt.dss.validation102853.report.Reports; import eu.europa.ec.markt.dss.validation102853.scope.SignatureScopeFinder; /** * This class is the base class for ASiC containers. * <p/> * Mime-type handling: FROM: ETSI TS 102 918 V1.2.1 * A.1 Mimetype * The "mimetype" object, when stored in a ZIP, file can be used to support operating systems that rely on some content in * specific positions in a file (the so called "magic number" as described in RFC 4288 [11] in order to select the specific * application that can load and elaborate the file content. The following restrictions apply to the mimetype to support this * feature: * • it has to be the first in the archive; * • it cannot contain "Extra fields" (i.e. extra field length at offset 28 shall be zero); * • it cannot be compressed (i.e. compression method at offset 8 shall be zero); * • the first 4 octets shall have the hex values: "50 4B 03 04". * An application can ascertain if this feature is used by checking if the string "mimetype" is found starting at offset 30. In * this case it can be assumed that a string representing the container mime type is present starting at offset 38; the length * of this string is contained in the 4 octets starting at offset 18. * All multi-octets values are little-endian. * The "mimetype" shall NOT be compressed or encrypted inside the ZIP file. * <p/> * --> The use of two first bytes is not standard conforming. * <p/> * 5.2.1 Media type identification * 1) File extension: ".asics"|".asice" should be used (".scs"|".sce" is allowed for operating systems and/or file systems not * allowing more than 3 characters file extensions). In the case where the container content is to be handled * manually, the ".zip" extension may be used. * <p/> * DISCLAIMER: Project owner DG-MARKT. * * @author <a href="mailto:dgmarkt.Project-DSS@arhs-developments.com">ARHS Developments</a> * @version $Revision: 1016 $ - $Date: 2011-06-17 15:30:45 +0200 (Fri, 17 Jun 2011) $ */ public class ASiCContainerValidator extends SignedDocumentValidator { private static final Logger LOG = LoggerFactory.getLogger(ASiCContainerValidator.class); private static final String MIME_TYPE = "mimetype"; private static final String MIME_TYPE_COMMENT = MIME_TYPE + "="; private static final String META_INF_FOLDER = "META-INF/"; private final DSSDocument asicContainer; /** * This is the subordinated validator: can be XML or CMS */ private SignedDocumentValidator subordinatedValidator; /** * The list of the signatures contained within the container. */ private final List<DSSDocument> signatures = new ArrayList<DSSDocument>(); /** * This list caches the validated signatures. */ private List<AdvancedSignature> validatedSignatures; /** * This mime-type comes from the container file name: (zip, asic...). */ // private MimeType asicContainerMimeType; /** * This mime-type comes from the 'mimetype' file included within the container. */ private MimeType asicMimeType; /** * This mime-type comes from the ZIP comment:<br/> * The comment field in the ZIP header may be used to identify the type of the data object within the container. * If this field is present, it should be set with "mimetype=" followed by the mime type of the data object held in * the signed data object. */ // protected MimeType asicCommentMimeType; private boolean cadesSigned = false; private boolean xadesSigned = false; private boolean timestamped = false; public ASiCContainerValidator(final DSSDocument asicContainer) { this.asicContainer = asicContainer; } /** * This method creates a dedicated {@code SignedDocumentValidator} based on the given container type: XAdES or CAdES. * * @param asicContainer The instance of {@code DSSDocument} to validate * @return {@code SignedDocumentValidator} * @throws DSSException */ public static SignedDocumentValidator getInstanceForAsics(final DSSDocument asicContainer) throws DSSException { final ASiCContainerValidator asicContainerValidator = new ASiCContainerValidator(asicContainer); asicContainerValidator.analyseEntries(); // ASiC-S: // - throw new DSSException("ASiC-S profile support only one data file"); // - DSSNotETSICompliantException.MSG.MORE_THAN_ONE_SIGNATURE asicContainerValidator.createSubordinatedContainerValidators(); return asicContainerValidator; } private MimeType determinateAsicMimeType(final MimeType asicContainerMimetype, final MimeType asicEntryMimetype) { if (isASiCMimeType(asicContainerMimetype)) { return asicContainerMimetype; } if (isASiCMimeType(asicEntryMimetype)) { return asicEntryMimetype; } final MimeType asicCommentString = getZipComment(asicContainer.getBytes()); if (isASiCMimeType(asicCommentString)) { return asicCommentString; } return null; } private static boolean isASiCMimeType(final MimeType asicMimeType) { return MimeType.ASICS.equals(asicMimeType) || MimeType.ASICE.equals(asicMimeType); } private AsicManifestDocument getRelatedAsicManifest(final DSSDocument signature) { for (final DSSDocument detachedContent : detachedContents) { if (!(detachedContent instanceof AsicManifestDocument)) { continue; } final AsicManifestDocument asicManifestDocument = (AsicManifestDocument) detachedContent; final String signatureUri = asicManifestDocument.getSignatureUri(); if (signatureUri.equals(signature.getName())) { return asicManifestDocument; } } return null; } /** * @return */ @Override public SignedDocumentValidator getSubordinatedValidator() { return subordinatedValidator; } private void createSubordinatedContainerValidators() { SignedDocumentValidator previousValidator = null; for (final DSSDocument signature : signatures) { final SignedDocumentValidator currentSubordinatedValidator; if (xadesSigned) { currentSubordinatedValidator = new ASiCXMLDocumentValidator(signature, detachedContents); } else if (cadesSigned) { currentSubordinatedValidator = new ASiCCMSDocumentValidator(signature, detachedContents); } else if (timestamped) { currentSubordinatedValidator = new ASiCTimestampDocumentValidator(signature, detachedContents); } else { throw new DSSException("The format of the signature is unknown! It is neither XAdES nor CAdES, nor timestamp signature!"); } if (previousValidator != null) { previousValidator.setNextValidator(currentSubordinatedValidator); } else { subordinatedValidator = currentSubordinatedValidator; } previousValidator = currentSubordinatedValidator; } if (subordinatedValidator == null) { throw new DSSException("This is not an ASiC container. The signature cannot be found!"); } } private void analyseEntries() throws DSSException { ZipInputStream asicsInputStream = null; try { MimeType asicEntryMimeType = null; asicsInputStream = new ZipInputStream(asicContainer.openStream()); // The underlying stream is closed by the parent (asicsInputStream). for (ZipEntry entry = asicsInputStream.getNextEntry(); entry != null; entry = asicsInputStream.getNextEntry()) { String entryName = entry.getName(); if (isCAdES(entryName)) { if (xadesSigned) { throw new DSSNotETSICompliantException(DSSNotETSICompliantException.MSG.DIFFERENT_SIGNATURE_FORMATS); } addEntryElement(entryName, signatures, asicsInputStream); cadesSigned = true; } else if (isXAdES(entryName)) { if (cadesSigned) { throw new DSSNotETSICompliantException(DSSNotETSICompliantException.MSG.DIFFERENT_SIGNATURE_FORMATS); } addEntryElement(entryName, signatures, asicsInputStream); xadesSigned = true; } else if (isTimestamp(entryName)) { addEntryElement(entryName, signatures, asicsInputStream); timestamped = true; } else if (isASiCManifest(entryName)) { addAsicManifestEntryElement(entryName, detachedContents, asicsInputStream); } else if (isManifest(entryName)) { addEntryElement(entryName, detachedContents, asicsInputStream); } else if (isContainer(entryName)) { addEntryElement(entryName, detachedContents, asicsInputStream); } else if (isMetadata(entryName)) { addEntryElement(entryName, detachedContents, asicsInputStream); } else if (MIME_TYPE.equalsIgnoreCase(entryName)) { final DSSDocument mimeType = addEntryElement(entryName, detachedContents, asicsInputStream); asicEntryMimeType = getMimeType(mimeType); } else if (entryName.indexOf("/") == -1) { addEntryElement(entryName, detachedContents, asicsInputStream); } else if (entryName.endsWith("/")) { // Folder continue; } else { addEntryElement(entryName, detachedContents, asicsInputStream); } } asicMimeType = determinateAsicMimeType(asicContainer.getMimeType(), asicEntryMimeType); if (MimeType.ASICS == asicMimeType) { final ListIterator<DSSDocument> dssDocumentListIterator = detachedContents.listIterator(); while (dssDocumentListIterator.hasNext()) { final DSSDocument dssDocument = dssDocumentListIterator.next(); final String detachedContentName = dssDocument.getName(); if ("mimetype".equals(detachedContentName)) { dssDocumentListIterator.remove(); } else if (detachedContentName.indexOf('/') != -1) { dssDocumentListIterator.remove(); } } } } catch (Exception e) { if (e instanceof DSSException) { throw (DSSException) e; } throw new DSSException(e); } finally { DSSUtils.closeQuietly(asicsInputStream); } } public MimeType getAsicMimeType() { return asicMimeType; } public void setAsicMimeType(final MimeType asicMimeType) { this.asicMimeType = asicMimeType; } private static MimeType getMimeType(final DSSDocument mimeType) throws DSSException { try { final InputStream inputStream = mimeType.openStream(); final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); DSSUtils.copy(inputStream, byteArrayOutputStream); final String mimeTypeString = byteArrayOutputStream.toString("UTF-8"); final MimeType asicMimeType = MimeType.fromMimeTypeString(mimeTypeString); return asicMimeType; } catch (UnsupportedEncodingException e) { throw new DSSException(e); } } private static DSSDocument addEntryElement(final String entryName, final List<DSSDocument> list, final ZipInputStream asicsInputStream) { final ByteArrayOutputStream signature = new ByteArrayOutputStream(); DSSUtils.copy(asicsInputStream, signature); final InMemoryDocument inMemoryDocument = new InMemoryDocument(signature.toByteArray(), entryName); list.add(inMemoryDocument); return inMemoryDocument; } private static void addAsicManifestEntryElement(final String entryName, final List<DSSDocument> list, final ZipInputStream asicsInputStream) { final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); DSSUtils.copy(asicsInputStream, byteArrayOutputStream); final AsicManifestDocument inMemoryDocument = new AsicManifestDocument(byteArrayOutputStream.toByteArray(), entryName); list.add(inMemoryDocument); } /** * 6.2.2 Contents of Container * 4) Other application specific information may be added in further files contained within the META-INF directory, such as: * c) "META-INF/metadata.xml" has a user defined content. If present, its content shall be well formed XML conformant to OEBPS Container Format (OCF) [4] specifications. * * @param entryName * @return */ private static boolean isMetadata(final String entryName) { final boolean manifest = entryName.equals(META_INF_FOLDER + "metadata.xml"); return manifest; } /** * 6.2.2 Contents of Container * 4) Other application specific information may be added in further files contained within the META-INF directory, such as: * a) "META-INF/container.xml" if present shall be well formed XML conformant to OEBPS Container Format (OCF) [4] specifications. It shall identify the MIME type and full path * of all the root data objects in the container, as specified in OCF. * * @param entryName * @return */ private static boolean isContainer(final String entryName) { final boolean manifest = entryName.equals(META_INF_FOLDER + "container.xml"); return manifest; } /** * 6.2.2 Contents of Container * 4) Other application specific information may be added in further files contained within the META-INF directory, such as: * b) "META-INF/manifest.xml" if present shall be well formed XML conformant to OASIS Open Document Format [6] specifications. * NOTE 4: according to ODF [6] specifications, inclusion of reference to other META-INF information, such as *signatures*.xml, in manifest.xml is optional. In this way it is * possible to protect the container's content signing manifest.xml while allowing to add later signatures. * * @param entryName * @return */ private static boolean isManifest(final String entryName) { final boolean manifest = entryName.equals(META_INF_FOLDER + "manifest.xml"); return manifest; } private static boolean isASiCManifest(String entryName) { final boolean manifest = entryName.endsWith(".xml") && entryName.startsWith(META_INF_FOLDER + "ASiCManifest"); return manifest; } public static boolean isTimestamp(String entryName) { final boolean timestamp = entryName.endsWith(".tst") && entryName.startsWith(META_INF_FOLDER) && entryName.contains("timestamp"); return timestamp; } public static boolean isXAdES(final String entryName) { final boolean signature = entryName.endsWith(".xml") && entryName.startsWith(META_INF_FOLDER) && entryName.contains("signature"); return signature; } public static boolean isCAdES(final String entryName) { final boolean signature = entryName.endsWith(".p7s") && entryName.startsWith(META_INF_FOLDER) && entryName.contains("signature"); return signature; } private static MimeType getZipComment(final byte[] buffer) { final int len = buffer.length; final byte[] magicDirEnd = {0x50, 0x4b, 0x05, 0x06}; final int buffLen = Math.min(buffer.length, len); // Check the buffer from the end for (int ii = buffLen - magicDirEnd.length - 22; ii >= 0; ii--) { boolean isMagicStart = true; for (int jj = 0; jj < magicDirEnd.length; jj++) { if (buffer[ii + jj] != magicDirEnd[jj]) { isMagicStart = false; break; } } if (isMagicStart) { // Magic Start found! int commentLen = buffer[ii + 20] + buffer[ii + 21] * 256; int realLen = buffLen - ii - 22; if (commentLen != realLen) { LOG.warn("WARNING! ZIP comment size mismatch: directory says len is " + commentLen + ", but file ends after " + realLen + " bytes!"); } final String comment = new String(buffer, ii + 22, Math.min(commentLen, realLen)); final int indexOf = comment.indexOf(MIME_TYPE_COMMENT); if (indexOf > -1) { final String asicCommentMimeTypeString = comment.substring(MIME_TYPE_COMMENT.length() + indexOf); final MimeType mimeType = MimeType.fromMimeTypeString(asicCommentMimeTypeString); return mimeType; } } } LOG.warn("ZIP comment NOT found!"); return null; } /** * Validates the document and all its signatures. The {@code validationPolicyDom} contains the constraint file. If null or empty the default file is used. * * @param validationPolicy {@code ValidationPolicy} * @return */ @Override public Reports validateDocument(final ValidationPolicy validationPolicy) { Reports lastReports = null; Reports firstReport = null; DocumentValidator currentSubordinatedValidator = subordinatedValidator; do { currentSubordinatedValidator.setProcessExecutor(processExecutor); if (MimeType.ASICE.equals(asicMimeType) && currentSubordinatedValidator instanceof ASiCCMSDocumentValidator) { final DSSDocument signature = currentSubordinatedValidator.getDocument(); final AsicManifestDocument relatedAsicManifest = getRelatedAsicManifest(signature); final ArrayList<DSSDocument> relatedAsicManifests = new ArrayList<DSSDocument>(); relatedAsicManifests.add(relatedAsicManifest); currentSubordinatedValidator.setDetachedContents(relatedAsicManifests); } else { currentSubordinatedValidator.setDetachedContents(detachedContents); } currentSubordinatedValidator.setCertificateVerifier(certificateVerifier); final Reports currentReports = currentSubordinatedValidator.validateDocument(validationPolicy); if (lastReports == null) { firstReport = currentReports; } else { lastReports.setNextReport(currentReports); } lastReports = currentReports; currentSubordinatedValidator = currentSubordinatedValidator.getNextValidator(); } while (currentSubordinatedValidator != null); return firstReport; } @Override protected SignatureScopeFinder getSignatureScopeFinder() { return null; } /** * This is an experimental implementation for Aho's contribution. It is likely to be changed. * * @return {@code List} of {@code AdvancedSignature} within the container */ @Override public List<AdvancedSignature> getSignatures() { if (signatures == null) { return null; } if (validatedSignatures != null) { return validatedSignatures; } validatedSignatures = new ArrayList<AdvancedSignature>(); DocumentValidator currentSubordinatedValidator = subordinatedValidator; do { final List<AdvancedSignature> signatures = currentSubordinatedValidator.getSignatures(); for (final AdvancedSignature signature : signatures) { validatedSignatures.add(signature); } currentSubordinatedValidator = currentSubordinatedValidator.getNextValidator(); } while (currentSubordinatedValidator != null); return validatedSignatures; } /** * This is an experimental implementation for Aho's contribution. It is likely to be changed. The current implementation does not work with CAdES signatures. * * @param signatureId the id of the signature to be removed. * @return the {@code DSSDocument} with removed given signature * @throws DSSException */ @Override public DSSDocument removeSignature(final String signatureId) throws DSSException { if (DSSUtils.isBlank(signatureId)) { throw new DSSNullException(String.class, "signatureId"); } for (int i = 0; i < signatures.size(); i++) { final DSSDocument signature = signatures.get(i); final Document root = DSSXMLUtils.buildDOM(signature); final Element signatureEl = (Element) root.getDocumentElement().getFirstChild(); final String idIdentifier = DSSXMLUtils.getIDIdentifier(signatureEl); if (signatureId.equals(idIdentifier)) { signatures.remove(i); final Document signatureDOM = DSSXMLUtils.createDocument(ASiCNamespaces.ASiC, ASiCService.ASICS_NS); for (int j = 0; j < signatures.size(); j++) { final Document doc = DSSXMLUtils.buildDOM(signature); final Node signatureElement = doc.getDocumentElement().getFirstChild(); final Element newElement = signatureDOM.getDocumentElement(); signatureDOM.adoptNode(signatureElement); newElement.appendChild(signatureElement); } return new InMemoryDocument(DSSXMLUtils.serializeNode(signatureDOM)); } } throw new DSSException("The signature with the given id was not found!"); } }