cz.hobrasoft.pdfmu.operation.signature.OperationSignatureAdd.java Source code

Java tutorial

Introduction

Here is the source code for cz.hobrasoft.pdfmu.operation.signature.OperationSignatureAdd.java

Source

/* 
 * Copyright (C) 2016 Hobrasoft s.r.o.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package cz.hobrasoft.pdfmu.operation.signature;

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.ExceptionConverter;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.security.BouncyCastleDigest;
import com.itextpdf.text.pdf.security.CrlClient;
import com.itextpdf.text.pdf.security.DigestAlgorithms;
import com.itextpdf.text.pdf.security.ExternalDigest;
import com.itextpdf.text.pdf.security.ExternalSignature;
import com.itextpdf.text.pdf.security.MakeSignature;
import com.itextpdf.text.pdf.security.OcspClient;
import com.itextpdf.text.pdf.security.PrivateKeySignature;
import com.itextpdf.text.pdf.security.TSAClient;
import cz.hobrasoft.pdfmu.ExceptionMessagePattern;
import cz.hobrasoft.pdfmu.PdfmuUtils;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_FAIL;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_SIGNATURE_EXCEPTION;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_BAD_CERTIFICATE;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_HANDSHAKE_FAILURE;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_LOGIN_FAIL;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_SSL_FATAL_ALERT;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_SSL_HANDSHAKE_EXCEPTION;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNAUTHORIZED;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNREACHABLE;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNTRUSTED;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_UNSUPPORTED_DIGEST_ALGORITHM;
import static cz.hobrasoft.pdfmu.error.ErrorType.SSL_TRUSTSTORE_EMPTY;
import static cz.hobrasoft.pdfmu.error.ErrorType.SSL_TRUSTSTORE_INCORRECT_TYPE;
import cz.hobrasoft.pdfmu.jackson.SignatureAdd;
import cz.hobrasoft.pdfmu.operation.Operation;
import cz.hobrasoft.pdfmu.operation.OperationCommon;
import cz.hobrasoft.pdfmu.operation.OperationException;
import cz.hobrasoft.pdfmu.operation.args.InOutPdfArgs;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

/**
 * Adds a digital signature to a PDF document
 *
 * @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a>
 */
public class OperationSignatureAdd extends OperationCommon {

    private static final Logger logger = Logger.getLogger(OperationSignatureAdd.class.getName());

    private final InOutPdfArgs inout = new InOutPdfArgs();

    @Override
    public Subparser configureSubparser(Subparser subparser) {
        String help = "Add a digital signature to a PDF document";

        // Configure the subparser
        subparser.help(help).description(help).defaultHelp(true);

        inout.addArguments(subparser);
        signatureParameters.addArguments(subparser);

        return subparser;
    }

    // digitalsignatures20130304.pdf : Code sample 1.6
    // Initialize the security provider
    private static final BouncyCastleProvider provider = new BouncyCastleProvider();

    static {
        // We need to register the provider because it needs to be accessible by its name globally.
        // {@link com.itextpdf.text.pdf.security.PrivateKeySignature#PrivateKeySignature(PrivateKey pk, String hashAlgorithm, String provider)}
        // uses the provider name.
        Security.addProvider(provider);
    }

    // Initialize the digest algorithm
    private static final ExternalDigest externalDigest = new BouncyCastleDigest();

    // `signatureParameters` is a member variable
    // so that we can add the arguments to the parser in `configureSubparser`.
    // We need an instance of {@link SignatureParameters} in `configureSubparser`
    // because the interface `ArgsConfiguration` does not allow static methods.
    private final SignatureParameters signatureParameters = new SignatureParameters();

    @Override
    public void execute(Namespace namespace) throws OperationException {
        inout.setFromNamespace(namespace);

        // Initialize signature parameters
        signatureParameters.setFromNamespace(namespace);

        writeResult(sign(inout, signatureParameters));
    }

    private static SignatureAdd sign(InOutPdfArgs inout, SignatureParameters signatureParameters)
            throws OperationException {
        SignatureAdd sa;
        try { // inout
            inout.openSignature();
            PdfStamper stp = inout.getPdfStamper();
            sa = sign(stp, signatureParameters);
            inout.close(true);
        } finally {
            inout.close(false);
        }
        return sa;
    }

    // Initialize and load the keystore
    private static SignatureAdd sign(PdfStamper stp, SignatureParameters signatureParameters)
            throws OperationException {
        // Unwrap the signature parameters
        SignatureAppearanceParameters signatureAppearanceParameters = signatureParameters.appearance;
        KeystoreParameters keystoreParameters = signatureParameters.keystore;
        KeyParameters keyParameters = signatureParameters.key;
        String digestAlgorithm = signatureParameters.digestAlgorithm;

        TSAClient tsaClient = signatureParameters.timestamp.getTSAClient();
        if (tsaClient != null) {
            logger.info("Using a timestamp authority to attach a timestamp.");
        } else {
            logger.info("No timestamp authority was specified.");
        }

        MakeSignature.CryptoStandard sigtype = signatureParameters.format;

        assert keystoreParameters != null;

        // Initialize and load keystore
        KeyStore ks = keystoreParameters.loadKeystore();

        // Fix the values, especially if they were not set at all
        keyParameters.fix(ks, keystoreParameters.getPassword());

        return sign(stp, signatureAppearanceParameters, ks, keyParameters, digestAlgorithm, tsaClient, sigtype);
    }

