Skip to content

Commit bbd5b17

Browse files
author
Hai-May Chao
committed
8339280: jarsigner -verify performs cross-checking between CEN and LOC
Reviewed-by: mullan, weijun, lancea
1 parent d4d1835 commit bbd5b17

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.io.*;
2929
import java.net.UnknownHostException;
3030
import java.net.URLClassLoader;
31+
import java.nio.file.Files;
32+
import java.nio.file.Path;
3133
import java.security.cert.CertPathValidatorException;
3234
import java.security.cert.PKIXBuilderParameters;
3335
import java.security.spec.ECParameterSpec;
@@ -242,6 +244,8 @@ public ExitException(int errorCode) {
242244
private Throwable chainNotValidatedReason = null;
243245
private Throwable tsaChainNotValidatedReason = null;
244246

247+
private List<String> crossChkWarnings = new ArrayList<>();
248+
245249
PKIXBuilderParameters pkixParameters;
246250
Set<X509Certificate> trustedCerts = new HashSet<>();
247251

@@ -1103,6 +1107,7 @@ void verifyJar(String jarName)
11031107
}
11041108
}
11051109
System.out.println();
1110+
crossCheckEntries(jarName);
11061111

11071112
if (!anySigned) {
11081113
if (disabledAlgFound) {
@@ -1129,6 +1134,143 @@ void verifyJar(String jarName)
11291134
}
11301135
}
11311136

