From 63f1c913aeedd803d739843577d431e9f1cad636 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Tue, 5 Mar 2024 11:12:57 +0100 Subject: [PATCH 1/2] [MGPG-108] Update site and doco Document the latest changes. --- https://issues.apache.org/jira/browse/MGPG-108 --- .../maven/plugins/gpg/AbstractGpgMojo.java | 102 +++++++++--------- .../apache/maven/plugins/gpg/BcSigner.java | 78 ++++++++------ .../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, 225 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..2840fb8 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,42 @@ 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 +278,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 +339,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(); } } From 2cb445c6918683dca7372deb63f0dddb419aa8c8 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 7 Mar 2024 13:32:50 +0100 Subject: [PATCH 2/2] Reformat --- src/main/java/org/apache/maven/plugins/gpg/BcSigner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 2840fb8..6a0fc2a 100644 --- a/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java +++ b/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java @@ -213,7 +213,8 @@ private String load(byte[] fingerprint, Path socketPath) throws IOException { + " " + "X " + "GnuPG+Passphrase " - + "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+" + displayFingerprint + + "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();