001/* 
002 * Copyright (C) 2016 Hobrasoft s.r.o.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU Affero General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU Affero General Public License for more details.
013 *
014 * You should have received a copy of the GNU Affero General Public License
015 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
016 */
017package cz.hobrasoft.pdfmu.operation.signature;
018
019import com.itextpdf.text.DocumentException;
020import com.itextpdf.text.ExceptionConverter;
021import com.itextpdf.text.pdf.PdfSignatureAppearance;
022import com.itextpdf.text.pdf.PdfStamper;
023import com.itextpdf.text.pdf.security.BouncyCastleDigest;
024import com.itextpdf.text.pdf.security.CrlClient;
025import com.itextpdf.text.pdf.security.DigestAlgorithms;
026import com.itextpdf.text.pdf.security.ExternalDigest;
027import com.itextpdf.text.pdf.security.ExternalSignature;
028import com.itextpdf.text.pdf.security.MakeSignature;
029import com.itextpdf.text.pdf.security.OcspClient;
030import com.itextpdf.text.pdf.security.PrivateKeySignature;
031import com.itextpdf.text.pdf.security.TSAClient;
032import cz.hobrasoft.pdfmu.ExceptionMessagePattern;
033import cz.hobrasoft.pdfmu.PdfmuUtils;
034import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_FAIL;
035import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_SIGNATURE_EXCEPTION;
036import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_BAD_CERTIFICATE;
037import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_HANDSHAKE_FAILURE;
038import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_LOGIN_FAIL;
039import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_SSL_FATAL_ALERT;
040import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_SSL_HANDSHAKE_EXCEPTION;
041import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNAUTHORIZED;
042import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNREACHABLE;
043import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNTRUSTED;
044import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_UNSUPPORTED_DIGEST_ALGORITHM;
045import static cz.hobrasoft.pdfmu.error.ErrorType.SSL_TRUSTSTORE_EMPTY;
046import static cz.hobrasoft.pdfmu.error.ErrorType.SSL_TRUSTSTORE_INCORRECT_TYPE;
047import cz.hobrasoft.pdfmu.jackson.SignatureAdd;
048import cz.hobrasoft.pdfmu.operation.Operation;
049import cz.hobrasoft.pdfmu.operation.OperationCommon;
050import cz.hobrasoft.pdfmu.operation.OperationException;
051import cz.hobrasoft.pdfmu.operation.args.InOutPdfArgs;
052import java.io.FileNotFoundException;
053import java.io.IOException;
054import java.net.SocketException;
055import java.net.UnknownHostException;
056import java.security.GeneralSecurityException;
057import java.security.KeyStore;
058import java.security.PrivateKey;
059import java.security.Provider;
060import java.security.Security;
061import java.security.SignatureException;
062import java.security.cert.Certificate;
063import java.util.AbstractMap.SimpleEntry;
064import java.util.ArrayList;
065import java.util.Arrays;
066import java.util.Collection;
067import java.util.HashSet;
068import java.util.Set;
069import java.util.logging.Logger;
070import javax.net.ssl.SSLException;
071import javax.net.ssl.SSLHandshakeException;
072import net.sourceforge.argparse4j.inf.Namespace;
073import net.sourceforge.argparse4j.inf.Subparser;
074import org.bouncycastle.jce.provider.BouncyCastleProvider;
075
076/**
077 * Adds a digital signature to a PDF document
078 *
079 * @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a>
080 */
081public class OperationSignatureAdd extends OperationCommon {
082
083    private static final Logger logger = Logger.getLogger(OperationSignatureAdd.class.getName());
084
085    private final InOutPdfArgs inout = new InOutPdfArgs();
086
087    @Override
088    public Subparser configureSubparser(Subparser subparser) {
089        String help = "Add a digital signature to a PDF document";
090
091        // Configure the subparser
092        subparser.help(help)
093                .description(help)
094                .defaultHelp(true);
095
096        inout.addArguments(subparser);
097        signatureParameters.addArguments(subparser);
098
099        return subparser;
100    }
101
102    // digitalsignatures20130304.pdf : Code sample 1.6
103    // Initialize the security provider
104    private static final BouncyCastleProvider provider = new BouncyCastleProvider();
105
106    static {
107        // We need to register the provider because it needs to be accessible by its name globally.
108        // {@link com.itextpdf.text.pdf.security.PrivateKeySignature#PrivateKeySignature(PrivateKey pk, String hashAlgorithm, String provider)}
109        // uses the provider name.
110        Security.addProvider(provider);
111    }
112
113    // Initialize the digest algorithm
114    private static final ExternalDigest externalDigest = new BouncyCastleDigest();
115
116    // `signatureParameters` is a member variable
117    // so that we can add the arguments to the parser in `configureSubparser`.
118    // We need an instance of {@link SignatureParameters} in `configureSubparser`
119    // because the interface `ArgsConfiguration` does not allow static methods.
120    private final SignatureParameters signatureParameters = new SignatureParameters();
121
122    @Override
123    public void execute(Namespace namespace) throws OperationException {
124        inout.setFromNamespace(namespace);
125
126        // Initialize signature parameters
127        signatureParameters.setFromNamespace(namespace);
128
129        writeResult(sign(inout, signatureParameters));
130    }
131
132    private static SignatureAdd sign(InOutPdfArgs inout,
133            SignatureParameters signatureParameters) throws OperationException {
134        SignatureAdd sa;
135        try { // inout
136            inout.openSignature();
137            PdfStamper stp = inout.getPdfStamper();
138            sa = sign(stp, signatureParameters);
139            inout.close(true);
140        } finally {
141            inout.close(false);
142        }
143        return sa;
144    }
145
146    // Initialize and load the keystore
147    private static SignatureAdd sign(PdfStamper stp,
148            SignatureParameters signatureParameters) throws OperationException {
149        // Unwrap the signature parameters
150        SignatureAppearanceParameters signatureAppearanceParameters = signatureParameters.appearance;
151        KeystoreParameters keystoreParameters = signatureParameters.keystore;
152        KeyParameters keyParameters = signatureParameters.key;
153        String digestAlgorithm = signatureParameters.digestAlgorithm;
154
155        TSAClient tsaClient = signatureParameters.timestamp.getTSAClient();
156        if (tsaClient != null) {
157            logger.info("Using a timestamp authority to attach a timestamp.");
158        } else {
159            logger.info("No timestamp authority was specified.");
160        }
161
162        MakeSignature.CryptoStandard sigtype = signatureParameters.format;
163
164        assert keystoreParameters != null;
165
166        // Initialize and load keystore
167        KeyStore ks = keystoreParameters.loadKeystore();
168
169        // Fix the values, especially if they were not set at all
170        keyParameters.fix(ks, keystoreParameters.getPassword());
171
172        return sign(stp, signatureAppearanceParameters, ks, keyParameters, digestAlgorithm, tsaClient, sigtype);
173    }
174
175    // Initialize the signature appearance
176    private static SignatureAdd sign(PdfStamper stp,
177            SignatureAppearanceParameters signatureAppearanceParameters,
178            KeyStore ks,
179            KeyParameters keyParameters,
180            String digestAlgorithm,
181            TSAClient tsaClient,
182            MakeSignature.CryptoStandard sigtype) throws OperationException {
183        // Initialize the signature appearance
184        PdfSignatureAppearance sap = signatureAppearanceParameters.getSignatureAppearance(stp);
185        assert sap != null; // `stp` must have been created using `PdfStamper.createSignature` static method
186
187        return sign(sap, ks, keyParameters, digestAlgorithm, tsaClient, sigtype);
188    }
189
190    // Get the private key and the certificate chain from the keystore
191    private static SignatureAdd sign(PdfSignatureAppearance sap,
192            KeyStore ks,
193            KeyParameters keyParameters,
194            String digestAlgorithm,
195            TSAClient tsaClient,
196            MakeSignature.CryptoStandard sigtype) throws OperationException {
197        assert keyParameters != null;
198        String alias = keyParameters.alias;
199        SignatureAdd sa = new SignatureAdd(alias);
200
201        PrivateKey pk = keyParameters.getPrivateKey(ks);
202        Certificate[] chain = keyParameters.getCertificateChain(ks);
203
204        Provider signatureProvider;
205        { // ksProvider
206            Provider ksProvider = ks.getProvider();
207            // "SunMSCAPI" provider must be used for signing if it was used for keystore loading.
208            // In case of other keystore providers,
209            // we use the default signature provider.
210            // https://community.oracle.com/thread/1528230
211            if ("SunMSCAPI".equals(ksProvider.getName())) {
212                signatureProvider = ksProvider;
213            } else {
214                signatureProvider = provider;
215            }
216        }
217
218        sign(sap, pk, digestAlgorithm, chain, tsaClient, sigtype, signatureProvider);
219
220        return sa;
221    }
222
223    // Initialize the signature algorithm
224    private static void sign(PdfSignatureAppearance sap,
225            PrivateKey pk,
226            String digestAlgorithm,
227            Certificate[] chain,
228            TSAClient tsaClient,
229            MakeSignature.CryptoStandard sigtype,
230            Provider signatureProvider) throws OperationException {
231        assert digestAlgorithm != null;
232
233        // Initialize the signature algorithm
234        logger.info(String.format("Digest algorithm: %s", digestAlgorithm));
235        if (DigestAlgorithms.getAllowedDigests(digestAlgorithm) == null) {
236            throw new OperationException(SIGNATURE_ADD_UNSUPPORTED_DIGEST_ALGORITHM,
237                    PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("digestAlgorithm", digestAlgorithm)));
238        }
239
240        logger.info(String.format("Signature security provider: %s", signatureProvider.getName()));
241        ExternalSignature externalSignature = new PrivateKeySignature(pk, digestAlgorithm, signatureProvider.getName());
242
243        sign(sap, externalSignature, chain, tsaClient, sigtype);
244    }
245
246    // Set the "external digest" algorithm
247    private static void sign(PdfSignatureAppearance sap,
248            ExternalSignature externalSignature,
249            Certificate[] chain,
250            TSAClient tsaClient,
251            MakeSignature.CryptoStandard sigtype) throws OperationException {
252        // Use the static BouncyCastleDigest instance
253        sign(sap, externalDigest, externalSignature, chain, tsaClient, sigtype);
254    }
255
256    // Sign the document
257    private static void sign(PdfSignatureAppearance sap,
258            ExternalDigest externalDigest,
259            ExternalSignature externalSignature,
260            Certificate[] chain,
261            TSAClient tsaClient,
262            MakeSignature.CryptoStandard sigtype) throws OperationException {
263        // TODO?: Set some of the following parameters more sensibly
264
265        // Certificate Revocation List
266        // digitalsignatures20130304.pdf : Section 3.2
267        Collection<CrlClient> crlList = null;
268
269        // Online Certificate Status Protocol
270        // digitalsignatures20130304.pdf : Section 3.2.4
271        OcspClient ocspClient = null;
272
273        // digitalsignatures20130304.pdf : Section 3.5
274        // The value of 0 means "try a generous educated guess".
275        // We need not change this unless we want to optimize the resulting PDF document size.
276        int estimatedSize = 0;
277
278        logger.info(String.format("Cryptographic standard (signature format): %s", sigtype));
279
280        try {
281            MakeSignature.signDetached(sap, externalDigest, externalSignature, chain, crlList, ocspClient, tsaClient, estimatedSize, sigtype);
282        } catch (ExceptionConverter ex) {
283            Exception exInner = ex.getException();
284            if (exInner instanceof IOException) {
285                if (exInner instanceof SSLHandshakeException) {
286                    Set<ExceptionMessagePattern> patterns = new HashSet<>();
287
288                    // Untrusted
289                    patterns.add(new ExceptionMessagePattern(
290                            SIGNATURE_ADD_TSA_UNTRUSTED,
291                            "sun\\.security\\.validator\\.ValidatorException: PKIX path building failed: sun\\.security\\.provider\\.certpath\\.SunCertPathBuilderException: unable to find valid certification path to requested target",
292                            new ArrayList<String>()));
293
294                    // Bad certificate
295                    patterns.add(new ExceptionMessagePattern(
296                            SIGNATURE_ADD_TSA_BAD_CERTIFICATE,
297                            "Received fatal alert: bad_certificate",
298                            new ArrayList<String>()));
299
300                    // Handshake failure
301                    patterns.add(new ExceptionMessagePattern(
302                            SIGNATURE_ADD_TSA_HANDSHAKE_FAILURE,
303                            "Received fatal alert: handshake_failure",
304                            new ArrayList<String>()));
305
306                    OperationException oe = null;
307                    for (ExceptionMessagePattern p : patterns) {
308                        oe = p.getOperationException(exInner);
309                        if (oe != null) {
310                            break;
311                        }
312                    }
313                    if (oe == null) {
314                        ExceptionMessagePattern emp = new ExceptionMessagePattern(
315                                SIGNATURE_ADD_TSA_SSL_FATAL_ALERT,
316                                "Received fatal alert: (?<alert>.*)",
317                                Arrays.asList(new String[]{"alert"}));
318                        oe = emp.getOperationException(exInner);
319
320                        if (oe == null) {
321                            // Unknown exception
322                            oe = new OperationException(SIGNATURE_ADD_TSA_SSL_HANDSHAKE_EXCEPTION, exInner);
323                        }
324                    }
325                    assert oe != null;
326                    throw oe;
327                }
328
329                if (exInner instanceof SSLException) {
330                    ExceptionMessagePattern emp = new ExceptionMessagePattern(
331                            SSL_TRUSTSTORE_EMPTY,
332                            "java\\.lang\\.RuntimeException: Unexpected error: java\\.security\\.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty",
333                            new ArrayList<String>());
334                    OperationException oe = emp.getOperationException(exInner);
335                    if (oe != null) {
336                        throw oe;
337                    }
338                    throw new OperationException(SIGNATURE_ADD_FAIL, exInner);
339                }
340
341                if (exInner instanceof UnknownHostException
342                        || exInner instanceof FileNotFoundException) {
343                    String host = exInner.getMessage();
344                    throw new OperationException(SIGNATURE_ADD_TSA_UNREACHABLE,
345                            exInner,
346                            new SimpleEntry<String, Object>("host", host));
347                }
348
349                if (exInner instanceof SocketException) {
350                    ExceptionMessagePattern emp = new ExceptionMessagePattern(
351                            SSL_TRUSTSTORE_INCORRECT_TYPE,
352                            "java\\.security\\.NoSuchAlgorithmException: Error constructing implementation \\(algorithm: (?<algorithm>.*), provider: (?<provider>.*), class: (?<class>.*)\\)",
353                            Arrays.asList(new String[]{"algorithm", "provider", "class"}));
354                    OperationException oe = emp.getOperationException(exInner);
355                    if (oe != null) {
356                        throw oe;
357                    }
358                    throw new OperationException(SIGNATURE_ADD_FAIL, exInner);
359                }
360
361                Set<ExceptionMessagePattern> patterns = new HashSet<>();
362
363                // No username
364                // May also be returned if the username and password are incorrect.
365                patterns.add(new ExceptionMessagePattern(
366                        SIGNATURE_ADD_TSA_UNAUTHORIZED,
367                        "Server returned HTTP response code: 401 for URL: (?<url>.*)",
368                        Arrays.asList(new String[]{"url"})));
369
370                // Incorrect username or incorrect password
371                patterns.add(new ExceptionMessagePattern(
372                        SIGNATURE_ADD_TSA_LOGIN_FAIL,
373                        "Invalid TSA '(?<url>.*)' response, code (?<code>\\d+)",
374                        Arrays.asList(new String[]{"url", "code"})));
375
376                patterns.add(new ExceptionMessagePattern(
377                        SIGNATURE_ADD_FAIL,
378                        "unknown tag (?<tag>\\d+) encountered",
379                        Arrays.asList(new String[]{"tag"})));
380
381                OperationException oe = null;
382                for (ExceptionMessagePattern p : patterns) {
383                    oe = p.getOperationException(exInner);
384                    if (oe != null) {
385                        break;
386                    }
387                }
388                if (oe == null) {
389                    // Unknown exception
390                    oe = new OperationException(SIGNATURE_ADD_FAIL, exInner);
391                }
392                assert oe != null;
393                throw oe;
394            }
395            throw new OperationException(SIGNATURE_ADD_FAIL, exInner);
396        } catch (SignatureException ex) {
397            throw new OperationException(SIGNATURE_ADD_SIGNATURE_EXCEPTION, ex);
398        } catch (IOException | DocumentException | GeneralSecurityException ex) {
399            throw new OperationException(SIGNATURE_ADD_FAIL, ex);
400        } catch (NullPointerException ex) {
401            // Invalid digest algorithm?
402            throw new OperationException(SIGNATURE_ADD_FAIL, ex);
403        }
404        logger.info("Document successfully signed.");
405    }
406
407    private static Operation instance = null;
408
409    public static Operation getInstance() {
410        if (instance == null) {
411            instance = new OperationSignatureAdd();
412        }
413        return instance;
414    }
415
416    // Singleton
417    private OperationSignatureAdd() {
418        super();
419    }
420
421}