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