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.gpg;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.util.List;
25  
26  import org.apache.maven.execution.MavenSession;
27  import org.apache.maven.plugin.AbstractMojo;
28  import org.apache.maven.plugin.MojoExecutionException;
29  import org.apache.maven.plugin.MojoFailureException;
30  import org.apache.maven.plugins.annotations.Parameter;
31  import org.apache.maven.project.MavenProject;
32  import org.apache.maven.settings.Server;
33  import org.apache.maven.settings.Settings;
34  import org.apache.maven.settings.building.SettingsProblem;
35  import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
36  import org.apache.maven.settings.crypto.SettingsDecrypter;
37  import org.apache.maven.settings.crypto.SettingsDecryptionResult;
38  
39  /**
40   * @author Benjamin Bentmann
41   */
42  public abstract class AbstractGpgMojo extends AbstractMojo {
43      public static final String DEFAULT_ENV_MAVEN_GPG_KEY = "MAVEN_GPG_KEY";
44      public static final String DEFAULT_ENV_MAVEN_GPG_FINGERPRINT = "MAVEN_GPG_KEY_FINGERPRINT";
45      public static final String DEFAULT_ENV_MAVEN_GPG_PASSPHRASE = "MAVEN_GPG_PASSPHRASE";
46  
47      /**
48       * BC Signer only: The comma separate list of Unix Domain Socket paths, to use to communicate with GnuPG agent.
49       * If relative, they are resolved against user home directory.
50       *
51       * @since 3.2.0
52       */
53      @Parameter(property = "gpg.agentSocketLocations", defaultValue = ".gnupg/S.gpg-agent")
54      private String agentSocketLocations;
55  
56      /**
57       * BC Signer only: The path of the exported key in
58       * <a href="https://openpgp.dev/book/private_keys.html#transferable-secret-key-format">TSK format</a>,
59       * and may be passphrase protected. If relative, the file is resolved against user home directory.
60       * <p>
61       * <em>Note: it is not recommended to have sensitive files checked into SCM repository. Key file should reside on
62       * developer workstation, outside of SCM tracked repository. For CI-like use cases you should set the
63       * key material as env variable instead.</em>
64       *
65       * @since 3.2.0
66       */
67      @Parameter(property = "gpg.keyFilePath", defaultValue = "maven-signing-key.key")
68      private String keyFilePath;
69  
70      /**
71       * BC Signer only: The fingerprint of the key to use for signing. If not given, first key in keyring will be used.
72       *
73       * @since 3.2.0
74       */
75      @Parameter(property = "gpg.keyFingerprint")
76      private String keyFingerprint;
77  
78      /**
79       * BC Signer only: The env variable name where the GnuPG key is set.
80       * To use BC Signer you must provide GnuPG key, as it does not use GnuPG home directory to extract/find the
81       * key (while it does use GnuPG Agent to ask for password in interactive mode). The key should be in
82       * <a href="https://openpgp.dev/book/private_keys.html#transferable-secret-key-format">TSK format</a> and may
83       * be passphrase protected.
84       *
85       * @since 3.2.0
86       */
87      @Parameter(property = "gpg.keyEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_KEY)
88      private String keyEnvName;
89  
90      /**
91       * BC Signer only: The env variable name where the GnuPG key fingerprint is set, if the provided keyring contains
92       * multiple keys.
93       *
94       * @since 3.2.0
95       */
96      @Parameter(property = "gpg.keyFingerprintEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_FINGERPRINT)
97      private String keyFingerprintEnvName;
98  
99      /**
100      * The env variable name where the GnuPG passphrase is set. This is the recommended way to pass passphrase
101      * for signing in batch mode execution of Maven.
102      *
103      * @since 3.2.0
104      */
105     @Parameter(property = "gpg.passphraseEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_PASSPHRASE)
106     private String passphraseEnvName;
107 
108     /**
109      * GPG Signer only: The directory from which gpg will load keyrings. If not specified, gpg will use the value configured for its
110      * installation, e.g. <code>~/.gnupg</code> or <code>%APPDATA%/gnupg</code>.
111      *
112      * @since 1.0
113      */
114     @Parameter(property = "gpg.homedir")
115     private File homedir;
116 
117     /**
118      * The passphrase to use when signing. If not given, look up the value under Maven
119      * settings using server id at 'passphraseServerKey' configuration. <em>Do not use this parameter, it leaks
120      * sensitive data. Passphrase should be provided only via gpg-agent or via env variable.
121      * If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured.</em>
122      *
123      * @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env
124      * variables instead.
125      **/
126     @Deprecated
127     @Parameter(property = GPG_PASSPHRASE)
128     private String passphrase;
129 
130     /**
131      * Server id to lookup the passphrase under Maven settings. <em>Do not use this parameter, it leaks
132      * sensitive data. Passphrase should be provided only via gpg-agent or via env variable.
133      * If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured.</em>
134      * Is programatically defaulted to {@link #GPG_PASSPHRASE}.
135      *
136      * @since 1.6
137      * @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env
138      * variables instead.
139      **/
140     @Deprecated
141     @Parameter(property = "gpg.passphraseServerId")
142     private String passphraseServerId;
143 
144     /**
145      * GPG Signer only: The "name" of the key to sign with. Passed to gpg as <code>--local-user</code>.
146      */
147     @Parameter(property = "gpg.keyname")
148     private String keyname;
149 
150     /**
151      * All signers: whether gpg-agent is allowed to be used or not. If enabled, passphrase is optional, as agent may
152      * provide it. Have to be noted, that in "batch" mode, gpg-agent will be prevented to pop up pinentry
153      * dialogue, hence best is to "prime" the agent caches beforehand.
154      * <p>
155      * GPG Signer: Passes <code>--use-agent</code> or <code>--no-use-agent</code> option to gpg if it is version 2.1
156      * or older. Otherwise, will use an agent. In non-interactive mode gpg options are appended with
157      * <code>--pinentry-mode error</code>, preventing gpg agent to pop up pinentry dialogue. Agent will be able to
158      * hand over only cached passwords.
159      * <p>
160      * BC Signer: Allows signer to communicate with gpg agent. In non-interactive mode it uses
161      * <code>--no-ask</code> option with the <code>GET_PASSPHRASE</code> function. Agent will be able to hand over
162      * only cached passwords.
163      */
164     @Parameter(property = "gpg.useagent", defaultValue = "true")
165     private boolean useAgent;
166 
167     /**
168      * GPG Signer only: The path to the GnuPG executable to use for artifact signing. Defaults to either "gpg" or
169      * "gpg.exe" depending on the operating system.
170      *
171      * @since 1.1
172      */
173     @Parameter(property = "gpg.executable")
174     private String executable;
175 
176     /**
177      * GPG Signer only: Whether to add the default keyrings from gpg's home directory to the list of used keyrings.
178      *
179      * @since 1.2
180      */
181     @Parameter(property = "gpg.defaultKeyring", defaultValue = "true")
182     private boolean defaultKeyring;
183 
184     /**
185      * GPG Signer only: The path to a secret keyring to add to the list of keyrings. By default, only the
186      * {@code secring.gpg} from gpg's home directory is considered. Use this option (in combination with
187      * {@link #publicKeyring} and {@link #defaultKeyring} if required) to use a different secret key.
188      * <em>Note:</em> Relative paths are resolved against gpg's home directory, not the project base directory.
189      * <p>
190      * <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All secret keys are stored in the
191      * ‘private-keys-v1.d’ directory below the GnuPG home directory.
192      *
193      * @since 1.2
194      * @deprecated Obsolete option since GnuPG 2.1 version.
195      */
196     @Deprecated
197     @Parameter(property = "gpg.secretKeyring")
198     private String secretKeyring;
199 
200     /**
201      * GPG Signer only: The path to a public keyring to add to the list of keyrings. By default, only the
202      * {@code pubring.gpg} from gpg's home directory is considered. Use this option (and {@link #defaultKeyring}
203      * if required) to use a different public key. <em>Note:</em> Relative paths are resolved against gpg's home
204      * directory, not the project base directory.
205      * <p>
206      * <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All public keys are stored in the
207      * ‘pubring.kbx’ file below the GnuPG home directory.
208      *
209      * @since 1.2
210      * @deprecated Obsolete option since GnuPG 2.1 version.
211      */
212     @Deprecated
213     @Parameter(property = "gpg.publicKeyring")
214     private String publicKeyring;
215 
216     /**
217      * GPG Signer only: The lock mode to use when invoking gpg. By default no lock mode will be specified. Valid
218      * values are {@code once}, {@code multiple} and {@code never}. The lock mode gets translated into the
219      * corresponding {@code --lock-___} command line argument. Improper usage of this option may lead to data and
220      * key corruption.
221      *
222      * @see <a href="http://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html">the
223      *      --lock-options</a>
224      * @since 1.5
225      */
226     @Parameter(property = "gpg.lockMode")
227     private String lockMode;
228 
229     /**
230      * Skip doing the gpg signing.
231      */
232     @Parameter(property = "gpg.skip", defaultValue = "false")
233     private boolean skip;
234 
235     /**
236      * GPG Signer only: Sets the arguments to be passed to gpg. Example:
237      *
238      * <pre>
239      * &lt;gpgArguments&gt;
240      *   &lt;arg&gt;--no-random-seed-file&lt;/arg&gt;
241      *   &lt;arg&gt;--no-permission-warning&lt;/arg&gt;
242      * &lt;/gpgArguments&gt;
243      * </pre>
244      *
245      * @since 1.5
246      */
247     @Parameter
248     private List<String> gpgArguments;
249 
250     /**
251      * The name of the Signer implementation to use. Accepted values are {@code "gpg"} (the default, uses GnuPG
252      * executable) and {@code "bc"} (uses Bouncy Castle pure Java signer).
253      *
254      * @since 3.2.0
255      */
256     @Parameter(property = "gpg.signer", defaultValue = GpgSigner.NAME)
257     private String signer;
258 
259     /**
260      * Switch to improve plugin enforcement of "best practices". If set to {@code false}, plugin retains all the
261      * backward compatibility regarding getting secrets (but will warn). If set to {@code true}, plugin will fail
262      * if any "bad practices" regarding sensitive data handling are detected. By default, plugin remains backward
263      * compatible (this flag is {@code false}). Somewhere in the future, when this parameter enabling transitioning
264      * from older plugin versions is removed, the logic using this flag will be modified like it is set to {@code true}.
265      * It is warmly advised to configure this parameter to {@code true} and migrate project and user environment
266      * regarding how sensitive information is stored.
267      *
268      * @since 3.2.0
269      */
270     @Parameter(property = "gpg.bestPractices", defaultValue = "false")
271     private boolean bestPractices;
272 
273     /**
274      * Whether to terminate the passphrase with LF character or not, as on some systems and some GPG executable combinations
275      * lack of trailing LF may cause GPG to not detect passphrase on STDIN. Since 3.2.0 it was always appended, unless
276      * passphrase itself ended with it. Note: before 3.2.7 the "line separator" was used for termination, that on
277      * other hand caused issues on Windows, where line separator is CRLF while GPG handles LF only.
278      * This parameter affects ONLY the GPG signer, not the BC signer.
279      * <p>
280      * By default, this parameter is {@code true}.
281      *
282      * @since 3.2.7
283      * @see <a href="https://issues.apache.org/jira/browse/MGPG-99">MGPG-99</a>
284      * @see <a href="https://issues.apache.org/jira/browse/MGPG-136">MGPG-136</a>
285      */
286     @Parameter(property = "gpg.terminatePassphrase", defaultValue = "true")
287     private boolean terminatePassphrase;
288 
289     /**
290      * Current user system settings for use in Maven.
291      *
292      * @since 1.6
293      */
294     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
295     protected Settings settings;
296 
297     /**
298      * @since 3.0.0
299      */
300     @Inject
301     protected MavenSession session;
302 
303     /**
304      * @since 3.2.6
305      */
306     @Inject
307     protected SettingsDecrypter settingsDecrypter;
308 
309     @Override
310     public final void execute() throws MojoExecutionException, MojoFailureException {
311         if (skip) {
312             // We're skipping the signing stuff
313             return;
314         }
315         if (bestPractices) {
316             enforceBestPractices();
317         } else {
318             if (!isNotBlank(passphraseServerId)) {
319                 // default it programmatically: this is needed to handle different cases re bestPractices
320                 passphraseServerId = GPG_PASSPHRASE;
321             }
322         }
323 
324         doExecute();
325     }
326 
327     protected void enforceBestPractices() throws MojoFailureException {
328         // if any of those are not blank: meaning user did explicitly configure these
329         if (isNotBlank(passphrase) || isNotBlank(passphraseServerId)) {
330             // Stop propagating worst practices: passphrase MUST NOT be in any file on disk
331             throw new MojoFailureException(
332                     "Do not store passphrase in any file (disk or SCM repository), rely on GnuPG agent or provide passphrase in "
333                             + passphraseEnvName + " environment variable.");
334         }
335     }
336 
337     protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;
338 
339     private void logBestPracticeWarning(String source) {
340         getLog().warn("");
341         getLog().warn("W A R N I N G");
342         getLog().warn("");
343         getLog().warn("Do not store passphrase in any file (disk or SCM repository),");
344         getLog().warn("instead rely on GnuPG agent or provide passphrase in ");
345         getLog().warn(passphraseEnvName + " environment variable for batch mode.");
346         getLog().warn("");
347         getLog().warn("Sensitive content loaded from " + source);
348         getLog().warn("");
349     }
350 
351     protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFailureException {
352         AbstractGpgSigner signer = createSigner(this.signer);
353 
354         signer.setLog(getLog());
355         signer.setInteractive(settings.isInteractiveMode());
356         signer.setKeyName(keyname);
357         signer.setUseAgent(useAgent);
358         signer.setHomeDirectory(homedir);
359         signer.setDefaultKeyring(defaultKeyring);
360         signer.setSecretKeyring(secretKeyring);
361         signer.setPublicKeyring(publicKeyring);
362         signer.setLockMode(lockMode);
363         signer.setArgs(gpgArguments);
364         signer.setTerminatePassphrase(terminatePassphrase);
365 
366         // "new way": env prevails
367         String passphrase =
368                 (String) session.getRepositorySession().getConfigProperties().get("env." + passphraseEnvName);
369         if (isNotBlank(passphrase)) {
370             signer.setPassPhrase(passphrase);
371         } else if (!bestPractices) {
372             // "old way": mojo config
373             passphrase = this.passphrase;
374             if (isNotBlank(passphrase)) {
375                 logBestPracticeWarning("Mojo configuration");
376                 signer.setPassPhrase(passphrase);
377             } else {
378                 // "old way": serverId + settings
379                 passphrase = loadGpgPassphrase();
380                 if (isNotBlank(passphrase)) {
381                     logBestPracticeWarning("settings.xml");
382                     signer.setPassPhrase(passphrase);
383                 } else {
384                     // "old way": project properties
385                     passphrase = getPassphrase(mavenProject);
386                     if (isNotBlank(passphrase)) {
387                         logBestPracticeWarning("Project properties");
388                         signer.setPassPhrase(passphrase);
389                     }
390                 }
391             }
392         }
393         signer.prepare();
394 
395         return signer;
396     }
397 
398     protected AbstractGpgSigner createSigner(String name) throws MojoFailureException {
399         AbstractGpgSigner signer;
400         if (GpgSigner.NAME.equals(name)) {
401             signer = new GpgSigner(executable);
402         } else if (BcSigner.NAME.equals(name)) {
403             signer = new BcSigner(
404                     session.getRepositorySession(),
405                     keyEnvName,
406                     keyFingerprintEnvName,
407                     agentSocketLocations,
408                     keyFilePath,
409                     keyFingerprint);
410         } else {
411             throw new MojoFailureException("Unknown signer: " + name);
412         }
413         return signer;
414     }
415 
416     private boolean isNotBlank(String string) {
417         return string != null && !string.trim().isEmpty();
418     }
419 
420     // Below is attic, to be thrown out
421 
422     @Deprecated
423     private static final String GPG_PASSPHRASE = "gpg.passphrase";
424 
425     @Deprecated
426     private String loadGpgPassphrase() throws MojoFailureException {
427         if (isNotBlank(passphraseServerId)) {
428             Server server = settings.getServer(passphraseServerId);
429             if (server != null) {
430                 if (isNotBlank(server.getPassphrase())) {
431                     SettingsDecryptionResult result =
432                             settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(server));
433                     for (SettingsProblem problem : result.getProblems()) {
434                         switch (problem.getSeverity()) {
435                             case WARNING:
436                             case ERROR:
437                                 getLog().warn(problem.getMessage(), problem.getException());
438                                 break;
439                             case FATAL:
440                                 getLog().error(problem.getMessage(), problem.getException());
441                                 throw new MojoFailureException(problem.getMessage(), problem.getException());
442                             default:
443                                 throw new IllegalStateException("Unknown severity: "
444                                         + problem.getSeverity().toString());
445                         }
446                     }
447                     return result.getServer().getPassphrase();
448                 }
449             }
450         }
451         return null;
452     }
453 
454     @Deprecated
455     public String getPassphrase(MavenProject project) {
456         String pass = null;
457         if (project != null) {
458             pass = project.getProperties().getProperty(GPG_PASSPHRASE);
459             if (pass == null) {
460                 MavenProject prj2 = findReactorProject(project);
461                 pass = prj2.getProperties().getProperty(GPG_PASSPHRASE);
462             }
463         }
464         if (project != null && pass != null) {
465             findReactorProject(project).getProperties().setProperty(GPG_PASSPHRASE, pass);
466         }
467         return pass;
468     }
469 
470     @Deprecated
471     private MavenProject findReactorProject(MavenProject prj) {
472         if (prj.getParent() != null
473                 && prj.getParent().getBasedir() != null
474                 && prj.getParent().getBasedir().exists()) {
475             return findReactorProject(prj.getParent());
476         }
477         return prj;
478     }
479 }