diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index 0878028369f03..852e752a75c70 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -143,6 +143,30 @@ + + maven-dependency-plugin + + + download-signed-jar + generate-test-resources + + copy + + + + + org.eclipse.jgit + org.eclipse.jgit.ssh.apache + 6.9.0.202403050737-r + jar + signed.jar + + + ${project.build.testOutputDirectory} + + + + maven-surefire-plugin diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index 89521eee66ef3..5b77b68fb79fb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -5,7 +5,6 @@ import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectOutputStream; @@ -42,12 +41,12 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; import org.jboss.logging.Logger; @@ -92,14 +91,14 @@ /** * This build step builds both the thin jars and uber jars. - * + *

* The way this is built is a bit convoluted. In general, we only want a single one built, * as determined by the {@link PackageConfig} (unless the config explicitly asks for both of them) - * + *

* However, we still need an extension to be able to ask for a specific one of these despite the config, * e.g. if a serverless environment needs an uberjar to build its deployment package then we need * to be able to provide this. - * + *

* To enable this we have two build steps that strongly produce the respective artifact type build * items, but not a {@link ArtifactResultBuildItem}. We then * have another two build steps that only run if they are configured to consume these explicit @@ -931,7 +930,7 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB } else { // we copy jars for which we remove entries to the same directory // which seems a bit odd to me - filterZipFile(resolvedDep, targetPath, removedFromThisArchive); + filterJarFile(resolvedDep, targetPath, removedFromThisArchive); } } } @@ -1125,7 +1124,7 @@ private void copyLibraryJars(FileSystem runnerZipFs, OutputTargetBuildItem outpu + resolvedDep.getFileName(); final Path targetPath = libDir.resolve(fileName); classPath.append(" lib/").append(fileName); - filterZipFile(resolvedDep, targetPath, transformedFromThisArchive); + filterJarFile(resolvedDep, targetPath, transformedFromThisArchive); } } else { // This case can happen when we are building a jar from inside the Quarkus repository @@ -1240,16 +1239,26 @@ private void handleParent(FileSystem runnerZipFs, String fileName, Map transformedFromThisArchive) { - + static void filterJarFile(Path resolvedDep, Path targetPath, Set transformedFromThisArchive) { try { byte[] buffer = new byte[10000]; - try (ZipFile in = new ZipFile(resolvedDep.toFile())) { - try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(targetPath.toFile()))) { - Enumeration entries = in.entries(); + try (JarFile in = new JarFile(resolvedDep.toFile(), false)) { + Manifest manifest = in.getManifest(); + if (manifest != null) { + // Remove signature entries + manifest.getEntries().clear(); + } else { + manifest = new Manifest(); + } + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath), manifest)) { + Enumeration entries = in.entries(); while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (!transformedFromThisArchive.contains(entry.getName())) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (!transformedFromThisArchive.contains(entryName) + && !entryName.equals(JarFile.MANIFEST_NAME) + && !entryName.equals("META-INF/INDEX.LIST") + && !isSignatureFile(entryName)) { entry.setCompressedSize(-1); out.putNextEntry(entry); try (InputStream inStream = in.getInputStream(entry)) { @@ -1258,6 +1267,8 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf out.write(buffer, 0, r); } } + } else { + log.debugf("Removed %s from %s", entryName, resolvedDep); } } } @@ -1265,10 +1276,21 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf Files.setLastModifiedTime(targetPath, Files.getLastModifiedTime(resolvedDep)); } } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } } + private static boolean isSignatureFile(String entry) { + entry = entry.toUpperCase(); + if (entry.startsWith("META-INF/") && entry.indexOf('/', "META-INF/".length()) == -1) { + return entry.endsWith(".SF") + || entry.endsWith(".DSA") + || entry.endsWith(".RSA") + || entry.endsWith(".EC"); + } + return false; + } + /** * Manifest generation is quite simple : we just have to push some attributes in manifest. * However, it gets a little more complex if the manifest preexists. @@ -1612,12 +1634,8 @@ public boolean downloadIfNecessary() { "https://repo.maven.apache.org/maven2/org/vineflower/vineflower/%s/vineflower-%s.jar", context.versionStr, context.versionStr); try (BufferedInputStream in = new BufferedInputStream(new URL(downloadURL).openStream()); - FileOutputStream fileOutputStream = new FileOutputStream(decompilerJar.toFile())) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - } + OutputStream fileOutputStream = Files.newOutputStream(decompilerJar)) { + in.transferTo(fileOutputStream); return true; } catch (IOException e) { log.error("Unable to download Vineflower from " + downloadURL, e); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java new file mode 100644 index 0000000000000..7cfb2c4ece496 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java @@ -0,0 +1,34 @@ +package io.quarkus.deployment.pkg.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Test for {@link JarResultBuildStep} + */ +class JarResultBuildStepTest { + + @Test + void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception { + Path signedJarFilePath = Path.of(getClass().getClassLoader().getResource("signed.jar").toURI()); + Path jarFilePath = tempDir.resolve("unsigned.jar"); + JarResultBuildStep.filterJarFile(signedJarFilePath, jarFilePath, + Set.of("org/eclipse/jgit/transport/sshd/SshdSessionFactory.class")); + try (JarFile jarFile = new JarFile(jarFilePath.toFile())) { + assertThat(jarFile.stream().map(JarEntry::getName)).doesNotContain("META-INF/ECLIPSE_.RSA", "META-INF/ECLIPSE_.SF"); + // Check that the manifest is still present + Manifest manifest = jarFile.getManifest(); + assertThat(manifest.getMainAttributes()).isNotEmpty(); + assertThat(manifest.getEntries()).isEmpty(); + } + } + +}