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