Original issue on Spring Native side: https://github.com/spring-projects-experimental/spring-native/issues/1699

Hey,

Spring AOT mode currently fails if you're using an auto-configuration from an external JAR which has been signed with jarsigner tool.

I've created a reproducer here: https://github.com/mhalbritter/spring-aot-jarsigner-reproducer

The problem is that dependency.jar contains an auto-configuration named DependencyAutoConfiguration in the dependency package. The dependency.jar has been signed with jarsigner and contains a META-INF/SIGN-KEY.SF file. The AOT mode generates code (dependency.DependencyAutoConfiguration__BeanDefinitions) which uses the same package as in dependency.jar, which is getting included in the main boot JAR. But this JAR doesn't have the same signature on it. This will lead to this exception thrown by the JVM when using gradle bootRun:

java.lang.SecurityException: class "dependency.DependencyAutoConfiguration__BeanDefinitions"'s signer information does not match signer information of other classes in the same package
        at java.base/java.lang.ClassLoader.checkCerts(ClassLoader.java:1158) ~[na:na]
        at java.base/java.lang.ClassLoader.preDefineClass(ClassLoader.java:902) ~[na:na]
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1010) ~[na:na]
        at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150) ~[na:na]
        at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862) ~[na:na]
        at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760) ~[na:na]
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681) ~[na:na]
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639) ~[na:na]
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[na:na]
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) ~[na:na]
        at com.example.signerdemo.SignerDemoApplication__BeanFactoryRegistrations.registerBeanDefinitions(SignerDemoApplication__BeanFactoryRegistrations.java:48) ~[aot/:na]
        at com.example.signerdemo.SignerDemoApplication__ApplicationContextInitializer.initialize(SignerDemoApplication__ApplicationContextInitializer.java:19) ~[aot/:na]
        at com.example.signerdemo.SignerDemoApplication__ApplicationContextInitializer.initialize(SignerDemoApplication__ApplicationContextInitializer.java:13) ~[aot/:na]
        at org.springframework.context.aot.ApplicationContextAotInitializer.initialize(ApplicationContextAotInitializer.java:53) ~[spring-context-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.lambda$addAotGeneratedInitializerIfNecessary$2(SpringApplication.java:419) ~[spring-boot-3.0.0-SNAPSHOT.jar:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.applyInitializers(SpringApplication.java:604) ~[spring-boot-3.0.0-SNAPSHOT.jar:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.prepareContext(SpringApplication.java:380) ~[spring-boot-3.0.0-SNAPSHOT.jar:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:311) ~[spring-boot-3.0.0-SNAPSHOT.jar:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-3.0.0-SNAPSHOT.jar:3.0.0-SNAPSHOT]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-3.0.0-SNAPSHOT.jar:3.0.0-SNAPSHOT]
        at com.example.signerdemo.SignerDemoApplication.main(SignerDemoApplication.java:17) ~[main/:na]

Comment From: mhalbritter

Interestingly, the generated JAR from boot can be run, both in normal and in AOT mode.

But when i try to build the native image with gradle nativeCompile, the building of the image fails with:

[1/7] Initializing...                                                                                    (0,0s @ 0,27GB)
Fatal error: java.lang.SecurityException: class "dependency.SomeBean"'s signer information does not match signer information of other classes in the same package
        at java.base/java.lang.ClassLoader.checkCerts(ClassLoader.java:1158)
        at java.base/java.lang.ClassLoader.preDefineClass(ClassLoader.java:902)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1010)
        at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
        at java.base/java.net.URLClassLoader.defineClass(URLClassLoader.java:524)
        at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:427)
        at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:421)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
        at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:420)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:587)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
        at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
        at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3402)
        at java.base/java.lang.Class.getDeclaredMethod(Class.java:2673)
        at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.buildImage(NativeImageGeneratorRunner.java:359)
        at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.build(NativeImageGeneratorRunner.java:585)
        at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.main(NativeImageGeneratorRunner.java:128)
    Error: Image build request failed with exit status 1

> Task :app:nativeCompile FAILED

Comment From: sdeleuze

Maybe a GraalVM bug to raise on their bugtracker?

Comment From: mhalbritter

I don't think this is a bug, as we're doing something (generating code in the same package as in a signed JAR) which is not allowed by the JVM. I'm quite surprised that the resulting JAR from gradle build runs. I would have expected that to fail in the same way.

