001/** 002 * Copyright 2019 Emmanuel Bourg 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign; 018 019import java.security.KeyStore; 020import java.security.KeyStoreException; 021import java.security.NoSuchAlgorithmException; 022import java.security.PrivateKey; 023import java.security.Provider; 024import java.security.PublicKey; 025import java.security.Security; 026import java.security.SignatureException; 027import java.security.UnrecoverableKeyException; 028import java.security.cert.Certificate; 029import java.security.cert.CertificateEncodingException; 030import java.security.cert.X509Certificate; 031import java.util.ArrayList; 032import java.util.List; 033 034import org.bouncycastle.asn1.ASN1Encodable; 035import org.bouncycastle.asn1.ASN1EncodableVector; 036import org.bouncycastle.asn1.DERNull; 037import org.bouncycastle.asn1.DERSet; 038import org.bouncycastle.asn1.cms.Attribute; 039import org.bouncycastle.asn1.cms.AttributeTable; 040import org.bouncycastle.asn1.cms.CMSAttributes; 041import org.bouncycastle.asn1.cms.ContentInfo; 042import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; 043import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 044import org.bouncycastle.cert.X509CertificateHolder; 045import org.bouncycastle.cert.jcajce.JcaCertStore; 046import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; 047import org.bouncycastle.cms.CMSAttributeTableGenerator; 048import org.bouncycastle.cms.CMSException; 049import org.bouncycastle.cms.CMSSignedData; 050import org.bouncycastle.cms.DefaultCMSSignatureEncryptionAlgorithmFinder; 051import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator; 052import org.bouncycastle.cms.SignerInfoGenerator; 053import org.bouncycastle.cms.SignerInfoGeneratorBuilder; 054import org.bouncycastle.cms.SignerInformation; 055import org.bouncycastle.cms.SignerInformationStore; 056import org.bouncycastle.cms.SignerInformationVerifier; 057import org.bouncycastle.cms.jcajce.JcaSignerInfoVerifierBuilder; 058import org.bouncycastle.operator.ContentSigner; 059import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; 060import org.bouncycastle.operator.DigestCalculatorProvider; 061import org.bouncycastle.operator.OperatorCreationException; 062import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 063 064import net.jsign.asn1.authenticode.AuthenticodeDigestCalculatorProvider; 065import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; 066import net.jsign.asn1.authenticode.AuthenticodeSignedDataGenerator; 067import net.jsign.asn1.authenticode.FilteredAttributeTableGenerator; 068import net.jsign.asn1.authenticode.SpcSpOpusInfo; 069import net.jsign.asn1.authenticode.SpcStatementType; 070import net.jsign.pe.DataDirectory; 071import net.jsign.pe.DataDirectoryType; 072import net.jsign.pe.PEFile; 073import net.jsign.timestamp.Timestamper; 074import net.jsign.timestamp.TimestampingMode; 075 076/** 077 * Sign a file with Authenticode. Timestamping is enabled by default and relies 078 * on the Sectigo server (http://timestamp.sectigo.com). 079 * 080 * <p>Example:</p> 081 * <pre> 082 * KeyStore keystore = new KeyStoreBuilder().keystore("keystore.p12").storepass("password").build(); 083 * 084 * AuthenticodeSigner signer = new AuthenticodeSigner(keystore, "alias", "secret"); 085 * signer.withProgramName("My Application") 086 * .withProgramURL("http://www.example.com") 087 * .withTimestamping(true) 088 * .withTimestampingAuthority("http://timestamp.sectigo.com"); 089 * 090 * try (Signable file = Signable.of(new File("application.exe"))) { 091 * signer.sign(file); 092 * } 093 * </pre> 094 * 095 * @author Emmanuel Bourg 096 * @since 3.0 097 */ 098public class AuthenticodeSigner { 099 100 protected Certificate[] chain; 101 protected PrivateKey privateKey; 102 protected DigestAlgorithm digestAlgorithm = DigestAlgorithm.getDefault(); 103 protected String signatureAlgorithm; 104 protected Provider signatureProvider; 105 protected String programName; 106 protected String programURL; 107 protected boolean replace; 108 protected boolean timestamping = true; 109 protected TimestampingMode tsmode = TimestampingMode.AUTHENTICODE; 110 protected String[] tsaurlOverride; 111 protected Timestamper timestamper; 112 protected int timestampingRetries = -1; 113 protected int timestampingRetryWait = -1; 114 115 /** 116 * Create a signer with the specified certificate chain and private key. 117 * 118 * @param chain the certificate chain. The first certificate is the signing certificate 119 * @param privateKey the private key 120 * @throws IllegalArgumentException if the chain is empty 121 */ 122 public AuthenticodeSigner(Certificate[] chain, PrivateKey privateKey) { 123 this.chain = chain; 124 this.privateKey = privateKey; 125 126 if (chain == null || chain.length == 0) { 127 throw new IllegalArgumentException("The certificate chain is empty"); 128 } 129 } 130 131 /** 132 * Create a signer with a certificate chain and private key from the specified keystore. 133 * 134 * @param keystore the keystore holding the certificate and the private key 135 * @param alias the alias of the certificate in the keystore 136 * @param password the password to get the private key 137 * @throws KeyStoreException if the keystore has not been initialized (loaded). 138 * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found 139 * @throws UnrecoverableKeyException if the key cannot be recovered (e.g., the given password is wrong). 140 */ 141 public AuthenticodeSigner(KeyStore keystore, String alias, String password) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException { 142 Certificate[] chain = keystore.getCertificateChain(alias); 143 if (chain == null) { 144 throw new IllegalArgumentException("No certificate found in the keystore with the alias '" + alias + "'"); 145 } 146 this.chain = chain; 147 this.privateKey = (PrivateKey) keystore.getKey(alias, password != null ? password.toCharArray() : null); 148 } 149 150 /** 151 * Set the program name embedded in the signature. 152 * 153 * @param programName the program name 154 * @return the current signer 155 */ 156 public AuthenticodeSigner withProgramName(String programName) { 157 this.programName = programName; 158 return this; 159 } 160 161 /** 162 * Set the program URL embedded in the signature. 163 * 164 * @param programURL the program URL 165 * @return the current signer 166 */ 167 public AuthenticodeSigner withProgramURL(String programURL) { 168 this.programURL = programURL; 169 return this; 170 } 171 172 /** 173 * Enable or disable the replacement of the previous signatures (disabled by default). 174 * 175 * @param replace <code>true</code> if the new signature should replace the existing ones, <code>false</code> to append it 176 * @return the current signer 177 * @since 2.0 178 */ 179 public AuthenticodeSigner withSignaturesReplaced(boolean replace) { 180 this.replace = replace; 181 return this; 182 } 183 184 /** 185 * Enable or disable the timestamping (enabled by default). 186 * 187 * @param timestamping <code>true</code> to enable timestamping, <code>false</code> to disable it 188 * @return the current signer 189 */ 190 public AuthenticodeSigner withTimestamping(boolean timestamping) { 191 this.timestamping = timestamping; 192 return this; 193 } 194 195 /** 196 * RFC3161 or Authenticode (Authenticode by default). 197 * 198 * @param tsmode the timestamping mode 199 * @return the current signer 200 * @since 1.3 201 */ 202 public AuthenticodeSigner withTimestampingMode(TimestampingMode tsmode) { 203 this.tsmode = tsmode; 204 return this; 205 } 206 207 /** 208 * Set the URL of the timestamping authority. Both RFC 3161 (as used for jar signing) 209 * and Authenticode timestamping services are supported. 210 * 211 * @param url the URL of the timestamping authority 212 * @return the current signer 213 * @since 2.1 214 */ 215 public AuthenticodeSigner withTimestampingAuthority(String url) { 216 return withTimestampingAuthority(new String[] { url }); 217 } 218 219 /** 220 * Set the URLs of the timestamping authorities. Both RFC 3161 (as used for jar signing) 221 * and Authenticode timestamping services are supported. 222 * 223 * @param urls the URLs of the timestamping authorities 224 * @return the current signer 225 * @since 2.1 226 */ 227 public AuthenticodeSigner withTimestampingAuthority(String... urls) { 228 this.tsaurlOverride = urls; 229 return this; 230 } 231 232 /** 233 * Set the Timestamper implementation. 234 * 235 * @param timestamper the timestamper implementation to use 236 * @return the current signer 237 */ 238 public AuthenticodeSigner withTimestamper(Timestamper timestamper) { 239 this.timestamper = timestamper; 240 return this; 241 } 242 243 /** 244 * Set the number of retries for timestamping. 245 * 246 * @param timestampingRetries the number of retries 247 * @return the current signer 248 */ 249 public AuthenticodeSigner withTimestampingRetries(int timestampingRetries) { 250 this.timestampingRetries = timestampingRetries; 251 return this; 252 } 253 254 /** 255 * Set the number of seconds to wait between timestamping retries. 256 * 257 * @param timestampingRetryWait the wait time between retries (in seconds) 258 * @return the current signer 259 */ 260 public AuthenticodeSigner withTimestampingRetryWait(int timestampingRetryWait) { 261 this.timestampingRetryWait = timestampingRetryWait; 262 return this; 263 } 264 265 /** 266 * Set the digest algorithm to use (SHA-256 by default) 267 * 268 * @param algorithm the digest algorithm 269 * @return the current signer 270 */ 271 public AuthenticodeSigner withDigestAlgorithm(DigestAlgorithm algorithm) { 272 if (algorithm != null) { 273 this.digestAlgorithm = algorithm; 274 } 275 return this; 276 } 277 278 /** 279 * Explicitly sets the signature algorithm to use. 280 * 281 * @param signatureAlgorithm the signature algorithm 282 * @return the current signer 283 * @since 2.0 284 */ 285 public AuthenticodeSigner withSignatureAlgorithm(String signatureAlgorithm) { 286 this.signatureAlgorithm = signatureAlgorithm; 287 return this; 288 } 289 290 /** 291 * Explicitly sets the signature algorithm and provider to use. 292 * 293 * @param signatureAlgorithm the signature algorithm 294 * @param signatureProvider the security provider for the specified algorithm 295 * @return the current signer 296 * @since 2.0 297 */ 298 public AuthenticodeSigner withSignatureAlgorithm(String signatureAlgorithm, String signatureProvider) { 299 return withSignatureAlgorithm(signatureAlgorithm, Security.getProvider(signatureProvider)); 300 } 301 302 /** 303 * Explicitly sets the signature algorithm and provider to use. 304 * 305 * @param signatureAlgorithm the signature algorithm 306 * @param signatureProvider the security provider for the specified algorithm 307 * @return the current signer 308 * @since 2.0 309 */ 310 public AuthenticodeSigner withSignatureAlgorithm(String signatureAlgorithm, Provider signatureProvider) { 311 this.signatureAlgorithm = signatureAlgorithm; 312 this.signatureProvider = signatureProvider; 313 return this; 314 } 315 316 /** 317 * Set the signature provider to use. 318 * 319 * @param signatureProvider the security provider for the signature algorithm 320 * @return the current signer 321 * @since 2.0 322 */ 323 public AuthenticodeSigner withSignatureProvider(Provider signatureProvider) { 324 this.signatureProvider = signatureProvider; 325 return this; 326 } 327 328 /** 329 * Sign the specified file. 330 * 331 * @param file the file to sign 332 * @throws Exception if signing fails 333 */ 334 public void sign(Signable file) throws Exception { 335 if (file instanceof PEFile) { 336 PEFile pefile = (PEFile) file; 337 338 if (replace) { 339 DataDirectory certificateTable = pefile.getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE); 340 if (certificateTable != null && !certificateTable.isTrailing()) { 341 // erase the previous signature 342 certificateTable.erase(); 343 certificateTable.write(0, 0); 344 } 345 } 346 } 347 348 CMSSignedData sigData = createSignedData(file); 349 350 if (!replace) { 351 List<CMSSignedData> signatures = file.getSignatures(); 352 if (!signatures.isEmpty()) { 353 // append the nested signature 354 sigData = addNestedSignature(signatures.get(0), sigData); 355 } 356 } 357 358 file.setSignature(sigData); 359 file.save(); 360 } 361 362 /** 363 * Create the PKCS7 message with the signature and the timestamp. 364 * 365 * @param file the file to sign 366 * @return the PKCS7 message with the signature and the timestamp 367 * @throws Exception if an error occurs 368 */ 369 protected CMSSignedData createSignedData(Signable file) throws Exception { 370 // compute the signature 371 ContentInfo contentInfo = file.createContentInfo(digestAlgorithm); 372 AuthenticodeSignedDataGenerator generator = createSignedDataGenerator(); 373 CMSSignedData sigData = generator.generate(contentInfo.getContentType(), contentInfo.getContent()); 374 375 // verify the signature 376 verify(sigData); 377 378 // timestamping 379 if (timestamping) { 380 Timestamper ts = timestamper; 381 if (ts == null) { 382 ts = Timestamper.create(tsmode); 383 } 384 if (tsaurlOverride != null) { 385 ts.setURLs(tsaurlOverride); 386 } 387 if (timestampingRetries != -1) { 388 ts.setRetries(timestampingRetries); 389 } 390 if (timestampingRetryWait != -1) { 391 ts.setRetryWait(timestampingRetryWait); 392 } 393 sigData = ts.timestamp(digestAlgorithm, sigData); 394 } 395 396 return sigData; 397 } 398 399 private AuthenticodeSignedDataGenerator createSignedDataGenerator() throws CMSException, OperatorCreationException, CertificateEncodingException { 400 // create content signer 401 final String sigAlg; 402 if (signatureAlgorithm != null) { 403 sigAlg = signatureAlgorithm; 404 } else if ("EC".equals(privateKey.getAlgorithm())) { 405 sigAlg = digestAlgorithm + "withECDSA"; 406 } else { 407 sigAlg = digestAlgorithm + "with" + privateKey.getAlgorithm(); 408 } 409 JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(sigAlg); 410 if (signatureProvider != null) { 411 contentSignerBuilder.setProvider(signatureProvider); 412 } 413 ContentSigner shaSigner = contentSignerBuilder.build(privateKey); 414 415 DigestCalculatorProvider digestCalculatorProvider = new AuthenticodeDigestCalculatorProvider(); 416 417 // prepare the authenticated attributes 418 CMSAttributeTableGenerator attributeTableGenerator = new DefaultSignedAttributeTableGenerator(createAuthenticatedAttributes()); 419 attributeTableGenerator = new FilteredAttributeTableGenerator(attributeTableGenerator, CMSAttributes.signingTime, CMSAttributes.cmsAlgorithmProtect); 420 421 // fetch the signing certificate 422 X509CertificateHolder certificate = new JcaX509CertificateHolder((X509Certificate) chain[0]); 423 424 // prepare the signerInfo with the extra authenticated attributes 425 SignerInfoGeneratorBuilder signerInfoGeneratorBuilder = new SignerInfoGeneratorBuilder(digestCalculatorProvider, new DefaultCMSSignatureEncryptionAlgorithmFinder(){ 426 @Override 427 public AlgorithmIdentifier findEncryptionAlgorithm(final AlgorithmIdentifier signatureAlgorithm) { 428 //enforce "RSA" instead of "shaXXXRSA" for digest signature to be more like signtool 429 if (signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha256WithRSAEncryption) || 430 signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha384WithRSAEncryption) || 431 signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha512WithRSAEncryption)) { 432 return new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE); 433 } else { 434 return super.findEncryptionAlgorithm(signatureAlgorithm); 435 } 436 } 437 }); 438 signerInfoGeneratorBuilder.setSignedAttributeGenerator(attributeTableGenerator); 439 signerInfoGeneratorBuilder.setContentDigest(createContentDigestAlgorithmIdentifier(shaSigner.getAlgorithmIdentifier())); 440 SignerInfoGenerator signerInfoGenerator = signerInfoGeneratorBuilder.build(shaSigner, certificate); 441 442 AuthenticodeSignedDataGenerator generator = new AuthenticodeSignedDataGenerator(); 443 generator.addCertificates(new JcaCertStore(removeRoot(chain))); 444 generator.addSignerInfoGenerator(signerInfoGenerator); 445 446 return generator; 447 } 448 449 /** 450 * Remove the root certificate from the chain, unless the chain consists in a single self signed certificate. 451 * 452 * @param certificates the certificate chain to process 453 * @return the certificate chain without the root certificate 454 */ 455 private List<Certificate> removeRoot(Certificate[] certificates) { 456 List<Certificate> list = new ArrayList<>(); 457 458 if (certificates.length == 1) { 459 list.add(certificates[0]); 460 } else { 461 for (Certificate certificate : certificates) { 462 if (!isSelfSigned((X509Certificate) certificate)) { 463 list.add(certificate); 464 } 465 } 466 } 467 468 return list; 469 } 470 471 private boolean isSelfSigned(X509Certificate certificate) { 472 return certificate.getSubjectDN().equals(certificate.getIssuerDN()); 473 } 474 475 private void verify(CMSSignedData signedData) throws SignatureException, OperatorCreationException { 476 PublicKey publicKey = chain[0].getPublicKey(); 477 DigestCalculatorProvider digestCalculatorProvider = new AuthenticodeDigestCalculatorProvider(); 478 SignerInformationVerifier verifier = new JcaSignerInfoVerifierBuilder(digestCalculatorProvider).build(publicKey); 479 480 boolean result = false; 481 Throwable cause = null; 482 try { 483 result = signedData.verifySignatures(signerId -> verifier, false); 484 } catch (Exception e) { 485 cause = e; 486 while (cause.getCause() != null) { 487 cause = cause.getCause(); 488 } 489 } 490 491 if (!result) { 492 throw new SignatureException("Signature verification failed, the private key doesn't match the certificate", cause); 493 } 494 } 495 496 /** 497 * Creates the authenticated attributes for the SignerInfo section of the signature. 498 * 499 * @return the authenticated attributes 500 */ 501 private AttributeTable createAuthenticatedAttributes() { 502 List<Attribute> attributes = new ArrayList<>(); 503 504 SpcStatementType spcStatementType = new SpcStatementType(AuthenticodeObjectIdentifiers.SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID); 505 attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_STATEMENT_TYPE_OBJID, new DERSet(spcStatementType))); 506 507 SpcSpOpusInfo spcSpOpusInfo = new SpcSpOpusInfo(programName, programURL); 508 attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_SP_OPUS_INFO_OBJID, new DERSet(spcSpOpusInfo))); 509 510 return new AttributeTable(new DERSet(attributes.toArray(new ASN1Encodable[0]))); 511 } 512 513 /** 514 * Embed a signature as an unsigned attribute of an existing signature. 515 * 516 * @param primary the root signature hosting the nested secondary signature 517 * @param secondary the additional signature to nest inside the primary one 518 * @return the signature combining the specified signatures 519 */ 520 protected CMSSignedData addNestedSignature(CMSSignedData primary, CMSSignedData secondary) { 521 SignerInformation signerInformation = primary.getSignerInfos().getSigners().iterator().next(); 522 523 AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); 524 if (unsignedAttributes == null) { 525 unsignedAttributes = new AttributeTable(new DERSet()); 526 } 527 Attribute nestedSignaturesAttribute = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); 528 if (nestedSignaturesAttribute == null) { 529 // first nested signature 530 unsignedAttributes = unsignedAttributes.add(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID, secondary.toASN1Structure()); 531 } else { 532 // append the signature to the previous nested signatures 533 ASN1EncodableVector nestedSignatures = new ASN1EncodableVector(); 534 for (ASN1Encodable nestedSignature : nestedSignaturesAttribute.getAttrValues()) { 535 nestedSignatures.add(nestedSignature); 536 } 537 nestedSignatures.add(secondary.toASN1Structure()); 538 539 ASN1EncodableVector attributes = unsignedAttributes.remove(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID).toASN1EncodableVector(); 540 attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID, new DERSet(nestedSignatures))); 541 542 unsignedAttributes = new AttributeTable(attributes); 543 } 544 545 signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); 546 return CMSSignedData.replaceSigners(primary, new SignerInformationStore(signerInformation)); 547 } 548 549 /** 550 * Create the digest algorithm identifier to use as content digest. 551 * By default looks up the default identifier but also makes sure it includes 552 * the algorithm parameters and if not includes a DER NULL in order to align 553 * with what signtool currently does. 554 * 555 * @param signatureAlgorithm to get the corresponding digest algorithm identifier for 556 * @return an AlgorithmIdentifier for the digestAlgorithm and including parameters 557 */ 558 protected AlgorithmIdentifier createContentDigestAlgorithmIdentifier(AlgorithmIdentifier signatureAlgorithm) { 559 AlgorithmIdentifier ai = new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm); 560 if (ai.getParameters() == null) { 561 // Always include parameters to align with what signtool does 562 ai = new AlgorithmIdentifier(ai.getAlgorithm(), DERNull.INSTANCE); 563 } 564 return ai; 565 } 566}