View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.jarsigner;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.time.Duration;
24  import java.util.List;
25  import java.util.concurrent.Callable;
26  import java.util.concurrent.ExecutionException;
27  import java.util.concurrent.ExecutorService;
28  import java.util.concurrent.Executors;
29  import java.util.concurrent.Future;
30  import java.util.stream.Collectors;
31  
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugins.annotations.LifecyclePhase;
34  import org.apache.maven.plugins.annotations.Mojo;
35  import org.apache.maven.plugins.annotations.Parameter;
36  import org.apache.maven.plugins.jarsigner.TsaSelector.TsaServer;
37  import org.apache.maven.shared.jarsigner.JarSigner;
38  import org.apache.maven.shared.jarsigner.JarSignerRequest;
39  import org.apache.maven.shared.jarsigner.JarSignerSignRequest;
40  import org.apache.maven.shared.jarsigner.JarSignerUtil;
41  import org.apache.maven.shared.utils.StringUtils;
42  import org.apache.maven.shared.utils.cli.Commandline;
43  import org.apache.maven.shared.utils.cli.javatool.JavaToolException;
44  import org.apache.maven.shared.utils.cli.javatool.JavaToolResult;
45  
46  /**
47   * Signs a project artifact and attachments using jarsigner.
48   *
49   * @author <a href="cs@schulte.it">Christian Schulte</a>
50   * @since 1.0
51   */
52  @Mojo(name = "sign", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true)
53  public class JarsignerSignMojo extends AbstractJarsignerMojo {
54  
55      /**
56       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
57       */
58      @Parameter(property = "jarsigner.keypass")
59      private String keypass;
60  
61      /**
62       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
63       */
64      @Parameter(property = "jarsigner.sigfile")
65      private String sigfile;
66  
67      /**
68       * Indicates whether existing signatures should be removed from the processed JAR files prior to signing them. If
69       * enabled, the resulting JAR will appear as being signed only once.
70       *
71       * @since 1.1
72       */
73      @Parameter(property = "jarsigner.removeExistingSignatures", defaultValue = "false")
74      private boolean removeExistingSignatures;
75  
76      /**
77       * <p>URL(s) to Time Stamping Authority (TSA) server(s) to use to timestamp the signing.
78       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
79       * Separate multiple TSA URLs with comma (without space) or a nested XML tag.</p>
80       *
81       * <pre>{@code
82       * <configuration>
83       *   <tsa>http://timestamp.digicert.com,http://timestamp.globalsign.com/tsa/r6advanced1</tsa>
84       * </configuration>
85       * }</pre>
86       *
87       * <pre>{@code
88       * <configuration>
89       *   <tsa>
90       *     <url>http://timestamp.digicert.com</url>
91       *     <url>http://timestamp.globalsign.com/tsa/r6advanced1</url>
92       *   </tsa>
93       * </configuration>
94       * }</pre>
95       *
96       * <p>Usage of multiple TSA servers only makes sense when {@link #maxTries} is more than 1. A different TSA server
97       * will only be used at retries.</p>
98       *
99       * <p>Changed to a list since 3.1.0. Single XML element (without comma) is still supported.</p>
100      *
101      * @since 1.3
102      */
103     @Parameter(property = "jarsigner.tsa")
104     private String[] tsa;
105 
106     /**
107      * <p>Alias(es) for certificate(s) in the active keystore used to find a TSA URL. From the certificate the X509v3
108      * extension "Subject Information Access" field is examined to find the TSA server URL. See
109      * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
110      * Separate multiple aliases with comma (without space) or a nested XML tag.</p>
111      *
112      * <pre>{@code
113      * <configuration>
114      *   <tsacert>alias1,alias2</tsacert>
115      * </configuration>
116      * }</pre>
117      *
118      * <pre>{@code
119      * <configuration>
120      *   <tsacert>
121      *     <alias>alias1</alias>
122      *     <alias>alias2</alias>
123      *   </tsacert>
124      * </configuration>
125      * }</pre>
126      *
127      * <p>Should not be used at the same time as the {@link #tsa} parameter (because jarsigner will typically ignore
128      * tsacert, if tsa is set).</p>
129      *
130      * <p>Usage of multiple aliases only makes sense when {@link #maxTries} is more than 1. A different TSA server
131      * will only be used at retries.</p>
132      *
133      * <p>Changed to a list since 3.1.0. Single XML element (without comma) is still supported.</p>
134      *
135      * @since 1.3
136      */
137     @Parameter(property = "jarsigner.tsacert")
138     private String[] tsacert;
139 
140     /**
141      * <p>OID(s) to send to the TSA server to identify the policy ID the server should use. If not specified TSA server
142      * will choose a default policy ID. Each TSA server vendor will typically define their own policy OIDs. See
143      * <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html#CCHIFIAD">options</a>.
144      * Separate multiple OIDs with comma (without space) or a nested XML tag.</p>
145      *
146      * <pre>{@code
147      * <configuration>
148      *   <tsapolicyid>1.3.6.1.4.1.4146.2.3.1.2,2.16.840.1.114412.7.1</tsapolicyid>
149      * </configuration>
150      * }</pre>
151      *
152      * <pre>{@code
153      * <configuration>
154      *   <tsapolicyid>
155      *     <oid>1.3.6.1.4.1.4146.2.3.1.2</oid>
156      *     <oid>2.16.840.1.114412.7.1</oid>
157      *   </tsapolicyid>
158      * </configuration>
159      * }</pre>
160      *
161      * <p>If used, the number of OIDs should be the same as the number of elements in {@link #tsa} or {@link #tsacert}.
162      * The first OID will be used for the first TSA server, the second OID for the second TSA server and so on.</p>
163      *
164      * @since 3.1.0
165      */
166     @Parameter(property = "jarsigner.tsapolicyid")
167     private String[] tsapolicyid;
168 
169     /**
170      * The message digest algorithm to use in the messageImprint that the TSA server will timestamp. A default value
171      * (for example {@code SHA-384}) will be selected by jarsigner if this parameter is not set. Only available in
172      * Java 11 and later. See <a href="https://docs.oracle.com/en/java/javase/11/tools/jarsigner.html">options</a>.
173      *
174      * @since 3.1.0
175      */
176     @Parameter(property = "jarsigner.tsadigestalg")
177     private String tsadigestalg;
178 
179     /**
180      * Location of the extra certificate chain file. See
181      * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
182      *
183      * @since 1.5
184      */
185     @Parameter(property = "jarsigner.certchain", required = false)
186     private File certchain;
187 
188     /**
189      * How many times to try to sign a jar (assuming each previous attempt is a failure). This option may be desirable
190      * if any network operations are used during signing, for example using a Time Stamp Authority or network based
191      * PKCS11 HSM solution for storing code signing keys.
192      *
193      * The default value of 1 indicates that no retries should be made.
194      *
195      * @since 3.1.0
196      */
197     @Parameter(property = "jarsigner.maxTries", defaultValue = "1")
198     private int maxTries;
199 
200     /**
201      * Maximum delay, in seconds, to wait after a failed attempt before re-trying. The delay after a failed attempt
202      * follows an exponential backoff strategy, with increasing delay times.
203      *
204      * @since 3.1.0
205      */
206     @Parameter(property = "jarsigner.maxRetryDelaySeconds", defaultValue = "0")
207     private int maxRetryDelaySeconds;
208 
209     /**
210      * Maximum number of parallel threads to use when signing jar files. Increases performance when signing multiple jar
211      * files, especially when network operations are used during signing, for example when using a Time Stamp Authority
212      * or network based PKCS11 HSM solution for storing code signing keys. Note: the logging from the signing process
213      * will be interleaved, and harder to read, when using many threads.
214      *
215      * @since 3.1.0
216      */
217     @Parameter(property = "jarsigner.threadCount", defaultValue = "1")
218     private int threadCount;
219 
220     /** Current WaitStrategy, to allow for sleeping after a signing failure. */
221     private WaitStrategy waitStrategy = this::defaultWaitStrategy;
222 
223     private TsaSelector tsaSelector;
224 
225     /** Exponent limit for exponential wait after failure function. 2^20 = 1048576 sec ~= 12 days. */
226     private static final int MAX_WAIT_EXPONENT_ATTEMPT = 20;
227 
228     @Override
229     protected String getCommandlineInfo(final Commandline commandLine) {
230         String commandLineInfo = commandLine != null ? commandLine.toString() : null;
231 
232         if (commandLineInfo != null) {
233             commandLineInfo = StringUtils.replace(commandLineInfo, this.keypass, "'*****'");
234         }
235 
236         return commandLineInfo;
237     }
238 
239     @Override
240     protected void preProcessArchive(final File archive) throws MojoExecutionException {
241         if (removeExistingSignatures) {
242             try {
243                 JarSignerUtil.unsignArchive(archive);
244             } catch (IOException e) {
245                 throw new MojoExecutionException("Failed to unsign archive " + archive + ": " + e.getMessage(), e);
246             }
247         }
248     }
249 
250     @Override
251     protected void validateParameters() throws MojoExecutionException {
252         super.validateParameters();
253 
254         if (maxTries < 1) {
255             getLog().warn(getMessage("invalidMaxTries", maxTries));
256             maxTries = 1;
257         }
258 
259         if (maxRetryDelaySeconds < 0) {
260             getLog().warn(getMessage("invalidMaxRetryDelaySeconds", maxRetryDelaySeconds));
261             maxRetryDelaySeconds = 0;
262         }
263 
264         if (threadCount < 1) {
265             getLog().warn(getMessage("invalidThreadCount", threadCount));
266             threadCount = 1;
267         }
268 
269         if (tsa.length > 0 && tsacert.length > 0) {
270             getLog().warn(getMessage("warnUsageTsaAndTsacertSimultaneous"));
271         }
272         if (tsapolicyid.length > tsa.length || tsapolicyid.length > tsacert.length) {
273             getLog().warn(getMessage("warnUsageTsapolicyidTooMany", tsapolicyid.length, tsa.length, tsacert.length));
274         }
275         if (tsa.length > 1 && maxTries == 1) {
276             getLog().warn(getMessage("warnUsageMultiTsaWithoutRetry", tsa.length));
277         }
278         if (tsacert.length > 1 && maxTries == 1) {
279             getLog().warn(getMessage("warnUsageMultiTsacertWithoutRetry", tsacert.length));
280         }
281         tsaSelector = new TsaSelector(tsa, tsacert, tsapolicyid, tsadigestalg);
282     }
283 
284     /**
285      * {@inheritDoc}
286      */
287     @Override
288     protected JarSignerRequest createRequest(File archive) throws MojoExecutionException {
289         JarSignerSignRequest request = new JarSignerSignRequest();
290         request.setSigfile(sigfile);
291         updateJarSignerRequestWithTsa(request, tsaSelector.getServer());
292         request.setCertchain(certchain);
293 
294         // Special handling for passwords through the Maven Security Dispatcher
295         request.setKeypass(decrypt(keypass));
296         return request;
297     }
298 
299     /** Modifies JarSignerRequest with TSA parameters */
300     private void updateJarSignerRequestWithTsa(JarSignerSignRequest request, TsaServer tsaServer) {
301         request.setTsaLocation(tsaServer.getTsaUrl());
302         request.setTsaAlias(tsaServer.getTsaAlias());
303         request.setTsapolicyid(tsaServer.getTsaPolicyId());
304         request.setTsadigestalg(tsaServer.getTsaDigestAlt());
305     }
306 
307     /**
308      * {@inheritDoc} Processing of files may be parallelized for increased performance.
309      */
310     @Override
311     protected void processArchives(List<File> archives) throws MojoExecutionException {
312         ExecutorService executor = Executors.newFixedThreadPool(threadCount);
313         List<Future<Void>> futures = archives.stream()
314                 .map(file -> executor.submit((Callable<Void>) () -> {
315                     processArchive(file);
316                     return null; // Return dummy value to conform with Void type
317                 }))
318                 .collect(Collectors.toList());
319         try {
320             for (Future<Void> future : futures) {
321                 future.get(); // Wait for completion. Result ignored, but may raise any Exception
322             }
323         } catch (InterruptedException e) {
324             Thread.currentThread().interrupt();
325             throw new MojoExecutionException("Thread interrupted while waiting for jarsigner to complete", e);
326         } catch (ExecutionException e) {
327             if (e.getCause() instanceof MojoExecutionException) {
328                 throw (MojoExecutionException) e.getCause();
329             }
330             throw new MojoExecutionException("Error processing archives", e);
331         } finally {
332             // Shutdown of thread pool. If an Exception occurred, remaining threads will be aborted "best effort"
333             executor.shutdownNow();
334         }
335     }
336 
337     /**
338      * {@inheritDoc}
339      *
340      * Will retry signing up to maxTries times if it fails.
341      *
342      * @throws MojoExecutionException if all signing attempts fail
343      */
344     @Override
345     protected void executeJarSigner(JarSigner jarSigner, JarSignerRequest request)
346             throws JavaToolException, MojoExecutionException {
347         for (int attempt = 0; attempt < maxTries; attempt++) {
348             JavaToolResult result = jarSigner.execute(request);
349             int resultCode = result.getExitCode();
350             if (resultCode == 0) {
351                 return;
352             }
353             tsaSelector.registerFailure(); // Could be TSA server problem or something unrelated to TSA
354 
355             if (attempt < maxTries - 1) { // If not last attempt
356                 waitStrategy.waitAfterFailure(attempt, Duration.ofSeconds(maxRetryDelaySeconds));
357                 updateJarSignerRequestWithTsa((JarSignerSignRequest) request, tsaSelector.getServer());
358             } else {
359                 // Last attempt failed, use this failure as resulting failure
360                 throw new MojoExecutionException(
361                         getMessage("failure", getCommandlineInfo(result.getCommandline()), resultCode));
362             }
363         }
364     }
365 
366     /** Set current WaitStrategy. Package private for testing. */
367     void setWaitStrategy(WaitStrategy waitStrategy) {
368         this.waitStrategy = waitStrategy;
369     }
370 
371     /** Wait/sleep after a signing failure before the next re-try should happen. */
372     @FunctionalInterface
373     interface WaitStrategy {
374         /**
375          * Will be called after a signing failure, if a re-try is about to happen. May as a side effect sleep current
376          * thread for some time.
377          *
378          * @param attempt the attempt number (0 is the first)
379          * @param maxRetryDelay the maximum duration to sleep (may be zero)
380          * @throws MojoExecutionException if the sleep was interrupted
381          */
382         void waitAfterFailure(int attempt, Duration maxRetryDelay) throws MojoExecutionException;
383     }
384 
385     private void defaultWaitStrategy(int attempt, Duration maxRetryDelay) throws MojoExecutionException {
386         waitAfterFailure(attempt, maxRetryDelay, Thread::sleep);
387     }
388 
389     /** Thread.sleep(long millis) interface to make testing easier */
390     @FunctionalInterface
391     interface Sleeper {
392         void sleep(long millis) throws InterruptedException;
393     }
394 
395     /** Package private for testing */
396     void waitAfterFailure(int attempt, Duration maxRetryDelay, Sleeper sleeper) throws MojoExecutionException {
397         // Use attempt as exponent in the exponential function, but limit it to avoid too big values.
398         int exponentAttempt = Math.min(attempt, MAX_WAIT_EXPONENT_ATTEMPT);
399         long delayMillis = (long) (Duration.ofSeconds(1).toMillis() * Math.pow(2, exponentAttempt));
400         delayMillis = Math.min(delayMillis, maxRetryDelay.toMillis());
401         if (delayMillis > 0) {
402             getLog().info("Sleeping after failed attempt for " + (delayMillis / 1000) + " seconds...");
403             try {
404                 sleeper.sleep(delayMillis);
405             } catch (InterruptedException e) {
406                 Thread.currentThread().interrupt();
407                 throw new MojoExecutionException("Thread interrupted while waiting after failure", e);
408             }
409         }
410     }
411 }