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}