Skip to content

Commit 33c5e12

Browse files
committed
Write signature files to uber jars to for Oracle Java 17 verification
Update Gradle and Maven plugins to write an empty `META-INF/BOOT.SF` file whenever there is a nested signed jar. This update allows Oracle Java 17 to correctly verify the nested JARs. The file is required because `JarVerifier` has code roughly equivalent to: if (!jarManifestNameChecked && SharedSecrets .getJavaUtilZipFileAccess().getManifestName(jf, true) == null) { throw new JarException("The JCE Provider " + jarURL.toString() + " is not signed."); } The `SharedSecrets.getJavaUtilZipFileAccess().getManifestName(jf, true)` call ends up in `ZipFile.getManifestName(onlyIfSignatureRelatedFiles)` which is a private method that we cannot override in our `NestedJarFile` subclass. By writing an empty `.SF` file we ensure that the `Manifest` is always returned because there are always "signature related files". Fixes gh-28837
1 parent fe752de commit 33c5e12

File tree

17 files changed

+266
-17
lines changed

17 files changed

+266
-17
lines changed

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,13 @@ private String determineSpringBootVersion() {
123123
}
124124

125125
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies,
126-
LoaderImplementation loaderImplementation) {
127-
return createCopyAction(jar, resolvedDependencies, loaderImplementation, null, null);
126+
LoaderImplementation loaderImplementation, boolean supportsSignatureFile) {
127+
return createCopyAction(jar, resolvedDependencies, loaderImplementation, supportsSignatureFile, null, null);
128128
}
129129

130130
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies,
131-
LoaderImplementation loaderImplementation, LayerResolver layerResolver, String layerToolsLocation) {
131+
LoaderImplementation loaderImplementation, boolean supportsSignatureFile, LayerResolver layerResolver,
132+
String layerToolsLocation) {
132133
File output = jar.getArchiveFile().get().getAsFile();
133134
Manifest manifest = jar.getManifest();
134135
boolean preserveFileTimestamps = jar.isPreserveFileTimestamps();
@@ -143,7 +144,8 @@ CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies,
143144
String encoding = jar.getMetadataCharset();
144145
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode,
145146
includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec,
146-
compressionResolver, encoding, resolvedDependencies, layerResolver, loaderImplementation);
147+
compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver,
148+
loaderImplementation);
147149
return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action;
148150
}
149151

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ protected CopyAction createCopyAction() {
147147
if (!isLayeredDisabled()) {
148148
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
149149
String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null;
150-
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver,
151-
layerToolsLocation);
150+
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true,
151+
layerResolver, layerToolsLocation);
152152
}
153-
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation);
153+
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true);
154154
}
155155

156156
@Override

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ protected CopyAction createCopyAction() {
121121
if (!isLayeredDisabled()) {
122122
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
123123
String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null;
124-
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver,
125-
layerToolsLocation);
124+
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false,
125+
layerResolver, layerToolsLocation);
126126
}
127-
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation);
127+
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false);
128128
}
129129