Comment From: stliu

Interestingly, the generated JAR from boot can be run, both in normal and in AOT mode. yeah, seems it only throws SecurityException with gradle bootRun but not from java -jar app-0.0.1-SNAPSHOT.jar

what's the differences for these two?

Comment From: sdeleuze

FYI this issue is blocking Azure support deployed JAR to be signed.

what's the differences for these two?

Not sure since that's more a Boot question. Maybe java -jar app-0.0.1-SNAPSHOT.jar just checks the app JAR (which is unsigned) while gradle bootRun is running in exploded mode and try to load dependency-0.0.1-SNAPSHOT.jar which is signed.

Comment From: snicoll

@jhoeller and I brainstormed this morning and we believe that offering an option where AOT does not create a split package should be added. We're even considering this to be the default, with an opt-in optimization to the current behavior.

We like that AOT creates a structure that matches the structure of the original configuration. With Spring Boot in particular, it is very easy to see which auto-configurations were processed. We think we should keep this, by adding this infrastructure under the application's package name. Rather than generating code in org.springframework.boot.web.servlet.SomeAutoConfiguration it could be com.example.myapp.aot.org.springframework.boot.web.servlet.SomeAutoConfiguration or even com.example.myapp.aot.boot.web.servlet.SomeAutoConfiguration where com.example.myapp is the package of the application.

Looking at the API, we've already quite a good abstraction with ClassNameGenerator that we can extend. A first step would be to be able to manage package spaces that do not exist. I've started to work on this.

Comment From: sdeleuze

Looks good, but please let's have data points on the RSS footprint impact and a team discussion before deciding if we switch the default or not.

Comment From: snicoll

Some WIP is here https://github.com/snicoll/spring-framework/tree/gh-29019 - I can see two problems so far:

  • Creating a class for a Feature does not work as it's using the class of the component. We probably need to change ClassNameGenerator to handle both strategies somewho.
  • The generated code does not compile as AccessVisibility is very basic.

Comment From: snicoll

Unfortunately, the default code fragments assume that if an privileged access is required, the generated code is in the package where the privileged member is located. We need to improve that before considering what I've started as an option.

Comment From: snicoll

Also blocked by #28875

Comment From: sdeleuze

I had a deeper look on alternative solutions, and was able to find a workaround on both JVM and native by passing a -Djava.security.properties=custom.security parameter to java or native-image with custom.security content being jdk.jar.disabledAlgorithms=MD2, MD5, RSA, DSA.

Few remarks: - The default configuration provided on most JDK is jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, DSA keySize < 1024. - Another solution could be to explode signed JARs since the verification does not happen on directories, but using -Djava.security.properties looks less involved. - It is still possible to verify the JAR signature with jarsigner -verify foo.jar because we don't modify the JVM default security configuration and the JAR signature itself is valid, the SecurityException appears only when loading classes from split packages.

Comment From: jhoeller

After a lot of consideration, we have realized that this is a problem that is not practical to solve at the core framework level. Our generated configuration needs to have access to package-local elements in common scenarios, not least of it all in order to avoid unnecessary reflection. For that reason, we decided to preserve our package-local generation approach.

If a separate jar with a split package arrangement or different jar signatures turns out to be an issue (also e.g. in the module system), the application build may combine them into a single jar that contains both the original classes and the generated configuration. Alternatively, the application build may also simply remove the jar signature before proceeding.

Comment From: sdeleuze

FYI the currrent workaround proposed to get both Native Build Tools and Buildpacks support is:

<build>
   <plugins>
      <plugin>
         <groupId>org.graalvm.buildtools</groupId>
         <artifactId>native-maven-plugin</artifactId>
         <configuration>
            <buildArgs>
               <arg>-Djava.security.properties=src/main/resources/custom.security</arg>
            </buildArgs>
         </configuration>
      </plugin>
      <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
         <configuration>
            <image>
               <env>
                  <BP_NATIVE_IMAGE_BUILD_ARGUMENTS>-Djava.security.properties=/workspace/BOOT-INF/classes/custom.security</BP_NATIVE_IMAGE_BUILD_ARGUMENTS>
               </env>
            </image>
         </configuration>
      </plugin>
   </plugins>
</build>

With an src/main/resources/custom.security file with the following content:

jdk.jar.disabledAlgorithms=MD2, MD5, RSA, DSA