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.Arrays; 033import java.util.Collection; 034import java.util.List; 035 036import org.bouncycastle.asn1.ASN1Encodable; 037import org.bouncycastle.asn1.DERNull; 038import org.bouncycastle.asn1.DERSet; 039import org.bouncycastle.asn1.cms.Attribute; 040import org.bouncycastle.asn1.cms.AttributeTable; 041import org.bouncycastle.asn1.cms.CMSAttributes; 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.CMSSignedDataGenerator; 051import org.bouncycastle.cms.CMSTypedData; 052import org.bouncycastle.cms.DefaultCMSSignatureEncryptionAlgorithmFinder; 053import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator; 054import org.bouncycastle.cms.SignerInfoGenerator; 055import org.bouncycastle.cms.SignerInfoGeneratorBuilder; 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; 063import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 064 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.jca.SigningServiceJcaProvider; 071import net.jsign.nuget.NugetFile; 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 * <p>Example:</p> 080 * <pre> 081 * KeyStore keystore = new KeyStoreBuilder().keystore("keystore.p12").storepass("password").build(); 082 * 083 * AuthenticodeSigner signer = new AuthenticodeSigner(keystore, "alias", "secret"); 084 * signer.withProgramName("My Application") 085 * .withProgramURL("http://www.example.com") 086 * .withTimestamping(true) 087 * .withTimestampingAuthority("http://timestamp.sectigo.com"); 088 * 089 * try (Signable file = Signable.of(new File("application.exe"))) { 090 * signer.sign(file); 091 * } 092 * </pre> 093 * 094 * @author Emmanuel Bourg 095 * @since 3.0 096 */ 097public class AuthenticodeSigner { 098 099 protected Certificate[] chain; 100 protected PrivateKey privateKey; 101 protected DigestAlgorithm digestAlgorithm = DigestAlgorithm.getDefault(); 102 protected String signatureAlgorithm; 103 protected Provider signatureProvider; 104 protected String programName; 105 protected String programURL; 106 protected boolean replace; 107 protected boolean timestamping = true; 108 protected TimestampingMode tsmode = TimestampingMode.AUTHENTICODE; 109 protected String[] tsaurlOverride; 110 protected Timestamper timestamper; 111 protected int timestampingRetries = -1; 112 protected int timestampingRetryWait = -1; 113 114 /** 115 * Create a signer with the specified certificate chain and private key. 116 * 117 * @param chain the certificate chain. The first certificate is the signing certificate 118 * @param privateKey the private key 119 * @throws IllegalArgumentException if the chain is empty 120 */ 121 public AuthenticodeSigner(Certificate[] chain, PrivateKey privateKey) { 122 this.chain = chain; 123 this.privateKey = privateKey; 124 125 if (chain == null || chain.length == 0) { 126 throw new IllegalArgumentException("The certificate chain is empty"); 127 } 128 } 129 130 /** 131 * Create a signer with a certificate chain and private key from the specified keystore. 132 * 133 * @param keystore the keystore holding the certificate and the private key 134 * @param alias the alias of the certificate in the keystore 135 * @param password the password to get the private key 136 * @throws KeyStoreException if the keystore has not been initialized (loaded). 137 * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found 138 * @throws UnrecoverableKeyException if the key cannot be recovered (e.g., the given password is wrong). 139 */ 140 public AuthenticodeSigner(KeyStore keystore, String alias, String password) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException { 141 Certificate[] chain = keystore.getCertificateChain(alias); 142 if (chain == null) { 143 throw new IllegalArgumentException("No certificate found in the keystore with the alias '" + alias + "'"); 144 } 145 this.chain = chain; 146 this.privateKey = (PrivateKey) keystore.getKey(alias, password != null ? password.toCharArray() : null); 147 148 Provider provider = keystore.getProvider(); 149 if (provider.getName().startsWith("SunPKCS11") || provider instanceof SigningServiceJcaProvider) { 150 this.signatureProvider = provider; 151 } 152 } 153 154 /** 155 * Set the program name embedded in the signature. 156 * 157 * @param programName the program name 158 * @return the current signer 159 */ 160 public AuthenticodeSigner withProgramName(String programName) { 161 this.programName = programName; 162 return this; 163 } 164 165 /** 166 * Set the program URL embedded in the signature. 167 * 168 * @param programURL the program URL 169 * @return the current signer 170 */ 171 public AuthenticodeSigner withProgramURL(String programURL) { 172 this.programURL = programURL; 173 return this; 174 } 175 176 /** 177 * Enable or disable the replacement of the previous signatures (disabled by default). 178 * 179 * @param replace <code>true</code> if the new signature should replace the existing ones, <code>false</code> to append it 180 * @return the current signer 181 * @since 2.0 182 */ 183 public AuthenticodeSigner withSignaturesReplaced(boolean replace) { 184 this.replace = replace; 185 return this; 186 } 187 188 /** 189 * Enable or disable the timestamping (enabled by default). 190 * 191 * @param timestamping <code>true</code> to enable timestamping, <code>false</code> to disable it 192 * @return the current signer 193 */ 194 public AuthenticodeSigner withTimestamping(boolean timestamping) { 195 this.timestamping = timestamping; 196 return this; 197 } 198 199 /** 200 * RFC3161 or Authenticode (Authenticode by default). 201 * 202 * @param tsmode the timestamping mode 203 * @return the current signer 204 * @since 1.3 205 */ 206 public AuthenticodeSigner withTimestampingMode(TimestampingMode tsmode) { 207 this.tsmode = tsmode; 208 return this; 209 } 210 211 /** 212 * Set the URL of the timestamping authority. Both RFC 3161 (as used for jar signing) 213 * and Authenticode timestamping services are supported. 214 * 215 * @param url the URL of the timestamping authority 216 * @return the current signer 217 * @since 2.1 218 */ 219 public AuthenticodeSigner withTimestampingAuthority(String url) { 220 return withTimestampingAuthority(new String[] { url }); 221 } 222 223 /** 224 * Set the URLs of the timestamping authorities. Both RFC 3161 (as used for jar signing) 225 * and Authenticode timestamping services are supported. 226 * 227 * @param urls the URLs of the timestamping authorities 228 * @return the current signer 229 * @since 2.1 230 */ 231 public AuthenticodeSigner withTimestampingAuthority(String... urls) { 232 this.tsaurlOverride = urls; 233 return this; 234 } 235 236 /** 237 * Set the Timestamper implementation. 238 * 239 * @param timestamper the timestamper implementation to use 240 * @return the current signer 241 */ 242 public AuthenticodeSigner withTimestamper(Timestamper timestamper) { 243 this.timestamper = timestamper; 244 return this; 245 } 246 247 /** 248 * Set the number of retries for timestamping. 249 * 250 * @param timestampingRetries the number of retries 251 * @return the current signer 252 */ 253 public AuthenticodeSigner withTimestampingRetries(int timestampingRetries) { 254 this.timestampingRetries = timestampingRetries; 255 return this; 256 } 257 258 /** 259 * Set the number of seconds to wait between timestamping retries. 260 * 261 * @param timestampingRetryWait the wait time between retries (in seconds) 262 * @return the current signer 263 */ 264 public AuthenticodeSigner withTimestampingRetryWait(int timestampingRetryWait) { 265 this.timestampingRetryWait = timestampingRetryWait; 266 return this; 267 } 268 269 /** 270 * Set the digest algorithm to use (SHA-256 by default) 271 * 272 * @param algorithm the digest algorithm 273 * @return the current signer 274 */ 275 public AuthenticodeSigner withDigestAlgorithm(DigestAlgorithm algorithm) { 276 if (algorithm != null) { 277 this.digestAlgorithm = algorithm; 278 } 279 return this; 280 } 281 282 /** 283 * Explicitly sets the signature algorithm to use. 284 * 285 * @param signatureAlgorithm the signature algorithm 286 * @return the current signer 287 * @since 2.0 288 */ 289 public AuthenticodeSigner withSignatureAlgorithm(String signatureAlgorithm) { 290 this.signatureAlgorithm = signatureAlgorithm; 291 return this; 292 } 293 294 /** 295 * Returns the signature algorithm to use. 296 */ 297 private String getSignatureAlgorithm() { 298 if (signatureAlgorithm != null) { 299 return signatureAlgorithm; 300 } else if ("EC".equals(privateKey.getAlgorithm())) { 301 return digestAlgorithm + "withECDSA"; 302 } else if ("EdDSA".equals(privateKey.getAlgorithm())) { 303 X509Certificate certificate = (X509Certificate) chain[0]; 304 PublicKey publicKey = certificate.getPublicKey(); 305 if (publicKey.toString().contains("Ed25519")) { 306 return "Ed25519"; 307 } else if (publicKey.toString().contains("Ed448")) { 308 return "Ed448"; 309 } 310 // return ((EdECKey) publicKey).getParams().getName(); // todo with Java 15+ 311 } 312 313 return digestAlgorithm + "with" + privateKey.getAlgorithm(); 314 } 315 316 /** 317 * Explicitly sets the signature algorithm and provider to use. 318 * 319 * @param signatureAlgorithm the signature algorithm 320 * @param signatureProvider the security provider for the specified algorithm 321 * @return the current signer 322 * @since 2.0 323 */ 324 public AuthenticodeSigner withSignatureAlgorithm(String signatureAlgorithm, String signatureProvider) { 325 return withSignatureAlgorithm(signatureAlgorithm, Security.getProvider(signatureProvider)); 326 } 327 328 /** 329 * Explicitly sets the signature algorithm and provider to use. 330 * 331 * @param signatureAlgorithm the signature algorithm 332 * @param signatureProvider the security provider for the specified algorithm 333 * @return the current signer 334 * @since 2.0 335 */ 336 public AuthenticodeSigner withSignatureAlgorithm(String signatureAlgorithm, Provider signatureProvider) { 337 this.signatureAlgorithm = signatureAlgorithm; 338 this.signatureProvider = signatureProvider; 339 return this; 340 } 341 342 /** 343 * Set the signature provider to use. 344 * 345 * @param signatureProvider the security provider for the signature algorithm 346 * @return the current signer 347 * @since 2.0 348 */ 349 public AuthenticodeSigner withSignatureProvider(Provider signatureProvider) { 350 this.signatureProvider = signatureProvider; 351 return this; 352 } 353 354 /** 355 * Sign the specified file. 356 * 357 * @param file the file to sign 358 * @throws Exception if signing fails 359 */ 360 public void sign(Signable file) throws Exception { 361 file.validate(chain[0]); 362 363 if (file instanceof NugetFile && !replace) { 364 List<CMSSignedData> signatures = file.getSignatures(); 365 if (!signatures.isEmpty()) { 366 throw new SignerException("The file is already signed, the existing signature must be replaced explicitly"); 367 } 368 } 369 370 CMSSignedData sigData = createSignedData(file); 371 372 List<CMSSignedData> signatures = new ArrayList<>(); 373 if (!replace) { 374 signatures.addAll(file.getSignatures()); 375 } 376 signatures.add(sigData); 377 378 file.setSignatures(signatures); 379 file.save(); 380 } 381 382 /** 383 * Create the PKCS7 message with the signature and the timestamp. 384 * 385 * @param file the file to sign 386 * @return the PKCS7 message with the signature and the timestamp 387 * @throws Exception if an error occurs 388 */ 389 protected CMSSignedData createSignedData(Signable file) throws Exception { 390 // compute the signature 391 CMSTypedData contentInfo = file.createSignedContent(digestAlgorithm); 392 CMSSignedDataGenerator generator = createSignedDataGenerator(file, contentInfo); 393 CMSSignedData sigData = generator.generate(contentInfo, true); 394 395 // verify the signature 396 verify(sigData); 397 398 // timestamping 399 if (timestamping) { 400 Timestamper ts = timestamper; 401 if (ts == null) { 402 boolean authenticode = AuthenticodeObjectIdentifiers.isAuthenticode(sigData.getSignedContentTypeOID()); 403 ts = Timestamper.create(authenticode ? tsmode : TimestampingMode.RFC3161); 404 } 405 if (tsaurlOverride != null) { 406 ts.setURLs(tsaurlOverride); 407 } 408 if (timestampingRetries != -1) { 409 ts.setRetries(timestampingRetries); 410 } 411 if (timestampingRetryWait != -1) { 412 ts.setRetryWait(timestampingRetryWait); 413 } 414 sigData = ts.timestamp(digestAlgorithm, sigData); 415 } 416 417 return sigData; 418 } 419 420 private CMSSignedDataGenerator createSignedDataGenerator(Signable file, CMSTypedData contentInfo) throws CMSException, OperatorCreationException, CertificateEncodingException { 421 List<X509Certificate> fullChain = CertificateUtils.getFullCertificateChain((Collection) Arrays.asList(chain)); 422 if (fullChain.size() > 1) { 423 fullChain.removeIf(CertificateUtils::isSelfSigned); 424 } 425 426 boolean authenticode = AuthenticodeObjectIdentifiers.isAuthenticode(contentInfo.getContentType().getId()); 427 CMSSignedDataGenerator generator = authenticode ? new AuthenticodeSignedDataGenerator() : new CMSSignedDataGenerator(); 428 generator.addCertificates(new JcaCertStore(fullChain)); 429 generator.addSignerInfoGenerator(createSignerInfoGenerator(file, authenticode)); 430 431 return generator; 432 } 433 434 private SignerInfoGenerator createSignerInfoGenerator(Signable file, boolean authenticode) throws OperatorCreationException, CertificateEncodingException { 435 // create content signer 436 JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(getSignatureAlgorithm()); 437 if (signatureProvider != null) { 438 contentSignerBuilder.setProvider(signatureProvider); 439 } 440 ContentSigner shaSigner = contentSignerBuilder.build(privateKey); 441 442 DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); 443 444 // prepare the authenticated attributes 445 List<Attribute> attributes = new ArrayList<>(authenticode ? createAuthenticatedAttributes() : file.createSignedAttributes((X509Certificate) chain[0])); 446 AttributeTable attributeTable = new AttributeTable(new DERSet(attributes.toArray(new ASN1Encodable[0]))); 447 CMSAttributeTableGenerator attributeTableGenerator = new DefaultSignedAttributeTableGenerator(attributeTable); 448 if (authenticode) { 449 attributeTableGenerator = new FilteredAttributeTableGenerator(attributeTableGenerator, CMSAttributes.cmsAlgorithmProtect, CMSAttributes.signingTime); 450 } else { 451 attributeTableGenerator = new FilteredAttributeTableGenerator(attributeTableGenerator, CMSAttributes.cmsAlgorithmProtect); 452 } 453 454 // fetch the signing certificate 455 X509CertificateHolder certificate = new JcaX509CertificateHolder((X509Certificate) chain[0]); 456 457 // prepare the signerInfo with the extra authenticated attributes 458 SignerInfoGeneratorBuilder signerInfoGeneratorBuilder = new SignerInfoGeneratorBuilder(digestCalculatorProvider, new DefaultCMSSignatureEncryptionAlgorithmFinder(){ 459 @Override 460 public AlgorithmIdentifier findEncryptionAlgorithm(final AlgorithmIdentifier signatureAlgorithm) { 461 //enforce "RSA" instead of "shaXXXRSA" for digest signature to be more like signtool 462 if (signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha256WithRSAEncryption) || 463 signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha384WithRSAEncryption) || 464 signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha512WithRSAEncryption)) { 465 return new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE); 466 } else { 467 return super.findEncryptionAlgorithm(signatureAlgorithm); 468 } 469 } 470 }); 471 signerInfoGeneratorBuilder.setSignedAttributeGenerator(attributeTableGenerator); 472 signerInfoGeneratorBuilder.setContentDigest(createContentDigestAlgorithmIdentifier(shaSigner.getAlgorithmIdentifier())); 473 return signerInfoGeneratorBuilder.build(shaSigner, certificate); 474 } 475 476 private void verify(CMSSignedData signedData) throws SignatureException, OperatorCreationException { 477 X509Certificate certificate = (X509Certificate) chain[0]; 478 PublicKey publicKey = certificate.getPublicKey(); 479 DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); 480 SignerInformationVerifier verifier = new JcaSignerInfoVerifierBuilder(digestCalculatorProvider).build(publicKey); 481 482 boolean result = false; 483 Throwable cause = null; 484 try { 485 result = signedData.verifySignatures(signerId -> verifier, false); 486 } catch (Exception e) { 487 cause = e; 488 while (cause.getCause() != null) { 489 cause = cause.getCause(); 490 } 491 } 492 493 if (!result) { 494 boolean ca = certificate.getBasicConstraints() != -1; 495 String message = "Signature verification failed, "; 496 if (ca) { 497 message += "the certificate is a root or intermediate CA certificate (" + certificate.getSubjectX500Principal() + ")"; 498 } else { 499 message += "the private key doesn't match the certificate"; 500 } 501 throw new SignatureException(message, cause); 502 } 503 } 504 505 /** 506 * Creates the authenticated attributes for the SignerInfo section of the signature. 507 * 508 * @return the authenticated attributes 509 */ 510 private List<Attribute> createAuthenticatedAttributes() { 511 List<Attribute> attributes = new ArrayList<>(); 512 513 SpcStatementType spcStatementType = new SpcStatementType(AuthenticodeObjectIdentifiers.SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID); 514 attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_STATEMENT_TYPE_OBJID, new DERSet(spcStatementType))); 515 516 if (programName != null || programURL != null) { 517 SpcSpOpusInfo spcSpOpusInfo = new SpcSpOpusInfo(programName, programURL); 518 attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_SP_OPUS_INFO_OBJID, new DERSet(spcSpOpusInfo))); 519 } 520 521 return attributes; 522 } 523 524 /** 525 * Create the digest algorithm identifier to use as content digest. 526 * By default looks up the default identifier but also makes sure it includes 527 * the algorithm parameters and if not includes a DER NULL in order to align 528 * with what signtool currently does. 529 * 530 * @param signatureAlgorithm to get the corresponding digest algorithm identifier for 531 * @return an AlgorithmIdentifier for the digestAlgorithm and including parameters 532 */ 533 protected AlgorithmIdentifier createContentDigestAlgorithmIdentifier(AlgorithmIdentifier signatureAlgorithm) { 534 if ("1.3.101.112".equals(signatureAlgorithm.getAlgorithm().getId()) // Ed25519 535 || "1.3.101.113".equals(signatureAlgorithm.getAlgorithm().getId())) { // Ed448 536 return new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); 537 } 538 539 AlgorithmIdentifier ai = new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm); 540 if (ai.getParameters() == null) { 541 // Always include parameters to align with what signtool does 542 ai = new AlgorithmIdentifier(ai.getAlgorithm(), DERNull.INSTANCE); 543 } 544 return ai; 545 } 546}