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}