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}