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