Skip to content

Commit 0b24ee8

Browse files
wilkinsonaphilwebb
andcommitted
Improve loading of jar entry certificates
Co-Authored-By: Phillip Webb <[email protected]>
1 parent 112cfc8 commit 0b24ee8

File tree

10 files changed

+340
-45
lines changed

10 files changed

+340
-45
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.jar;
18+
19+
import java.io.Closeable;
20+
import java.io.DataInputStream;
21+
import java.io.EOFException;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.util.Arrays;
25+
import java.util.jar.JarEntry;
26+
import java.util.jar.JarInputStream;
27+
import java.util.zip.Inflater;
28+
import java.util.zip.ZipEntry;
29+
30+
/**
31+
* Helper class to iterate entries in a jar file and check that content matches a related
32+
* entry.
33+
*
34+
* @author Phillip Webb
35+
* @author Andy Wilkinson
36+
*/
37+
class JarEntriesStream implements Closeable {
38+
39+
private static final int BUFFER_SIZE = 4 * 1024;
40+
41+
private final JarInputStream in;
42+
43+
private final byte[] inBuffer = new byte[BUFFER_SIZE];
44+
45+
private final byte[] compareBuffer = new byte[BUFFER_SIZE];
46+
47+
private final Inflater inflater = new Inflater(true);
48+
49+
private JarEntry entry;
50+
51+
JarEntriesStream(InputStream in) throws IOException {
52+
this.in = new JarInputStream(in);
53+
}
54+
55+
JarEntry getNextEntry() throws IOException {
56+
this.entry = this.in.getNextJarEntry();
57+
if (this.entry != null) {
58+
this.entry.getSize();
59+
}
60+
this.inflater.reset();
61+
return this.entry;
62+
}
63+
64+
boolean matches(boolean directory, int size, int compressionMethod, InputStreamSupplier streamSupplier)
65+
throws IOException {
66+
if (this.entry.isDirectory() != directory) {
67+
fail("directory");
68+
}
69+
if (this.entry.getMethod() != compressionMethod) {
70+
fail("compression method");
71+
}
72+
if (this.entry.isDirectory()) {
73+
this.in.closeEntry();
74+
return true;
75+
}
76+
try (DataInputStream expected = new DataInputStream(getInputStream(size, streamSupplier))) {
77+
assertSameContent(expected);
78+
}
79+
return true;
80+
}
81+
82+
private InputStream getInputStream(int size, InputStreamSupplier streamSupplier) throws IOException {
83+
InputStream inputStream = streamSupplier.get();
84+
return (this.entry.getMethod() != ZipEntry.DEFLATED) ? inputStream
85+
: new ZipInflaterInputStream(inputStream, this.inflater, size);
86+
}
87+
88+
private void assertSameContent(DataInputStream expected) throws IOException {
89+
int len;
90+
while ((len = this.in.read(this.inBuffer)) > 0) {
91+
try {
92+
expected.readFully(this.compareBuffer, 0, len);
93+
if (Arrays.equals(this.inBuffer, 0, len, this.compareBuffer, 0, len)) {
94+
continue;
95+
}
96+
}
97+
catch (EOFException ex) {
98+
// Continue and throw exception due to mismatched content length.
99+
}
100+
fail("content");
101+
}
102+
if (expected.read() != -1) {
103+
fail("content");
104+
}
105+
}
106+
107+
private void fail(String check) {
108+
throw new IllegalStateException("Content mismatch when reading security info for entry '%s' (%s check)"
109+
.formatted(this.entry.getName(), check));
110+
}
111+
112+
@Override
113+
public void close() throws IOException {
114+
this.inflater.end();
115+
this.in.close();
116+
}
117+
118+
@FunctionalInterface
119+
interface InputStreamSupplier {
120+
121+
InputStream get() throws IOException;
122+
123+
}
124+
125+
}

spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java

+17-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,7 +26,6 @@
2626
import java.util.NoSuchElementException;
2727
import java.util.jar.Attributes;
2828
import java.util.jar.Attributes.Name;
29-
import java.util.jar.JarInputStream;
3029
import java.util.jar.Manifest;
3130
import java.util.zip.ZipEntry;
3231

