Skip to content
ecdsa.com

ML-KEM-768 encapsulation · .NET

How to encapsulate and decapsulate a shared secret with ML-KEM-768 in C# (.NET)

To encapsulate and decapsulate a shared secret with ML-KEM-768 in C#, use the MLKem class of .NET 10: the sender imports the encapsulation key and calls Encapsulate(out ciphertext, out sharedSecret), the receiver calls Decapsulate(ciphertext). On Linux it needs OpenSSL 3.5 underneath, which is why this recipe runs in the Alpine 3.24 SDK image; it also decapsulates a ciphertext OpenSSL made.

Tested with .NET 10.0.13 (SDK 10.0.402) (Alpine 3.24, OpenSSL 3.5.9 backend).

kem.cs — key pair, encapsulation, decapsulation
using System.Security.Cryptography;
// .NET 10: on Linux this needs OpenSSL 3.5 or later underneath
if (!MLKem.IsSupported) throw new PlatformNotSupportedException("ML-KEM is not available on this platform");
// Receiver: a key pair; only the encapsulation key is handed out
using MLKem receiver = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
byte[] encapsulationKey = receiver.ExportEncapsulationKey();
// Sender: import the encapsulation key, get a fresh secret and its ciphertext
using MLKem sender = MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, encapsulationKey);
sender.Encapsulate(out byte[] ciphertext, out byte[] sharedSecret);
// Receiver: the same secret from the ciphertext
byte[] received = receiver.Decapsulate(ciphertext);
Console.WriteLine($"encapsulation key {encapsulationKey.Length} bytes, ciphertext {ciphertext.Length} bytes, shared secret {sharedSecret.Length} bytes");
Console.WriteLine($"shared secret agreed: {CryptographicOperations.FixedTimeEquals(received, sharedSecret)}");
interop.cs — OpenSSL's encapsulation key from PEM, and a key from a 64-byte seed
using System.Security.Cryptography;
// SPKI / PKCS#8 / PEM methods are marked experimental (SYSLIB5006) in .NET 10
#pragma warning disable SYSLIB5006
using MLKem openssl = MLKem.ImportFromPem(File.ReadAllText("openssl-pub.pem"));
#pragma warning restore SYSLIB5006
// The 64-byte seed (d || z) is the whole decapsulation key
byte[] seed = Convert.FromHexString(Environment.GetEnvironmentVariable("SEED_HEX")!);
using MLKem seeded = MLKem.ImportPrivateSeed(MLKemAlgorithm.MLKem768, seed);
Console.WriteLine($"key from the shared seed equals OpenSSL's: {seeded.ExportEncapsulationKey().AsSpan().SequenceEqual(openssl.ExportEncapsulationKey())}");
byte[] ciphertext = Convert.FromBase64String(File.ReadAllText("openssl-ct.b64"));
Console.WriteLine($"secret from OpenSSL's ciphertext: {Convert.ToHexStringLower(seeded.Decapsulate(ciphertext))}");

How it works

  1. MLKem.IsSupported tells whether the platform provides ML-KEM; check it first.
  2. MLKem.GenerateKey(MLKemAlgorithm.MLKem768) creates the key; ExportEncapsulationKey() returns the 1184-byte key the sender imports with ImportEncapsulationKey.
  3. Encapsulate(out ciphertext, out sharedSecret) gives 1088 and 32 bytes; Decapsulate(ciphertext) returns the same 32 bytes — compare with CryptographicOperations.FixedTimeEquals.
  4. ImportPrivateSeed takes the 64-byte seed: the shared seed gives OpenSSL's encapsulation key (read with ImportFromPem) and decapsulates OpenSSL's ciphertext to dee35810…30f9.

Gotchas

  • On the Ubuntu 24.04 SDK image (OpenSSL 3.0.13) MLKem.IsSupported is false; the Alpine 3.24 image ships OpenSSL 3.5 and supports it.
  • ImportFromPem and the other SPKI / PKCS#8 methods are experimental in .NET 10 (SYSLIB5006); a file-based app does not build until you suppress the diagnostic, as interop.cs does.
  • A tampered ciphertext returns a different secret without an exception (implicit rejection); a ciphertext of the wrong length throws ArgumentException.

Related recipes