org.signserver.module.pdfsigner.PDFSigner.java Source code

Java tutorial

Introduction

Here is the source code for org.signserver.module.pdfsigner.PDFSigner.java

Source

/*************************************************************************
 *                                                                       *
 *  SignServer: The OpenSource Automated Signing Server                  *
 *                                                                       *
 *  This software 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 any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/
package org.signserver.module.pdfsigner;

import com.lowagie.text.DocumentException;
import com.lowagie.text.exceptions.BadPasswordException;
import com.lowagie.text.pdf.*;

import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.naming.NamingException;
import javax.persistence.EntityManager;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.signserver.common.*;
import org.signserver.ejb.interfaces.IInternalWorkerSession;
import org.signserver.server.UsernamePasswordClientCredential;
import org.signserver.server.WorkerContext;
import org.signserver.server.archive.Archivable;
import org.signserver.server.archive.DefaultArchivable;
import org.signserver.server.cryptotokens.ICryptoInstance;
import org.signserver.server.cryptotokens.ICryptoToken;
import org.signserver.server.log.IWorkerLogger;
import org.signserver.server.log.LogMap;
import org.signserver.server.signers.BaseSigner;
import org.signserver.server.statistics.Event;
import org.signserver.validationservice.server.ValidationUtils;

/**
 * A Signer signing PDF files using the IText PDF library.
 *
 * Implements a ISigner and have the following properties: REASON = The reason
 * shown in the PDF signature LOCATION = The location shown in the PDF signature
 * RECTANGLE = The location of the visible signature field (llx, lly, urx, ury)
 *
 * TSA_URL = The URL of the timestamp authority TSA_USERNAME = Account
 * (username) of the TSA TSA_PASSWORD = Password for TSA
 *
 * CERTIFICATION_LEVEL = The level of certification for the document.
 * NOT_CERTIFIED, FORM_FILLING_AND_ANNOTATIONS, FORM_FILLING or NOT_CERTIFIED
 * (default: NOT_CERTIFIED).
 *
 * REFUSE_DOUBLE_INDIRECT_OBJECTS = True if documents with multiple indirect
 * objects with the same object number and generation number pair should be
 * refused. Used to mitigate a collision signature vulnerability described in 
 * http://pdfsig-collision.florz.de/
 *
 * REJECT_PERMISSIONS: Comma separated list of permissions for which SignServer 
 * will refuse to  sign the document if present. See Permissions for available 
 * permission names.
 *
 * @author Tomas Gustavsson
 * @author Aziz Gktepe
 * @author Markus Kils
 * @version $Id: PDFSigner.java 5977 2015-03-27 10:30:50Z netmackan $
 */
public class PDFSigner extends BaseSigner {

    /** Logger for this class. */
    public static final Logger LOG = Logger.getLogger(PDFSigner.class);

    // private final CSVFileStatisticsCollector cSVFileStatisticsCollector =
    // CSVFileStatisticsCollector.getInstance(this.getClass().getName(),
    // "PDF size in bytes");

    // Configuration Property constants
    // signature properties
    public static final String REASON = "REASON";
    public static final String REASONDEFAULT = "Signed by SignServer";
    public static final String LOCATION = "LOCATION";
    public static final String LOCATIONDEFAULT = "SignServer";

    // properties that control signature visibility
    public static final String ADD_VISIBLE_SIGNATURE = "ADD_VISIBLE_SIGNATURE";
    public static final boolean ADD_VISIBLE_SIGNATURE_DEFAULT = false;
    public static final String VISIBLE_SIGNATURE_PAGE = "VISIBLE_SIGNATURE_PAGE";
    public static final String VISIBLE_SIGNATURE_PAGE_DEFAULT = "First";
    public static final String VISIBLE_SIGNATURE_RECTANGLE = "VISIBLE_SIGNATURE_RECTANGLE";
    public static final String VISIBLE_SIGNATURE_RECTANGLE_DEFAULT = "400,700,500,800";
    public static final String VISIBLE_SIGNATURE_CUSTOM_IMAGE_BASE64 = "VISIBLE_SIGNATURE_CUSTOM_IMAGE_BASE64";
    public static final String VISIBLE_SIGNATURE_CUSTOM_IMAGE_PATH = "VISIBLE_SIGNATURE_CUSTOM_IMAGE_PATH";
    public static final String VISIBLE_SIGNATURE_CUSTOM_IMAGE_SCALE_TO_RECTANGLE = "VISIBLE_SIGNATURE_CUSTOM_IMAGE_RESIZE_TO_RECTANGLE";
    public static final boolean VISIBLE_SIGNATURE_CUSTOM_IMAGE_SCALE_TO_RECTANGLE_DEFAULT = true;
    public static final String CERTIFICATION_LEVEL = "CERTIFICATION_LEVEL";
    public static final int CERTIFICATION_LEVEL_DEFAULT = PdfSignatureAppearance.NOT_CERTIFIED;

    // properties that control timestamping of signature
    public static final String TSA_URL = "TSA_URL";
    public static final String TSA_USERNAME = "TSA_USERNAME";
    public static final String TSA_PASSWORD = "TSA_PASSWORD";
    public static final String TSA_WORKER = "TSA_WORKER";

