一款 Kroxylicious 过滤器插件,使用 ML-KEM(FIPS 203)密钥封装和 AES-256-GCM 对称加密,为 Apache Kafka 提供透明的 后量子密码学(PQC) 记录级加密。
Kafka 生产者和消费者零代码更改。Kroxylicious 代理 拦截流量,并在 Produce 时自动加密、在 Fetch 时自动解密。``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## 为什么要为 Kafka 使用 PQC?
经典密钥建立算法(RSA、ECDH)容易受到未来量子计算机的攻击。如果使用经典 KEM 建立每条记录的加密密钥,量子对手可以从存储在 broker 上、与密文一起保存的封装数据中恢复出这些密钥。
该插件使用 ML-KEM(FIPS 203)进行抗量子密钥封装,确保即使对手拥有具备密码学相关能力的量子计算机,也无法解密 Kafka broker 上的静态数据。
**注意:** 此过滤器保护的是 **broker 上的静态数据**,而不是传输中的 TLS 信道。有关哪些攻击受到防御、哪些未受到防御的完整分析,请参阅 [THREAT_MODEL.md](https://github.com/oscerd/kroxylicious-pqc-filter/blob/HEAD/THREAT_MODEL.md)。
| Standard | Algorithm | 本插件中的用途 |
|----------|-----------|------------------------|
| FIPS 203 | ML-KEM (Kyber) | 密钥封装——安全地为每条消息建立 AES 密钥 |
| N/A | AES-256-GCM | 记录负载的对称认证加密 |
| N/A | X25519 ECDH | 用于混合模式纵深防御的经典密钥协商 |
## 功能特性
- **透明加解密** - 无需客户端修改
- **ML-KEM-512、ML-KEM-768(默认)、ML-KEM-1024** 参数集
- **混合模式**(默认)- 结合 ML-KEM + X25519 ECDH,两者都必须被破解
- **每条记录加密** - 每条记录获得全新的 KEM 封装和随机 IV
- **主题过滤** - 通过正则表达式选择要加密的主题
- **篡改检测** - AES-GCM 认证加密可拒绝被修改的密文
- **语义安全** - 相同明文产生不同密文(IND-CCA2)
- **密钥自动生成** - 首次启动时若缺少 ML-KEM 密钥,将自动生成并保存
- **`x-pqc-encrypted` 标头** - 标记加密记录,供下游感知
- **可插拔密钥提供者** - `KeyProvider` SPI 支持文件系统(默认)和 HashiCorp Vault 后端
## 先决条件
| 要求 | 版本 |
|-------------|---------|
| JDK | 17+ (21+ recommended) |
| Maven | 3.8+ |
| Kroxylicious | 0.19.0 |
| Apache Kafka | 3.9.x |
## 快速开始
### 1. 构建插件```bash
git clone <this-repo>
cd kroxylicious-pqc-filter
mvn clean package -DskipTests
位于 target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar 的 shaded JAR 捆绑了 Bouncy Castle,
因此无需额外依赖即可直接放入 Kroxylicious。
要包含 HashiCorp Vault 密钥提供程序支持,请使用 vault profile 构建:```bash
mvn clean package -Pvault -DskipTests
This bundles `spring-vault-core` and the `VaultKeyProvider` into the JAR.
### 2. Generate ML-KEM keys```bash
java -cp target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar \
io.kroxylicious.filter.pqc.PqcKeyGeneratorCli \
ML_KEM_768 \
/etc/kroxylicious/pqc/
I don't see any source text in the input to translate. Please provide the Markdown content for chunk 9 of 33.``` Generating ML-KEM-768 key pair... Public key: /etc/kroxylicious/pqc/pqc-public.der Size: 1206 bytes Format: X.509 Private key: /etc/kroxylicious/pqc/pqc-private.der Size: 2498 bytes Format: PKCS#8
或者,在配置中省略密钥路径,过滤器将在首次启动时自动生成密钥。
### 3. 配置 Kroxylicious
将过滤器添加到你的 Kroxylicious 代理 YAML 配置中:```yaml
filterDefinitions:
- name: pqc-encryption
type: PqcRecordEncryptionFilterFactory
config:
kemAlgorithm: ML_KEM_768
hybridMode: true
publicKeyPath: /etc/kroxylicious/pqc/pqc-public.der
privateKeyPath: /etc/kroxylicious/pqc/pqc-private.der
topicPatterns:
- "sensitive-.*"
- "pii-.*"
defaultFilters:
- pqc-encryption
将 JAR 放置在 Kroxylicious 可访问的目录中,并通过
KROXYLICIOUS_CLASSPATH 环境变量将其添加到 classpath:```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
使用 Docker 时,请在容器环境中设置:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
然后启动代理。生产者和消费者连接到代理端口,而不是直接连接 broker。
Filesystem(keyProviderType: filesystem,默认):
从磁盘上的 DER 文件加载 ML-KEM 密钥。如果文件不存在,则生成新的密钥对并保存。需要 publicKeyPath 和 privateKeyPath。
HashiCorp Vault(keyProviderType: vault,需要 -Pvault 构建):
从 Vault KV v2 机密引擎获取 ML-KEM 密钥。密钥以 base64 编码的 DER 形式存储于 publicKey 和 privateKey 字段中。Vault 机密版本映射到密钥 ID,以支持密钥轮换。
Vault keyProviderConfig 属性:
Vault 配置示例:```yaml filterDefinitions:
### ML-KEM 参数集
| 算法 | 安全级别 | 公钥 | 私钥 | 密文开销 | 用例 |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128 位 | 822 B | 1,730 B | ~854 B | 轻量级、物联网 |
| ML-KEM-768 | 192 位 | 1,206 B | 2,498 B | ~1,174 B | **推荐默认** |
| ML-KEM-1024 | 256 位 | 1,590 B | 3,266 B | ~1,654 B | 涉密/长期数据 |
### 加密模式
**纯 PQC**(`hybridMode: false`):
仅使用 ML-KEM。AES-256 密钥由 ML-KEM 共享秘密通过
`SHA-256(0x01 || "kroxylicious-pqc-v1" || secret)` 派生。
**混合模式**(`hybridMode: true`,默认):
结合 ML-KEM + X25519。AES-256 密钥由两个秘密通过
`SHA-256(0x02 || "kroxylicious-pqc-hybrid-v1" || pqcSecret || x25519Secret)` 派生。
即使其中一种算法被攻破,也能确保安全性。
## 加密信封格式
每个加密的记录值都会被替换为二进制信封:```
PQC-only (version 0x01):
+--------+-----------+--------------------+--------+---------------------------+
| 1 byte | 2 bytes | N bytes | 12 B | remaining |
| 0x01 | encap len | ML-KEM encapsulat. | AES IV | AES-GCM ciphertext + tag |
+--------+-----------+--------------------+--------+---------------------------+
Hybrid (version 0x02):
+--------+-----------+--------------------+---------+--------+-----------------+
| 1 byte | 2 bytes | N bytes | 32 B | 12 B | remaining |
| 0x02 | encap len | ML-KEM encapsulat. | X25519 | AES IV | AES-GCM ct+tag |
| | | | eph pub | | |
+--------+-----------+--------------------+---------+--------+-----------------+
版本字节允许解密器无需配置即可确定模式。
kroxylicious-pqc-filter/ src/main/java/io/kroxylicious/filter/pqc/ PqcRecordEncryptionFilterFactory.java # FilterFactory entry point (@Plugin) PqcRecordEncryptionFilter.java # ProduceRequestFilter + FetchResponseFilter PqcKeyGeneratorCli.java # CLI key generation utility config/ PqcEncryptionConfig.java # Jackson-deserialized configuration POJO crypto/ KeyProvider.java # SPI interface for pluggable key backends FileSystemKeyProvider.java # Default: loads/generates keys from disk PqcCryptoEngine.java # ML-KEM encapsulation + AES-256-GCM PqcKeyManager.java # Resolves KeyProvider via ServiceLoader src/main/java-vault/ # (vault profile only) .../crypto/ VaultKeyProvider.java # HashiCorp Vault KV v2 key provider src/main/resources/ META-INF/services/ io.kroxylicious.proxy.filter.FilterFactory # ServiceLoader: filter registration io.kroxylicious.filter.pqc.crypto.KeyProvider # ServiceLoader: key provider src/main/resources-vault/ # (vault profile only) META-INF/services/ io.kroxylicious.filter.pqc.crypto.KeyProvider # Registers both FS + Vault providers src/test/java/... # Unit tests (49 tests) src/test/java-vault/... # Vault provider tests (17 tests) examples/ standalone/ # Runs without Kafka (crypto engine demo) docker/ # Docker Compose: Kafka + Vault + Kroxylicious
### 数据流
#### Vault 中存储的内容
Vault KV v2 在 `secret/<secretPath>`(例如 `secret/kroxylicious/pqc`)中保存 ML-KEM 密钥对:
| 字段 | 内容 | 格式 |
|-------|---------|--------|
| `publicKey` | ML-KEM 公钥(用于封装) | Base64 编码的 X.509 DER |
| `privateKey` | ML-KEM 私钥(用于解封装) | Base64 编码的 PKCS#8 DER |
每个 Vault secret 版本都充当一个密钥 ID,支持密钥轮换。新版本
加密新记录;旧版本仍可解密用它们加密的记录。
#### 启动流程```
┌─────────────────────────────────────────────────────────────────────┐
│ STARTUP │
│ │
│ 1. Vault starts (dev mode, port 8200) │
│ │ │
│ 2. vault-init container: │
│ │ • Reads pre-generated ML-KEM DER key files from disk │
│ │ • Base64-encodes them │
│ │ • POST /v1/secret/data/kroxylicious/pqc │
│ │ • Stores publicKey + privateKey in Vault KV v2 │
│ │ • Exits │
│ │ │
│ 3. Kafka starts (KRaft mode, port 9092) │
│ │ │
│ 4. Kroxylicious starts (port 9192): │
│ │ │
│ │ FilterFactory.initialize() │
│ │ → PqcKeyManager resolves VaultKeyProvider │
│ │ via ServiceLoader (matches keyProviderType: "vault") │
│ │ → VaultKeyProvider.configure() │
│ │ connects to Vault (token/approle/kubernetes auth) │
│ │ → Fetches ML-KEM key pair from Vault KV v2 │
│ │ (GET /v1/secret/data/kroxylicious/pqc) │
│ │ → Decodes: base64 → DER bytes → Java Key objects │
│ │ → PqcCryptoEngine initialized with key pair │
│ ▼ │
│ Proxy ready — Vault is NOT contacted again per-message │
└─────────────────────────────────────────────────────────────────────┘
Kroxylicious (:9192)
┌───────────────────────────────────────────┐
Producer ──plaintext──> │ PqcRecordEncryptionFilter │ │ .onProduceRequest() │ │ │ │ For each record in matching topic: │ │ 1. ML-KEM encapsulate (public key) │ │ → fresh shared secret │ │ → encapsulation blob │ │ 2. SHA-256(shared secret) → AES-256 key │ │ 3. AES-GCM encrypt record value │ │ 4. Build binary envelope: │ │ [ver|encap_len|encap|IV|ciphertext] │ │ 5. Add x-pqc-encrypted header │ └────────────────┬──────────────────────────┘ │ ▼ Kafka Broker (:9092) stores encrypted blob
#### 获取流程(解密)```
Kafka Broker (:9092)
returns encrypted blob
│
▼
┌────────────────┴──────────────────────────┐
│ PqcRecordEncryptionFilter │
│ .onFetchResponse() │
│ │
│ For each record with x-pqc-encrypted: │
│ 1. Parse envelope → extract encap + │
│ ciphertext │
│ 2. ML-KEM decapsulate (private key) │
│ → recover shared secret │
│ 3. SHA-256(shared secret) → AES-256 key │
│ 4. AES-GCM decrypt → plaintext │
│ 5. Remove x-pqc-encrypted header │
Consumer <──plaintext── │ │
└───────────────────────────────────────────┘
Kroxylicious (:9192)
Startup: FilterFactory.initialize() -> PqcKeyManager loads/generates ML-KEM key pair -> PqcCryptoEngine created with key pair -> Topic patterns compiled -> SharedPqcContext returned
Per connection: FilterFactory.createFilter() -> New PqcRecordEncryptionFilter instance -> Shares the same PqcCryptoEngine (thread-safe)
Produce request: onProduceRequest() -> For each topic matching topicPatterns: -> For each partition: -> For each record: -> ML-KEM encapsulate (fresh shared secret + encapsulation) -> Derive AES-256 key from shared secret -> AES-GCM encrypt the record value -> Replace value with encrypted envelope -> Add x-pqc-encrypted header -> Forward to broker
Fetch response: onFetchResponse() -> For each topic matching topicPatterns: -> For each partition: -> For each record with x-pqc-encrypted header: -> Read version + encapsulation from envelope -> ML-KEM decapsulate (recover shared secret) -> Derive AES-256 key -> AES-GCM decrypt -> Replace value with plaintext -> Remove x-pqc-encrypted header -> Forward to client
### 关键类
**`PqcRecordEncryptionFilterFactory`** 实现 `FilterFactory<PqcEncryptionConfig, SharedPqcContext>`。
使用 `@Plugin(configType = PqcEncryptionConfig.class)` 进行注解。
通过 `META-INF/services/io.kroxylicious.proxy.filter.FilterFactory` 注册。
在启动时调用一次(`initialize`),每个客户端连接调用一次(`createFilter`)。
**`PqcRecordEncryptionFilter`** 实现 `ProduceRequestFilter` 和 `FetchResponseFilter`。
拦截 `onProduceRequest` 进行加密,拦截 `onFetchResponse` 进行解密。
每个连接一个实例;无需同步(Kroxylicious 线程模型)。
**`PqcCryptoEngine`** 执行所有密码学操作。
除密钥材料和 `SecureRandom` 外无状态。
`encrypt()` 返回自描述信封;`decrypt()` 解析该信封。
在静态初始化器中注册 Bouncy Castle 提供者(`BC`、`BCPQC`)。
**`PqcKeyManager`** 通过 `ServiceLoader` 解析 `KeyProvider`,依据
`keyProviderType` 进行匹配。将所有密钥操作委托给已解析的提供者。
**`KeyProvider`** 是可插拔密钥存储后端的 SPI 接口。
实现通过 `META-INF/services` 发现。内置提供者:
`FileSystemKeyProvider`(默认)和 `VaultKeyProvider`(使用 `-Pvault`)。
**`PqcEncryptionConfig`** 是 Jackson 注解的 POJO。
从 Kroxylicious 代理 YAML 的 `config:` 块中反序列化。
支持 `keyProviderType` 和 `keyProviderConfig` 用于后端选择。
## 构建与测试```bash
# Compile
mvn compile
# Run unit tests (38 core tests)
mvn test
# Package (creates shaded JAR with Bouncy Castle bundled)
mvn package
# Build with Vault support (55 tests: 38 core + 17 vault)
mvn package -Pvault
# Install to local Maven repository
mvn install
总计:55 个测试(38 个核心 + 17 个 Vault)
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
在独立演示环境中测量(JVM 预热,1,000 次迭代,1 KB 消息):
| 算法 | 加密 | 解密 |
|---|---|---|
| ML-KEM-512 | ~7,700 msg/s (0.13 ms/msg) | ~7,800 msg/s (0.13 ms/msg) |
| ML-KEM-768 | ~9,600 msg/s (0.10 ms/msg) |
每条消息的开销为亚毫秒级。对于典型的 Kafka 工作负载 (KB-MB 范围内的消息),与网络和磁盘 I/O 相比, 加密成本可以忽略不计。
有关该过滤器防御什么、不防御什么的完整威胁模型, 请参阅 THREAT_MODEL.md。
chmod 600)。approle 或 kubernetes 认证,而非静态令牌。
切勿将 Vault 令牌提交到版本控制。null 的记录(Kafka 墓碑消息)
将不加密直接透传。Compression.NONE 写入加密批次,
因为加密数据压缩效果不佳。Apache License 2.0。详情请参阅 LICENSE。
| 属性 | 类型 | 必填 | 默认值 | 描述 |
|---|
kemAlgorithm | enum | 否 | ML_KEM_768 | ML-KEM 参数集。可为 ML_KEM_512、ML_KEM_768、ML_KEM_1024 之一。 |
hybridMode | boolean | 否 | true | 将 ML-KEM 与 X25519 ECDH 结合以实现深度防御。 |
publicKeyPath | string | 仅文件系统 | - | ML-KEM 公钥的文件系统路径(X.509 DER 编码)。 |
privateKeyPath | string | 仅文件系统 | - | ML-KEM 私钥的文件系统路径(PKCS#8 DER 编码)。 |
topicPatterns | list<string> | 否 | [".*"] | Java 正则表达式模式。仅匹配主题中的记录会被加密/解密。 |
keyProviderType | string | 否 | filesystem | 密钥存储后端。可选 filesystem、vault。 |
keyProviderConfig | map<string, string> | 仅 Vault | {} | 后端专用配置(参见下文 Vault 部分)。 |
| 属性 | 必填 | 默认值 | 描述 |
|---|
vaultAddress | 是 | VAULT_ADDR 环境变量 | Vault 服务器 URL(例如 http://vault:8200) |
vaultToken | token 认证时需要 | VAULT_TOKEN 环境变量 | Vault 认证令牌 |
secretPath | 是 | -- | 机密引擎内的路径(例如 kroxylicious/pqc) |
secretEngine | 否 | secret | KV v2 机密引擎挂载名称 |
authMethod | 否 | token | 认证方法:token、approle 或 kubernetes |
roleId | approle 认证时需要 | -- | AppRole 角色 ID |
secretId | approle 认证时需要 | -- | AppRole 机密 ID |
kubeRole | kubernetes 认证时需要 | -- | Kubernetes 认证角色名称 |
kubeTokenPath | 否 | /var/run/secrets/.../token | 服务账户令牌文件路径 |
| 测试类 | 测试数 | 验证内容 |
|---|
PqcCryptoEngineTest | 12 | 所有 3 种 ML-KEM 变体的加密/解密往返、null 处理、空载荷和 1MB 载荷、语义安全性、篡改检测、无效版本拒绝、密钥生成、信封版本字节 |
PqcEncryptionConfigTest | 9 | 默认值、显式值、null 拒绝、JSON 反序列化、不可变性、枚举属性、keyProviderConfig 反序列化 |
PqcKeyManagerTest | 6 | KeyProvider 解析、引擎创建、委托给提供程序、文件系统类型的回退 |
FileSystemKeyProviderTest | 11 | 密钥生成、加载现有密钥、默认密钥 ID、未知密钥 ID 拒绝、null 路径校验、ServiceLoader 发现、加密往返 |
VaultKeyProviderTest | 17 | 配置校验(缺失 path/address/token/approle/kube)、从 Vault 获取密钥、带版本的密钥检索、缓存密钥、无效版本、secret 中缺失字段、加密往返、关闭行为 |
| 依赖 | 版本 | 作用域 | 用途 |
|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | Filter API 接口 |
org.apache.kafka:kafka-clients | 3.9.0 | provided | Kafka 协议消息类型 |
com.fasterxml.jackson.core:jackson-annotations | 2.18.3 | provided | 配置绑定 |
org.bouncycastle:bcprov-jdk18on | 1.83 | compile | ML-KEM、AES-GCM、X25519(捆绑在 shaded JAR 中) |
org.bouncycastle:bcutil-jdk18on | 1.83 | compile | Bouncy Castle 工具类(捆绑在 shaded JAR 中) |
org.slf4j:slf4j-api | 2.0.17 | provided | 日志 |
org.springframework.vault:spring-vault-core | 3.1.2 | compile(vault profile) | Vault KV v2 客户端(使用 -Pvault 构建时捆绑) |
| ~10,200 msg/s (0.10 ms/msg) |
| ML-KEM-1024 | ~8,500 msg/s (0.12 ms/msg) | ~6,900 msg/s (0.14 ms/msg) |