You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2024/03/04 14:50:42 UTC
(maven-gpg-plugin) branch master updated: [MGPG-106] Introduce new signer: BC (#72)
This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-gpg-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new 6f50819 [MGPG-106] Introduce new signer: BC (#72)
6f50819 is described below
commit 6f50819233cb368edba8335d7cb82edf2e7f2659
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Mon Mar 4 15:50:36 2024 +0100
[MGPG-106] Introduce new signer: BC (#72)
This introduces new pure Java signer BC backed one. Currently two signers supported: "gpg" (uses external executable, as before), and "bc" (uses pure Java Bouncy Castle backed signer).
All the ITs (invoker and surefire) are _reused_ and now run twice, once with "gpg" and once with "bc". One IT needed adjustment, as BC does not emit error about "pinentry".
---
https://issues.apache.org/jira/browse/MGPG-106
---
pgp-keys-map.list | 5 +
pom.xml | 105 ++++--
.../sign-release-without-passphrase/verify.groovy | 6 +-
.../apache/maven/plugins/gpg/AbstractGpgMojo.java | 149 +++++++--
.../maven/plugins/gpg/AbstractGpgSigner.java | 5 +
.../org/apache/maven/plugins/gpg/BcSigner.java | 371 +++++++++++++++++++++
.../maven/plugins/gpg/GpgSignAttachedMojo.java | 4 +-
.../org/apache/maven/plugins/gpg/GpgSigner.java | 6 +
.../maven/plugins/gpg/SignAndDeployFileMojo.java | 3 +
.../org/apache/maven/plugins/gpg/BcSignerTest.java | 66 ++++
...pgSignArtifactIT.java => BcSignArtifactIT.java} | 21 +-
.../maven/plugins/gpg/it/GpgSignArtifactIT.java | 17 +-
.../plugins/gpg/it/GpgSignAttachedMojoIT.java | 18 +-
.../apache/maven/plugins/gpg/it/ITSupport.java} | 27 +-
.../maven/plugins/gpg/it/InvokerTestUtils.java | 6 +-
src/test/resources/signing-key.asc | 32 ++
16 files changed, 725 insertions(+), 116 deletions(-)
diff --git a/pgp-keys-map.list b/pgp-keys-map.list
index f0a0edb..c507a97 100644
--- a/pgp-keys-map.list
+++ b/pgp-keys-map.list
@@ -15,8 +15,13 @@
# specific language governing permissions and limitations
# under the License.
+com.kohlschutter.junixsocket:junixsocket-common = 0xB5C082F1158B8C92AE3E5E1C29B8FEA02804261C
+com.kohlschutter.junixsocket:junixsocket-core = 0xB5C082F1158B8C92AE3E5E1C29B8FEA02804261C
+com.kohlschutter.junixsocket:junixsocket-native-common = 0xB5C082F1158B8C92AE3E5E1C29B8FEA02804261C
commons-io:commons-io = 0x2DB4F1EF0FA761ECC4EA935C86FDC7E2A11262CB
org.apiguardian:apiguardian-api = 0xFF6E2C001948C5F2F38B0CC385911F425EC61B51
+org.bouncycastle:bcpg-jdk18on = 0x7B121B76A7ED6CE6E60AD51784E913A8E3A748C0
+org.bouncycastle:bcprov-jdk18on = 0x7B121B76A7ED6CE6E60AD51784E913A8E3A748C0
org.junit.jupiter:junit-jupiter-api = 0xFF6E2C001948C5F2F38B0CC385911F425EC61B51
org.junit.jupiter:junit-jupiter-params = 0xFF6E2C001948C5F2F38B0CC385911F425EC61B51
org.junit.platform:junit-platform-commons = 0xFF6E2C001948C5F2F38B0CC385911F425EC61B51
diff --git a/pom.xml b/pom.xml
index a0e437d..c547723 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,9 +60,10 @@ under the License.
</distributionManagement>
<properties>
+ <javaVersion>8</javaVersion>
<mavenVersion>3.9.6</mavenVersion>
<resolverVersion>1.9.18</resolverVersion>
- <javaVersion>8</javaVersion>
+ <bouncycastleVersion>1.77</bouncycastleVersion>
<project.build.outputTimestamp>2023-05-03T01:33:44Z</project.build.outputTimestamp>
<resource.delimiter>@</resource.delimiter>
</properties>
@@ -120,6 +121,22 @@ under the License.
<artifactId>plexus-utils</artifactId>
<version>3.5.1</version>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpg-jdk18on</artifactId>
+ <version>${bouncycastleVersion}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk18on</artifactId>
+ <version>${bouncycastleVersion}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.kohlschutter.junixsocket</groupId>
+ <artifactId>junixsocket-core</artifactId>
+ <version>2.9.0</version>
+ <type>pom</type>
+ </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -179,8 +196,8 @@ under the License.
<artifactId>apache-rat-plugin</artifactId>
<configuration>
<excludes combine.children="append">
- <!-- rat check errors seen on ASF Jenkins instance, but not on local machine... -->
- <exclude>src/test/resources/gnupg/**</exclude>
+ <!-- rat is too cheeky, ignore these resources -->
+ <exclude>src/test/resources/**</exclude>
</excludes>
</configuration>
</plugin>
@@ -220,6 +237,67 @@ under the License.
<build>
<plugins>
<plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-invoker-plugin</artifactId>
+ <configuration>
+ <settingsFile>src/it/settings.xml</settingsFile>
+ <pomIncludes>
+ <pomInclude>*</pomInclude>
+ </pomIncludes>
+ <pomExcludes>
+ <!-- not supported with gpg 2.1+ -->
+ <pomExclude>alternative-secret-keyring</pomExclude>
+ </pomExcludes>
+ <goals>
+ <goal>clean</goal>
+ <goal>install</goal>
+ </goals>
+ <properties>
+ <gpg.homedir>${project.build.testOutputDirectory}/gnupg</gpg.homedir>
+ </properties>
+ </configuration>
+ <executions>
+ <execution>
+ <id>integration-test</id>
+ <phase>none</phase>
+ </execution>
+ <execution>
+ <id>integration-test-install</id>
+ <goals>
+ <goal>install</goal>
+ </goals>
+ <phase>integration-test</phase>
+ </execution>
+ <execution>
+ <id>gpg-integration-tests</id>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <phase>integration-test</phase>
+ <configuration>
+ <properties>
+ <gpg.signer>gpg</gpg.signer>
+ </properties>
+ </configuration>
+ </execution>
+ <execution>
+ <id>bc-integration-tests</id>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <phase>integration-test</phase>
+ <configuration>
+ <properties>
+ <gpg.signer>bc</gpg.signer>
+ <!-- must be absolute -->
+ <gpg.keyFilePath>${project.basedir}/src/test/resources/signing-key.asc</gpg.keyFilePath>
+ </properties>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <!-- this failsafe invocation depends on invoker:install above -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
@@ -243,27 +321,6 @@ under the License.
</execution>
</executions>
</plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-invoker-plugin</artifactId>
- <configuration>
- <settingsFile>src/it/settings.xml</settingsFile>
- <pomIncludes>
- <pomInclude>*</pomInclude>
- </pomIncludes>
- <pomExcludes>
- <!-- not supported with gpg 2.1+ -->
- <pomExclude>alternative-secret-keyring</pomExclude>
- </pomExcludes>
- <goals>
- <goal>clean</goal>
- <goal>install</goal>
- </goals>
- <properties>
- <gpg.homedir>${project.build.testOutputDirectory}/gnupg</gpg.homedir>
- </properties>
- </configuration>
- </plugin>
</plugins>
</build>
</profile>
diff --git a/src/it/sign-release-without-passphrase/verify.groovy b/src/it/sign-release-without-passphrase/verify.groovy
index eaeee90..5815a83 100644
--- a/src/it/sign-release-without-passphrase/verify.groovy
+++ b/src/it/sign-release-without-passphrase/verify.groovy
@@ -28,8 +28,8 @@ if (!logContent.contains("Total time: ") || !logContent.contains("Finished at: "
throw new Exception("Maven build did not fail, but timed out")
}
-// assert that the Maven build failed, because pinentry is not allowed in non-interactive mode
-if (!logContent.contains("[GNUPG:] FAILURE sign 67108949")) {
+// gpg: assert that the Maven build failed, because pinentry is not allowed in non-interactive mode
+// bc: assert that the Maven build failed, because key to sign is encrypted by no passphrase provided
+if (!logContent.contains("[GNUPG:] FAILURE sign 67108949") && !logContent.contains("Secret key is encrypted but no passphrase provided")) {
throw new Exception("Maven build did not fail in consequence of pinentry not being available to GPG")
}
-
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 e0bb093..691834d 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgMojo.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgMojo.java
@@ -32,10 +32,61 @@ import org.apache.maven.plugins.annotations.Parameter;
* @author Benjamin Bentmann
*/
public abstract class AbstractGpgMojo extends AbstractMojo {
+ public static final String DEFAULT_ENV_MAVEN_GPG_KEY = "MAVEN_GPG_KEY";
+ public static final String DEFAULT_ENV_MAVEN_GPG_FINGERPRINT = "MAVEN_GPG_KEY_FINGERPRINT";
public static final String DEFAULT_ENV_MAVEN_GPG_PASSPHRASE = "MAVEN_GPG_PASSPHRASE";
+ /**
+ * BC Signer only: The comma separate list of Unix Domain Socket paths, to use to communicate with GnuPG agent.
+ * If relative, they are resolved against user home directory.
+ *
+ * @since 3.2.0
+ */
+ @Parameter(property = "gpg.agentSocketLocations", defaultValue = ".gnupg/S.gpg-agent")
+ 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.
+ * <p>
+ * <em>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.</em>
+ *
+ * @since 3.2.0
+ */
+ @Parameter(property = "gpg.keyFilePath", defaultValue = "maven-signing-key.key")
+ private String keyFilePath;
+
+ /**
+ * BC Signer only: The fingerprint of the key to use for signing. If not given, first key in keyring will be used.
+ *
+ * @since 3.2.0
+ */
+ @Parameter(property = "gpg.keyFingerprint")
+ private String keyFingerprint;
+
+ /**
+ * BC Signer only: The env variable name where the GnuPG key is set. The default value is {@code MAVEN_GPG_KEY}.
+ * 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).
+ *
+ * @since 3.2.0
+ */
+ @Parameter(property = "gpg.keyEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_KEY)
+ private String keyEnvName;
+
+ /**
+ * 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}.
+ *
+ * @since 3.2.0
+ */
+ @Parameter(property = "gpg.keyFingerprintEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_FINGERPRINT)
+ 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.
*
* @since 3.2.0
*/
@@ -43,7 +94,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
private String passphraseEnvName;
/**
- * The directory from which gpg will load keyrings. If not specified, gpg will use the value configured for its
+ * GPG Signer only: The directory from which gpg will load keyrings. If not specified, gpg will use the value configured for its
* installation, e.g. <code>~/.gnupg</code> or <code>%APPDATA%/gnupg</code>.
*
* @since 1.0
@@ -53,7 +104,9 @@ 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.
+ * settings using server id at 'passphraseServerKey' configuration. <em>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).</em>
*
* @deprecated Do not use this configuration, plugin will fail if set.
**/
@@ -62,9 +115,11 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
private String passphrase;
/**
- * Server id to lookup the passphrase under Maven settings.
- * @since 1.6
+ * Server id to lookup the passphrase under Maven settings. <em>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).</em>
*
+ * @since 1.6
* @deprecated Do not use this configuration, plugin will fail if set.
**/
@Deprecated
@@ -72,27 +127,32 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
private String passphraseServerId;
/**
- * The "name" of the key to sign with. Passed to gpg as <code>--local-user</code>.
+ * GPG Signer only: The "name" of the key to sign with. Passed to gpg as <code>--local-user</code>.
*/
@Parameter(property = "gpg.keyname")
private String keyname;
/**
- * Passes <code>--use-agent</code> or <code>--no-use-agent</code> 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.
+ * GPG Signer only: Passes <code>--use-agent</code> or <code>--no-use-agent</code> 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
*/
+ @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;
/**
- * The path to the GnuPG executable to use for artifact signing. Defaults to either "gpg" or "gpg.exe" depending on
- * the operating system.
+ * 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.
*
* @since 1.1
*/
@@ -100,7 +160,7 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
private String executable;
/**
- * Whether to add the default keyrings from gpg's home directory to the list of used keyrings.
+ * GPG Signer only: Whether to add the default keyrings from gpg's home directory to the list of used keyrings.
*
* @since 1.2
*/
@@ -108,32 +168,42 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
private boolean defaultKeyring;
/**
- * <p>The path to a secret keyring to add to the list of keyrings. By default, only the {@code secring.gpg} from
- * gpg's home directory is considered. Use this option (in combination with {@link #publicKeyring} and
- * {@link #defaultKeyring} if required) to use a different secret key. <em>Note:</em> Relative paths are resolved
- * against gpg's home directory, not the project base directory.</p>
+ * GPG Signer only: The path to a secret keyring to add to the list of keyrings. By default, only the
+ * {@code secring.gpg} from gpg's home directory is considered. Use this option (in combination with
+ * {@link #publicKeyring} and {@link #defaultKeyring} if required) to use a different secret key.
+ * <em>Note:</em> Relative paths are resolved against gpg's home directory, not the project base directory.
+ * <p>
* <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All secret keys are stored in the
* ‘private-keys-v1.d’ directory below the GnuPG home directory.
*
* @since 1.2
+ * @deprecated
*/
+ @Deprecated
@Parameter(property = "gpg.secretKeyring")
private String secretKeyring;
/**
- * The path to a public keyring to add to the list of keyrings. By default, only the {@code pubring.gpg} from gpg's
- * home directory is considered. Use this option (and {@link #defaultKeyring} if required) to use a different public
- * key. <em>Note:</em> Relative paths are resolved against gpg's home directory, not the project base directory.
+ * GPG Signer only: The path to a public keyring to add to the list of keyrings. By default, only the
+ * {@code pubring.gpg} from gpg's home directory is considered. Use this option (and {@link #defaultKeyring}
+ * if required) to use a different public key. <em>Note:</em> Relative paths are resolved against gpg's home
+ * directory, not the project base directory.
+ * <p>
+ * <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All public keys are stored in the
+ * ‘pubring.kbx’ file below the GnuPG home directory.
*
* @since 1.2
+ * @deprecated
*/
+ @Deprecated
@Parameter(property = "gpg.publicKeyring")
private String publicKeyring;
/**
- * The lock mode to use when invoking gpg. By default no lock mode will be specified. Valid values are {@code once},
- * {@code multiple} and {@code never}. The lock mode gets translated into the corresponding {@code --lock-___}
- * command line argument. Improper usage of this option may lead to data and key corruption.
+ * GPG Signer only: The lock mode to use when invoking gpg. By default no lock mode will be specified. Valid
+ * values are {@code once}, {@code multiple} and {@code never}. The lock mode gets translated into the
+ * corresponding {@code --lock-___} command line argument. Improper usage of this option may lead to data and
+ * key corruption.
*
* @see <a href="http://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html">the
* --lock-options</a>
@@ -163,6 +233,15 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
@Parameter
private List<String> gpgArguments;
+ /**
+ * The name of the Signer implementation to use. Accepted values are {@code "gpg"} (the default, uses GnuPG
+ * executable) and {@code "bc"} (uses Bouncy Castle pure Java signer).
+ *
+ * @since 3.2.0
+ */
+ @Parameter(property = "gpg.signer", defaultValue = GpgSigner.NAME)
+ private String signer;
+
/**
* @since 3.0.0
*/
@@ -188,8 +267,21 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;
- protected AbstractGpgSigner newSigner() throws MojoExecutionException, MojoFailureException {
- AbstractGpgSigner signer = new GpgSigner(executable);
+ protected AbstractGpgSigner newSigner() throws MojoFailureException {
+ AbstractGpgSigner signer;
+ if (GpgSigner.NAME.equals(this.signer)) {
+ signer = new GpgSigner(executable);
+ } else if (BcSigner.NAME.equals(this.signer)) {
+ signer = new BcSigner(
+ session.getRepositorySession(),
+ keyEnvName,
+ keyFingerprintEnvName,
+ agentSocketLocations,
+ keyFilePath,
+ keyFingerprint);
+ } else {
+ throw new MojoFailureException("Unknown signer: " + this.signer);
+ }
signer.setLog(getLog());
signer.setInteractive(interactive);
@@ -208,13 +300,14 @@ public abstract class AbstractGpgMojo extends AbstractMojo {
signer.setPassPhrase(passphrase);
}
- signer.setPassPhrase(passphrase);
- if (null == passphrase && !useAgent) {
- if (!interactive) {
- throw new MojoFailureException("Cannot obtain passphrase in batch mode");
- }
+ // 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;
}
}
diff --git a/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgSigner.java b/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgSigner.java
index d94c3cc..daef26d 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgSigner.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/AbstractGpgSigner.java
@@ -22,6 +22,7 @@ import java.io.File;
import java.util.List;
import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
/**
@@ -121,6 +122,10 @@ public abstract class AbstractGpgSigner {
publicKeyring = path;
}
+ public abstract String signerName();
+
+ public void prepare() throws MojoFailureException {}
+
/**
* Create a detached signature file for the provided file.
*
diff --git a/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java b/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java
new file mode 100644
index 0000000..3ab8c22
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/gpg/BcSigner.java
@@ -0,0 +1,371 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.gpg;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.SocketException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.bcpg.BCPGOutputStream;
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureGenerator;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
+import org.bouncycastle.util.encoders.Hex;
+import org.codehaus.plexus.util.io.CachingOutputStream;
+import org.eclipse.aether.RepositorySystemSession;
+import org.newsclub.net.unix.AFUNIXSocket;
+import org.newsclub.net.unix.AFUNIXSocketAddress;
+
+/**
+ * A signer implementation that uses pure Java Bouncy Castle implementation to sign.
+ */
+@SuppressWarnings("checkstyle:magicnumber")
+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}.
+ */
+ default byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
+ return null;
+ }
+
+ /**
+ * Returns the key fingerprint, or {@code null}.
+ */
+ default byte[] loadKeyFingerprint(RepositorySystemSession session) throws IOException {
+ return null;
+ }
+
+ /**
+ * Returns the key password, or {@code null}.
+ */
+ default char[] loadPassword(RepositorySystemSession session, long keyId) 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);
+ if (keyMaterial != null) {
+ return keyMaterial.getBytes(StandardCharsets.UTF_8);
+ }
+ return null;
+ }
+
+ @Override
+ public byte[] loadKeyFingerprint(RepositorySystemSession session) {
+ String keyFingerprint = (String) session.getConfigProperties().get("env." + keyFingerprintEnvName);
+ if (keyFingerprint != null) {
+ if (keyFingerprint.trim().length() == 40) {
+ return Hex.decode(keyFingerprint);
+ } else {
+ throw new IllegalArgumentException(
+ "Key fingerprint configuration is wrong (hex encoded, 40 characters)");
+ }
+ }
+ return null;
+ }
+ }
+
+ public final class GpgConfLoader implements Loader {
+ /**
+ * Maximum key size, see <a href="https://wiki.gnupg.org/LargeKeys">Large Keys</a>.
+ */
+ 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);
+ }
+ if (Files.isRegularFile(keyPath)) {
+ if (Files.size(keyPath) < MAX_SIZE) {
+ return Files.readAllBytes(keyPath);
+ } else {
+ throw new IOException("Refusing to load key " + keyPath + "; is larger than 5KB");
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public byte[] loadKeyFingerprint(RepositorySystemSession session) {
+ if (keyFingerprint != null) {
+ if (keyFingerprint.trim().length() == 40) {
+ return Hex.decode(keyFingerprint);
+ } else {
+ throw new IllegalArgumentException(
+ "Key fingerprint configuration is wrong (hex encoded, 40 characters)");
+ }
+ }
+ return null;
+ }
+ }
+
+ public final class GpgAgentPasswordLoader implements Loader {
+ @Override
+ public boolean isInteractive() {
+ return true;
+ }
+
+ @Override
+ public char[] loadPassword(RepositorySystemSession session, long keyId) throws IOException {
+ List<String> 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();
+ } catch (SocketException e) {
+ // try next location
+ }
+ }
+ return null;
+ }
+
+ private String load(long keyId, Path socketPath) throws IOException {
+ try (AFUNIXSocket sock = AFUNIXSocket.newInstance()) {
+ sock.connect(AFUNIXSocketAddress.of(socketPath));
+ try (BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
+ OutputStream os = sock.getOutputStream()) {
+
+ expectOK(in);
+ String display = System.getenv("DISPLAY");
+ if (display != null) {
+ os.write(("OPTION display=" + display + "\n").getBytes());
+ os.flush();
+ expectOK(in);
+ }
+ String term = System.getenv("TERM");
+ if (term != null) {
+ os.write(("OPTION ttytype=" + term + "\n").getBytes());
+ os.flush();
+ expectOK(in);
+ }
+ String hexKeyId = Long.toHexString(keyId & 0xFFFFFFFFL);
+ // 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
+ + "+to+use+it+for+signing+Maven+Artifacts\n";
+ os.write((instruction).getBytes());
+ os.flush();
+ return new String(Hex.decode(expectOK(in).trim()));
+ }
+ }
+ }
+
+ private String expectOK(BufferedReader in) throws IOException {
+ String response = in.readLine();
+ if (!response.startsWith("OK")) {
+ throw new IOException("Expected OK but got this instead: " + response);
+ }
+ return response.substring(Math.min(response.length(), 3));
+ }
+ }
+
+ private final RepositorySystemSession session;
+ private final String keyEnvName;
+ private final String keyFingerprintEnvName;
+ private final String agentSocketLocations;
+ private final String keyFilePath;
+ private final String keyFingerprint;
+ private PGPSecretKey secretKey;
+ private PGPPrivateKey privateKey;
+ private PGPSignatureSubpacketVector hashSubPackets;
+
+ public BcSigner(
+ RepositorySystemSession session,
+ String keyEnvName,
+ String keyFingerprintEnvName,
+ String agentSocketLocations,
+ String keyFilePath,
+ String keyFingerprint) {
+ this.session = session;
+ this.keyEnvName = keyEnvName;
+ this.keyFingerprintEnvName = keyFingerprintEnvName;
+ this.agentSocketLocations = agentSocketLocations;
+ this.keyFilePath = keyFilePath;
+ this.keyFingerprint = keyFingerprint;
+ }
+
+ @Override
+ public String signerName() {
+ return NAME;
+ }
+
+ @Override
+ public void prepare() throws MojoFailureException {
+ try {
+ List<Loader> loaders = Stream.of(new GpgEnvLoader(), new GpgConfLoader(), new GpgAgentPasswordLoader())
+ .filter(l -> this.isInteractive || !l.isInteractive())
+ .collect(Collectors.toList());
+
+ byte[] keyRingMaterial = null;
+ for (Loader loader : loaders) {
+ keyRingMaterial = loader.loadKeyRingMaterial(session);
+ if (keyRingMaterial != null) {
+ break;
+ }
+ }
+ if (keyRingMaterial == null) {
+ throw new MojoFailureException("Key ring material not found");
+ }
+
+ byte[] fingerprint = null;
+ for (Loader loader : loaders) {
+ fingerprint = loader.loadKeyFingerprint(session);
+ if (fingerprint != null) {
+ break;
+ }
+ }
+
+ PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(
+ PGPUtil.getDecoderStream(new ByteArrayInputStream(keyRingMaterial)),
+ new BcKeyFingerprintCalculator());
+
+ PGPSecretKey secretKey = null;
+ for (PGPSecretKeyRing ring : pgpSecretKeyRingCollection) {
+ for (PGPSecretKey key : ring) {
+ if (!key.isPrivateKeyEmpty()) {
+ if (fingerprint == null || Arrays.equals(fingerprint, key.getFingerprint())) {
+ secretKey = key;
+ break;
+ }
+ }
+ }
+ }
+ if (secretKey == null) {
+ throw new MojoFailureException("Secret key not found");
+ }
+ if (secretKey.isPrivateKeyEmpty()) {
+ throw new MojoFailureException("Private key not found in Secret key");
+ }
+
+ long validSeconds = secretKey.getPublicKey().getValidSeconds();
+ if (validSeconds > 0) {
+ LocalDateTime expireDateTime = secretKey
+ .getPublicKey()
+ .getCreationTime()
+ .toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDateTime()
+ .plusSeconds(validSeconds);
+ if (LocalDateTime.now().isAfter(expireDateTime)) {
+ throw new MojoFailureException("Secret key expired at: " + expireDateTime);
+ }
+ }
+
+ char[] keyPassword = passphrase != null ? passphrase.toCharArray() : null;
+ final boolean keyPassNeeded = secretKey.getKeyEncryptionAlgorithm() != SymmetricKeyAlgorithmTags.NULL;
+ if (keyPassNeeded && keyPassword == null) {
+ for (Loader loader : loaders) {
+ keyPassword = loader.loadPassword(session, secretKey.getKeyID());
+ if (keyPassword != null) {
+ break;
+ }
+ }
+ if (keyPassword == null) {
+ throw new MojoFailureException("Secret key is encrypted but no passphrase provided");
+ }
+ }
+
+ this.secretKey = secretKey;
+ this.privateKey = secretKey.extractPrivateKey(
+ new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(keyPassword));
+ PGPSignatureSubpacketGenerator subPacketGenerator = new PGPSignatureSubpacketGenerator();
+ subPacketGenerator.setIssuerFingerprint(false, secretKey);
+ this.hashSubPackets = subPacketGenerator.generate();
+ } catch (PGPException | IOException e) {
+ throw new MojoFailureException(e);
+ }
+ }
+
+ @Override
+ protected void generateSignatureForFile(File file, File signature) throws MojoExecutionException {
+ try (InputStream in = Files.newInputStream(file.toPath());
+ OutputStream out = new CachingOutputStream(signature.toPath())) {
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(
+ new BcPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA512));
+ sGen.init(PGPSignature.BINARY_DOCUMENT, privateKey);
+ sGen.setHashedSubpackets(hashSubPackets);
+ int len;
+ byte[] buffer = new byte[8 * 1024];
+ while ((len = in.read(buffer)) >= 0) {
+ sGen.update(buffer, 0, len);
+ }
+ try (BCPGOutputStream bcpgOutputStream = new BCPGOutputStream(new ArmoredOutputStream(out))) {
+ sGen.generate().encode(bcpgOutputStream);
+ }
+ } catch (PGPException | IOException e) {
+ throw new MojoExecutionException(e);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/gpg/GpgSignAttachedMojo.java b/src/main/java/org/apache/maven/plugins/gpg/GpgSignAttachedMojo.java
index 2032b0b..55ae88a 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/GpgSignAttachedMojo.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/GpgSignAttachedMojo.java
@@ -87,8 +87,8 @@ public class GpgSignAttachedMojo extends AbstractGpgMojo {
signer.setBuildDirectory(new File(project.getBuild().getDirectory()));
signer.setBaseDirectory(project.getBasedir());
- getLog().info("Signing " + items.size() + " file" + ((items.size() > 1) ? "s" : "") + " with "
- + ((signer.keyname == null) ? "default" : signer.keyname) + " secret key.");
+ getLog().info("Signer '" + signer.signerName() + "' is signing " + items.size() + " file"
+ + ((items.size() > 1) ? "s" : ""));
for (FilesCollector.Item item : items) {
getLog().debug("Generating signature for " + item.getFile());
diff --git a/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java b/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java
index c0f7a96..deaff7b 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java
@@ -33,12 +33,18 @@ import org.codehaus.plexus.util.cli.DefaultConsumer;
* A signer implementation that uses the GnuPG command line executable.
*/
public class GpgSigner extends AbstractGpgSigner {
+ public static final String NAME = "gpg";
private String executable;
public GpgSigner(String executable) {
this.executable = executable;
}
+ @Override
+ public String signerName() {
+ return NAME;
+ }
+
/**
* {@inheritDoc}
*/
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 c754950..623e3e3 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/SignAndDeployFileMojo.java
@@ -353,6 +353,9 @@ public class SignAndDeployFileMojo extends AbstractGpgMojo {
signer.setOutputDirectory(ascDirectory);
signer.setBaseDirectory(new File("").getAbsoluteFile());
+ getLog().info("Signer '" + signer.signerName() + "' is signing " + artifacts.size() + " file"
+ + ((artifacts.size() > 1) ? "s" : ""));
+
ArrayList<Artifact> signatures = new ArrayList<>();
for (Artifact a : artifacts) {
signatures.add(new DefaultArtifact(
diff --git a/src/test/java/org/apache/maven/plugins/gpg/BcSignerTest.java b/src/test/java/org/apache/maven/plugins/gpg/BcSignerTest.java
new file mode 100644
index 0000000..67d360d
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/gpg/BcSignerTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.gpg;
+
+import java.io.File;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
+import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
+import org.eclipse.aether.repository.LocalRepository;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link BcSigner}.
+ */
+class BcSignerTest {
+
+ /**
+ * Test for BC agent use. Disabled, as this test cannot run on CI, only on computer that have gpg-agent.
+ * The goal of this test is to prepare BC signer, but to be able to prepare, it needs passphrase for the
+ * passphrase protected signing key (provided in src/test/resources/signing-key.asc). Passphrase is "TEST"
+ * (without quotes, all caps). If you want to execute this test, remove disabled annotation and run it from
+ * IDE (or whatever is your preferred way). On first run, Agent will pop a dialogue asking for password,
+ * and it will cache your response, so subsequent invocation will NOT ask for password.
+ * <p>
+ * IF you enter correct password ("TEST"), the test will pass (prepare will execute without any issue).
+ * IF you enter incorrect password, the test will fail with some message like:
+ * {@code org.apache.maven.plugin.MojoFailureException: org.bouncycastle.openpgp.PGPException: checksum mismatch at in checksum of 20 bytes}
+ * and this would cause plugin failure as well.
+ * <p>
+ * On Un*x, to make agent "forget" what you entered, use {@code gpg-connect-agent RELOADAGENT} command. To exit use
+ * Ctrl+D (EOF).
+ */
+ @Disabled
+ @Test
+ void testAgent() throws Exception {
+ DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
+ session.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer())
+ .newInstance(session, new LocalRepository("target/local-repo")));
+ BcSigner signer = new BcSigner(
+ session,
+ "unimportant",
+ "unimportant",
+ ".gnupg/S.gpg-agent",
+ new File("src/test/resources/signing-key.asc").getAbsolutePath(),
+ null);
+ signer.prepare();
+ }
+}
diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignArtifactIT.java b/src/test/java/org/apache/maven/plugins/gpg/it/BcSignArtifactIT.java
similarity index 77%
copy from src/test/java/org/apache/maven/plugins/gpg/it/GpgSignArtifactIT.java
copy to src/test/java/org/apache/maven/plugins/gpg/it/BcSignArtifactIT.java
index c9b4ab6..91aef65 100644
--- a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignArtifactIT.java
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/BcSignArtifactIT.java
@@ -26,23 +26,9 @@ import org.apache.maven.shared.invoker.InvocationRequest;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class GpgSignArtifactIT {
- private final File mavenHome;
- private final File localRepository;
- private final File mavenUserSettings;
- private final File gpgHome;
-
- public GpgSignArtifactIT() throws Exception {
- this.mavenHome = new File(System.getProperty("maven.home"));
- this.localRepository = new File(System.getProperty("localRepositoryPath"));
- this.mavenUserSettings = InvokerTestUtils.getTestResource("/it/settings.xml");
- this.gpgHome = new File(System.getProperty("gpg.homedir"));
- }
+import static org.junit.jupiter.api.Assertions.*;
+public class BcSignArtifactIT extends ITSupport {
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{
@@ -74,7 +60,8 @@ public class GpgSignArtifactIT {
throws Exception {
// given
final File pomFile = InvokerTestUtils.getTestResource(pomPath);
- final InvocationRequest request = InvokerTestUtils.createRequest(pomFile, mavenUserSettings, gpgHome, true);
+ final InvocationRequest request =
+ InvokerTestUtils.createRequest(pomFile, mavenUserSettings, gpgHome, "bc", true);
final File integrationTestRootDirectory = new File(pomFile.getParent());
final File expectedOutputDirectory = new File(integrationTestRootDirectory + expectedFileLocation);
diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignArtifactIT.java b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignArtifactIT.java
index c9b4ab6..69160ed 100644
--- a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignArtifactIT.java
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignArtifactIT.java
@@ -30,19 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-public class GpgSignArtifactIT {
- private final File mavenHome;
- private final File localRepository;
- private final File mavenUserSettings;
- private final File gpgHome;
-
- public GpgSignArtifactIT() throws Exception {
- this.mavenHome = new File(System.getProperty("maven.home"));
- this.localRepository = new File(System.getProperty("localRepositoryPath"));
- this.mavenUserSettings = InvokerTestUtils.getTestResource("/it/settings.xml");
- this.gpgHome = new File(System.getProperty("gpg.homedir"));
- }
-
+public class GpgSignArtifactIT extends ITSupport {
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{
@@ -74,7 +62,8 @@ public class GpgSignArtifactIT {
throws Exception {
// given
final File pomFile = InvokerTestUtils.getTestResource(pomPath);
- final InvocationRequest request = InvokerTestUtils.createRequest(pomFile, mavenUserSettings, gpgHome, true);
+ final InvocationRequest request =
+ InvokerTestUtils.createRequest(pomFile, mavenUserSettings, gpgHome, "gpg", true);
final File integrationTestRootDirectory = new File(pomFile.getParent());
final File expectedOutputDirectory = new File(integrationTestRootDirectory + expectedFileLocation);
diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java
index d75880d..c13e1c1 100644
--- a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java
@@ -28,26 +28,14 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-public class GpgSignAttachedMojoIT {
-
- private final File mavenHome;
- private final File localRepository;
- private final File mavenUserSettings;
- private final File gpgHome;
-
- public GpgSignAttachedMojoIT() throws Exception {
- this.mavenHome = new File(System.getProperty("maven.home"));
- this.localRepository = new File(System.getProperty("localRepositoryPath"));
- this.mavenUserSettings = InvokerTestUtils.getTestResource(System.getProperty("settingsFile"));
- this.gpgHome = new File(System.getProperty("gpg.homedir"));
- }
-
+public class GpgSignAttachedMojoIT extends ITSupport {
@Test
void testInteractiveWithoutPassphrase() throws Exception {
// given
final File pomFile =
InvokerTestUtils.getTestResource("/it/sign-release-without-passphrase-interactive/pom.xml");
- final InvocationRequest request = InvokerTestUtils.createRequest(pomFile, mavenUserSettings, gpgHome, false);
+ final InvocationRequest request =
+ InvokerTestUtils.createRequest(pomFile, mavenUserSettings, gpgHome, "gpg", false);
// require Maven interactive mode
request.setBatchMode(false);
diff --git a/src/it/sign-release-without-passphrase/verify.groovy b/src/test/java/org/apache/maven/plugins/gpg/it/ITSupport.java
similarity index 55%
copy from src/it/sign-release-without-passphrase/verify.groovy
copy to src/test/java/org/apache/maven/plugins/gpg/it/ITSupport.java
index eaeee90..4741266 100644
--- a/src/it/sign-release-without-passphrase/verify.groovy
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/ITSupport.java
@@ -16,20 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
+package org.apache.maven.plugins.gpg.it;
+import java.io.File;
-import org.codehaus.plexus.util.FileUtils
+import org.junit.jupiter.api.BeforeEach;
-var buildLog = new File(basedir, "build.log")
-var logContent = FileUtils.fileRead(buildLog)
+public abstract class ITSupport {
+ protected File mavenHome;
+ protected File localRepository;
+ protected File mavenUserSettings;
+ protected File gpgHome;
-// assert that the Maven build properly failed and did not time out
-if (!logContent.contains("Total time: ") || !logContent.contains("Finished at: ")) {
- throw new Exception("Maven build did not fail, but timed out")
+ @BeforeEach
+ public void prepare() throws Exception {
+ this.mavenHome = new File(System.getProperty("maven.home"));
+ this.localRepository = new File(System.getProperty("localRepositoryPath"));
+ this.mavenUserSettings = InvokerTestUtils.getTestResource(System.getProperty("settingsFile"));
+ this.gpgHome = new File(System.getProperty("gpg.homedir"));
+ }
}
-
-// assert that the Maven build failed, because pinentry is not allowed in non-interactive mode
-if (!logContent.contains("[GNUPG:] FAILURE sign 67108949")) {
- throw new Exception("Maven build did not fail in consequence of pinentry not being available to GPG")
-}
-
diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java b/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java
index 2c642e9..89bbeea 100644
--- a/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java
@@ -41,7 +41,7 @@ import org.apache.maven.shared.invoker.PrintStreamLogger;
public class InvokerTestUtils {
public static InvocationRequest createRequest(
- File pomFile, File mavenUserSettings, File gpgHome, boolean providePassphraseEnv) {
+ File pomFile, File mavenUserSettings, File gpgHome, String signer, boolean providePassphraseEnv) {
final InvocationRequest request = new DefaultInvocationRequest();
request.setUserSettingsFile(mavenUserSettings);
request.setShowVersion(true);
@@ -64,6 +64,10 @@ public class InvokerTestUtils {
properties.setProperty("https.protocols", httpsProtocols);
}
+ if (signer != null) {
+ properties.setProperty("gpg.signer", signer);
+ properties.setProperty("gpg.keyFilePath", new File("src/test/resources/signing-key.asc").getAbsolutePath());
+ }
properties.setProperty("gpg.homedir", gpgHome.getAbsolutePath());
return request;
diff --git a/src/test/resources/signing-key.asc b/src/test/resources/signing-key.asc
new file mode 100644
index 0000000..9f9bcbd
--- /dev/null
+++ b/src/test/resources/signing-key.asc
@@ -0,0 +1,32 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lQHpBEqeoMkRBADgRe1HMAJ1LdxnVgfXijYTtlSBe7gYo5SqYvzvi26xUYsgOF96
+N4xXnf+aNKL4DZ+onPqpR1SdNu4Dc78vZGrSzyAjrpLuxODIr5r/xCmOzRrzM8nU
+GkP8KVSvcK9V5dHrwZZH+N340BptiMwTJIrvuUOArzHEOc00Bh0pXowSmwCgwN7w
+u/uvC2EGpTghHV+ht1VM53cEALmXyodsYQsIdSFPh1slK14Gwo8sipag7rhypek3
+OPXyW1UfysiVXVh0deWbbiFSx/i2yWq1AumTPPfTRyRGWAIFAeRV6Vniil4ufq1a
+Un2yeYpGo/cVT+DyQYP1OW6/+/mAjbbEtZauQeUEjVa3j4SqyR10sS4yebJ+ctPQ
+Bbe/A/9v5bTjl/u9TbiImbM34K19tBIe3jTQt/V6Tj1mZ8P/ssVa7BEgapp3/BY6
+4V48SkhxumEjHcy1wc3njLMb8Xbv9js2O6bpG84CBP3IN9iZOz7ANhxBuwG5LJN2
+0bEcQsE3gCNLx8Sz2A4Z/8sZfJzq5NdGmiX2X0L3EfF+pTMlKv4HAwKMaiQPAe58
+Xf97gsMrVrd6ruUzLYyUFNXwino8MD9uQS/URJ+hVGAM6WvD3WKMKaHGYt8DgeTM
+bJZASGOYMP2j4b3JtDVNYXZlbiBHUEcgUGx1Z2luIChURVNUSU5HIEtFWSkgPGRl
+dkBtYXZlbi5hcGFjaGUub3JnPohgBBMRAgAgBQJKnqDJAhsDBgsJCAcDAgQVAggD
+BBYCAwECHgECF4AACgkQg8qoh2UlSiYJpACeOfzjOBNIzmbfdDHqA9iY33BwgxQA
+n1U07P548XAkochaezFB2Cr/ZeGTnQJrBEqeoMkQCACKRwro2fnPATtzEcpsvt0j
+XMF7WgQcYFUYed5DZ6ra7bVp5yOyZmDswHlhSBXjYWgLBps5FEzawly5mbIc7Wh5
++izUtD8LiyUWcRiCAvoBOF9J0HKXHhnZxkM0XK6BrSRlARr7HsEpTOosFUPoHsMX
+xixwd2WzmV6U3A+4cOnjrnZD5YUJhKU/5JwuP+ZRZpyiyR1Q2qdQdaODy5zvM84q
+i2GuORGdYWs5hlkV4ur8ihu7zErQD9sBXPhlzFqRUM/2L57z2wUgDLoNgFzCQSZp
+1Rtb0VZvIjhXZ0vquJ8UKNWVpuhzlj7qOdNFHwEA6pRUy/fquk3f1vgo1x5SZv0X
+AAMFB/9u0u+BCOJH00762Tr1hClzuzPkQQpaWAmSqbDfj7Lb/0KKlBfOorby1Gdl
+Lz1XqsbkwzboDob7hbuAI5YV6iBYEVW8wYI49h+jA1cPFDj2kb1tT4RQrpvfP7GA
+n1/GdKHDGupTr2ZoKgU6iEb86ru5gcGbPPItx7rlPDMPGEbCvFtuPImNoscQMh1F
+ONHt+8YPJBTYVJFrsFag+p5JXsZWReBsnER5znhHoH8PTgZAGhdMWnKshqESFrNL
+rgz9JpGguJ1TL+suTG9CPubCnvXpsMGXTMURYSomwfQD4lXhiwx3DTBL1LXpFbTq
+iNBb/jaiJ5sTaGuwTp2+tppPiJ4//gcDAhi2Ufij/zA1/0UivR37hD8OQD4kIJDF
+TnFd6hYdJcRJdJAV8hI7ai4f6My7ihC29WoXBrCN2Uaizno2KCBt7m4hUSzLWhSm
+audxW4oPzGJwMu9Ux7+m4GCfVYhJBBgRAgAJBQJKnqDJAhsMAAoJEIPKqIdlJUom
+IxcAnjPrJT6I0XPlfKuz23x6KFinX4XtAJ40KkIfgDWI2Smg85Fc24E7QRqReg==
+=1WyT
+-----END PGP PRIVATE KEY BLOCK-----
\ No newline at end of file