    // Initialize the signature appearance
    private static SignatureAdd sign(PdfStamper stp, SignatureAppearanceParameters signatureAppearanceParameters,
            KeyStore ks, KeyParameters keyParameters, String digestAlgorithm, TSAClient tsaClient,
            MakeSignature.CryptoStandard sigtype) throws OperationException {
        // Initialize the signature appearance
        PdfSignatureAppearance sap = signatureAppearanceParameters.getSignatureAppearance(stp);
        assert sap != null; // `stp` must have been created using `PdfStamper.createSignature` static method

        return sign(sap, ks, keyParameters, digestAlgorithm, tsaClient, sigtype);
    }

    // Get the private key and the certificate chain from the keystore
    private static SignatureAdd sign(PdfSignatureAppearance sap, KeyStore ks, KeyParameters keyParameters,
            String digestAlgorithm, TSAClient tsaClient, MakeSignature.CryptoStandard sigtype)
            throws OperationException {
        assert keyParameters != null;
        String alias = keyParameters.alias;
        SignatureAdd sa = new SignatureAdd(alias);

        PrivateKey pk = keyParameters.getPrivateKey(ks);
        Certificate[] chain = keyParameters.getCertificateChain(ks);

        Provider signatureProvider;
        { // ksProvider
            Provider ksProvider = ks.getProvider();
            // "SunMSCAPI" provider must be used for signing if it was used for keystore loading.
            // In case of other keystore providers,
            // we use the default signature provider.
            // https://community.oracle.com/thread/1528230
            if ("SunMSCAPI".equals(ksProvider.getName())) {
                signatureProvider = ksProvider;
            } else {
                signatureProvider = provider;
            }
        }

        sign(sap, pk, digestAlgorithm, chain, tsaClient, sigtype, signatureProvider);

        return sa;
    }

    // Initialize the signature algorithm
    private static void sign(PdfSignatureAppearance sap, PrivateKey pk, String digestAlgorithm, Certificate[] chain,
            TSAClient tsaClient, MakeSignature.CryptoStandard sigtype, Provider signatureProvider)
            throws OperationException {
        assert digestAlgorithm != null;

        // Initialize the signature algorithm
        logger.info(String.format("Digest algorithm: %s", digestAlgorithm));
        if (DigestAlgorithms.getAllowedDigests(digestAlgorithm) == null) {
            throw new OperationException(SIGNATURE_ADD_UNSUPPORTED_DIGEST_ALGORITHM,
                    PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("digestAlgorithm", digestAlgorithm)));
        }

        logger.info(String.format("Signature security provider: %s", signatureProvider.getName()));
        ExternalSignature externalSignature = new PrivateKeySignature(pk, digestAlgorithm,
                signatureProvider.getName());

