JDK-8313742 : ZipFile.getManifestName fails during jar verification for Spring Boot
  • Type: Bug
  • Component: security-libs
  • Sub-Component: javax.crypto
  • Affected Version: 17,20,21
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2023-08-03
  • Updated: 2023-12-26
  • Resolved: 2023-11-09
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 17 JDK 21 JDK 22
17.0.10-oracleFixed 21.0.2Fixed 22 b24Fixed
Description
ADDITIONAL SYSTEM INFORMATION :
Windows 10 22H2

java version "17.0.8" 2023-07-18 LTS
Java(TM) SE Runtime Environment (build 17.0.8+9-LTS-211)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.8+9-LTS-211, mixed mode, sharing)

A DESCRIPTION OF THE PROBLEM :
Loading a keystore using BouncyCastleProvider causes the following error:

error constructing MAC: java.lang.SecurityException: JCE cannot authenticate the provider BC

A debugging session revealed the following details.

javax.crypto.JarVerifier.verifySingleFile first closes the file:

Enumeration<JarEntry> entries = jf.entries();

and later tries to read it:

if (!jarManifestNameChecked && SharedSecrets.getJavaUtilZipFileAccess().getManifestName(jf, true) == null) {

which causes a "zip file closed" error.

The problem is well known:
https://github.com/spring-projects/spring-boot/issues/28837
https://github.com/bcgit/bc-java/issues/1067

REGRESSION : Last worked in version 11.0.20

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
I reproduced the issue in a simple spring boot application:
See demoBugJarVerifier-main.zip
Readme file explains the details, but it boils down to building a fat jar and launching it.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Program should finish without errors.
ACTUAL -
Program stops with an exception:

Caused by: java.io.IOException: error constructing MAC: java.lang.SecurityException: JCE cannot authenticate the provider BC

---------- BEGIN SOURCE ----------
See demoBugJarVerifier-main.zip
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Use jks (not pkcs12) or don't use fat jar for bcprov-jdk15on.

FREQUENCY : always



Comments
[~serb] the JCE security crypto verifier is a feature of the Oracle JDK. OpenJDK doesn't verify crypto providers.
19-12-2023

@coffeys how this issues/regression was fixed? The fix version is 22 and the repo for it is open but there is no any commits associated with bug, is it a duplicate of some other bug?
19-12-2023

Note that the Spring application should work fine outside of the Spring jar environment. i.e. For unix, extract the jar and run: $JDK_HOME/bin/java -cp "BOOT-INF/lib/*:." org.springframework.boot.loader.JarLauncher or for windows $JDK_HOME/bin/java -cp "BOOT-INF/lib/*;." org.springframework.boot.loader.JarLauncher
22-11-2023

Ran the reproducer through debugger : during init phase spring boot application seems to load jar resources and then close them out : close:831, ZipFile (java.util.zip) <init>:131, JarFile (org.springframework.boot.loader.jar) <init>:125, JarFile (org.springframework.boot.loader.jar) createJarFileFromFileEntry:335, JarFile (org.springframework.boot.loader.jar) createJarFileFromEntry:312, JarFile (org.springframework.boot.loader.jar) getNestedJarFile:301, JarFile (org.springframework.boot.loader.jar) getNestedJarFile:290, JarFile (org.springframework.boot.loader.jar) getNestedArchive:116, JarFileArchive (org.springframework.boot.loader.archive) adapt:274, JarFileArchive$NestedArchiveIterator (org.springframework.boot.loader.archive) adapt:265, JarFileArchive$NestedArchiveIterator (org.springframework.boot.loader.archive) next:226, JarFileArchive$AbstractIterator (org.springframework.boot.loader.archive) createClassLoader:104, ExecutableArchiveLauncher (org.springframework.boot.loader) launch:55, Launcher (org.springframework.boot.loader) main:65, JarLauncher (org.springframework.boot.loader) At jar verification phase, we call into verifySingleJar: verifySingleJar:380, JarVerifier (javax.crypto) verifyJars:320, JarVerifier (javax.crypto) verify:263, JarVerifier (javax.crypto) verify:130, ProviderVerifier (javax.crypto) verifyProvider:196, JceSecurity (javax.crypto) apply:222, JceSecurity$2 (javax.crypto) apply:211, JceSecurity$2 (javax.crypto) computeIfAbsent:1710, ConcurrentHashMap (java.util.concurrent) getVerificationResult:211, JceSecurity (javax.crypto) getInstance:146, JceSecurity (javax.crypto) getInstance:272, Mac (javax.crypto) createMac:-1, ProviderJcaJceHelper (org.bouncycastle.jcajce.util) calculatePbeMac:-1, PKCS12KeyStoreSpi (org.bouncycastle.jcajce.provider.keystore.pkcs12) engineLoad:-1, PKCS12KeyStoreSpi (org.bouncycastle.jcajce.provider.keystore.pkcs12) engineLoad:-1, AdaptingKeyStoreSpi (org.bouncycastle.jcajce.provider.keystore.util) load:1500, KeyStore (java.security) main:16, DemoBugJarVerifierApplication (com.example.demoBugJarVerifier) invokeStatic:-1, LambdaForm$DMH/0x00007f79b4014800 (java.lang.invoke) invoke:-1, LambdaForm$MH/0x00007f79b4015c00 (java.lang.invoke) invokeExact_MT:-1, Invokers$Holder (java.lang.invoke) invokeImpl:154, DirectMethodHandleAccessor (jdk.internal.reflect) invoke:103, DirectMethodHandleAccessor (jdk.internal.reflect) invoke:580, Method (java.lang.reflect) run:49, MainMethodRunner (org.springframework.boot.loader) launch:95, Launcher (org.springframework.boot.loader) launch:58, Launcher (org.springframework.boot.loader) main:65, JarLauncher (org.springframework.boot.loader) logic in verifySingleJar to analyze the manifest if present: if (!jarManifestNameChecked && SharedSecrets.getJavaUtilZipFileAccess().getManifestName( jf, true) == null) { throw new JarException("The JCE Provider " + jarURL.toString() + " is not signed."); } The getManifestName method boils down to: public String getManifestName(JarFile jar, boolean onlyIfHasSignatureRelatedFiles) { return ((ZipFile)jar).getManifestName(onlyIfHasSignatureRelatedFiles); } That method has an ensureOpen() call and we fail since the spring boot env has closed out the jar file. private String getManifestName(boolean onlyIfSignatureRelatedFiles) { synchronized (this) { ensureOpen(); // *********** Source zsrc = res.zsrc; int pos = zsrc.manifestPos;
29-08-2023

I think this issue would have to be evaluated in the spring-boot project. It appears that the spring-boot project extends the java.util.jar.JarFile. That extended JarFile instances get used by the spring-boot launcher code. There's a specific piece of code in the extended class implementation here which immediately calls close() on the java.util.jar.JarFile https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java#L131 which thus marks the JarFile instance as closed. I am not familiar with spring-boot and why it does this.
09-08-2023

The observations on Windows 11: JDK 11: the reproducer can not run with JDK 11. JDK 17ea+1: Failed, IOException observed. JDK 20: Failed. JDK 21ea+33: Failed.
04-08-2023