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