Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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 static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
Expand Down Expand Up @@ -60,6 +61,10 @@ public String toString() {
}
}

public static void patchJar(File inputJar, File outputJar, Collection<PatcherInfo> patchers) {
patchJar(inputJar, outputJar, patchers, false);
}

/**
* Patches the classes in the input JAR file, using the collection of patchers. Each patcher specifies a target class (its jar entry
* name) and the SHA256 digest on the class bytes.
Expand All @@ -69,8 +74,11 @@ public String toString() {
* @param inputFile the JAR file to patch
* @param outputFile the output (patched) JAR file
* @param patchers list of patcher info (classes to patch (jar entry name + optional SHA256 digest) and ASM visitor to transform them)
* @param unsignJar whether to remove class signatures from the JAR Manifest; set this to true when patching a signed JAR,
* otherwise the patched classes will fail to load at runtime due to mismatched signatures.
* @see <a href="https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html">Understanding Signing and Verification</a>
*/
public static void patchJar(File inputFile, File outputFile, Collection<PatcherInfo> patchers) {
public static void patchJar(File inputFile, File outputFile, Collection<PatcherInfo> patchers, boolean unsignJar) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the unsignJar should be the default behaviour since if there is a signature it will always be invalid after patching the jar. It also shouldn't impact existing usages of this method since they appear to not use signed jars (they work after patching).

var classPatchers = patchers.stream().collect(Collectors.toMap(PatcherInfo::jarEntryName, Function.identity()));
var mismatchedClasses = new ArrayList<MismatchInfo>();
try (JarFile jarFile = new JarFile(inputFile); JarOutputStream jos = new JarOutputStream(new FileOutputStream(outputFile))) {
Expand Down Expand Up @@ -101,9 +109,15 @@ public static void patchJar(File inputFile, File outputFile, Collection<PatcherI
);
}
} else {
// Read the entry's data and write it to the new JAR
try (InputStream is = jarFile.getInputStream(entry)) {
is.transferTo(jos);
if (unsignJar && entryName.equals("META-INF/MANIFEST.MF")) {
var manifest = new Manifest(is);
manifest.getEntries().clear();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearing all entries here might cause unexpected behaviour if the logic is using information from the manifest, ideally this should only clear signature specific entries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out the only thing we need to do is delete the signature file - it seems without a base signature, the JVM makes no attempt to validate the class signatures from the manifest, so we don't need to edit the manifest at all.

I'm more comfortable making this (deleting any signature files we find) the default behaviour as well; what do you think @ldematte @rjernst ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the signature files sounds good, though leaving the manifest as-is seems like it would produce a jar that is internally inconsistent. Even if the behavior is it ignores verification if signature files are missing, I don't think we should rely on that behavior of silently ignoring what the manifest claims.

manifest.write(jos);
} else if (unsignJar == false || entryName.matches("META-INF/.*\\.SF") == false) {
// Read the entry's data and write it to the new JAR
is.transferTo(jos);
}
}
}
jos.closeEntry();
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog/128613.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 128613
summary: Improve support for bytecode patching signed jars
area: Infra/Core
type: enhancement
issues: []
Loading