从零放弃学习 Spring - 在 Native Image 中使用 Bouncy Castle

基于 Bouncy Castle 的 RSA 实现加解密

我的开发环境是 Oracle GraalVM 17.0.8+9.1,Linux x64。

完整代码参考 native-bc

加密代码就不贴了,参考我的仓库代码,只需要执行:

1
make deps keys run

Fallback Image

在上面环境基础上,执行命令构建 Native Image:

1
2
make native
./rsa

这一步虽然构建出了可执行文件并且也可以正常运行,但请注意控制台输出:

1
Warning: Image 'rsa' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).

不难看出,Native Image 为了做到最大兼容性,默认情况下构建出来的可执行文件可能仍然依赖 JDK,如果想要生成不依赖 JDK 的可执行文件,还需要增加一个 --no-fallback 参数。

Image heap writing found a class not seen during static analysis…

为了构建不依赖 JDK 的可执行文件,尝试执行 make native-no-fallback 构建,然后出现异常:

1
2
3
> com.oracle.svm.core.util.VMError$HostedError: com.oracle.svm.core.util.UserError$UserException: Image heap writing found a class not seen during static analysis. Did a static field or an object referenced from a static field change during native image generation? For example, a lazily initialized cache could have been initialized during image generation, in which case you need to force eager initialization of the cache before static analysis or reset the cache using a field value recomputation.
class: sun.security.x509.X509CertImpl
...

这个异常应该是一个 Bug,我目前没找到正确的解决方法,包括添加元数据配置,有一个可以不出现异常的办法是把 GraalVM 回滚到 22.X 版本。顺便一提,GraalVM 目前命名已经变成 GraalVM Oracle JDK 了,所以版本直接跟着官方的版本走,早期则是年份作为大版本,比如 2022 发布的版本为 GraalVM 22.X。

实际上,经过我测试发现还有一种可以在当前版本绕过这个异常的办法,就是使用 Spring Boot 框架构建 Native Image(我只测试过 Spring Boot 框架,没有测试过其他框架)。其实我也不理解为什么 Spring Boot 构建可以避免上述的异常。

no such algorithm: RSA for provider BC…

参考项目代码 native-spring-bc

把 RSA 加解密代码移入 Spring Boot 项目,使用 mvn -Pnative native:compile 顺利构建出了可执行文件,但是运行时又出现了一个新异常:

1
2
3
4
5
6
7
8
Exception in thread "main" org.bouncycastle.openssl.PEMException: unable to convert key pair: no such algorithm: RSA for provider BC
at org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter.getPublicKey(Unknown Source)
at com.example.nativebc.NativeBcApplication.decodePublicKey(NativeBcApplication.java:47)
at com.example.nativebc.NativeBcApplication.main(NativeBcApplication.java:62)
Caused by: java.security.NoSuchAlgorithmException: no such algorithm: RSA for provider BC
at java.base@17.0.8/sun.security.jca.GetInstance.getService(GetInstance.java:87)
at java.base@17.0.8/sun.security.jca.GetInstance.getInstance(GetInstance.java:206)
...

这是因为 Native Image 的 Security Provider 都是在构建期间确定的,不能在运行期间注册。这里分享两种可以在构建期间注册 Security Provider 的方式。

使用 org.graalvm.nativeimage.hosted.Feature 在构建时执行代码

这个方法参考自 insinfo/java_native

首先需要添加一个依赖:

1
2
3
4
5
6
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>23.0.1</version>
<scope>provided</scope>
</dependency>

然后创建 BouncyCastleFeature 类实现 org.graalvm.nativeimage.hosted.Feature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.nativebc;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;

import java.security.Security;

public class BouncyCastleFeature implements Feature {

@Override
public void afterRegistration(AfterRegistrationAccess access) {
RuntimeClassInitialization.initializeAtBuildTime("org.bouncycastle");
RuntimeClassInitialization.initializeAtRunTime("org.bouncycastle.jcajce.provider.drbg.DRBG$Default");
RuntimeClassInitialization.initializeAtRunTime("org.bouncycastle.jcajce.provider.drbg.DRBG$NonceAndIV");
Security.addProvider(new BouncyCastleProvider());
}

}

这个 Feature 会在构建时做这几件事:

  • org.bouncycastle 下所有子包都标记为构建时初始化
  • 单独标记两个需要运行时初始化的类
  • 注册 BouncyCastleProvider

还没完,创建构建配置 META-INF/native-image/native-image.properties,启用自定义的 Feature:

1
Args = --features=com.example.nativebc.BouncyCastleFeature

然后执行命令:

1
mvn -Pnative native:compile

会观察到构建时输出:

1
2
3
4
5
6
...
[1/8] Initializing...
...
2 user-specific feature(s)
- com.example.nativebc.BouncyCastleFeature
...

