Skip to content

Commit 6e75afa

Browse files
[8.19] improve support for bytecode patching signed jars (#128613) (#129317)
* improve support for bytecode patching signed jars (#128613) * improve support for bytecode patching signed jars * Update docs/changelog/128613.yaml --------- Co-authored-by: elasticsearchmachine <[email protected]> Co-authored-by: Johannes Freden Jansson <[email protected]> (cherry picked from commit 94e9513) # Conflicts: # build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/Utils.java * fixup!Add missing PatherInfo --------- Co-authored-by: Richard Dennehy <[email protected]>
1 parent cb59197 commit 6e75afa

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.gradle.internal.dependencies.patches;
11+
12+
import org.objectweb.asm.ClassVisitor;
13+
import org.objectweb.asm.ClassWriter;
14+
15+
import java.util.Arrays;
16+
import java.util.HexFormat;
17+
import java.util.function.Function;
18+
19+
public final class PatcherInfo {
20+
private final String jarEntryName;
21+
private final byte[] classSha256;
22+
private final Function<ClassWriter, ClassVisitor> visitorFactory;
23+
24+
private PatcherInfo(String jarEntryName, byte[] classSha256, Function<ClassWriter, ClassVisitor> visitorFactory) {
25+
this.jarEntryName = jarEntryName;
26+
this.classSha256 = classSha256;
27+
this.visitorFactory = visitorFactory;
28+
}
29+
30+
/**
31+
* Creates a patcher info entry, linking a jar entry path name and its SHA256 digest to a patcher factory (a factory to create an ASM
32+
* visitor)
33+
*
34+
* @param jarEntryName the jar entry path, as a string
35+
* @param classSha256 the SHA256 digest of the class bytes, as a HEX string
36+
* @param visitorFactory the factory to create an ASM visitor from a ASM writer
37+
*/
38+
public static PatcherInfo classPatcher(String jarEntryName, String classSha256, Function<ClassWriter, ClassVisitor> visitorFactory) {
39+
return new PatcherInfo(jarEntryName, HexFormat.of().parseHex(classSha256), visitorFactory);
40+
}
41+
42+
boolean matches(byte[] otherClassSha256) {
43+
return Arrays.equals(this.classSha256, otherClassSha256);
44+
}
45+
46+
public String jarEntryName() {
47+
return jarEntryName;
48+
}
49+
50+
public byte[] classSha256() {
51+
return classSha256;
52+
}
53+
54+
public ClassVisitor createVisitor(ClassWriter classWriter) {
55+
return visitorFactory.apply(classWriter);
56+
}
57+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.gradle.internal.dependencies.patches;
11+
12+
import org.objectweb.asm.ClassReader;
13+
import org.objectweb.asm.ClassWriter;
14+
15+
import java.io.File;
16+
import java.io.FileOutputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.security.MessageDigest;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.Enumeration;
24+
import java.util.HexFormat;
25+
import java.util.Locale;
26+
import java.util.function.Function;
27+
import java.util.jar.Attributes;
28+
import java.util.jar.JarEntry;
29+
import java.util.jar.JarFile;
30+
import java.util.jar.JarOutputStream;
31+
import java.util.jar.Manifest;
32+
import java.util.stream.Collectors;
33+
34+
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
35+
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
36+
37+
public class Utils {
38+
39+
private static final MessageDigest SHA_256;
40+
41+
static {
42+
try {
43+
SHA_256 = MessageDigest.getInstance("SHA-256");
44+
} catch (NoSuchAlgorithmException e) {
45+
throw new RuntimeException(e);
46+
}
47+
}
48+
49+
private record MismatchInfo(String jarEntryName, String expectedClassSha256, String foundClassSha256) {
50+
@Override
51+
public String toString() {
52+
return "[class='"
53+
+ jarEntryName
54+
+ '\''
55+
+ ", expected='"
56+
+ expectedClassSha256
57+
+ '\''
58+
+ ", found='"
59+
+ foundClassSha256
60+
+ '\''
61+
+ ']';
62+
}
63+
}
64+
65+
public static void patchJar(File inputJar, File outputJar, Collection<PatcherInfo> patchers) {
66+
patchJar(inputJar, outputJar, patchers, false);
67+
}
68+
69+
/**
70+
* Patches the classes in the input JAR file, using the collection of patchers. Each patcher specifies a target class (its jar entry
71+
* name) and the SHA256 digest on the class bytes.
72+
* This digest is checked against the class bytes in the JAR, and if it does not match, an IllegalArgumentException is thrown.
73+
* If the input file does not contain all the classes to patch specified in the patcher info collection, an IllegalArgumentException
74+
* is also thrown.
75+
* @param inputFile the JAR file to patch
76+
* @param outputFile the output (patched) JAR file
77+
* @param patchers list of patcher info (classes to patch (jar entry name + optional SHA256 digest) and ASM visitor to transform them)
78+
* @param unsignJar whether to remove class signatures from the JAR Manifest; set this to true when patching a signed JAR,
79+
* otherwise the patched classes will fail to load at runtime due to mismatched signatures.
80+
* @see <a href="https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html">Understanding Signing and Verification</a>
81+
*/
82+
public static void patchJar(File inputFile, File outputFile, Collection<PatcherInfo> patchers, boolean unsignJar) {
83+
var classPatchers = patchers.stream().collect(Collectors.toMap(PatcherInfo::jarEntryName, Function.identity()));
84+
var mismatchedClasses = new ArrayList<MismatchInfo>();
85+
try (JarFile jarFile = new JarFile(inputFile); JarOutputStream jos = new JarOutputStream(new FileOutputStream(outputFile))) {
86+
Enumeration<JarEntry> entries = jarFile.entries();
87+
while (entries.hasMoreElements()) {
88+
JarEntry entry = entries.nextElement();
89+
String entryName = entry.getName();
90+
// Add the entry to the new JAR file
91+
jos.putNextEntry(new JarEntry(entryName));
92+
93+
var classPatcher = classPatchers.remove(entryName);
94+
if (classPatcher != null) {
95+
byte[] classToPatch = jarFile.getInputStream(entry).readAllBytes();
96+
var classSha256 = SHA_256.digest(classToPatch);
97+
98+
if (classPatcher.matches(classSha256)) {
99+
ClassReader classReader = new ClassReader(classToPatch);
100+
ClassWriter classWriter = new ClassWriter(classReader, COMPUTE_MAXS | COMPUTE_FRAMES);
101+
classReader.accept(classPatcher.createVisitor(classWriter), 0);
102+
jos.write(classWriter.toByteArray());
103+
} else {
104+
mismatchedClasses.add(
105+
new MismatchInfo(
106+
classPatcher.jarEntryName(),
107+
HexFormat.of().formatHex(classPatcher.classSha256()),
108+
HexFormat.of().formatHex(classSha256)
109+
)
110+
);
111+
}
112+
} else {
113+
try (InputStream is = jarFile.getInputStream(entry)) {
114+
if (unsignJar && entryName.equals("META-INF/MANIFEST.MF")) {
115+
var manifest = new Manifest(is);
116+
for (var manifestEntry : manifest.getEntries().entrySet()) {
117+
var nonSignatureAttributes = new Attributes();
118+
for (var attribute : manifestEntry.getValue().entrySet()) {
119+
if (attribute.getKey().toString().endsWith("Digest") == false) {
120+
nonSignatureAttributes.put(attribute.getKey(), attribute.getValue());
121+
}
122+
}
123+
manifestEntry.setValue(nonSignatureAttributes);
124+
}
125+
manifest.write(jos);
126+
} else if (unsignJar == false || entryName.matches("META-INF/.*\\.SF") == false) {
127+
// Read the entry's data and write it to the new JAR
128+
is.transferTo(jos);
129+
}
130+
}
131+
}
132+
jos.closeEntry();
133+
}
134+
} catch (IOException ex) {
135+
throw new RuntimeException(ex);
136+
}
137+
138+
if (mismatchedClasses.isEmpty() == false) {
139+
throw new IllegalArgumentException(
140+
String.format(
141+
Locale.ROOT,
142+
"""
143+
Error patching JAR [%s]: SHA256 digest mismatch (%s). This JAR was updated to a version that contains different \
144+
classes, for which this patcher was not designed. Please check if the patcher still \
145+
applies correctly, and update the SHA256 digest(s).""",
146+
inputFile.getName(),
147+
mismatchedClasses.stream().map(MismatchInfo::toString).collect(Collectors.joining())
148+
)
149+
);
150+
}
151+
152+
if (classPatchers.isEmpty() == false) {
153+
throw new IllegalArgumentException(
154+
String.format(
155+
Locale.ROOT,
156+
"error patching [%s]: the jar does not contain [%s]",
157+
inputFile.getName(),
158+
String.join(", ", classPatchers.keySet())
159+
)
160+
);
161+
}
162+
}
163+
}

docs/changelog/128613.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 128613
2+
summary: Improve support for bytecode patching signed jars
3+
area: Infra/Core
4+
type: enhancement
5+
issues: []

0 commit comments

Comments
 (0)