@@ -334,37 +333,30 @@ private AsciiBytes applyFilter(AsciiBytes name) {
334333
JarEntryCertification getCertification(JarEntry entry) throws IOException {
335334
JarEntryCertification[] certifications = this.certifications;
336335
if (certifications == null) {
337-
certifications = new JarEntryCertification[this.size];
338-
// We fall back to use JarInputStream to obtain the certs. This isn't that
339-
// fast, but hopefully doesn't happen too often.
340-
try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) {
341-
java.util.jar.JarEntry certifiedEntry;
342-
while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) {
343-
// Entry must be closed to trigger a read and set entry certificates
344-
certifiedJarStream.closeEntry();
345-
int index = getEntryIndex(certifiedEntry.getName());
346-
if (index != -1) {
347-
certifications[index] = JarEntryCertification.from(certifiedEntry);
348-
}
349-
}
350-
}
336+
certifications = getCertifications();
351337
this.certifications = certifications;
352338
}
353339
JarEntryCertification certification = certifications[entry.getIndex()];
354340
return (certification != null) ? certification : JarEntryCertification.NONE;
355341
}
356342

357-
private int getEntryIndex(CharSequence name) {
358-
int hashCode = AsciiBytes.hashCode(name);
359-
int index = getFirstIndex(hashCode);
360-
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
361-
FileHeader candidate = getEntry(index, FileHeader.class, false, null);
362-
if (candidate.hasName(name, NO_SUFFIX)) {
363-
return index;
343+
private JarEntryCertification[] getCertifications() throws IOException {
344+
JarEntryCertification[] certifications = new JarEntryCertification[this.size];
345+
try (JarEntriesStream entries = new JarEntriesStream(this.jarFile.getData().getInputStream())) {
346+
java.util.jar.JarEntry entry = entries.getNextEntry();
347+
while (entry != null) {
348+
JarEntry relatedEntry = this.doGetEntry(entry.getName(), JarEntry.class, false, null);
349+
if (relatedEntry != null && entries.matches(relatedEntry.isDirectory(), (int) relatedEntry.getSize(),
350+
relatedEntry.getMethod(), () -> getEntryData(relatedEntry).getInputStream())) {
351+
int index = relatedEntry.getIndex();
352+
if (index != -1) {
353+
certifications[index] = JarEntryCertification.from(entry);
354+
}
355+
}
356+
entry = entries.getNextEntry();
364357
}
365-
index++;
366358
}
367-
return -1;
359+
return certifications;
368360
}
369361

370362
private static void swap(int[] array, int i, int j) {

spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,12 +30,23 @@
3030
*/
3131
class ZipInflaterInputStream extends InflaterInputStream {
3232

33+
private final boolean ownsInflator;
34+
3335
private int available;
3436

3537
private boolean extraBytesWritten;
3638

3739
ZipInflaterInputStream(InputStream inputStream, int size) {
38-
super(inputStream, new Inflater(true), getInflaterBufferSize(size));
40+
this(inputStream, new Inflater(true), size, true);
41+
}
42+
43+
ZipInflaterInputStream(InputStream inputStream, Inflater inflater, int size) {
44+
this(inputStream, inflater, size, false);
45+
}
46+
47+
private ZipInflaterInputStream(InputStream inputStream, Inflater inflater, int size, boolean ownsInflator) {
48+
super(inputStream, inflater, getInflaterBufferSize(size));
49+
this.ownsInflator = ownsInflator;
3950
this.available = size;
4051
}
4152

@@ -59,7 +70,9 @@ public int read(byte[] b, int off, int len) throws IOException {
5970
@Override
6071
public void close() throws IOException {
6172
super.close();
62-
this.inf.end();
73+
if (this.ownsInflator) {
74+
this.inf.end();
75+
}
6376
}
6477

6578
@Override

spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java

+20
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,26 @@ void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception {
666666
}
667667
}
668668

669+
@Test
670+
void mismatchedStreamEntriesThrowsException() throws IOException {
671+
File mismatchJar = new File("src/test/resources/jars/mismatch.jar");
672+
IllegalStateException failure = null;
673+
try (JarFile jarFile = new JarFile(mismatchJar)) {
674+
JarFile nestedJarFile = jarFile.getNestedJarFile(jarFile.getJarEntry("inner.jar"));
675+
Enumeration<JarEntry> entries = nestedJarFile.entries();
676+
while (entries.hasMoreElements()) {
677+
try {
678+
entries.nextElement().getCodeSigners();
679+
}
680+
catch (IllegalStateException ex) {
681+
failure = (failure != null) ? failure : ex;
682+
}
683+
}
684+
}
685+
assertThat(failure)
686+
.hasMessage("Content mismatch when reading security info for entry 'content' (content check)");
687+
}
688+
669689
private File createJarFileWithEpochTimeOfZero() throws Exception {
670690
File jarFile = new File(this.tempDir, "temp.jar");
671691
FileOutputStream fileOutputStream = new FileOutputStream(jarFile);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.jar;
18+
19+
import java.io.Closeable;
20+
import java.io.DataInputStream;
21+
import java.io.EOFException;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.util.Arrays;
25+
import java.util.jar.JarEntry;
26+
import java.util.jar.JarInputStream;
27+
import java.util.zip.Inflater;
28+
import java.util.zip.ZipEntry;
29+
30+
/**
31+
* Helper class to iterate entries in a jar file and check that content matches a related
32+
* entry.
33+
*
34+
* @author Phillip Webb
35+
* @author Andy Wilkinson
36+
*/
37+
class JarEntriesStream implements Closeable {
38+
39+
private static final int BUFFER_SIZE = 4 * 1024;
40+
41+
private final JarInputStream in;
42+
43+
private final byte[] inBuffer = new byte[BUFFER_SIZE];
44+
45+
private final byte[] compareBuffer = new byte[BUFFER_SIZE];
46+
47+
private final Inflater inflater = new Inflater(true);
48+
49+
private JarEntry entry;
50+
51+
JarEntriesStream(InputStream in) throws IOException {
52+
this.in = new JarInputStream(in);
53+
}
54+
55+
JarEntry getNextEntry() throws IOException {
56+
this.entry = this.in.getNextJarEntry();
57+
if (this.entry != null) {
58+
this.entry.getSize();
59+
}
60+
this.inflater.reset();
61+
return this.entry;
62+
}
63+
64+
boolean matches(boolean directory, int size, int compressionMethod, InputStreamSupplier streamSupplier)
65+
throws IOException {
66+
if (this.entry.isDirectory() != directory) {
67+
fail("directory");
68+
}
69+
if (this.entry.getMethod() != compressionMethod) {
70+
fail("compression method");
71+
}
72+
if (this.entry.isDirectory()) {
73+
this.in.closeEntry();
74+
return true;
75+
}
76+
try (DataInputStream expected = new DataInputStream(getInputStream(size, streamSupplier))) {
77+
assertSameContent(expected);
78+
}
79+
return true;
80+
}
81+
82+
private InputStream getInputStream(int size, InputStreamSupplier streamSupplier) throws IOException {
83+
InputStream inputStream = streamSupplier.get();
84+
return (this.entry.getMethod() != ZipEntry.DEFLATED) ? inputStream
85+
: new ZipInflaterInputStream(inputStream, this.inflater, size);
86+
}
87+
88+
private void assertSameContent(DataInputStream expected) throws IOException {
89+
int len;
90+
while ((len = this.in.read(this.inBuffer)) > 0) {
91+
try {
92+
expected.readFully(this.compareBuffer, 0, len);
93+
if (Arrays.equals(this.inBuffer, 0, len, this.compareBuffer, 0, len)) {
94+
continue;
95+
}
96+
}
97+
catch (EOFException ex) {
98+
// Continue and throw exception due to mismatched content length.
99+
}
100+
fail("content");
101+
}
102+
if (expected.read() != -1) {
103+
fail("content");
104+
}
105+
}
106+
107+
private void fail(String check) {
108+
throw new IllegalStateException("Content mismatch when reading security info for entry '%s' (%s check)"
109+
.formatted(this.entry.getName(), check));
110+
}
111+
112+
@Override
113+
public void close() throws IOException {
114+
this.inflater.end();
115+
this.in.close();
116+
}
117+
118+
@FunctionalInterface
119+
interface InputStreamSupplier {
120+
121+
InputStream get() throws IOException;
122+
123+
}
124+
125+
}

0 commit comments

Comments
 (0)