
Um plugin de filtro Kroxylicious que fornece criptografia transparente pós-quântica (ML-KEM + AES-256-GCM) em nível de registro para Apache Kafka, sem exigir alterações no código do cliente.
Um plugin de filtro Kroxylicious que fornece criptografia transparente em nível de registro de Pós-Quantum (PQC) para Apache Kafka usando encapsulamento de chave ML-KEM (FIPS 203) com criptografia simétrica AES-256-GCM.
Produtores e consumidores Kafka não exigem nenhuma alteração de código. O proxy Kroxylicious intercepta o tráfego e criptografa no Produce / descriptografa no Fetch automaticamente.``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## Por que PQC para Kafka?
Algoritmos clássicos de estabelecimento de chaves (RSA, ECDH) são vulneráveis a futuros computadores quânticos. Se as chaves de criptografia por registro forem estabelecidas usando um KEM clássico, um adversário quântico poderia recuperar essas chaves das encapsulações armazenadas junto ao texto cifrado no broker.
Este plugin usa ML-KEM (FIPS 203) para encapsulamento de chaves resistente a quântica, garantindo que dados em repouso no broker Kafka não possam ser descriptografados mesmo por um adversário com um computador quântico criptograficamente relevante.
**Nota:** Este filtro protege **dados em repouso no broker**, não o canal TLS em trânsito. Consulte [THREAT_MODEL.md](https://github.com/oscerd/kroxylicious-pqc-filter/blob/main/THREAT_MODEL.md) para uma análise completa do que é e não é defendido.
| Padrão | Algoritmo | Propósito neste plugin |
|----------|-----------|------------------------|
| FIPS 203 | ML-KEM (Kyber) | Encapsulamento de chaves - estabelece com segurança uma chave AES por mensagem |
| N/A | AES-256-GCM | Criptografia autenticada simétrica do payload do registro |
| N/A | X25519 ECDH | Acordo de chaves clássico para defesa em profundidade em modo híbrido |
## Funcionalidades
- **Criptografia/descriptografia transparente** - nenhuma alteração no lado do cliente é necessária
- **ML-KEM-512, ML-KEM-768 (padrão), ML-KEM-1024** conjuntos de parâmetros
- **Modo híbrido** (padrão) - combina ML-KEM + X25519 ECDH para que ambos precisem ser quebrados
- **Criptografia por registro** - cada registro recebe uma nova encapsulação KEM + IV aleatório
- **Filtragem de tópicos** - padrões regex selecionam quais tópicos criptografar
- **Detecção de violação** - criptografia autenticada AES-GCM rejeita texto cifrado modificado
- **Segurança semântica** - textos simples idênticos produzem textos cifrados diferentes (IND-CCA2)
- **Geração automática de chaves** - gera e salva chaves ML-KEM na primeira inicialização se ausentes
- **Cabeçalho `x-pqc-encrypted`** - marca registros criptografados para conscientização downstream
- **Provedores de chaves plugáveis** - `KeyProvider` SPI suporta backends de sistema de arquivos (padrão) e HashiCorp Vault
## Pré-requisitos
| Requisito | Versão |
|-------------|---------|
| JDK | 17+ (21+ recomendado) |
| Maven | 3.8+ |
| Kroxylicious | 0.19.0 |
| Apache Kafka | 3.9.x |
## Início Rápido
### 1. Construir o plugin```bash
git clone <this-repo>
cd kroxylicious-pqc-filter
mvn clean package -DskipTests
O JAR shaded em target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar agrupa o Bouncy Castle para que possa ser inserido no Kroxylicious sem dependências extras.
Para incluir suporte ao provedor de chaves HashiCorp Vault, compile com o perfil vault:```bash
mvn clean package -Pvault -DskipTests
Isso agrupa `spring-vault-core` e o `VaultKeyProvider` no JAR.
### 2. Gerar chaves ML-KEM```bash
java -cp target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar \
io.kroxylicious.filter.pqc.PqcKeyGeneratorCli \
ML_KEM_768 \
/etc/kroxylicious/pqc/
Saída:``` 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
Alternativamente, omita os caminhos das chaves na configuração e o filtro gerará
chaves automaticamente na primeira inicialização.
### 3. Configurar o Kroxylicious
Adicione o filtro à sua configuração YAML do proxy Kroxylicious:```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
Coloque o JAR em um diretório acessível ao Kroxylicious e adicione-o ao classpath através da variável de ambiente KROXYLICIOUS_CLASSPATH:```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
Ao usar Docker, defina-o no ambiente do seu container:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
Em seguida, inicie o proxy. Produtores e consumidores conectam-se à porta do proxy em vez de diretamente ao broker.
| Propriedade | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
kemAlgorithm | enum | Não | ML_KEM_768 | Conjunto de parâmetros ML-KEM. Um de ML_KEM_512, ML_KEM_768, ML_KEM_1024. |
hybridMode | boolean | Não | true | Combina ML-KEM com X25519 ECDH para defesa em profundidade. |
publicKeyPath | string | Apenas sistema de arquivos | - | Caminho no sistema de arquivos para a chave pública ML-KEM (codificada em X.509 DER). |
privateKeyPath | string | Apenas sistema de arquivos | - | Caminho no sistema de arquivos para a chave privada ML-KEM (codificada em PKCS#8 DER). |
topicPatterns | list<string> | Não | [".*"] | Padrões regex Java. Apenas registros em tópicos correspondentes são criptografados/descriptografados. |
keyProviderType | string | Não | filesystem | Backend de armazenamento de chaves. Um de filesystem, vault. |
keyProviderConfig | map<string, string> | Apenas Vault | {} | Configuração específica do backend (veja a seção Vault abaixo). |
Sistema de Arquivos (keyProviderType: filesystem, padrão):
Carrega chaves ML-KEM de arquivos DER no disco. Se os arquivos não existirem, gera um novo par de chaves e os salva. Requer publicKeyPath e privateKeyPath.
HashiCorp Vault (keyProviderType: vault, requer compilação -Pvault):
Obtém chaves ML-KEM de um mecanismo de segredos KV v2 do Vault. As chaves são armazenadas como DER codificada em base64 nos campos publicKey e privateKey. As versões dos segredos do Vault mapeiam para IDs de chave para suporte a rotação de chaves.
Propriedades keyProviderConfig do Vault:
| Propriedade | Obrigatório | Padrão | Descrição |
|---|---|---|---|
vaultAddress | Sim | Variável de ambiente VAULT_ADDR | URL do servidor Vault (ex.: http://vault:8200) |
vaultToken | Para autenticação token | Variável de ambiente VAULT_TOKEN | Token de autenticação do Vault |
secretPath | Sim | -- | Caminho dentro do mecanismo de segredos (ex.: kroxylicious/pqc) |
secretEngine | Não | secret | Nome do ponto de montagem do mecanismo de segredos KV v2 |
authMethod | Não | token | Método de autenticação: token, approle ou kubernetes |
roleId | Para approle | -- | ID da função AppRole |
secretId | Para approle | -- | ID do segredo AppRole |
kubeRole | Para kubernetes | -- | Nome da função de autenticação Kubernetes |
kubeTokenPath | Não | /var/run/secrets/.../token | Caminho do arquivo de token da conta de serviço |
Exemplo de configuração do Vault:```yaml filterDefinitions:
### Conjuntos de Parâmetros ML-KEM
| Algoritmo | Nível de Segurança | Chave Pública | Chave Privada | Sobrecarga do Cifrador | Caso de Uso |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128-bit | 822 B | 1.730 B | ~854 B | Leve, IoT |
| ML-KEM-768 | 192-bit | 1.206 B | 2.498 B | ~1.174 B | **Padrão recomendado** |
| ML-KEM-1024 | 256-bit | 1.590 B | 3.266 B | ~1.654 B | Dados classificados / de longa duração |
### Modos de Criptografia
**Apenas PQC** (`hybridMode: false`):
Usa exclusivamente ML-KEM. A chave AES-256 é derivada do segredo compartilhado do ML-KEM
através de `SHA-256(0x01 || "kroxylicious-pqc-v1" || secret)`.
**Híbrido** (`hybridMode: true`, padrão):
Combina ML-KEM + X25519. A chave AES-256 é derivada de ambos os segredos através de
`SHA-256(0x02 || "kroxylicious-pqc-hybrid-v1" || pqcSecret || x25519Secret)`.
Isso garante segurança mesmo se um algoritmo for quebrado.
## Formato do Envelope Criptografado
Cada valor de registro criptografado é substituído por um envelope binário:```
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 | | |
+--------+-----------+--------------------+---------+--------+-----------------+
O byte de versão permite que o decriptador determine o modo sem configuração.
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
### Fluxo de dados
#### O que é armazenado no Vault
O Vault KV v2 armazena o par de chaves ML-KEM em `secret/<secretPath>` (por exemplo, `secret/kroxylicious/pqc`):
| Campo | Conteúdo | Formato |
|-------|----------|---------|
| `publicKey` | Chave pública ML-KEM (usada para encapsulamento) | Base64-encoded X.509 DER |
| `privateKey` | Chave privada ML-KEM (usada para desencapsulamento) | Base64-encoded PKCS#8 DER |
Cada versão do segredo no Vault atua como um ID de chave, permitindo a rotação de chaves. Novas versões
criptografam novos registos; versões antigas ainda podem descriptografar registos criptografados com elas.
#### Fluxo de inicialização```
┌─────────────────────────────────────────────────────────────────────┐
│ 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
#### Fluxo de busca (descriptografia)```
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
### Classes Principais
**`PqcRecordEncryptionFilterFactory`** implementa `FilterFactory<PqcEncryptionConfig, SharedPqcContext>`.
Anotada com `@Plugin(configType = PqcEncryptionConfig.class)`.
Registrada via `META-INF/services/io.kroxylicious.proxy.filter.FilterFactory`.
Chamada uma vez na inicialização (`initialize`) e uma vez por conexão de cliente (`createFilter`).
**`PqcRecordEncryptionFilter`** implementa `ProduceRequestFilter` e `FetchResponseFilter`.
Intercepta `onProduceRequest` para criptografar e `onFetchResponse` para descriptografar.
Uma instância por conexão; nenhuma sincronização necessária (modelo de thread do Kroxylicious).
**`PqcCryptoEngine`** realiza todas as operações criptográficas.
Sem estado, exceto pelo material de chave e `SecureRandom`.
`encrypt()` retorna um envelope auto-descritivo; `decrypt()` o analisa.
Registra os provedores Bouncy Castle (`BC`, `BCPQC`) em um inicializador estático.
**`PqcKeyManager`** resolve um `KeyProvider` via `ServiceLoader`, correspondendo pelo
`keyProviderType`. Delega todas as operações de chave para o provedor resolvido.
**`KeyProvider`** é a interface SPI para backends de armazenamento de chaves plugáveis.
Implementações são descobertas via `META-INF/services`. Provedores embutidos:
`FileSystemKeyProvider` (padrão) e `VaultKeyProvider` (com `-Pvault`).
**`PqcEncryptionConfig`** é um POJO anotado com Jackson.
Desserializado do bloco `config:` no YAML do proxy Kroxylicious.
Suporta `keyProviderType` e `keyProviderConfig` para seleção de backend.
## Construção e Testes```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
| Classe de Teste | Testes | O que é verificado |
|---|---|---|
PqcCryptoEngineTest | 12 | Encrypt/decrypt roundtrip para todas as 3 variantes do ML-KEM, tratamento de null, payloads vazios e de 1 MB, segurança semântica, detecção de adulteração, rejeição de versão inválida, geração de chaves, bytes de versão do envelope |
PqcEncryptionConfigTest | 9 | Valores padrão, valores explícitos, rejeição de null, desserialização JSON, imutabilidade, propriedades de enum, desserialização de keyProviderConfig |
PqcKeyManagerTest | 6 | Resolução de KeyProvider, criação de engine, delegação para provedores, fallback para tipo filesystem |
FileSystemKeyProviderTest | 11 | Geração de chaves, carregamento de chaves existentes, ID de chave padrão, rejeição de ID de chave desconhecida, validação de caminho null, descoberta via ServiceLoader, roundtrip criptográfico |
VaultKeyProviderTest | 17 | Validação de configuração (caminho/endereço/token/approle/kube ausentes), busca de chave do Vault, recuperação de chave versionada, chaves em cache, versões inválidas, campos ausentes no segredo, roundtrip criptográfico, comportamento de close |
Total: 55 testes (38 core + 17 vault)
| Dependência | Versão | Escopo | Propósito |
|---|---|---|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | Interfaces da API de filtro |
org.apache.kafka:kafka-clients | 3.9.0 | provided | Tipos de mensagens do protocolo Kafka |
com.fasterxml.jackson.core:jackson-annotations | 2.18.3 | provided | Vinculação de configuração |
org.bouncycastle:bcprov-jdk18on | 1.83 | compile | ML-KEM, AES-GCM, X25519 (incluído no JAR sombreado) |
org.bouncycastle:bcutil-jdk18on | 1.83 | compile | Utilitários do Bouncy Castle (incluídos no JAR sombreado) |
org.slf4j:slf4j-api | 2.0.17 | provided | Registro de logs |
org.springframework.vault:spring-vault-core | 3.1.2 | compile (perfil vault) | Cliente Vault KV v2 (incluído quando compilado com -Pvault) |
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
Medido na demonstração standalone (JVM aquecida, 1.000 iterações, mensagens de 1 KB):
| Algoritmo | Criptografar | Descriptografar |
|---|---|---|
| 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) | ~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) |
A sobrecarga por mensagem é submilissegundo. Para cargas de trabalho típicas do Kafka (mensagens na faixa de KB-MB), o custo da criptografia é insignificante comparado à E/S de rede e disco.
Para um modelo de ameaça completo cobrindo contra o que este filtro defende e o que não defende, veja THREAT_MODEL.md.
chmod 600).approle ou kubernetes em vez de tokens estáticos
em produção. Nunca commit tokens do Vault no controle de versão.null (tombstones do Kafka) são transmitidos
sem criptografia.Compression.NONE
porque dados criptografados não comprimem bem.Licença Apache 2.0. Veja LICENSE para detalhes.