130130
@Override

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ class BootZipCopyAction implements CopyAction {
111111

112112
private final ResolvedDependencies resolvedDependencies;
113113

114+
private final boolean supportsSignatureFile;
115+
114116
private final LayerResolver layerResolver;
115117

116118
private final LoaderImplementation loaderImplementation;
@@ -119,7 +121,7 @@ class BootZipCopyAction implements CopyAction {
119121
boolean includeDefaultLoader, String layerToolsLocation, Spec<FileTreeElement> requiresUnpack,
120122
Spec<FileTreeElement> exclusions, LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
121123
Function<FileCopyDetails, ZipCompression> compressionResolver, String encoding,
122-
ResolvedDependencies resolvedDependencies, LayerResolver layerResolver,
124+
ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile, LayerResolver layerResolver,
123125
LoaderImplementation loaderImplementation) {
124126
this.output = output;
125127
this.manifest = manifest;
@@ -135,6 +137,7 @@ class BootZipCopyAction implements CopyAction {
135137
this.compressionResolver = compressionResolver;
136138
this.encoding = encoding;
137139
this.resolvedDependencies = resolvedDependencies;
140+
this.supportsSignatureFile = supportsSignatureFile;
138141
this.layerResolver = layerResolver;
139142
this.loaderImplementation = loaderImplementation;
140143
}
@@ -302,6 +305,7 @@ private String getParentDirectory(String name) {
302305
void finish() throws IOException {
303306
writeLoaderEntriesIfNecessary(null);
304307
writeJarToolsIfNecessary();
308+
writeSignatureFileIfNecessary();
305309
writeClassPathIndexIfNecessary();
306310
writeNativeImageArgFileIfNecessary();
307311
// We must write the layer index last
@@ -351,6 +355,22 @@ private void writeJarModeLibrary(String location, JarModeLibrary library) throws
351355
}
352356
}
353357

358+
private void writeSignatureFileIfNecessary() throws IOException {
359+
if (BootZipCopyAction.this.supportsSignatureFile && hasSignedLibrary()) {
360+
writeEntry("META-INF/BOOT.SF", (out) -> {
361+
}, false);
362+
}
363+
}
364+
365+
private boolean hasSignedLibrary() throws IOException {
366+
for (FileCopyDetails writtenLibrary : this.writtenLibraries.values()) {
367+
if (FileUtils.isSignedJarFile(writtenLibrary.getFile())) {
368+
return true;
369+
}
370+
}
371+
return false;
372+
}
373+
354374
private void writeClassPathIndexIfNecessary() throws IOException {
355375
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
356376
String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616

1717
package org.springframework.boot.gradle.tasks.bundling;
1818

19+
import java.io.File;
1920
import java.io.IOException;
2021
import java.util.Arrays;
2122
import java.util.Set;
2223
import java.util.TreeSet;
24+
import java.util.jar.JarFile;
2325

2426
import org.gradle.testkit.runner.BuildResult;
27+
import org.gradle.testkit.runner.TaskOutcome;
2528
import org.junit.jupiter.api.TestTemplate;
2629

2730
import org.springframework.boot.gradle.junit.GradleCompatibility;
@@ -42,6 +45,15 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
4245
super("bootJar", "BOOT-INF/lib/", "BOOT-INF/classes/", "BOOT-INF/");
4346
}
4447

48+
@TestTemplate
49+
void signed() throws Exception {
50+
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
51+
File jar = new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0];
52+
try (JarFile jarFile = new JarFile(jar)) {
53+
assertThat(jarFile.getEntry("META-INF/BOOT.SF")).isNotNull();
54+
}
55+
}
56+
4557
@TestTemplate
4658
void whenAResolvableCopyOfAnUnresolvableConfigurationIsResolvedThenResolutionSucceeds() {
4759
this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.0").build("build");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '{version}'
4+
}
5+
6+
bootJar {
7+
mainClass = 'com.example.Application'
8+
}
9+
10+
repositories {
11+
mavenCentral()
12+
maven { url "file:repository" }
13+
}
14+
15+
dependencies {
16+
implementation("org.bouncycastle:bcprov-jdk18on:1.76")
17+
}

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 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.
@@ -18,6 +18,9 @@
1818

1919
import java.io.File;
2020
import java.io.IOException;
21+
import java.util.jar.Attributes;
22+
import java.util.jar.JarFile;
23+
import java.util.jar.Manifest;
2124

2225
/**
2326
* Utilities for manipulating files and directories in Spring Boot tooling.
@@ -61,4 +64,31 @@ public static String sha1Hash(File file) throws IOException {
6164
return Digest.sha1(InputStreamSupplier.forFile(file));
6265
}
6366

67+
/**
68+
* Returns {@code true} if the given jar file has been signed.
69+
* @param file the file to check
70+
* @return if the file has been signed
71+
* @throws IOException on IO error
72+
*/
73+
public static boolean isSignedJarFile(File file) throws IOException {
74+
try (JarFile jarFile = new JarFile(file)) {
75+
if (hasDigestEntry(jarFile.getManifest())) {
76+
return true;
77+
}
78+
}
79+
return false;
80+
}
81+
82+
private static boolean hasDigestEntry(Manifest manifest) {
83+
return (manifest != null) && manifest.getEntries().values().stream().anyMatch(FileUtils::hasDigestName);
84+
}
85+
86+
private static boolean hasDigestName(Attributes attributes) {
87+
return attributes.keySet().stream().anyMatch(FileUtils::isDigestName);
88+
}
89+
90+
private static boolean isDigestName(Object name) {
91+
return String.valueOf(name).toUpperCase().endsWith("-DIGEST");
92+
}
93+
6494
}

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ private void write(JarFile sourceJar, AbstractJarWriter writer, PackagedLibrarie
217217
if (isLayered()) {
218218
writeLayerIndex(writer);
219219
}
220+
writeSignatureFileIfNecessary(writtenLibraries, writer);
220221
}
221222

