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     /**
262      * Switch to improve plugin enforcement of "best practices". If set to {@code false}, plugin retains all the
263      * backward compatibility regarding getting secrets (but will warn). If set to {@code true}, plugin will fail
264      * if any "bad practices" regarding sensitive data handling are detected. By default, plugin remains backward
265      * compatible (this flag is {@code false}). Somewhere in the future, when this parameter enabling transitioning
266      * from older plugin versions is removed, the logic using this flag will be modified like it is set to {@code true}.
267      * It is warmly advised to configure this parameter to {@code true} and migrate project and user environment
268      * regarding how sensitive information is stored.
269      *
270      * @since 3.2.0
271      */
272     @Parameter(property = "gpg.bestPractices", defaultValue = "false")
273     private boolean bestPractices;
274 
275     /**
276      * Current user system settings for use in Maven.
277      *
278      * @since 1.6
279      */
280     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
281     protected Settings settings;
282 
283     /**
284      * Maven Security Dispatcher.
285      *
286      * @since 1.6
287      * @deprecated Provides quasi-encryption, should be avoided.
288      */
289     @Deprecated
290     @Component
291     private SecDispatcher secDispatcher;
292 
293     @Override
294     public final void execute() throws MojoExecutionException, MojoFailureException {
295         if (skip) {
296             // We're skipping the signing stuff
297             return;
298         }
299         if (bestPractices && (isNotBlank(passphrase) || isNotBlank(passphraseServerId))) {
300             // Stop propagating worst practices: passphrase MUST NOT be in any file on disk
301             throw new MojoFailureException(
302                     "Do not store passphrase in any file (disk or SCM repository), rely on GnuPG agent or provide passphrase in "
303                             + passphraseEnvName + " environment variable.");
304         }
305 
306         doExecute();
307     }
308 
309     protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;
310 
311     private void logBestPracticeWarning(String source) {
312         getLog().warn("");
313         getLog().warn("W A R N I N G");
314         getLog().warn("");
315         getLog().warn("Do not store passphrase in any file (disk or SCM repository),");
316         getLog().warn("instead rely on GnuPG agent or provide passphrase in ");
317         getLog().warn(passphraseEnvName + " environment variable for batch mode.");
318         getLog().warn("");
319         getLog().warn("Sensitive content loaded from " + source);
320         getLog().warn("");
321     }
322 
323     protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFailureException {
324         AbstractGpgSigner signer;
325         if (GpgSigner.NAME.equals(this.signer)) {
326             signer = new GpgSigner(executable);
327         } else if (BcSigner.NAME.equals(this.signer)) {
328             signer = new BcSigner(
329                     session.getRepositorySession(),
330                     keyEnvName,
331                     keyFingerprintEnvName,
332                     agentSocketLocations,
333                     keyFilePath,
334                     keyFingerprint);
335         } else {
336             throw new MojoFailureException("Unknown signer: " + this.signer);
337         }
338 
339         signer.setLog(getLog());
340         signer.setInteractive(settings.isInteractiveMode());
341         signer.setKeyName(keyname);
342         signer.setUseAgent(useAgent);
343         signer.setHomeDirectory(homedir);
344         signer.setDefaultKeyring(defaultKeyring);
345         signer.setSecretKeyring(secretKeyring);
346         signer.setPublicKeyring(publicKeyring);
347         signer.setLockMode(lockMode);
348         signer.setArgs(gpgArguments);
349 
350         // "new way": env prevails
351         String passphrase =
352                 (String) session.getRepositorySession().getConfigProperties().get("env." + passphraseEnvName);
353         if (isNotBlank(passphrase)) {
354             signer.setPassPhrase(passphrase);
355         } else if (!bestPractices) {
356             // "old way": mojo config
357             passphrase = this.passphrase;
358             if (isNotBlank(passphrase)) {
359                 logBestPracticeWarning("Mojo configuration");
360                 signer.setPassPhrase(passphrase);
361             } else {
362                 // "old way": serverId + settings
363                 passphrase = loadGpgPassphrase();
364                 if (isNotBlank(passphrase)) {
365                     logBestPracticeWarning("settings.xml");
366                     signer.setPassPhrase(passphrase);
367                 } else {
368                     // "old way": project properties
369                     passphrase = getPassphrase(mavenProject);
370                     if (isNotBlank(passphrase)) {
371                         logBestPracticeWarning("Project properties");
372                         signer.setPassPhrase(passphrase);
373                     }
374                 }
375             }
376         }
377         signer.prepare();
378 
379         return signer;
380     }
381 
382     private boolean isNotBlank(String string) {
383         return string != null && !string.trim().isEmpty();
384     }
385 
386     // Below is attic, to be thrown out
387 
388     @Deprecated
389     private static final String GPG_PASSPHRASE = "gpg.passphrase";
390 
391     @Deprecated
392     private String loadGpgPassphrase() throws MojoFailureException {
393         if (isNotBlank(passphraseServerId)) {
394             Server server = settings.getServer(passphraseServerId);
395             if (server != null) {
396                 if (isNotBlank(server.getPassphrase())) {
397                     try {
398                         return secDispatcher.decrypt(server.getPassphrase());
399                     } catch (SecDispatcherException e) {
400                         throw new MojoFailureException("Unable to decrypt gpg passphrase", e);
401                     }
402                 }
403             }
404         }
405         return null;
406     }
407 
408     @Deprecated
409     public String getPassphrase(MavenProject project) {
410         String pass = null;
411         if (project != null) {
412             pass = project.getProperties().getProperty(GPG_PASSPHRASE);
413             if (pass == null) {
414                 MavenProject prj2 = findReactorProject(project);
415                 pass = prj2.getProperties().getProperty(GPG_PASSPHRASE);
416             }
417         }
418         if (project != null && pass != null) {
419             findReactorProject(project).getProperties().setProperty(GPG_PASSPHRASE, pass);
420         }
421         return pass;
422     }
423 
424     @Deprecated
425     private MavenProject findReactorProject(MavenProject prj) {
426         if (prj.getParent() != null
427                 && prj.getParent().getBasedir() != null
428                 && prj.getParent().getBasedir().exists()) {
429             return findReactorProject(prj.getParent());
430         }
431         return prj;
432     }
433 }