From 1efd15f83e0966a60779b6e7c38f69724d9d3172 Mon Sep 17 00:00:00 2001 From: Jeff Mesnil Date: Mon, 6 Jan 2025 14:42:18 +0100 Subject: [PATCH] Revert "Merge pull request #268 from spyrkob/mvn_verify" This reverts commit 52c9d150e3dd571401f6c2f4dc118986762704e2, reversing changes made to 63a1a95146f2da9338af19c2de6a2bd777d0e72c. --- .../java/org/wildfly/channel/Channel.java | 53 +-- .../java/org/wildfly/channel/ChannelImpl.java | 15 +- .../channel/ChannelManifestCoordinate.java | 21 +- .../org/wildfly/channel/ChannelMapper.java | 5 +- .../channel/ChannelMetadataCoordinate.java | 11 - .../org/wildfly/channel/ChannelSession.java | 105 +---- .../InvalidChannelMetadataException.java | 5 - .../channel/SignedVersionResolverWrapper.java | 222 ----------- .../channel/spi/ArtifactIdentifier.java | 79 ---- .../channel/spi/MavenVersionsResolver.java | 7 +- .../wildfly/channel/spi/SignatureResult.java | 85 ----- .../channel/spi/SignatureValidator.java | 77 ---- .../org/wildfly/channel/v2.1.0/schema.json | 166 -------- .../channel/ChannelMapperTestCase.java | 44 +-- .../channel/ChannelSessionTestCase.java | 115 +----- .../channel/ChannelWithBlocklistTestCase.java | 16 +- .../ChannelWithRequirementsTestCase.java | 5 +- .../SignedVersionResolverWrapperTest.java | 186 --------- .../channel/mapping/ChannelTestCase.java | 11 - .../channels/channel-with-gpg-check.yaml | 18 - doc/spec.adoc | 33 +- gpg-validator/pom.xml | 56 --- .../org/wildfly/channel/gpg/GpgKeystore.java | 47 --- .../channel/gpg/GpgSignatureValidator.java | 360 ------------------ .../gpg/GpgSignatureValidatorListener.java | 42 -- .../org/wildfly/channel/gpg/Keyserver.java | 107 ------ .../gpg/KeystoreOperationException.java | 34 -- .../gpg/GpgSignatureValidatorTest.java | 347 ----------------- .../channel/maven/VersionResolverFactory.java | 34 +- .../maven/VersionResolverFactoryTest.java | 26 +- pom.xml | 25 -- 31 files changed, 76 insertions(+), 2281 deletions(-) delete mode 100644 core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java delete mode 100644 core/src/main/java/org/wildfly/channel/spi/ArtifactIdentifier.java delete mode 100644 core/src/main/java/org/wildfly/channel/spi/SignatureResult.java delete mode 100644 core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java delete mode 100644 core/src/main/resources/org/wildfly/channel/v2.1.0/schema.json delete mode 100644 core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java delete mode 100644 core/src/test/resources/channels/channel-with-gpg-check.yaml delete mode 100644 gpg-validator/pom.xml delete mode 100644 gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgKeystore.java delete mode 100644 gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidator.java delete mode 100644 gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidatorListener.java delete mode 100644 gpg-validator/src/main/java/org/wildfly/channel/gpg/Keyserver.java delete mode 100644 gpg-validator/src/main/java/org/wildfly/channel/gpg/KeystoreOperationException.java delete mode 100644 gpg-validator/src/test/java/org/wildfly/channel/gpg/GpgSignatureValidatorTest.java diff --git a/core/src/main/java/org/wildfly/channel/Channel.java b/core/src/main/java/org/wildfly/channel/Channel.java index 0f022638..e3861c33 100644 --- a/core/src/main/java/org/wildfly/channel/Channel.java +++ b/core/src/main/java/org/wildfly/channel/Channel.java @@ -47,8 +47,6 @@ public class Channel { private BlocklistCoordinate blocklistCoordinate; private ChannelManifestCoordinate manifestCoordinate; private NoStreamStrategy noStreamStrategy = NoStreamStrategy.NONE; - private Boolean gpgCheck; - private List gpgUrls; // no-arg constructor for maven plugins public Channel() { @@ -58,7 +56,7 @@ public Channel() { /** * Representation of a Channel resource using the current schema version. * - * @see #Channel(String, String, String, Vendor, List, ChannelManifestCoordinate, BlocklistCoordinate, NoStreamStrategy, Boolean, String) + * @see #Channel(String, String, String, Vendor, List, ChannelManifestCoordinate, BlocklistCoordinate, NoStreamStrategy) */ public Channel(String name, String description, @@ -74,8 +72,7 @@ public Channel(String name, repositories, manifestCoordinate, blocklistCoordinate, - noStreamStrategy, - null, null); + noStreamStrategy); } @JsonCreator @@ -87,9 +84,7 @@ public Channel(@JsonProperty(value = "schemaVersion", required = true) String sc @JsonInclude(NON_EMPTY) List repositories, @JsonProperty(value = "manifest") ChannelManifestCoordinate manifestCoordinate, @JsonProperty(value = "blocklist") @JsonInclude(NON_EMPTY) BlocklistCoordinate blocklistCoordinate, - @JsonProperty(value = "resolve-if-no-stream") NoStreamStrategy noStreamStrategy, - @JsonProperty(value = "gpg-check") Boolean gpgCheck, - @JsonProperty(value = "gpg-urls") List gpgUrls) { + @JsonProperty(value = "resolve-if-no-stream") NoStreamStrategy noStreamStrategy) { this.schemaVersion = schemaVersion; this.name = name; this.description = description; @@ -98,8 +93,6 @@ public Channel(@JsonProperty(value = "schemaVersion", required = true) String sc this.blocklistCoordinate = blocklistCoordinate; this.manifestCoordinate = manifestCoordinate; this.noStreamStrategy = (noStreamStrategy != null) ? noStreamStrategy: NoStreamStrategy.NONE; - this.gpgCheck = gpgCheck; - this.gpgUrls = (gpgUrls != null) ? gpgUrls : emptyList(); } public String getSchemaVersion() { @@ -144,25 +137,6 @@ public NoStreamStrategy getNoStreamStrategy() { return noStreamStrategy; } - // using a private method to return a Boolean for serializing - // this way channels without gpg-check field can be read/written without modifications - @JsonInclude(JsonInclude.Include.NON_NULL) - @JsonProperty("gpg-check") - private Boolean _isGpgCheck() { - return gpgCheck; - } - - @JsonIgnore - public boolean isGpgCheck() { - return gpgCheck!=null?gpgCheck:false; - } - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - @JsonProperty("gpg-urls") - public List getGpgUrls() { - return gpgUrls; - } - /** * Strategies for resolving artifact versions if it is not listed in streams. *
    @@ -223,26 +197,22 @@ public static class Builder { private NoStreamStrategy strategy; private String description; private Vendor vendor; - private Boolean gpgCheck; - private List gpgUrls; public Builder() { } public Builder(Channel from) { this.name = from.getName(); - this.repositories = from.getRepositories() == null ? null : new ArrayList<>(from.getRepositories()); + this.repositories = new ArrayList<>(from.getRepositories()); this.manifestCoordinate = from.getManifestCoordinate(); this.blocklistCoordinate = from.getBlocklistCoordinate(); this.strategy = from.getNoStreamStrategy(); this.description = from.getDescription(); this.vendor = from.getVendor(); - this.gpgCheck = from._isGpgCheck(); - this.gpgUrls = from.getGpgUrls() == null ? null : new ArrayList<>(from.getGpgUrls()); } public Channel build() { - return new Channel(ChannelMapper.CURRENT_SCHEMA_VERSION, name, description, vendor, repositories, manifestCoordinate, blocklistCoordinate, strategy, gpgCheck, gpgUrls); + return new Channel(name, description, vendor, repositories, manifestCoordinate, blocklistCoordinate, strategy); } public Builder setName(String name) { @@ -308,18 +278,5 @@ public Builder setResolveStrategy(NoStreamStrategy strategy) { this.strategy = strategy; return this; } - - public Builder setGpgCheck(boolean gpgCheck) { - this.gpgCheck = gpgCheck; - return this; - } - - public Builder addGpgUrl(String gpgUrl) { - if (this.gpgUrls == null) { - this.gpgUrls = new ArrayList<>(); - } - this.gpgUrls.add(gpgUrl); - return this; - } } } diff --git a/core/src/main/java/org/wildfly/channel/ChannelImpl.java b/core/src/main/java/org/wildfly/channel/ChannelImpl.java index 51f13630..85eb7027 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelImpl.java +++ b/core/src/main/java/org/wildfly/channel/ChannelImpl.java @@ -80,7 +80,7 @@ void init(MavenVersionsResolver.Factory factory, List channels) { return; } - resolver = factory.create(channelDefinition); + resolver = factory.create(channelDefinition.getRepositories()); final Channel.Builder resolvedChannelBuilder = new Channel.Builder(channelDefinition); if (channelDefinition.getManifestCoordinate() != null) { @@ -149,16 +149,9 @@ private ChannelImpl createNewChannelFromMaven(MavenVersionsResolver.Factory fact version = latest.orElseThrow(() -> new RuntimeException(String.format("Can not determine the latest version for Maven artifact %s:%s:%s:%s", groupId, artifactId, ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER))); } - final Channel requiredChannelDefinition = new Channel.Builder(channelDefinition) - .setName(null) - .setDescription(null) - .setVendor(null) - .setManifestCoordinate(groupId, artifactId, version) - .setResolveStrategy(Channel.NoStreamStrategy.NONE) - .build(); - - final ChannelImpl requiredChannel = new ChannelImpl(requiredChannelDefinition); - + final ChannelImpl requiredChannel = new ChannelImpl(new Channel(null, null, null, channelDefinition.getRepositories(), + new ChannelManifestCoordinate(groupId, artifactId, version), null, + Channel.NoStreamStrategy.NONE)); try { requiredChannel.init(factory, channels); } catch (UnresolvedMavenArtifactException e) { diff --git a/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java b/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java index 1f7aa143..8b4efcc1 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java +++ b/core/src/main/java/org/wildfly/channel/ChannelManifestCoordinate.java @@ -45,23 +45,12 @@ public ChannelManifestCoordinate(URL url) { super(url); } - public ChannelManifestCoordinate(URL url, URL signatureUrl) { - super(url, signatureUrl); - } - public ChannelManifestCoordinate() { super(ChannelManifest.CLASSIFIER, ChannelManifest.EXTENSION); } - public static ChannelManifestCoordinate create(String url, - MavenCoordinate gav) throws MalformedURLException { - return create(url, null, gav); - } - @JsonCreator - public static ChannelManifestCoordinate create(@JsonProperty(value = "url") String url, - @JsonProperty(value = "signature-url") String signatureUrl, - @JsonProperty(value = "maven") MavenCoordinate gav) throws MalformedURLException { + public static ChannelManifestCoordinate create(@JsonProperty(value = "url") String url, @JsonProperty(value = "maven") MavenCoordinate gav) throws MalformedURLException { if (gav != null) { if (gav.getVersion() == null || gav.getVersion().isEmpty()) { return new ChannelManifestCoordinate(gav.getGroupId(), gav.getArtifactId()); @@ -69,7 +58,7 @@ public static ChannelManifestCoordinate create(@JsonProperty(value = "url") Stri return new ChannelManifestCoordinate(gav.getGroupId(), gav.getArtifactId(), gav.getVersion()); } } else { - return new ChannelManifestCoordinate(new URL(url), signatureUrl == null ? null : new URL(signatureUrl)); + return new ChannelManifestCoordinate(new URL(url)); } } @@ -92,10 +81,4 @@ private boolean isEmpty(String text) { public URL getUrl() { return super.getUrl(); } - - @JsonProperty(value = "signature-url") - @JsonInclude(NON_NULL) - public URL getSignatureUrl() { - return super.getSignatureUrl(); - } } diff --git a/core/src/main/java/org/wildfly/channel/ChannelMapper.java b/core/src/main/java/org/wildfly/channel/ChannelMapper.java index 30301c90..e1d1f04c 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelMapper.java +++ b/core/src/main/java/org/wildfly/channel/ChannelMapper.java @@ -53,12 +53,10 @@ public class ChannelMapper { public static final String SCHEMA_VERSION_1_0_0 = "1.0.0"; public static final String SCHEMA_VERSION_2_0_0 = "2.0.0"; - public static final String SCHEMA_VERSION_2_1_0 = "2.1.0"; - public static final String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_2_1_0; + public static final String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_2_0_0; private static final String SCHEMA_1_0_0_FILE = "org/wildfly/channel/v1.0.0/schema.json"; private static final String SCHEMA_2_0_0_FILE = "org/wildfly/channel/v2.0.0/schema.json"; - private static final String SCHEMA_2_1_0_FILE = "org/wildfly/channel/v2.1.0/schema.json"; private static final YAMLFactory YAML_FACTORY = new YAMLFactory() .configure(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR, true); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(YAML_FACTORY) @@ -73,7 +71,6 @@ public class ChannelMapper { } SCHEMAS.put(SCHEMA_VERSION_1_0_0, SCHEMA_FACTORY.getSchema(ChannelMapper.class.getClassLoader().getResourceAsStream(SCHEMA_1_0_0_FILE))); SCHEMAS.put(SCHEMA_VERSION_2_0_0, SCHEMA_FACTORY.getSchema(ChannelMapper.class.getClassLoader().getResourceAsStream(SCHEMA_2_0_0_FILE))); - SCHEMAS.put(SCHEMA_VERSION_2_1_0, SCHEMA_FACTORY.getSchema(ChannelMapper.class.getClassLoader().getResourceAsStream(SCHEMA_2_1_0_FILE))); } private static JsonSchema getSchema(JsonNode node) { diff --git a/core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java b/core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java index 82fd2c32..7eb1435c 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java +++ b/core/src/main/java/org/wildfly/channel/ChannelMetadataCoordinate.java @@ -35,7 +35,6 @@ public class ChannelMetadataCoordinate { private String extension; private URL url; - protected URL signatureUrl; protected ChannelMetadataCoordinate() { } @@ -58,12 +57,6 @@ public ChannelMetadataCoordinate(URL url) { requireNonNull(url); } - public ChannelMetadataCoordinate(URL url, URL signatureUrl) { - this(null, null, null, null, null, url); - requireNonNull(url); - this.signatureUrl = signatureUrl; - } - private ChannelMetadataCoordinate(String groupId, String artifactId, String version, String classifier, String extension, URL url) { this.groupId = groupId; this.artifactId = artifactId; @@ -101,10 +94,6 @@ public String getExtension() { return extension; } - public URL getSignatureUrl() { - return signatureUrl; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/src/main/java/org/wildfly/channel/ChannelSession.java b/core/src/main/java/org/wildfly/channel/ChannelSession.java index d1507b73..3a03ff8a 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelSession.java +++ b/core/src/main/java/org/wildfly/channel/ChannelSession.java @@ -33,7 +33,6 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; -import org.apache.commons.lang3.tuple.Pair; import org.jboss.logging.Logger; import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.version.VersionMatcher; @@ -48,6 +47,8 @@ public class ChannelSession implements AutoCloseable { private final List channels; private final ChannelRecorder recorder = new ChannelRecorder(); + // resolver used for direct dependencies only. Uses combination of all repositories in the channels. + private final MavenVersionsResolver combinedResolver; private final int versionResolutionParallelism; /** @@ -65,7 +66,7 @@ public ChannelSession(List channelDefinitions, MavenVersionsResolver.Fa /** * Create a ChannelSession. * - * @param channelDefinitions the list of channels to resolve Maven artifact + * @param channels the list of channels to resolve Maven artifact * @param factory Factory to create {@code MavenVersionsResolver} that are performing the actual Maven resolution. * @param versionResolutionParallelism Number of threads to use when resolving available artifact versions. * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels @@ -75,6 +76,9 @@ public ChannelSession(List channelDefinitions, MavenVersionsResolver.Fa requireNonNull(channelDefinitions); requireNonNull(factory); + final Set repositories = channelDefinitions.stream().flatMap(c -> c.getRepositories().stream()).collect(Collectors.toSet()); + this.combinedResolver = factory.create(repositories); + List channelList = channelDefinitions.stream().map(ChannelImpl::new).collect(Collectors.toList()); for (ChannelImpl channel : channelList) { channel.init(factory, channelList); @@ -198,37 +202,9 @@ public MavenArtifact resolveDirectMavenArtifact(String groupId, String artifactI requireNonNull(artifactId); requireNonNull(version); - /* - * when resolving a direct artifact we don't care if a channel manifest lists that artifact, we resolve it - * if it is present in underlying repositories. - * BUT if the channel requires a GPG check, the artifact has to still be verified. - * Therefore, we're trying to resolve artifact from each available channel and returning the first match. - */ - - File file = null; - UnresolvedMavenArtifactException ex = null; - for (ChannelImpl channel : channels) { - try { - file = channel.resolveArtifact(groupId, artifactId, extension, classifier, version).file; - break; - } catch (UnresolvedMavenArtifactException e) { - ex = e; - } - } - - if (file != null) { - recorder.recordStream(groupId, artifactId, version); - return new MavenArtifact(groupId, artifactId, extension, classifier, version, file); - } else if (ex != null) { - throw ex; - } else { - throw new ArtifactTransferException("Unable to resolve direct artifact.", - Set.of(new ArtifactCoordinate(groupId, artifactId, extension, classifier, version)), - channels.stream() - .map(ChannelImpl::getResolvedChannelDefinition) - .flatMap(cd->cd.getRepositories().stream()) - .collect(Collectors.toSet())); - } + File file = combinedResolver.resolveArtifact(groupId, artifactId, extension, classifier, version); + recorder.recordStream(groupId, artifactId, version); + return new MavenArtifact(groupId, artifactId, extension, classifier, version, file); } /** @@ -246,64 +222,16 @@ public List resolveDirectMavenArtifacts(List requireNonNull(c.getArtifactId()); requireNonNull(c.getVersion()); }); + final List files = combinedResolver.resolveArtifacts(coordinates); - /* - * When resolving a "direct" artifact, we don't care if the artifact is listed in the channel's manifest, - * only if the underlying repositories contain that artifact. - * BUT, we still need to verify the artifact signature if the channel requires it. To achieve that, we're - * going to query each channel in turn, taking the artifacts it was able to resolve and keeping the rest - * to be resolved by remaining channels. At the end we should be left with no un-resolved artifacts, or have - * a list of artifacts not available in any channels. - * NOTE: if the same artifact is available in both GPG-enabled and GPG-disabled channel there is no guarantee - * which channel will be queried first. - */ - - // list of artifacts that are being resolved in this step - List currentQuery = new ArrayList<>(coordinates); - // we need to preserve the ordering of artifacts, but that can be affected by the order of query/resolution between channels - final HashMap> resolvedArtifacts = new HashMap<>(); - for (ChannelImpl channel : channels) { - if (currentQuery.isEmpty()) { - break; - } + final ArrayList res = new ArrayList<>(); + for (int i = 0; i < coordinates.size(); i++) { + final ArtifactCoordinate request = coordinates.get(i); + final MavenArtifact resolvedArtifact = new MavenArtifact(request.getGroupId(), request.getArtifactId(), request.getExtension(), request.getClassifier(), request.getVersion(), files.get(i)); - try { - final List resolved = channel.resolveArtifacts(currentQuery); - // keep a map of AC -> File - for (int i = 0; i < currentQuery.size(); i++) { - resolvedArtifacts.put(currentQuery.get(i), Pair.of(resolved.get(i).file, channel.getResolvedChannelDefinition().getName())); - } - // all the artifacts were resolved by this point, lets remove all artifacts from the current query - currentQuery = Collections.emptyList(); - } catch (UnresolvedMavenArtifactException e) { - // at the end need to map them into a correct order - final Set unresolvedArtifacts = e.getUnresolvedArtifacts(); - // coordinates - unresolved = it should be possible to resolve those artifacts from this channel - // we need to call resolve again, because the first call threw an exception - currentQuery.removeAll(unresolvedArtifacts); - final List resolved = channel.resolveArtifacts(currentQuery); - for (int i = 0; i < currentQuery.size(); i++) { - resolvedArtifacts.put(currentQuery.get(i), Pair.of(resolved.get(i).file, channel.getResolvedChannelDefinition().getName())); - } - // unresolved - try with another channel, rinse and repeat until run out of channels or resolve all artifacts - currentQuery = new ArrayList<>(unresolvedArtifacts); - } - } - if (!currentQuery.isEmpty()) { - throw new ArtifactTransferException("Unable to resolve some direct artifacts", new HashSet<>(currentQuery), - channels.stream() - .map(ChannelImpl::getResolvedChannelDefinition) - .flatMap(cd->cd.getRepositories().stream()) - .collect(Collectors.toSet())); + recorder.recordStream(resolvedArtifact.getGroupId(), resolvedArtifact.getArtifactId(), resolvedArtifact.getVersion()); + res.add(resolvedArtifact); } - - // finally, build a list of resolved files in a correct order to return and record the streams - final List res = coordinates.stream().map(ac -> new MavenArtifact(ac.getGroupId(), ac.getArtifactId(), ac.getExtension(), - ac.getClassifier(), ac.getVersion(), resolvedArtifacts.get(ac).getLeft(), resolvedArtifacts.get(ac).getRight())).collect(Collectors.toList()); - - res.forEach(resolvedArtifact-> - recorder.recordStream(resolvedArtifact.getGroupId(), resolvedArtifact.getArtifactId(), resolvedArtifact.getVersion()) - ); return res; } @@ -329,6 +257,7 @@ public void close() { for (ChannelImpl channel : channels) { channel.close(); } + combinedResolver.close(); } /** diff --git a/core/src/main/java/org/wildfly/channel/InvalidChannelMetadataException.java b/core/src/main/java/org/wildfly/channel/InvalidChannelMetadataException.java index 1e7b027a..5d2208de 100644 --- a/core/src/main/java/org/wildfly/channel/InvalidChannelMetadataException.java +++ b/core/src/main/java/org/wildfly/channel/InvalidChannelMetadataException.java @@ -26,11 +26,6 @@ public InvalidChannelMetadataException(String message, List messages) { this.messages = messages; } - public InvalidChannelMetadataException(String message, List messages, Exception cause) { - super(message, cause); - this.messages = messages; - } - public List getValidationMessages() { return messages; } diff --git a/core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java b/core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java deleted file mode 100644 index aa5d060c..00000000 --- a/core/src/main/java/org/wildfly/channel/SignedVersionResolverWrapper.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2022 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel; - -import static java.util.Collections.singleton; -import static java.util.Objects.requireNonNull; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import org.wildfly.channel.spi.ArtifactIdentifier; -import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.version.VersionMatcher; - -/** - * Resolve and validate a signature using the wrapped {@code MavenVersionsResolver}. - */ -public class SignedVersionResolverWrapper implements MavenVersionsResolver { - - protected static final String SIGNATURE_FILE_SUFFIX = ".asc"; - private final MavenVersionsResolver wrapped; - private final SignatureValidator signatureValidator; - private final List gpgUrls; - private final Collection repositories; - - public SignedVersionResolverWrapper(MavenVersionsResolver wrapped, Collection repositories, - SignatureValidator signatureValidator, List gpgUrls) { - this.wrapped = wrapped; - this.repositories = repositories; - this.signatureValidator = signatureValidator; - this.gpgUrls = gpgUrls; - } - - private void validateGpgSignature(String groupId, String artifactId, String extension, String classifier, - String version, File artifact) { - final ArtifactIdentifier mavenArtifact = new ArtifactIdentifier.MavenResource(groupId, artifactId, extension, - classifier, version); - try { - final File signature = wrapped.resolveArtifact(groupId, artifactId, extension + SIGNATURE_FILE_SUFFIX, - classifier, version); - final SignatureResult signatureResult = signatureValidator.validateSignature( - mavenArtifact, new FileInputStream(artifact), new FileInputStream(signature), - gpgUrls); - if (signatureResult.getResult() != SignatureResult.Result.OK) { - throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); - } - } catch (ArtifactTransferException | FileNotFoundException e) { - throw new SignatureValidator.SignatureException("Unable to find required signature for " + mavenArtifact, - e, SignatureResult.noSignature(mavenArtifact)); - } - } - - private void validateGpgSignature(URL artifactFile, URL signature) throws IOException { - final SignatureResult signatureResult = signatureValidator.validateSignature( - new ArtifactIdentifier.UrlResource(artifactFile), - artifactFile.openStream(), signature.openStream(), - gpgUrls - ); - - if (signatureResult.getResult() != SignatureResult.Result.OK) { - throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); - } - } - - @Override - public Set getAllVersions(String groupId, String artifactId, String extension, String classifier) { - return wrapped.getAllVersions(groupId, artifactId, extension, classifier); - } - - @Override - public File resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) throws ArtifactTransferException { - final File artifact = wrapped.resolveArtifact(groupId, artifactId, extension, classifier, version); - - validateGpgSignature(groupId, artifactId, extension, classifier, version, artifact); - - return artifact; - } - - @Override - public List resolveArtifacts(List coordinates) throws ArtifactTransferException { - final List resolvedArtifacts = wrapped.resolveArtifacts(coordinates); - - try { - final List signatures = wrapped.resolveArtifacts(coordinates.stream() - .map(c->new ArtifactCoordinate(c.getGroupId(), c.getArtifactId(), c.getExtension() + SIGNATURE_FILE_SUFFIX, - c.getClassifier(), c.getVersion())) - .collect(Collectors.toList())); - for (int i = 0; i < resolvedArtifacts.size(); i++) { - final File artifact = resolvedArtifacts.get(i); - final ArtifactCoordinate c = coordinates.get(i); - final ArtifactIdentifier.MavenResource mavenArtifact = new ArtifactIdentifier.MavenResource(c.getGroupId(), c.getArtifactId(), - c.getExtension(), c.getClassifier(), c.getVersion()); - final File signature = signatures.get(i); - try { - final SignatureResult signatureResult = signatureValidator.validateSignature(mavenArtifact, - new FileInputStream(artifact), new FileInputStream(signature), gpgUrls); - if (signatureResult.getResult() != SignatureResult.Result.OK) { - throw new SignatureValidator.SignatureException("Failed to verify an artifact signature", signatureResult); - } - } catch (FileNotFoundException e) { - throw new SignatureValidator.SignatureException(String.format("Unable to find required signature for %s:%s:%s", - mavenArtifact.getGroupId(), mavenArtifact.getArtifactId(), mavenArtifact.getVersion()), - SignatureResult.noSignature(mavenArtifact)); - } - } - } catch (ArtifactTransferException e) { - final ArtifactIdentifier.MavenResource artifact = new ArtifactIdentifier.MavenResource(e.getUnresolvedArtifacts().stream().findFirst().get()); - throw new SignatureValidator.SignatureException(String.format("Unable to find required signature for %s:%s:%s", - artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()), - SignatureResult.noSignature(artifact)); - } - - - return resolvedArtifacts; - } - - @Override - public List resolveChannelMetadata(List coords) throws ArtifactTransferException { - requireNonNull(coords); - - final List resolvedMetadata = wrapped.resolveChannelMetadata(coords); - - List signatures = new ArrayList<>(); - - for (ChannelMetadataCoordinate coord : coords) { - if (coord.getUrl() != null) { - try { - final URL signatureUrl; - if (coord.getSignatureUrl() == null) { - signatureUrl = new URL(coord.getUrl().toExternalForm() + SIGNATURE_FILE_SUFFIX); - } else { - signatureUrl = coord.getSignatureUrl(); - } - signatures.add(signatureUrl); - } catch (IOException e) { - throw new InvalidChannelMetadataException("Unable to download a detached signature file from: " + coord.getUrl().toExternalForm()+ SIGNATURE_FILE_SUFFIX, - List.of(e.getMessage()), e); - } - continue; - } - - String version = coord.getVersion(); - if (version == null) { - Set versions = wrapped.getAllVersions(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier()); - Optional latestVersion = VersionMatcher.getLatestVersion(versions); - if (latestVersion.isPresent()){ - version = latestVersion.get(); - } else { - throw new ArtifactTransferException(String.format("Unable to resolve the latest version of channel metadata signature %s:%s", coord.getGroupId(), coord.getArtifactId()), - singleton(new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), "")), - attemptedRepositories()); - } - } - - try { - File channelArtifact = wrapped.resolveArtifact(coord.getGroupId(), coord.getArtifactId(), - coord.getExtension() + SIGNATURE_FILE_SUFFIX, coord.getClassifier(), version); - signatures.add(channelArtifact.toURI().toURL()); - } catch (ArtifactTransferException e) { - throw new SignatureValidator.SignatureException("Unable to find required signature for " + coord, - e, SignatureResult.noSignature(new ArtifactIdentifier.MavenResource(coord.getGroupId(), coord.getArtifactId(), coord.getExtension(), coord.getClassifier(), version))); - } catch (MalformedURLException e) { - throw new ArtifactTransferException(String.format("Unable to resolve the latest version of channel metadata signature %s:%s", coord.getGroupId(), coord.getArtifactId()), e, - singleton(new ArtifactCoordinate(coord.getGroupId(), coord.getArtifactId(), - coord.getExtension(), coord.getClassifier(), coord.getVersion())), - attemptedRepositories()); - } - } - - try { - for (int i = 0; i < resolvedMetadata.size(); i++) { - validateGpgSignature(resolvedMetadata.get(i), signatures.get(i)); - } - } catch (IOException e) { - throw new InvalidChannelMetadataException("Unable to read a detached signature file from: " + signatures, - List.of(e.getMessage()), e); - } - return resolvedMetadata; - } - - private Set attemptedRepositories() { - return repositories.stream() - .map(r -> new Repository(r.getId(), r.getUrl())) - .collect(Collectors.toSet()); - } - - @Override - public String getMetadataReleaseVersion(String groupId, String artifactId) { - return wrapped.getMetadataReleaseVersion(groupId, artifactId); - } - - @Override - public String getMetadataLatestVersion(String groupId, String artifactId) { - return wrapped.getMetadataLatestVersion(groupId, artifactId); - } -} diff --git a/core/src/main/java/org/wildfly/channel/spi/ArtifactIdentifier.java b/core/src/main/java/org/wildfly/channel/spi/ArtifactIdentifier.java deleted file mode 100644 index 6942e487..00000000 --- a/core/src/main/java/org/wildfly/channel/spi/ArtifactIdentifier.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.spi; - -import java.net.URL; - -import org.wildfly.channel.ArtifactCoordinate; - -/** - * An identifier of an artifact being validated. It can be either a Maven coordinate or an URL. - */ -public interface ArtifactIdentifier { - - class UrlResource implements ArtifactIdentifier { - private final URL resourceUrl; - - public UrlResource(URL resourceUrl) { - this.resourceUrl = resourceUrl; - } - - public URL getResourceUrl() { - return resourceUrl; - } - - @Override - public String getDescription() { - return resourceUrl.toExternalForm(); - } - } - - class MavenResource extends ArtifactCoordinate implements ArtifactIdentifier { - - public MavenResource(String groupId, String artifactId, String extension, String classifier, String version) { - super(groupId, artifactId, extension, classifier, version); - } - - public MavenResource(ArtifactCoordinate artifactCoordinate) { - super(artifactCoordinate.getGroupId(), - artifactCoordinate.getArtifactId(), - artifactCoordinate.getExtension(), - artifactCoordinate.getClassifier(), - artifactCoordinate.getVersion()); - } - - @Override - public String getDescription() { - StringBuilder sb = new StringBuilder(); - sb.append(groupId).append(":").append(artifactId).append(":"); - if (classifier != null && !classifier.isEmpty()) { - sb.append(classifier).append(":"); - } - if (extension != null && !extension.isEmpty()) { - sb.append(extension).append(":"); - } - sb.append(version); - return sb.toString(); - } - } - - String getDescription(); - - default boolean isMavenResource() { - return this instanceof MavenResource; - } -} diff --git a/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java b/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java index ecc04c85..b82889b7 100644 --- a/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java +++ b/core/src/main/java/org/wildfly/channel/spi/MavenVersionsResolver.java @@ -19,13 +19,14 @@ import java.io.Closeable; import java.io.File; import java.net.URL; +import java.util.Collection; import java.util.List; import java.util.Set; import org.wildfly.channel.ArtifactCoordinate; import org.wildfly.channel.ArtifactTransferException; -import org.wildfly.channel.Channel; import org.wildfly.channel.ChannelMetadataCoordinate; +import org.wildfly.channel.Repository; import org.wildfly.channel.UnresolvedMavenArtifactException; /** @@ -125,11 +126,11 @@ default void close() { * * A client of this library is responsible to provide an implementation of the {@link Factory} interface. * - * The {@link #create(Channel)}} method will be called once for every channel. + * The {@link #create(Collection)}} method will be called once for every channel. */ interface Factory extends Closeable { - MavenVersionsResolver create(Channel channel); + MavenVersionsResolver create(Collection repositories); default void close() { } diff --git a/core/src/main/java/org/wildfly/channel/spi/SignatureResult.java b/core/src/main/java/org/wildfly/channel/spi/SignatureResult.java deleted file mode 100644 index 0bbf694e..00000000 --- a/core/src/main/java/org/wildfly/channel/spi/SignatureResult.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.spi; - -/** - * Represents a result of artifact verification - */ -public class SignatureResult { - - /** - * Identifier of the artifact that was being verified. - */ - private ArtifactIdentifier resource; - /** - * Identifier of the certificate used to verify the artifact. - */ - private String keyId; - /** - * Optional message with details of validation. - */ - private String message; - - public static SignatureResult noMatchingCertificate(ArtifactIdentifier resource, String keyID) { - return new SignatureResult(Result.NO_MATCHING_CERT, resource, keyID, null); - } - - public static SignatureResult revoked(ArtifactIdentifier resource, String keyID, String revocationReason) { - return new SignatureResult(Result.REVOKED, resource, keyID, revocationReason); - } - - public static SignatureResult expired(ArtifactIdentifier resource, String keyID) { - return new SignatureResult(Result.EXPIRED, resource, keyID, null); - } - - public static SignatureResult noSignature(ArtifactIdentifier resource) { - return new SignatureResult(Result.NO_SIGNATURE, resource, null, null); - } - - public static SignatureResult invalid(ArtifactIdentifier resource, String keyID) { - return new SignatureResult(Result.INVALID, resource, keyID, null); - } - - public enum Result {OK, NO_MATCHING_CERT, REVOKED, EXPIRED, NO_SIGNATURE, INVALID;} - private final Result result; - public static SignatureResult ok() { - return new SignatureResult(Result.OK, null, null, null); - } - - private SignatureResult(Result result, ArtifactIdentifier resource, String keyID, String message) { - this.result = result; - this.resource = resource; - this.keyId = keyID; - this.message = message; - } - - public Result getResult() { - return result; - } - - public ArtifactIdentifier getResource() { - return resource; - } - - public String getKeyId() { - return keyId; - } - - public String getMessage() { - return message; - } -} diff --git a/core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java b/core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java deleted file mode 100644 index daa98dae..00000000 --- a/core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.spi; - -import java.io.InputStream; -import java.util.List; - -/** - * Called to validate detached signatures of artifacts resolved in the channel - */ -public interface SignatureValidator { - /** - * A default validator, rejecting all artifacts - */ - SignatureValidator REJECTING_VALIDATOR = (artifactSource, artifactStream, signatureStream, gpgUrls) -> { - throw new SignatureException("Not implemented", SignatureResult.noSignature(artifactSource)); - }; - - /** - * validates a signature of an artifact. The locally downloaded {@code signature} has to be an armour encoded GPG signature. - * - * @param artifactId - an identifier of the resource to be validated. - * @param artifactStream - an {@code InputStream} of the artifact to be verified. - * @param signatureStream - an {@code InputStream} of the armour encoded detached GPG signature for the artifact. - * @param gpgUrls - URLs of the keys defined in the channel. Empty collection if channel does not define any signatures. - * @return {@link SignatureResult} with the result of validation - * @throws SignatureException - if an unexpected error occurred when handling the keys. - */ - SignatureResult validateSignature(ArtifactIdentifier artifactId, InputStream artifactStream, - InputStream signatureStream, List gpgUrls) throws SignatureException; - - /** - * An exception signifying issue with an artifact signature validation. - */ - class SignatureException extends RuntimeException { - private final SignatureResult signatureResult; - private String missingSignature; - - public SignatureException(String message, Throwable cause, SignatureResult signatureResult) { - super(buildErrorMessage(message, signatureResult), cause); - this.signatureResult = signatureResult; - this.missingSignature = signatureResult.getKeyId(); - } - - public SignatureException(String message, SignatureResult signatureResult) { - super(buildErrorMessage(message, signatureResult)); - this.signatureResult = signatureResult; - this.missingSignature = signatureResult.getKeyId(); - } - - private static String buildErrorMessage(String message, SignatureResult signatureResult) { - return String.format("%s: %s%s", message, signatureResult.getResult(), signatureResult.getMessage() == null ? "" : signatureResult.getResult()); - } - - public SignatureResult getSignatureResult() { - return signatureResult; - } - - public String getMissingSignature() { - return missingSignature; - } - } -} diff --git a/core/src/main/resources/org/wildfly/channel/v2.1.0/schema.json b/core/src/main/resources/org/wildfly/channel/v2.1.0/schema.json deleted file mode 100644 index 36779440..00000000 --- a/core/src/main/resources/org/wildfly/channel/v2.1.0/schema.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "$id": "https://wildfly.org/channels/v2.1.0/schema.json", - "$schema": "https://json-schema.org/draft/2019-09/schema#", - "type": "object", - "required": ["schemaVersion", "repositories"], - "properties": { - "schemaVersion": { - "description": "The version of the schema defining a channel resource.", - "type": "string", - "pattern": "^[0-9]+.[0-9]+.[0-9]+$" - }, - "name": { - "description": "Name of the channel. This is a one-line human-readable description of the channel", - "type": "string" - }, - "description": { - "description": "Description of the channel. This is a multi-lines human-readable description of the channel", - "type": "string" - }, - "vendor": { - "description": "Vendor of the channel.", - "type": "object", - "properties": { - "name": { - "description": "Name of the vendor", - "type": "string" - }, - "support": { - "description": "Support level provided by the vendor", - "type": "string", - "enum": [ - "supported", - "tech-preview", - "community" - ] - } - }, - "required": ["name", "support"] - }, - "repositories": { - "description": "Repositories the channel uses to resolve its streams.", - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "id": { - "description": "Id of the repository", - "type": "string" - }, - "url": { - "description": "URL of the repository", - "type": "string" - } - }, - "required": ["id", "url"] - } - }, - "manifest": { - "description": "Location of the channel's manifest", - "type": "object", - "properties": { - "maven": { - "type": "object", - "properties": { - "groupId": { - "description": "GroupID Maven coordinate of the manifest", - "type": "string" - }, - "artifactId": { - "description": "ArtifactID Maven coordinate of the manifest", - "type": "string" - }, - "version": { - "description": "Version Maven coordinate of the manifest", - "type": "string" - } - }, - "required": ["groupId", "artifactId"] - }, - "url": { - "description": "URL of the manifest file.", - "type": "string" - }, - "signature-url": { - "description": "URL of the manifest signature file.", - "type": "string" - } - }, - "oneOf": [ - { - "required": [ - "maven" - ] - }, - { - "required": [ - "url" - ] - } - ] - }, - "blocklist": { - "description": "Location of the channel's blocklist", - "type": "object", - "properties": { - "maven": { - "type": "object", - "properties": { - "groupId": { - "description": "GroupID Maven coordinate of the blocklist", - "type": "string" - }, - "artifactId": { - "description": "ArtifactID Maven coordinate of the blocklist", - "type": "string" - }, - "version": { - "description": "Version Maven coordinate of the blocklist", - "type": "string" - } - }, - "required": ["groupId", "artifactId"] - }, - "url": { - "description": "URL of the blocklist file.", - "type": "string" - } - }, - "oneOf": [ - { - "required": [ - "maven" - ] - }, - { - "required": [ - "url" - ] - } - ] - }, - "resolve-if-no-stream": { - "description": "Strategy for resolving artifact versions if it is not listed in streams. If not specified, 'original' strategy is used.", - "type": "string", - "enum": [ - "latest", - "maven-latest", - "maven-release", - "none" - ] - }, - "gpg-check": { - "description": "Verify the signatures of artifacts provided from this channel.", - "type": "boolean" - }, - "gpg-urls": { - "description": "The URLs of the public keys used to sign channel artifacts.", - "type": "array", - "minItems": "1", - "items": { - "type": "string" - } - } - } -} diff --git a/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java index 7ecf2dd3..212d9d34 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelMapperTestCase.java @@ -16,16 +16,14 @@ */ package org.wildfly.channel; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.net.URL; import java.util.List; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; public class ChannelMapperTestCase { @@ -108,44 +106,4 @@ public void writeChannelWithNoResolveStrategy() throws Exception { Channel readChannel = ChannelMapper.fromString(yaml).get(0); assertEquals(Channel.NoStreamStrategy.NONE, readChannel.getNoStreamStrategy()); } - - @Test - public void setGpgCheck() throws Exception { - verifyGpgCheck(false); - verifyGpgCheck(true); - } - - @Test - public void nullGpgCheckIsNotSerialized() throws Exception { - Channel.Builder channel = new Channel.Builder() - .addRepository("test", "https://test.org/repository"); - - final String yaml = ChannelMapper.toYaml(channel.build()); - assertThat(yaml) - .doesNotContain("gpg-check"); - } - - @Test - public void writeChannelWithGpgKeys() throws Exception { - Channel.Builder channel = new Channel.Builder() - .addRepository("test", "https://test.org/repository") - .addGpgUrl("https://gpg.test/key"); - - final String yaml = ChannelMapper.toYaml(channel.build()); - - Channel readChannel = ChannelMapper.fromString(yaml).get(0); - Assertions.assertThat(readChannel.getGpgUrls()) - .containsExactly("https://gpg.test/key"); - } - - private static void verifyGpgCheck(boolean value) throws IOException { - Channel.Builder channel = new Channel.Builder() - .addRepository("test", "https://test.org/repository") - .setGpgCheck(value); - - final String yaml = ChannelMapper.toYaml(channel.build()); - - Channel readChannel = ChannelMapper.fromString(yaml).get(0); - assertEquals(value, readChannel.isGpgCheck()); - } } diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java index 3ab970d2..0bcc08f6 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java @@ -48,8 +48,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.mockito.ArgumentMatcher; -import org.mockito.ArgumentMatchers; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.wildfly.channel.spi.MavenVersionsResolver; @@ -81,7 +79,7 @@ public void testFindLatestMavenArtifactVersion() throws Exception { assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } public static List mockChannel(MavenVersionsResolver resolver, Path tempDir, String... manifests) throws IOException { @@ -130,7 +128,7 @@ public void testFindLatestMavenArtifactVersionThrowsUnresolvedMavenArtifactExcep } } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -165,7 +163,7 @@ public void testResolveLatestMavenArtifact() throws Exception { assertEquals("channel-0", artifact.getChannelName().get()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -194,7 +192,7 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( } } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -233,7 +231,7 @@ public void testResolveDirectMavenArtifact() throws Exception { assertEquals(Optional.empty(), artifact.getChannelName(), "The channel name should be null when resolving version directly"); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -282,7 +280,7 @@ public void testResolveMavenArtifactsFromOneChannel() throws Exception { assertEquals("25.0.0.Final", stream.get().getVersion()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -344,7 +342,7 @@ public List answer(InvocationOnMock invocationOnMock) throws Throwable { assertEquals("25.0.0.Final", stream.get().getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(3)).close(); } @Test @@ -375,100 +373,8 @@ public void testResolveDirectMavenArtifacts() throws Exception { assertNotNull(resolved); final List expected = asList( - new MavenArtifact("org.foo", "foo", null, null, "25.0.0.Final", resolvedArtifactFile1, "channel-0"), - new MavenArtifact("org.bar", "bar", null, null, "26.0.0.Final", resolvedArtifactFile2, "channel-0") - ); - assertContainsAll(expected, resolved); - - Optional stream = session.getRecordedChannel().findStreamFor("org.bar", "bar"); - assertTrue(stream.isPresent()); - assertEquals("26.0.0.Final", stream.get().getVersion()); - stream = session.getRecordedChannel().findStreamFor("org.foo", "foo"); - assertTrue(stream.isPresent()); - assertEquals("25.0.0.Final", stream.get().getVersion()); - } - - verify(resolver, times(1)).close(); - } - - @Test - public void testResolveDirectMavenArtifactsFromTwoChannels() throws Exception { - String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + - "streams:\n" + - " - groupId: org.not\n" + - " artifactId: used\n" + - " version: \"1.0.0.Final\""; - - /* - * create two resolvers. The first one will be used by the first channel, the other by the second channel - * Each resolver is only able to resolve one artifact and throws error when searching for both artifacts - */ - MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); - MavenVersionsResolver resolver1 = mock(MavenVersionsResolver.class); - MavenVersionsResolver resolver2 = mock(MavenVersionsResolver.class); - File resolvedArtifactFile1 = mock(File.class); - File resolvedArtifactFile2 = mock(File.class); - - when(factory.create(any())).thenAnswer(inv->{ - final Channel channel = inv.getArgument(0); - if (channel.getName().equals("channel-0")) { - return resolver1; - } else if (channel.getName().equals("channel-1")) { - return resolver2; - } else { - throw new RuntimeException("Unexpected channel " + channel.getName()); - } - }); - - final ArtifactCoordinate fooArtifact = new ArtifactCoordinate("org.foo", "foo", null, null, "25.0.0.Final"); - final ArtifactCoordinate barArtifact = new ArtifactCoordinate("org.bar", "bar", null, null, "26.0.0.Final"); - final List coordinates = asList( - fooArtifact, - barArtifact); - when(resolver1.resolveArtifacts(any())).thenAnswer(inv -> { - final List coords = inv.getArgument(0); - if (coords.size() == 2) { - throw new ArtifactTransferException("", - Set.of(barArtifact), - Set.of(new Repository("test", "http://test.te")) - ); - } else if (coords.get(0).equals(fooArtifact)) { - return List.of(resolvedArtifactFile1); - } else { - throw new RuntimeException("Unexpected query " + coords); - } - }); - when(resolver2.resolveArtifacts(any())).thenAnswer(inv -> { - final List coords = inv.getArgument(0); - if (coords.size() == 2) { - throw new ArtifactTransferException("", - Set.of(fooArtifact), - Set.of(new Repository("test", "http://test.te")) - ); - } else if (coords.get(0).equals(barArtifact)) { - return List.of(resolvedArtifactFile2); - } else { - throw new RuntimeException("Unexpected query " + coords); - } - }); - - /* - * create channel session with two channels. The manifests don't matter, but set different names - */ - final List channels = mockChannel(resolver1, tempDir, manifest); - channels.add(new Channel.Builder(mockChannel(resolver2, tempDir, manifest).get(0)) - .setName("channel-1").build()); - - try (ChannelSession session = new ChannelSession(channels, factory)) { - - List resolved = session.resolveDirectMavenArtifacts(coordinates); - assertNotNull(resolved); - - final List expected = asList( - new MavenArtifact(fooArtifact.getGroupId(), fooArtifact.getArtifactId(), fooArtifact.getExtension(), - fooArtifact.getClassifier(), fooArtifact.getVersion(), resolvedArtifactFile1, "channel-0"), - new MavenArtifact(barArtifact.getGroupId(), barArtifact.getArtifactId(), barArtifact.getExtension(), - barArtifact.getClassifier(), barArtifact.getVersion(), resolvedArtifactFile2, "channel-1") + new MavenArtifact("org.foo", "foo", null, null, "25.0.0.Final", resolvedArtifactFile1), + new MavenArtifact("org.bar", "bar", null, null, "26.0.0.Final", resolvedArtifactFile2) ); assertContainsAll(expected, resolved); @@ -480,8 +386,7 @@ public void testResolveDirectMavenArtifactsFromTwoChannels() throws Exception { assertEquals("25.0.0.Final", stream.get().getVersion()); } - verify(resolver1, times(1)).close(); - verify(resolver2, times(1)).close(); + verify(resolver, times(2)).close(); } @Test diff --git a/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java index edd915b6..4a0a2c39 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java @@ -94,7 +94,7 @@ public void testFindLatestMavenArtifactVersion() throws Exception { assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -142,7 +142,7 @@ public void testFindLatestMavenArtifactVersionBlocklistDoesntExist() throws Exce assertEquals("25.0.1.Final", version.getVersion()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -187,7 +187,7 @@ public void testFindLatestMavenArtifactVersionWithWildcardBlocklist() throws Exc assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -235,7 +235,7 @@ public void testFindLatestMavenArtifactVersionBlocklistsAllVersionsException() t } } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -290,7 +290,7 @@ public void testResolveLatestMavenArtifact() throws Exception { assertEquals(resolvedArtifactFile, artifact.getFile()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -338,7 +338,7 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( } } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } private void mockBlocklistResolution(MavenVersionsResolver resolver, String fileName) { @@ -417,7 +417,7 @@ public void testResolveMavenArtifactsFromOneChannel() throws Exception { assertEquals("26.0.0.Final", stream.get().getVersion()); } - verify(resolver, times(1)).close(); + verify(resolver, times(2)).close(); } @Test @@ -469,7 +469,7 @@ public void testFindLatestMavenArtifactVersionInRequiredChannel() throws Excepti assertEquals("25.0.0.Final", version.getVersion()); } - verify(resolver, times(2)).close(); + verify(resolver, times(3)).close(); } @Test diff --git a/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java index a1b95244..36ca5c83 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelWithRequirementsTestCase.java @@ -26,14 +26,12 @@ import java.io.File; import java.io.IOException; -import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Set; import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.Assertions; @@ -583,7 +581,8 @@ public void testRequiredChannelIgnoresNoStreamStrategy() throws Exception { File resolvedArtifactFile = mock(File.class); URL resolvedRequiredManifestURL = tccl.getResource("channels/required-manifest.yaml"); - mockManifest(resolver, resolvedRequiredManifestURL, "org.test:required-manifest:1.0.0"); + when(resolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("org.test", "required-manifest", "1.0.0")))) + .thenReturn(List.of(resolvedRequiredManifestURL)); when(factory.create(any())) .thenReturn(resolver); diff --git a/core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java b/core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java deleted file mode 100644 index e9a9ee91..00000000 --- a/core/src/test/java/org/wildfly/channel/SignedVersionResolverWrapperTest.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.wildfly.channel.SignedVersionResolverWrapper.SIGNATURE_FILE_SUFFIX; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; - -import org.apache.commons.lang3.RandomUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.wildfly.channel.spi.ArtifactIdentifier; -import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; - -class SignedVersionResolverWrapperTest { - - private static final ArtifactIdentifier.MavenResource ARTIFACT = new ArtifactIdentifier.MavenResource( - "org.wildfly", "wildfly-ee-galleon-pack", "zip", null, "25.0.1.Final"); - - @TempDir - private Path tempDir; - private MavenVersionsResolver resolver; - private SignatureValidator signatureValidator; - private SignedVersionResolverWrapper signedResolver; - - private File signatureFile; - private File resolvedArtifactFile; - - @BeforeEach - public void setUp() throws Exception { - resolver = mock(MavenVersionsResolver.class); - signatureValidator = mock(SignatureValidator.class); - signedResolver = new SignedVersionResolverWrapper(resolver, List.of(new Repository("test", "test")), - signatureValidator, Collections.emptyList()); - - MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); - when(factory.create(any())).thenReturn(resolver); - - signatureFile = tempDir.resolve("test-signature.asc").toFile(); - Files.createFile(signatureFile.toPath()); - - resolvedArtifactFile = tempDir.resolve("test-artifact").toFile(); - Files.createFile(resolvedArtifactFile.toPath()); - - when(factory.create(any())) - .thenReturn(resolver); - when(resolver.resolveArtifact("org.example", "foo-bar", null, null, "1.2.0.Final")) - .thenReturn(resolvedArtifactFile); - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)) - .thenReturn(resolvedArtifactFile); - } - - @Test - public void invalidSignatureCausesError() throws Exception { - final ChannelManifest baseManifest = new ManifestBuilder() - .setId("manifest-one") - .build(); - mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); - - Files.createFile(tempDir.resolve("test-manifest.yaml.asc")); - when(resolver.resolveArtifact("test.channels", "base-manifest", - ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenReturn(tempDir.resolve("test-manifest.yaml.asc").toFile()); - when(signatureValidator.validateSignature(any(), any(), any(), any())).thenReturn(SignatureResult.invalid(mock(ArtifactIdentifier.class), "abcd")); - assertThrows(SignatureValidator.SignatureException.class, - () -> signedResolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test.channels", "base-manifest", "1.0.0")))); - } - - @Test - public void mvnManifestWithoutSignatureCausesError() throws Exception { - final ChannelManifest baseManifest = new ManifestBuilder() - .setId("manifest-one") - .build(); - final Path manifestFile = tempDir.resolve("test-manifest.yaml"); - Files.writeString(manifestFile, ChannelManifestMapper.toYaml(baseManifest)); - - when(resolver.resolveArtifact("test.channels", "base-manifest", - ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) - .thenThrow(ArtifactTransferException.class); - assertThrows(SignatureValidator.SignatureException.class, - () -> signedResolver.resolveChannelMetadata(List.of(new ChannelManifestCoordinate("test.channels", "base-manifest", "1.0.0")))); - } - - @Test - public void artifactWithCorrectSignatureIsValidated() throws Exception { - // return signature when resolving it from Maven repository - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, - ARTIFACT.classifier, ARTIFACT.version)) - .thenReturn(signatureFile); - // accept all the validation requests - when(signatureValidator.validateSignature(any(), - any(), any(), any())).thenReturn(SignatureResult.ok()); - - - assertEquals(resolvedArtifactFile, signedResolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)); - - verify(signatureValidator).validateSignature(any(), any(), any(), any()); - } - - @Test - public void artifactWithoutSignatureIsRejected() throws Exception { - // simulate situation where the signature file does not exist in the repository - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, - ARTIFACT.classifier, ARTIFACT.version)) - .thenThrow(ArtifactTransferException.class); - // accept all the validation requests - when(signatureValidator.validateSignature(any(), - any(), any(), any())).thenReturn(SignatureResult.ok()); - - - assertThrows(SignatureValidator.SignatureException.class, () -> signedResolver.resolveArtifact( - ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)); - - - // validateSignature should not have been called - verify(signatureValidator, never()).validateSignature(any(), any(), any(), any()); - } - - @Test - public void failedSignatureValidationThrowsException() throws Exception { - // return signature when resolving it from Maven repository - when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension + SIGNATURE_FILE_SUFFIX, - ARTIFACT.classifier, ARTIFACT.version)) - .thenReturn(signatureFile); - when(signatureValidator.validateSignature(eq(ARTIFACT), - any(), any(), any())).thenReturn(SignatureResult.invalid(ARTIFACT, "abcd")); - - assertThrows(SignatureValidator.SignatureException.class, () -> signedResolver.resolveArtifact(ARTIFACT.groupId, - ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)); - - - verify(signatureValidator).validateSignature(any(), any(), any(), any()); - } - - private void mockManifest(MavenVersionsResolver resolver, ChannelManifest manifest, String gav) throws IOException { - - mockManifest(resolver, ChannelManifestMapper.toYaml(manifest), gav); - } - - private void mockManifest(MavenVersionsResolver resolver, String manifest, String gav) throws IOException { - Path manifestFile = tempDir.resolve("manifest_" + RandomUtils.nextInt() + ".yaml"); - Files.writeString(manifestFile, manifest); - - mockManifest(resolver, manifestFile.toUri().toURL(), gav); - } - - private void mockManifest(MavenVersionsResolver resolver, URL manifestUrl, String gavString) throws IOException { - final String[] splitGav = gavString.split(":"); - final MavenCoordinate gav = new MavenCoordinate(splitGav[0], splitGav[1], splitGav.length == 3 ? splitGav[2] : null); - - when(resolver.resolveChannelMetadata(eq(List.of(ChannelManifestCoordinate.create(null, gav))))) - .thenReturn(List.of(manifestUrl)); - } - -} \ No newline at end of file diff --git a/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java b/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java index 4a2d3935..938b03ee 100644 --- a/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java +++ b/core/src/test/java/org/wildfly/channel/mapping/ChannelTestCase.java @@ -18,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.InputStream; @@ -100,14 +99,4 @@ public void channelWithBlocklist() throws MalformedURLException { assertEquals("org.wildfly", blocklist.getGroupId()); assertEquals("1.2.3", blocklist.getVersion()); } - - @Test - public void channelWithGpgCheck() throws IOException { - ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - URL file = tccl.getResource("channels/channel-with-gpg-check.yaml"); - - Channel channel = ChannelMapper.from(file); - - assertTrue(channel.isGpgCheck()); - } } diff --git a/core/src/test/resources/channels/channel-with-gpg-check.yaml b/core/src/test/resources/channels/channel-with-gpg-check.yaml deleted file mode 100644 index da55f7c0..00000000 --- a/core/src/test/resources/channels/channel-with-gpg-check.yaml +++ /dev/null @@ -1,18 +0,0 @@ -schemaVersion: "2.1.0" -name: My Channel -description: |- - This is my channel - with my stuff -vendor: - name: My Vendor - support: community -gpg-check: true -manifest: - maven: - groupId: org.wildfly - artifactId: test-manifest - versionPattern: ".*" -repositories: - - id: test - url: http://test.te - diff --git a/doc/spec.adoc b/doc/spec.adoc index 949dca01..9ef8d093 100644 --- a/doc/spec.adoc +++ b/doc/spec.adoc @@ -5,7 +5,7 @@ [cols="1,1"] |=== -| Channel schema Version | 2.1.0 +| Channel schema Version | 2.0.0 | Manifest schema Version | 1.1.0 | Blocklist schema Version | 1.0.0 |=== @@ -65,7 +65,6 @@ A channel is composed of several fields: **** Mandatory `groupId` and `artifactId` elements that are the Maven coordinates of the manifest. **** Optional `version` to stick to a given manifest version (instead of requiring the latest version of that manifest). In the absence of this `version`, the latest version of the manifest will be determined based on the Maven repository metadata (see <>). *** `url` corresponding to a URL where the manifest file can be found. -*** `signature-url` corresponding to a URL where the signature of the manifest file can be found. * Optional `blocklist` corresponding to the Blocklist artifact. Blocklist is used to define versions of artifacts excluded from a channel. ** One of the following, mutually exclusive fields, used to resolve the Blocklist: *** `maven` corresponds to Maven coordinates of the Blocklist. It's composed of: @@ -77,8 +76,6 @@ A channel is composed of several fields: ** `maven-latest` - a version marked as `latest` in the Maven metadata ** `maven-release` - a version marked as `release` in the Maven metadata ** `none` - do not attempt to resolve versions of artifact not listed in the `streams` collection. Default value if no strategy is provided. -* Optional `gpg-check` boolean indicating that during artifact resolution, this channel will verify a signature of every artifact. If the artifact signature cannot be found, or cannot be validated, the artifact will not be resolved from the channel. The channel repositories must contain a detached GPG signature paired with the artifact as described below. -* Optional `gpg-urls` a list of URLs that the public GPG keys used to validate artifact signatures are resolved from. ### Manifest definition A Channel Manifest is composed of following fields: @@ -237,38 +234,12 @@ A blocklist applies only to the channel that defined it, not its required channe During artifact version resolution, a stream matching artifact GA is located in the channel. The blocklist is always checked for excluded versions, except when using `resolveDirectMavenArtifact` method. The excluded versions are removed from the set of available artifact versions before the latest remaining version matching the stream’s pattern is used to resolve the artifact. If the blocklist excludes all available artifact versions, `UnresolvedMavenArtifactException` is thrown. -### Verifying artifact PGP signatures - -If a channel sets value of `gpg-check` property to `true`, any artifact resolved from it (including the manifest itself) must have a valid GPG signature. - -#### Verifying Maven artifact signatures - -Maven artifacts are expected to have their detached GPG signatures available in the channel repositories. The detached artifacts must have the same GAV as the artifact, but append ".asc" suffix to the file extension. The signature file must contain an armoured GPG signature. - -The signature file is resolved at the same time as the artifact. If the signature is invalid, the public key is not found, the public key is expired or revoked, the artifact must be rejected and a SignatureException must be thrown. - -#### Verifying manifest signatures - -If the manifest of a channel is defined as a Maven GA(V), it is treated as any other maven artifact. If it is defined as an URL, the signature file must be available at the same URL with ".asc" suffix. Alternatively, a `signature-url` element can be used to provide a location of the signature. If the signature cannot be resolved, the channel creation must fail. - -#### Public keys - -The public keys are identified by the a keyID in hexadecimal form. The public keys required by the channel can be defined using `gpg-urls` property. Each `gpg-urls` must be a URL to a downloadable key. - -Implementations may provide additional means of providing keys - local stores, remote keyservers, etc. - ### Changelog -### Version 2.1.0 - -* Adding ability to verify artifact signatures -** Adding `gpg-check` and `gpg-urls` fields to the Channel definition. -** Adding `signature-url` field to the Channel's manifest coordinate definition. - ### Version 2.0.0 * Introduction of the Channel Manifest and Blocklist ### Version 1.0.0 -* Initial release of the Channel specification +* Initial release of the Channel specification \ No newline at end of file diff --git a/gpg-validator/pom.xml b/gpg-validator/pom.xml deleted file mode 100644 index 0dac107d..00000000 --- a/gpg-validator/pom.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - channel-parent - org.wildfly.channel - 1.2.2.Final-SNAPSHOT - - 4.0.0 - - gpg-validator - WildFly Channel - GPG Validator - - - - org.bouncycastle - bcpg-jdk18on - - - org.bouncycastle - bcprov-jdk18on - - - org.bouncycastle - bcutil-jdk18on - - - org.wildfly.channel - channel-core - - - org.pgpainless - pgpainless-core - test - - - - org.jboss.logging - jboss-logging - - - org.junit.jupiter - junit-jupiter - test - - - org.mockito - mockito-core - test - - - org.assertj - assertj-core - test - - - diff --git a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgKeystore.java b/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgKeystore.java deleted file mode 100644 index 8e1a9375..00000000 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgKeystore.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.gpg; - -import org.bouncycastle.openpgp.PGPPublicKey; - -import java.util.List; - -/** - * Local store of trusted public keys. - * - * Note: the keystore can reject a public key being added. In such case, the {@code GpgSignatureValidator} has to reject this key. - */ -public interface GpgKeystore { - - /** - * resolve a public key from the store. - * - * @param keyID - a HEX form of the key ID - * @return - the resolved public key or {@code null} if the key was not found - */ - PGPPublicKey get(String keyID); - - /** - * records the public keys in the store for future use. - * - * @param publicKey - list of trusted public keys - * @return true if the public keys have been added successfully - * false otherwise. - * @throws KeystoreOperationException if the keystore threw an error during the operation - */ - boolean add(List publicKey) throws KeystoreOperationException; -} diff --git a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidator.java b/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidator.java deleted file mode 100644 index 84d38174..00000000 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidator.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.gpg; - -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.math.BigInteger; -import java.net.URI; -import java.net.URLConnection; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Objects; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; -import org.jboss.logging.Logger; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ArtifactIdentifier; - -/** - * Implementation of a GPG signature validator. - * - * Uses a combination of a local {@link GpgKeystore} and {@code GPG keyservers} to resolve certificates. - * To resolve a public key required by the artifact signature: - *
      - *
    • check if the key is present in the local GpgKeystore.
    • - *
    • check if one of the configured remote keystores contains the key.
    • - *
    • try to download the keys linked in the {@code gpgUrls}
    • - *
    - * - * The {@code GpgKeystore} acts as a source of trusted keys. A new key, resolved from either the keyserver or - * the gpgUrls is added to the GpgKeystore and used in subsequent checks. - */ -public class GpgSignatureValidator implements SignatureValidator { - private static final Logger LOG = Logger.getLogger(GpgSignatureValidator.class); - private final GpgKeystore keystore; - private final Keyserver keyserver; - - private GpgSignatureValidatorListener listener = new NoopListener(); - - public GpgSignatureValidator(GpgKeystore keystore) { - this(keystore, new Keyserver(Collections.emptyList())); - } - - public GpgSignatureValidator(GpgKeystore keystore, Keyserver keyserver) { - this.keystore = keystore; - this.keyserver = keyserver; - } - - public void addListener(GpgSignatureValidatorListener listener) { - this.listener = listener; - } - - @Override - public SignatureResult validateSignature(ArtifactIdentifier artifactId, InputStream artifactStream, - InputStream signatureStream, List gpgUrls) throws SignatureException { - Objects.requireNonNull(artifactId); - Objects.requireNonNull(artifactStream); - Objects.requireNonNull(signatureStream); - - final PGPSignature pgpSignature; - try { - if (LOG.isTraceEnabled()) { - LOG.trace("Reading the signature of artifact."); - } - pgpSignature = readSignatureFile(signatureStream); - } catch (IOException e) { - throw new SignatureException("Could not find signature in provided signature file", e, - SignatureResult.noSignature(artifactId)); - } - - if (pgpSignature == null) { - LOG.error("Could not read the signature in provided signature file"); - return SignatureResult.noSignature(artifactId); - } - - final String keyID = getKeyID(pgpSignature); - if (LOG.isTraceEnabled()) { - LOG.tracef("The signature was created using public key %s.", keyID); - } - - final PGPPublicKey publicKey; - if (keystore.get(keyID) != null) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Using a public key %s was found in the local keystore.", keyID); - } - publicKey = keystore.get(keyID); - } else { - if (LOG.isTraceEnabled()) { - LOG.tracef("Trying to download a public key %s from remote keyservers.", keyID); - } - List pgpPublicKeys = null; - PGPPublicKey key = null; - try { - final PGPPublicKeyRing keyRing = keyserver.downloadKey(keyID); - if (keyRing != null) { - final Iterator publicKeys = keyRing.getPublicKeys(); - key = keyRing.getPublicKey(new BigInteger(keyID, 16).longValue()); - pgpPublicKeys = new ArrayList<>(); - while (publicKeys.hasNext()) { - pgpPublicKeys.add(publicKeys.next()); - } - } - } catch (PGPException | IOException e) { - throw new SignatureException("Unable to parse the certificate downloaded from keyserver", e, - SignatureResult.noMatchingCertificate(artifactId, keyID)); - } - - if (key == null) { - for (String gpgUrl : gpgUrls) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Trying to download a public key %s from channel defined URL %s.", keyID, gpgUrl); - } - try { - pgpPublicKeys = downloadPublicKey(gpgUrl); - } catch (IOException e) { - throw new SignatureException("Unable to parse the certificate downloaded from " + gpgUrl, e, - SignatureResult.noMatchingCertificate(artifactId, keyID)); - } - if (pgpPublicKeys.stream().anyMatch(k -> k.getKeyID() == pgpSignature.getKeyID())) { - key = pgpPublicKeys.stream().filter(k -> k.getKeyID() == pgpSignature.getKeyID()).findFirst().get(); - break; - } - } - - if (key == null) { - if (LOG.isTraceEnabled()) { - LOG.tracef("A public key %s not found in the channel defined URLs.", keyID); - } - return SignatureResult.noMatchingCertificate(artifactId, keyID); - } - } - - - if (keystore.add(pgpPublicKeys)) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Adding a public key %s to the local keystore.", keyID); - } - publicKey = key; - } else { - return SignatureResult.noMatchingCertificate(artifactId, keyID); - } - } - - if (LOG.isTraceEnabled()) { - LOG.tracef("Checking if the public key %s is still valid.", artifactId); - } - SignatureResult res = checkRevoked(artifactId, keyID, publicKey); - if (res.getResult() != SignatureResult.Result.OK) { - return res; - } - - res = checkExpired(artifactId, publicKey, keyID); - if (res.getResult() != SignatureResult.Result.OK) { - return res; - } - - if (LOG.isTraceEnabled()) { - LOG.tracef("Verifying that artifact %s has been signed with public key %s.", artifactId, keyID); - } - try { - pgpSignature.init(new BcPGPContentVerifierBuilderProvider(), publicKey); - } catch (PGPException e) { - throw new SignatureException("Unable to verify the signature using key " + keyID, e, - SignatureResult.invalid(artifactId, keyID)); - } - final SignatureResult result = verifyFile(artifactId, artifactStream, pgpSignature); - - if (result.getResult() == SignatureResult.Result.OK) { - listener.artifactSignatureCorrect(artifactId, publicKey); - } else { - listener.artifactSignatureInvalid(artifactId, publicKey); - } - - return result; - } - - private static SignatureResult checkExpired(ArtifactIdentifier artifactId, PGPPublicKey publicKey, String keyID) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Checking if public key %s is not expired.", keyID); - } - if (publicKey.getValidSeconds() > 0) { - final Instant expiry = Instant.from(publicKey.getCreationTime().toInstant().plus(publicKey.getValidSeconds(), ChronoUnit.SECONDS)); - if (LOG.isTraceEnabled()) { - LOG.tracef("Public key %s expirates on %s.", keyID, expiry); - } - if (expiry.isBefore(Instant.now())) { - return SignatureResult.expired(artifactId, keyID); - } - } else { - if (LOG.isTraceEnabled()) { - LOG.tracef("Public key %s has no expiration.", keyID); - } - } - return SignatureResult.ok(); - } - - private SignatureResult checkRevoked(ArtifactIdentifier artifactId, String keyID, PGPPublicKey publicKey) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Checking if public key %s has been revoked.", keyID); - } - - if (publicKey.hasRevocation()) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Public key %s has been revoked.", keyID); - } - return SignatureResult.revoked(artifactId, keyID, getRevocationReason(publicKey)); - } - - final Iterator subKeys = publicKey.getSignaturesOfType(PGPSignature.SUBKEY_BINDING); - while (subKeys.hasNext()) { - final PGPSignature subKeySignature = subKeys.next(); - final PGPPublicKey subKey = keystore.get(getKeyID(subKeySignature)); - if (subKey.hasRevocation()) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Sub-key %s has been revoked.", Long.toHexString(subKey.getKeyID()).toUpperCase(Locale.ROOT)); - } - return SignatureResult.revoked(artifactId, keyID, getRevocationReason(publicKey)); - } - } - return SignatureResult.ok(); - } - - private static String getRevocationReason(PGPPublicKey publicKey) { - Iterator keySignatures = publicKey.getSignaturesOfType(PGPSignature.KEY_REVOCATION); - String revocationDescription = null; - while (keySignatures.hasNext()) { - final PGPSignature sign = keySignatures.next(); - if (sign.getSignatureType() == PGPSignature.KEY_REVOCATION) { - final PGPSignatureSubpacketVector hashedSubPackets = sign.getHashedSubPackets(); - revocationDescription = hashedSubPackets.getRevocationReason().getRevocationDescription(); - } - } - return revocationDescription; - } - - private static SignatureResult verifyFile(ArtifactIdentifier artifactSource, InputStream artifactStream, PGPSignature pgpSignature) throws SignatureException { - // Read file to verify - byte[] data = new byte[1024]; - InputStream inputStream = null; - try { - inputStream = new DataInputStream(new BufferedInputStream(artifactStream)); - while (true) { - int bytesRead = inputStream.read(data, 0, 1024); - if (bytesRead == -1) - break; - pgpSignature.update(data, 0, bytesRead); - } - inputStream.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - - // Verify the signature - try { - if (!pgpSignature.verify()) { - return SignatureResult.invalid(artifactSource, getKeyID(pgpSignature)); - } else { - return SignatureResult.ok(); - } - } catch (PGPException e) { - throw new SignatureException("Unable to verify the file signature", e, - SignatureResult.invalid(artifactSource, getKeyID(pgpSignature))); - } - } - - private static String getKeyID(PGPSignature pgpSignature) { - return Long.toHexString(pgpSignature.getKeyID()).toUpperCase(Locale.ROOT); - } - - private static PGPSignature readSignatureFile(InputStream signatureStream) throws IOException { - PGPSignature pgpSignature = null; - try (InputStream decoderStream = PGPUtil.getDecoderStream(signatureStream)) { - final PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(decoderStream, new JcaKeyFingerprintCalculator()); - Object o = pgpObjectFactory.nextObject(); - if (o instanceof PGPSignatureList) { - PGPSignatureList signatureList = (PGPSignatureList) o; - if (signatureList.isEmpty()) { - throw new RuntimeException("signatureList must not be empty"); - } - pgpSignature = signatureList.get(0); - } else if (o instanceof PGPSignature) { - pgpSignature = (PGPSignature) o; - } - } - return pgpSignature; - } - - private static List downloadPublicKey(String signatureUrl) throws IOException { - final URI uri = URI.create(signatureUrl); - final InputStream inputStream; - if (uri.getScheme().equals("classpath")) { - if (LOG.isTraceEnabled()) { - LOG.tracef("Resolving the public key from classpath %s.", uri); - } - final String keyPath = uri.getSchemeSpecificPart(); - inputStream = GpgSignatureValidator.class.getClassLoader().getResourceAsStream(keyPath); - } else { - if (LOG.isTraceEnabled()) { - LOG.tracef("Downloading the public key from %s.", uri); - } - final URLConnection urlConnection = uri.toURL().openConnection(); - urlConnection.connect(); - inputStream = urlConnection.getInputStream(); - } - try (InputStream decoderStream = new ArmoredInputStream(inputStream)) { - final PGPPublicKeyRing pgpPublicKeys = new PGPPublicKeyRing(decoderStream, new JcaKeyFingerprintCalculator()); - final ArrayList res = new ArrayList<>(); - final Iterator publicKeys = pgpPublicKeys.getPublicKeys(); - while (publicKeys.hasNext()) { - res.add(publicKeys.next()); - } - return res; - } - } - - private static class NoopListener implements GpgSignatureValidatorListener { - - @Override - public void artifactSignatureCorrect(ArtifactIdentifier artifact, PGPPublicKey publicKey) { - // noop - } - - @Override - public void artifactSignatureInvalid(ArtifactIdentifier artifact, PGPPublicKey publicKey) { - // noop - } - } -} diff --git a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidatorListener.java b/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidatorListener.java deleted file mode 100644 index 61167f3c..00000000 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidatorListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.gpg; - -import org.bouncycastle.openpgp.PGPPublicKey; -import org.wildfly.channel.spi.ArtifactIdentifier; - -/** - * Validation callbacks used for example for additional logging. - */ -public interface GpgSignatureValidatorListener { - - /** - * Called when and artifact signature was successfully verified. - * - * @param artifact - the ID of the artifact being verified - * @param publicKey - the public key used to verify the artifact - */ - void artifactSignatureCorrect(ArtifactIdentifier artifact, PGPPublicKey publicKey); - - /** - * Called when and artifact signature was found to be invalid. - * - * @param artifact - the ID of the artifact being verified - * @param publicKey - the public key used to verify the artifact - */ - void artifactSignatureInvalid(ArtifactIdentifier artifact, PGPPublicKey publicKey); -} diff --git a/gpg-validator/src/main/java/org/wildfly/channel/gpg/Keyserver.java b/gpg-validator/src/main/java/org/wildfly/channel/gpg/Keyserver.java deleted file mode 100644 index e2fc4fe7..00000000 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/Keyserver.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.gpg; - -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; - -import java.io.IOException; -import java.io.InputStream; -import java.math.BigInteger; -import java.net.URI; -import java.net.URL; -import java.util.List; - -/** - * Retrieves a public key from a remote GPG keyserver using a PKS query - */ -public class Keyserver { - - private static final String LOOKUP_PATH = "/pks/lookup"; - - private final List servers; - - public Keyserver(List serverUrls) { - this.servers = serverUrls; - } - - /** - * download a public key matching the {@code keyID} from one of defined GPG keyservers - * - * @param keyID - hex representation of a GPG public key - * @return - the public key associated with the {@code keyID} or null if not found - * @throws PGPException - * @throws IOException - */ - public PGPPublicKeyRing downloadKey(String keyID) throws PGPException, IOException { - for (URL server : servers) { - final PGPPublicKeyRing publicKey = tryDownloadKey(server, keyID); - if (publicKey != null) { - return publicKey; - } - } - return null; - } - - private PGPPublicKeyRing tryDownloadKey(URL serverUrl, String keyID) throws IOException, PGPException { - final String protocol; - if (serverUrl.getProtocol().equals("hkps")) { - protocol = "https"; - } else if (serverUrl.getProtocol().equals("hkp")) { - protocol = "http"; - } else { - protocol = serverUrl.getProtocol(); - } - - final String host = serverUrl.getHost(); - final int port = serverUrl.getPort(); - final String path = serverUrl.getPath(); - - final URI keyUri = URI.create(protocol + "://" + host + ":" + port + "/" + path + LOOKUP_PATH + "?" + getQueryStringForGetKey(keyID)); - - final HttpUriRequest request = new HttpGet(keyUri); - - try (final CloseableHttpClient client = HttpClientBuilder.create().build(); - final CloseableHttpResponse response = client.execute(request)) { - if (response.getStatusLine().getStatusCode() == 200) { - - final HttpEntity responseEntity = response.getEntity(); - try (InputStream inputStream = responseEntity.getContent()) { - final InputStream keyIn = PGPUtil.getDecoderStream(inputStream); - final PGPPublicKeyRingCollection pgpRing = new PGPPublicKeyRingCollection(keyIn, new BcKeyFingerprintCalculator()); - final BigInteger bi = new BigInteger(keyID, 16); - return pgpRing.getPublicKeyRing(bi.longValue()); - } - } else { - return null; - } - } - } - - private static String getQueryStringForGetKey(String keyID) { - return String.format("op=get&options=mr&search=0x%s", keyID); - } -} diff --git a/gpg-validator/src/main/java/org/wildfly/channel/gpg/KeystoreOperationException.java b/gpg-validator/src/main/java/org/wildfly/channel/gpg/KeystoreOperationException.java deleted file mode 100644 index c052bb7d..00000000 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/KeystoreOperationException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.gpg; - -public class KeystoreOperationException extends RuntimeException { - public KeystoreOperationException() { - } - - public KeystoreOperationException(String message) { - super(message); - } - - public KeystoreOperationException(String message, Throwable cause) { - super(message, cause); - } - - public KeystoreOperationException(Throwable cause) { - super(cause); - } -} diff --git a/gpg-validator/src/test/java/org/wildfly/channel/gpg/GpgSignatureValidatorTest.java b/gpg-validator/src/test/java/org/wildfly/channel/gpg/GpgSignatureValidatorTest.java deleted file mode 100644 index 8d1dabda..00000000 --- a/gpg-validator/src/test/java/org/wildfly/channel/gpg/GpgSignatureValidatorTest.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2024 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.wildfly.channel.gpg; - -import org.assertj.core.api.Assertions; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.protection.UnprotectedKeysProtector; -import org.pgpainless.key.util.RevocationAttributes; -import org.wildfly.channel.ArtifactCoordinate; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ArtifactIdentifier; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -public class GpgSignatureValidatorTest { - @TempDir - Path tempDir; - - private PGPSecretKeyRing pgpValidKeys; - private PGPSecretKeyRing pgpAttackerKeys; - private PGPSecretKeyRing pgpExpiredKeys; - private TestKeystore keystore = new TestKeystore(); - private GpgSignatureValidator validator; - private ArtifactIdentifier.MavenResource anArtifact; - private InputStream artifactInputStream; - private File artifactFile; - private InputStream signatureInputStream; - private File signatureFile; - - @BeforeEach - public void setUp() throws Exception { - pgpValidKeys = PGPainless.generateKeyRing().simpleRsaKeyRing("Test ", RsaLength._4096); - pgpAttackerKeys = PGPainless.generateKeyRing().simpleRsaKeyRing("Fake ", RsaLength._4096); - pgpExpiredKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .addUserId("Test ") - .setExpirationDate(new Date(System.currentTimeMillis()+1_000)) - .build(); - - keystore = new TestKeystore(); - validator = new GpgSignatureValidator(keystore); - - this.artifactFile = tempDir.resolve("test-one.jar").toFile(); - Files.writeString(artifactFile.toPath(), "test"); - this.artifactInputStream = new FileInputStream(artifactFile); - anArtifact = new ArtifactIdentifier.MavenResource("org.test", "test-one", "jar", null, "1.0.0"); - - this.signatureFile = signFile(artifactFile, pgpValidKeys); - this.signatureInputStream = new FileInputStream(signatureFile); - } - - @Test - public void validSignatureIsAccepted() throws Exception { - keystore.using(pgpValidKeys); - - Assertions.assertThat(validator.validateSignature(anArtifact, artifactInputStream, signatureInputStream, Collections.emptyList())) - .hasFieldOrPropertyWithValue("result", SignatureResult.Result.OK); - } - - @Test - public void invalidSignatureReturnsErrorStatus() throws Exception { - keystore.using(pgpValidKeys); - - final File signatureFile = signFile(artifactFile, pgpAttackerKeys); - - Assertions.assertThat(validator.validateSignature(anArtifact, artifactInputStream, signatureInputStream, Collections.emptyList())) - .extracting( - SignatureResult::getResult, - SignatureResult::getResource) - .containsExactly( - SignatureResult.Result.NO_MATCHING_CERT, - anArtifact); - } - - @Test - public void expiredSignatureReturnsError() throws Exception { - keystore.using(pgpExpiredKeys); - - // the certificate has to have an expiry date at least now()+1 second, otherwise it's treated as never-expiring - // wait for certificate to expire - while (!isExpired(pgpExpiredKeys.getPublicKey())) { - Thread.sleep(100); - } - - final File signatureFile = signFile(artifactFile, pgpExpiredKeys); - - Assertions.assertThat(validator.validateSignature(anArtifact, artifactInputStream, new FileInputStream(signatureFile), Collections.emptyList())) - .extracting( - SignatureResult::getResult, - SignatureResult::getResource, - SignatureResult::getKeyId) - .containsExactly( - SignatureResult.Result.EXPIRED, - anArtifact, - toHex(pgpExpiredKeys.getPublicKey().getKeyID())); - } - - @Test - public void revokedSignatureReturnsError() throws Exception { - // order of operations matter! sign artifact, revoke the key, init the keystore - final PGPSecretKeyRing pgpExpiredKeys = PGPainless.modifyKeyRing(pgpValidKeys) - .revoke(new UnprotectedKeysProtector(), - RevocationAttributes - .createKeyRevocation() - .withReason(RevocationAttributes.Reason.KEY_COMPROMISED) - .withDescription("The key is revoked")) - .done(); - keystore.using(pgpExpiredKeys); - - Assertions.assertThat(validator.validateSignature(anArtifact, artifactInputStream, signatureInputStream, Collections.emptyList())) - .extracting( - SignatureResult::getResult, - SignatureResult::getResource, - SignatureResult::getKeyId, - SignatureResult::getMessage) - .containsExactly( - SignatureResult.Result.REVOKED, - anArtifact, - toHex(pgpValidKeys.getPublicKey().getKeyID()), - "The key is revoked"); - } - - @Test - public void downloadsSignatureIfUrlIsProvided() throws Exception { - keystore.using(Collections.emptyList()); - - // export the public certificate - final File publicCertFile = exportPublicCertificate(pgpValidKeys); - - Assertions.assertThat(validator.validateSignature(anArtifact, artifactInputStream, signatureInputStream, List.of(publicCertFile.toURI().toString()))) - .extracting(SignatureResult::getResult) - .isEqualTo(SignatureResult.Result.OK); - - Assertions.assertThat(keystore.getKeys().keySet()) - .containsOnly(toHex(pgpValidKeys.getPublicKey().getKeyID())); - } - - @Test - public void failedSignatureDownloadThrowsException() throws Exception { - keystore.using(Collections.emptyList()); - - // export the public certificate - final File publicCertFile = tempDir.resolve("public.crt").toFile(); - Files.writeString(publicCertFile.toPath(), "I'm not a certificate"); - final String certUrl = publicCertFile.toURI().toString(); - - Assertions.assertThatThrownBy(()->validator.validateSignature(anArtifact, artifactInputStream, signatureInputStream, List.of(certUrl))) - .isInstanceOf(SignatureValidator.SignatureException.class) - .hasMessageContainingAll("Unable to parse the certificate downloaded from " + certUrl); - } - - @Test - public void invalidSignatureDownloadedReturnsError() throws Exception { - keystore.using(Collections.emptyList()); - - final File signatureFile = signFile(artifactFile, pgpAttackerKeys); - - // export the public certificate - final File publicCertFile = exportPublicCertificate(pgpValidKeys); - - Assertions.assertThat(validator.validateSignature(anArtifact, artifactInputStream, - new FileInputStream(signatureFile), List.of(publicCertFile.toURI().toString()))) - .hasFieldOrPropertyWithValue("result", SignatureResult.Result.NO_MATCHING_CERT) - .extracting( - SignatureResult::getResult, - SignatureResult::getResource, - SignatureResult::getKeyId) - .containsExactly( - SignatureResult.Result.NO_MATCHING_CERT, - anArtifact, - toHex(pgpAttackerKeys.getPublicKey().getKeyID())); - - // no certificates should have been imported - Assertions.assertThat(keystore.getKeys().keySet()) - .isEmpty(); - } - - @Test - public void keystoreRejectingCertificateReturnsError() throws Exception { - final GpgKeystore rejectingKeystore = Mockito.mock(GpgKeystore.class); - Mockito.when(rejectingKeystore.add(Mockito.anyList())).thenReturn(false); - validator = new GpgSignatureValidator(rejectingKeystore); - - final File publicCertFile = exportPublicCertificate(pgpValidKeys); - - Assertions.assertThat(validator.validateSignature(anArtifact, artifactInputStream, signatureInputStream, - List.of(publicCertFile.toURI().toString()))) - .extracting( - SignatureResult::getResult, - SignatureResult::getResource, - SignatureResult::getKeyId) - .containsExactly( - SignatureResult.Result.NO_MATCHING_CERT, - anArtifact, - toHex(pgpValidKeys.getPublicKey().getKeyID())); - } - - private ArtifactCoordinate toCoord() { - return new ArtifactCoordinate(anArtifact.getGroupId(), anArtifact.getArtifactId(), anArtifact.getExtension(), - anArtifact.getClassifier(), anArtifact.getVersion()); - } - - private File exportPublicCertificate(PGPSecretKeyRing keyRing) throws IOException { - // export the public certificate - final File publicCertFile = tempDir.resolve("public.crt").toFile(); - try (ArmoredOutputStream outStream = new ArmoredOutputStream(new FileOutputStream(publicCertFile))) { - keyRing.getPublicKey().encode(outStream); - } - return publicCertFile; - } - - private boolean isExpired(PGPPublicKey publicKey) { - if (publicKey.getValidSeconds() == 0) { - return false; - } else { - final Instant expiry = Instant.from(publicKey.getCreationTime().toInstant().plus(publicKey.getValidSeconds(), ChronoUnit.SECONDS)); - return expiry.isBefore(Instant.now()); - } - } - - private File signFile(File file, PGPSecretKeyRing pgpSecretKeys) throws PGPException, IOException { - final SigningOptions signOptions = SigningOptions.get() - .addDetachedSignature(new UnprotectedKeysProtector(), pgpSecretKeys); - - final File signatureFile = tempDir.resolve("test-one.jar.asc").toFile(); - final EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(new FileOutputStream(signatureFile)) - .withOptions(ProducerOptions.sign(signOptions)); - - Streams.pipeAll(new FileInputStream(file), encryptionStream); // pipe the data through - encryptionStream.close(); - - // wrap signature in armour - try(FileOutputStream fos = new FileOutputStream(signatureFile); - final ArmoredOutputStream aos = new ArmoredOutputStream(fos)) { - for (SubkeyIdentifier subkeyIdentifier : encryptionStream.getResult().getDetachedSignatures().keySet()) { - final Set pgpSignatures = encryptionStream.getResult().getDetachedSignatures().get(subkeyIdentifier); - for (PGPSignature pgpSignature : pgpSignatures) { - pgpSignature.encode(aos); - } - } - } - return signatureFile; - } - - private static class TestKeystore implements GpgKeystore { - - private final HashMap keys = new HashMap<>(); - - TestKeystore() { - - } - - public void using(PGPSecretKeyRing pgpSecretKeys) { - this.using(PGPainless.extractCertificate(pgpSecretKeys)); - } - - void using(PGPPublicKeyRing pgpPublicKeys) { - keys.clear(); - - final Iterator publicKeys = pgpPublicKeys.getPublicKeys(); - while (publicKeys.hasNext()) { - final PGPPublicKey key = publicKeys.next(); - keys.put(toHex(key.getKeyID()), key); - } - } - - public void using(List publicKeys) { - keys.clear(); - - for (PGPPublicKey key : publicKeys) { - keys.put(toHex(key.getKeyID()), key); - } - } - - public HashMap getKeys() { - return keys; - } - - @Override - public PGPPublicKey get(String keyID) { - return keys.get(keyID); - } - - @Override - public boolean add(List publicKeys) { - for (PGPPublicKey key : publicKeys) { - keys.put(toHex(key.getKeyID()), key); - } - return true; - } - } - - private static String toHex(long keyID) { - return Long.toHexString(keyID).toUpperCase(Locale.ROOT); - } - -} \ No newline at end of file diff --git a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java index 17c8d7cd..88555df0 100644 --- a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java +++ b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java @@ -26,6 +26,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -60,10 +62,8 @@ import org.wildfly.channel.ChannelMetadataCoordinate; import org.wildfly.channel.Repository; import org.wildfly.channel.NoStreamFoundException; -import org.wildfly.channel.SignedVersionResolverWrapper; import org.wildfly.channel.UnresolvedMavenArtifactException; import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureValidator; import org.wildfly.channel.version.VersionMatcher; import org.jboss.logging.Logger; @@ -91,44 +91,27 @@ public class VersionResolverFactory implements MavenVersionsResolver.Factory { private final RepositorySystem system; private final RepositorySystemSession session; private final Function repositoryFactory; - private SignatureValidator signatureValidator; public VersionResolverFactory(RepositorySystem system, RepositorySystemSession session) { - this(system, session, null); + this(system, session, DEFAULT_REPOSITORY_MAPPER); } - public VersionResolverFactory(RepositorySystem system, RepositorySystemSession session, - SignatureValidator signatureValidator) { - this(system, session, signatureValidator, DEFAULT_REPOSITORY_MAPPER); - } - - public VersionResolverFactory(RepositorySystem system, - RepositorySystemSession session, - SignatureValidator signatureValidator, Function repositoryFactory) { this.system = system; this.session = session; - this.signatureValidator = signatureValidator; this.repositoryFactory = repositoryFactory; } @Override - public MavenVersionsResolver create(Channel channel) { - Objects.requireNonNull(channel); + public MavenVersionsResolver create(Collection repositories) { + Objects.requireNonNull(repositories); - final List mvnRepositories = channel.getRepositories().stream() + final List mvnRepositories = repositories.stream() .map(repositoryFactory::apply) .collect(Collectors.toList()); - if (channel.isGpgCheck()) { - if (signatureValidator == null) { - throw new RuntimeException("The channel %s requires GPG verification, but the signature validator is not configured."); - } - return new SignedVersionResolverWrapper(create(mvnRepositories), channel.getRepositories(), signatureValidator, channel.getGpgUrls()); - } else { - return create(mvnRepositories); - } + return create(mvnRepositories); } private MavenResolverImpl create(List mvnRepositories) { @@ -190,8 +173,7 @@ public Set getAllVersions(String groupId, String artifactId, String exte } @Override - public File resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) - throws ArtifactTransferException, SignatureValidator.SignatureException { + public File resolveArtifact(String groupId, String artifactId, String extension, String classifier, String version) throws ArtifactTransferException { requireNonNull(groupId); requireNonNull(artifactId); requireNonNull(version); diff --git a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java index 0ab75b4b..0793f9e5 100644 --- a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java +++ b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java @@ -88,7 +88,7 @@ public void testResolverGetAllVersions() throws VersionRangeResolutionException when(system.resolveVersionRange(eq(session), any(VersionRangeRequest.class))).thenReturn(versionRangeResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel.Builder().addRepository(testRepository.getId(), testRepository.getUrl()).build()); + MavenVersionsResolver resolver = factory.create(Collections.singletonList(testRepository)); Set allVersions = resolver.getAllVersions("org.foo", "bar", null, null); assertEquals(3, allVersions.size()); @@ -111,7 +111,7 @@ public void testResolverResolveArtifact() throws ArtifactResolutionException { when(system.resolveArtifact(eq(session), any(ArtifactRequest.class))).thenReturn(artifactResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); File resolvedArtifact = resolver.resolveArtifact("org.foo", "bar", null, null, "1.0.0"); assertEquals(artifactFile, resolvedArtifact); @@ -125,7 +125,7 @@ public void testResolverCanNotResolveArtifact() throws ArtifactResolutionExcepti when(system.resolveArtifact(eq(session), any(ArtifactRequest.class))).thenThrow(ArtifactResolutionException.class); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { resolver.resolveArtifact("org.foo", "does-not-exist", null, null, "1.0.0"); @@ -179,7 +179,7 @@ public void testResolverResolveAllArtifacts() throws ArtifactResolutionException when(system.resolveArtifacts(eq(session), any(List.class))).thenReturn(Arrays.asList(artifactResult1, artifactResult2)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); final List coordinates = asList( new ArtifactCoordinate("org.foo", "bar", null, null, "1.0.0"), @@ -197,7 +197,7 @@ public void testResolverResolveMetadataUsingUrl() throws ArtifactResolutionExcep RepositorySystemSession session = mock(RepositorySystemSession.class); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate(new URL("http://test.channel")))); assertEquals(new URL("http://test.channel"), resolvedURL.get(0)); @@ -231,7 +231,7 @@ public void testResolverResolveMetadataUsingGa() throws ArtifactResolutionExcept when(system.resolveArtifact(eq(session), artifactRequestArgumentCaptor.capture())).thenReturn(artifactResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.singletonList(testRepository)); List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate("org.test", "channel"))); assertEquals(artifactFile.toURI().toURL(), resolvedURL.get(0)); @@ -253,7 +253,7 @@ public void testResolverResolveMetadataUsingGav() throws ArtifactResolutionExcep when(system.resolveArtifact(eq(session), artifactRequestArgumentCaptor.capture())).thenReturn(artifactResult); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); List resolvedURL = resolver.resolveChannelMetadata(List.of(new ChannelCoordinate("org.test", "channel", "1.0.0"))); assertEquals(artifactFile.toURI().toURL(), resolvedURL.get(0)); @@ -265,11 +265,9 @@ public void testRepositoryFactory() throws Exception { RepositorySystem system = mock(RepositorySystem.class); RepositorySystemSession session = mock(RepositorySystemSession.class); - VersionResolverFactory factory = new VersionResolverFactory(system, session, null, + VersionResolverFactory factory = new VersionResolverFactory(system, session, r -> new RemoteRepository.Builder(r.getId(), "default", r.getUrl() + ".new").build()); - MavenVersionsResolver resolver = factory.create(new Channel.Builder() - .addRepository("test_1", "http://test_1") - .build()); + MavenVersionsResolver resolver = factory.create(List.of(new Repository("test_1", "http://test_1"))); File artifactFile = new File("test"); ArtifactResult artifactResult = new ArtifactResult(new ArtifactRequest()); @@ -314,7 +312,7 @@ public void testResolveLatestFromMetadata() throws Exception { when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result1, result2)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); final String res = resolver.getMetadataLatestVersion("org.foo", "bar"); @@ -334,7 +332,7 @@ public void testResolveLatestFromMetadataNoVersioning() throws Exception { when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { resolver.getMetadataLatestVersion("org.foo", "bar"); @@ -350,7 +348,7 @@ public void testResolveLatestFromMetadataNoLatestVersion() throws Exception { when(system.resolveMetadata(eq(session), any())).thenReturn(List.of(result)); VersionResolverFactory factory = new VersionResolverFactory(system, session); - MavenVersionsResolver resolver = factory.create(new Channel()); + MavenVersionsResolver resolver = factory.create(Collections.emptyList()); Assertions.assertThrows(UnresolvedMavenArtifactException.class, () -> { resolver.getMetadataLatestVersion("org.foo", "bar"); diff --git a/pom.xml b/pom.xml index 38b1ee7e..190d2f4b 100644 --- a/pom.xml +++ b/pom.xml @@ -31,12 +31,10 @@ 3.6.1.Final 3.17.0 4.5.14 - 1.78.1 0.19.1 5.14.2 3.10.0 3.26.3 - 1.6.7 3.1.1 1.9.22 3.9.9 @@ -80,21 +78,6 @@ maven-repository-metadata ${version.maven.repository.metadata} - - org.bouncycastle - bcpg-jdk18on - ${version.org.bouncycastle} - - - org.bouncycastle - bcprov-jdk18on - ${version.org.bouncycastle} - - - org.bouncycastle - bcutil-jdk18on - ${version.org.bouncycastle} - org.jboss.logging jboss-logging @@ -130,15 +113,8 @@ org.assertj assertj-core - test ${version.org.assertj} - - org.pgpainless - pgpainless-core - ${version.org.pgpainless} - test - org.wildfly.channel @@ -156,7 +132,6 @@ core maven-resolver - gpg-validator