    // extra properties
    public static final String EMBED_CRL = "EMBED_CRL";
    public static final boolean EMBED_CRL_DEFAULT = false;
    public static final String EMBED_OCSP_RESPONSE = "EMBED_OCSP_RESPONSE";
    public static final boolean EMBED_OCSP_RESPONSE_DEFAULT = false;

    /** Used to mitigate a collision signature vulnerability described in http://pdfsig-collision.florz.de/ */
    public static final String REFUSE_DOUBLE_INDIRECT_OBJECTS = "REFUSE_DOUBLE_INDIRECT_OBJECTS";

    // Permissions properties
    /** List of permissions for which SignServer will refuse to sign the document if present. **/
    public static final String REJECT_PERMISSIONS = "REJECT_PERMISSIONS";
    /** List of permissions to set (all other are cleared). **/
    public static final String SET_PERMISSIONS = "SET_PERMISSIONS";
    /** List of permissions to remove (all other existing permissions are left untouched). **/
    public static final String REMOVE_PERMISSIONS = "REMOVE_PERMISSIONS";
    /** Future property with list of permissions to add). **/
    // public static final String ADD_PERMISSIONS = "ADD_PERMISSIONS";
    /** Password to set as owner password. */
    public static final String SET_OWNERPASSWORD = "SET_OWNERPASSWORD";

    // archivetodisk properties
    public static final String PROPERTY_ARCHIVETODISK = "ARCHIVETODISK";
    public static final String PROPERTY_ARCHIVETODISK_PATH_BASE = "ARCHIVETODISK_PATH_BASE";
    public static final String PROPERTY_ARCHIVETODISK_PATH_PATTERN = "ARCHIVETODISK_PATH_PATTERN";
    public static final String PROPERTY_ARCHIVETODISK_FILENAME_PATTERN = "ARCHIVETODISK_FILENAME_PATTERN";

    public static final String DEFAULT_ARCHIVETODISK_PATH_PATTERN = "${DATE:yyyy/MM/dd}";
    public static final String DEFAULT_ARCHIVETODISK_FILENAME_PATTERN = "${WORKERID}-${REQUESTID}-${DATE:HHmmssSSS}.pdf";

    private static final String ARCHIVETODISK_PATTERN_REGEX = "\\$\\{(.+?)\\}";
    private static final String CONTENT_TYPE = "application/pdf";

    public static final String DIGESTALGORITHM = "DIGESTALGORITHM";
    private static final String DEFAULTDIGESTALGORITHM = "SHA1";

    private Pattern archivetodiskPattern;

    /** Random used for instance when setting a random owner/permissions password*/
    private SecureRandom random = new SecureRandom();

    private List<String> configErrors;

    private IInternalWorkerSession workerSession;

    private String digestAlgorithm = DEFAULTDIGESTALGORITHM;
    private int minimumPdfVersion;

    @Override
    public void init(int signerId, WorkerConfig config, WorkerContext workerContext,
            EntityManager workerEntityManager) {
        super.init(signerId, config, workerContext, workerEntityManager);

        configErrors = new LinkedList<String>();

        // Check properties for archive to disk
        if (StringUtils.equalsIgnoreCase("TRUE", config.getProperty(PROPERTY_ARCHIVETODISK))) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Archiving to disk");
            }

