diff --git a/core/src/main/java/org/wildfly/channel/ChannelImpl.java b/core/src/main/java/org/wildfly/channel/ChannelImpl.java index 59ae8e6c..39e00322 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelImpl.java +++ b/core/src/main/java/org/wildfly/channel/ChannelImpl.java @@ -48,6 +48,7 @@ class ChannelImpl implements AutoCloseable { private static final Logger LOG = Logger.getLogger(ChannelImpl.class); + protected static final String SIGNATURE_FILE_SUFFIX = ".asc"; private Channel channelDefinition; @@ -203,8 +204,7 @@ static class ResolveLatestVersionResult { } private ChannelManifest resolveManifest(ChannelManifestCoordinate manifestCoordinate) throws UnresolvedMavenArtifactException { - final List manifestUrls = resolveChannelMetadata(List.of(manifestCoordinate), false); - return manifestUrls + return resolveChannelMetadata(List.of(manifestCoordinate), false) .stream() .map(ChannelManifestMapper::from) .findFirst().orElseThrow(); @@ -238,9 +238,9 @@ public List resolveChannelMetadata(List resolveArtifacts(List coordinate if (channelDefinition.requiresGpgCheck()) { try { final List signatures = resolver.resolveArtifacts(coordinates.stream() - .map(c->new ArtifactCoordinate(c.getGroupId(), c.getArtifactId(), c.getExtension() + ".asc", + .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++) { diff --git a/core/src/main/java/org/wildfly/channel/ChannelSession.java b/core/src/main/java/org/wildfly/channel/ChannelSession.java index eb3f52e4..552d9b81 100644 --- a/core/src/main/java/org/wildfly/channel/ChannelSession.java +++ b/core/src/main/java/org/wildfly/channel/ChannelSession.java @@ -81,6 +81,7 @@ public ChannelSession(List channelDefinitions, MavenVersionsResolver.Fa int versionResolutionParallelism, SignatureValidator signatureValidator) { requireNonNull(channelDefinitions); requireNonNull(factory); + requireNonNull(signatureValidator); final Set repositories = channelDefinitions.stream().flatMap(c -> c.getRepositories().stream()).collect(Collectors.toSet()); this.combinedResolver = factory.create(repositories); diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java index e654b95b..d18538fb 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionInitTestCase.java @@ -21,9 +21,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; 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 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; @@ -37,6 +41,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.wildfly.channel.ChannelImpl.SIGNATURE_FILE_SUFFIX; public class ChannelSessionInitTestCase { @TempDir @@ -360,6 +365,87 @@ public void duplicatedManifestIDsAreDetected() throws Exception { assertThrows(RuntimeException.class, () -> new ChannelSession(channels, factory)); } + @Test + public void mavenManifestWithoutSignatureCausesError() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new Channel.Builder() + .setName("channel one") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .setGpgCheck(true) + .build() + ); + + when(resolver.resolveArtifact("test.channels", "base-manifest", + ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) + .thenThrow(ArtifactTransferException.class); + assertThrows(SignatureValidator.SignatureException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void urlManifestWithoutSignatureCausesError() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("manifest-one") + .build(); + final Path manifestFile = tempDir.resolve("test-manifest.yaml"); + Files.writeString(manifestFile, ChannelManifestMapper.toYaml(baseManifest)); + + // two channels providing base- and required- manifests + List channels = List.of(new Channel.Builder() + .setName("channel one") + .addRepository("test", "test") + .setManifestCoordinate(new ChannelManifestCoordinate(manifestFile.toUri().toURL())) + .setGpgCheck(true) + .build() + ); + + when(resolver.resolveArtifact("test.channels", "base-manifest", + ChannelManifest.EXTENSION + SIGNATURE_FILE_SUFFIX, ChannelManifest.CLASSIFIER, "1.0.0")) + .thenThrow(ArtifactTransferException.class); + assertThrows(InvalidChannelMetadataException.class, () -> new ChannelSession(channels, factory)); + } + + @Test + public void invalidSignatureCausesError() throws Exception { + MavenVersionsResolver.Factory factory = mock(MavenVersionsResolver.Factory.class); + MavenVersionsResolver resolver = mock(MavenVersionsResolver.class); + final SignatureValidator signatureValidator = mock(SignatureValidator.class); + when(factory.create(any())).thenReturn(resolver); + + final ChannelManifest baseManifest = new ManifestBuilder() + .setId("manifest-one") + .build(); + mockManifest(resolver, baseManifest, "test.channels:base-manifest:1.0.0"); + + // two channels providing base- and required- manifests + List channels = List.of(new Channel.Builder() + .setName("channel one") + .addRepository("test", "test") + .setManifestCoordinate("test.channels", "base-manifest", "1.0.0") + .setGpgCheck(true) + .build() + ); + + 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))); + assertThrows(SignatureValidator.SignatureException.class, () -> new ChannelSession(channels, factory)); + } + private void mockManifest(MavenVersionsResolver resolver, ChannelManifest manifest, String gav) throws IOException { mockManifest(resolver, ChannelManifestMapper.toYaml(manifest), gav); } diff --git a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java index 6e189845..6058a5e6 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionTestCase.java @@ -50,6 +50,9 @@ 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 new file mode 100644 index 00000000..aceec384 --- /dev/null +++ b/core/src/test/java/org/wildfly/channel/ChannelSessionWithSignatureValidationTestCase.java @@ -0,0 +1,159 @@ +package org.wildfly.channel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.wildfly.channel.ChannelImpl.SIGNATURE_FILE_SUFFIX; +import static org.wildfly.channel.ChannelManifestMapper.CURRENT_SCHEMA_VERSION; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +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 ChannelSessionWithSignatureValidationTestCase { + + private static final ValidationResource.MavenResource ARTIFACT = new ValidationResource.MavenResource( + "org.wildfly", "wildfly-ee-galleon-pack", "zip", null, "25.0.1.Final"); + + private static final ValidationResource.MavenResource MANIFEST = new ValidationResource.MavenResource( + "org.channels", "test-manifest", ChannelManifest.EXTENSION, ChannelManifest.CLASSIFIER, "1.0.0"); + + @TempDir + private Path tempDir; + private MavenVersionsResolver resolver; + private SignatureValidator signatureValidator; + private MavenVersionsResolver.Factory factory; + private File resolvedArtifactFile; + private List channels; + private File signatureFile; + + @BeforeEach + public void setUp() throws Exception { + factory = mock(MavenVersionsResolver.Factory.class); + resolver = mock(MavenVersionsResolver.class); + signatureValidator = mock(SignatureValidator.class); + when(factory.create(any())).thenReturn(resolver); + + // create a manfiest with a versionPattern to test signature od the latest resolved version is downloaded + final String manifest = "schemaVersion: " + CURRENT_SCHEMA_VERSION + "\n" + + "streams:\n" + + " - groupId: org.wildfly\n" + + " artifactId: '*'\n" + + " versionPattern: '25\\.\\d+\\.\\d+.Final'"; + // create a channel requiring a gpg check + channels = List.of(new Channel.Builder() + .setName("channel-0") + .setGpgCheck(true) + .setManifestCoordinate(MANIFEST.groupId, MANIFEST.artifactId, MANIFEST.version) + .build()); + + // the resolved files need to exist otherwise we can't create streams from them + resolvedArtifactFile = tempDir.resolve("test-artifact").toFile(); + Files.createFile(resolvedArtifactFile.toPath()); + signatureFile = tempDir.resolve("test-signature.asc").toFile(); + Files.createFile(signatureFile.toPath()); + + + when(resolver.getAllVersions(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier)) + .thenReturn(new HashSet<>(Arrays.asList("25.0.0.Final", ARTIFACT.version))); + when(resolver.resolveArtifact(ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, ARTIFACT.version)) + .thenReturn(resolvedArtifactFile); + + + Path manifestFile = Files.writeString(tempDir.resolve("manifest.yaml"), manifest); + when(resolver.resolveArtifact(MANIFEST.groupId, MANIFEST.artifactId, MANIFEST.extension, MANIFEST.classifier, MANIFEST.version)) + .thenReturn(manifestFile.toFile()); + when(resolver.resolveArtifact(MANIFEST.groupId, MANIFEST.artifactId, + MANIFEST.extension + SIGNATURE_FILE_SUFFIX, MANIFEST.classifier, MANIFEST.version)) + .thenReturn(signatureFile); + } + + @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()); + + + try (ChannelSession session = new ChannelSession(channels, factory, signatureValidator)) { + MavenArtifact artifact = session.resolveMavenArtifact( + ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, null); + assertNotNull(artifact); + + assertEquals(ARTIFACT.groupId, artifact.getGroupId()); + assertEquals(ARTIFACT.artifactId, artifact.getArtifactId()); + assertEquals(ARTIFACT.extension, artifact.getExtension()); + assertNull(artifact.getClassifier()); + assertEquals(ARTIFACT.version, artifact.getVersion()); + assertEquals(resolvedArtifactFile, artifact.getFile()); + assertEquals("channel-0", artifact.getChannelName().get()); + } + + // validateSignature should have been called for the manifest and the artifact + verify(signatureValidator, times(2)).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()); + + try (ChannelSession session = new ChannelSession(channels, factory, signatureValidator)) { + assertThrows(SignatureValidator.SignatureException.class, () -> session.resolveMavenArtifact( + ARTIFACT.groupId, ARTIFACT.artifactId, ARTIFACT.extension, ARTIFACT.classifier, null)); + } + + // validateSignature should have been called for the manifest only + verify(signatureValidator, times(1)).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); + // simulate a valid signature of the channel manifest, and invalid signature of the artifact + when(signatureValidator.validateSignature(eq(new ValidationResource.MavenResource( + MANIFEST.groupId, MANIFEST.artifactId, MANIFEST.extension, MANIFEST.classifier, MANIFEST.version)), + any(), any(), any())).thenReturn(SignatureResult.ok()); + when(signatureValidator.validateSignature(eq(ARTIFACT), + any(), any(), any())).thenReturn(SignatureResult.invalid(ARTIFACT)); + + + try (ChannelSession session = new ChannelSession(channels, factory, signatureValidator)) { + assertThrows(SignatureValidator.SignatureException.class, () -> session.resolveMavenArtifact("org.wildfly", + "wildfly-ee-galleon-pack", "zip", null, "25.0.0.Final")); + } + + // validateSignature should have been called for the manifest and the artifact + verify(signatureValidator, times(2)).validateSignature(any(), any(), any(), any()); + } +} diff --git a/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java index f17ceb58..91ff1328 100644 --- a/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java +++ b/core/src/test/java/org/wildfly/channel/ChannelWithBlocklistTestCase.java @@ -325,9 +325,8 @@ public void testResolveLatestMavenArtifactThrowUnresolvedMavenArtifactException( verify(resolver, times(2)).close(); } - private void mockBlocklist(MavenVersionsResolver resolver, String blocklistFileLocation, String groupId, String artifactId, String version) throws URISyntaxException { -// when(resolver.resolveChannelMetadata(List.of(new BlocklistCoordinate("org.wildfly", "wildfly-blocklist")))) -// .thenReturn(List.of(this.getClass().getClassLoader().getResource("channels/test-blocklist.yaml"))); + private void mockBlocklist(MavenVersionsResolver resolver, String blocklistFileLocation, + String groupId, String artifactId, String version) throws URISyntaxException { if (version == null) { when(resolver.getAllVersions(groupId, artifactId, BlocklistCoordinate.EXTENSION,