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.Collections;
23  import java.util.List;
24  
25  import org.apache.maven.execution.MavenSession;
26  import org.apache.maven.plugin.AbstractMojo;
27  import org.apache.maven.plugin.MojoExecutionException;
28  import org.apache.maven.plugin.MojoFailureException;
29  import org.apache.maven.plugins.annotations.Component;
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.sonatype.plexus.components.cipher.DefaultPlexusCipher;
35  import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher;
36  import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
37  import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;
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      *
135      * @since 1.6
136      * @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env
137      * variables instead.
138      **/
139     @Deprecated
140     @Parameter(property = "gpg.passphraseServerId", defaultValue = GPG_PASSPHRASE)
141     private String passphraseServerId;
142 
143     /**
144      * GPG Signer only: The "name" of the key to sign with. Passed to gpg as <code>--local-user</code>.
145      */
146     @Parameter(property = "gpg.keyname")
147     private String keyname;
148 
149     /**
150      * All signers: whether gpg-agent is allowed to be used or not. If enabled, passphrase is optional, as agent may
151      * provide it. Have to be noted, that in "batch" mode, gpg-agent will be prevented to pop up pinentry
152      * dialogue, hence best is to "prime" the agent caches beforehand.
153      * <p>
154      * GPG Signer: Passes <code>--use-agent</code> or <code>--no-use-agent</code> option to gpg if it is version 2.1
155      * or older. Otherwise, will use an agent. In non-interactive mode gpg options are appended with
156      * <code>--pinentry-mode error</code>, preventing gpg agent to pop up pinentry dialogue. Agent will be able to
157      * hand over only cached passwords.
158      * <p>
159      * BC Signer: Allows signer to communicate with gpg agent. In non-interactive mode it uses
160      * <code>--no-ask</code> option with the <code>GET_PASSPHRASE</code> function. Agent will be able to hand over
161      * only cached passwords.
162      */
163     @Parameter(property = "gpg.useagent", defaultValue = "true")
164     private boolean useAgent;
165 
166     /**
167      * GPG Signer only: The path to the GnuPG executable to use for artifact signing. Defaults to either "gpg" or
168      * "gpg.exe" depending on the operating system.
169      *
170      * @since 1.1
171      */
172     @Parameter(property = "gpg.executable")
173     private String executable;
174 
175     /**
176      * GPG Signer only: Whether to add the default keyrings from gpg's home directory to the list of used keyrings.
177      *
178      * @since 1.2
179      */
180     @Parameter(property = "gpg.defaultKeyring", defaultValue = "true")
181     private boolean defaultKeyring;
182 
183     /**
184      * GPG Signer only: The path to a secret keyring to add to the list of keyrings. By default, only the
185      * {@code secring.gpg} from gpg's home directory is considered. Use this option (in combination with
186      * {@link #publicKeyring} and {@link #defaultKeyring} if required) to use a different secret key.
187      * <em>Note:</em> Relative paths are resolved against gpg's home directory, not the project base directory.
188      * <p>
189      * <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All secret keys are stored in the
190      * ‘private-keys-v1.d’ directory below the GnuPG home directory.
191      *
192      * @since 1.2
193      * @deprecated Obsolete option since GnuPG 2.1 version.
194      */
195     @Deprecated
196     @Parameter(property = "gpg.secretKeyring")
197     private String secretKeyring;
198 
199     /**
200      * GPG Signer only: The path to a public keyring to add to the list of keyrings. By default, only the
201      * {@code pubring.gpg} from gpg's home directory is considered. Use this option (and {@link #defaultKeyring}
202      * if required) to use a different public key. <em>Note:</em> Relative paths are resolved against gpg's home
203      * directory, not the project base directory.
204      * <p>
205      * <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All public keys are stored in the
206      * ‘pubring.kbx’ file below the GnuPG home directory.
207      *
208      * @since 1.2
209      * @deprecated Obsolete option since GnuPG 2.1 version.
210      */
211     @Deprecated
212     @Parameter(property = "gpg.publicKeyring")
213     private String publicKeyring;
214 
215     /**
216      * GPG Signer only: The lock mode to use when invoking gpg. By default no lock mode will be specified. Valid
217      * values are {@code once}, {@code multiple} and {@code never}. The lock mode gets translated into the
218      * corresponding {@code --lock-___} command line argument. Improper usage of this option may lead to data and
219      * key corruption.
220      *
221      * @see <a href="http://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html">the
222      *      --lock-options</a>
223      * @since 1.5
224      */
225     @Parameter(property = "gpg.lockMode")
226     private String lockMode;
227 
228     /**
229      * Skip doing the gpg signing.
230      */
231     @Parameter(property = "gpg.skip", defaultValue = "false")
232     private boolean skip;
233 
234     /**
235      * GPG Signer only: Sets the arguments to be passed to gpg. Example:
236      *
237      * <pre>
238      * &lt;gpgArguments&gt;
239      *   &lt;arg&gt;--no-random-seed-file&lt;/arg&gt;
240      *   &lt;arg&gt;--no-permission-warning&lt;/arg&gt;
241      * &lt;/gpgArguments&gt;
242      * </pre>
243      *
244      * @since 1.5
245      */
246     @Parameter
247     private List<String> gpgArguments;
248 
249     /**
250      * The name of the Signer implementation to use. Accepted values are {@code "gpg"} (the default, uses GnuPG
251      * executable) and {@code "bc"} (uses Bouncy Castle pure Java signer).
252      *
253      * @since 3.2.0
254      */
255     @Parameter(property = "gpg.signer", defaultValue = GpgSigner.NAME)
256     private String signer;
257 
258     /**
259      * @since 3.0.0
260      */
261     @Component
262     protected MavenSession session;
263 
264     /**
265      * Switch to improve plugin enforcement of "best practices". If set to {@code false}, plugin retains all the
266      * backward compatibility regarding getting secrets (but will warn). If set to {@code true}, plugin will fail
267      * if any "bad practices" regarding sensitive data handling are detected. By default, plugin remains backward
268      * compatible (this flag is {@code false}). Somewhere in the future, when this parameter enabling transitioning
269      * from older plugin versions is removed, the logic using this flag will be modified like it is set to {@code true}.
270      * It is warmly advised to configure this parameter to {@code true} and migrate project and user environment
271      * regarding how sensitive information is stored.
272      *
273      * @since 3.2.0
274      */
275     @Parameter(property = "gpg.bestPractices", defaultValue = "false")
276     private boolean bestPractices;
277 
278     /**
279      * Current user system settings for use in Maven.
280      *
281      * @since 1.6
282      */
283     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
284     protected Settings settings;
285 
286     /**
287      * Maven Security Dispatcher.
288      *
289      * @since 1.6
290      * @deprecated Provides quasi-encryption, should be avoided.
291      */
292     @Deprecated
293     private final SecDispatcher secDispatcher =
294             new DefaultSecDispatcher(new DefaultPlexusCipher(), Collections.emptyMap(), "~/.m2/settings-security.xml");
295 
296     @Override
297     public final void execute() throws MojoExecutionException, MojoFailureException {
298         if (skip) {
299             // We're skipping the signing stuff
300             return;
301         }
302         if (bestPractices && (isNotBlank(passphrase) || isNotBlank(passphraseServerId))) {
303             // Stop propagating worst practices: passphrase MUST NOT be in any file on disk
304             throw new MojoFailureException(
305                     "Do not store passphrase in any file (disk or SCM repository), rely on GnuPG agent or provide passphrase in "
306                             + passphraseEnvName + " environment variable.");
307         }
308 
309         doExecute();
310     }
311 
312     protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;
313 
314     private void logBestPracticeWarning(String source) {
315         getLog().warn("");
316         getLog().warn("W A R N I N G");
317         getLog().warn("");
318         getLog().warn("Do not store passphrase in any file (disk or SCM repository),");
319         getLog().warn("instead rely on GnuPG agent or provide passphrase in ");
320         getLog().warn(passphraseEnvName + " environment variable for batch mode.");
321         getLog().warn("");
322         getLog().warn("Sensitive content loaded from " + source);
323         getLog().warn("");
324     }
325 
326     protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFailureException {
327         AbstractGpgSigner signer;
328         if (GpgSigner.NAME.equals(this.signer)) {
329             signer = new GpgSigner(executable);
330         } else if (BcSigner.NAME.equals(this.signer)) {
331             signer = new BcSigner(
332                     session.getRepositorySession(),
333                     keyEnvName,
334                     keyFingerprintEnvName,
335                     agentSocketLocations,
336                     keyFilePath,
337                     keyFingerprint);
338         } else {
339             throw new MojoFailureException("Unknown signer: " + this.signer);
340         }
341 
342         signer.setLog(getLog());
343         signer.setInteractive(settings.isInteractiveMode());
344         signer.setKeyName(keyname);
345         signer.setUseAgent(useAgent);
346         signer.setHomeDirectory(homedir);
347         signer.setDefaultKeyring(defaultKeyring);
348         signer.setSecretKeyring(secretKeyring);
349         signer.setPublicKeyring(publicKeyring);
350         signer.setLockMode(lockMode);
351         signer.setArgs(gpgArguments);
352 
353         // "new way": env prevails
354         String passphrase =
355                 (String) session.getRepositorySession().getConfigProperties().get("env." + passphraseEnvName);
356         if (isNotBlank(passphrase)) {
357             signer.setPassPhrase(passphrase);
358         } else if (!bestPractices) {
359             // "old way": mojo config
360             passphrase = this.passphrase;
361             if (isNotBlank(passphrase)) {
362                 logBestPracticeWarning("Mojo configuration");
363                 signer.setPassPhrase(passphrase);
364             } else {
365                 // "old way": serverId + settings
366                 passphrase = loadGpgPassphrase();
367                 if (isNotBlank(passphrase)) {
368                     logBestPracticeWarning("settings.xml");
369                     signer.setPassPhrase(passphrase);
370                 } else {
371                     // "old way": project properties
372                     passphrase = getPassphrase(mavenProject);
373                     if (isNotBlank(passphrase)) {
374                         logBestPracticeWarning("Project properties");
375                         signer.setPassPhrase(passphrase);
376                     }
377                 }
378             }
379         }
380         signer.prepare();
381 
382         return signer;
383     }
384 
385     private boolean isNotBlank(String string) {
386         return string != null && !string.trim().isEmpty();
387     }
388 
389     // Below is attic, to be thrown out
390 
391     @Deprecated
392     private static final String GPG_PASSPHRASE = "gpg.passphrase";
393 
394     @Deprecated
395     private String loadGpgPassphrase() throws MojoFailureException {
396         if (isNotBlank(passphraseServerId)) {
397             Server server = settings.getServer(passphraseServerId);
398             if (server != null) {
399                 if (isNotBlank(server.getPassphrase())) {
400                     try {
401                         return secDispatcher.decrypt(server.getPassphrase());
402                     } catch (SecDispatcherException e) {
403                         throw new MojoFailureException("Unable to decrypt gpg passphrase", e);
404                     }
405                 }
406             }
407         }
408         return null;
409     }
410 
411     @Deprecated
412     public String getPassphrase(MavenProject project) {
413         String pass = null;
414         if (project != null) {
415             pass = project.getProperties().getProperty(GPG_PASSPHRASE);
416             if (pass == null) {
417                 MavenProject prj2 = findReactorProject(project);
418                 pass = prj2.getProperties().getProperty(GPG_PASSPHRASE);
419             }
420         }
421         if (project != null && pass != null) {
422             findReactorProject(project).getProperties().setProperty(GPG_PASSPHRASE, pass);
423         }
424         return pass;
425     }
426 
427     @Deprecated
428     private MavenProject findReactorProject(MavenProject prj) {
429         if (prj.getParent() != null
430                 && prj.getParent().getBasedir() != null
431                 && prj.getParent().getBasedir().exists()) {
432             return findReactorProject(prj.getParent());
433         }
434         return prj;
435     }
436 }