1137+
private void crossCheckEntries(String jarName) throws Exception {
1138+
Set<String> locEntries = new HashSet<>();
1139+
1140+
try (JarFile jarFile = new JarFile(jarName);
1141+
JarInputStream jis = new JarInputStream(
1142+
Files.newInputStream(Path.of(jarName)))) {
1143+
1144+
Manifest cenManifest = jarFile.getManifest();
1145+
Manifest locManifest = jis.getManifest();
1146+
compareManifest(cenManifest, locManifest);
1147+
1148+
JarEntry locEntry;
1149+
while ((locEntry = jis.getNextJarEntry()) != null) {
1150+
String entryName = locEntry.getName();
1151+
locEntries.add(entryName);
1152+
1153+
JarEntry cenEntry = jarFile.getJarEntry(entryName);
1154+
if (cenEntry == null) {
1155+
crossChkWarnings.add(String.format(rb.getString(
1156+
"entry.1.present.when.reading.jarinputstream.but.missing.via.jarfile"),
1157+
entryName));
1158+
continue;
1159+
}
1160+
1161+
try {
1162+
readEntry(jis);
1163+
} catch (SecurityException e) {
1164+
crossChkWarnings.add(String.format(rb.getString(
1165+
"signature.verification.failed.on.entry.1.when.reading.via.jarinputstream"),
1166+
entryName));
1167+
continue;
1168+
}
1169+
1170+
try (InputStream cenInputStream = jarFile.getInputStream(cenEntry)) {
1171+
if (cenInputStream == null) {
1172+
crossChkWarnings.add(String.format(rb.getString(
1173+
"entry.1.present.in.jarfile.but.unreadable"),
1174+
entryName));
1175+
continue;
1176+
} else {
1177+
try {
1178+
readEntry(cenInputStream);
1179+
} catch (SecurityException e) {
1180+
crossChkWarnings.add(String.format(rb.getString(
1181+
"signature.verification.failed.on.entry.1.when.reading.via.jarfile"),
1182+
entryName));
1183+
continue;
1184+
}
1185+
}
1186+
}
1187+
1188+
compareSigners(cenEntry, locEntry);
1189+
}
1190+
1191+
jarFile.stream()
1192+
.map(JarEntry::getName)
1193+
.filter(n -> !locEntries.contains(n) && !n.equals(JarFile.MANIFEST_NAME))
1194+
.forEach(n -> crossChkWarnings.add(String.format(rb.getString(
1195+
"entry.1.present.when.reading.jarfile.but.missing.via.jarinputstream"), n)));
1196+
}
1197+
}
1198+
1199+
private void readEntry(InputStream is) throws IOException {
1200+
is.transferTo(OutputStream.nullOutputStream());
1201+
}
1202+
1203+
private void compareManifest(Manifest cenManifest, Manifest locManifest) {
1204+
if (cenManifest == null) {
1205+
crossChkWarnings.add(rb.getString(
1206+
"manifest.missing.when.reading.jarfile"));
1207+
return;
1208+
}
1209+
if (locManifest == null) {
1210+
crossChkWarnings.add(rb.getString(
1211+
"manifest.missing.when.reading.jarinputstream"));
1212+
return;
1213+
}
1214+
1215+
Attributes cenMainAttrs = cenManifest.getMainAttributes();
1216+
Attributes locMainAttrs = locManifest.getMainAttributes();
1217+
1218+
for (Object key : cenMainAttrs.keySet()) {
1219+
Object cenValue = cenMainAttrs.get(key);
1220+
Object locValue = locMainAttrs.get(key);
1221+
1222+
if (locValue == null) {
1223+
crossChkWarnings.add(String.format(rb.getString(
1224+
"manifest.attribute.1.present.when.reading.jarfile.but.missing.via.jarinputstream"),
1225+
key));
1226+
} else if (!cenValue.equals(locValue)) {
1227+
crossChkWarnings.add(String.format(rb.getString(
1228+
"manifest.attribute.1.differs.jarfile.value.2.jarinputstream.value.3"),
1229+
key, cenValue, locValue));
1230+
}
1231+
}
1232+
1233+
for (Object key : locMainAttrs.keySet()) {
1234+
if (!cenMainAttrs.containsKey(key)) {
1235+
crossChkWarnings.add(String.format(rb.getString(
1236+
"manifest.attribute.1.present.when.reading.jarinputstream.but.missing.via.jarfile"),
1237+
key));
1238+
}
1239+
}
1240+
}
1241+
1242+
private void compareSigners(JarEntry cenEntry, JarEntry locEntry) {
1243+
CodeSigner[] cenSigners = cenEntry.getCodeSigners();
1244+
CodeSigner[] locSigners = locEntry.getCodeSigners();
1245+
1246+
boolean cenHasSigners = cenSigners != null;
1247+
boolean locHasSigners = locSigners != null;
1248+
1249+
if (cenHasSigners && locHasSigners) {
1250+
if (!Arrays.equals(cenSigners, locSigners)) {
1251+
crossChkWarnings.add(String.format(rb.getString(
1252+
"codesigners.different.for.entry.1.when.reading.jarfile.and.jarinputstream"),
1253+
cenEntry.getName()));
1254+
}
1255+
} else if (cenHasSigners) {
1256+
crossChkWarnings.add(String.format(rb.getString(
1257+
"entry.1.is.signed.in.jarfile.but.is.not.signed.in.jarinputstream"),
1258+
cenEntry.getName()));
1259+
} else if (locHasSigners) {
1260+
crossChkWarnings.add(String.format(rb.getString(
1261+
"entry.1.is.signed.in.jarinputstream.but.is.not.signed.in.jarfile"),
1262+
locEntry.getName()));
1263+
}
1264+
}
1265+
1266+
private void displayCrossChkWarnings() {
1267+
System.out.println();
1268+
// First is a summary warning
1269+
System.out.println(rb.getString("jar.contains.internal.inconsistencies.result.in.different.contents.via.jarfile.and.jarinputstream"));
1270+
// each warning message with prefix "- "
1271+
crossChkWarnings.forEach(warning -> System.out.println("- " + warning));
1272+
}
1273+
11321274
private void displayMessagesAndResult(boolean isSigning) {
11331275
String result;
11341276
List<String> errors = new ArrayList<>();
@@ -1360,13 +1502,19 @@ private void displayMessagesAndResult(boolean isSigning) {
13601502
System.out.println(rb.getString("Warning."));
13611503
warnings.forEach(System.out::println);
13621504
}
1505+
if (!crossChkWarnings.isEmpty()) {
1506+
displayCrossChkWarnings();
1507+
}
13631508
} else {
13641509
if (!errors.isEmpty() || !warnings.isEmpty()) {
13651510
System.out.println();
13661511
System.out.println(rb.getString("Warning."));
13671512
errors.forEach(System.out::println);
13681513
warnings.forEach(System.out::println);
13691514
}
1515+
if (!crossChkWarnings.isEmpty()) {
1516+
displayCrossChkWarnings();
1517+
}
13701518
}
13711519

