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(jarFile.getInputStream(entry));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the IS be closed? (try)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fair point; I'll restructure this

manifest.getEntries().clear();
manifest.write(jos);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this means we keep the "main" attributes, e.g.

Manifest-Version: 1.0
Implementation-Title: Microsoft Azure Java Core Library
Implementation-Version: 1.55.3
Build-Jdk-Spec: 21
Created-By: Maven JAR Plugin 3.4.2
Implementation-Vendor: Microsoft Corporation

Copy link
Contributor

@ldematte ldematte May 30, 2025

Choose a reason for hiding this comment

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

Just an idea; instead of a fixed "unsign", why don't you add a manifest transformer?
e.g.

public static void patchJar(..., Consumer<Manifest> manifestConsumer) {
....
   if (entryName.equals("META-INF/MANIFEST.MF")) {
      var manifest = new Manifest(jarFile.getInputStream(entry));
      manifestConsumer.accept(manifest);
      manifest.write(jos);
   }

This way the logic can be more precise and cover more scenarios

} else if (unsignJar == false || entryName.matches("META-INF/.*\\.SF") == false) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the Jar I'm patching includes a MSTFSIG.SF, which I assume will be named differently in other signed JARs

Copy link
Contributor

Choose a reason for hiding this comment

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

Umm if you need to perform this kind of manipulation, maybe it's even better to have a more generic
BiFunction<Boolean, InputStream, OutputStream>

The boolean will tell if you handled the entry or not:

try (InputStream is = jarFile.getInputStream(entry)) {
   if (function.apply(is, jos) == false) {
      // by default, copy
      is.transferTo(jos);
   }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, if you go down that route (you need that), I will consider refactoring this so that the transformers fit into this too... IDK if this is something you want to do though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, I think this path can get very complicated; it's probably better to wait until someone needs to transform a Manifest file in a different way (or another META-INF file) and this can be restructured to support that

// Read the entry's data and write it to the new JAR
try (InputStream is = jarFile.getInputStream(entry)) {
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