Skip to content

Implement FIPS 203 (ML-KEM) Support for Quantum-Resistant Key Encapsulation in OpenTDFKit #29

@superninja-app

Description

@superninja-app

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

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

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

Implementations & Libraries

Research & Analysis

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 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions