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.text.MessageFormat;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Optional;
30  import java.util.ResourceBundle;
31  
32  import org.apache.maven.artifact.Artifact;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugins.annotations.Component;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.project.MavenProject;
39  import org.apache.maven.settings.Settings;
40  import org.apache.maven.shared.jarsigner.JarSigner;
41  import org.apache.maven.shared.jarsigner.JarSignerRequest;
42  import org.apache.maven.shared.jarsigner.JarSignerUtil;
43  import org.apache.maven.shared.utils.ReaderFactory;
44  import org.apache.maven.shared.utils.StringUtils;
45  import org.apache.maven.shared.utils.cli.Commandline;
46  import org.apache.maven.shared.utils.cli.javatool.JavaToolException;
47  import org.apache.maven.shared.utils.io.FileUtils;
48  import org.apache.maven.toolchain.Toolchain;
49  import org.apache.maven.toolchain.ToolchainManager;
50  import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
51  import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;
52  
53  /**
54   * Maven Jarsigner Plugin base class.
55   *
56   * @author <a href="cs@schulte.it">Christian Schulte</a>
57   */
58  public abstract class AbstractJarsignerMojo extends AbstractMojo {
59  
60      /**
61       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
62       */
63      @Parameter(property = "jarsigner.verbose", defaultValue = "false")
64      private boolean verbose;
65  
66      /**
67       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
68       */
69      @Parameter(property = "jarsigner.keystore")
70      private String keystore;
71  
72      /**
73       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
74       */
75      @Parameter(property = "jarsigner.storetype")
76      private String storetype;
77  
78      /**
79       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
80       */
81      @Parameter(property = "jarsigner.storepass")
82      private String storepass;
83  
84      /**
85       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
86       */
87      @Parameter(property = "jarsigner.providerName")
88      private String providerName;
89  
90      /**
91       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
92       */
93      @Parameter(property = "jarsigner.providerClass")
94      private String providerClass;
95  
96      /**
97       * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
98       */
99      @Parameter(property = "jarsigner.providerArg")
100     private String providerArg;
101 
102     /**
103      * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
104      */
105     @Parameter(property = "jarsigner.alias")
106     private String alias;
107 
108     /**
109      * The maximum memory available to the JAR signer, e.g. <code>256M</code>. See <a
110      * href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/java.html#Xms">-Xmx</a> for more details.
111      */
112     @Parameter(property = "jarsigner.maxMemory")
113     private String maxMemory;
114 
115     /**
116      * Archive to process. If set, neither the project artifact nor any attachments or archive sets are processed.
117      */
118     @Parameter(property = "jarsigner.archive")
119     private File archive;
120 
121     /**
122      * The base directory to scan for JAR files using Ant-like inclusion/exclusion patterns.
123      *
124      * @since 1.1
125      */
126     @Parameter(property = "jarsigner.archiveDirectory")
127     private File archiveDirectory;
128 
129     /**
130      * The Ant-like inclusion patterns used to select JAR files to process. The patterns must be relative to the
131      * directory given by the parameter {@link #archiveDirectory}. By default, the pattern
132      * <code>&#42;&#42;/&#42;.?ar</code> is used.
133      *
134      * @since 1.1
135      */
136     @Parameter
137     private String[] includes = {"**/*.?ar"};
138 
139     /**
140      * The Ant-like exclusion patterns used to exclude JAR files from processing. The patterns must be relative to the
141      * directory given by the parameter {@link #archiveDirectory}.
142      *
143      * @since 1.1
144      */
145     @Parameter
146     private String[] excludes = {};
147 
148     /**
149      * List of additional arguments to append to the jarsigner command line. Each argument should be specified as a
150      * separate element. For example, to specify the name of the signed jar, two elements are needed:
151      * <ul>
152      *     <li>Alternative using the command line: {@code -Djarsigner.arguments="-signedjar,my-project_signed.jar"}</li>
153      *     <li>Alternative using the Maven POM configuration:</li>
154      * </ul>
155      * <pre>
156      * {@code
157      * <configuration>
158      *   <arguments>
159      *     <argument>-signedjar</argument>
160      *     <argument>my-project_signed.jar</argument>
161      *   </arguments>
162      * </configuration>
163      * }</pre>
164      */
165     @Parameter(property = "jarsigner.arguments")
166     private String[] arguments;
167 
168     /**
169      * Set to {@code true} to disable the plugin.
170      */
171     @Parameter(property = "jarsigner.skip", defaultValue = "false")
172     private boolean skip;
173 
174     /**
175      * Controls processing of the main artifact produced by the project.
176      *
177      * @since 1.1
178      */
179     @Parameter(property = "jarsigner.processMainArtifact", defaultValue = "true")
180     private boolean processMainArtifact;
181 
182     /**
183      * Controls processing of project attachments. If enabled, attached artifacts that are no JAR/ZIP files will be
184      * automatically excluded from processing.
185      *
186      * @since 1.1
187      */
188     @Parameter(property = "jarsigner.processAttachedArtifacts", defaultValue = "true")
189     private boolean processAttachedArtifacts;
190 
191     /**
192      * Must be set to true if the password must be given via a protected
193      * authentication path such as a dedicated PIN reader.
194      *
195      * @since 1.3
196      */
197     @Parameter(property = "jarsigner.protectedAuthenticationPath", defaultValue = "false")
198     private boolean protectedAuthenticationPath;
199 
200     /**
201      * A set of artifact classifiers describing the project attachments that should be processed. This parameter is only
202      * relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, all attachments are included.
203      *
204      * @since 1.2
205      */
206     @Parameter
207     private String[] includeClassifiers;
208 
209     /**
210      * A set of artifact classifiers describing the project attachments that should not be processed. This parameter is
211      * only relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, no attachments are excluded.
212      *
213      * @since 1.2
214      */
215     @Parameter
216     private String[] excludeClassifiers;
217 
218     /**
219      * The Maven project.
220      */
221     @Parameter(defaultValue = "${project}", readonly = true, required = true)
222     private MavenProject project;
223 
224     /**
225      * The Maven settings.
226      *
227      * @since 1.5
228      */
229     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
230     private Settings settings;
231 
232     /**
233      * Location of the working directory.
234      *
235      * @since 1.3
236      */
237     @Parameter(defaultValue = "${project.basedir}")
238     private File workingDirectory;
239 
240     /**
241      */
242     @Component
243     private JarSigner jarSigner;
244 
245     /**
246      * The current build session instance. This is used for
247      * toolchain manager API calls.
248      *
249      * @since 1.3
250      */
251     @Parameter(defaultValue = "${session}", readonly = true, required = true)
252     private MavenSession session;
253 
254     /**
255      * To obtain a toolchain if possible.
256      *
257      * @since 1.3
258      */
259     @Component
260     private ToolchainManager toolchainManager;
261 
262     /**
263      * @since 1.3.2
264      */
265     @Component(hint = "mng-4384")
266     private SecDispatcher securityDispatcher;
267 
268     @Override
269     public final void execute() throws MojoExecutionException {
270         if (this.skip) {
271             getLog().info(getMessage("disabled"));
272             return;
273         }
274 
275         validateParameters();
276 
277         Toolchain toolchain = getToolchain();
278         if (toolchain != null) {
279             getLog().info("Toolchain in maven-jarsigner-plugin: " + toolchain);
280             jarSigner.setToolchain(toolchain);
281         }
282 
283         List<File> archives = findJarfiles();
284         processArchives(archives);
285         getLog().info(getMessage("processed", archives.size()));
286     }
287 
288     /**
289      * Finds all jar files, by looking at the Maven project and user configuration.
290      *
291      * @return a List of File objects
292      * @throws MojoExecutionException if it was not possible to build a list of jar files
293      */
294     private List<File> findJarfiles() throws MojoExecutionException {
295         if (this.archive != null) {
296             // Only process this, but nothing more
297             return Arrays.asList(this.archive);
298         }
299 
300         List<File> archives = new ArrayList<>();
301         if (processMainArtifact) {
302             getFileFromArtifact(this.project.getArtifact()).ifPresent(archives::add);
303         }
304 
305         if (processAttachedArtifacts) {
306             Collection<String> includes = new HashSet<>();
307             if (includeClassifiers != null) {
308                 includes.addAll(Arrays.asList(includeClassifiers));
309             }
310 
311             Collection<String> excludes = new HashSet<>();
312             if (excludeClassifiers != null) {
313                 excludes.addAll(Arrays.asList(excludeClassifiers));
314             }
315 
316             for (Artifact artifact : this.project.getAttachedArtifacts()) {
317                 if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) {
318                     continue;
319                 }
320 
321                 if (excludes.contains(artifact.getClassifier())) {
322                     continue;
323                 }
324 
325                 getFileFromArtifact(artifact).ifPresent(archives::add);
326             }
327         } else {
328             if (verbose) {
329                 getLog().info(getMessage("ignoringAttachments"));
330             } else {
331                 getLog().debug(getMessage("ignoringAttachments"));
332             }
333         }
334 
335         if (archiveDirectory != null) {
336             String includeList = (includes != null) ? StringUtils.join(includes, ",") : null;
337             String excludeList = (excludes != null) ? StringUtils.join(excludes, ",") : null;
338 
339             try {
340                 archives.addAll(FileUtils.getFiles(archiveDirectory, includeList, excludeList));
341             } catch (IOException e) {
342                 throw new MojoExecutionException("Failed to scan archive directory for JARs: " + e.getMessage(), e);
343             }
344         }
345 
346         return archives;
347     }
348 
349     /**
350      * Creates the jar signer request to be executed.
351      *
352      * @param archive the archive file to treat by jarsigner
353      * @return the request
354      * @throws MojoExecutionException if an exception occurs
355      * @since 1.3
356      */
357     protected abstract JarSignerRequest createRequest(File archive) throws MojoExecutionException;
358 
359     /**
360      * Gets a string representation of a {@code Commandline}.
361      * <p>
362      * This method creates the string representation by calling {@code commandLine.toString()} by default.
363      * </p>
364      *
365      * @param commandLine The {@code Commandline} to get a string representation of.
366      * @return The string representation of {@code commandLine}.
367      * @throws NullPointerException if {@code commandLine} is {@code null}
368      */
369     protected String getCommandlineInfo(final Commandline commandLine) {
370         if (commandLine == null) {
371             throw new NullPointerException("commandLine");
372         }
373 
374         String commandLineInfo = commandLine.toString();
375         commandLineInfo = StringUtils.replace(commandLineInfo, this.storepass, "'*****'");
376         return commandLineInfo;
377     }
378 
379     public String getStoretype() {
380         return storetype;
381     }
382 
383     public String getStorepass() {
384         return storepass;
385     }
386 
387     /**
388      * Checks whether the specified artifact is a ZIP file.
389      *
390      * @param artifact The artifact to check, may be <code>null</code>.
391      * @return <code>true</code> if the artifact looks like a ZIP file, <code>false</code> otherwise.
392      */
393     private static boolean isZipFile(final Artifact artifact) {
394         return artifact != null && artifact.getFile() != null && JarSignerUtil.isZipFile(artifact.getFile());
395     }
396 
397     /**
398      * Examines an Artifact and extract the File object pointing to the Artifact jar file.
399      *
400      * @param artifact the artifact to examine
401      * @return An Optional containing the File, or Optional.empty() if the File is not a jar file.
402      * @throws NullPointerException if {@code artifact} is {@code null}
403      */
404     private Optional<File> getFileFromArtifact(final Artifact artifact) {
405         if (artifact == null) {
406             throw new NullPointerException("artifact");
407         }
408 
409         if (isZipFile(artifact)) {
410             return Optional.of(artifact.getFile());
411         }
412 
413         if (this.verbose) {
414             getLog().info(getMessage("unsupported", artifact));
415         } else if (getLog().isDebugEnabled()) {
416             getLog().debug(getMessage("unsupported", artifact));
417         }
418         return Optional.empty();
419     }
420 
421     /**
422      * Pre-processes a given archive.
423      *
424      * @param archive The archive to process, must not be <code>null</code>.
425      * @throws MojoExecutionException if pre-processing failed
426      */
427     protected void preProcessArchive(final File archive) throws MojoExecutionException {
428         // Default implementation does nothing
429     }
430 
431     /**
432      * Validate the user supplied configuration/parameters.
433      *
434      * @throws MojoExecutionException if the user supplied configuration make further execution impossible
435      */
436     protected void validateParameters() throws MojoExecutionException {
437         // Default implementation does nothing
438     }
439 
440     /**
441      * Process (sign/verify) a list of archives.
442      *
443      * @param archives list of jar files to process
444      * @throws MojoExecutionException if an error occurs during the processing of archives
445      */
446     protected void processArchives(List<File> archives) throws MojoExecutionException {
447         for (File file : archives) {
448             processArchive(file);
449         }
450     }
451 
452     /**
453      * Processes a given archive.
454      *
455      * @param archive The archive to process.
456      * @throws NullPointerException if {@code archive} is {@code null}
457      * @throws MojoExecutionException if processing {@code archive} fails
458      */
459     protected final void processArchive(final File archive) throws MojoExecutionException {
460         if (archive == null) {
461             throw new NullPointerException("archive");
462         }
463 
464         preProcessArchive(archive);
465 
466         if (this.verbose) {
467             getLog().info(getMessage("processing", archive));
468         } else if (getLog().isDebugEnabled()) {
469             getLog().debug(getMessage("processing", archive));
470         }
471 
472         JarSignerRequest request = createRequest(archive);
473         request.setVerbose(verbose);
474         request.setAlias(alias);
475         request.setArchive(archive);
476         request.setKeystore(keystore);
477         request.setStoretype(storetype);
478         request.setProviderArg(providerArg);
479         request.setProviderClass(providerClass);
480         request.setProviderName(providerName);
481         request.setWorkingDirectory(workingDirectory);
482         request.setMaxMemory(maxMemory);
483         request.setProtectedAuthenticationPath(protectedAuthenticationPath);
484 
485         // Preserves 'file.encoding' the plugin is executed with.
486         final List<String> additionalArguments = new ArrayList<>();
487 
488         boolean fileEncodingSeen = false;
489 
490         if (this.arguments != null) {
491             for (final String argument : this.arguments) {
492                 if (argument.trim().startsWith("-J-Dfile.encoding=")) {
493                     fileEncodingSeen = true;
494                 }
495 
496                 additionalArguments.add(argument);
497             }
498         }
499 
500         if (!fileEncodingSeen) {
501             additionalArguments.add("-J-Dfile.encoding=" + ReaderFactory.FILE_ENCODING);
502         }
503 
504         // Adds proxy information.
505         if (this.settings != null
506                 && this.settings.getActiveProxy() != null
507                 && StringUtils.isNotEmpty(this.settings.getActiveProxy().getHost())) {
508             additionalArguments.add(
509                     "-J-Dhttp.proxyHost=" + this.settings.getActiveProxy().getHost());
510             additionalArguments.add(
511                     "-J-Dhttps.proxyHost=" + this.settings.getActiveProxy().getHost());
512             additionalArguments.add(
513                     "-J-Dftp.proxyHost=" + this.settings.getActiveProxy().getHost());
514 
515             if (this.settings.getActiveProxy().getPort() > 0) {
516                 additionalArguments.add(
517                         "-J-Dhttp.proxyPort=" + this.settings.getActiveProxy().getPort());
518                 additionalArguments.add(
519                         "-J-Dhttps.proxyPort=" + this.settings.getActiveProxy().getPort());
520                 additionalArguments.add(
521                         "-J-Dftp.proxyPort=" + this.settings.getActiveProxy().getPort());
522             }
523 
524             if (StringUtils.isNotEmpty(this.settings.getActiveProxy().getNonProxyHosts())) {
525                 additionalArguments.add("-J-Dhttp.nonProxyHosts=\""
526                         + this.settings.getActiveProxy().getNonProxyHosts() + "\"");
527 
528                 additionalArguments.add("-J-Dftp.nonProxyHosts=\""
529                         + this.settings.getActiveProxy().getNonProxyHosts() + "\"");
530             }
531         }
532 
533         request.setArguments(
534                 !additionalArguments.isEmpty()
535                         ? additionalArguments.toArray(new String[additionalArguments.size()])
536                         : null);
537 
538         // Special handling for passwords through the Maven Security Dispatcher
539         request.setStorepass(decrypt(storepass));
540 
541         try {
542             executeJarSigner(jarSigner, request);
543         } catch (JavaToolException e) {
544             throw new MojoExecutionException(getMessage("commandLineException", e.getMessage()), e);
545         }
546     }
547 
548     /**
549      * Executes jarsigner (execute signing or verification for a jar file).
550      *
551      * @param jarSigner the JarSigner execution interface
552      * @param request the JarSignerRequest with parameters JarSigner should use
553      * @throws JavaToolException if jarsigner could not be invoked
554      * @throws MojoExecutionException if the invocation of jarsigner succeeded, but returned a non-zero exit code
555      */
556     protected abstract void executeJarSigner(JarSigner jarSigner, JarSignerRequest request)
557             throws JavaToolException, MojoExecutionException;
558 
559     protected String decrypt(String encoded) throws MojoExecutionException {
560         try {
561             return securityDispatcher.decrypt(encoded);
562         } catch (SecDispatcherException e) {
563             getLog().error("error using security dispatcher: " + e.getMessage(), e);
564             throw new MojoExecutionException("error using security dispatcher: " + e.getMessage(), e);
565         }
566     }
567 
568     /**
569      * Gets a message for a given key from the resource bundle backing the implementation.
570      *
571      * @param key the key of the message to return
572      * @param args arguments to format the message with
573      * @return the message with key {@code key} from the resource bundle backing the implementation
574      * @throws NullPointerException if {@code key} is {@code null}
575      * @throws java.util.MissingResourceException
576      *             if there is no message available matching {@code key} or accessing
577      *             the resource bundle fails
578      */
579     String getMessage(final String key, final Object... args) {
580         if (key == null) {
581             throw new NullPointerException("key");
582         }
583 
584         return new MessageFormat(ResourceBundle.getBundle("jarsigner").getString(key)).format(args);
585     }
586 
587     /**
588      * the part with ToolchainManager lookup once we depend on
589      * 2.0.9 (have it as prerequisite). Define as regular component field then.
590      * hint: check maven-compiler-plugin code
591      *
592      * @return Toolchain instance
593      */
594     private Toolchain getToolchain() {
595         Toolchain tc = null;
596         if (toolchainManager != null) {
597             tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
598         }
599 
600         return tc;
601     }
602 }