From 0b24ee857189e139f48826bf2aef10ae8680c11b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Aug 2024 10:56:25 +0100 Subject: [PATCH] Improve loading of jar entry certificates Co-Authored-By: Phillip Webb --- .../boot/loader/jar/JarEntriesStream.java | 125 ++++++++++++++++++ .../boot/loader/jar/JarFileEntries.java | 42 +++--- .../loader/jar/ZipInflaterInputStream.java | 19 ++- .../boot/loader/jar/JarFileTests.java | 20 +++ .../src/test/resources/jars/mismatch.jar | Bin 0 -> 4953 bytes .../boot/loader/jar/JarEntriesStream.java | 125 ++++++++++++++++++ .../boot/loader/jar/SecurityInfo.java | 31 ++--- .../loader/jar/ZipInflaterInputStream.java | 4 +- .../boot/loader/jar/NestedJarFileTests.java | 19 +++ .../src/test/resources/jars/mismatch.jar | Bin 0 -> 4953 bytes 10 files changed, 340 insertions(+), 45 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/jars/mismatch.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/mismatch.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java new file mode 100644 index 000000000000..35d9421874b4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.zip.Inflater; +import java.util.zip.ZipEntry; + +/** + * Helper class to iterate entries in a jar file and check that content matches a related + * entry. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class JarEntriesStream implements Closeable { + + private static final int BUFFER_SIZE = 4 * 1024; + + private final JarInputStream in; + + private final byte[] inBuffer = new byte[BUFFER_SIZE]; + + private final byte[] compareBuffer = new byte[BUFFER_SIZE]; + + private final Inflater inflater = new Inflater(true); + + private JarEntry entry; + + JarEntriesStream(InputStream in) throws IOException { + this.in = new JarInputStream(in); + } + + JarEntry getNextEntry() throws IOException { + this.entry = this.in.getNextJarEntry(); + if (this.entry != null) { + this.entry.getSize(); + } + this.inflater.reset(); + return this.entry; + } + + boolean matches(boolean directory, int size, int compressionMethod, InputStreamSupplier streamSupplier) + throws IOException { + if (this.entry.isDirectory() != directory) { + fail("directory"); + } + if (this.entry.getMethod() != compressionMethod) { + fail("compression method"); + } + if (this.entry.isDirectory()) { + this.in.closeEntry(); + return true; + } + try (DataInputStream expected = new DataInputStream(getInputStream(size, streamSupplier))) { + assertSameContent(expected); + } + return true; + } + + private InputStream getInputStream(int size, InputStreamSupplier streamSupplier) throws IOException { + InputStream inputStream = streamSupplier.get(); + return (this.entry.getMethod() != ZipEntry.DEFLATED) ? inputStream + : new ZipInflaterInputStream(inputStream, this.inflater, size); + } + + private void assertSameContent(DataInputStream expected) throws IOException { + int len; + while ((len = this.in.read(this.inBuffer)) > 0) { + try { + expected.readFully(this.compareBuffer, 0, len); + if (Arrays.equals(this.inBuffer, 0, len, this.compareBuffer, 0, len)) { + continue; + } + } + catch (EOFException ex) { + // Continue and throw exception due to mismatched content length. + } + fail("content"); + } + if (expected.read() != -1) { + fail("content"); + } + } + + private void fail(String check) { + throw new IllegalStateException("Content mismatch when reading security info for entry '%s' (%s check)" + .formatted(this.entry.getName(), check)); + } + + @Override + public void close() throws IOException { + this.inflater.end(); + this.in.close(); + } + + @FunctionalInterface + interface InputStreamSupplier { + + InputStream get() throws IOException; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java index d151c8d80a85..bf4e3bcbbd0e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import java.util.NoSuchElementException; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; -import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; @@ -334,37 +333,30 @@ private AsciiBytes applyFilter(AsciiBytes name) { JarEntryCertification getCertification(JarEntry entry) throws IOException { JarEntryCertification[] certifications = this.certifications; if (certifications == null) { - certifications = new JarEntryCertification[this.size]; - // We fall back to use JarInputStream to obtain the certs. This isn't that - // fast, but hopefully doesn't happen too often. - try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) { - java.util.jar.JarEntry certifiedEntry; - while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) { - // Entry must be closed to trigger a read and set entry certificates - certifiedJarStream.closeEntry(); - int index = getEntryIndex(certifiedEntry.getName()); - if (index != -1) { - certifications[index] = JarEntryCertification.from(certifiedEntry); - } - } - } + certifications = getCertifications(); this.certifications = certifications; } JarEntryCertification certification = certifications[entry.getIndex()]; return (certification != null) ? certification : JarEntryCertification.NONE; } - private int getEntryIndex(CharSequence name) { - int hashCode = AsciiBytes.hashCode(name); - int index = getFirstIndex(hashCode); - while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { - FileHeader candidate = getEntry(index, FileHeader.class, false, null); - if (candidate.hasName(name, NO_SUFFIX)) { - return index; + private JarEntryCertification[] getCertifications() throws IOException { + JarEntryCertification[] certifications = new JarEntryCertification[this.size]; + try (JarEntriesStream entries = new JarEntriesStream(this.jarFile.getData().getInputStream())) { + java.util.jar.JarEntry entry = entries.getNextEntry(); + while (entry != null) { + JarEntry relatedEntry = this.doGetEntry(entry.getName(), JarEntry.class, false, null); + if (relatedEntry != null && entries.matches(relatedEntry.isDirectory(), (int) relatedEntry.getSize(), + relatedEntry.getMethod(), () -> getEntryData(relatedEntry).getInputStream())) { + int index = relatedEntry.getIndex(); + if (index != -1) { + certifications[index] = JarEntryCertification.from(entry); + } + } + entry = entries.getNextEntry(); } - index++; } - return -1; + return certifications; } private static void swap(int[] array, int i, int j) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java index 67624460ccd7..71750d1ab432 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,12 +30,23 @@ */ class ZipInflaterInputStream extends InflaterInputStream { + private final boolean ownsInflator; + private int available; private boolean extraBytesWritten; ZipInflaterInputStream(InputStream inputStream, int size) { - super(inputStream, new Inflater(true), getInflaterBufferSize(size)); + this(inputStream, new Inflater(true), size, true); + } + + ZipInflaterInputStream(InputStream inputStream, Inflater inflater, int size) { + this(inputStream, inflater, size, false); + } + + private ZipInflaterInputStream(InputStream inputStream, Inflater inflater, int size, boolean ownsInflator) { + super(inputStream, inflater, getInflaterBufferSize(size)); + this.ownsInflator = ownsInflator; this.available = size; } @@ -59,7 +70,9 @@ public int read(byte[] b, int off, int len) throws IOException { @Override public void close() throws IOException { super.close(); - this.inf.end(); + if (this.ownsInflator) { + this.inf.end(); + } } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index 0e11c4858226..1b4d02d9a25e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -666,6 +666,26 @@ void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception { } } + @Test + void mismatchedStreamEntriesThrowsException() throws IOException { + File mismatchJar = new File("src/test/resources/jars/mismatch.jar"); + IllegalStateException failure = null; + try (JarFile jarFile = new JarFile(mismatchJar)) { + JarFile nestedJarFile = jarFile.getNestedJarFile(jarFile.getJarEntry("inner.jar")); + Enumeration entries = nestedJarFile.entries(); + while (entries.hasMoreElements()) { + try { + entries.nextElement().getCodeSigners(); + } + catch (IllegalStateException ex) { + failure = (failure != null) ? failure : ex; + } + } + } + assertThat(failure) + .hasMessage("Content mismatch when reading security info for entry 'content' (content check)"); + } + private File createJarFileWithEpochTimeOfZero() throws Exception { File jarFile = new File(this.tempDir, "temp.jar"); FileOutputStream fileOutputStream = new FileOutputStream(jarFile); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/jars/mismatch.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/jars/mismatch.jar new file mode 100644 index 0000000000000000000000000000000000000000..1f096171614ab46183abcf0a709d1ece07a019e3 GIT binary patch literal 4953 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 YBl7UT9n=pnqXJL?ipeixz9Znj01OK8^#A|> literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java new file mode 100644 index 000000000000..35d9421874b4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.zip.Inflater; +import java.util.zip.ZipEntry; + +/** + * Helper class to iterate entries in a jar file and check that content matches a related + * entry. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class JarEntriesStream implements Closeable { + + private static final int BUFFER_SIZE = 4 * 1024; + + private final JarInputStream in; + + private final byte[] inBuffer = new byte[BUFFER_SIZE]; + + private final byte[] compareBuffer = new byte[BUFFER_SIZE]; + + private final Inflater inflater = new Inflater(true); + + private JarEntry entry; + + JarEntriesStream(InputStream in) throws IOException { + this.in = new JarInputStream(in); + } + + JarEntry getNextEntry() throws IOException { + this.entry = this.in.getNextJarEntry(); + if (this.entry != null) { + this.entry.getSize(); + } + this.inflater.reset(); + return this.entry; + } + + boolean matches(boolean directory, int size, int compressionMethod, InputStreamSupplier streamSupplier) + throws IOException { + if (this.entry.isDirectory() != directory) { + fail("directory"); + } + if (this.entry.getMethod() != compressionMethod) { + fail("compression method"); + } + if (this.entry.isDirectory()) { + this.in.closeEntry(); + return true; + } + try (DataInputStream expected = new DataInputStream(getInputStream(size, streamSupplier))) { + assertSameContent(expected); + } + return true; + } + + private InputStream getInputStream(int size, InputStreamSupplier streamSupplier) throws IOException { + InputStream inputStream = streamSupplier.get(); + return (this.entry.getMethod() != ZipEntry.DEFLATED) ? inputStream + : new ZipInflaterInputStream(inputStream, this.inflater, size); + } + + private void assertSameContent(DataInputStream expected) throws IOException { + int len; + while ((len = this.in.read(this.inBuffer)) > 0) { + try { + expected.readFully(this.compareBuffer, 0, len); + if (Arrays.equals(this.inBuffer, 0, len, this.compareBuffer, 0, len)) { + continue; + } + } + catch (EOFException ex) { + // Continue and throw exception due to mismatched content length. + } + fail("content"); + } + if (expected.read() != -1) { + fail("content"); + } + } + + private void fail(String check) { + throw new IllegalStateException("Content mismatch when reading security info for entry '%s' (%s check)" + .formatted(this.entry.getName(), check)); + } + + @Override + public void close() throws IOException { + this.inflater.end(); + this.in.close(); + } + + @FunctionalInterface + interface InputStreamSupplier { + + InputStream get() throws IOException; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java index 3b20bebdbe4d..a6a0a08b1ec2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,30 +81,31 @@ static SecurityInfo get(ZipContent content) { * @return the security info * @throws IOException on I/O error */ + @SuppressWarnings("resource") private static SecurityInfo load(ZipContent content) throws IOException { int size = content.size(); boolean hasSecurityInfo = false; Certificate[][] entryCertificates = new Certificate[size][]; CodeSigner[][] entryCodeSigners = new CodeSigner[size][]; - try (JarInputStream in = new JarInputStream(content.openRawZipData().asInputStream())) { - JarEntry jarEntry = in.getNextJarEntry(); - while (jarEntry != null) { - in.closeEntry(); // Close to trigger a read and set certs/signers - Certificate[] certificates = jarEntry.getCertificates(); - CodeSigner[] codeSigners = jarEntry.getCodeSigners(); - if (certificates != null || codeSigners != null) { - ZipContent.Entry contentEntry = content.getEntry(jarEntry.getName()); - if (contentEntry != null) { + try (JarEntriesStream entries = new JarEntriesStream(content.openRawZipData().asInputStream())) { + JarEntry entry = entries.getNextEntry(); + while (entry != null) { + ZipContent.Entry relatedEntry = content.getEntry(entry.getName()); + if (relatedEntry != null && entries.matches(relatedEntry.isDirectory(), + relatedEntry.getUncompressedSize(), relatedEntry.getCompressionMethod(), + () -> relatedEntry.openContent().asInputStream())) { + Certificate[] certificates = entry.getCertificates(); + CodeSigner[] codeSigners = entry.getCodeSigners(); + if (certificates != null || codeSigners != null) { hasSecurityInfo = true; - entryCertificates[contentEntry.getLookupIndex()] = certificates; - entryCodeSigners[contentEntry.getLookupIndex()] = codeSigners; + entryCertificates[relatedEntry.getLookupIndex()] = certificates; + entryCodeSigners[relatedEntry.getLookupIndex()] = codeSigners; } } - jarEntry = in.getNextJarEntry(); + entry = entries.getNextEntry(); } - return (!hasSecurityInfo) ? NONE : new SecurityInfo(entryCertificates, entryCodeSigners); } - + return (!hasSecurityInfo) ? NONE : new SecurityInfo(entryCertificates, entryCodeSigners); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java index 1528f0b9c507..095d24874916 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ * * @author Phillip Webb */ -abstract class ZipInflaterInputStream extends InflaterInputStream { +class ZipInflaterInputStream extends InflaterInputStream { private int available; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java index ad7882a4e6cd..f1d381cf308c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -412,6 +412,25 @@ void getCommentAlignsWithJdkJar() throws Exception { assertThat(nested).isEqualTo(jdk); } + @Test + void mismatchedStreamEntriesThrowsException() throws IOException { + File mismatchJar = new File("src/test/resources/jars/mismatch.jar"); + IllegalStateException failure = null; + try (NestedJarFile innerJar = new NestedJarFile(mismatchJar, "inner.jar")) { + Enumeration entries = innerJar.entries(); + while (entries.hasMoreElements()) { + try { + entries.nextElement().getCodeSigners(); + } + catch (IllegalStateException ex) { + failure = (failure != null) ? failure : ex; + } + } + } + assertThat(failure) + .hasMessage("Content mismatch when reading security info for entry 'content' (content check)"); + } + private List collectComments(JarFile jarFile) throws IOException { try { List comments = new ArrayList<>(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/mismatch.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/mismatch.jar new file mode 100644 index 0000000000000000000000000000000000000000..1f096171614ab46183abcf0a709d1ece07a019e3 GIT binary patch literal 4953 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 YBl7UT9n=pnqXJL?ipeixz9Znj01OK8^#A|> literal 0 HcmV?d00001