Skip to content

Commit 036dfe0

Browse files
authored
[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` | ✔️ | ✔️ | ✔️ | |`useAgent && !interactive` | ❌ | ✔️ | ✔️ | |`!useAgent && interactive` | ❌ | ❌ | ✔️ | |`!useAgent && !interactive` | ❌ | ❌ | ✔️ | 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
1 parent 0771b61 commit 036dfe0

File tree

7 files changed

+226
-100
lines changed

7 files changed

+226
-100
lines changed

src/main/java/org/apache/maven/plugins/gpg/AbstractGpgMojo.java

+50-52
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
5151
private String agentSocketLocations;
5252

5353
/**
54-
* BC Signer only: The path of the exported key in TSK format, and probably passphrase protected. If relative,
55-
* the file is resolved against Maven local repository root.
54+
* BC Signer only: The path of the exported key in
55+
* <a href="https://openpgp.dev/book/private_keys.html#transferable-secret-key-format">TSK format</a>,
56+
* and may be passphrase protected. If relative, the file is resolved against user home directory.
5657
* <p>
57-
* <em>Note: it is not recommended to have sensitive files on disk or SCM repository, this mode is more to be used
58-
* in local environment (workstations) or for testing purposes.</em>
58+
* <em>Note: it is not recommended to have sensitive files checked into SCM repository. Key file should reside on
59+
* developer workstation, outside of SCM tracked repository. For CI-like use cases you should set the
60+
* key material as env variable instead.</em>
5961
*
6062
* @since 3.2.0
6163
*/
@@ -71,9 +73,11 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
7173
private String keyFingerprint;
7274

7375
/**
74-
* BC Signer only: The env variable name where the GnuPG key is set. The default value is {@code MAVEN_GPG_KEY}.
76+
* BC Signer only: The env variable name where the GnuPG key is set.
7577
* To use BC Signer you must provide GnuPG key, as it does not use GnuPG home directory to extract/find the
76-
* key (while it does use GnuPG Agent to ask for password in interactive mode).
78+
* key (while it does use GnuPG Agent to ask for password in interactive mode). The key should be in
79+
* <a href="https://openpgp.dev/book/private_keys.html#transferable-secret-key-format">TSK format</a> and may
80+
* be passphrase protected.
7781
*
7882
* @since 3.2.0
7983
*/
@@ -82,16 +86,16 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
8286

8387
/**
8488
* BC Signer only: The env variable name where the GnuPG key fingerprint is set, if the provided keyring contains
85-
* multiple keys. The default value is {@code MAVEN_GPG_KEY_FINGERPRINT}.
89+
* multiple keys.
8690
*
8791
* @since 3.2.0
8892
*/
8993
@Parameter(property = "gpg.keyFingerprintEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_FINGERPRINT)
9094
private String keyFingerprintEnvName;
9195

9296
/**
93-
* The env variable name where the GnuPG passphrase is set. The default value is {@code MAVEN_GPG_PASSPHRASE}.
94-
* This is the recommended way to pass passphrase for signing in batch mode execution of Maven.
97+
* The env variable name where the GnuPG passphrase is set. This is the recommended way to pass passphrase
98+
* for signing in batch mode execution of Maven.
9599
*
96100
* @since 3.2.0
97101
*/
@@ -109,23 +113,25 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
109113

110114
/**
111115
* The passphrase to use when signing. If not given, look up the value under Maven
112-
* settings using server id at 'passphraseServerKey' configuration. <em>Do not use this parameter, if set, the
113-
* plugin will fail. Passphrase should be provided only via gpg-agent (interactive) or via env variable
114-
* (non-interactive).</em>
116+
* settings using server id at 'passphraseServerKey' configuration. <em>Do not use this parameter, it leaks
117+
* sensitive data. Passphrase should be provided only via gpg-agent or via env variable.
118+
* If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured.</em>
115119
*
116-
* @deprecated Do not use this configuration, plugin will fail if set.
120+
* @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env
121+
* variables instead.
117122
**/
118123
@Deprecated
119124
@Parameter(property = "gpg.passphrase")
120125
private String passphrase;
121126

122127
/**
123-
* Server id to lookup the passphrase under Maven settings. <em>Do not use this parameter, if set, the
124-
* plugin will fail. Passphrase should be provided only via gpg-agent (interactive) or via env variable
125-
* (non-interactive).</em>
128+
* Server id to lookup the passphrase under Maven settings. <em>Do not use this parameter, it leaks
129+
* sensitive data. Passphrase should be provided only via gpg-agent or via env variable.
130+
* If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured.</em>
126131
*
127132
* @since 1.6
128-
* @deprecated Do not use this configuration, plugin will fail if set.
133+
* @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env
134+
* variables instead.
129135
**/
130136
@Deprecated
131137
@Parameter(property = "gpg.passphraseServerId")
@@ -138,23 +144,22 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
138144
private String keyname;
139145

140146
/**
141-
* GPG Signer only: Passes <code>--use-agent</code> or <code>--no-use-agent</code> to gpg. If using an agent, the
142-
* passphrase is optional as the agent will provide it. For gpg2, specify true as --no-use-agent was removed in
143-
* gpg2 and doesn't ask for a passphrase anymore. Deprecated, and better to rely on session "interactive" setting
144-
* (if interactive, agent will be used, otherwise not).
145-
*
146-
* @deprecated
147+
* All signers: whether gpg-agent is allowed to be used or not. If enabled, passphrase is optional, as agent may
148+
* provide it. Have to be noted, that in "batch" mode, gpg-agent will be prevented to pop up pinentry
149+
* dialogue, hence best is to "prime" the agent caches beforehand.
150+
* <p>
151+
* GPG Signer: Passes <code>--use-agent</code> or <code>--no-use-agent</code> option to gpg if it is version 2.1
152+
* or older. Otherwise, will use an agent. In non-interactive mode gpg options are appended with
153+
* <code>--pinentry-mode error</code>, preventing gpg agent to pop up pinentry dialogue. Agent will be able to
154+
* hand over only cached passwords.
155+
* <p>
156+
* BC Signer: Allows signer to communicate with gpg agent. In non-interactive mode it uses
157+
* <code>--no-ask</code> option with the <code>GET_PASSPHRASE</code> function. Agent will be able to hand over
158+
* only cached passwords.
147159
*/
148-
@Deprecated
149160
@Parameter(property = "gpg.useagent", defaultValue = "true")
150161
private boolean useAgent;
151162

152-
/**
153-
* Detect is session interactive or not.
154-
*/
155-
@Parameter(defaultValue = "${settings.interactiveMode}", readonly = true)
156-
private boolean interactive;
157-
158163
/**
159164
* GPG Signer only: The path to the GnuPG executable to use for artifact signing. Defaults to either "gpg" or
160165
* "gpg.exe" depending on the operating system.
@@ -182,7 +187,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
182187
* ‘private-keys-v1.d’ directory below the GnuPG home directory.
183188
*
184189
* @since 1.2
185-
* @deprecated
190+
* @deprecated Obsolete option since GnuPG 2.1 version.
186191
*/
187192
@Deprecated
188193
@Parameter(property = "gpg.secretKeyring")
@@ -198,7 +203,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
198203
* ‘pubring.kbx’ file below the GnuPG home directory.
199204
*
200205
* @since 1.2
201-
* @deprecated
206+
* @deprecated Obsolete option since GnuPG 2.1 version.
202207
*/
203208
@Deprecated
204209
@Parameter(property = "gpg.publicKeyring")
@@ -224,7 +229,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
224229
private boolean skip;
225230

226231
/**
227-
* Sets the arguments to be passed to gpg. Example:
232+
* GPG Signer only: Sets the arguments to be passed to gpg. Example:
228233
*
229234
* <pre>
230235
* &lt;gpgArguments&gt;
@@ -256,32 +261,32 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
256261
// === Deprecated stuff
257262

258263
/**
259-
* Switch to lax plugin enforcement of "best practices". If set to {@code false}, plugin will retain all the
260-
* backward compatibility regarding getting secrets (but will warn). By default, plugin enforces "best practices"
261-
* and in such cases plugin fails.
264+
* Switch to improve plugin enforcement of "best practices". If set to {@code false}, plugin retains all the
265+
* backward compatibility regarding getting secrets (but will warn). If set to {@code true}, plugin will fail
266+
* if any "bad practices" regarding sensitive data handling are detected. By default, plugin remains backward
267+
* compatible (this flag is {@code false}). Somewhere in the future, when this parameter enabling transitioning
268+
* from older plugin versions is removed, the logic using this flag will be modified like it is set to {@code true}.
269+
* It is warmly advised to configure this parameter to {@code true} and migrate project and user environment
270+
* regarding how sensitive information is stored.
262271
*
263272
* @since 3.2.0
264-
* @deprecated
265273
*/
266-
@Deprecated
267-
@Parameter(property = "gpg.bestPractices", defaultValue = "true")
274+
@Parameter(property = "gpg.bestPractices", defaultValue = "false")
268275
private boolean bestPractices;
269276

270277
/**
271278
* Current user system settings for use in Maven.
272279
*
273280
* @since 1.6
274-
* @deprecated
275281
*/
276-
@Deprecated
277282
@Parameter(defaultValue = "${settings}", readonly = true, required = true)
278283
private Settings settings;
279284

280285
/**
281286
* Maven Security Dispatcher.
282287
*
283288
* @since 1.6
284-
* @deprecated
289+
* @deprecated Provides quasi-encryption, should be avoided.
285290
*/
286291
@Deprecated
287292
@Component
@@ -310,7 +315,7 @@ private void logBestPracticeWarning(String source) {
310315
getLog().warn("W A R N I N G");
311316
getLog().warn("");
312317
getLog().warn("Do not store passphrase in any file (disk or SCM repository),");
313-
getLog().warn("instead rely on GnuPG agent in interactive sessions, or provide passphrase in ");
318+
getLog().warn("instead rely on GnuPG agent or provide passphrase in ");
314319
getLog().warn(passphraseEnvName + " environment variable for batch mode.");
315320
getLog().warn("");
316321
getLog().warn("Sensitive content loaded from " + source);
@@ -334,7 +339,7 @@ protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFail
334339
}
335340

336341
signer.setLog(getLog());
337-
signer.setInteractive(interactive);
342+
signer.setInteractive(settings.isInteractiveMode());
338343
signer.setKeyName(keyname);
339344
signer.setUseAgent(useAgent);
340345
signer.setHomeDirectory(homedir);
@@ -371,13 +376,6 @@ protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFail
371376
}
372377
}
373378
}
374-
375-
// gpg signer: always failed if no passphrase and no agent and not interactive: retain this behavior
376-
// bc signer: it is optimistic, will fail during prepare() only IF key is passphrase protected
377-
if (GpgSigner.NAME.equals(this.signer) && null == passphrase && !useAgent && !interactive) {
378-
throw new MojoFailureException("Cannot obtain passphrase in batch mode");
379-
}
380-
381379
signer.prepare();
382380

383381
return signer;
@@ -419,7 +417,7 @@ public String getPassphrase(MavenProject project) {
419417
pass = prj2.getProperties().getProperty(GPG_PASSPHRASE);
420418
}
421419
}
422-
if (project != null) {
420+
if (project != null && pass != null) {
423421
findReactorProject(project).getProperties().setProperty(GPG_PASSPHRASE, pass);
424422
}
425423
return pass;

src/main/java/org/apache/maven/plugins/gpg/BcSigner.java

+46-33
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.time.ZoneId;
3535
import java.util.Arrays;
3636
import java.util.List;
37+
import java.util.Locale;
3738
import java.util.stream.Collectors;
3839
import java.util.stream.Stream;
3940

@@ -71,11 +72,6 @@ public class BcSigner extends AbstractGpgSigner {
7172
public static final String NAME = "bc";
7273

7374
public interface Loader {
74-
/**
75-
* Returns {@code true} if this loader requires user interactivity.
76-
*/
77-
boolean isInteractive();
78-
7975
/**
8076
* Returns the key ring material, or {@code null}.
8177
*/
@@ -93,17 +89,12 @@ default byte[] loadKeyFingerprint(RepositorySystemSession session) throws IOExce
9389
/**
9490
* Returns the key password, or {@code null}.
9591
*/
96-
default char[] loadPassword(RepositorySystemSession session, long keyId) throws IOException {
92+
default char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
9793
return null;
9894
}
9995
}
10096

10197
public final class GpgEnvLoader implements Loader {
102-
@Override
103-
public boolean isInteractive() {
104-
return false;
105-
}
106-
10798
@Override
10899
public byte[] loadKeyRingMaterial(RepositorySystemSession session) {
109100
String keyMaterial = (String) session.getConfigProperties().get("env." + keyEnvName);
@@ -134,16 +125,13 @@ public final class GpgConfLoader implements Loader {
134125
*/
135126
private static final long MAX_SIZE = 5 * 1024 + 1L;
136127

137-
@Override
138-
public boolean isInteractive() {
139-
return false;
140-
}
141-
142128
@Override
143129
public byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
144130
Path keyPath = Paths.get(keyFilePath);
145131
if (!keyPath.isAbsolute()) {
146-
keyPath = session.getLocalRepository().getBasedir().toPath().resolve(keyPath);
132+
keyPath = Paths.get(System.getProperty("user.home"))
133+
.resolve(keyPath)
134+
.toAbsolutePath();
147135
}
148136
if (Files.isRegularFile(keyPath)) {
149137
if (Files.size(keyPath) < MAX_SIZE) {
@@ -171,27 +159,33 @@ public byte[] loadKeyFingerprint(RepositorySystemSession session) {
171159

172160
public final class GpgAgentPasswordLoader implements Loader {
173161
@Override
174-
public boolean isInteractive() {
175-
return true;
176-
}
177-
178-
@Override
179-
public char[] loadPassword(RepositorySystemSession session, long keyId) throws IOException {
162+
public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
163+
if (!useAgent) {
164+
return null;
165+
}
180166
List<String> socketLocations = Arrays.stream(agentSocketLocations.split(","))
181167
.filter(s -> s != null && !s.isEmpty())
182168
.collect(Collectors.toList());
183169
for (String socketLocation : socketLocations) {
184170
try {
185-
return load(keyId, Paths.get(System.getProperty("user.home"), socketLocation))
186-
.toCharArray();
171+
Path socketLocationPath = Paths.get(socketLocation);
172+
if (!socketLocationPath.isAbsolute()) {
173+
socketLocationPath = Paths.get(System.getProperty("user.home"))
174+
.resolve(socketLocationPath)
175+
.toAbsolutePath();
176+
}
177+
String pw = load(fingerprint, socketLocationPath);
178+
if (pw != null) {
179+
return pw.toCharArray();
180+
}
187181
} catch (SocketException e) {
188182
// try next location
189183
}
190184
}
191185
return null;
192186
}
193187

194-
private String load(long keyId, Path socketPath) throws IOException {
188+
private String load(byte[] fingerprint, Path socketPath) throws IOException {
195189
try (AFUNIXSocket sock = AFUNIXSocket.newInstance()) {
196190
sock.connect(AFUNIXSocketAddress.of(socketPath));
197191
try (BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
@@ -210,23 +204,43 @@ private String load(long keyId, Path socketPath) throws IOException {
210204
os.flush();
211205
expectOK(in);
212206
}
213-
String hexKeyId = Long.toHexString(keyId & 0xFFFFFFFFL);
207+
String hexKeyFingerprint = Hex.toHexString(fingerprint);
208+
String displayFingerprint = hexKeyFingerprint.toUpperCase(Locale.ROOT);
214209
// https://unix.stackexchange.com/questions/71135/how-can-i-find-out-what-keys-gpg-agent-has-cached-like-how-ssh-add-l-shows-yo
215-
String instruction = "GET_PASSPHRASE " + hexKeyId + " " + "Passphrase+incorrect"
216-
+ " GnuPG+Key+Passphrase Enter+passphrase+for+encrypted+GnuPG+key+" + hexKeyId
210+
String instruction = "GET_PASSPHRASE "
211+
+ (!isInteractive ? "--no-ask " : "")
212+
+ hexKeyFingerprint
213+
+ " "
214+
+ "X "
215+
+ "GnuPG+Passphrase "
216+
+ "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+"
217+
+ displayFingerprint
217218
+ "+to+use+it+for+signing+Maven+Artifacts\n";
218219
os.write((instruction).getBytes());
219220
os.flush();
220-
return new String(Hex.decode(expectOK(in).trim()));
221+
String pw = mayExpectOK(in);
222+
if (pw != null) {
223+
return new String(Hex.decode(pw.trim()));
224+
}
225+
return null;
221226
}
222227
}
223228
}
224229

225-
private String expectOK(BufferedReader in) throws IOException {
230+
private void expectOK(BufferedReader in) throws IOException {
226231
String response = in.readLine();
227232
if (!response.startsWith("OK")) {
228233
throw new IOException("Expected OK but got this instead: " + response);
229234
}
235+
}
236+
237+
private String mayExpectOK(BufferedReader in) throws IOException {
238+
String response = in.readLine();
239+
if (response.startsWith("ERR")) {
240+
return null;
241+
} else if (!response.startsWith("OK")) {
242+
throw new IOException("Expected OK/ERR but got this instead: " + response);
243+
}
230244
return response.substring(Math.min(response.length(), 3));
231245
}
232246
}
@@ -265,7 +279,6 @@ public String signerName() {
265279
public void prepare() throws MojoFailureException {
266280
try {
267281
List<Loader> loaders = Stream.of(new GpgEnvLoader(), new GpgConfLoader(), new GpgAgentPasswordLoader())
268-
.filter(l -> this.isInteractive || !l.isInteractive())
269282
.collect(Collectors.toList());
270283

271284
byte[] keyRingMaterial = null;
@@ -327,7 +340,7 @@ public void prepare() throws MojoFailureException {
327340
final boolean keyPassNeeded = secretKey.getKeyEncryptionAlgorithm() != SymmetricKeyAlgorithmTags.NULL;
328341
if (keyPassNeeded && keyPassword == null) {
329342
for (Loader loader : loaders) {
330-
keyPassword = loader.loadPassword(session, secretKey.getKeyID());
343+
keyPassword = loader.loadPassword(session, secretKey.getFingerprint());
331344
if (keyPassword != null) {
332345
break;
333346
}

0 commit comments

Comments
 (0)