        sign(sap, externalSignature, chain, tsaClient, sigtype);
    }

    // Set the "external digest" algorithm
    private static void sign(PdfSignatureAppearance sap, ExternalSignature externalSignature, Certificate[] chain,
            TSAClient tsaClient, MakeSignature.CryptoStandard sigtype) throws OperationException {
        // Use the static BouncyCastleDigest instance
        sign(sap, externalDigest, externalSignature, chain, tsaClient, sigtype);
    }

    // Sign the document
    private static void sign(PdfSignatureAppearance sap, ExternalDigest externalDigest,
            ExternalSignature externalSignature, Certificate[] chain, TSAClient tsaClient,
            MakeSignature.CryptoStandard sigtype) throws OperationException {
        // TODO?: Set some of the following parameters more sensibly

        // Certificate Revocation List
        // digitalsignatures20130304.pdf : Section 3.2
        Collection<CrlClient> crlList = null;

        // Online Certificate Status Protocol
        // digitalsignatures20130304.pdf : Section 3.2.4
        OcspClient ocspClient = null;

        // digitalsignatures20130304.pdf : Section 3.5
        // The value of 0 means "try a generous educated guess".
        // We need not change this unless we want to optimize the resulting PDF document size.
        int estimatedSize = 0;

        logger.info(String.format("Cryptographic standard (signature format): %s", sigtype));

        try {
            MakeSignature.signDetached(sap, externalDigest, externalSignature, chain, crlList, ocspClient,
                    tsaClient, estimatedSize, sigtype);
        } catch (ExceptionConverter ex) {
            Exception exInner = ex.getException();
            if (exInner instanceof IOException) {
                if (exInner instanceof SSLHandshakeException) {
                    Set<ExceptionMessagePattern> patterns = new HashSet<>();

                    // Untrusted
                    patterns.add(new ExceptionMessagePattern(SIGNATURE_ADD_TSA_UNTRUSTED,
                            "sun\\.security\\.validator\\.ValidatorException: PKIX path building failed: sun\\.security\\.provider\\.certpath\\.SunCertPathBuilderException: unable to find valid certification path to requested target",
                            new ArrayList<String>()));

                    // Bad certificate
                    patterns.add(new ExceptionMessagePattern(SIGNATURE_ADD_TSA_BAD_CERTIFICATE,
                            "Received fatal alert: bad_certificate", new ArrayList<String>()));

                    // Handshake failure
                    patterns.add(new ExceptionMessagePattern(SIGNATURE_ADD_TSA_HANDSHAKE_FAILURE,
                            "Received fatal alert: handshake_failure", new ArrayList<String>()));

                    OperationException oe = null;
                    for (ExceptionMessagePattern p : patterns) {
                        oe = p.getOperationException(exInner);
                        if (oe != null) {
                            break;
                        }
                    }
                    if (oe == null) {
                        ExceptionMessagePattern emp = new ExceptionMessagePattern(SIGNATURE_ADD_TSA_SSL_FATAL_ALERT,
                                "Received fatal alert: (?<alert>.*)", Arrays.asList(new String[] { "alert" }));
                        oe = emp.getOperationException(exInner);

                        if (oe == null) {
                            // Unknown exception
                            oe = new OperationException(SIGNATURE_ADD_TSA_SSL_HANDSHAKE_EXCEPTION, exInner);
                        }
                    }
                    assert oe != null;
                    throw oe;
                }

                if (exInner instanceof SSLException) {
                    ExceptionMessagePattern emp = new ExceptionMessagePattern(SSL_TRUSTSTORE_EMPTY,
                            "java\\.lang\\.RuntimeException: Unexpected error: java\\.security\\.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty",
                            new ArrayList<String>());
                    OperationException oe = emp.getOperationException(exInner);
                    if (oe != null) {
                        throw oe;
                    }
                    throw new OperationException(SIGNATURE_ADD_FAIL, exInner);
                }

                if (exInner instanceof UnknownHostException || exInner instanceof FileNotFoundException) {
                    String host = exInner.getMessage();
                    throw new OperationException(SIGNATURE_ADD_TSA_UNREACHABLE, exInner,
                            new SimpleEntry<String, Object>("host", host));
                }

                if (exInner instanceof SocketException) {
                    ExceptionMessagePattern emp = new ExceptionMessagePattern(SSL_TRUSTSTORE_INCORRECT_TYPE,
                            "java\\.security\\.NoSuchAlgorithmException: Error constructing implementation \\(algorithm: (?<algorithm>.*), provider: (?<provider>.*), class: (?<class>.*)\\)",
                            Arrays.asList(new String[] { "algorithm", "provider", "class" }));
                    OperationException oe = emp.getOperationException(exInner);
                    if (oe != null) {
                        throw oe;
                    }
                    throw new OperationException(SIGNATURE_ADD_FAIL, exInner);
                }

                Set<ExceptionMessagePattern> patterns = new HashSet<>();

                // No username
                // May also be returned if the username and password are incorrect.
                patterns.add(new ExceptionMessagePattern(SIGNATURE_ADD_TSA_UNAUTHORIZED,
                        "Server returned HTTP response code: 401 for URL: (?<url>.*)",
                        Arrays.asList(new String[] { "url" })));

                // Incorrect username or incorrect password
                patterns.add(new ExceptionMessagePattern(SIGNATURE_ADD_TSA_LOGIN_FAIL,
                        "Invalid TSA '(?<url>.*)' response, code (?<code>\\d+)",
                        Arrays.asList(new String[] { "url", "code" })));

                patterns.add(new ExceptionMessagePattern(SIGNATURE_ADD_FAIL, "unknown tag (?<tag>\\d+) encountered",
                        Arrays.asList(new String[] { "tag" })));

                OperationException oe = null;
                for (ExceptionMessagePattern p : patterns) {
                    oe = p.getOperationException(exInner);
                    if (oe != null) {
                        break;
                    }
                }
                if (oe == null) {
                    // Unknown exception
                    oe = new OperationException(SIGNATURE_ADD_FAIL, exInner);
                }
                assert oe != null;
                throw oe;
            }
            throw new OperationException(SIGNATURE_ADD_FAIL, exInner);
        } catch (SignatureException ex) {
            throw new OperationException(SIGNATURE_ADD_SIGNATURE_EXCEPTION, ex);
        } catch (IOException | DocumentException | GeneralSecurityException ex) {
            throw new OperationException(SIGNATURE_ADD_FAIL, ex);
        } catch (NullPointerException ex) {
            // Invalid digest algorithm?
            throw new OperationException(SIGNATURE_ADD_FAIL, ex);
        }
        logger.info("Document successfully signed.");
    }

    private static Operation instance = null;

    public static Operation getInstance() {
        if (instance == null) {
            instance = new OperationSignatureAdd();
        }
        return instance;
    }

    // Singleton
    private OperationSignatureAdd() {
        super();
    }

}