            final String path = config.getProperty(PROPERTY_ARCHIVETODISK_PATH_BASE);
            if (path == null) {
                LOG.warn("Worker[" + workerId + "]: Archiving path missing");
            } else if (!new File(path).exists()) {
                LOG.warn("Worker[" + workerId + "]: Archiving path does not exists: " + path);
            }
        }
        archivetodiskPattern = Pattern.compile(ARCHIVETODISK_PATTERN_REGEX);

        digestAlgorithm = config.getProperty(DIGESTALGORITHM, DEFAULTDIGESTALGORITHM);

        try {
            // calculate minimum PDF version based on digest algorithm
            minimumPdfVersion = getMinimumPdfVersion();
        } catch (IllegalArgumentException e) {
            configErrors.add("Illegal digest algorithm: " + digestAlgorithm);
        }

        // additionally check that at least one certificate is included, assumed by iText
        // (initIncludeCertificateLevels already checks non-negative values)
        if (hasSetIncludeCertificateLevels && includeCertificateLevels == 0) {
            configErrors.add("Illegal value for property " + WorkerConfig.PROPERTY_INCLUDE_CERTIFICATE_LEVELS
                    + ". Only numbers >= 1 supported.");
        }

        // check that TSA_URL and TSA_WORKER is not set at the same time
        if (config.getProperty(TSA_URL) != null && config.getProperty(TSA_WORKER) != null) {
            configErrors.add("Can not specify " + TSA_URL + " and " + TSA_WORKER + " at the same time.");
        }
    }

    @Override
    protected List<String> getCryptoTokenFatalErrors() {
        final List<String> errors = super.getCryptoTokenFatalErrors();

        // according to the PDF specification, only SHA1 is permitted as digest algorithm
        // for DSA public/private keys
        try {
            final ICryptoToken token = getCryptoToken();

            if (token != null) {
                final PublicKey pub = token.getPublicKey(ICryptoToken.PURPOSE_SIGN);
                final PrivateKey priv = token.getPrivateKey(ICryptoToken.PURPOSE_SIGN);

                if (pub instanceof DSAPublicKey || priv instanceof DSAPrivateKey) {
                    if (!"SHA1".equals(digestAlgorithm)) {
                        errors.add("Only SHA1 is permitted as digest algorithm for DSA public/private keys");
                    }
                }
            }
        } catch (CryptoTokenOfflineException e) { // NOPMD
            // In this case, we can't tell if the keys are DSA
            // appropriate crypto token errors should be handled by the base class
        } catch (SignServerException e) { // NOPMD
            // In this case, we can't tell if the keys are DSA
            // appropriate crypto token errors should be handled by the base class
        }

        return errors;
    }

    /**
     * The main method performing the actual signing operation. Expects the
     * signRequest to be a GenericSignRequest containing a signed PDF file.
     * 
     * @throws SignServerException
     * @see org.signserver.server.IProcessable#processData(org.signserver.common.ProcessRequest,
     *      org.signserver.common.RequestContext)
     */
    public ProcessResponse processData(ProcessRequest signRequest, RequestContext requestContext)
            throws IllegalRequestException, CryptoTokenOfflineException, SignServerException {
        // Check that the request contains a valid GenericSignRequest object
        // with a byte[].
        if (!(signRequest instanceof GenericSignRequest)) {
            throw new IllegalRequestException("Recieved request wasn't a expected GenericSignRequest.");
        }

        final ISignRequest sReq = (ISignRequest) signRequest;

        if (!(sReq.getRequestData() instanceof byte[])) {
            throw new IllegalRequestException("Recieved request data wasn't a expected byte[].");
        }

        // Log values
        final LogMap logMap = LogMap.getInstance(requestContext);

        // retrieve and preprocess configuration parameter values
        PDFSignerParameters params = new PDFSignerParameters(workerId, config);

        // Start processing the actual signature
        GenericSignResponse signResponse = null;
        byte[] pdfbytes = (byte[]) sReq.getRequestData();
        final String archiveId = createArchiveId(pdfbytes,
                (String) requestContext.get(RequestContext.TRANSACTION_ID));
        if (requestContext.get(RequestContext.STATISTICS_EVENT) != null) {
            Event event = (Event) requestContext.get(RequestContext.STATISTICS_EVENT);
            event.addCustomStatistics("PDFBYTES", pdfbytes.length);
        }

        ICryptoInstance crypto = null;
        try {
            crypto = acquireCryptoInstance(ICryptoToken.PURPOSE_SIGN, signRequest, requestContext);

            if (params.isRefuseDoubleIndirectObjects()) {
                checkForDuplicateObjects(pdfbytes);
            }

            // Get the password to open the PDF with
            final byte[] password = getPassword(requestContext);
            if (password == null || password.length == 0) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Password was null or empty");
                }
                logMap.put(IWorkerLogger.LOG_PDF_PASSWORD_SUPPLIED, Boolean.FALSE.toString());
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Password length was " + password.length + " bytes");
                }
                logMap.put(IWorkerLogger.LOG_PDF_PASSWORD_SUPPLIED, Boolean.TRUE.toString());
            }

            byte[] signedbytes = addSignatureToPDFDocument(crypto, params, pdfbytes, password, 0, signRequest,
                    requestContext);
            final Collection<? extends Archivable> archivables = Arrays
                    .asList(new DefaultArchivable(Archivable.TYPE_RESPONSE, CONTENT_TYPE, signedbytes, archiveId));

            if (signRequest instanceof GenericServletRequest) {
                signResponse = new GenericServletResponse(sReq.getRequestID(), signedbytes,
                        getSigningCertificate(signRequest, requestContext), archiveId, archivables, CONTENT_TYPE);
            } else {
                signResponse = new GenericSignResponse(sReq.getRequestID(), signedbytes,
                        getSigningCertificate(signRequest, requestContext), archiveId, archivables);
            }

            // Archive to disk
            if (StringUtils.equalsIgnoreCase("TRUE", config.getProperty(PROPERTY_ARCHIVETODISK))) {
                archiveToDisk(sReq, signedbytes, requestContext);
            }

            // The client can be charged for the request
            requestContext.setRequestFulfilledByWorker(true);
        } catch (DocumentException e) {
            throw new IllegalRequestException("Could not sign document: " + e.getMessage(), e);
        } catch (BadPasswordException ex) {
            throw new IllegalRequestException(
                    "A valid password is required to sign the document: " + ex.getMessage(), ex);
        } catch (UnsupportedEncodingException ex) {
            throw new IllegalRequestException("The supplied password could not be read: " + ex.getMessage(), ex);
        } catch (IOException e) {
            throw new IllegalRequestException("Could not sign document: " + e.getMessage(), e);
        } finally {
            releaseCryptoInstance(crypto, requestContext);
        }

        return signResponse;
    }

    /**
     * Calculates an estimate of the PKCS#7 structure size given the provided  
     * input parameters.
     *
     * Questions that we need to answer to construct an formula for calculating 
     * a good enough estimate:
     *
     * 1. What are the parameters influencing the PKCS#7 size?
     *    - static or depending on algorithms: PKCS#7 signature size, 
     *    - Certificates list
     *    - CRL list
     *    - OCSP bytes
     *    - timestamp response
     *
     * 2. How much does the size increase when the size of an certificate increases?
     *    - It appears to be at maximum the same increase in size
     *
     * 3. How much does the size increase for each new certificate, not including the certificate size?
     *    - 0. No increase for each certificate except the actual certificate size
     *
     * 4. How much does the size increase when the size of the timestamp responses increases?
     *    - It appears to be at maximum the same increase in size
     *    - However as the response is sent after the signing and possibly 
     *      from an external server we can not be sure about what size it 
     *      will have. We should use a large enough (but reasonable) value that 
     *      it is not so likely that we will have to do a second try.
     * 
     * 5. How much does the size increase when the size of an CRL increases?
     *    - It appears to be the same increase in size most of the times but in
     *      in one case it got 1 byte larger.
     *    - It turns out that the CRLs are included twice (!)
     *    
     * 6. How much does the size increase for each new CRL, not including the CRL size?
     *    - 0. No increase for each CRL except the actual CRL size
     *
     * 7. What is a typical size of an timestamp response?
     *    - That depends mostly on the included certificate chain
     *
     * 8. What value should we use in the initial estimate for the timestamp?
     *    - Currently 4096 is used but with a chain of 4 "normal" certificates
     *      that is a little bit too little.
     *    - Lets use 7168 and there are room for about 6 "normal" certificates
     * 
     * 
     * See also PDFSignerUnitTest for tests that the answers to the questions 
     * above still holds.
     * @param certChain The signing certificate chain
     * @param tsc Timestamp client, this can be null if no timestamp response is used. The contribution is estimated by using a fixed value
     * @param ocsp The OCSP response, can be null
     * @param crlList The list of CRLs included in the signature, this can be null
     * 
     * @return Returns the estimated signature size in bytes
     */
    protected int calculateEstimatedSignatureSize(Certificate[] certChain, TSAClient tsc, byte[] ocsp,
            CRL[] crlList) throws SignServerException {
        int estimatedSize = 0;

        if (LOG.isDebugEnabled()) {
            LOG.debug("Calculating estimated signature size");
        }

        for (Certificate cert : certChain) {
            try {
                int certSize = cert.getEncoded().length;
                estimatedSize += certSize;

                if (LOG.isDebugEnabled()) {
                    LOG.debug("Adding " + certSize + " bytes for certificate");
                }

            } catch (CertificateEncodingException e) {
                throw new SignServerException("Error estimating signature size contribution for certificate", e);
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Total size of certificate chain: " + estimatedSize);
        }

        // add estimate for PKCS#7 structure + hash
        estimatedSize += 2000;

        // add space for OCSP response
        if (ocsp != null) {
            estimatedSize += ocsp.length;

            if (LOG.isDebugEnabled()) {
                LOG.debug("Adding " + ocsp.length + " bytes for OCSP response");
            }
        }

        if (tsc != null) {
            // add guess for timestamp response (which we can't really know)
            // TODO: we might be able to store the size of the last TSA response and re-use next time...
            final int tscSize = 4096;

            estimatedSize += tscSize;

            if (LOG.isDebugEnabled()) {
                LOG.debug("Adding " + tscSize + " bytes for TSA");
            }
        }

        // add estimate for CRL
        if (crlList != null) {
            for (CRL crl : crlList) {
                if (crl instanceof X509CRL) {
                    X509CRL x509Crl = (X509CRL) crl;

                    try {
                        int crlSize = x509Crl.getEncoded().length;
                        // the CRL is included twice in the signature...
                        estimatedSize += crlSize * 2;

                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Adding " + crlSize * 2 + " bytes for CRL");
                        }

                    } catch (CRLException e) {
                        throw new SignServerException("Error estimating signature size contribution for CRL", e);
                    }
                }
            }
            estimatedSize += 100;
        }

        return estimatedSize;
    }

    protected byte[] calculateSignature(PdfPKCS7 sgn, int size, MessageDigest messageDigest, Calendar cal,
            PDFSignerParameters params, Certificate[] certChain, TSAClient tsc, byte[] ocsp,
            PdfSignatureAppearance sap) throws IOException, DocumentException, SignServerException {

        final HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
        exc.put(PdfName.CONTENTS, Integer.valueOf(size * 2 + 2));
        sap.preClose(exc);

        InputStream data = sap.getRangeStream();

        byte buf[] = new byte[8192];
        int n;
        while ((n = data.read(buf)) > 0) {
            messageDigest.update(buf, 0, n);
        }
        byte hash[] = messageDigest.digest();

        byte sh[] = sgn.getAuthenticatedAttributeBytes(hash, cal, ocsp);
        try {
            sgn.update(sh, 0, sh.length);
        } catch (SignatureException e) {
            throw new SignServerException("Error calculating signature", e);
        }

        byte[] encodedSig = sgn.getEncodedPKCS7(hash, cal, tsc, ocsp);

        return encodedSig;
    }

    /**
     * Get the minimum PDF version (x in 1.x)
     * given the configured digest algorithm.
     * 
     * @return PDF version ("suffix" version)
     * @throws IllegalArgumentException in case of an unknown digest algorithm
     */
    private int getMinimumPdfVersion() throws IllegalArgumentException {
        if ("SHA1".equals(digestAlgorithm)) {
            return 0;
        } else if ("SHA256".equals(digestAlgorithm)) {
            return 6;
        } else if ("SHA384".equals(digestAlgorithm)) {
            return 7;
        } else if ("SHA512".equals(digestAlgorithm)) {
            return 7;
        } else if ("RIPEMD160".equals(digestAlgorithm)) {
            return 7;
        } else {
            throw new IllegalArgumentException("Unknown digest algorithm: " + digestAlgorithm);
        }
    }

    protected byte[] addSignatureToPDFDocument(final ICryptoInstance crypto, PDFSignerParameters params,
            byte[] pdfbytes, byte[] password, int contentEstimated, final ProcessRequest request,
            final RequestContext context) throws IOException, DocumentException, CryptoTokenOfflineException,
            SignServerException, IllegalRequestException {
        // when given a content length (i.e. non-zero), it means we are running a second try
        boolean secondTry = contentEstimated != 0;

        // get signing cert certificate chain and private key
        final List<Certificate> certs = getSigningCertificateChain(crypto);
        if (certs == null) {
            throw new SignServerException("Null certificate chain. This signer needs a certificate.");
        }
        final List<Certificate> includedCerts = includedCertificates(certs);
        Certificate[] certChain = includedCerts.toArray(new Certificate[includedCerts.size()]);
        PrivateKey privKey = crypto.getPrivateKey();

        // need to check digest algorithms for DSA private key at signing
        // time since we can't be sure what key a configured alias selector gives back
        if (privKey instanceof DSAPrivateKey) {
            if (!"SHA1".equals(digestAlgorithm)) {
                throw new IllegalRequestException(
                        "Only SHA1 is permitted as digest algorithm for DSA private keys");
            }
        }

        PdfReader reader = new PdfReader(pdfbytes, password);
        boolean appendMode = true; // TODO: This could be good to have as a property in the future

        int pdfVersion;

        try {
            pdfVersion = Integer.parseInt(Character.toString(reader.getPdfVersion()));
        } catch (NumberFormatException e) {
            pdfVersion = 0;
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("PDF version: " + pdfVersion);
        }

        // Don't certify already certified documents
        if (reader.getCertificationLevel() != PdfSignatureAppearance.NOT_CERTIFIED
                && params.getCertification_level() != PdfSignatureAppearance.NOT_CERTIFIED) {
            throw new IllegalRequestException("Will not certify an already certified document");
        }

        // Don't sign documents where the certification does not allow it
        if (reader.getCertificationLevel() == PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED
                || reader.getCertificationLevel() == PdfSignatureAppearance.CERTIFIED_FORM_FILLING) {
            throw new IllegalRequestException("Will not sign a certified document where signing is not allowed");
        }

        Permissions currentPermissions = Permissions.fromInt(reader.getPermissions());

        if (params.getSetPermissions() != null && params.getRemovePermissions() != null) {
            throw new SignServerException("Signer " + workerId + " missconfigured. Only one of " + SET_PERMISSIONS
                    + " and " + REMOVE_PERMISSIONS + " should be specified.");
        }

        Permissions newPermissions;
        if (params.getSetPermissions() != null) {
            newPermissions = params.getSetPermissions();
        } else if (params.getRemovePermissions() != null) {
            newPermissions = currentPermissions.withRemoved(params.getRemovePermissions());
        } else {
            newPermissions = null;
        }

        Permissions rejectPermissions = Permissions.fromSet(params.getRejectPermissions());
        byte[] userPassword = reader.computeUserPassword();
        int cryptoMode = reader.getCryptoMode();
        if (LOG.isDebugEnabled()) {
            StringBuilder buff = new StringBuilder();
            buff.append("Current permissions: ").append(currentPermissions).append("\n")
                    .append("Remove permissions: ").append(params.getRemovePermissions()).append("\n")
                    .append("Reject permissions: ").append(rejectPermissions).append("\n")
                    .append("New permissions: ").append(newPermissions).append("\n").append("userPassword: ")
                    .append(userPassword == null ? "null" : "yes").append("\n").append("ownerPassword: ")
                    .append(password == null ? "no" : (isUserPassword(reader, password) ? "no" : "yes"))
                    .append("\n").append("setOwnerPassword: ")
                    .append(params.getSetOwnerPassword() == null ? "no" : "yes").append("\n").append("cryptoMode: ")
                    .append(cryptoMode);
            LOG.debug(buff.toString());
        }

        if (appendMode && (newPermissions != null || params.getSetOwnerPassword() != null)) {
            appendMode = false;
            if (LOG.isDebugEnabled()) {
                LOG.debug("Changing appendMode to false to be able to change permissions");
            }
        }

        ByteArrayOutputStream fout = new ByteArrayOutputStream();

        // increase PDF version if needed by digest algorithm
        final char updatedPdfVersion;
        if (minimumPdfVersion > pdfVersion) {
            updatedPdfVersion = Character.forDigit(minimumPdfVersion, 10);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Need to upgrade PDF to version 1." + updatedPdfVersion);
            }

            // check that the document isn't already signed 
            // when trying to upgrade version
            final AcroFields af = reader.getAcroFields();
            final List<String> sigNames = af.getSignatureNames();

            if (!sigNames.isEmpty()) {
                // TODO: in the future we might want to support
                // a fallback option in this case to allow re-signing using the same version (using append)
                throw new IllegalRequestException(
                        "Can not upgrade an already signed PDF and a higher version is required to support the configured digest algorithm");
            }

            appendMode = false;
        } else {
            updatedPdfVersion = '\0';
        }

        PdfStamper stp = PdfStamper.createSignature(reader, fout, updatedPdfVersion, null, appendMode);
        PdfSignatureAppearance sap = stp.getSignatureAppearance();

        // Set the new permissions
        if (newPermissions != null || params.getSetOwnerPassword() != null) {
            if (cryptoMode < 0) {
                cryptoMode = PdfWriter.STANDARD_ENCRYPTION_128;
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Setting default encryption algorithm");
                }
            }
            if (newPermissions == null) {
                newPermissions = currentPermissions;
            }
            if (params.getSetOwnerPassword() != null) {
                password = params.getSetOwnerPassword().getBytes("ISO-8859-1");
            } else if (isUserPassword(reader, password)) {
                // We do not have an owner password so lets use a random one
                password = new byte[16];
                random.nextBytes(password);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Setting random owner password");
                }
            }
            stp.setEncryption(userPassword, password, newPermissions.asInt(), cryptoMode);
            currentPermissions = newPermissions;
        }

        // Reject if any permissions are rejected and the document does not use a permission password
        // or if it contains any of the rejected permissions
        if (rejectPermissions.asInt() != 0) {
            if (cryptoMode < 0 || currentPermissions.containsAnyOf(rejectPermissions)) {
                throw new IllegalRequestException("Document contains permissions not allowed by this signer");
            }
        }

        // include signer certificate crl inside cms package if requested
        CRL[] crlList = null;
        if (params.isEmbed_crl()) {
            crlList = getCrlsForChain(certs);
        }
        sap.setCrypto(null, certChain, crlList, PdfSignatureAppearance.SELF_SIGNED);

        // add visible signature if requested
        if (params.isAdd_visible_signature()) {
            int signaturePage = getPageNumberForSignature(reader, params);
            sap.setVisibleSignature(new com.lowagie.text.Rectangle(params.getVisible_sig_rectangle_llx(),
                    params.getVisible_sig_rectangle_lly(), params.getVisible_sig_rectangle_urx(),
                    params.getVisible_sig_rectangle_ury()), signaturePage, null);

            // set custom image if requested
            if (params.isUse_custom_image()) {
                sap.setAcro6Layers(true);
                PdfTemplate n2 = sap.getLayer(2);
                params.getCustom_image().setAbsolutePosition(0, 0);
                n2.addImage(params.getCustom_image());
            }
        }

        // Certification level
        sap.setCertificationLevel(params.getCertification_level());

        PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, new PdfName("adbe.pkcs7.detached"));
        dic.setReason(params.getReason());
        dic.setLocation(params.getLocation());
        dic.setDate(new PdfDate(Calendar.getInstance()));

        sap.setCryptoDictionary(dic);

        // add timestamp to signature if requested
        TSAClient tsc = null;
        if (params.isUse_timestamp()) {
            final String tsaUrl = params.getTsa_url();

            if (tsaUrl != null) {
                tsc = getTimeStampClient(params.getTsa_url(), params.getTsa_username(), params.getTsa_password());
            } else {
                tsc = new InternalTSAClient(getWorkerSession(), params.getTsa_worker(), params.getTsa_username(),
                        params.getTsa_password());
            }
        }

        // embed ocsp response in cms package if requested
        // for ocsp request to be formed there needs to be issuer certificate in
        // chain
        byte[] ocsp = null;
        if (params.isEmbed_ocsp_response() && certChain.length >= 2) {
            String url;
            try {
                url = PdfPKCS7.getOCSPURL((X509Certificate) certChain[0]);
                if (url != null && url.length() > 0) {
                    ocsp = new OcspClientBouncyCastle((X509Certificate) certChain[0],
                            (X509Certificate) certChain[1], url).getEncoded();
                }
            } catch (CertificateParsingException e) {
                throw new SignServerException("Error getting OCSP URL from certificate", e);
            }

        }

        PdfPKCS7 sgn;
        try {
            sgn = new PdfPKCS7(privKey, certChain, crlList, digestAlgorithm, null, false);
        } catch (InvalidKeyException e) {
            throw new SignServerException("Error constructing PKCS7 package", e);
        } catch (NoSuchProviderException e) {
            throw new SignServerException("Error constructing PKCS7 package", e);
        } catch (NoSuchAlgorithmException e) {
            throw new SignServerException("Error constructing PKCS7 package", e);
        }

        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance(digestAlgorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new SignServerException("Error creating " + digestAlgorithm + " digest", e);
        }

        Calendar cal = Calendar.getInstance();

        // calculate signature size
        if (contentEstimated == 0) {
            contentEstimated = calculateEstimatedSignatureSize(certChain, tsc, ocsp, crlList);
        }

        byte[] encodedSig = calculateSignature(sgn, contentEstimated, messageDigest, cal, params, certChain, tsc,
                ocsp, sap);

        if (LOG.isDebugEnabled()) {
            LOG.debug("Estimated size: " + contentEstimated);
            LOG.debug("Encoded length: " + encodedSig.length);
        }

        if (contentEstimated + 2 < encodedSig.length) {
            if (!secondTry) {
                int contentExact = encodedSig.length;
                LOG.warn(
                        "Estimated signature size too small, usinging accurate calculation (resulting in an extra signature computation).");

                if (LOG.isDebugEnabled()) {
                    LOG.debug("Estimated size: " + contentEstimated + ", actual size: " + contentExact);
                }

                // try signing again
                return addSignatureToPDFDocument(crypto, params, pdfbytes, password, contentExact, request,
                        context);
            } else {
                // if we fail to get an accurate signature size on the second attempt, bail out (this shouldn't happen)
                throw new SignServerException("Failed to calculate signature size");
            }
        }

        byte[] paddedSig = new byte[contentEstimated];
        System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length);

        PdfDictionary dic2 = new PdfDictionary();
        dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));
        sap.close(dic2);
        reader.close();

        fout.close();
        return fout.toByteArray();
    }

    protected IInternalWorkerSession getWorkerSession() {
        if (workerSession == null) {
            try {
                workerSession = ServiceLocator.getInstance().lookupLocal(IInternalWorkerSession.class);
            } catch (NamingException ex) {
                throw new RuntimeException("Unable to lookup worker session", ex);
            }
        }
        return workerSession;
    }

    /**
     * returns crl list containing crl for each certifcate in crl chain. CRLs
     * are fetched using address specified in CDP.
     * 
     * @return n
     * @throws SignServerException
     */
    private CRL[] getCrlsForChain(final Collection<Certificate> certChain) throws SignServerException {

        List<CRL> retCrls = new ArrayList<CRL>();
        for (Certificate currCert : certChain) {
            CRL currCrl = null;
            try {
                URL currCertURL = getCRLDistributionPoint(currCert);
                if (currCertURL == null) {
                    continue;
                }

                currCrl = ValidationUtils.fetchCRLFromURL(currCertURL);
            } catch (CertificateParsingException e) {
                throw new SignServerException("Error obtaining CDP from signing certificate", e);
            }

            retCrls.add(currCrl);
        }

        if (retCrls.isEmpty()) {
            return null;
        } else {
            return retCrls.toArray(new CRL[retCrls.size()]);
        }

    }

    static URL getCRLDistributionPoint(final Certificate certificate) throws CertificateParsingException {
        return org.signserver.module.pdfsigner.org.ejbca.util.CertTools.getCrlDistributionPoint(certificate);
    }

    /**
     * get the page number at which to draw signature rectangle
     * 
     * @param pReader
     * @param pParams
     * @return
     */
    private int getPageNumberForSignature(PdfReader pReader, PDFSignerParameters pParams) {
        int totalNumOfPages = pReader.getNumberOfPages();
        if (pParams.getVisible_sig_page().trim().equals("First")) {
            return 1;
        } else if (pParams.getVisible_sig_page().trim().equals("Last")) {
            return totalNumOfPages;
        } else {
            try {
                int pNum = Integer.parseInt(pParams.getVisible_sig_page());
                if (pNum < 1) {
                    return 1;
                } else if (pNum > totalNumOfPages) {
                    return totalNumOfPages;
                } else {
                    return pNum;
                }
            } catch (NumberFormatException ex) {
                // not a numeric argument draw on first line
                return 1;
            }
        }
    }

    private void archiveToDisk(ISignRequest sReq, byte[] signedbytes, RequestContext requestContext)
            throws SignServerException {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Archiving to disk");
        }

        // Fill in fields that can be used to construct path and filename
        final Map<String, String> fields = new HashMap<String, String>();
        fields.put("WORKERID", String.valueOf(workerId));
        fields.put("WORKERNAME", config.getProperty("NAME"));
        fields.put("REMOTEIP", (String) requestContext.get(RequestContext.REMOTE_IP));
        fields.put("TRANSACTIONID", (String) requestContext.get(RequestContext.TRANSACTION_ID));
        fields.put("REQUESTID", String.valueOf(sReq.getRequestID()));

        Object credential = requestContext.get(RequestContext.CLIENT_CREDENTIAL);
        if (credential instanceof UsernamePasswordClientCredential) {
            fields.put("USERNAME", ((UsernamePasswordClientCredential) credential).getUsername());
        }

        final String pathFromPattern = formatFromPattern(archivetodiskPattern,
                config.getProperty(PROPERTY_ARCHIVETODISK_PATH_PATTERN, DEFAULT_ARCHIVETODISK_PATH_PATTERN),
                new Date(), fields);

        final File outputPath = new File(new File(config.getProperty(PROPERTY_ARCHIVETODISK_PATH_BASE)),
                pathFromPattern);

        if (!outputPath.exists()) {
            if (!outputPath.mkdirs()) {
                LOG.warn("Output path could not be created: " + outputPath.getAbsolutePath());
            }
        }

        final String fileNameFromPattern = formatFromPattern(archivetodiskPattern,
                config.getProperty(PROPERTY_ARCHIVETODISK_FILENAME_PATTERN, DEFAULT_ARCHIVETODISK_FILENAME_PATTERN),
                new Date(), fields);

        final File outputFile = new File(outputPath, fileNameFromPattern);

        if (LOG.isDebugEnabled()) {
            LOG.debug("Worker[" + workerId + "]: Archive to file: " + outputFile.getAbsolutePath());
        }

        OutputStream out = null;
        try {
            out = new FileOutputStream(outputFile);
            out.write(signedbytes);
        } catch (IOException ex) {
            throw new SignServerException("Could not archive signed document", ex);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException ex) {
                    LOG.debug("Exception closing file", ex);
                    throw new SignServerException("Could not archive signed document", ex);
                }
            }
        }
    }

    /**
     * Helper method for formatting a text given a set of fields and a date.
     *
     * Sample:
     * "${WORKERID}-${REQUESTID}_${DATE:yyyy-MM-dd}.pdf"
     * Could be:
     * "42-123123123_2010-04-28.pdf"
     * 
     * @param pattern Pre-compiled pattern to use for parsing
     * @param text The text that contains keys to be replaced with values
     * @param date The date to use if date should be inserted
     * @param fields Keys and their values that should be used if they exist in
     * the text.
     * @return The test with keys replaced with values from fields or by
     * formatted date
     * @see java.text.SimpleDateFormat
     */
    static String formatFromPattern(final Pattern pattern, final String text, final Date date,
            final Map<String, String> fields) {
        final String result;

        if (LOG.isDebugEnabled()) {
            LOG.debug("Input string: " + text);
        }

        final StringBuffer sb = new StringBuffer();
        Matcher m = pattern.matcher(text);
        while (m.find()) {
            // when the pattern is ${identifier}, group 0 is 'identifier'
            final String key = m.group(1);

            final String value;
            if (key.startsWith("DATE:")) {
                final SimpleDateFormat sdf = new SimpleDateFormat(key.substring("DATE:".length()).trim());
                value = sdf.format(date);
            } else {
                value = fields.get(key);
            }

            // if the pattern does exists, replace it by its value
            // otherwise keep the pattern ( it is group(0) )
            if (value != null) {
                m.appendReplacement(sb, value);
            } else {
                // I'm doing this to avoid the backreference problem as there will be a $
                // if I replace directly with the group 0 (which is also a pattern)
                m.appendReplacement(sb, "");
                final String unknown = m.group(0);
                sb.append(unknown);
            }
        }
        m.appendTail(sb);
        result = sb.toString();

        if (LOG.isDebugEnabled()) {
            LOG.debug("Result: " + result);
        }
        return result;
    }

    private void checkForDuplicateObjects(byte[] pdfbytes) throws IOException, SignServerException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(">checkForDuplicateObjects");
        }
        final PRTokeniser tokens = new PRTokeniser(pdfbytes);
        final Set<String> idents = new HashSet<String>();
        final byte[] line = new byte[16];

        while (tokens.readLineSegment(line)) {
            final int[] obj = PRTokeniser.checkObjectStart(line);
            if (obj != null) {
                final String ident = obj[0] + " " + obj[1];

                if (idents.add(ident)) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Object: " + ident);
                    }
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Duplicate object: " + ident);
                    }
                    throw new SignServerException("Incorrect document");
                }
            }
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("<checkForDuplicateObjects");
        }
    }

    private static byte[] getPassword(final RequestContext context) throws UnsupportedEncodingException {
        final byte[] result;
        final String password = RequestMetadata.getInstance(context).get(RequestContext.METADATA_PDFPASSWORD);
        if (password == null) {
            result = null;
        } else {
            result = password.getBytes("ISO-8859-1");
        }
        return result;
    }

    /**
     * @return True if the supplied password is equal to the user password 
     * and thus is not the owner password.
     */
    private boolean isUserPassword(PdfReader reader, byte[] password) {
        return Arrays.equals(reader.computeUserPassword(), password);
    }

    protected TSAClient getTimeStampClient(String url, String username, String password) {
        return new TSAClientBouncyCastle(url, username, password);
    }

    @Override
    protected List<String> getFatalErrors() {
        final List<String> fatalErrors = super.getFatalErrors();

        fatalErrors.addAll(configErrors);
        return fatalErrors;
    }

    /**
     * Internal method for the unit test to set the included certificate levels (to a non-zero value)
     * without having to initializing the signer.
     * 
     * @param includeCertificateLevels
     */
    void setIncludeCertificateLevels(final int includeCertificateLevels) {
        this.includeCertificateLevels = includeCertificateLevels;
    }

}