arkavo-org / arkavo-org/OpenTDFKit
Implement FIPS 203 (ML-KEM) Support for Quantum-Resistant Key Encapsulation in OpenTDFKit
- Dominant language
- Swift
- Stars
- 1
- Forks
- 1
- Avg merge
- 9d 4h
- Merged PRs (30d)
- 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](https://github.com/arkavo-org/opentdf-rs/issues/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)**
```swift
// 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
```swift
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()
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
```swift
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:
```swift
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:
```swift
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:
```json
{
"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 #22**: [Adopt Post-Quantum Cryptography with Hybrid Approach](https://github.com/arkavo-org/OpenTDFKit/issues/22)
- Comprehensive PQC roadmap and strategy for OpenTDFKit
- Hybrid implementation approach
- Swift ecosystem considerations
- **opentdf-rs Issue #60**: [Implement FIPS 203 (ML-KEM) Support](https://github.com/arkavo-org/opentdf-rs/issues/60)
- **CRITICAL**: Sister implementation in Rust
- **Must coordinate** on protocol extensions and test vectors
- **Interoperability required** for cross-platform TDF support
## Dependencies
### Swift Packages
```swift
// 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
### 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](https://doi.org/10.6028/NIST.FIPS.203)
- [NIST Post-Quantum Cryptography Project](https://csrc.nist.gov/projects/post-quantum-cryptography)
- [NIST SP 800-227: ML-KEM Implementation Guidance](https://csrc.nist.gov/publications/detail/sp/800-227/draft)
### Implementations & Libraries
- [liboqs - Open Quantum Safe](https://github.com/open-quantum-safe/liboqs)
- [Signal SPQR Protocol](https://signal.org/docs/specifications/pqxdh/) - Production PQC deployment
- [libcrux-ml-kem (Cryspen)](https://github.com/cryspen/libcrux) - Reference for Rust implementation
- [PQClean](https://github.com/PQClean/PQClean) - Reference implementations
### Research & Analysis
- [CRYSTALS-Kyber Paper](https://pq-crystals.org/kyber/data/kyber-specification-round3-20210804.pdf)
- [Post-Quantum Cryptography: Current State and Quantum Mitigation](https://csrc.nist.gov/CSRC/media/Presentations/2023/pqc-current-state-and-quantum-mitigation/images-media/PQC-Current-State-and-Quantum-Mitigation-Moody-May2023.pdf)
## Labels
- `enhancement` - New feature request
- `cryptography` - Cryptographic implementation
- `post-quantum` - Post-quantum cryptography
- `security` - Security enhancement
- `FIPS` - FIPS standards compliance
- `interoperability` - 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 #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
Contributor guide
No contributing guide indexed for this repository
Research direction
Begin with the proposed MlKemWrapper and MlKemKeyEncapsulation entry points, then inspect the MlKemTests examples and the existing PQC roadmap in Issue #22. Confirm the ML-KEM-768 sizes, round-trip behavior, and interoperability requirements against opentdf-rs Issue #60; the work is done when the specified tests and cross-SDK TDF interoperability pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, swift
- Domain
- cryptography, mobile-dev, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100