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