/** * 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.pdf.pdfbox; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import eu.europa.esig.dss.DSSASN1Utils; import eu.europa.esig.dss.DSSDocument; import eu.europa.esig.dss.DSSException; import eu.europa.esig.dss.DSSUtils; import eu.europa.esig.dss.DigestAlgorithm; import eu.europa.esig.dss.pades.PAdESSignatureParameters; import eu.europa.esig.dss.pades.SignatureImageParameters; import eu.europa.esig.dss.pades.signature.visible.ImageAndResolution; import eu.europa.esig.dss.pades.signature.visible.ImageUtils; import eu.europa.esig.dss.pdf.DSSDictionaryCallback; import eu.europa.esig.dss.pdf.PDFSignatureService; import eu.europa.esig.dss.pdf.PdfDict; import eu.europa.esig.dss.pdf.PdfDssDict; import eu.europa.esig.dss.pdf.PdfSignatureInfo; import eu.europa.esig.dss.pdf.PdfSignatureOrDocTimestampInfo; import eu.europa.esig.dss.pdf.PdfSignatureOrDocTimestampInfoComparator; import eu.europa.esig.dss.pdf.SignatureValidationCallback; import eu.europa.esig.dss.utils.Utils; import eu.europa.esig.dss.x509.CertificatePool; import eu.europa.esig.dss.x509.CertificateToken; import eu.europa.esig.dss.x509.Token; import eu.europa.esig.dss.x509.crl.CRLToken; import eu.europa.esig.dss.x509.ocsp.OCSPToken; class PdfBoxSignatureService implements PDFSignatureService { private static final Logger logger = LoggerFactory.getLogger(PdfBoxSignatureService.class); @Override public byte[] digest(final InputStream toSignDocument, final PAdESSignatureParameters parameters, final DigestAlgorithm digestAlgorithm) throws DSSException { final byte[] signatureValue = DSSUtils.EMPTY_BYTE_ARRAY; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PDDocument pdDocument = null; try { pdDocument = PDDocument.load(toSignDocument); PDSignature pdSignature = createSignatureDictionary(parameters); return signDocumentAndReturnDigest(parameters, signatureValue, outputStream, pdDocument, pdSignature, digestAlgorithm); } catch (IOException e) { throw new DSSException(e); } finally { Utils.closeQuietly(pdDocument); Utils.closeQuietly(outputStream); } } @Override public void sign(final InputStream pdfData, final byte[] signatureValue, final OutputStream signedStream, final PAdESSignatureParameters parameters, final DigestAlgorithm digestAlgorithm) throws DSSException { PDDocument pdDocument = null; try { pdDocument = PDDocument.load(pdfData); final PDSignature pdSignature = createSignatureDictionary(parameters); signDocumentAndReturnDigest(parameters, signatureValue, signedStream, pdDocument, pdSignature, digestAlgorithm); } catch (IOException e) { throw new DSSException(e); } finally { Utils.closeQuietly(pdDocument); } } private byte[] signDocumentAndReturnDigest(final PAdESSignatureParameters parameters, final byte[] signatureBytes, final OutputStream fileOutputStream, final PDDocument pdDocument, final PDSignature pdSignature, final DigestAlgorithm digestAlgorithm) throws DSSException { SignatureOptions options = new SignatureOptions(); try { final MessageDigest digest = DSSUtils.getMessageDigest(digestAlgorithm); // register signature dictionary and sign interface SignatureInterface signatureInterface = new SignatureInterface() { @Override public byte[] sign(InputStream content) throws IOException { byte[] b = new byte[4096]; int count; while ((count = content.read(b)) > 0) { digest.update(b, 0, count); } return signatureBytes; } }; options.setPreferredSignatureSize(parameters.getSignatureSize()); if (parameters.getImageParameters() != null) { fillImageParameters(pdDocument, parameters.getImageParameters(), options); } pdDocument.addSignature(pdSignature, signatureInterface, options); saveDocumentIncrementally(parameters, fileOutputStream, pdDocument); final byte[] digestValue = digest.digest(); if (logger.isDebugEnabled()) { logger.debug("Digest to be signed: " + Utils.toHex(digestValue)); } return digestValue; } catch (IOException e) { throw new DSSException(e); } finally { Utils.closeQuietly(options.getVisualSignature()); } } private void fillImageParameters(final PDDocument doc, final SignatureImageParameters imgParams, SignatureOptions options) throws IOException { // DSS-747. Using the DPI resolution to convert java size to dot ImageAndResolution ires = ImageUtils.create(imgParams); InputStream is = ires.getInputStream(); try { PDVisibleSignDesigner visibleSig = new PDVisibleSignDesigner(doc, is, imgParams.getPage()); visibleSig.xAxis(imgParams.getxAxis()).yAxis(imgParams.getyAxis()); visibleSig.width(ires.toXPoint(visibleSig.getWidth())).height(ires.toYPoint(visibleSig.getHeight())); visibleSig.zoom(imgParams.getZoom() - 100); // pdfbox is 0 based PDVisibleSigProperties signatureProperties = new PDVisibleSigProperties(); signatureProperties.visualSignEnabled(true).setPdVisibleSignature(visibleSig).buildSignature(); options.setVisualSignature(signatureProperties); options.setPage(imgParams.getPage() - 1); // DSS-1138 } finally { Utils.closeQuietly(is); } } private PDSignature createSignatureDictionary(final PAdESSignatureParameters parameters) { final PDSignature signature = new PDSignature(); signature.setType(getType()); // signature.setName(String.format("SD-DSS Signature %s", parameters.getDeterministicId())); Date date = parameters.bLevel().getSigningDate(); String encodedDate = " " + Utils.toHex(DSSUtils.digest(DigestAlgorithm.SHA1, Long.toString(date.getTime()).getBytes())); CertificateToken token = parameters.getSigningCertificate(); if (token == null) { signature.setName("Unknown signer" + encodedDate); } else { String shortName = DSSASN1Utils.getHumanReadableName(parameters.getSigningCertificate()) + encodedDate; signature.setName(shortName); } signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter // sub-filter for basic and PAdES Part 2 signatures signature.setSubFilter(getSubFilter()); if (COSName.SIG.equals(getType())) { if (Utils.isStringNotEmpty(parameters.getContactInfo())) { signature.setContactInfo(parameters.getContactInfo()); } if (Utils.isStringNotEmpty(parameters.getLocation())) { signature.setLocation(parameters.getLocation()); } if (Utils.isStringNotEmpty(parameters.getReason())) { signature.setReason(parameters.getReason()); } } // 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; } protected COSName getType() { return COSName.SIG; } public void saveDocumentIncrementally(PAdESSignatureParameters parameters, OutputStream outputStream, PDDocument pdDocument) throws DSSException { try { // 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(outputStream); } catch (IOException e) { throw new DSSException(e); } } protected COSName getSubFilter() { return PDSignature.SUBFILTER_ETSI_CADES_DETACHED; } @Override public void validateSignatures(CertificatePool validationCertPool, DSSDocument document, SignatureValidationCallback callback) throws DSSException { // recursive search of signature InputStream inputStream = document.openStream(); try { List<PdfSignatureOrDocTimestampInfo> signaturesFound = getSignatures(validationCertPool, Utils.toByteArray(inputStream)); for (PdfSignatureOrDocTimestampInfo pdfSignatureOrDocTimestampInfo : signaturesFound) { callback.validate(pdfSignatureOrDocTimestampInfo); } } catch (IOException e) { logger.error("Cannot validate signatures : " + e.getMessage(), e); } Utils.closeQuietly(inputStream); } private List<PdfSignatureOrDocTimestampInfo> getSignatures(CertificatePool validationCertPool, byte[] originalBytes) { List<PdfSignatureOrDocTimestampInfo> signatures = new ArrayList<PdfSignatureOrDocTimestampInfo>(); PDDocument doc = null; try { doc = PDDocument.load(originalBytes); List<PDSignature> pdSignatures = doc.getSignatureDictionaries(); if (Utils.isCollectionNotEmpty(pdSignatures)) { logger.debug("{} signature(s) found", pdSignatures.size()); PdfDict catalog = new PdfBoxDict(doc.getDocumentCatalog().getCOSObject(), doc); PdfDssDict dssDictionary = PdfDssDict.extract(catalog); for (PDSignature signature : pdSignatures) { String subFilter = signature.getSubFilter(); byte[] cms = signature.getContents(originalBytes); if (Utils.isStringEmpty(subFilter) || Utils.isArrayEmpty(cms)) { logger.warn("Wrong signature with empty subfilter or cms."); continue; } byte[] signedContent = signature.getSignedContent(originalBytes); int[] byteRange = signature.getByteRange(); PdfSignatureOrDocTimestampInfo signatureInfo = null; if (PdfBoxDocTimeStampService.SUB_FILTER_ETSI_RFC3161.getName().equals(subFilter)) { boolean isArchiveTimestamp = false; // LT or LTA if (dssDictionary != null) { // check is DSS dictionary already exist if (isDSSDictionaryPresentInPreviousRevision(getOriginalBytes(byteRange, signedContent))) { isArchiveTimestamp = true; } } signatureInfo = new PdfBoxDocTimestampInfo(validationCertPool, signature, dssDictionary, cms, signedContent, isArchiveTimestamp); } else { signatureInfo = new PdfBoxSignatureInfo(validationCertPool, signature, dssDictionary, cms, signedContent); } if (signatureInfo != null) { signatures.add(signatureInfo); } } Collections.sort(signatures, new PdfSignatureOrDocTimestampInfoComparator()); linkSignatures(signatures); for (PdfSignatureOrDocTimestampInfo sig : signatures) { logger.debug("Signature " + sig.uniqueId() + " found with byteRange " + Arrays.toString(sig.getSignatureByteRange()) + " (" + sig.getSubFilter() + ")"); } } } catch (Exception e) { logger.warn("Cannot analyze signatures : " + e.getMessage(), e); } finally { Utils.closeQuietly(doc); } return signatures; } /** * This method links previous signatures to the new one. This is useful to get revision number and to know if a TSP * is over the DSS dictionary */ private void linkSignatures(List<PdfSignatureOrDocTimestampInfo> signatures) { List<PdfSignatureOrDocTimestampInfo> previousList = new ArrayList<PdfSignatureOrDocTimestampInfo>(); for (PdfSignatureOrDocTimestampInfo sig : signatures) { if (Utils.isCollectionNotEmpty(previousList)) { for (PdfSignatureOrDocTimestampInfo previous : previousList) { previous.addOuterSignature(sig); } } previousList.add(sig); } } private boolean isDSSDictionaryPresentInPreviousRevision(byte[] originalBytes) { PDDocument doc = null; PdfDssDict dssDictionary = null; try { doc = PDDocument.load(originalBytes); List<PDSignature> pdSignatures = doc.getSignatureDictionaries(); if (Utils.isCollectionNotEmpty(pdSignatures)) { PdfDict catalog = new PdfBoxDict(doc.getDocumentCatalog().getCOSObject(), doc); dssDictionary = PdfDssDict.extract(catalog); } } catch (Exception e) { logger.warn("Cannot check in previous revisions if DSS dictionary already exist : " + e.getMessage(), e); } finally { Utils.closeQuietly(doc); } return dssDictionary != null; } private byte[] getOriginalBytes(int[] byteRange, byte[] signedContent) { final int length = byteRange[1]; final byte[] result = new byte[length]; System.arraycopy(signedContent, 0, result, 0, length); return result; } @Override public void addDssDictionary(InputStream inputStream, OutputStream outputStream, List<DSSDictionaryCallback> callbacks) { PDDocument pdDocument = null; try { pdDocument = PDDocument.load(inputStream); if (Utils.isCollectionNotEmpty(callbacks)) { final COSDictionary cosDictionary = pdDocument.getDocumentCatalog().getCOSObject(); cosDictionary.setItem("DSS", buildDSSDictionary(callbacks)); cosDictionary.setNeedToBeUpdated(true); } if (pdDocument.getDocumentId() == null) { pdDocument.setDocumentId(0L); } pdDocument.saveIncremental(outputStream); } catch (Exception e) { throw new DSSException(e); } finally { Utils.closeQuietly(pdDocument); } } private COSDictionary buildDSSDictionary(List<DSSDictionaryCallback> callbacks) throws Exception { COSDictionary dss = new COSDictionary(); Map<String, COSStream> streams = new HashMap<String, COSStream>(); Set<CRLToken> allCrls = new HashSet<CRLToken>(); Set<OCSPToken> allOcsps = new HashSet<OCSPToken>(); Set<CertificateToken> allCertificates = new HashSet<CertificateToken>(); COSDictionary vriDictionary = new COSDictionary(); for (DSSDictionaryCallback callback : callbacks) { COSDictionary sigVriDictionary = new COSDictionary(); sigVriDictionary.setDirect(true); if (Utils.isCollectionNotEmpty(callback.getCertificates())) { COSArray vriCertArray = new COSArray(); for (CertificateToken token : callback.getCertificates()) { vriCertArray.add(getStream(streams, token)); allCertificates.add(token); } sigVriDictionary.setItem("Cert", vriCertArray); } if (Utils.isCollectionNotEmpty(callback.getOcsps())) { COSArray vriOcspArray = new COSArray(); for (OCSPToken token : callback.getOcsps()) { vriOcspArray.add(getStream(streams, token)); allOcsps.add(token); } sigVriDictionary.setItem("OCSP", vriOcspArray); } if (Utils.isCollectionNotEmpty(callback.getCrls())) { COSArray vriCrlArray = new COSArray(); for (CRLToken token : callback.getCrls()) { vriCrlArray.add(getStream(streams, token)); allCrls.add(token); } sigVriDictionary.setItem("CRL", vriCrlArray); } // We can't use CMSSignedData, the pdSignature content is trimmed (000000) PdfSignatureInfo pdfSignatureInfo = callback.getSignature().getPdfSignatureInfo(); final byte[] digest = DSSUtils.digest(DigestAlgorithm.SHA1, pdfSignatureInfo.getContent()); String hexHash = Utils.toHex(digest).toUpperCase(); vriDictionary.setItem(hexHash, sigVriDictionary); } dss.setItem("VRI", vriDictionary); if (Utils.isCollectionNotEmpty(allCertificates)) { COSArray arrayAllCerts = new COSArray(); for (CertificateToken token : allCertificates) { arrayAllCerts.add(getStream(streams, token)); } dss.setItem("Certs", arrayAllCerts); } if (Utils.isCollectionNotEmpty(allOcsps)) { COSArray arrayAllOcsps = new COSArray(); for (OCSPToken token : allOcsps) { arrayAllOcsps.add(getStream(streams, token)); } dss.setItem("OCSPs", arrayAllOcsps); } if (Utils.isCollectionNotEmpty(allCrls)) { COSArray arrayAllCrls = new COSArray(); for (CRLToken token : allCrls) { arrayAllCrls.add(getStream(streams, token)); } dss.setItem("CRLs", arrayAllCrls); } return dss; } private COSStream getStream(Map<String, COSStream> streams, Token token) throws IOException { COSStream stream = streams.get(token.getDSSIdAsString()); if (stream == null) { stream = new COSStream(); OutputStream unfilteredStream = stream.createOutputStream(); unfilteredStream.write(token.getEncoded()); unfilteredStream.flush(); unfilteredStream.close(); streams.put(token.getDSSIdAsString(), stream); } return stream; } }