spring-framework icon indicating copy to clipboard operation
spring-framework copied to clipboard

AOT mode with signed JARs fails

Open mhalbritter opened this issue 1 year ago • 5 comments

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]

mhalbritter avatar Aug 26 '22 08:08 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

mhalbritter avatar Aug 26 '22 08:08 mhalbritter

Maybe a GraalVM bug to raise on their bugtracker?

sdeleuze avatar Aug 26 '22 09:08 sdeleuze

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.

mhalbritter avatar Aug 26 '22 09:08 mhalbritter

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?

stliu avatar Aug 29 '22 08:08 stliu

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.

sdeleuze avatar Sep 19 '22 06:09 sdeleuze

@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.

snicoll avatar Sep 22 '22 12:09 snicoll

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.

sdeleuze avatar Sep 23 '22 04:09 sdeleuze

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.

snicoll avatar Sep 23 '22 09:09 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.

snicoll avatar Sep 23 '22 14:09 snicoll

Also blocked by #28875

snicoll avatar Sep 26 '22 12:09 snicoll

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.

sdeleuze avatar Sep 28 '22 09:09 sdeleuze

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.

jhoeller avatar Oct 05 '22 15:10 jhoeller