Table of Contents

Key Encapsulation Mechanisms (KEM) Reference

This page provides detailed documentation for the Key Encapsulation Mechanisms (KEMs) implemented in the CryptoHives.Foundation.Security.Cryptography package.

Namespace

using CryptoHives.Foundation.Security.Cryptography.Kem;

Overview

A Key Encapsulation Mechanism establishes a shared secret between two parties using public-key cryptography. The sender encapsulates a fresh shared secret against the receiver's public (encapsulation) key, producing a ciphertext; the receiver decapsulates the ciphertext with its private (decapsulation) key to recover the same secret. The shared secret is then typically fed into a KDF (e.g. HKDF) to derive symmetric keys.

KEM Source Security Category Primary Use
ML-KEM-512 FIPS 203 1 (~AES-128) Constrained environments
ML-KEM-768 FIPS 203 3 (~AES-192) Recommended default
ML-KEM-1024 FIPS 203 5 (~AES-256) Maximum security margin

Why Post-Quantum KEMs

ML-KEM (Module-Lattice-Based KEM, derived from CRYSTALS-Kyber) is the NIST-standardized post-quantum replacement for key establishment based on RSA or elliptic-curve Diffie-Hellman, which are broken by a cryptographically relevant quantum computer. Migration timelines (e.g. CNSA 2.0) require new systems to support quantum-resistant key establishment well before 2030.

This implementation is fully managed and runs identically on every target framework — including .NET Framework 4.6.2 and .NET Standard 2.0, where the in-box System.Security.Cryptography.MLKem (.NET 10+, OS-backed) is not available.


ML-KEM (FIPS 203)

ML-KEM is specified in FIPS 203 (final, August 2024). It is an IND-CCA2-secure KEM built from the Module-LWE problem, combining the K-PKE public-key encryption scheme with the Fujisaki–Okamoto transform using implicit rejection.

Parameters (FIPS 203 Table 1)

Parameter ML-KEM-512 ML-KEM-768 ML-KEM-1024
Security category 1 3 5
Module rank k 2 3 4
η₁ / η₂ 3 / 2 2 / 2 2 / 2
d_u / d_v 10 / 4 10 / 4 11 / 5
Encapsulation key 800 bytes 1,184 bytes 1,568 bytes
Decapsulation key (expanded) 1,632 bytes 2,400 bytes 3,168 bytes
Private seed (d ‖ z) 64 bytes 64 bytes 64 bytes
Ciphertext 768 bytes 1,088 bytes 1,568 bytes
Shared secret 32 bytes 32 bytes 32 bytes

Two API Levels

API Classes Best For
Key-holding (recommended) MLKem, MLKemAlgorithm Application code; mirrors .NET 10's System.Security.Cryptography.MLKem
Low-level, stateless IKem, MLKem512, MLKem768, MLKem1024 Protocol implementations that manage raw key bytes; allocation-free span APIs

Key-Holding API (MLKem)

MLKem carries the same type name, member names and member signatures as .NET 10's System.Security.Cryptography.MLKem, so it is a source-level drop-in — switching to the managed implementation means changing the using and nothing else:

-using System.Security.Cryptography;
+using CryptoHives.Foundation.Security.Cryptography.Kem;

Importing both namespaces in one file is CS0104 (ambiguous reference) on .NET 10, which is the intended consequence of matching the BCL names. Alias one of them (using Bcl = System.Security.Cryptography;) if you need both in the same file.

Unlike the in-box type, this implementation is not an abstract class with platform-backed derived types — it is a single sealed managed class, so MLKem.IsSupported is always true and behaviour is identical on every OS and every target framework down to net462.

using CryptoHives.Foundation.Security.Cryptography.Kem;

// Receiver: generate a key pair and publish the encapsulation key.
using var receiver = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
byte[] encapsulationKey = receiver.ExportEncapsulationKey();

// Sender: encapsulate a shared secret for the receiver.
using var sender = MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, encapsulationKey);
byte[] ciphertext   = new byte[sender.Algorithm.CiphertextSizeInBytes];
byte[] senderSecret = new byte[sender.Algorithm.SharedSecretSizeInBytes];
sender.Encapsulate(ciphertext, senderSecret);

// Receiver: recover the same shared secret.
byte[] receiverSecret = new byte[receiver.Algorithm.SharedSecretSizeInBytes];
receiver.Decapsulate(ciphertext, receiverSecret);
// senderSecret and receiverSecret are identical

