From ba523d7d5a01b461797a2b2e8f5d7d7298bcfb6d Mon Sep 17 00:00:00 2001 From: Bartosz Spyrko-Smietanko Date: Fri, 2 Aug 2024 12:41:38 +0200 Subject: [PATCH] Update Javadocs --- .../java/org/wildfly/channel/ChannelImpl.java | 11 +- .../org/wildfly/channel/ChannelSession.java | 11 +- ...nResource.java => ArtifactIdentifier.java} | 11 +- .../wildfly/channel/spi/SignatureResult.java | 28 ++- .../channel/spi/SignatureValidator.java | 16 +- .../channel/ChannelSessionInitTestCase.java | 5 +- .../channel/ChannelSessionTestCase.java | 3 - ...essionWithSignatureValidationTestCase.java | 24 ++- .../org/wildfly/channel/gpg/GpgKeystore.java | 18 ++ .../channel/gpg/GpgSignatureValidator.java | 186 ++++++++++++------ .../gpg/GpgSignatureValidatorListener.java | 42 ++++ .../org/wildfly/channel/gpg/Keyserver.java | 11 ++ .../gpg/GpgSignatureValidatorTest.java | 6 +- 13 files changed, 280 insertions(+), 92 deletions(-) rename core/src/main/java/org/wildfly/channel/spi/{ValidationResource.java => ArtifactIdentifier.java} (83%) create mode 100644 gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidatorListener.java diff --git a/core/src/main/java/org/wildfly/channel/ChannelImpl.java b/core/src/main/java/org/wildfly/channel/ChannelImpl.java index 39e00322..58ba360c 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelImpl.java +++ b/core/src/main/java/org/wildfly/channel/ChannelImpl.java @@ -39,7 +39,7 @@ import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.spi.SignatureResult; import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ValidationResource; +import org.wildfly.channel.spi.ArtifactIdentifier; import org.wildfly.channel.version.VersionMatcher; /** @@ -77,6 +77,7 @@ public ChannelImpl(Channel channelDefinition) { * * @param factory * @param channels + * @param signatureValidator - the validator used to check the signatures of resolved artifacts * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels * @throws CyclicDependencyException - if the required manifests form a cyclic dependency */ @@ -407,7 +408,7 @@ ResolveArtifactResult resolveArtifact(String groupId, String artifactId, String private void validateGpgSignature(String groupId, String artifactId, String extension, String classifier, String version, File artifact) { - final ValidationResource mavenArtifact = new ValidationResource.MavenResource(groupId, artifactId, extension, + final ArtifactIdentifier mavenArtifact = new ArtifactIdentifier.MavenResource(groupId, artifactId, extension, classifier, version); try { final File signature = resolver.resolveArtifact(groupId, artifactId, extension + SIGNATURE_FILE_SUFFIX, @@ -426,7 +427,7 @@ mavenArtifact, new FileInputStream(artifact), new FileInputStream(signature), private void validateGpgSignature(URL artifactFile, URL signature) throws IOException { final SignatureResult signatureResult = signatureValidator.validateSignature( - new ValidationResource.UrlResource(artifactFile), + new ArtifactIdentifier.UrlResource(artifactFile), artifactFile.openStream(), signature.openStream(), channelDefinition.getGpgUrls() ); @@ -448,7 +449,7 @@ List resolveArtifacts(List coordinate for (int i = 0; i < resolvedArtifacts.size(); i++) { final File artifact = resolvedArtifacts.get(i); final ArtifactCoordinate c = coordinates.get(i); - final ValidationResource.MavenResource mavenArtifact = new ValidationResource.MavenResource(c.getGroupId(), c.getArtifactId(), + final ArtifactIdentifier.MavenResource mavenArtifact = new ArtifactIdentifier.MavenResource(c.getGroupId(), c.getArtifactId(), c.getExtension(), c.getClassifier(), c.getVersion()); final File signature = signatures.get(i); try { @@ -464,7 +465,7 @@ List resolveArtifacts(List coordinate } } } catch (ArtifactTransferException e) { - final ValidationResource.MavenResource artifact = new ValidationResource.MavenResource(e.getUnresolvedArtifacts().stream().findFirst().get()); + 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)); diff --git a/core/src/main/java/org/wildfly/channel/ChannelSession.java b/core/src/main/java/org/wildfly/channel/ChannelSession.java index 552d9b81..fb2b3046 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelSession.java +++ b/core/src/main/java/org/wildfly/channel/ChannelSession.java @@ -53,7 +53,7 @@ public class ChannelSession implements AutoCloseable { private final int versionResolutionParallelism; /** - * Create a ChannelSession. + * Create a ChannelSession with a default rejecting signature validator. * * @param channelDefinitions the list of channels to resolve Maven artifact * @param factory Factory to create {@code MavenVersionsResolver} that are performing the actual Maven resolution. @@ -64,6 +64,15 @@ public ChannelSession(List channelDefinitions, MavenVersionsResolver.Fa this(channelDefinitions, factory, DEFAULT_SPLIT_ARTIFACT_PARALLELISM, SignatureValidator.REJECTING_VALIDATOR); } + /** + * Create a ChannelSession with a default rejecting signature validator. + * + * @param channelDefinitions the list of channels to resolve Maven artifact + * @param factory Factory to create {@code MavenVersionsResolver} that are performing the actual Maven resolution. + * @param signatureValidator Validator to verify signatures of downloaded artifacts + * @throws UnresolvedRequiredManifestException - if a required manifest cannot be resolved either via maven coordinates or in the list of channels + * @throws CyclicDependencyException - if the required manifests form a cyclic dependency + */ public ChannelSession(List channelDefinitions, MavenVersionsResolver.Factory factory, SignatureValidator signatureValidator) { this(channelDefinitions, factory, DEFAULT_SPLIT_ARTIFACT_PARALLELISM, signatureValidator); } diff --git a/core/src/main/java/org/wildfly/channel/spi/ValidationResource.java b/core/src/main/java/org/wildfly/channel/spi/ArtifactIdentifier.java similarity index 83% rename from core/src/main/java/org/wildfly/channel/spi/ValidationResource.java rename to core/src/main/java/org/wildfly/channel/spi/ArtifactIdentifier.java index 8cd7164d..353c517f 100644 --- a/core/src/main/java/org/wildfly/channel/spi/ValidationResource.java +++ b/core/src/main/java/org/wildfly/channel/spi/ArtifactIdentifier.java @@ -20,10 +20,13 @@ import org.wildfly.channel.ArtifactCoordinate; -public interface ValidationResource { +/** + * An identifier of an artifact being validated. It can be either a Maven coordinate or an URL. + */ +public interface ArtifactIdentifier { - class UrlResource implements ValidationResource { - private URL resourceUrl; + class UrlResource implements ArtifactIdentifier { + private final URL resourceUrl; public UrlResource(URL resourceUrl) { this.resourceUrl = resourceUrl; @@ -34,7 +37,7 @@ public URL getResourceUrl() { } } - class MavenResource extends ArtifactCoordinate implements ValidationResource { + class MavenResource extends ArtifactCoordinate implements ArtifactIdentifier { public MavenResource(String groupId, String artifactId, String extension, String classifier, String version) { super(groupId, artifactId, extension, classifier, version); diff --git a/core/src/main/java/org/wildfly/channel/spi/SignatureResult.java b/core/src/main/java/org/wildfly/channel/spi/SignatureResult.java index 4619c1c6..f02eb8e5 100644 --- a/core/src/main/java/org/wildfly/channel/spi/SignatureResult.java +++ b/core/src/main/java/org/wildfly/channel/spi/SignatureResult.java @@ -16,29 +16,41 @@ */ package org.wildfly.channel.spi; +/** + * Represents a result of artifact verification + */ public class SignatureResult { - private ValidationResource resource; + /** + * 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(ValidationResource resource, String keyID) { + public static SignatureResult noMatchingCertificate(ArtifactIdentifier resource, String keyID) { return new SignatureResult(Result.NO_MATCHING_CERT, resource, keyID, null); } - public static SignatureResult revoked(ValidationResource resource, String keyID, String revocationReason) { + public static SignatureResult revoked(ArtifactIdentifier resource, String keyID, String revocationReason) { return new SignatureResult(Result.REVOKED, resource, keyID, revocationReason); } - public static SignatureResult expired(ValidationResource resource, String keyID) { + public static SignatureResult expired(ArtifactIdentifier resource, String keyID) { return new SignatureResult(Result.EXPIRED, resource, keyID, null); } - public static SignatureResult noSignature(ValidationResource resource) { + public static SignatureResult noSignature(ArtifactIdentifier resource) { return new SignatureResult(Result.NO_SIGNATURE, resource, null, null); } - public static SignatureResult invalid(ValidationResource resource) { + public static SignatureResult invalid(ArtifactIdentifier resource) { return new SignatureResult(Result.INVALID, resource, null, null); } @@ -48,7 +60,7 @@ public static SignatureResult ok() { return new SignatureResult(Result.OK, null, null, null); } - private SignatureResult(Result result, ValidationResource resource, String keyID, String message) { + private SignatureResult(Result result, ArtifactIdentifier resource, String keyID, String message) { this.result = result; this.resource = resource; this.keyId = keyID; @@ -59,7 +71,7 @@ public Result getResult() { return result; } - public ValidationResource getResource() { + public ArtifactIdentifier getResource() { return resource; } diff --git a/core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java b/core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java index adc42bdb..4a1da13f 100644 --- a/core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java +++ b/core/src/main/java/org/wildfly/channel/spi/SignatureValidator.java @@ -23,21 +23,29 @@ * 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 {@code artifact}. The locally downloaded {@code signature} has to be an armour encoded GPG signature. + * validates a signature of an artifact. The locally downloaded {@code signature} has to be an armour encoded GPG signature. * - * @param artifact - {@code MavenArtifact} to validate. Includes a full GAV and the local artifact file. - * @param signature - local file containing armour encoded detached GPG signature for the {@code artifact}. + * @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(ValidationResource artifactSource, InputStream artifactStream, InputStream signatureStream, List gpgUrls) throws SignatureException; + 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; diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java index d18538fb..366cbda0 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java @@ -23,11 +23,10 @@ import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.spi.SignatureResult; import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ValidationResource; +import org.wildfly.channel.spi.ArtifactIdentifier; 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.List; @@ -442,7 +441,7 @@ public void invalidSignatureCausesError() throws Exception { 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(ValidationResource.class))); + when(signatureValidator.validateSignature(any(), any(), any(), any())).thenReturn(SignatureResult.invalid(mock(ArtifactIdentifier.class))); assertThrows(SignatureValidator.SignatureException.class, () -> new ChannelSession(channels, factory)); } diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java index 6058a5e6..6e189845 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java @@ -50,9 +50,6 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.wildfly.channel.spi.MavenVersionsResolver; -import org.wildfly.channel.spi.SignatureResult; -import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ValidationResource; public class ChannelSessionTestCase { diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java index aceec384..1a47b080 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java @@ -1,3 +1,19 @@ +/* + * 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.assertEquals; @@ -26,14 +42,14 @@ import org.wildfly.channel.spi.MavenVersionsResolver; import org.wildfly.channel.spi.SignatureResult; import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ValidationResource; +import org.wildfly.channel.spi.ArtifactIdentifier; public class ChannelSessionWithSignatureValidationTestCase { - private static final ValidationResource.MavenResource ARTIFACT = new ValidationResource.MavenResource( + private static final ArtifactIdentifier.MavenResource ARTIFACT = new ArtifactIdentifier.MavenResource( "org.wildfly", "wildfly-ee-galleon-pack", "zip", null, "25.0.1.Final"); - private static final ValidationResource.MavenResource MANIFEST = new ValidationResource.MavenResource( + private static final ArtifactIdentifier.MavenResource MANIFEST = new ArtifactIdentifier.MavenResource( "org.channels", "test-manifest", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.0.0"); @TempDir @@ -141,7 +157,7 @@ public void failedSignatureValidationThrowsException() throws Exception { ARTIFACT.classifier, ARTIFACT.version)) .thenReturn(signatureFile); // simulate a valid signature of the channel manifest, and invalid signature of the artifact - when(signatureValidator.validateSignature(eq(new ValidationResource.MavenResource( + when(signatureValidator.validateSignature(eq(new ArtifactIdentifier.MavenResource( MANIFEST.groupId, MANIFEST.artifactId, MANIFEST.extension, MANIFEST.classifier, MANIFEST.version)), any(), any(), any())).thenReturn(SignatureResult.ok()); when(signatureValidator.validateSignature(eq(ARTIFACT), 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 index d1cef35a..5e3e6e4c 100644 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgKeystore.java +++ b/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgKeystore.java @@ -20,9 +20,27 @@ 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 succesfully + * false otherwise. + */ boolean add(List publicKey); } 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 index dd6c326f..889c7dfa 100644 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidator.java +++ b/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidator.java @@ -44,29 +44,30 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.jboss.logging.Logger; -import org.wildfly.channel.ArtifactCoordinate; -import org.wildfly.channel.MavenArtifact; import org.wildfly.channel.spi.SignatureResult; import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ValidationResource; +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 SignatureValidatorListener listener = new SignatureValidatorListener() { - - @Override - public void artifactSignatureCorrect(ValidationResource artifact, PGPPublicKey publicKey) { - // noop - } - - @Override - public void artifactSignatureInvalid(ValidationResource artifact, PGPPublicKey publicKey) { - // noop - } - }; + private GpgSignatureValidatorListener listener = new NoopListener(); public GpgSignatureValidator(GpgKeystore keystore) { this(keystore, new Keyserver(Collections.emptyList())); @@ -77,36 +78,48 @@ public GpgSignatureValidator(GpgKeystore keystore, Keyserver keyserver) { this.keyserver = keyserver; } - public void addListener(SignatureValidatorListener listener) { + public void addListener(GpgSignatureValidatorListener listener) { this.listener = listener; } @Override - public SignatureResult validateSignature(ValidationResource artifactSource, InputStream artifactStream, - InputStream signatureStream, List gpgUrls) throws SignatureException { - Objects.requireNonNull(artifactSource); + 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(artifactSource)); + SignatureResult.noSignature(artifactId)); } if (pgpSignature == null) { LOG.error("Could not read the signature in provided signature file"); - return SignatureResult.noSignature(artifactSource); + return SignatureResult.noSignature(artifactId); } final String keyID = Long.toHexString(pgpSignature.getKeyID()).toUpperCase(Locale.ROOT); + 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 { @@ -121,75 +134,122 @@ public SignatureResult validateSignature(ValidationResource artifactSource, Inpu } } catch (PGPException | IOException e) { throw new SignatureException("Unable to parse the certificate downloaded from keyserver", e, - SignatureResult.noSignature(artifactSource)); + SignatureResult.noSignature(artifactId)); } 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.noSignature(artifactSource)); + SignatureResult.noSignature(artifactId)); } if (pgpPublicKeys.stream().anyMatch(k -> k.getKeyID() == pgpSignature.getKeyID())) { key = pgpPublicKeys.stream().filter(k -> k.getKeyID() == pgpSignature.getKeyID()).findFirst().get(); break; } } - } - if (key == null) { - return SignatureResult.noMatchingCertificate(artifactSource, keyID); - } else { - if (keystore.add(pgpPublicKeys)) { - publicKey = key; - } else { - return SignatureResult.noMatchingCertificate(artifactSource, keyID); + + 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); } } - } - final Iterator subKeys = publicKey.getSignaturesOfType(PGPSignature.SUBKEY_BINDING); - while (subKeys.hasNext()) { - final PGPSignature subKey = subKeys.next(); - final PGPPublicKey masterKey = keystore.get(Long.toHexString(subKey.getKeyID()).toUpperCase(Locale.ROOT)); - if (masterKey.hasRevocation()) { - return SignatureResult.revoked(artifactSource, keyID, getRevocationReason(publicKey)); + + 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 (publicKey.hasRevocation()) { - return SignatureResult.revoked(artifactSource, keyID, getRevocationReason(publicKey)); + 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; } - if (publicKey.getValidSeconds() > 0) { - final Instant expiry = Instant.from(publicKey.getCreationTime().toInstant().plus(publicKey.getValidSeconds(), ChronoUnit.SECONDS)); - if (expiry.isBefore(Instant.now())) { - return SignatureResult.expired(artifactSource, keyID); - } + 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(artifactSource)); + SignatureResult.invalid(artifactId)); } - - final SignatureResult result = verifyFile(artifactSource, artifactStream, pgpSignature); + final SignatureResult result = verifyFile(artifactId, artifactStream, pgpSignature); if (result.getResult() == SignatureResult.Result.OK) { - listener.artifactSignatureCorrect(artifactSource, publicKey); + listener.artifactSignatureCorrect(artifactId, publicKey); } else { - listener.artifactSignatureInvalid(artifactSource, publicKey); + listener.artifactSignatureInvalid(artifactId, publicKey); } return result; } - private static ArtifactCoordinate toArtifactCoordinate(MavenArtifact artifact) { - final ArtifactCoordinate coord = new ArtifactCoordinate(artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(), artifact.getClassifier(), artifact.getVersion()); - return coord; + 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(Long.toHexString(subKeySignature.getKeyID()).toUpperCase(Locale.ROOT)); + 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) { @@ -205,7 +265,7 @@ private static String getRevocationReason(PGPPublicKey publicKey) { return revocationDescription; } - private static SignatureResult verifyFile(ValidationResource artifactSource, InputStream artifactStream, PGPSignature pgpSignature) throws SignatureException { + private static SignatureResult verifyFile(ArtifactIdentifier artifactSource, InputStream artifactStream, PGPSignature pgpSignature) throws SignatureException { // Read file to verify byte[] data = new byte[1024]; InputStream inputStream = null; @@ -257,9 +317,15 @@ private static List downloadPublicKey(String signatureUrl) throws 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(); @@ -275,10 +341,16 @@ private static List downloadPublicKey(String signatureUrl) throws } } - public interface SignatureValidatorListener { + private static class NoopListener implements GpgSignatureValidatorListener { - void artifactSignatureCorrect(ValidationResource artifact, PGPPublicKey publicKey); + @Override + public void artifactSignatureCorrect(ArtifactIdentifier artifact, PGPPublicKey publicKey) { + // noop + } - void artifactSignatureInvalid(ValidationResource artifact, PGPPublicKey publicKey); + @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 new file mode 100644 index 00000000..61167f3c --- /dev/null +++ b/gpg-validator/src/main/java/org/wildfly/channel/gpg/GpgSignatureValidatorListener.java @@ -0,0 +1,42 @@ +/* + * 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 index 16def58c..e2fc4fe7 100644 --- a/gpg-validator/src/main/java/org/wildfly/channel/gpg/Keyserver.java +++ b/gpg-validator/src/main/java/org/wildfly/channel/gpg/Keyserver.java @@ -35,6 +35,9 @@ 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"; @@ -45,6 +48,14 @@ 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); 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 index 5a8bac76..8d1dabda 100644 --- a/gpg-validator/src/test/java/org/wildfly/channel/gpg/GpgSignatureValidatorTest.java +++ b/gpg-validator/src/test/java/org/wildfly/channel/gpg/GpgSignatureValidatorTest.java @@ -42,7 +42,7 @@ import org.wildfly.channel.ArtifactCoordinate; import org.wildfly.channel.spi.SignatureResult; import org.wildfly.channel.spi.SignatureValidator; -import org.wildfly.channel.spi.ValidationResource; +import org.wildfly.channel.spi.ArtifactIdentifier; import java.io.File; import java.io.FileInputStream; @@ -70,7 +70,7 @@ public class GpgSignatureValidatorTest { private PGPSecretKeyRing pgpExpiredKeys; private TestKeystore keystore = new TestKeystore(); private GpgSignatureValidator validator; - private ValidationResource.MavenResource anArtifact; + private ArtifactIdentifier.MavenResource anArtifact; private InputStream artifactInputStream; private File artifactFile; private InputStream signatureInputStream; @@ -92,7 +92,7 @@ public void setUp() throws Exception { this.artifactFile = tempDir.resolve("test-one.jar").toFile(); Files.writeString(artifactFile.toPath(), "test"); this.artifactInputStream = new FileInputStream(artifactFile); - anArtifact = new ValidationResource.MavenResource("org.test", "test-one", "jar", null, "1.0.0"); + anArtifact = new ArtifactIdentifier.MavenResource("org.test", "test-one", "jar", null, "1.0.0"); this.signatureFile = signFile(artifactFile, pgpValidKeys); this.signatureInputStream = new FileInputStream(signatureFile);