/*
* 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.signature.pdf.pdfbox;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.exceptions.SignatureException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.europa.ec.markt.dss.DSSPDFUtils;
import eu.europa.ec.markt.dss.DSSUtils;
import eu.europa.ec.markt.dss.DigestAlgorithm;
import eu.europa.ec.markt.dss.exception.DSSException;
import eu.europa.ec.markt.dss.parameter.SignatureParameters;
import eu.europa.ec.markt.dss.signature.pdf.PDFSignatureService;
import eu.europa.ec.markt.dss.signature.pdf.PdfDict;
import eu.europa.ec.markt.dss.signature.pdf.PdfDocTimestampInfo;
import eu.europa.ec.markt.dss.signature.pdf.PdfSignatureOrDocTimestampInfo;
import eu.europa.ec.markt.dss.signature.pdf.SignatureValidationCallback;
import eu.europa.ec.markt.dss.validation102853.CertificatePool;
import eu.europa.ec.markt.dss.validation102853.TimestampType;
class PdfBoxSignatureService implements PDFSignatureService {
private static final Logger LOG = LoggerFactory.getLogger(PdfBoxSignatureService.class);
@Override
public byte[] digest(final InputStream toSignDocument, final SignatureParameters parameters, final DigestAlgorithm digestAlgorithm,
final Map.Entry<String, PdfDict>... extraDictionariesToAddBeforeSign) throws DSSException {
final byte[] signatureValue = DSSUtils.EMPTY_BYTE_ARRAY;
File toSignFile = null;
File signedFile = null;
PDDocument pdDocument = null;
try {
toSignFile = DSSPDFUtils.getFileFromPdfData(toSignDocument);
pdDocument = PDDocument.load(toSignFile);
addExtraDictionaries(pdDocument, extraDictionariesToAddBeforeSign);
PDSignature pdSignature = createSignatureDictionary(parameters);
signedFile = File.createTempFile("sd-dss-", "-signed.pdf");
final FileOutputStream fileOutputStream = DSSPDFUtils.getFileOutputStream(toSignFile, signedFile);
final byte[] digestValue = signDocumentAndReturnDigest(parameters, signatureValue, signedFile, fileOutputStream, pdDocument, pdSignature, digestAlgorithm);
return digestValue;
} catch (IOException e) {
throw new DSSException(e);
} finally {
DSSUtils.delete(toSignFile);
DSSUtils.delete(signedFile);
DSSPDFUtils.close(pdDocument);
}
}
@Override
public void sign(final InputStream pdfData, final byte[] signatureValue, final OutputStream signedStream, final SignatureParameters parameters,
final DigestAlgorithm digestAlgorithm, final Map.Entry<String, PdfDict>... extraDictionariesToAddBeforeSign) throws DSSException {
File toSignFile = null;
File signedFile = null;
FileInputStream fileInputStream = null;
FileInputStream finalFileInputStream = null;
PDDocument pdDocument = null;
try {
toSignFile = DSSPDFUtils.getFileFromPdfData(pdfData);
pdDocument = PDDocument.load(toSignFile);
addExtraDictionaries(pdDocument, extraDictionariesToAddBeforeSign);
final PDSignature pdSignature = createSignatureDictionary(parameters);
signedFile = File.createTempFile("sd-dss-", "-signed.pdf");
final FileOutputStream fileOutputStream = DSSPDFUtils.getFileOutputStream(toSignFile, signedFile);
signDocumentAndReturnDigest(parameters, signatureValue, signedFile, fileOutputStream, pdDocument, pdSignature, digestAlgorithm);
finalFileInputStream = new FileInputStream(signedFile);
DSSUtils.copy(finalFileInputStream, signedStream);
} catch (IOException e) {
throw new DSSException(e);
} finally {
DSSUtils.closeQuietly(fileInputStream);
DSSUtils.closeQuietly(finalFileInputStream);
DSSUtils.delete(toSignFile);
DSSUtils.delete(signedFile);
DSSPDFUtils.close(pdDocument);
}
}
private byte[] signDocumentAndReturnDigest(final SignatureParameters parameters, final byte[] signatureBytes, final File signedFile, final FileOutputStream fileOutputStream,
final PDDocument pdDocument, final PDSignature pdSignature, final DigestAlgorithm digestAlgorithm) throws DSSException {
try {
final MessageDigest digest = DSSUtils.getMessageDigest(digestAlgorithm);
// register signature dictionary and sign interface
SignatureInterface signatureInterface = new SignatureInterface() {
@Override
public byte[] sign(InputStream content) throws SignatureException, IOException {
byte[] b = new byte[4096];
int count;
while ((count = content.read(b)) > 0) {
digest.update(b, 0, count);
}
return signatureBytes;
}
};
pdDocument.addSignature(pdSignature, signatureInterface);
saveDocumentIncrementally(parameters, signedFile, fileOutputStream, pdDocument);
final byte[] digestValue = digest.digest();
if (LOG.isDebugEnabled()) {
LOG.debug("Digest to be signed: " + DSSUtils.encodeHexString(digestValue));
}
fileOutputStream.close();
return digestValue;
} catch (NoSuchAlgorithmException e) {
throw new DSSException(e);
} catch (IOException e) {
throw new DSSException(e);
} catch (SignatureException e) {
throw new DSSException(e);
}
}
private void addExtraDictionaries(final PDDocument doc, final Map.Entry<String, PdfDict>[] extraDictionariesToAddBeforeSign) {
final COSDictionary cosDictionary = doc.getDocumentCatalog().getCOSDictionary();
for (final Map.Entry<String, PdfDict> pdfDictEntry : extraDictionariesToAddBeforeSign) {
final String key = pdfDictEntry.getKey();
final PdfBoxDict value = (PdfBoxDict) pdfDictEntry.getValue();
final COSDictionary wrapped = value.getWrapped();
cosDictionary.setItem(key, wrapped);
}
}
private PDSignature createSignatureDictionary(final SignatureParameters parameters) {
final PDSignature signature = new PDSignature();
signature.setName(String.format("SD-DSS Signature %s", parameters.getDeterministicId()));
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter
// sub-filter for basic and PAdES Part 2 signatures
signature.setSubFilter(getSubFilter());
// the signing date, needed for valid signature
final Calendar cal = Calendar.getInstance();
final Date signingDate = parameters.bLevel().getSigningDate();
cal.setTime(signingDate);
signature.setSignDate(cal);
return signature;
}
public static void saveDocumentIncrementally(SignatureParameters parameters, File signedFile, FileOutputStream fileOutputStream, PDDocument pdDocument) throws DSSException {
FileInputStream signedFileInputStream = null;
try {
signedFileInputStream = new FileInputStream(signedFile);
// the document needs to have an ID, if not a ID based on the current system time is used, and then the digest of the signed data is different
if (pdDocument.getDocumentId() == null) {
final byte[] documentIdBytes = DSSUtils.digest(DigestAlgorithm.MD5, parameters.bLevel().getSigningDate().toString().getBytes());
pdDocument.setDocumentId(DSSUtils.toLong(documentIdBytes));
pdDocument.setDocumentId(0L);
}
pdDocument.saveIncremental(signedFileInputStream, fileOutputStream);
} catch (IOException e) {
throw new DSSException(e);
} catch (COSVisitorException e) {
throw new DSSException(e);
} finally {
DSSUtils.closeQuietly(signedFileInputStream);
}
}
protected COSName getSubFilter() {
return PDSignature.SUBFILTER_ETSI_CADES_DETACHED;
}
@Override
public void validateSignatures(CertificatePool validationCertPool, InputStream input, SignatureValidationCallback callback) throws DSSException {
// recursive search of signature
Map<String, Map<PdfSignatureOrDocTimestampInfo, Boolean>> byteRangeMap = new HashMap<String, Map<PdfSignatureOrDocTimestampInfo, Boolean>>();
final Map<PdfSignatureOrDocTimestampInfo, Boolean> signaturesFound = validateSignatures(validationCertPool, byteRangeMap, null, input);
for (PdfSignatureOrDocTimestampInfo pdfSignatureOrDocTimestampInfo : signaturesFound.keySet()) {
callback.validate(pdfSignatureOrDocTimestampInfo);
}
}
/* This is O(scary), but seems quick enough in practice. */
/**
* @param validationCertPool
* @param byteRangeMap
* @param outerCatalog the PdfDictionary of the document that enclose the document stored in the input InputStream
* @param input the Pdf bytes to open as a PDF
* @return
* @throws DSSException
*/
private Map<PdfSignatureOrDocTimestampInfo, Boolean> validateSignatures(CertificatePool validationCertPool,
Map<String, Map<PdfSignatureOrDocTimestampInfo, Boolean>> byteRangeMap, PdfDict outerCatalog,
InputStream input) throws DSSException {
Map<PdfSignatureOrDocTimestampInfo, Boolean> signaturesFound = new LinkedHashMap<PdfSignatureOrDocTimestampInfo, Boolean>();
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
PDDocument doc = null;
try {
DSSUtils.copy(input, buffer);
doc = PDDocument.load(new ByteArrayInputStream(buffer.toByteArray()));
final PdfDict catalog = new PdfBoxDict(doc.getDocumentCatalog().getCOSDictionary(), doc);
final List<PDSignature> signatureDictionaries = doc.getSignatureDictionaries();
if (LOG.isDebugEnabled()) {
LOG.debug("Found {} signatures in PDF dictionary of PDF sized {} bytes", signatureDictionaries.size(), buffer.size());
}
for (int i = 0; i < signatureDictionaries.size(); i++) {
final PDSignature signature = signatureDictionaries.get(i);
/**
* SubFilter Name (Required) The value of SubFilter identifies the format of the data contained in the stream.
* A conforming reader may use any conforming signature handler that supports the specified format.
* When the value of Type is DocTimestamp, the value of SubFilter shall be ETSI.RFC3161.
*/
final String subFilter = signature.getSubFilter();
if (DSSUtils.isBlank(subFilter)) {
LOG.warn("No signature found in signature Dictionary:Content, SUB_FILTER is empty!");
continue;
}
byte[] cms = new PdfBoxDict(signature.getDictionary(), doc).get("Contents");
PdfSignatureOrDocTimestampInfo signatureInfo;
try {
if (PdfBoxDocTimeStampService.SUB_FILTER_ETSI_RFC3161.getName().equals(subFilter)) {
signatureInfo = PdfSignatureFactory.createPdfTimestampInfo(validationCertPool, outerCatalog, doc, signature, cms, buffer);
} else {
signatureInfo = PdfSignatureFactory.createPdfSignatureInfo(validationCertPool, outerCatalog, doc, signature, cms, buffer);
}
} catch (PdfSignatureOrDocTimestampInfo.DSSPadesNoSignatureFound e) {
LOG.debug("No signature found in signature Dictionary:Content", e);
continue;
}
signatureInfo = signatureAlreadyInListOrSelf(signaturesFound, signatureInfo);
// should store in memory this byte range with a list of signature found there
final String byteRange = Arrays.toString(signature.getByteRange());
Map<PdfSignatureOrDocTimestampInfo, Boolean> innerSignaturesFound = byteRangeMap.get(byteRange);
if (innerSignaturesFound == null) {
// Recursive call to find inner signatures in the byte range covered by this signature. Deep first search.
final byte[] originalBytes = signatureInfo.getOriginalBytes();
if (LOG.isDebugEnabled()) {
LOG.debug("Searching signature in the previous revision of the document, size of revision is {} bytes", originalBytes.length);
}
innerSignaturesFound = validateSignatures(validationCertPool, byteRangeMap, catalog, new ByteArrayInputStream(originalBytes));
byteRangeMap.put(byteRange, innerSignaturesFound);
}
// need to mark a signature as included inside another one. It's needed to link timestamp signature with the signatures covered by the timestamp.
for (PdfSignatureOrDocTimestampInfo innerSignature : innerSignaturesFound.keySet()) {
innerSignature = signatureAlreadyInListOrSelf(signaturesFound, innerSignature);
signaturesFound.put(innerSignature, true);
innerSignature.addOuterSignature(signatureInfo);
}
signaturesFound.put(signatureInfo, true);
}
} catch (IOException up) {
LOG.error("Error loading buffer of size {}", buffer.size(), up);
// ignore error when loading signatures
} finally {
DSSPDFUtils.close(doc);
}
return signaturesFound;
}
/**
* This method is needed because we will encounter many times the same signature during our document analysis.
* We make sure that we always add it only once.
*
* @param signaturesFound
* @param pdfSignatureOrDocTimestampInfo
* @return
*/
public static PdfSignatureOrDocTimestampInfo signatureAlreadyInListOrSelf(Map<PdfSignatureOrDocTimestampInfo, Boolean> signaturesFound,
final PdfSignatureOrDocTimestampInfo pdfSignatureOrDocTimestampInfo) {
final int uniqueId = pdfSignatureOrDocTimestampInfo.uniqueId();
for (final PdfSignatureOrDocTimestampInfo existingSignature : signaturesFound.keySet()) {
if (existingSignature.uniqueId() == uniqueId) {
if (existingSignature instanceof PdfDocTimestampInfo) {
final PdfDocTimestampInfo existingSignatureDocTimestamp = (PdfDocTimestampInfo) existingSignature;
if (existingSignatureDocTimestamp.getTimestampToken().getTimeStampType() == TimestampType.SIGNATURE_TIMESTAMP) {
if (LOG.isDebugEnabled()) {
LOG.debug("Signature was already found in the external doc. Returning newly (inner) found signature {} {}",
existingSignature.getClass().getSimpleName(), uniqueId);
}
return existingSignatureDocTimestamp;
}
}
for (final PdfSignatureOrDocTimestampInfo outerSignature : existingSignature.getOuterSignatures().keySet()) {
pdfSignatureOrDocTimestampInfo.addOuterSignature(outerSignature);
}
signaturesFound.remove(existingSignature);
signaturesFound.put(pdfSignatureOrDocTimestampInfo, true);
if (LOG.isDebugEnabled()) {
LOG.debug("Signature was already found in the external doc. Returning newly (inner) found signature {} {}",
pdfSignatureOrDocTimestampInfo.getClass().getSimpleName(), uniqueId);
}
return pdfSignatureOrDocTimestampInfo;
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Signature newly found {} {}", pdfSignatureOrDocTimestampInfo.getClass().getSimpleName(), uniqueId);
}
return pdfSignatureOrDocTimestampInfo;
}
}