Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WFMP-275] Add GPG checks to the channel provisioning #569

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@
<groupId>org.wildfly.channel</groupId>
<artifactId>maven-resolver</artifactId>
</dependency>
<dependency>
<groupId>org.wildfly.channel</groupId>
<artifactId>gpg-validator</artifactId>
</dependency>
<dependency>
<groupId>org.wildfly.prospero</groupId>
<artifactId>prospero-metadata</artifactId>
Expand Down
9 changes: 9 additions & 0 deletions plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -377,6 +378,12 @@ public class DevMojo extends AbstractServerStartMojo {
@Parameter(property = PropertyNames.CHANNELS)
private List<ChannelConfiguration> channels;

@Parameter(alias = "keyserver-urls")
protected List<URL> keyserverUrls = Collections.emptyList();

@Parameter(alias = "trusted-keyring")
protected Path trustedKeyring;

/**
* Specifies the name used for the deployment.
* <p>
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -223,6 +224,12 @@ abstract class AbstractProvisionServerMojo extends AbstractMojo {
@Parameter(alias = "dry-run")
boolean dryRun;

@Parameter(alias = "keyserver-urls")
protected List<URL> keyserverUrls = Collections.emptyList();

@Parameter(alias = "trusted-keyring")
protected File trustedKeyring;

private Path wildflyDir;

protected MavenRepoManager artifactResolver;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import org.apache.maven.plugin.MojoExecutionException;
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 {
Expand All @@ -30,6 +31,10 @@ public class ChannelConfiguration {
private boolean multipleManifest;
private String name;

private boolean gpgCheck;

private List<URL> gpgUrls;

/**
* @return the manifest
*/
Expand Down Expand Up @@ -110,10 +115,18 @@ private void validate() throws MojoExecutionException {

public Channel toChannel(List<RemoteRepository> repositories) throws MojoExecutionException {
validate();
List<Repository> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -63,18 +71,24 @@ public class ChannelMavenArtifactRepositoryManager implements MavenRepoManager,
private final RepositorySystem system;
private final DefaultRepositorySystemSession session;
private final List<RemoteRepository> repositories;
private final Set<String> artifactSources = new HashSet<>();
private final Path gpgKeyring;

public ChannelMavenArtifactRepositoryManager(List<ChannelConfiguration> channels,
RepositorySystem system,
RepositorySystemSession contextSession,
List<RemoteRepository> repositories, Log log, boolean offline)
List<RemoteRepository> repositories,
List<URL> keystoreUrls,
Path gpgKeyring,
Log log, boolean offline)
throws MalformedURLException, UnresolvedMavenArtifactException, MojoExecutionException {
if (channels.isEmpty()) {
throw new MojoExecutionException("No channel specified.");
}
this.log = log;
session = MavenRepositorySystemUtils.newSession();
this.repositories = repositories;
this.gpgKeyring = gpgKeyring;
session.setLocalRepositoryManager(contextSession.getLocalRepositoryManager());
session.setOffline(offline);
Map<String, RemoteRepository> mapping = new HashMap<>();
Expand All @@ -91,7 +105,20 @@ public ChannelMavenArtifactRepositoryManager(List<ChannelConfiguration> 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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, PGPPublicKey> keyCache = new HashMap<>();

public PGPPublicKey get(String keyID) {
if (publicKeyRingCollection != null) {
final Iterator<PGPPublicKeyRing> 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<PGPPublicKey> 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<String> userIDs = pgpPublicKey.getUserIDs();
while (userIDs.hasNext()) {
sb.append(userIDs.next());
}
sb.append(": ").append(Hex.toHexString(pgpPublicKey.getFingerprint()));
return sb.toString();
}
}
9 changes: 7 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@
<!-- This version property is also retrieved by plugin at runtime to resolve CLI artifact -->
<version.org.wildfly.core>25.0.2.Final</version.org.wildfly.core>
<version.org.wildfly>32.0.1.Final</version.org.wildfly>
<version.org.wildfly.channel>1.1.0.Final</version.org.wildfly.channel>
<version.org.wildfly.prospero>1.2.1.Final</version.org.wildfly.prospero>
<version.org.wildfly.channel>1.1.1.Final-SNAPSHOT</version.org.wildfly.channel>
<version.org.wildfly.prospero>1.2.2.Final-SNAPSHOT</version.org.wildfly.prospero>
<!-- maven dependencies -->
<version.javax.inject.javax.inject>1</version.javax.inject.javax.inject>
<version.org.apache.maven.maven-core>3.9.4</version.org.apache.maven.maven-core>
Expand Down Expand Up @@ -394,6 +394,11 @@
<artifactId>maven-resolver</artifactId>
<version>${version.org.wildfly.channel}</version>
</dependency>
<dependency>
<groupId>org.wildfly.channel</groupId>
<artifactId>gpg-validator</artifactId>
<version>${version.org.wildfly.channel}</version>
</dependency>
<dependency>
<groupId>org.wildfly.checkstyle</groupId>
<artifactId>wildfly-checkstyle-config</artifactId>
Expand Down
Loading