From 036dfe005c64a3f881f493806e854ead033c563d Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 8 Mar 2024 09:02:38 +0100 Subject: [PATCH] [MGPG-105] [MGPG-108] Make plugin backward compat and update site and doco (#77) Document the latest changes. But also implement Java changes related to agent usage and back compat. Now we distinguish really (and option is un-deprecated): * `useAgent` * `interactive` Means to provide secret needed for signing: |flag|agent pinentry|agent cached|env variable| |---|---|---|---| |`useAgent && interactive` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | |`useAgent && !interactive` | :x: | :heavy_check_mark: | :heavy_check_mark: | |`!useAgent && interactive` | :x: | :x: | :heavy_check_mark: | |`!useAgent && !interactive` | :x: | :x: | :heavy_check_mark: | Finally, `!bestPractices` provides existing "pass in passphrase as property" mode as well. As first really means "can we talk to the agent" and second means "can agent pop up pinentry dialogue" for both signers. In fact, this was the case already in `GpgSigner`, but `BcSigner` conflated the two. As it turns out, `gpg-agent` also supports "non interactive" password caching that now both signers make use of. --- https://issues.apache.org/jira/browse/MGPG-108 https://issues.apache.org/jira/browse/MGPG-105 --- .../maven/plugins/gpg/AbstractGpgMojo.java | 102 +++++++++--------- .../apache/maven/plugins/gpg/BcSigner.java | 79 ++++++++------ .../plugins/gpg/SignAndDeployFileMojo.java | 2 +- .../apt/examples/deploy-signed-artifacts.apt | 55 +++++++++- src/site/apt/index.apt.vm | 19 ++++ src/site/apt/usage.apt.vm | 54 ++++++++-- .../maven/plugins/gpg/BcSignerTest.java | 15 +++ 7 files changed, 226 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgMojo.java b/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgMojo.java index 91009f8..52c61b2 100644 --- a/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgMojo.java +++ b/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgMojo.java @@ -51,11 +51,13 @@ public abstract class AbstractGpgMojo extends AbstractMojo { private String agentSocketLocations; /** - * BC Signer only: The path of the exported key in TSK format, and probably passphrase protected. If relative, - * the file is resolved against Maven local repository root. + * BC Signer only: The path of the exported key in + * TSK format, + * and may be passphrase protected. If relative, the file is resolved against user home directory. *

- * Note: it is not recommended to have sensitive files on disk or SCM repository, this mode is more to be used - * in local environment (workstations) or for testing purposes. + * Note: it is not recommended to have sensitive files checked into SCM repository. Key file should reside on + * developer workstation, outside of SCM tracked repository. For CI-like use cases you should set the + * key material as env variable instead. * * @since 3.2.0 */ @@ -71,9 +73,11 @@ public abstract class AbstractGpgMojo extends AbstractMojo { private String keyFingerprint; /** - * BC Signer only: The env variable name where the GnuPG key is set. The default value is {@code MAVEN_GPG_KEY}. + * BC Signer only: The env variable name where the GnuPG key is set. * To use BC Signer you must provide GnuPG key, as it does not use GnuPG home directory to extract/find the - * key (while it does use GnuPG Agent to ask for password in interactive mode). + * key (while it does use GnuPG Agent to ask for password in interactive mode). The key should be in + * TSK format and may + * be passphrase protected. * * @since 3.2.0 */ @@ -82,7 +86,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo { /** * BC Signer only: The env variable name where the GnuPG key fingerprint is set, if the provided keyring contains - * multiple keys. The default value is {@code MAVEN_GPG_KEY_FINGERPRINT}. + * multiple keys. * * @since 3.2.0 */ @@ -90,8 +94,8 @@ public abstract class AbstractGpgMojo extends AbstractMojo { private String keyFingerprintEnvName; /** - * The env variable name where the GnuPG passphrase is set. The default value is {@code MAVEN_GPG_PASSPHRASE}. - * This is the recommended way to pass passphrase for signing in batch mode execution of Maven. + * The env variable name where the GnuPG passphrase is set. This is the recommended way to pass passphrase + * for signing in batch mode execution of Maven. * * @since 3.2.0 */ @@ -109,23 +113,25 @@ public abstract class AbstractGpgMojo extends AbstractMojo { /** * The passphrase to use when signing. If not given, look up the value under Maven - * settings using server id at 'passphraseServerKey' configuration. Do not use this parameter, if set, the - * plugin will fail. Passphrase should be provided only via gpg-agent (interactive) or via env variable - * (non-interactive). + * settings using server id at 'passphraseServerKey' configuration. Do not use this parameter, it leaks + * sensitive data. Passphrase should be provided only via gpg-agent or via env variable. + * If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured. * - * @deprecated Do not use this configuration, plugin will fail if set. + * @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env + * variables instead. **/ @Deprecated @Parameter(property = "gpg.passphrase") private String passphrase; /** - * Server id to lookup the passphrase under Maven settings. Do not use this parameter, if set, the - * plugin will fail. Passphrase should be provided only via gpg-agent (interactive) or via env variable - * (non-interactive). + * Server id to lookup the passphrase under Maven settings. Do not use this parameter, it leaks + * sensitive data. Passphrase should be provided only via gpg-agent or via env variable. + * If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured. * * @since 1.6 - * @deprecated Do not use this configuration, plugin will fail if set. + * @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env + * variables instead. **/ @Deprecated @Parameter(property = "gpg.passphraseServerId") @@ -138,23 +144,22 @@ public abstract class AbstractGpgMojo extends AbstractMojo { private String keyname; /** - * GPG Signer only: Passes --use-agent or --no-use-agent to gpg. If using an agent, the - * passphrase is optional as the agent will provide it. For gpg2, specify true as --no-use-agent was removed in - * gpg2 and doesn't ask for a passphrase anymore. Deprecated, and better to rely on session "interactive" setting - * (if interactive, agent will be used, otherwise not). - * - * @deprecated + * All signers: whether gpg-agent is allowed to be used or not. If enabled, passphrase is optional, as agent may + * provide it. Have to be noted, that in "batch" mode, gpg-agent will be prevented to pop up pinentry + * dialogue, hence best is to "prime" the agent caches beforehand. + *

+ * GPG Signer: Passes --use-agent or --no-use-agent option to gpg if it is version 2.1 + * or older. Otherwise, will use an agent. In non-interactive mode gpg options are appended with + * --pinentry-mode error, preventing gpg agent to pop up pinentry dialogue. Agent will be able to + * hand over only cached passwords. + *

+ * BC Signer: Allows signer to communicate with gpg agent. In non-interactive mode it uses + * --no-ask option with the GET_PASSPHRASE function. Agent will be able to hand over + * only cached passwords. */ - @Deprecated @Parameter(property = "gpg.useagent", defaultValue = "true") private boolean useAgent; - /** - * Detect is session interactive or not. - */ - @Parameter(defaultValue = "${settings.interactiveMode}", readonly = true) - private boolean interactive; - /** * GPG Signer only: The path to the GnuPG executable to use for artifact signing. Defaults to either "gpg" or * "gpg.exe" depending on the operating system. @@ -182,7 +187,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo { * ‘private-keys-v1.d’ directory below the GnuPG home directory. * * @since 1.2 - * @deprecated + * @deprecated Obsolete option since GnuPG 2.1 version. */ @Deprecated @Parameter(property = "gpg.secretKeyring") @@ -198,7 +203,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo { * ‘pubring.kbx’ file below the GnuPG home directory. * * @since 1.2 - * @deprecated + * @deprecated Obsolete option since GnuPG 2.1 version. */ @Deprecated @Parameter(property = "gpg.publicKeyring") @@ -224,7 +229,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo { private boolean skip; /** - * Sets the arguments to be passed to gpg. Example: + * GPG Signer only: Sets the arguments to be passed to gpg. Example: * *

      * <gpgArguments>
@@ -256,24 +261,24 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
     // === Deprecated stuff
 
     /**
-     * Switch to lax plugin enforcement of "best practices". If set to {@code false}, plugin will retain all the
-     * backward compatibility regarding getting secrets (but will warn). By default, plugin enforces "best practices"
-     * and in such cases plugin fails.
+     * Switch to improve plugin enforcement of "best practices". If set to {@code false}, plugin retains all the
+     * backward compatibility regarding getting secrets (but will warn). If set to {@code true}, plugin will fail
+     * if any "bad practices" regarding sensitive data handling are detected. By default, plugin remains backward
+     * compatible (this flag is {@code false}). Somewhere in the future, when this parameter enabling transitioning
+     * from older plugin versions is removed, the logic using this flag will be modified like it is set to {@code true}.
+     * It is warmly advised to configure this parameter to {@code true} and migrate project and user environment
+     * regarding how sensitive information is stored.
      *
      * @since 3.2.0
-     * @deprecated
      */
-    @Deprecated
-    @Parameter(property = "gpg.bestPractices", defaultValue = "true")
+    @Parameter(property = "gpg.bestPractices", defaultValue = "false")
     private boolean bestPractices;
 
     /**
      * Current user system settings for use in Maven.
      *
      * @since 1.6
-     * @deprecated
      */
-    @Deprecated
     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
     private Settings settings;
 
@@ -281,7 +286,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
      * Maven Security Dispatcher.
      *
      * @since 1.6
-     * @deprecated
+     * @deprecated Provides quasi-encryption, should be avoided.
      */
     @Deprecated
     @Component
@@ -310,7 +315,7 @@ private void logBestPracticeWarning(String source) {
         getLog().warn("W A R N I N G");
         getLog().warn("");
         getLog().warn("Do not store passphrase in any file (disk or SCM repository),");
-        getLog().warn("instead rely on GnuPG agent in interactive sessions, or provide passphrase in ");
+        getLog().warn("instead rely on GnuPG agent or provide passphrase in ");
         getLog().warn(passphraseEnvName + " environment variable for batch mode.");
         getLog().warn("");
         getLog().warn("Sensitive content loaded from " + source);
@@ -334,7 +339,7 @@ protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFail
         }
 
         signer.setLog(getLog());
-        signer.setInteractive(interactive);
+        signer.setInteractive(settings.isInteractiveMode());
         signer.setKeyName(keyname);
         signer.setUseAgent(useAgent);
         signer.setHomeDirectory(homedir);
@@ -371,13 +376,6 @@ protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFail
                 }
             }
         }
-
-        // gpg signer: always failed if no passphrase and no agent and not interactive: retain this behavior
-        // bc signer: it is optimistic, will fail during prepare() only IF key is passphrase protected
-        if (GpgSigner.NAME.equals(this.signer) && null == passphrase && !useAgent && !interactive) {
-            throw new MojoFailureException("Cannot obtain passphrase in batch mode");
-        }
-
         signer.prepare();
 
         return signer;
@@ -419,7 +417,7 @@ public String getPassphrase(MavenProject project) {
                 pass = prj2.getProperties().getProperty(GPG_PASSPHRASE);
             }
         }
-        if (project != null) {
+        if (project != null && pass != null) {
             findReactorProject(project).getProperties().setProperty(GPG_PASSPHRASE, pass);
         }
         return pass;
diff --git a/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java b/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java
index 3ab8c22..6a0fc2a 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java
@@ -34,6 +34,7 @@
 import java.time.ZoneId;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -71,11 +72,6 @@ public class BcSigner extends AbstractGpgSigner {
     public static final String NAME = "bc";
 
     public interface Loader {
-        /**
-         * Returns {@code true} if this loader requires user interactivity.
-         */
-        boolean isInteractive();
-
         /**
          * Returns the key ring material, or {@code null}.
          */
@@ -93,17 +89,12 @@ default byte[] loadKeyFingerprint(RepositorySystemSession session) throws IOExce
         /**
          * Returns the key password, or {@code null}.
          */
-        default char[] loadPassword(RepositorySystemSession session, long keyId) throws IOException {
+        default char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
             return null;
         }
     }
 
     public final class GpgEnvLoader implements Loader {
-        @Override
-        public boolean isInteractive() {
-            return false;
-        }
-
         @Override
         public byte[] loadKeyRingMaterial(RepositorySystemSession session) {
             String keyMaterial = (String) session.getConfigProperties().get("env." + keyEnvName);
@@ -134,16 +125,13 @@ public final class GpgConfLoader implements Loader {
          */
         private static final long MAX_SIZE = 5 * 1024 + 1L;
 
-        @Override
-        public boolean isInteractive() {
-            return false;
-        }
-
         @Override
         public byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
             Path keyPath = Paths.get(keyFilePath);
             if (!keyPath.isAbsolute()) {
-                keyPath = session.getLocalRepository().getBasedir().toPath().resolve(keyPath);
+                keyPath = Paths.get(System.getProperty("user.home"))
+                        .resolve(keyPath)
+                        .toAbsolutePath();
             }
             if (Files.isRegularFile(keyPath)) {
                 if (Files.size(keyPath) < MAX_SIZE) {
@@ -171,19 +159,25 @@ public byte[] loadKeyFingerprint(RepositorySystemSession session) {
 
     public final class GpgAgentPasswordLoader implements Loader {
         @Override
-        public boolean isInteractive() {
-            return true;
-        }
-
-        @Override
-        public char[] loadPassword(RepositorySystemSession session, long keyId) throws IOException {
+        public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
+            if (!useAgent) {
+                return null;
+            }
             List socketLocations = Arrays.stream(agentSocketLocations.split(","))
                     .filter(s -> s != null && !s.isEmpty())
                     .collect(Collectors.toList());
             for (String socketLocation : socketLocations) {
                 try {
-                    return load(keyId, Paths.get(System.getProperty("user.home"), socketLocation))
-                            .toCharArray();
+                    Path socketLocationPath = Paths.get(socketLocation);
+                    if (!socketLocationPath.isAbsolute()) {
+                        socketLocationPath = Paths.get(System.getProperty("user.home"))
+                                .resolve(socketLocationPath)
+                                .toAbsolutePath();
+                    }
+                    String pw = load(fingerprint, socketLocationPath);
+                    if (pw != null) {
+                        return pw.toCharArray();
+                    }
                 } catch (SocketException e) {
                     // try next location
                 }
@@ -191,7 +185,7 @@ public char[] loadPassword(RepositorySystemSession session, long keyId) throws I
             return null;
         }
 
-        private String load(long keyId, Path socketPath) throws IOException {
+        private String load(byte[] fingerprint, Path socketPath) throws IOException {
             try (AFUNIXSocket sock = AFUNIXSocket.newInstance()) {
                 sock.connect(AFUNIXSocketAddress.of(socketPath));
                 try (BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
@@ -210,23 +204,43 @@ private String load(long keyId, Path socketPath) throws IOException {
                         os.flush();
                         expectOK(in);
                     }
-                    String hexKeyId = Long.toHexString(keyId & 0xFFFFFFFFL);
+                    String hexKeyFingerprint = Hex.toHexString(fingerprint);
+                    String displayFingerprint = hexKeyFingerprint.toUpperCase(Locale.ROOT);
                     // https://unix.stackexchange.com/questions/71135/how-can-i-find-out-what-keys-gpg-agent-has-cached-like-how-ssh-add-l-shows-yo
-                    String instruction = "GET_PASSPHRASE " + hexKeyId + " " + "Passphrase+incorrect"
-                            + " GnuPG+Key+Passphrase Enter+passphrase+for+encrypted+GnuPG+key+" + hexKeyId
+                    String instruction = "GET_PASSPHRASE "
+                            + (!isInteractive ? "--no-ask " : "")
+                            + hexKeyFingerprint
+                            + " "
+                            + "X "
+                            + "GnuPG+Passphrase "
+                            + "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+"
+                            + displayFingerprint
                             + "+to+use+it+for+signing+Maven+Artifacts\n";
                     os.write((instruction).getBytes());
                     os.flush();
-                    return new String(Hex.decode(expectOK(in).trim()));
+                    String pw = mayExpectOK(in);
+                    if (pw != null) {
+                        return new String(Hex.decode(pw.trim()));
+                    }
+                    return null;
                 }
             }
         }
 
-        private String expectOK(BufferedReader in) throws IOException {
+        private void expectOK(BufferedReader in) throws IOException {
             String response = in.readLine();
             if (!response.startsWith("OK")) {
                 throw new IOException("Expected OK but got this instead: " + response);
             }
+        }
+
+        private String mayExpectOK(BufferedReader in) throws IOException {
+            String response = in.readLine();
+            if (response.startsWith("ERR")) {
+                return null;
+            } else if (!response.startsWith("OK")) {
+                throw new IOException("Expected OK/ERR but got this instead: " + response);
+            }
             return response.substring(Math.min(response.length(), 3));
         }
     }
@@ -265,7 +279,6 @@ public String signerName() {
     public void prepare() throws MojoFailureException {
         try {
             List loaders = Stream.of(new GpgEnvLoader(), new GpgConfLoader(), new GpgAgentPasswordLoader())
-                    .filter(l -> this.isInteractive || !l.isInteractive())
                     .collect(Collectors.toList());
 
             byte[] keyRingMaterial = null;
@@ -327,7 +340,7 @@ public void prepare() throws MojoFailureException {
             final boolean keyPassNeeded = secretKey.getKeyEncryptionAlgorithm() != SymmetricKeyAlgorithmTags.NULL;
             if (keyPassNeeded && keyPassword == null) {
                 for (Loader loader : loaders) {
-                    keyPassword = loader.loadPassword(session, secretKey.getKeyID());
+                    keyPassword = loader.loadPassword(session, secretKey.getFingerprint());
                     if (keyPassword != null) {
                         break;
                     }
diff --git a/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java b/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
index 52f9828..2d2c699 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
@@ -135,7 +135,7 @@ public class SignAndDeployFileMojo extends AbstractGpgMojo {
 
     /**
      * URL where the artifact will be deployed. 
- * ie ( file:///C:/m2-repo or scp://host.com/path/to/repo ) + * ie ( file:///C:/m2-repo or https://host.com/path/to/repo ) */ @Parameter(property = "url", required = true) private String url; diff --git a/src/site/apt/examples/deploy-signed-artifacts.apt b/src/site/apt/examples/deploy-signed-artifacts.apt index 79c49ae..cfbc789 100644 --- a/src/site/apt/examples/deploy-signed-artifacts.apt +++ b/src/site/apt/examples/deploy-signed-artifacts.apt @@ -32,16 +32,61 @@ Deploy Signed Artifacts mvn deploy +----------+ - If you have configured this plugin according to the instructions in the - {{{../usage.html}usage page}}, you just need to specify the passphrase for - your private key on the command line like this: + If you have configured this plugin according to the instructions in the + {{{../usage.html}usage page}}, nothing changes for interactive sessions: +----------+ -mvn deploy -Dgpg.passphrase=thephrase +mvn deploy ++----------+ + + And the gpg-agent will prompt you for passphrase. + + General remark regarding environment variables: Examples below are NOT + instructions how to invoke Maven, as if you'd follow these examples + literally, it would defy the goal of not leaking cleartext passphrases, + as these would end up in terminal history! You should set these environment + variables on your own discretion in some secure manner. + + If you use "batch" build (or build is invoked by Maven Release Plugin), + then gpg-agent will be unable to ask interactively for password. In such + cases you want to "prime" the agent with passwords first. See {{{../usage.html}usage page}} + for details how to "prime" gpg-agent. + + In "agent-less" (CI like usage) mode one can supply passphrase via environment + variable only. + ++----------+ +MAVEN_GPG_PASSPHRASE=thephrase mvn --batch-mode deploy ++----------+ + +* Sign using BC Signer + + By default the plugin uses the "gpg" Signer (that relies on GnuPG tool installed + on host OS). The "bc" Signer on the other hand implements signing in pure + Java using Bouncy Castle libraries. + + The "bc" signer, unlike "gpg", does not and cannot make use of <<<~/.gnupg>>> + directory in user home, and have to have configured both, the key used + to sign and the passphrase (if key is passphrase protected). The key is expected to be in + TSK format (see {{{https://openpgp.dev/book/private_keys.html#transferable-secret-key-format}"Transferable Secret Keys"}} format). + ++----------+ +mvn deploy -Dgpg.signer=bc -Dgpg.keyFilePath=path/to/key ++----------+ + + In interactive sessions, similarly as with "gpg" Signer, gpg-agent will be used to + ask for password. In batch sessions, you can use environment variables to achieve + similar thing: + ++----------+ +MAVEN_GPG_PASSPHRASE=thephrase mvn deploy -Dgpg.signer=bc -Dgpg.keyFilePath=path/to/key +----------+ - If you don't specify a passphrase, it will prompt for one. + Ultimately, you can place both, they key and passphrase into environment variables: ++----------+ +MAVEN_GPG_KEY=thekeymaterial MAVEN_GPG_PASSPHRASE=thephrase mvn deploy -Dgpg.signer=bc ++----------+ * Install/Deploy without configuring the plugin in the POM diff --git a/src/site/apt/index.apt.vm b/src/site/apt/index.apt.vm index a0e4840..4a6f049 100644 --- a/src/site/apt/index.apt.vm +++ b/src/site/apt/index.apt.vm @@ -40,6 +40,25 @@ ${project.name} General instructions on how to use the GPG Plugin can be found on the {{{./usage.html}usage page}}. Some more specific use cases are described in the examples given below. + Since 3.2.0, plugin can enforce "best practices", and will fail the build if any violation are detected. + In short, intent is to stop users putting secrets (plaintext or quasi-encrypted) in their Maven configuration + files (settings.xml, POMs) or use secrets in a way they leave trace (like in terminal history). In this mode, + plugin leaves two options to obtain passphrase: use of gpg-agent (with pinentry in interactive sessions, or pre-seeded + "cached" passwords in non-interactive mode), and use of environment variables in batch/non-interactive/no-agent + sessions. To enable "best practices" configure the plugin accordingly (see goals, look for <<>> + configuration). By default, the plugin does not enforce these, but does emit warnings. + + To "prime" the GnuPG agent, you have several options: either just "sign" something beforehand (usable on + workstations) like <<>>, or use + {{{https://www.gnupg.org/documentation/manuals/gnupg/Invoking-gpg_002dpreset_002dpassphrase.html}gpg-preset-passphrase}} + GnuPG command, that will "cache" the password in gpg-agent for given login session, cache content is lost between + reboots. Note: this tool, while is part of GnuPG suite, may not be on path. Check your OS documentation for it. + For example, on modern versions of Fedora this tool is not on path, but is located in <<>>. + + <> The GpgSigner, that uses GnuPG tool installed and configured on the host OS, while it does contain support + for older GnuPGP versions, is tested (locally by developers and on CI systems) only by using + {{{https://www.gnupg.org/download/index.html}latest "stable" GnuPG version}} (scroll to bottom of page for EOL information). + In case you still have questions regarding the plugin's usage, please have a look at the {{{./faq.html}FAQ}} and feel free to contact the {{{./mailing-lists.html}user mailing list}}. The posts to the mailing list are archived and could already contain the answer to your question as part of an older thread. Hence, it is also worth browsing/searching diff --git a/src/site/apt/usage.apt.vm b/src/site/apt/usage.apt.vm index 4e27740..c69776a 100644 --- a/src/site/apt/usage.apt.vm +++ b/src/site/apt/usage.apt.vm @@ -29,7 +29,7 @@ Usage Signs all of a project's attached artifacts with GnuPG. - You need to have previously configured the default key. + You need to have previously configured the default key using GnuPG. <<>> also needs to be on the search path. @@ -60,27 +60,63 @@ Usage +----------+ - Then you specify the passphrase on the command line. Like this: + Ideally, if invoked on workstation, you should rely on gpg-agent to + collect passphrase from, as in that way no secrets will enter terminal history nor + any file on disk. In agent-less (batch) sessions, typically on CI, you should provide + passphrases via environment variable (see goals). + + <> When using the GPG Plugin in combination with the Maven Release Plugin, + on a developer Workstation, you should rely on gpg-agent, but have it "primed", + as Release plugin invokes build in batch mode, that will prevent agent to present + the "pinentry pop up". If fully unattended release is being done, for example + on a CI system, then with <<>> set to <<>> one can pass + the passphrase via environment variable. + + <>, one can perform simple "sign" operation on + workstation like this <<>> or can use + gpg command {{{https://www.gnupg.org/documentation/manuals/gnupg/Invoking-gpg_002dpreset_002dpassphrase.html}gpg-preset-passphrase}}. + + General remark regarding environment variables: Examples below are NOT + instructions how to invoke Maven, as if you'd follow these examples + literally, it would defy the goal of not leaking cleartext passphrases, + as these would end up in terminal history! You should set these environment + variables on your own discretion in some secure manner. +----------+ -mvn verify -Dgpg.passphrase=thephrase +MAVEN_GPG_PASSPHRASE=thephrase mvn release:perform +----------+ - If you don't specify a passphrase, it will prompt for one. + One "real life" example, on Un*x systems could be this: + ++----------+ +read -s -p "Enter your GnuPG key passphrase: " MAVEN_GPG_PASSPHRASE; mvn release:perform ++----------+ - <> When using the GPG Plugin in combination with the Maven Release Plugin, you might need to specify the passphrase - like this: + Finally, the passphrase can be given on the command line as well, but this is not recommended, + and plugin will emit warnings. This mode of invocation is highly discouraged, + as passphrase in cleartext is recorded into Terminal history. +----------+ -mvn release:perform -Darguments=-Dgpg.passphrase=thephrase +mvn verify -Dgpg.passphrase=thephrase +----------+ - This accounts for the fact, that the Release Plugin forks Maven and system properties of the current Maven session are - unfortunately not automatically propagated to the forked Maven session (see also {{{https://issues.apache.org/jira/browse/MGPG-9}MGPG-9}}). +* Security considerations + In the future, plugin will operate in <<>> mode enabled, and will fail + the build if any violation of those is detected. The goal of this change was to protect + plugin users from possible "leaks" of sensitive information (like passphrase is). + Sensitive information like passphrases should never be stored on disks (plaintext + or quasi-encrypted), nor should be used in way they may "leak" into other files + (for example bash terminal history). + + Hence, examples below will work by emit warnings. In the future, once "best practices" + become enforced, these examples will not work anymore. * Configure passphrase in settings.xml + <> These techniques below are highly discouraged. Ideally sensitive information + should enter via gpg-agent or via environment variables. + Instead of specifying the passphrase on the command line, you can place it in your local <<>> either in clear or {{{/guides/mini/guide-encryption.html}encrypted}} text. diff --git a/src/test/java/org/apache/maven/plugins/gpg/BcSignerTest.java b/src/test/java/org/apache/maven/plugins/gpg/BcSignerTest.java index 67d360d..49bfb97 100644 --- a/src/test/java/org/apache/maven/plugins/gpg/BcSignerTest.java +++ b/src/test/java/org/apache/maven/plugins/gpg/BcSignerTest.java @@ -54,6 +54,7 @@ void testAgent() throws Exception { DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(); session.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) .newInstance(session, new LocalRepository("target/local-repo"))); + // first: interactive session: it will pop up a pinentry dialogue, enter "TEST" BcSigner signer = new BcSigner( session, "unimportant", @@ -61,6 +62,20 @@ void testAgent() throws Exception { ".gnupg/S.gpg-agent", new File("src/test/resources/signing-key.asc").getAbsolutePath(), null); + signer.setUseAgent(true); + signer.setInteractive(true); + signer.prepare(); + + // second: non-interactive: will use agent but no 2nd popup will appear + signer = new BcSigner( + session, + "unimportant", + "unimportant", + ".gnupg/S.gpg-agent", + new File("src/test/resources/signing-key.asc").getAbsolutePath(), + null); + signer.setUseAgent(true); + signer.setInteractive(false); signer.prepare(); } }