13721520
if (!isSigning && (!errors.isEmpty() || !warnings.isEmpty())) {

src/jdk.jartool/share/classes/sun/security/tools/jarsigner/resources/jarsigner.properties

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,17 @@ Cannot.find.environment.variable.=Cannot find environment variable:\u0020
208208
Cannot.find.file.=Cannot find file:\u0020
209209
event.ocsp.check=Contacting OCSP server at %s ...
210210
event.crl.check=Downloading CRL from %s ...
211+
manifest.missing.when.reading.jarfile=Manifest is missing when reading via JarFile
212+
manifest.missing.when.reading.jarinputstream=Manifest is missing when reading via JarInputStream
213+
manifest.attribute.1.present.when.reading.jarfile.but.missing.via.jarinputstream=Manifest main attribute %s is present when reading via JarFile but missing when reading via JarInputStream
214+
manifest.attribute.1.present.when.reading.jarinputstream.but.missing.via.jarfile=Manifest main attribute %s is present when reading via JarInputStream but missing when reading via JarFile
215+
manifest.attribute.1.differs.jarfile.value.2.jarinputstream.value.3=Manifest main attribute %1$s differs: JarFile value = %2$s, JarInputStream value = %3$s
216+
entry.1.present.when.reading.jarinputstream.but.missing.via.jarfile=Entry %s is present when reading via JarInputStream but missing when reading via JarFile
217+
entry.1.present.when.reading.jarfile.but.missing.via.jarinputstream=Entry %s is present when reading via JarFile but missing when reading via JarInputStream
218+
entry.1.present.in.jarfile.but.unreadable=Entry %s is present in JarFile but unreadable
219+
codesigners.different.for.entry.1.when.reading.jarfile.and.jarinputstream=Code signers are different for entry %s when reading from JarFile and JarInputStream
220+
entry.1.is.signed.in.jarfile.but.is.not.signed.in.jarinputstream=Entry %s is signed in JarFile but is not signed in JarInputStream
221+
entry.1.is.signed.in.jarinputstream.but.is.not.signed.in.jarfile=Entry %s is signed in JarInputStream but is not signed in JarFile
222+
jar.contains.internal.inconsistencies.result.in.different.contents.via.jarfile.and.jarinputstream=This JAR file contains internal inconsistencies that may result in different contents when reading via JarFile and JarInputStream:
223+
signature.verification.failed.on.entry.1.when.reading.via.jarinputstream=Signature verification failed on entry %s when reading via JarInputStream
224+
signature.verification.failed.on.entry.1.when.reading.via.jarfile=Signature verification failed on entry %s when reading via JarFile

src/jdk.jartool/share/man/jarsigner.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,11 @@ was selected, and the others were discarded.
929929
hasNonexistentEntries
930930
: This JAR contains signed entries for files that do not exist.
931931

932+
internalInconsistenciesDetected
933+
: This JAR contains internal inconsistencies detected during verification
934+
that may result in different contents when reading via JarFile
935+
and JarInputStream.
936+
932937
legacyAlg
933938
: An algorithm used is considered a security risk but not disabled.
934939

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8339280
27+
* @summary Test that jarsigner -verify emits a warning when the filename of
28+
* an entry in the LOC is changed
29+
* @library /test/lib
30+
* @run junit VerifyJarEntryName
31+
*/
32+
33+
import org.junit.jupiter.api.BeforeAll;
34+
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.Test;
36+
37+
import java.io.FileOutputStream;
38+
import java.nio.charset.StandardCharsets;
39+
import java.nio.file.Files;
40+
import java.nio.file.Path;
41+
import java.util.jar.JarFile;
42+
import java.util.zip.ZipEntry;
43+
import java.util.zip.ZipOutputStream;
44+
45+
import jdk.test.lib.SecurityTools;
46+
import static org.junit.jupiter.api.Assertions.assertTrue;
47+
48+
public class VerifyJarEntryName {
49+
50+
private static final Path ORIGINAL_JAR = Path.of("test.jar");
51+
private static final Path MODIFIED_JAR = Path.of("modified_test.jar");
52+
53+
@BeforeAll
54+
static void setup() throws Exception {
55+
try (FileOutputStream fos = new FileOutputStream(ORIGINAL_JAR.toFile());
56+
ZipOutputStream zos = new ZipOutputStream(fos)) {
57+
zos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
58+
zos.write("Manifest-Version: 1.0\nCreated-By: Test\n".
59+
getBytes(StandardCharsets.UTF_8));
60+
zos.closeEntry();
61+
62+
// Add hello.txt file
63+
ZipEntry textEntry = new ZipEntry("hello.txt");
64+
zos.putNextEntry(textEntry);
65+
zos.write("hello".getBytes(StandardCharsets.UTF_8));
66+
zos.closeEntry();
67+
}
68+
69+
SecurityTools.keytool("-genkeypair -keystore ks -storepass changeit "
70+
+ "-alias mykey -keyalg rsa -dname CN=me ");
71+
72+
SecurityTools.jarsigner("-keystore ks -storepass changeit "
73+
+ ORIGINAL_JAR + " mykey")
74+
.shouldHaveExitValue(0);
75+
}
76+
77+
@BeforeEach
78+
void cleanup() throws Exception {
79+
Files.deleteIfExists(MODIFIED_JAR);
80+
}
81+
82+
/*
83+
* Modify a single byte in "MANIFEST.MF" filename in LOC, and
84+
* validate that jarsigner -verify emits a warning message.
85+
*/
86+
@Test
87+
void verifyManifestEntryName() throws Exception {
88+
modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "MANIFEST.MF");
89+
SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR)
90+
.shouldContain("This JAR file contains internal " +
91+
"inconsistencies that may result in different " +
92+
"contents when reading via JarFile and JarInputStream:")
93+
.shouldContain("- Manifest is missing when " +
94+
"reading via JarInputStream")
95+
.shouldHaveExitValue(0);
96+
}
97+
98+
/*
99+
* Modify a single byte in signature filename in LOC, and
100+
* validate that jarsigner -verify emits a warning message.
101+
*/
102+
@Test
103+
void verifySignatureEntryName() throws Exception {
104+
modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "MYKEY.SF");
105+
SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR)
106+
.shouldContain("This JAR file contains internal " +
107+
"inconsistencies that may result in different " +
108+
"contents when reading via JarFile and JarInputStream:")
109+
.shouldContain("- Entry XETA-INF/MYKEY.SF is present when reading " +
110+
"via JarInputStream but missing when reading via JarFile")
111+
.shouldHaveExitValue(0);
112+
}
113+
114+
/*
115+
* Validate that jarsigner -verify on a valid JAR works without
116+
* emitting warnings about internal inconsistencies.
117+
*/
118+
@Test
119+
void verifyOriginalJar() throws Exception {
120+
SecurityTools.jarsigner("-verify -verbose " + ORIGINAL_JAR)
121+
.shouldNotContain("This JAR file contains internal " +
122+
"inconsistencies that may result in different contents when " +
123+
"reading via JarFile and JarInputStream:")
124+
.shouldHaveExitValue(0);
125+
}
126+
127+
private void modifyJarEntryName(Path origJar, Path modifiedJar,
128+
String entryName) throws Exception {
129+
byte[] jarBytes = Files.readAllBytes(origJar);
130+
var jarString = new String(jarBytes, StandardCharsets.UTF_8);
131+
var pos = jarString.indexOf(entryName);
132+
assertTrue(pos != -1, entryName + " is not present in the JAR");
133+
jarBytes[pos] = 'X';
134+
Files.write(modifiedJar, jarBytes);
135+
}
136+
}

0 commit comments

Comments
 (0)