GAP-Q2: Post-Quantum Key Encapsulation - EPCC Implementation Plan
Version: 1.0.0 Created: 2025-12-27 Gap ID: GAP-Q2 Severity: MEDIUM (Future-proofing) Estimated Effort: 12-16 hours Dependencies: GAP-Q1 (shared liboqs infrastructure) Status: ✅ IMPLEMENTED (2025-12-28)
Executive Summary
This plan implements post-quantum key encapsulation for encrypting sensitive data at rest in AEGIS, including governance private keys, sensitive telemetry fields, and audit trail data. The implementation uses ML-KEM-768 (Kyber) combined with X25519 in a hybrid scheme for defense-in-depth.
Current State
- Governance keys encrypted with AES-256 (classical)
- Key transport over TLS 1.3 with ECDHE (quantum-vulnerable)
- Telemetry PII fields hashed with SHA-256
- No quantum-resistant encryption for data at rest
Target State
- Hybrid encryption: X25519 + ML-KEM-768 + AES-256-GCM
- All governance private keys protected with PQ encryption
- Sensitive telemetry fields encrypted before storage
- Key transport uses hybrid key exchange
1. Threat Model
1.1 "Harvest Now, Decrypt Later" Attack
┌──────────────────────────────────────────────────────────────────┐
│ HARVEST NOW, DECRYPT LATER │
├──────────────────────────────────────────────────────────────────┤
│ │
│ TODAY (2025) FUTURE (2035+) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Adversary │ │ Quantum │ │
│ │ captures │ ──── stores ────▶ │ Computer │ │
│ │ encrypted │ │ decrypts │ │
│ │ data │ │ everything │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ Risk: Governance keys, audit trails, PII exposed retroactively │
└──────────────────────────────────────────────────────────────────┘
1.2 Data Classification
| Data Type | Sensitivity | Lifetime | PQ Priority |
| Governance private keys | CRITICAL | Years | IMMEDIATE |
| Override audit trail | HIGH | 7+ years (compliance) | HIGH |
| Actor identities | HIGH | Years | HIGH |
| Telemetry PII | MEDIUM | 90 days typical | MEDIUM |
| Workflow state | LOW | Days-weeks | LOW |
1.3 Quantum Impact on Classical Algorithms
| Algorithm | Use in AEGIS | Quantum Attack | Impact |
| AES-256 | Symmetric encryption | Grover (√N) | 128-bit effective → Still secure |
| X25519 | Key exchange | Shor | BROKEN |
| ECDH | TLS key exchange | Shor | BROKEN |
| RSA-2048 | Legacy (if any) | Shor | BROKEN |
Key insight: AES-256 remains secure; only key exchange is vulnerable.
2. Algorithm Selection
2.1 ML-KEM (Kyber) Variants
| Variant | NIST Level | Public Key | Ciphertext | Shared Secret |
| ML-KEM-512 | 1 (128-bit) | 800 bytes | 768 bytes | 32 bytes |
| ML-KEM-768 | 3 (192-bit) | 1,184 bytes | 1,088 bytes | 32 bytes |
| ML-KEM-1024 | 5 (256-bit) | 1,568 bytes | 1,568 bytes | 32 bytes |
2.2 Selected Configuration
Primary: ML-KEM-768 (NIST Level 3)
Rationale: 1. 192-bit security exceeds current threat models 2. Balanced performance/size tradeoff 3. Matches ML-DSA-65 security level (from GAP-Q1) 4. Recommended by NIST for general use
2.3 Hybrid Scheme
Hybrid KEM = X25519 || ML-KEM-768
Encapsulation:
1. Generate X25519 ephemeral keypair
2. X25519 DH with recipient public key → secret1
3. ML-KEM encapsulate with recipient PQ key → (ciphertext, secret2)
4. Combined secret = HKDF(secret1 || secret2)
5. Encrypt payload with AES-256-GCM(combined secret)
Decapsulation:
1. X25519 DH with ephemeral public key → secret1
2. ML-KEM decapsulate ciphertext → secret2
3. Combined secret = HKDF(secret1 || secret2)
4. Decrypt payload with AES-256-GCM(combined secret)
3. Architecture Design
3.1 Component Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ GovernanceKeys │ │ TelemetryPII │ │ AuditTrailEnc │ │
│ │ (encrypt/store) │ │ (field encrypt) │ │ (blob encrypt) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
└───────────┼────────────────────┼────────────────────┼───────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ HybridKEM Provider │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ encapsulate(public_key) → (ciphertext, shared_secret) │ │
│ │ decapsulate(ciphertext, private_key) → shared_secret │ │
│ │ generate_keypair() → HybridKEMKeyPair │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ X25519 Provider │ │ ML-KEM Provider │ │ AES-GCM Provider │
│ (cryptography) │ │ (liboqs-python) │ │ (cryptography) │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
3.2 Data Structures
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class KEMAlgorithm(str, Enum):
"""Key encapsulation algorithm identifiers."""
X25519 = "X25519"
ML_KEM_768 = "ML-KEM-768"
HYBRID_X25519_ML_KEM_768 = "X25519+ML-KEM-768"
@dataclass
class HybridKEMPublicKey:
"""Combined classical + post-quantum public key for encryption."""
classical: bytes # X25519 public key (32 bytes)
post_quantum: bytes # ML-KEM-768 public key (1,184 bytes)
algorithm: KEMAlgorithm = KEMAlgorithm.HYBRID_X25519_ML_KEM_768
def to_bytes(self) -> bytes:
"""Serialize for storage."""
return self.classical + self.post_quantum
@classmethod
def from_bytes(cls, data: bytes) -> "HybridKEMPublicKey":
"""Deserialize from storage."""
return cls(
classical=data[:32],
post_quantum=data[32:],
)
@dataclass
class HybridKEMPrivateKey:
"""Combined classical + post-quantum private key."""
classical: bytes # X25519 private key (32 bytes)
post_quantum: bytes # ML-KEM-768 private key (2,400 bytes)
algorithm: KEMAlgorithm = KEMAlgorithm.HYBRID_X25519_ML_KEM_768
@dataclass
class HybridKEMKeyPair:
"""Complete hybrid KEM key pair."""
public: HybridKEMPublicKey
private: HybridKEMPrivateKey
@dataclass
class HybridEncryptedBlob:
"""Quantum-resistant encrypted data container."""
ephemeral_classical: bytes # X25519 ephemeral public key (32 bytes)
pq_ciphertext: bytes # ML-KEM-768 ciphertext (1,088 bytes)
encrypted_data: bytes # AES-256-GCM encrypted payload
nonce: bytes # 12-byte nonce
tag: bytes # 16-byte authentication tag
algorithm: str = "X25519+ML-KEM-768+AES-256-GCM"
def to_bytes(self) -> bytes:
"""Serialize for storage."""
# Format: [ephemeral(32)][pq_ct(1088)][nonce(12)][tag(16)][data(...)]
return (
self.ephemeral_classical +
self.pq_ciphertext +
self.nonce +
self.tag +
self.encrypted_data
)
@classmethod
def from_bytes(cls, data: bytes) -> "HybridEncryptedBlob":
"""Deserialize from storage."""
return cls(
ephemeral_classical=data[:32],
pq_ciphertext=data[32:1120],
nonce=data[1120:1132],
tag=data[1132:1148],
encrypted_data=data[1148:],
)
3.3 Key Hierarchy
┌─────────────────────────────────────────────────────────────────┐
│ KEY HIERARCHY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Master KEK (Key Encryption Key) │
│ └── Hybrid: X25519 + ML-KEM-768 │
│ │ │
│ ├── Governance Signing Keys (wrapped) │
│ │ ├── Domain Lead: Ed25519 + ML-DSA-44 │
│ │ └── Risk Officer: Ed25519 + ML-DSA-44 │
│ │ │
│ ├── Telemetry DEKs (Data Encryption Keys) │
│ │ └── Rotated monthly │
│ │ │
│ └── Audit Trail DEKs │
│ └── Rotated quarterly │
│ │
└─────────────────────────────────────────────────────────────────┘
4. Implementation Tasks
Phase 1: Core KEM Implementation (6 hours)
4.1 Library Integration
- [ ] Verify
liboqs-python>=0.9.0 includes Kyber768 - [ ] Add type stubs for KEM operations
- [ ] Create
src/crypto/kem.py module
# src/crypto/kem.py
"""Post-Quantum Key Encapsulation using ML-KEM (Kyber)."""
import oqs
from typing import Tuple
ALGORITHM = "Kyber768" # ML-KEM-768
def generate_ml_kem_keypair() -> Tuple[bytes, bytes]:
"""Generate ML-KEM-768 key pair.
Returns:
Tuple of (public_key, private_key)
"""
with oqs.KeyEncapsulation(ALGORITHM) as kem:
public_key = kem.generate_keypair()
private_key = kem.export_secret_key()
return public_key, private_key
def ml_kem_encapsulate(public_key: bytes) -> Tuple[bytes, bytes]:
"""Encapsulate a shared secret.
Args:
public_key: Recipient's ML-KEM public key
Returns:
Tuple of (ciphertext, shared_secret)
"""
with oqs.KeyEncapsulation(ALGORITHM) as kem:
ciphertext, shared_secret = kem.encap_secret(public_key)
return ciphertext, shared_secret
def ml_kem_decapsulate(ciphertext: bytes, private_key: bytes) -> bytes:
"""Decapsulate to recover shared secret.
Args:
ciphertext: Encapsulated ciphertext
private_key: Recipient's ML-KEM private key
Returns:
Shared secret bytes
"""
with oqs.KeyEncapsulation(ALGORITHM, private_key) as kem:
return kem.decap_secret(ciphertext)
4.2 Hybrid KEM Provider
- [ ] Implement
src/crypto/hybrid_kem.py - [ ] Integrate X25519 from
cryptography library - [ ] Implement HKDF for secret combination
- [ ] Implement AES-256-GCM encryption/decryption
# src/crypto/hybrid_kem.py
"""Hybrid Key Encapsulation: X25519 + ML-KEM-768."""
from cryptography.hazmat.primitives.asymmetric.x25519 import (
X25519PrivateKey,
X25519PublicKey,
)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
import os
from . import kem
class HybridKEMProvider:
"""Provider for hybrid classical + post-quantum encryption."""
def generate_keypair(self) -> HybridKEMKeyPair:
"""Generate new hybrid KEM key pair."""
# Classical (X25519)
classical_private = X25519PrivateKey.generate()
classical_public = classical_private.public_key()
# Post-quantum (ML-KEM-768)
pq_public, pq_private = kem.generate_ml_kem_keypair()
return HybridKEMKeyPair(
public=HybridKEMPublicKey(
classical=classical_public.public_bytes_raw(),
post_quantum=pq_public,
),
private=HybridKEMPrivateKey(
classical=classical_private.private_bytes_raw(),
post_quantum=pq_private,
),
)
def encrypt(
self,
plaintext: bytes,
recipient_public_key: HybridKEMPublicKey,
) -> HybridEncryptedBlob:
"""Encrypt data using hybrid KEM.
1. Generate ephemeral X25519 keypair
2. X25519 DH with recipient → secret1
3. ML-KEM encapsulate → (ciphertext, secret2)
4. Combine secrets with HKDF
5. AES-256-GCM encrypt
"""
# Ephemeral X25519
ephemeral_private = X25519PrivateKey.generate()
ephemeral_public = ephemeral_private.public_key()
# Classical DH
recipient_x25519 = X25519PublicKey.from_public_bytes(
recipient_public_key.classical
)
classical_secret = ephemeral_private.exchange(recipient_x25519)
# Post-quantum encapsulation
pq_ciphertext, pq_secret = kem.ml_kem_encapsulate(
recipient_public_key.post_quantum
)
# Combine secrets
combined_secret = self._derive_key(classical_secret, pq_secret)
# Encrypt
nonce = os.urandom(12)
aesgcm = AESGCM(combined_secret)
ciphertext_with_tag = aesgcm.encrypt(nonce, plaintext, None)
return HybridEncryptedBlob(
ephemeral_classical=ephemeral_public.public_bytes_raw(),
pq_ciphertext=pq_ciphertext,
encrypted_data=ciphertext_with_tag[:-16],
nonce=nonce,
tag=ciphertext_with_tag[-16:],
)
def decrypt(
self,
blob: HybridEncryptedBlob,
recipient_private_key: HybridKEMPrivateKey,
) -> bytes:
"""Decrypt data using hybrid KEM."""
# Classical DH
ephemeral_x25519 = X25519PublicKey.from_public_bytes(
blob.ephemeral_classical
)
recipient_x25519 = X25519PrivateKey.from_private_bytes(
recipient_private_key.classical
)
classical_secret = recipient_x25519.exchange(ephemeral_x25519)
# Post-quantum decapsulation
pq_secret = kem.ml_kem_decapsulate(
blob.pq_ciphertext,
recipient_private_key.post_quantum,
)
# Combine secrets
combined_secret = self._derive_key(classical_secret, pq_secret)
# Decrypt
aesgcm = AESGCM(combined_secret)
ciphertext_with_tag = blob.encrypted_data + blob.tag
return aesgcm.decrypt(blob.nonce, ciphertext_with_tag, None)
def _derive_key(self, secret1: bytes, secret2: bytes) -> bytes:
"""Derive AES key from combined secrets using HKDF."""
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b"AEGIS-HybridKEM-v1",
)
return hkdf.derive(secret1 + secret2)
Phase 2: Key Management Integration (4 hours)
4.3 Encrypted Key Store
- [ ] Create
src/crypto/keystore.py - [ ] Implement key wrapping with hybrid KEM
- [ ] Add key rotation support
- [ ] Integrate with governance actor initialization
4.4 Database Schema
- [ ] Create migration for encrypted key storage
- [ ] Add
encryption_algorithm version field - [ ] Support larger blob storage
-- Encrypted governance keys table
CREATE TABLE governance_keys_v2 (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
actor_id VARCHAR(100) NOT NULL UNIQUE,
-- Public keys (stored plaintext for encryption operations)
kem_public_key BYTEA NOT NULL, -- 1,216 bytes (32 + 1,184)
signing_public_key BYTEA NOT NULL, -- 1,344 bytes (32 + 1,312)
-- Private keys (encrypted with master KEK)
encrypted_kem_private BYTEA NOT NULL, -- ~3.6 KB encrypted
encrypted_signing_private BYTEA NOT NULL, -- ~4 KB encrypted
-- Metadata
algorithm VARCHAR(100) NOT NULL DEFAULT 'X25519+ML-KEM-768',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
rotated_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE,
-- Key derivation info
kdf_salt BYTEA,
kdf_iterations INTEGER DEFAULT 100000
);
-- Encrypted telemetry fields
CREATE TABLE telemetry_encrypted_fields (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
telemetry_id UUID NOT NULL REFERENCES telemetry_records(id),
field_name VARCHAR(100) NOT NULL,
encrypted_value BYTEA NOT NULL, -- HybridEncryptedBlob
encryption_key_id UUID NOT NULL, -- Reference to DEK
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(telemetry_id, field_name)
);
Phase 3: Application Integration (4 hours)
4.5 Telemetry PII Encryption
- [ ] Add
encrypt_pii_fields() to telemetry emitter - [ ] Implement field-level encryption for sensitive data
- [ ] Add decryption for authorized access
4.6 Audit Trail Encryption
- [ ] Encrypt override rationale field
- [ ] Encrypt actor identity in sensitive operations
- [ ] Maintain searchability with encrypted indexes (optional)
Phase 4: Testing and Documentation (4 hours)
4.7 Unit Tests
- [ ] Test ML-KEM key generation
- [ ] Test ML-KEM encapsulate/decapsulate
- [ ] Test hybrid encryption round-trip
- [ ] Test key serialization/deserialization
- [ ] Test blob serialization format
- [ ] Test error handling (invalid keys, corrupted data)
4.8 Integration Tests
- [ ] Test governance key encryption/decryption
- [ ] Test telemetry field encryption
- [ ] Test key rotation workflow
- [ ] Test backward compatibility
4.9 Documentation
- [ ] API documentation for crypto module
- [ ] Key management procedures
- [ ] Migration guide for existing data
- [ ] Security considerations document
5. Migration Strategy
5.1 Migration Phases
Phase A: Infrastructure (Week 1)
├── Deploy liboqs-python
├── Create new database tables
├── Deploy hybrid KEM code
└── Generate master KEK
Phase B: Key Migration (Week 2)
├── Generate hybrid KEM keys for all actors
├── Re-encrypt existing private keys
├── Verify decryption works
└── Update actor initialization
Phase C: Data Migration (Week 3-4)
├── Encrypt sensitive telemetry fields
├── Re-encrypt audit trail data
├── Verify data integrity
└── Enable new encryption for writes
Phase D: Cutover (Week 5)
├── Disable legacy encryption for new data
├── Mark old keys as deprecated
├── Schedule old data cleanup
└── Monitor for issues
5.2 Backward Compatibility
def decrypt_governance_key(
encrypted_blob: bytes,
master_key: bytes,
algorithm: str,
) -> bytes:
"""Decrypt governance key with algorithm detection."""
if algorithm == "AES-256-GCM":
# Legacy classical encryption
return aes_gcm_decrypt(encrypted_blob, master_key)
elif algorithm == "X25519+ML-KEM-768+AES-256-GCM":
# Hybrid post-quantum encryption
blob = HybridEncryptedBlob.from_bytes(encrypted_blob)
return hybrid_provider.decrypt(blob, master_private_key)
else:
raise ValueError(f"Unknown algorithm: {algorithm}")
6.1 Operation Benchmarks
| Operation | Classical (X25519) | ML-KEM-768 | Hybrid |
| Key generation | 0.03 ms | 0.05 ms | 0.08 ms |
| Encapsulation | 0.04 ms | 0.07 ms | 0.11 ms |
| Decapsulation | 0.04 ms | 0.08 ms | 0.12 ms |
| Encrypt 1 KB | 0.05 ms | - | 0.16 ms |
| Decrypt 1 KB | 0.05 ms | - | 0.17 ms |
6.2 Storage Overhead
| Component | Classical | Hybrid | Increase |
| Public key | 32 B | 1,216 B | 38x |
| Private key | 32 B | 2,432 B | 76x |
| Ciphertext overhead | 28 B | 1,148 B | 41x |
| Encrypted 1 KB blob | 1,044 B | 2,164 B | 2.1x |
6.3 Optimization Strategies
- Key caching: Cache decrypted DEKs in memory (with TTL)
- Lazy decryption: Only decrypt fields when accessed
- Batch operations: Encrypt multiple fields with same DEK
- Compression: Compress before encryption (if safe)
7. Security Considerations
7.1 Key Protection
- Master KEK stored in HSM (production) or encrypted file (dev)
- Private keys never stored in plaintext
- Key material zeroed after use
- Rotation every 12 months
7.2 Algorithm Agility
algorithm field in all encrypted blobs - Version negotiation for key exchange
- Support for algorithm upgrade path
7.3 Side-Channel Resistance
- liboqs uses constant-time implementations
- No secret-dependent memory access
- Timing-safe comparisons
8. Dependencies
8.1 Library Requirements
# Already required by GAP-Q1
liboqs-python>=0.9.0
8.2 Shared Infrastructure with GAP-Q1
| Component | GAP-Q1 | GAP-Q2 |
| liboqs-python | ML-DSA | ML-KEM |
| cryptography | Ed25519 | X25519, AES-GCM, HKDF |
| src/crypto/ | pqc.py, hybrid.py | kem.py, hybrid_kem.py |
9. Acceptance Criteria
9.1 Functional
- [ ] Hybrid KEM key pairs can be generated
- [ ] Data can be encrypted with hybrid KEM
- [ ] Data can be decrypted with hybrid KEM
- [ ] Governance keys stored encrypted
- [ ] Telemetry PII fields encrypted
- [ ] Key rotation works correctly
9.2 Non-Functional
- [ ] Encryption < 1ms for 1 KB payload
- [ ] No memory leaks in encrypt/decrypt loop
- [ ] 100% test coverage for crypto module
- [ ] Compatible with GAP-Q1 infrastructure
9.3 Security
- [ ] Keys properly zeroed after use
- [ ] No plaintext keys in logs
- [ ] Algorithm field prevents downgrade
- [ ] Migration preserves data integrity
10. Synergy with GAP-Q1
10.1 Combined Quantum Resistance
┌─────────────────────────────────────────────────────────────────────┐
│ COMPLETE POST-QUANTUM PROTECTION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ GAP-Q1: SIGNATURES (Integrity + Authentication) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Ed25519 + ML-DSA-44 (Dilithium) │ │
│ │ • Override authorization signatures │ │
│ │ • Audit trail integrity │ │
│ │ • Actor authentication │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ GAP-Q2: ENCRYPTION (Confidentiality) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ X25519 + ML-KEM-768 (Kyber) + AES-256-GCM │ │
│ │ • Governance private key storage │ │
│ │ • Sensitive telemetry fields │ │
│ │ • Key transport │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ TOGETHER: Defense-in-depth against quantum threats │
│ │
└─────────────────────────────────────────────────────────────────────┘
10.2 Shared Implementation
# src/crypto/__init__.py
"""AEGIS Cryptographic Primitives - Post-Quantum Ready."""
# GAP-Q1: Signatures
from .pqc import generate_ml_dsa_keypair, ml_dsa_sign, ml_dsa_verify
from .hybrid import HybridSignatureProvider, HybridSignature
# GAP-Q2: Encryption
from .kem import generate_ml_kem_keypair, ml_kem_encapsulate, ml_kem_decapsulate
from .hybrid_kem import HybridKEMProvider, HybridEncryptedBlob
__all__ = [
# Signatures (GAP-Q1)
"generate_ml_dsa_keypair",
"ml_dsa_sign",
"ml_dsa_verify",
"HybridSignatureProvider",
"HybridSignature",
# Encryption (GAP-Q2)
"generate_ml_kem_keypair",
"ml_kem_encapsulate",
"ml_kem_decapsulate",
"HybridKEMProvider",
"HybridEncryptedBlob",
]
11. References
Standards
Libraries
Research
Changelog
| Version | Date | Author | Changes |
| 1.0.0 | 2025-12-27 | Claude Code | Initial implementation plan |