Skip to content

Crypto — PQC envelope

Crypto — PQC envelope

Pollen8 encrypts every at-rest credential with a hybrid post-quantum envelope. Configure + audit at /admin/crypto.

What’s in the envelope

Every secret_box.encrypt(plain) call now produces a v2 blob with the pqcv2: prefix containing:

LayerAlgorithmStandard
Outer KEM (classical leg)X25519 ECDH(PQ-transition hybrid)
Outer KEM (PQ leg)ML-KEM-1024FIPS 203 (Aug 2024)
KDFHKDF-SHA3-512SHA-3 family is PQ-safe
AEADAES-256-GCM256-bit symmetric is PQ-safe
Audit-chain signatureML-DSA-65FIPS 204

Both KEM shared secrets are concatenated before KDF — failure-safe. If ML-KEM is later broken, X25519 still protects. If a CRQC breaks X25519, ML-KEM still protects.

Why now

NIST finalized FIPS 203 / 204 / 205 in August 2024. CNSA 2.0 mandate transitions federal-grade systems by 2030. The “harvest-now-decrypt-later” threat justifies starting the migration today — anything encrypted with classical-only crypto in 2025 is at risk in 2030.

Key material

The static keypairs that decrypt all v2 blobs live in deployment env vars:

POLLENIX_PQC_MLKEM_PK=<base64>
POLLENIX_PQC_MLKEM_SK=<base64>
POLLENIX_PQC_MLDSA_PK=<base64>
POLLENIX_PQC_MLDSA_SK=<base64>

Generate them once at deploy time:

Terminal window
python -m pollenix_core.tools.gen_pqc_keys

Paste the four lines into your Helm values / K8s Secret / AWS Secrets Manager. The X25519 keypair derives deterministically from pqc_master_seed (or sso_secret_key fallback) — no additional env var.

When the env vars are missing, the process generates ephemeral keys at startup and logs a warning. The /admin/crypto status banner surfaces the same warning. Ephemeral is fine for local dev; production restarts will break decryption of any v2 blob written during the prior process lifetime.

Migration from v1 (legacy Fernet)

Every existing encrypted column starts on v1 (Fernet, AES-128-CBC + HMAC-SHA256). Two migration paths run in parallel:

Lazy — call sites that use secret_box.decrypt_and_maybe_rotate() get back (plaintext, new_v2_blob_if_rotation_needed). The call site can write the upgraded blob in the same transaction it reads. Idempotent — once a row is v2, new_blob returns None.

Background/admin/cryptoRotate v1 → v2 sweeps every encrypted column in batches (default 100 rows × 10k cap per call). Pages through v1 rows, decrypts + re-encrypts as v2, writes back. Run on a schedule until the per-table report shows v1 = 0 everywhere.

Tables swept (defined in services/pqc_rotator.py):

  • ai_providers.creds_encrypted
  • sso_configs.client_secret_encrypted
  • voice_providers.creds_encrypted
  • vault_entries.value_encrypted
  • migration_connections.client_secret_encrypted
  • legal_citation_providers.creds_encrypted
  • legal_dms_connections.oauth_tokens_encrypted + .api_key_encrypted
  • legal_bot_installations.creds_encrypted

Add new encrypted columns? Append to the ENCRYPTED_COLUMNS tuple and the next survey picks them up.

API

GET /api/v1/admin/crypto/status keymat fingerprint + algorithm details
GET /api/v1/admin/crypto/survey per-column v1 vs v2 counts (read-only)
POST /api/v1/admin/crypto/rotate decrypt + re-encrypt up to max_rows rows

Library

We ship pqcrypto — Python bindings around PQClean’s vetted reference implementations of ML-KEM and ML-DSA. Pure-pip install; no external C library required.

For deployments that need the FIPS-certified module (federal contracts, CNSA 2.0 mandate), swap to liboqs-python (bindings to liboqs.so). The secret_box.encrypt/decrypt contract stays identical — only the import in pqc.py changes.

Headline trade-offs

What ships
Cipher suiteNIST-standardized, August 2024 final
LibraryPQClean reference implementations (vetted, not yet certified)
PerformanceML-KEM-1024 keygen ~5x slower than X25519 alone; still microseconds
Blob size~1.6 KB larger than v1 (ML-KEM CT is 1568 B); storage cost negligible
HardwarePure-Python path; HSM / Nitro Enclave isolation on the roadmap
Key isolationEnv vars → process memory; enclave isolation on the roadmap

On the roadmap

  • Per-tenant keypairs. Single deployment-wide keymat today; per-tenant key isolation lands when there’s customer pull.
  • Hardware enclaves (Nitro / SGX / SEV-SNP). The cryptographic primitives don’t depend on enclaves; runtime isolation is a separate hardening pass.
  • TLS hybrid key exchange. That’s load-balancer config (Cloudflare already supports X25519MLKEM768); no application code touches it.
  • JWT signing. Session JWTs still use HS256. Migration to ML-DSA-65 happens in a follow-up.