Apache Pulsar 中 Bouncy Castle 安全提供者的打包机制与 FIPS 切换实战指南
【免费下载链接】pulsarApache Pulsar - distributed pub-sub messaging system项目地址: https://gitcode.com/gh_mirrors/pulsar28/pulsar
导读
Apache Pulsar 的 TLS 认证、传输加密等安全与加密能力底层依赖Bouncy Castle(BC)这个 Java 加密库。为了让用户能够在一套构建体系下轻松地在 BC 非 FIPS 版本与 FIPS 版本之间切换,Pulsar 在bouncy-castle模块中设计了jar-in-jar的打包方案,并配套提供了BouncyCastleLoader/BouncyCastleFipsLoader两个 Provider 加载器。本文将基于 Pulsar 仓库中的实际文档与源码,讲清 BC 在 Pulsar 中如何被打包、如何被引入、为什么 shaded(fat jar)模块必须排除 BC,以及如何把 Broker 从 BC-non-FIPS 平滑切换到 BC-FIPS。
Bouncy Castle 与 Pulsar 的关系
Bouncy Castle是一个补充 Java 默认 JCE(Java Cryptographic Extension)的 Java 加密库。相对于 Sun/Oracle JVM 自带的 JCE,它提供了更多密码套件(cipher suites)与算法,同时还内置了大量解析 PEM、ASN.1 等晦涩格式的工具类——这些格式通常没有开发者愿意自己重新实现。
在 Pulsar 中,安全与加密切面普遍依赖 Bouncy Castle 的 Jar 包,典型场景包括:
- TLS 认证(TLS Authentication),详见 site2/docs/security-tls-authentication.md,该文档明确指出:"Bouncy Castle Provider 为 Pulsar 提供 TLS 相关的密码套件与算法,如果需要 FIPS 版本,请参考 Bouncy Castle 页面。"
- 传输加密(Transport Encryption / 端到端消息加密),详见 site2/docs/security-encryption.md。
由于安全与加密是 Pulsar 的刚性依赖,BC 的 Jar 会以多种方式出现在 Broker、Client 及其 shaded 产物中,这直接导致了本文要讨论的打包与版本切换问题。
FIPS 与非 FIPS:二者不可共存
Bouncy Castle官方同时提供FIPS 版本与非 FIPS 版本(本文简称 BC-FIPS 与 BC-non-FIPS)。二者在 JVM 中不能同时存在:在一个 JVM 里引入其中一个版本时,必须先排除另一个版本,否则 Provider 注册会出现冲突。
关于 BC-FIPS 的详细安装与配置(如 FIPS 模式下的 self-test、Policy 文件等),Bouncy Castle 官方文档提供了专门的User Guides与Security Policy两份 PDF,需要部署 FIPS 环境时务必参考官方材料。本仓库侧重点是 Pulsar 如何在构建层面支持两种版本的切换。
从 Pulsar 源码可以看到,两套 Provider 在运行时拥有独立的注册名称:
- 非 FIPS 版本注册名为
BC,对应类org.bouncycastle.jce.provider.BouncyCastleProvider; - FIPS 版本注册名为
BCFIPS,对应类org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider。
这两个常量定义在 pulsar-common/src/main/java/org/apache/pulsar/common/util/SecurityUtility.java 中:
public static final String BC_FIPS_PROVIDER_CLASS = "org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider"; public static final String BC_NON_FIPS_PROVIDER_CLASS = "org.bouncycastle.jce.provider.BouncyCastleProvider"; public static final String BC_FIPS = "BCFIPS"; public static final String BC = "BC";运行时SecurityUtility.getProvider()会先检查BC与BCFIPS是否已注册,再决定返回哪个 Provider;从 classpath 加载时则优先尝试非 FIPS 版本,失败后再尝试 FIPS 版本,这是出于向后兼容性的考虑(见 SecurityUtility.java)。isBCFIPS()方法则通过比对 Provider 的类名来判断当前是否处于 FIPS 模式。
Pulsar 的 bouncy-castle 模块:两个子模块 + 一个测试模块
Pulsar 在 bouncy-castle/pom.xml 中定义了一个名为bouncy-castle-parent的父模块,其注释点明了设计目标:"make it easy for user to load Bouncy Castle and Bouncy Castle FIPS",即让用户能方便地引入/排除 BC 与 BC-FIPS。它聚合了三个子模块:
| 子模块 | ArtifactId | 用途 |
|---|---|---|
bc | bouncy-castle-bc | 打包 Pulsar 需要的 BC 非 FIPS Jar,供 NAR/常规依赖使用 |
bcfips | bouncy-castle-bcfips | 打包 Pulsar 需要的 BC FIPS Jar,供 NAR/常规依赖使用 |
bcfips-include-test | bcfips-include-test | 用于验证 Broker + Client 在引入 FIPS 版本后认证功能正常的测试模块 |
jar-in-jar:为什么不能直接打一个 uber-jar
打包思路是把多个 Bouncy Castle Jar 合并进一个bouncy-castle-bc/bouncy-castle-bcfipsJar 中,以简化引入与排除。但这里有一个关键障碍:签名。
每个原始 Bouncy Castle Jar 都与安全相关,BC 官方对每个 JAR 都进行了签名。使用常规 Maven Shade 插件做 re-package 时,Shade 会把 BC Jar解包(explode)并把签名文件放入META-INF。由于签名只对原始 BC Jar 有效,重新合并出的 uber-jar 里这些签名就是非法的。此时运行会报出经典错误:
java.lang.SecurityException: Invalid signature file digest for Manifest main attributes常规解法是在 pom 中把这些签名文件排除掉:
<exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude>但排除签名会引发另一类更难排查的错误,例如:
java.security.NoSuchAlgorithmException: PBEWithSHA256And256BitAES-CBC-BC SecretKeyFactory not available当显式指定算法来源后:
SecretKeyFactory.getInstance("PBEWithSHA256And256BitAES-CBC-BC", "BC")会暴露真正的根因:
java.security.NoSuchProviderException: JCE cannot authenticate the provider BC这正是 JCE 无法认证 Provider 的典型表现——Provider 的 Jar 签名不合法。
为此,Pulsar 采用了executable-packer-maven-plugin(de.ntcomputer:executable-packer-maven-plugin)的jar-in-jar方案:外层是一个可执行 jar,内部嵌套存放原始的、保持完整签名的 Bouncy Castle Jar,从而既保留 BC 的签名有效性,又得到单一可用的 Jar 产物。两个模块的 pom 中都以mainClass指定了对应的 Loader 类:
- bouncy-castle/bc/pom.xml:
mainClass为org.apache.pulsar.bcloader.BouncyCastleLoader; - bouncy-castle/bcfips/pom.xml:
mainClass为org.apache.pulsar.bcloader.BouncyCastleFipsLoader。
使用 jar-in-jar 产物时,需要在依赖声明中显式带上<classifier>pkg</classifier>。
Loader 类的职责
jar-in-jar 的入口(即 pom 中指定的 mainClass)是 Pulsar 自定义的 Provider 加载器,二者均实现org.apache.pulsar.common.util.BCLoader接口(见 pulsar-common/src/main/java/org/apache/pulsar/common/util/BCLoader.java):
public interface BCLoader { Provider getProvider(); }- BouncyCastleLoader.java(非 FIPS):静态初始化块中检查
Security.getProvider("BC")是否为空,为空则Security.addProvider(new BouncyCastleProvider()),并记录 Provider 信息; - BouncyCastleFipsLoader.java(FIPS):逻辑相同,但注册的是
BouncyCastleFipsProvider,名称是BCFIPS。
运行时 Pulsar 的SecurityUtility会根据 classpath 上实际存在的 Provider 类自动选择加载哪一套,这也是两种版本"二选一"在代码层面的落点。
引入 BC-non-FIPS:bouncy-castle-bc 模块
bouncy-castle-bc(由 bouncy-castle/bc/pom.xml 定义)打包了 Pulsar 所需的非 FIPS Jar,以 jar-in-jar 形式发布(需要<classifier>pkg</classifier>)。其依赖如下:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>${bouncycastle.version}</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-ext-jdk15on</artifactId> <version>${bouncycastle.version}</version> </dependency>当前仓库根 pom.xml 中定义的版本为:
<bouncycastle.version>1.69</bouncycastle.version> <bouncycastlefips.version>1.0.2</bouncycastlefips.version>其中bcprov-ext-jdk15on提供带扩展算法实现的加密 Provider,bcpkix-jdk15on提供 PKIX、CMS、TSP 等更高层的证书与消息处理 API。通过bouncy-castle-bc这一个模块,用户即可完成对 BC 非 FIPS Jar 集合的整体引入或整体排除。
哪些模块默认带上了 bouncy-castle-bc
Pulsar Client 侧需要使用 Bouncy Castle,因此pulsar-client-original(pulsar-client模块)会引入bouncy-castle-bc,并设置<classifier>pkg</classifier>指向 jar-in-jar 产物(见 pulsar-client/pom.xml):
<dependency> <groupId>org.apache.pulsar</groupId> <artifactId>bouncy-castle-bc</artifactId> <version>${project.parent.version}</version> <classifier>pkg</classifier> </dependency>而pulsar-client-original又被大量其他模块依赖,例如pulsar-client-admin、pulsar-broker(pulsar-broker/pom.xml 依赖pulsar-client-original),因此默认情况下 BC 非 FIPS Jar 会随着这些模块一起进入 classpath。
shaded 模块为何必须排除 BC
由于上文所述的 jar 签名原因,Pulsar不会把bouncy-castle相关模块直接打进pulsar-client-all及其他的 shaded 产物,例如pulsar-client-shaded、pulsar-client-admin-shaded、pulsar-broker-shaded。在这些 shaded 模块的 maven-shade-plugin 配置中,会对pulsar-client-original做如下过滤:
<filters> <filter> <artifact>org.apache.pulsar:pulsar-client-original</artifact> <includes> <include>**</include> </includes> <excludes> <exclude>org/bouncycastle/**</exclude> </excludes> </filter> </filters>仓库中 pulsar-broker-shaded/pom.xml、pulsar-client-shaded/pom.xml、pulsar-client-admin-shaded/pom.xml 均有同样的排除配置,并配有注释 "bouncycastle jars could not be shaded, or the signatures will be wrong"。
这意味着:这些 fat jar 中不会包含 bouncy-castle 相关 Jar。使用 shaded 产物的用户需要按自己的安全策略自行引入 BC(通常显式声明bouncy-castle-bc或bouncy-castle-bcfips依赖),从而避免因 shade 解包导致的签名失效问题。
引入 BC-FIPS:bouncy-castle-bcfips 模块
bouncy-castle-bcfips(由 bouncy-castle/bcfips/pom.xml 定义)打包了 Pulsar 所需的 FIPS Jar。与bouncy-castle-bc类似,它同样以 jar-in-jar 形式发布,便于整体引入与排除,依赖如下:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bc-fips</artifactId> <version>${bouncycastlefips.version}</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-fips</artifactId> <version>${bouncycastlefips.version}</version> </dependency>即 FIPS 版本对应替换为bc-fips与bcpkix-fips,版本由${bouncycastlefips.version}统一管理(当前仓库为1.0.2)。
实战:从 BC-non-FIPS 切换到 BC-FIPS
切换的本质是"先排除非 FIPS 的bouncy-castle-bc,再引入 FIPS 的bouncy-castle-bcfips(pkgclassifier)"。以pulsar-broker模块为例:
<dependency> <groupId>org.apache.pulsar</groupId> <artifactId>pulsar-broker</artifactId> <version>${pulsar.version}</version> <exclusions> <exclusion> <groupId>org.apache.pulsar</groupId> <artifactId>bouncy-castle-bc</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.pulsar</groupId> <artifactId>bouncy-castle-bcfips</artifactId> <version>${pulsar.version}</version> <classifier>pkg</classifier> </dependency>同样的思路也适用于 Client 侧(pulsar-client-original、pulsar-client-admin等依赖了bouncy-castle-bc的模块):先排除,再引入 FIPS 版本。
参考实现:bcfips-include-test 模块
仓库中 bouncy-castle/bcfips-include-test/pom.xml 是官方提供的最完整切换示例:它在依赖pulsar-broker的两处声明(test-jar 与普通 test 依赖)中都排除了bouncy-castle-bc,然后单独引入bouncy-castle-bcfips:
<dependency> <groupId>org.apache.pulsar</groupId> <artifactId>bouncy-castle-bcfips</artifactId> <version>${project.version}</version> <classifier>pkg</classifier> </dependency>其注释明确写道:"exclude bouncy castle, then load fips version"。
配套的测试代码 bouncy-castle/bcfips-include-test/src/test/java/org/apache/pulsar/client/TlsProducerConsumerTest.java 在 FIPS 环境下验证了三类 TLS 场景:
- 大消息传输:验证超过单个 TLS chunk 上限(
2^14字节)的16KB+1字节消息可以正常生产/消费; - 二进制协议双向 TLS 认证:不带客户端证书时握手应失败,携带证书后消费可成功;
- HTTP 协议双向 TLS 认证:同样验证"无证书失败、有证书成功"。
这套用例证明了切换后的 FIPS 环境在真实 Broker/Client 交互中可用。可以按需在本地运行该模块的测试来验证自己的切换配置:
mvn test -pl bouncy-castle/bcfips-include-test切换后运行时的注意事项
- FIPS 模式下 Provider 名称是
BCFIPS。若代码中硬编码了SecretKeyFactory.getInstance(..., "BC")这类写法,需要确认 Pulsar 的SecurityUtility已统一通过 Provider 常量获取实例;从 SecurityUtility.java 的注释可见,BC/BCFIPS常量同时用于Security.getProvider以及CertificateFactory.getInstance("X.509", "BCFIPS")之类的工厂调用。 - 一个 JVM 内不得同时出现
bouncy-castle-bc与bouncy-castle-bcfips,切换时必须借助<exclusions>彻底排除旧版本,避免Security.addProvider阶段出现 Provider 冲突或算法查找异常。 - FIPS 本身对算法与密钥强度有严格约束(涉及 Policy、Self-test 等),这些属于 BC 官方 Security Policy 文档的范畴,在搭建 FIPS 合规环境前应完整阅读官方材料。
常见错误速查
| 现象 | 根因 | 处理方式 |
|---|---|---|
java.lang.SecurityException: Invalid signature file digest for Manifest main attributes | Shade 解包导致 BC 签名失效 | 不要对 BC 做 shade;改用bouncy-castle-bc/bouncy-castle-bcfips的 jar-in-jar(pkgclassifier)产物 |
java.security.NoSuchAlgorithmException: PBEWithSHA256And256BitAES-CBC-BC SecretKeyFactory not available | Provider 未正确注册或签名被破坏 | 确认引入的是完整签名产物,并检查是否同时混入了两套 BC |
java.security.NoSuchProviderException: JCE cannot authenticate the provider BC | Provider 认证失败(签名不合法) | 在 shaded 模块中排除org/bouncycastle/**,单独引入官方打包模块 |
| FIPS 切换后算法不可用 | JVM 中同时存在两套 BC,或仍在使用"BC"硬编码 | 使用<exclusions>排除bouncy-castle-bc,统一走SecurityUtility的 Provider 常量 |
总结
Pulsar 通过bouncy-castle父模块下的bouncy-castle-bc与bouncy-castle-bcfips两个 jar-in-jar 产物,将 Bouncy Castle 的引入与排除收敛为两个清晰的坐标,从根本上规避了 Shade 解包破坏 Jar 签名导致的 Provider 认证问题;BouncyCastleLoader/BouncyCastleFipsLoader与SecurityUtility则在运行时完成 Provider 的选择与注册。无论是默认的非 FIPS 部署,还是合规要求下的 FIPS 部署,只需遵循"排除 bouncy-castle-bc → 引入 bouncy-castle-bcfips(pkg classifier)"这一条主线,即可完成安全底座的整体切换,并借助bcfips-include-test模块完成端到端验证。
【免费下载链接】pulsarApache Pulsar - distributed pub-sub messaging system项目地址: https://gitcode.com/gh_mirrors/pulsar28/pulsar
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考