keyserverUrls = Collections.emptyList();
+
+ @Parameter(alias = "trusted-keyring")
+ protected Path trustedKeyring;
+
/**
* Specifies the name used for the deployment.
*
@@ -520,6 +527,8 @@ protected MavenRepoManager createMavenRepoManager() throws MojoExecutionExceptio
try {
return new ChannelMavenArtifactRepositoryManager(channels,
repoSystem, session, repositories,
+ keyserverUrls,
+ trustedKeyring,
getLog(), offlineProvisioning);
} catch (MalformedURLException | UnresolvedMavenArtifactException ex) {
throw new MojoExecutionException(ex.getLocalizedMessage(), ex);
diff --git a/plugin/src/main/java/org/wildfly/plugin/provision/AbstractProvisionServerMojo.java b/plugin/src/main/java/org/wildfly/plugin/provision/AbstractProvisionServerMojo.java
index fbc02041..df940304 100644
--- a/plugin/src/main/java/org/wildfly/plugin/provision/AbstractProvisionServerMojo.java
+++ b/plugin/src/main/java/org/wildfly/plugin/provision/AbstractProvisionServerMojo.java
@@ -10,6 +10,7 @@
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
+import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -223,6 +224,12 @@ abstract class AbstractProvisionServerMojo extends AbstractMojo {
@Parameter(alias = "dry-run")
boolean dryRun;
+ @Parameter(alias = "keyserver-urls")
+ protected List keyserverUrls = Collections.emptyList();
+
+ @Parameter(alias = "trusted-keyring")
+ protected File trustedKeyring;
+
private Path wildflyDir;
protected MavenRepoManager artifactResolver;
@@ -251,6 +258,8 @@ public void execute() throws MojoExecutionException, MojoFailureException {
try {
artifactResolver = new ChannelMavenArtifactRepositoryManager(channels,
repoSystem, repoSession, repositories,
+ keyserverUrls,
+ trustedKeyring == null ? null : trustedKeyring.toPath(),
getLog(), offlineProvisioning);
} catch (MalformedURLException | UnresolvedMavenArtifactException ex) {
throw new MojoExecutionException(ex.getLocalizedMessage(), ex);
diff --git a/plugin/src/main/java/org/wildfly/plugin/provision/ChannelConfiguration.java b/plugin/src/main/java/org/wildfly/plugin/provision/ChannelConfiguration.java
index 1a4273c1..9e1b88cf 100644
--- a/plugin/src/main/java/org/wildfly/plugin/provision/ChannelConfiguration.java
+++ b/plugin/src/main/java/org/wildfly/plugin/provision/ChannelConfiguration.java
@@ -6,7 +6,6 @@
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
@@ -14,12 +13,14 @@
import org.eclipse.aether.repository.RemoteRepository;
import org.wildfly.channel.Channel;
import org.wildfly.channel.ChannelManifestCoordinate;
-import org.wildfly.channel.Repository;
/**
* A channel configuration. Contains a {@code manifest} composed of a {@code groupId}, an {@code artifactId}
* an optional {@code version} or a {@code url}.
*
+ * Optionally can declare if the channel requires GPG signature validation ({@code gpgCheck}) and a list of GPG public
+ * keys used to verify them ({@code gpgUrls}).
+ *
* @author jdenise
*/
public class ChannelConfiguration {
@@ -30,6 +31,10 @@ public class ChannelConfiguration {
private boolean multipleManifest;
private String name;
+ private boolean gpgCheck;
+
+ private List gpgUrls;
+
/**
* @return the manifest
*/
@@ -110,10 +115,18 @@ private void validate() throws MojoExecutionException {
public Channel toChannel(List repositories) throws MojoExecutionException {
validate();
- List repos = new ArrayList<>();
+ final Channel.Builder builder = new Channel.Builder()
+ .setManifestCoordinate(getManifest())
+ .setGpgCheck(gpgCheck);
+
+ if (gpgUrls != null) {
+ gpgUrls.stream().map(URL::toExternalForm).forEach(builder::addGpgUrl);
+ }
+
for (RemoteRepository r : repositories) {
- repos.add(new Repository(r.getId(), r.getUrl()));
+ builder.addRepository(r.getId(), r.getUrl());
}
- return new Channel(name, null, null, repos, getManifest(), null, null);
+
+ return builder.build();
}
}
diff --git a/plugin/src/main/java/org/wildfly/plugin/provision/ChannelMavenArtifactRepositoryManager.java b/plugin/src/main/java/org/wildfly/plugin/provision/ChannelMavenArtifactRepositoryManager.java
index f40b00e7..0ccf8148 100644
--- a/plugin/src/main/java/org/wildfly/plugin/provision/ChannelMavenArtifactRepositoryManager.java
+++ b/plugin/src/main/java/org/wildfly/plugin/provision/ChannelMavenArtifactRepositoryManager.java
@@ -9,20 +9,24 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.net.MalformedURLException;
+import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.bouncycastle.openpgp.PGPPublicKey;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
@@ -46,7 +50,11 @@
import org.wildfly.channel.Repository;
import org.wildfly.channel.UnresolvedMavenArtifactException;
import org.wildfly.channel.VersionResult;
+import org.wildfly.channel.gpg.GpgSignatureValidator;
+import org.wildfly.channel.gpg.GpgSignatureValidatorListener;
+import org.wildfly.channel.gpg.Keyserver;
import org.wildfly.channel.maven.VersionResolverFactory;
+import org.wildfly.channel.spi.ArtifactIdentifier;
import org.wildfly.channel.spi.ChannelResolvable;
import org.wildfly.prospero.metadata.ManifestVersionRecord;
import org.wildfly.prospero.metadata.ManifestVersionResolver;
@@ -63,11 +71,16 @@ public class ChannelMavenArtifactRepositoryManager implements MavenRepoManager,
private final RepositorySystem system;
private final DefaultRepositorySystemSession session;
private final List repositories;
+ private final Set artifactSources = new HashSet<>();
+ private final Path gpgKeyring;
public ChannelMavenArtifactRepositoryManager(List channels,
RepositorySystem system,
RepositorySystemSession contextSession,
- List repositories, Log log, boolean offline)
+ List repositories,
+ List keystoreUrls,
+ Path gpgKeyring,
+ Log log, boolean offline)
throws MalformedURLException, UnresolvedMavenArtifactException, MojoExecutionException {
if (channels.isEmpty()) {
throw new MojoExecutionException("No channel specified.");
@@ -75,6 +88,7 @@ public ChannelMavenArtifactRepositoryManager(List channels
this.log = log;
session = MavenRepositorySystemUtils.newSession();
this.repositories = repositories;
+ this.gpgKeyring = gpgKeyring;
session.setLocalRepositoryManager(contextSession.getLocalRepositoryManager());
session.setOffline(offline);
Map mapping = new HashMap<>();
@@ -91,7 +105,20 @@ public ChannelMavenArtifactRepositoryManager(List channels
}
return rep;
};
- VersionResolverFactory factory = new VersionResolverFactory(system, session, mapper);
+ final GpgSignatureValidator signatureValidator = new GpgSignatureValidator(new GpgKeyring(gpgKeyring),
+ new Keyserver(keystoreUrls));
+ VersionResolverFactory factory = new VersionResolverFactory(system, session, signatureValidator, mapper);
+ signatureValidator.addListener(new GpgSignatureValidatorListener() {
+ @Override
+ public void artifactSignatureCorrect(ArtifactIdentifier artifact, PGPPublicKey publicKey) {
+ artifactSources.add(GpgKeyring.describeImportedKeys(publicKey));
+ }
+
+ @Override
+ public void artifactSignatureInvalid(ArtifactIdentifier artifact, PGPPublicKey publicKey) {
+
+ }
+ });
channelSession = new ChannelSession(this.channels, factory);
localCachePath = contextSession.getLocalRepositoryManager().getRepository().getBasedir().toPath();
this.system = system;
@@ -186,9 +213,17 @@ private void resolveFromChannels(MavenArtifact artifact) throws UnresolvedMavenA
public void done(Path home) throws MavenUniverseException, IOException {
ChannelManifest channelManifest = channelSession.getRecordedChannel();
- final ManifestVersionRecord currentVersions = new ManifestVersionResolver(localCachePath, system)
+ final ManifestVersionRecord currentVersions = new ManifestVersionResolver(localCachePath, system,
+ new GpgSignatureValidator(new GpgKeyring(gpgKeyring)))
.getCurrentVersions(channels);
ProsperoMetadataUtils.generate(home, channels, channelManifest, currentVersions);
+
+ if (!this.artifactSources.isEmpty()) {
+ log.info("Resolved artifacts were signed by:");
+ for (String artifactSource : this.artifactSources) {
+ log.info(" * " + artifactSource);
+ }
+ }
}
@Override
diff --git a/plugin/src/main/java/org/wildfly/plugin/provision/GpgKeyring.java b/plugin/src/main/java/org/wildfly/plugin/provision/GpgKeyring.java
new file mode 100644
index 00000000..b6458a88
--- /dev/null
+++ b/plugin/src/main/java/org/wildfly/plugin/provision/GpgKeyring.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.wildfly.plugin.provision;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.bouncycastle.util.encoders.Hex;
+import org.jboss.logging.Logger;
+import org.wildfly.channel.gpg.GpgKeystore;
+
+/**
+ * Read-only keystore used to read keys from a local GPG keyring file.
+ */
+public class GpgKeyring implements GpgKeystore {
+
+ private final Logger log = Logger.getLogger(GpgKeyring.class.getName());
+
+ private final PGPPublicKeyRingCollection publicKeyRingCollection;
+ private Map keyCache = new HashMap<>();
+
+ public PGPPublicKey get(String keyID) {
+ if (publicKeyRingCollection != null) {
+ final Iterator keyRings = publicKeyRingCollection.getKeyRings();
+ while (keyRings.hasNext()) {
+ final PGPPublicKeyRing keyRing = keyRings.next();
+ final PGPPublicKey publicKey = keyRing.getPublicKey(new BigInteger(keyID, 16).longValue());
+ if (publicKey != null) {
+ return publicKey;
+ }
+ }
+ return null;
+ } else {
+ return keyCache.get(keyID);
+ }
+ }
+
+ public GpgKeyring(Path keyringPath) {
+ if (keyringPath != null) {
+ try {
+ publicKeyRingCollection = new PGPPublicKeyRingCollection(
+ new ArmoredInputStream(new FileInputStream(keyringPath.toFile())),
+ new JcaKeyFingerprintCalculator());
+ } catch (IOException | PGPException e) {
+ throw new RuntimeException("Unable to access GPG keystore", e);
+ }
+ } else {
+ publicKeyRingCollection = null;
+ }
+ }
+
+ public boolean add(List publicKeys) {
+ for (PGPPublicKey publicKey : publicKeys) {
+ keyCache.put(Long.toHexString(publicKey.getKeyID()).toUpperCase(Locale.ROOT), publicKey);
+ }
+ return true;
+ }
+
+ static String describeImportedKeys(PGPPublicKey pgpPublicKey) {
+ final StringBuilder sb = new StringBuilder();
+ final Iterator userIDs = pgpPublicKey.getUserIDs();
+ while (userIDs.hasNext()) {
+ sb.append(userIDs.next());
+ }
+ sb.append(": ").append(Hex.toHexString(pgpPublicKey.getFingerprint()));
+ return sb.toString();
+ }
+}
diff --git a/pom.xml b/pom.xml
index f3eafce4..b2c35ee7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -79,8 +79,8 @@
25.0.2.Final
32.0.1.Final
- 1.1.0.Final
- 1.2.1.Final
+ 1.1.1.Final-SNAPSHOT
+ 1.2.2.Final-SNAPSHOT
1
3.9.4
@@ -394,6 +394,11 @@
maven-resolver
${version.org.wildfly.channel}