-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Implement FIPS 203 (ML-KEM) Support for Quantum-Resistant Key Encapsulation in OpenTDFKit
Description
Implement support for FIPS 203 (Module-Lattice-Based Key-Encapsulation Mechanism Standard), also known as ML-KEM, to provide quantum-resistant key encapsulation for OpenTDFKit. This enhancement builds upon the existing post-quantum cryptography (PQC) roadmap outlined in Issue #22 and provides concrete implementation steps for the Swift/iOS ecosystem.
Cross-Repository Coordination: This issue is linked to opentdf-rs Issue #60, which implements the same FIPS 203 support in Rust. Both implementations must interoperate to ensure TDF files encrypted by one SDK can be decrypted by the other.
Background on FIPS 203
FIPS 203 is the NIST-standardized Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM), finalized in August 2024. It is based on the CRYSTALS-Kyber algorithm and provides quantum-resistant key encapsulation with three security levels:
| Parameter Set | Security Level | Public Key Size | Ciphertext Size | Shared Secret |
|---|---|---|---|---|
| ML-KEM-512 | 128-bit (Category 1) | 800 bytes | 768 bytes | 32 bytes |
| ML-KEM-768 | 192-bit (Category 3) | 1,184 bytes | 1,088 bytes | 32 bytes |
| ML-KEM-1024 | 256-bit (Category 5) | 1,568 bytes | 1,568 bytes | 32 bytes |
Recommended: ML-KEM-768 (192-bit security) provides strong quantum resistance with reasonable key/ciphertext sizes.
Motivation
1. Quantum Threat Protection
- Harvest-Now-Decrypt-Later (HNDL): Adversaries can store encrypted TDF files today and decrypt them when quantum computers become available (estimated 10-15 years)
- Long-term Data Protection: TDF is designed for long-term encrypted storage, making quantum resistance critical
- Mobile Security: iOS/macOS devices are high-value targets for quantum decryption
- Zero Trust Alignment: PQC strengthens the Zero Trust security model
2. Standards Compliance
- NIST has finalized FIPS 203 (August 2024), making ML-KEM production-ready
- Future regulatory requirements likely to mandate post-quantum cryptography
- Early adoption positions OpenTDFKit as a security leader in the Swift ecosystem
3. Ecosystem Maturity
- Swift can leverage proven C/C++ libraries through interop:
- liboqs: Open Quantum Safe library with ML-KEM support
- PQClean: Reference implementations
- Signal has successfully deployed ML-KEM in production (SPQR protocol)
- Rust implementation (opentdf-rs) provides reference for interoperability
4. Current Vulnerability
OpenTDFKit currently uses quantum-vulnerable algorithms:
- ❌ P-256/P-384/P-521 ECDH (Key agreement) - Vulnerable to Shor's algorithm
- ❌ ECDSA P-256/P-384/P-521 (Signatures) - Vulnerable to Shor's algorithm
- ✅ AES-256-GCM - Already quantum-resistant
- ✅ HMAC-SHA256 - Already quantum-resistant
Proposed Implementation
Phase 1: ML-KEM-768 Integration (Months 1-4)
1.1 Choose Swift Integration Approach
Option A: C Library Bridging via liboqs (Recommended for Phase 1)
// Use liboqs via Swift C interop
import liboqs
public class MlKemWrapper {
private let kem: OQS_KEM
init() throws {
guard let kem = OQS_KEM_new(OQS_KEM_alg_ml_kem_768) else {
throw KemError.initializationFailed
}
self.kem = kem
}
func generateKeyPair() throws -> (publicKey: Data, privateKey: Data) {
var publicKey = [UInt8](repeating: 0, count: Int(kem.length_public_key))
var privateKey = [UInt8](repeating: 0, count: Int(kem.length_secret_key))
let result = OQS_KEM_keypair(kem, &publicKey, &privateKey)
guard result == OQS_SUCCESS else {
throw KemError.keyGenerationFailed
}
return (Data(publicKey), Data(privateKey))
}
func encapsulate(publicKey: Data) throws -> (ciphertext: Data, sharedSecret: Data) {
var ciphertext = [UInt8](repeating: 0, count: Int(kem.length_ciphertext))
var sharedSecret = [UInt8](repeating: 0, count: Int(kem.length_shared_secret))
let result = publicKey.withUnsafeBytes { pubKeyPtr in
OQS_KEM_encaps(kem, &ciphertext, &sharedSecret, pubKeyPtr.baseAddress)
}
guard result == OQS_SUCCESS else {
throw KemError.encapsulationFailed
}
return (Data(ciphertext), Data(sharedSecret))
}
func decapsulate(ciphertext: Data, privateKey: Data) throws -> Data {
var sharedSecret = [UInt8](repeating: 0, count: Int(kem.length_shared_secret))
let result = ciphertext.withUnsafeBytes { ctPtr in
privateKey.withUnsafeBytes { skPtr in
OQS_KEM_decaps(kem, &sharedSecret, ctPtr.baseAddress, skPtr.baseAddress)
}
}
guard result == OQS_SUCCESS else {
throw KemError.decapsulationFailed
}
return Data(sharedSecret)
}
}1.2 Implement Key Encapsulation Protocol
public protocol KeyEncapsulation {
associatedtype PublicKey
associatedtype PrivateKey
associatedtype WrappedKey
func wrap(_ key: Data, publicKey: PublicKey) throws -> WrappedKey
func unwrap(_ wrapped: WrappedKey, privateKey: PrivateKey) throws -> Data
}
public struct MlKemKeyEncapsulation: KeyEncapsulation {
public typealias PublicKey = Data // ML-KEM-768 public key (1184 bytes)
public typealias PrivateKey = Data // ML-KEM-768 private key
public typealias WrappedKey = Data // Ciphertext + nonce + wrapped key
private let mlkem: MlKemWrapper
public init() throws {
self.mlkem = try MlKemWrapper()
}
public func wrap(_ key: Data, publicKey: PublicKey) throws -> WrappedKey {
// 1. Encapsulate to generate shared secret and ciphertext
let (ciphertext, sharedSecret) = try mlkem.encapsulate(publicKey: publicKey)
// 2. Derive wrapping key from shared secret using HKDF
let salt = "OpenTDF-ML-KEM-768".data(using: .utf8)!
let wrappingKey = try deriveKey(from: sharedSecret, salt: salt, outputLength: 32)
// 3. Wrap payload key with AES-256-GCM
let nonce = try generateNonce()
let sealedBox = try AES.GCM.seal(key, using: SymmetricKey(data: wrappingKey), nonce: nonce)
// 4. Return ciphertext || nonce || wrapped_key
var result = Data()
result.append(ciphertext)
result.append(nonce)
result.append(sealedBox.ciphertext)
result.append(sealedBox.tag)
return result
}
public func unwrap(_ wrapped: WrappedKey, privateKey: PrivateKey) throws -> Data {
// 1. Parse components
let ciphertext = wrapped.prefix(1088)
let nonce = wrapped.dropFirst(1088).prefix(12)
let encryptedKey = wrapped.dropFirst(1100)
// 2. Decapsulate to recover shared secret
let sharedSecret = try mlkem.decapsulate(ciphertext: ciphertext, privateKey: privateKey)
// 3. Derive wrapping key from shared secret
let salt = "OpenTDF-ML-KEM-768".data(using: .utf8)!
let wrappingKey = try deriveKey(from: sharedSecret, salt: salt, outputLength: 32)
// 4. Unwrap payload key with AES-256-GCM
let sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: nonce),
ciphertext: encryptedKey.dropLast(16),
tag: encryptedKey.suffix(16))
let key = try AES.GCM.open(sealedBox, using: SymmetricKey(data: wrappingKey))
return key
}
private func deriveKey(from sharedSecret: Data, salt: Data, outputLength: Int) throws -> Data {
let hkdf = HKDF<SHA256>()
return try hkdf.deriveKey(inputKeyMaterial: SymmetricKey(data: sharedSecret),
salt: salt,
info: Data(),
outputByteCount: outputLength)
}
private func generateNonce() throws -> Data {
var nonce = Data(count: 12)
let result = nonce.withUnsafeMutableBytes { ptr in
SecRandomCopyBytes(kSecRandomDefault, 12, ptr.baseAddress!)
}
guard result == errSecSuccess else {
throw KemError.nonceGenerationFailed
}
return nonce
}
}1.3 Add Comprehensive Tests
import XCTest
@testable import OpenTDFKit
final class MlKemTests: XCTestCase {
func testMlKem768Roundtrip() throws {
let kem = try MlKemKeyEncapsulation()
let mlkem = try MlKemWrapper()
// Generate keypair
let (publicKey, privateKey) = try mlkem.generateKeyPair()
// Wrap a test key
let payloadKey = Data("test_payload_key_32_bytes_long!".utf8)
let wrapped = try kem.wrap(payloadKey, publicKey: publicKey)
// Unwrap and verify
let unwrapped = try kem.unwrap(wrapped, privateKey: privateKey)
XCTAssertEqual(payloadKey, unwrapped)
}
func testMlKemKeySizes() throws {
let mlkem = try MlKemWrapper()
let (publicKey, privateKey) = try mlkem.generateKeyPair()
// Verify FIPS 203 specified sizes
XCTAssertEqual(publicKey.count, 1184) // ML-KEM-768 public key
XCTAssertEqual(privateKey.count, 2400) // ML-KEM-768 private key
}
func testMlKemCiphertextSize() throws {
let kem = try MlKemKeyEncapsulation()
let mlkem = try MlKemWrapper()
let (publicKey, _) = try mlkem.generateKeyPair()
let payloadKey = Data("test_payload_key_32_bytes_long!".utf8)
let wrapped = try kem.wrap(payloadKey, publicKey: publicKey)
// Wrapped format: ciphertext (1088) + nonce (12) + wrapped_key (~48)
XCTAssertGreaterThanOrEqual(wrapped.count, 1148)
}
func testInteroperabilityWithRust() throws {
// Test vectors from opentdf-rs implementation
// This ensures OpenTDFKit can decrypt TDFs created by opentdf-rs
let rustPublicKey = Data(base64Encoded: "...")! // From opentdf-rs test
let rustCiphertext = Data(base64Encoded: "...")! // From opentdf-rs test
let expectedSharedSecret = Data(base64Encoded: "...")! // From opentdf-rs test
// Verify we can process Rust-generated keys
let kem = try MlKemKeyEncapsulation()
// Add interoperability test logic
}
}Phase 2: Hybrid Classical + PQC Mode (Months 5-8)
Implement hybrid mode combining ECDH with ML-KEM:
public enum KasKeyCurve: UInt8, Sendable, CaseIterable {
case secp256r1 = 0x00
case secp384r1 = 0x01
case secp521r1 = 0x02
// Add new hybrid curves
case mlkem768_secp256r1 = 0x10 // Hybrid: ML-KEM-768 + P-256
case mlkem1024_secp384r1 = 0x11 // Hybrid: ML-KEM-1024 + P-384
}
public struct HybridKeyEncapsulation: KeyEncapsulation {
private let classical: EcdhKeyEncapsulation
private let pqc: MlKemKeyEncapsulation
public func wrap(_ key: Data, publicKey: HybridPublicKey) throws -> HybridWrappedKey {
// 1. Wrap with classical KEM (ECDH)
let classicalWrapped = try classical.wrap(key, publicKey: publicKey.classical)
// 2. Wrap with ML-KEM
let pqcWrapped = try pqc.wrap(key, publicKey: publicKey.pqc)
// 3. Return combined wrapped key
return HybridWrappedKey(
classical: classicalWrapped,
pqc: pqcWrapped
)
}
public func unwrap(_ wrapped: HybridWrappedKey, privateKey: HybridPrivateKey) throws -> Data {
// 1. Unwrap with classical KEM
let keyClassical = try classical.unwrap(wrapped.classical, privateKey: privateKey.classical)
// 2. Unwrap with ML-KEM
let keyPqc = try pqc.unwrap(wrapped.pqc, privateKey: privateKey.pqc)
// 3. Verify both keys match (security: either KEM must be secure)
guard keyClassical == keyPqc else {
throw KemError.hybridKeyMismatch
}
return keyClassical
}
}Phase 3: NanoTDF Format Integration (Months 9-12)
Update NanoTDF header to support PQC metadata:
public struct Header {
// Existing fields
public var kas: KasMetadata
public var ephemeralKey: Data // Now can be classical OR hybrid
// New fields for hybrid mode
public var pqEphemeralKey: Data? // ML-KEM public key
public var pqPolicyBinding: Data? // ML-DSA signature (future)
// Version negotiation
public static let versionV12: UInt8 = 0x4C // "L1L" - classical
public static let version: UInt8 = 0x4D // "L1M" - classical
public static let versionV14Hybrid: UInt8 = 0x4E // "L1N" - hybrid PQ
}TDF manifest schema for PQC:
{
"encryptionInformation": {
"keyAccess": [{
"type": "hybrid",
"version": "v2-pqc-hybrid",
"classical": {
"algorithm": "ECDH-P256",
"ephemeralPublicKey": "base64_ecdh_public_key"
},
"postQuantum": {
"algorithm": "ML-KEM-768",
"ciphertext": "base64_mlkem_ciphertext"
},
"url": "https://kas.example.com",
"protocol": "kas",
"policyBinding": {...}
}]
}
}Acceptance Criteria
Functional Requirements
- ML-KEM-768 wrapper fully implemented with FIPS 203 compliance
- Key generation, encapsulation, and decapsulation working correctly
- Hybrid mode combining classical + PQC KEMs
- NanoTDF header format updated for PQC metadata
- Backward compatibility maintained with classical-only NanoTDF
- Interoperability with opentdf-rs verified through test vectors
Testing Requirements
- Unit tests for ML-KEM operations (roundtrip, key sizes, ciphertext sizes)
- Integration tests with KAS protocol
- Cross-platform interoperability tests with opentdf-rs
- Performance benchmarks on iOS/macOS (target <10ms overhead)
- Memory usage tests for mobile constraints
- Constant-time verification tests (timing side-channel resistance)
Documentation Requirements
- API documentation for PQC modules
- Security whitepaper explaining quantum threat and mitigation
- Migration guide for existing OpenTDFKit users
- Performance comparison benchmarks (iOS/macOS specific)
- Swift Package Manager integration guide
- Interoperability guide with opentdf-rs
Security Requirements
- Side-channel resistance validation
- Key zeroization after use (Swift memory management)
- Secure random number generation (SecRandomCopyBytes)
- HKDF key derivation for shared secret processing
- Code review by cryptography experts
Related Issues
-
Issue Adopt Post-Quantum Cryptography with Hybrid Approach #22: Adopt Post-Quantum Cryptography with Hybrid Approach
- Comprehensive PQC roadmap and strategy for OpenTDFKit
- Hybrid implementation approach
- Swift ecosystem considerations
-
opentdf-rs Issue #60: Implement FIPS 203 (ML-KEM) Support
- CRITICAL: Sister implementation in Rust
- Must coordinate on protocol extensions and test vectors
- Interoperability required for cross-platform TDF support
Dependencies
Swift Packages
// Package.swift
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
// Add liboqs via system library or SPM wrapper
]System Libraries
- liboqs: Open Quantum Safe library for ML-KEM
- Install via Homebrew:
brew install liboqs - Link via Swift Package Manager or manual bridging
- Install via Homebrew:
External Coordination
- OpenTDF Specification: Coordinate with OpenTDF community for PQC protocol extensions
- opentdf-rs: Joint RFC and implementation planning for interoperability
- KAS Servers: KAS implementations must support PQC rewrap protocol
- Swift Crypto Ecosystem: Monitor Apple's CryptoKit roadmap for native PQC support
Risks & Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| OpenTDF spec divergence | HIGH | HIGH | Coordinate with community via RFC before implementation |
| opentdf-rs incompatibility | HIGH | HIGH | Joint test vectors and continuous integration testing |
| C bridging complexity | MEDIUM | MEDIUM | Use proven liboqs library, extensive testing |
| Performance on iOS | MEDIUM | MEDIUM | Benchmark early, optimize hot paths, async operations |
| Library immaturity | LOW | MEDIUM | Use proven libraries (liboqs, used in production) |
| Key size overhead | LOW | LOW | +2KB per KAS acceptable for security benefit |
| Swift ecosystem gaps | MEDIUM | HIGH | Contribute to ecosystem, monitor Apple's roadmap |
Timeline
Phase 1: ML-KEM Implementation (Months 1-4)
- Month 1: Setup liboqs integration and Swift bridging
- Month 2: Implement ML-KEM wrapper and key encapsulation
- Month 3: Add comprehensive tests and benchmarks
- Month 4: Documentation and code review
Phase 2: Hybrid Mode (Months 5-8)
- Month 5: Implement HybridKeyEncapsulation
- Month 6: NanoTDF header format updates
- Month 7: Integration testing with KAS
- Month 8: Cross-platform interoperability testing with opentdf-rs
Phase 3: Production Readiness (Months 9-12)
- Month 9: Security audit preparation
- Month 10: Performance optimization for iOS/macOS
- Month 11: Beta release with opt-in PQC support
- Month 12: Documentation and migration guides
Performance Targets
Based on current OpenTDFKit benchmarks and mobile constraints:
| Operation | Current (P-256) | Target (Hybrid) | Notes |
|---|---|---|---|
| Key generation | ~1.1ms | <5ms | Acceptable for async operations |
| Key agreement | ~1.3ms | <10ms | Network-bound, tolerable overhead |
| Encapsulation | N/A | <8ms | New operation, must be efficient |
| NanoTDF size | ~250 bytes | <2KB | Acceptable for mobile data transfer |
| Memory usage | ~50KB | <500KB | Critical for iOS memory constraints |
References
Standards & Specifications
- NIST FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard
- NIST Post-Quantum Cryptography Project
- NIST SP 800-227: ML-KEM Implementation Guidance
Implementations & Libraries
- liboqs - Open Quantum Safe
- Signal SPQR Protocol - Production PQC deployment
- libcrux-ml-kem (Cryspen) - Reference for Rust implementation
- PQClean - Reference implementations
Research & Analysis
Labels
enhancement- New feature requestcryptography- Cryptographic implementationpost-quantum- Post-quantum cryptographysecurity- Security enhancementFIPS- FIPS standards complianceinteroperability- Cross-platform compatibility
Priority
HIGH - Critical for long-term security, future compliance, and ecosystem interoperability
Notes
- CRITICAL: Coordinate with opentdf-rs team (Issue #60) for interoperability
- CRITICAL: Joint test vectors and continuous integration testing required
- This builds upon existing PQC roadmap in Issue Adopt Post-Quantum Cryptography with Hybrid Approach #22
- FIPS 203 was finalized in August 2024, making this production-ready
- Signal has successfully deployed ML-KEM in production (SPQR protocol)
- Hybrid mode ensures security even if one algorithm is broken
- Swift ecosystem requires C bridging initially, but pure Swift implementation is long-term goal
- Performance on iOS/macOS must be carefully benchmarked and optimized