222223
private void writeLoaderClasses(AbstractJarWriter writer) throws IOException {
@@ -263,6 +264,10 @@ private void writeLayerIndex(AbstractJarWriter writer) throws IOException {
263264
}
264265
}
265266

267+
protected void writeSignatureFileIfNecessary(Map<String, Library> writtenLibraries, AbstractJarWriter writer)
268+
throws IOException {
269+
}
270+
266271
private EntryTransformer getEntityTransformer() {
267272
if (getLayout() instanceof RepackagingLayout repackagingLayout) {
268273
return new RepackagingEntryTransformer(repackagingLayout);

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 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.
@@ -19,6 +19,7 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.nio.file.attribute.FileTime;
22+
import java.util.Map;
2223
import java.util.jar.JarFile;
2324

2425
import org.springframework.util.Assert;
@@ -46,6 +47,24 @@ public Repackager(File source) {
4647
super(source);
4748
}
4849

50+
@Override
51+
protected void writeSignatureFileIfNecessary(Map<String, Library> writtenLibraries, AbstractJarWriter writer)
52+
throws IOException {
53+
if (getSource().getName().toLowerCase().endsWith(".jar") && hasSignedLibrary(writtenLibraries)) {
54+
writer.writeEntry("META-INF/BOOT.SF", (entryWriter) -> {
55+
});
56+
}
57+
}
58+
59+
private boolean hasSignedLibrary(Map<String, Library> writtenLibraries) throws IOException {
60+
for (Library library : writtenLibraries.values()) {
61+
if (!(library instanceof JarModeLibrary) && FileUtils.isSignedJarFile(library.getFile())) {
62+
return true;
63+
}
64+
}
65+
return false;
66+
}
67+
4968
/**
5069
* Sets if source files should be backed up when they would be overwritten.
5170
* @param backupSource if source files should be backed up

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ private File createLibraryJar() throws IOException {
660660
return library.getFile();
661661
}
662662

663-
private Library newLibrary(File file, LibraryScope scope, boolean unpackRequired) {
663+
protected Library newLibrary(File file, LibraryScope scope, boolean unpackRequired) {
664664
return new Library(null, file, scope, null, unpackRequired, false, true);
665665
}
666666

@@ -687,7 +687,7 @@ protected boolean hasPackagedLauncherClasses() throws IOException {
687687
&& hasPackagedEntry("org/springframework/boot/loader/launch/JarLauncher.class");
688688
}
689689

690-
private boolean hasPackagedEntry(String name) throws IOException {
690+
protected boolean hasPackagedEntry(String name) throws IOException {
691691
return getPackagedEntry(name) != null;
692692
}
693693

0 commit comments

Comments
 (0)