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            // pad the file on a 8 byte boundary (signtool refuses to sign files not properly padded)
322            // todo only if there was no previous certificate table
323            pefile.pad(8);
324
325            if (replace) {
326                DataDirectory certificateTable = pefile.getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
327                if (certificateTable != null && !certificateTable.isTrailing()) {
328                    // erase the previous signature
329                    certificateTable.erase();
330                    certificateTable.write(0, 0);
331                }
332            }
333
334        } else if (file instanceof MSIFile) {
335            MSIFile msi = (MSIFile) file;
336            
337            if (!replace && msi.hasExtendedSignature()) {
338                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");
339            }
340        }
341        
342        CMSSignedData sigData = createSignedData(file);
343        
344        if (!replace) {
345            List<CMSSignedData> signatures = file.getSignatures();
346            if (!signatures.isEmpty()) {
347                // append the nested signature
348                sigData = addNestedSignature(signatures.get(0), sigData);
349            }
350        }
351        
352        file.setSignature(sigData);
353        
354        file.save();
355        
356        if (file instanceof Closeable) {
357            ((Closeable) file).close();
358        }
359    }
360
361    /**
362     * Create the PKCS7 message with the signature and the timestamp.
363     * 
364     * @param file the file to sign
365     * @return the PKCS7 message with the signature and the timestamp
366     * @throws Exception if an error occurs
367     */
368    protected CMSSignedData createSignedData(Signable file) throws Exception {
369        // compute the signature
370        AuthenticodeSignedDataGenerator generator = createSignedDataGenerator();
371        CMSSignedData sigData = generator.generate(AuthenticodeObjectIdentifiers.SPC_INDIRECT_DATA_OBJID, file.createIndirectData(digestAlgorithm));
372        
373        // verify the signature
374        DigestCalculatorProvider digestCalculatorProvider = new AuthenticodeDigestCalculatorProvider();
375        SignerInformationVerifier verifier = new JcaSignerInfoVerifierBuilder(digestCalculatorProvider).build(chain[0].getPublicKey());
376        sigData.getSignerInfos().iterator().next().verify(verifier);
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 = digestAlgorithm + "with" + privateKey.getAlgorithm();
404        } else {
405            sigAlg = signatureAlgorithm;
406        }
407        JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(sigAlg);
408        if (signatureProvider != null) {
409            contentSignerBuilder.setProvider(signatureProvider);
410        }
411        ContentSigner shaSigner = contentSignerBuilder.build(privateKey);
412
413        DigestCalculatorProvider digestCalculatorProvider = new AuthenticodeDigestCalculatorProvider();
414        
415        // prepare the authenticated attributes
416        CMSAttributeTableGenerator attributeTableGenerator = new DefaultSignedAttributeTableGenerator(createAuthenticatedAttributes());
417        attributeTableGenerator = new FilteredAttributeTableGenerator(attributeTableGenerator, CMSAttributes.signingTime, CMSAttributes.cmsAlgorithmProtect);
418        
419        // fetch the signing certificate
420        X509CertificateHolder certificate = new JcaX509CertificateHolder((X509Certificate) chain[0]);
421        
422        // prepare the signerInfo with the extra authenticated attributes
423        SignerInfoGeneratorBuilder signerInfoGeneratorBuilder = new SignerInfoGeneratorBuilder(digestCalculatorProvider, new DefaultCMSSignatureEncryptionAlgorithmFinder(){
424            @Override
425            public AlgorithmIdentifier findEncryptionAlgorithm(final AlgorithmIdentifier signatureAlgorithm) {
426                //enforce "RSA" instead of "shaXXXRSA" for digest signature to be more like signtool
427                if (signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha256WithRSAEncryption) ||
428                    signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha384WithRSAEncryption) ||
429                    signatureAlgorithm.getAlgorithm().equals(PKCSObjectIdentifiers.sha512WithRSAEncryption)) {
430                    return new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE);
431                } else {
432                    return super.findEncryptionAlgorithm(signatureAlgorithm);
433                }
434            }
435        });
436        signerInfoGeneratorBuilder.setSignedAttributeGenerator(attributeTableGenerator);
437        SignerInfoGenerator signerInfoGenerator = signerInfoGeneratorBuilder.build(shaSigner, certificate);
438        
439        AuthenticodeSignedDataGenerator generator = new AuthenticodeSignedDataGenerator();
440        generator.addCertificates(new JcaCertStore(removeRoot(chain)));
441        generator.addSignerInfoGenerator(signerInfoGenerator);
442        
443        return generator;
444    }
445
446    /**
447     * Remove the root certificate from the chain, unless the chain consists in a single self signed certificate.
448     * 
449     * @param certificates the certificate chain to process
450     * @return the certificate chain without the root certificate
451     */
452    private List<Certificate> removeRoot(Certificate[] certificates) {
453        List<Certificate> list = new ArrayList<>();
454        
455        if (certificates.length == 1) {
456            list.add(certificates[0]);
457        } else {
458            for (Certificate certificate : certificates) {
459                if (!isSelfSigned((X509Certificate) certificate)) {
460                    list.add(certificate);
461                }
462            }
463        }
464        
465        return list;
466    }
467
468    private boolean isSelfSigned(X509Certificate certificate) {
469        return certificate.getSubjectDN().equals(certificate.getIssuerDN());
470    }
471
472    /**
473     * Creates the authenticated attributes for the SignerInfo section of the signature.
474     * 
475     * @return the authenticated attributes
476     */
477    private AttributeTable createAuthenticatedAttributes() {
478        List<Attribute> attributes = new ArrayList<>();
479        
480        SpcStatementType spcStatementType = new SpcStatementType(AuthenticodeObjectIdentifiers.SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID);
481        attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_STATEMENT_TYPE_OBJID, new DERSet(spcStatementType)));
482        
483        SpcSpOpusInfo spcSpOpusInfo = new SpcSpOpusInfo(programName, programURL);
484        attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_SP_OPUS_INFO_OBJID, new DERSet(spcSpOpusInfo)));
485
486        return new AttributeTable(new DERSet(attributes.toArray(new ASN1Encodable[0])));
487    }
488
489    /**
490     * Embed a signature as an unsigned attribute of an existing signature.
491     * 
492     * @param primary   the root signature hosting the nested secondary signature
493     * @param secondary the additional signature to nest inside the primary one
494     * @return the signature combining the specified signatures
495     */
496    protected CMSSignedData addNestedSignature(CMSSignedData primary, CMSSignedData secondary) {
497        SignerInformation signerInformation = primary.getSignerInfos().getSigners().iterator().next();
498        
499        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
500        if (unsignedAttributes == null) {
501            unsignedAttributes = new AttributeTable(new DERSet());
502        }
503        Attribute nestedSignaturesAttribute = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID);
504        if (nestedSignaturesAttribute == null) {
505            // first nested signature
506            unsignedAttributes = unsignedAttributes.add(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID, secondary.toASN1Structure());
507        } else {
508            // append the signature to the previous nested signatures
509            ASN1EncodableVector nestedSignatures = new ASN1EncodableVector();
510            for (ASN1Encodable nestedSignature : nestedSignaturesAttribute.getAttrValues()) {
511                nestedSignatures.add(nestedSignature);
512            }
513            nestedSignatures.add(secondary.toASN1Structure());
514            
515            ASN1EncodableVector attributes = unsignedAttributes.remove(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID).toASN1EncodableVector();
516            attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID, new DERSet(nestedSignatures)));
517            
518            unsignedAttributes = new AttributeTable(attributes);
519        }
520        
521        signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes);
522        return CMSSignedData.replaceSigners(primary, new SignerInformationStore(signerInformation));
523    }
524}