diff --git a/plugin/pom.xml b/plugin/pom.xml index f6dd5128..6cdbb7a8 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -172,6 +172,10 @@ org.wildfly.channel maven-resolver + + org.wildfly.channel + gpg-validator + org.wildfly.prospero prospero-metadata diff --git a/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java b/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java index 43523520..83ec52b0 100644 --- a/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java +++ b/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URL; import java.nio.file.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; @@ -377,6 +378,12 @@ public class DevMojo extends AbstractServerStartMojo { @Parameter(property = PropertyNames.CHANNELS) private List channels; + @Parameter(alias = "keyserver-urls") + protected List 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} + + org.wildfly.channel + gpg-validator + ${version.org.wildfly.channel} + org.wildfly.checkstyle wildfly-checkstyle-config