View Javadoc
1   /* 
2    * Copyright (C) 2016 Hobrasoft s.r.o.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Affero General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Affero General Public License for more details.
13   *
14   * You should have received a copy of the GNU Affero General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  package cz.hobrasoft.pdfmu.operation.signature;
18  
19  import com.itextpdf.text.DocumentException;
20  import com.itextpdf.text.ExceptionConverter;
21  import com.itextpdf.text.pdf.PdfSignatureAppearance;
22  import com.itextpdf.text.pdf.PdfStamper;
23  import com.itextpdf.text.pdf.security.BouncyCastleDigest;
24  import com.itextpdf.text.pdf.security.CrlClient;
25  import com.itextpdf.text.pdf.security.DigestAlgorithms;
26  import com.itextpdf.text.pdf.security.ExternalDigest;
27  import com.itextpdf.text.pdf.security.ExternalSignature;
28  import com.itextpdf.text.pdf.security.MakeSignature;
29  import com.itextpdf.text.pdf.security.OcspClient;
30  import com.itextpdf.text.pdf.security.PrivateKeySignature;
31  import com.itextpdf.text.pdf.security.TSAClient;
32  import cz.hobrasoft.pdfmu.ExceptionMessagePattern;
33  import cz.hobrasoft.pdfmu.PdfmuUtils;
34  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_FAIL;
35  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_SIGNATURE_EXCEPTION;
36  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_BAD_CERTIFICATE;
37  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_HANDSHAKE_FAILURE;
38  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_LOGIN_FAIL;
39  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_SSL_FATAL_ALERT;
40  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_SSL_HANDSHAKE_EXCEPTION;
41  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNAUTHORIZED;
42  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNREACHABLE;
43  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_TSA_UNTRUSTED;
44  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_UNSUPPORTED_DIGEST_ALGORITHM;
45  import static cz.hobrasoft.pdfmu.error.ErrorType.SSL_TRUSTSTORE_EMPTY;
46  import static cz.hobrasoft.pdfmu.error.ErrorType.SSL_TRUSTSTORE_INCORRECT_TYPE;
47  import cz.hobrasoft.pdfmu.jackson.SignatureAdd;
48  import cz.hobrasoft.pdfmu.operation.Operation;
49  import cz.hobrasoft.pdfmu.operation.OperationCommon;
50  import cz.hobrasoft.pdfmu.operation.OperationException;
51  import cz.hobrasoft.pdfmu.operation.args.InOutPdfArgs;
52  import java.io.FileNotFoundException;
53  import java.io.IOException;
54  import java.net.SocketException;
55  import java.net.UnknownHostException;
56  import java.security.GeneralSecurityException;
57  import java.security.KeyStore;
58  import java.security.PrivateKey;
59  import java.security.Provider;
60  import java.security.Security;
61  import java.security.SignatureException;
62  import java.security.cert.Certificate;
63  import java.util.AbstractMap.SimpleEntry;
64  import java.util.ArrayList;
65  import java.util.Arrays;
66  import java.util.Collection;
67  import java.util.HashSet;
68  import java.util.Set;
69  import java.util.logging.Logger;
70  import javax.net.ssl.SSLException;
71  import javax.net.ssl.SSLHandshakeException;
72  import net.sourceforge.argparse4j.inf.Namespace;
73  import net.sourceforge.argparse4j.inf.Subparser;
74  import org.bouncycastle.jce.provider.BouncyCastleProvider;
75  
76  /**
77   * Adds a digital signature to a PDF document
78   *
79   * @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a>
80   */
81  public class OperationSignatureAdd extends OperationCommon {
82  
83      private static final Logger logger = Logger.getLogger(OperationSignatureAdd.class.getName());
84  
85      private final InOutPdfArgs inout = new InOutPdfArgs();
86  
87      @Override
88      public Subparser configureSubparser(Subparser subparser) {
89          String help = "Add a digital signature to a PDF document";
90  
91          // Configure the subparser
92          subparser.help(help)
93                  .description(help)
94                  .defaultHelp(true);
95  
96          inout.addArguments(subparser);
97          signatureParameters.addArguments(subparser);
98  
99          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 }