这说明自定义的 Feature 生效了,顺利的话你可以在 target/ 目录下找到可以正常执行的 nativebc

完整代码参考 native-spring-bc

通过 java.security 注册 Security Provider

上面是通过 Feature 机制,在构建期间完成 Security Provider 的注册,需要引入官方的 SDK 和指定 Feature。官方文档 [JCA Security Services in Native Image] 中提到了另外一种注册 Security Provider 的办法,就是修改 java.security 文件。Native Image 构建时会从 java.security 读取预定义的 Provider 并按顺序加载。

查看 GraalVM 的 java.security 文件:

1
2
3
4
5
$ rg 'security.provider.\d+' $GRAALVM_HOME/conf/security/java.security
66:security.provider.1=SUN
67:security.provider.2=SunRsaSign
...
77:security.provider.12=SunPKCS11

java.security 文件内容开头的注释也向开发者说明了添加 Provider 的办法,也就是直接把 BC 的 Provider 追加到这个列表后面就可以了:

1
security.provider.13=org.bouncycastle.jce.provider.BouncyCastleProvider

NOTE: 虽然 Native Image 不支持运行时注册 Security Provider,但是对于已经注册的 Provider,允许 removeProvider 以后根据自己想要的顺序重新 addProvider/insertProviderAt

在 java.security 中添加 Provider 相对于前文 Feature 代码中的 Security.addProvider,所以还要通过配置 META-INF/native-image/native-image.properties 完成 Feature 代码中另外两个功能:

1
2
Args = --initialize-at-build-time=org.bouncycastle \
--initialize-at-run-time=org.bouncycastle.jcajce.provider.drbg.DRBG$Default,org.bouncycastle.jcajce.provider.drbg.DRBG$NonceAndIV

配置完成后就是构建和运行测试,没有意外的情况下不会有意外。

补充一下,如果不想修改 GraalVM JDK 中的配置文件,可以通过添加构建参数 -Djava.security.properties=path/to/security.properties 指定一个自定义的 security.properties,在自定义文件中注册 Security Provider。

JCE cannot authenticate the provider BC…

就算用了 Spring Boot 也不是非常顺利,虽然到目前 Native Image 可以成功构建和运行,但现在直接运行 Jar 文件反而会出现异常:

1
2
3
4
5
6
7
8
9
10
java.lang.SecurityException: JCE cannot authenticate the provider BC
Caused by: java.lang.SecurityException: JCE cannot authenticate the provider BC
at java.base/javax.crypto.Cipher.getInstance(Cipher.java:722)
at java.base/javax.crypto.Cipher.getInstance(Cipher.java:642)
...
Caused by: java.lang.IllegalStateException: zip file closed
at java.base/java.util.zip.ZipFile.ensureOpen(ZipFile.java:839)
at java.base/java.util.zip.ZipFile.getManifestName(ZipFile.java:1065)
at java.base/java.util.zip.ZipFile$1.getManifestName(ZipFile.java:1108)
at java.base/javax.crypto.JarVerifier.verifySingleJar(JarVerifier.java:464)

这是因为 Spring Boot 的 Jar in Jar 实现和 JCE 存在一些冲突(我猜的),有一个解决办法就是直接从本地文件加载 BC 对应的 jar 文件,而不是再使用 Spring 的 Jar in Jar,或者等待官方去修复这个问题。

最后

终于靠着瞎编把这个系列写完了。这一个月边学边写博客,全都是为了分享一下之前研究 GraalVM Native Image 构建 Bouncy Castle 的收获,主打一个为了碟醋包饺子。这篇本来是合在上一篇博客里,但是内容比较独立,还是决定拆出来单独发了。

吐槽一下 GraalVM,这个项目也是有点迷惑操作的。我 6 月刚开始研究 Native Image 时,一开始看到官网上的 GraalVM 是 23.0 版本,结果有一天官网突然改版,所有文档链接全部失效,从搜索引擎进文档花式 404 页面,我只能靠着修改链接上的格式找到了部分文档(后来知道在 Github 仓库上找文档)。对比不同版本文档也发现了有些不错的 feature 在调整后消失或者改名了,但是这种改动需要自己理解,如果你搜到了老文档,但是在新文档里没有或者对不上都是一件非常平常的事情。官网改后不久,GraalVM 也变成了 Oracle GraalVM JDK,下载安装的方式也变了,随着这种变动,ghcr 上的 GraalVM 镜像就对不上了,官方也没有及时更新,我在内部 CI/CD 中集成时,还要自己制作 builder 镜像(或者我应该直接使用 spring-boot:build-image 构建的思路?)。一个背靠大公司,备受社区期待的产品,经过了 5 年的打磨以后还是这个状态,我是不理解的。当然我不是一个专业的 Java 开发者,我也就是在自己的角度发表一些偏见。

参考