Key Storage via Private Seed

FIPS 203 recommends storing the 64-byte private seed (d ‖ z) instead of the expanded decapsulation key. A key created from a seed can always be re-expanded deterministically:

using var key = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
byte[] seed = key.ExportPrivateSeed(); // 64 bytes — store this

// Later / elsewhere:
using var restored = MLKem.ImportPrivateSeed(MLKemAlgorithm.MLKem768, seed);
// restored is byte-identical to the original key pair

Keys imported from an expanded decapsulation key (ImportDecapsulationKey) hold no seed; ExportPrivateSeed throws for them.

Methods

Method Description
IsSupported Always true — the managed implementation never depends on OS support
GenerateKey(MLKemAlgorithm) Generate a fresh key pair (retains the private seed)
ImportPrivateSeed(MLKemAlgorithm, ReadOnlySpan<byte>) / (…, byte[]) Expand a 64-byte (d ‖ z) seed into a key pair
ImportDecapsulationKey(MLKemAlgorithm, ReadOnlySpan<byte>) / (…, byte[]) Import an expanded decapsulation key (runs the §7.3 hash check)
ImportEncapsulationKey(MLKemAlgorithm, ReadOnlySpan<byte>) / (…, byte[]) Import a public key (runs the §7.2 modulus check)
Encapsulate(Span<byte>, Span<byte>) Produce ciphertext + shared secret into caller buffers
Encapsulate(out byte[], out byte[]) Produce ciphertext + shared secret as new arrays
Decapsulate(ReadOnlySpan<byte>, Span<byte>) Recover the shared secret (implicit rejection on invalid ciphertext)
Decapsulate(byte[]) Recover the shared secret as a new array
ExportPrivateSeed() / ExportPrivateSeed(Span<byte>) Export the 64-byte seed (seed-created keys only)
ExportEncapsulationKey() / ExportEncapsulationKey(Span<byte>) Export the public key
ExportDecapsulationKey() / ExportDecapsulationKey(Span<byte>) Export the expanded private key
ImportPkcs8PrivateKey / ImportSubjectPublicKeyInfo Import from DER, with byte[] and span overloads
ImportEncryptedPkcs8PrivateKey Import a password-protected key
ImportFromPem / ImportFromEncryptedPem Import from RFC 7468 text
ExportPkcs8PrivateKey / ExportSubjectPublicKeyInfo Export to DER, with TryExport… and …Pem forms
ExportEncryptedPkcs8PrivateKey Export password-protected, PBES2
Dispose() Zeroize the private seed and decapsulation key

This is the complete System.Security.Cryptography.MLKem surface apart from the key-format members listed under KEM Roadmap. MLKemAlgorithm likewise matches the in-box type, including IEquatable<MLKemAlgorithm>, == and !=.

Low-Level API (IKem)

The stateless per-parameter-set classes operate directly on caller-provided buffers and never retain key material:

using CryptoHives.Foundation.Security.Cryptography.Kem;

using var kem = MLKem768.Create();

// Key generation (random, or deterministic from a 64-byte seed)
byte[] ek = new byte[MLKem768.EncapsulationKeySizeBytesConst];   // 1184
byte[] dk = new byte[MLKem768.DecapsulationKeySizeBytesConst];   // 2400
kem.GenerateKeyPair(ek, dk);

// Encapsulation
byte[] ct  = new byte[MLKem768.CiphertextSizeBytesConst];        // 1088
byte[] ss1 = new byte[MLKem768.SharedSecretSizeBytesConst];      // 32
kem.Encapsulate(ek, ct, ss1);

// Decapsulation
byte[] ss2 = new byte[MLKem768.SharedSecretSizeBytesConst];
kem.Decapsulate(dk, ct, ss2);

Deterministic overloads (GenerateKeyPair(seed, …), Encapsulate(ek, seed, …)) exist for test vectors and derived-key schemes; do not use fixed seeds in production.


Key Import and Export

PKCS#8, SubjectPublicKeyInfo and PEM work exactly as they do for the signature algorithms, sharing one implementation. A generated key exports the seed arm of the ML-KEM private key CHOICE, so a 64-byte seed survives a PKCS#8 round trip rather than becoming a 1.6–3.2 KB decapsulation key; a key imported from a decapsulation key exports the expanded arm. Both match .NET 10 byte for byte.

See Key Import and Export for the private-key CHOICE, the password-based encryption rules, and why the encrypted-export members take a PbeOptions rather than the in-box PbeParameters.

No member takes a password, or returns a plaintext private key, as a string — a string cannot be overwritten once created. Passwords are ReadOnlySpan<char> or ReadOnlySpan<byte>, and the plaintext-PEM export writes into a buffer you own, sized by GetPkcs8PrivateKeyPemSize(). See Erasable Memory for what changed and how to port each call.


Security Properties

FIPS 203 Input Checks

Both API levels validate externally supplied keys as required by FIPS 203 §7:

  • §7.2 encapsulation key check (modulus check) — every 12-bit coefficient of the encoded key must be < q = 3329. Rejected keys throw (ArgumentException on the low-level API, CryptographicException on MLKem import).
  • §7.3 decapsulation key check (hash check) — the stored H(ekPKE) must match a freshly computed hash of the embedded encapsulation key.

Implicit Rejection

Decapsulating a tampered or invalid ciphertext of the correct length does not throw. Per FIPS 203, the result is a pseudorandom secret K̄ = J(z ‖ c) unrelated to the sender's secret, so an attacker learns nothing from the result. Protocols detect mismatch when the subsequently derived keys fail authentication.

Constant-Time Implementation

  • The ciphertext comparison in decapsulation uses a fixed-time comparison that yields an integer mask (never a bool), and the final secret selection is branchless.
  • Reduction uses Barrett/Montgomery multiply-shift arithmetic; no secret-dependent division, branching, or table indexing.
  • Rejection sampling (SampleNTT) branches only on public XOF output, as specified.

Key Hygiene

  • Secret intermediates (PRF output, secret/error polynomials, decrypted message, re-encryption buffer) are zeroed before scope exit.
  • Fresh key pairs run a pairwise consistency test (encapsulate/decapsulate round-trip) as expected by FIPS 140-3.
  • MLKem.Dispose() zeroizes the retained seed and decapsulation key.

Validation

The implementation is validated on every target framework by three independent means (see ML-KEM Test Vectors for details):

  1. NIST ACVP known-answer tests — official vectors from the NIST ACVP-Server for key generation, encapsulation, decapsulation (including byte-exact implicit-rejection outputs), and both §7 key checks, for all three parameter sets.
  2. BouncyCastle interop — same-seed key generation produces byte-identical expanded keys; encapsulation/decapsulation round-trips in both directions; implicit-rejection outputs match byte-for-byte.
  3. .NET 10 MLKem interop (on supported OS builds) — same-seed keys match the Windows CNG implementation byte-for-byte; cross-encapsulation and implicit rejection agree in both directions.

Comparison with .NET Built-in

Feature CryptoHives MLKem System.Security.Cryptography.MLKem
Availability All TFMs (.NET Framework 4.6.2+) .NET 10+ only
OS requirement None (fully managed) Windows CNG (recent builds) / OpenSSL 3.5+
IsSupported Always true Depends on the OS
Cross-platform consistency Guaranteed Depends on OS support
Parameter sets ML-KEM-512/768/1024 ML-KEM-512/768/1024
Type / member names Identical (source-level drop-in) —
Raw key + seed import/export ✅ ✅
byte[] and Span<byte> overloads ✅ ✅
§7.2 / §7.3 import checks ✅ ✅
MLKemAlgorithm value equality ✅ ✅
PKCS#8 / SPKI / PEM ✅ ✅
Extensible base class (MLKemCng, MLKemOpenSsl) ❌ sealed, single managed implementation ✅ abstract

KEM Roadmap

Algorithm Standard Status
ML-KEM-512/768/1024 FIPS 203 ✅ Implemented
ML-DSA-44/65/87 (signatures) FIPS 204 ✅ Implemented
SLH-DSA, all 12 parameter sets (signatures) FIPS 205 ✅ Implemented
HPKE RFC 9180 🔲 Under review
X-Wing (hybrid X25519 + ML-KEM-768) draft-connolly-cfrg-xwing-kem 🔲 Under review
PKCS#8 / SPKI / PEM key formats RFC 5958 / RFC 5280 / RFC 7468 ✅ Implemented

See Also


© 2026 The Keepers of the CryptoHives