Skip to content

Commit 89ae963

Browse files
authored
Merge pull request #47757 from gastaldi/jarunsigner
Unsign all dependency JARs during build
2 parents a04cf8e + fdf08f0 commit 89ae963

File tree

3 files changed

+174
-73
lines changed

3 files changed

+174
-73
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package io.quarkus.deployment.pkg;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.util.Enumeration;
8+
import java.util.Map;
9+
import java.util.function.Predicate;
10+
import java.util.jar.Attributes;
11+
import java.util.jar.JarEntry;
12+
import java.util.jar.JarFile;
13+
import java.util.jar.JarOutputStream;
14+
import java.util.jar.Manifest;
15+
16+
import org.jboss.logging.Logger;
17+
18+
/**
19+
* JarUnsigner is used to remove the signature from a jar file.
20+
*/
21+
public final class JarUnsigner {
22+
23+
private static final Logger log = Logger.getLogger(JarUnsigner.class);
24+
25+
private JarUnsigner() {
26+
// utility class
27+
}
28+
29+
/**
30+
* Unsigns a jar file by removing the signature entries.
31+
* If the JAR is not signed, it will simply copy the original JAR to the target path.
32+
*
33+
* @param jarPath the path to the jar file to unsign
34+
* @param targetPath the path to the target jar file
35+
* @throws IOException if an I/O error occurs
36+
*/
37+
public static void unsignJar(Path jarPath, Path targetPath) throws IOException {
38+
try (JarFile in = new JarFile(jarPath.toFile(), false)) {
39+
Manifest manifest = in.getManifest();
40+
boolean signed;
41+
if (manifest != null) {
42+
Map<String, Attributes> entries = manifest.getEntries();
43+
signed = !entries.isEmpty();
44+
// Remove signature entries
45+
entries.clear();
46+
} else {
47+
signed = false;
48+
manifest = new Manifest();
49+
}
50+
if (!signed) {
51+
in.close();
52+
log.debugf("JAR %s is not signed, skipping unsigning", jarPath);
53+
Files.copy(jarPath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
54+
} else {
55+
log.debugf("JAR %s is signed, removing signature", jarPath);
56+
// Reusing buffer for performance reasons
57+
byte[] buffer = new byte[10000];
58+
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath))) {
59+
JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME);
60+
// Set manifest time to epoch to always make the same jar
61+
manifestEntry.setTime(0);
62+
out.putNextEntry(manifestEntry);
63+
manifest.write(out);
64+
out.closeEntry();
65+
Enumeration<JarEntry> entries = in.entries();
66+
while (entries.hasMoreElements()) {
67+
JarEntry entry = entries.nextElement();
68+
String entryName = entry.getName();
69+
if (!entryName.equals(JarFile.MANIFEST_NAME)
70+
&& !entryName.equals("META-INF/INDEX.LIST")
71+
&& !isSignatureFile(entryName)) {
72+
entry.setCompressedSize(-1);
73+
out.putNextEntry(entry);
74+
try (InputStream inStream = in.getInputStream(entry)) {
75+
int r;
76+
while ((r = inStream.read(buffer)) > 0) {
77+
out.write(buffer, 0, r);
78+
}
79+
} finally {
80+
out.closeEntry();
81+
}
82+
} else {
83+
log.debugf("Removed %s from %s", entryName, jarPath);
84+
}
85+
}
86+
}
87+
}
88+
// let's make sure we keep the original timestamp
89+
Files.setLastModifiedTime(targetPath, Files.getLastModifiedTime(jarPath));
90+
}
91+
}
92+
93+
/**
94+
* Unsigns a jar file by removing the signature entries.
95+
*
96+
* @param jarPath the path to the jar file to unsign
97+
* @param targetPath the path to the target jar file
98+
* @param includePredicate a predicate to determine which entries to include in the target jar
99+
* @throws IOException if an I/O error occurs
100+
*/
101+
public static void unsignJar(Path jarPath, Path targetPath, Predicate<String> includePredicate) throws IOException {
102+
// Reusing buffer for performance reasons
103+
byte[] buffer = new byte[10000];
104+
try (JarFile in = new JarFile(jarPath.toFile(), false)) {
105+
Manifest manifest = in.getManifest();
106+
boolean signed;
107+
if (manifest != null) {
108+
Map<String, Attributes> entries = manifest.getEntries();
109+
signed = !entries.isEmpty();
110+
// Remove signature entries
111+
entries.clear();
112+
} else {
113+
signed = false;
114+
manifest = new Manifest();
115+
}
116+
if (signed) {
117+
log.debugf("JAR %s is signed, removing signature", jarPath);
118+
}
119+
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath))) {
120+
JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME);
121+
// Set manifest time to epoch to always make the same jar
122+
manifestEntry.setTime(0);
123+
out.putNextEntry(manifestEntry);
124+
manifest.write(out);
125+
out.closeEntry();
126+
Enumeration<JarEntry> entries = in.entries();
127+
while (entries.hasMoreElements()) {
128+
JarEntry entry = entries.nextElement();
129+
String entryName = entry.getName();
130+
if (includePredicate.test(entryName)
131+
&& !entryName.equals(JarFile.MANIFEST_NAME)
132+
&& !entryName.equals("META-INF/INDEX.LIST")
133+
&& !isSignatureFile(entryName)) {
134+
entry.setCompressedSize(-1);
135+
out.putNextEntry(entry);
136+
try (InputStream inStream = in.getInputStream(entry)) {
137+
int r;
138+
while ((r = inStream.read(buffer)) > 0) {
139+
out.write(buffer, 0, r);
140+
}
141+
} finally {
142+
out.closeEntry();
143+
}
144+
} else {
145+
log.debugf("Removed %s from %s", entryName, jarPath);
146+
}
147+
}
148+
}
149+
// let's make sure we keep the original timestamp
150+
Files.setLastModifiedTime(targetPath, Files.getLastModifiedTime(jarPath));
151+
}
152+
}
153+
154+
private static boolean isSignatureFile(String entry) {
155+
entry = entry.toUpperCase();
156+
if (entry.startsWith("META-INF/") && entry.indexOf('/', "META-INF/".length()) == -1) {
157+
return entry.endsWith(".SF")
158+
|| entry.endsWith(".DSA")
159+
|| entry.endsWith(".RSA")
160+
|| entry.endsWith(".EC");
161+
}
162+
return false;
163+
}
164+
}

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java

Lines changed: 5 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@
4343
import java.util.function.Consumer;
4444
import java.util.function.Predicate;
4545
import java.util.jar.Attributes;
46-
import java.util.jar.JarEntry;
47-
import java.util.jar.JarFile;
48-
import java.util.jar.JarOutputStream;
4946
import java.util.jar.Manifest;
5047
import java.util.stream.Collectors;
5148
import java.util.stream.Stream;
@@ -68,6 +65,7 @@
6865
import io.quarkus.deployment.builditem.QuarkusBuildCloseablesBuildItem;
6966
import io.quarkus.deployment.builditem.TransformedClassesBuildItem;
7067
import io.quarkus.deployment.configuration.ClassLoadingConfig;
68+
import io.quarkus.deployment.pkg.JarUnsigner;
7169
import io.quarkus.deployment.pkg.PackageConfig;
7270
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
7371
import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem;
@@ -959,7 +957,7 @@ private void copyDependency(Set<ArtifactKey> parentFirstArtifacts, OutputTargetB
959957
} else {
960958
// we copy jars for which we remove entries to the same directory
961959
// which seems a bit odd to me
962-
filterJarFile(resolvedDep, targetPath, removedFromThisArchive);
960+
JarUnsigner.unsignJar(resolvedDep, targetPath, Predicate.not(removedFromThisArchive::contains));
963961

964962
var list = new ArrayList<>(removedFromThisArchive);
965963
Collections.sort(list);
@@ -1135,7 +1133,6 @@ private void copyLibraryJars(FileSystem runnerZipFs, OutputTargetBuildItem outpu
11351133
TransformedClassesBuildItem transformedClasses, Path libDir,
11361134
StringBuilder classPath, Collection<ResolvedDependency> appDeps, Map<String, List<byte[]>> services,
11371135
Predicate<String> ignoredEntriesPredicate, Set<ArtifactKey> removedDependencies) throws IOException {
1138-
11391136
for (ResolvedDependency appDep : appDeps) {
11401137

11411138
// Exclude files that are not jars (typically, we can have XML files here, see https://github.com/quarkusio/quarkus/issues/2852)
@@ -1150,15 +1147,16 @@ private void copyLibraryJars(FileSystem runnerZipFs, OutputTargetBuildItem outpu
11501147
if (transformedFromThisArchive == null || transformedFromThisArchive.isEmpty()) {
11511148
final String fileName = appDep.getGroupId() + "." + resolvedDep.getFileName();
11521149
final Path targetPath = libDir.resolve(fileName);
1153-
Files.copy(resolvedDep, targetPath, StandardCopyOption.REPLACE_EXISTING);
1150+
// Unsign the jar before copying it
1151+
JarUnsigner.unsignJar(resolvedDep, targetPath);
11541152
classPath.append(" lib/").append(fileName);
11551153
} else {
11561154
//we have transformed classes, we need to handle them correctly
11571155
final String fileName = "modified-" + appDep.getGroupId() + "."
11581156
+ resolvedDep.getFileName();
11591157
final Path targetPath = libDir.resolve(fileName);
11601158
classPath.append(" lib/").append(fileName);
1161-
filterJarFile(resolvedDep, targetPath, transformedFromThisArchive);
1159+
JarUnsigner.unsignJar(resolvedDep, targetPath, Predicate.not(transformedFromThisArchive::contains));
11621160
}
11631161
} else {
11641162
// This case can happen when we are building a jar from inside the Quarkus repository
@@ -1272,66 +1270,6 @@ private void handleParent(FileSystem runnerZipFs, String fileName, Map<String, S
12721270
}
12731271
}
12741272

1275-
static void filterJarFile(Path resolvedDep, Path targetPath, Set<String> transformedFromThisArchive) {
1276-
try {
1277-
byte[] buffer = new byte[10000];
1278-
try (JarFile in = new JarFile(resolvedDep.toFile(), false)) {
1279-
Manifest manifest = in.getManifest();
1280-
if (manifest != null) {
1281-
// Remove signature entries
1282-
manifest.getEntries().clear();
1283-
} else {
1284-
manifest = new Manifest();
1285-
}
1286-
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath))) {
1287-
JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME);
1288-
// Set manifest time to epoch to always make the same jar
1289-
manifestEntry.setTime(0);
1290-
out.putNextEntry(manifestEntry);
1291-
manifest.write(out);
1292-
out.closeEntry();
1293-
Enumeration<JarEntry> entries = in.entries();
1294-
while (entries.hasMoreElements()) {
1295-
JarEntry entry = entries.nextElement();
1296-
String entryName = entry.getName();
1297-
if (!transformedFromThisArchive.contains(entryName)
1298-
&& !entryName.equals(JarFile.MANIFEST_NAME)
1299-
&& !entryName.equals("META-INF/INDEX.LIST")
1300-
&& !isSignatureFile(entryName)) {
1301-
entry.setCompressedSize(-1);
1302-
out.putNextEntry(entry);
1303-
try (InputStream inStream = in.getInputStream(entry)) {
1304-
int r = 0;
1305-
while ((r = inStream.read(buffer)) > 0) {
1306-
out.write(buffer, 0, r);
1307-
}
1308-
} finally {
1309-
out.closeEntry();
1310-
}
1311-
} else {
1312-
log.debugf("Removed %s from %s", entryName, resolvedDep);
1313-
}
1314-
}
1315-
}
1316-
// let's make sure we keep the original timestamp
1317-
Files.setLastModifiedTime(targetPath, Files.getLastModifiedTime(resolvedDep));
1318-
}
1319-
} catch (IOException e) {
1320-
throw new UncheckedIOException(e);
1321-
}
1322-
}
1323-
1324-
private static boolean isSignatureFile(String entry) {
1325-
entry = entry.toUpperCase();
1326-
if (entry.startsWith("META-INF/") && entry.indexOf('/', "META-INF/".length()) == -1) {
1327-
return entry.endsWith(".SF")
1328-
|| entry.endsWith(".DSA")
1329-
|| entry.endsWith(".RSA")
1330-
|| entry.endsWith(".EC");
1331-
}
1332-
return false;
1333-
}
1334-
13351273
/**
13361274
* Manifest generation is quite simple : we just have to push some attributes in manifest.
13371275
* However, it gets a little more complex if the manifest preexists.
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.quarkus.deployment.pkg.steps;
1+
package io.quarkus.deployment.pkg;
22

33
import static org.assertj.core.api.Assertions.assertThat;
44

@@ -16,7 +16,6 @@
1616
import java.security.cert.CertificateException;
1717
import java.util.Calendar;
1818
import java.util.Date;
19-
import java.util.Set;
2019
import java.util.jar.JarEntry;
2120
import java.util.jar.JarFile;
2221
import java.util.jar.Manifest;
@@ -41,9 +40,9 @@
4140
import jdk.security.jarsigner.JarSigner;
4241

4342
/**
44-
* Test for {@link JarResultBuildStep}
43+
* Test for {@link JarUnsigner}
4544
*/
46-
class JarResultBuildStepTest {
45+
class JarUnsignerTest {
4746

4847
@Test
4948
void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception {
@@ -58,7 +57,7 @@ void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception {
5857
FileOutputStream out = new FileOutputStream(signedJarPath.toFile())) {
5958
signer.sign(in, out);
6059
}
61-
JarResultBuildStep.filterJarFile(signedJarPath, unsignedJarToTestPath, Set.of("java/lang/Integer.class"));
60+
JarUnsigner.unsignJar(signedJarPath, unsignedJarToTestPath, p -> !p.equals("java/lang/Integer.class"));
6261
try (JarFile jarFile = new JarFile(unsignedJarToTestPath.toFile())) {
6362
assertThat(jarFile.stream().map(JarEntry::getName)).doesNotContain("META-INF/ECLIPSE_.RSA", "META-INF/ECLIPSE_.SF");
6463
// Check that the manifest is still present
@@ -76,7 +75,7 @@ void manifestTimeShouldAlwaysBeSetToEpoch(@TempDir Path tempDir) throws Exceptio
7675
Path initialJar = tempDir.resolve("initial.jar");
7776
Path filteredJar = tempDir.resolve("filtered.jar");
7877
archive.as(ZipExporter.class).exportTo(new File(initialJar.toUri()), true);
79-
JarResultBuildStep.filterJarFile(initialJar, filteredJar, Set.of("java/lang/Integer.class"));
78+
JarUnsigner.unsignJar(initialJar, filteredJar, p -> !p.equals("java/lang/Integer.class"));
8079
try (JarFile jarFile = new JarFile(filteredJar.toFile())) {
8180
assertThat(jarFile.stream())
8281
.filteredOn(jarEntry -> jarEntry.getName().equals(JarFile.MANIFEST_NAME))

0 commit comments

Comments
 (0)