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}