using System.Security.Cryptography; // .NET 10: on Linux this needs OpenSSL 3.5 or later underneathif (!MLKem.IsSupported) throw new PlatformNotSupportedException("ML-KEM is not available on this platform"); // Receiver: a key pair; only the encapsulation key is handed outusing MLKem receiver = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);byte[] encapsulationKey = receiver.ExportEncapsulationKey(); // Sender: import the encapsulation key, get a fresh secret and its ciphertextusing MLKem sender = MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, encapsulationKey);sender.Encapsulate(out byte[] ciphertext, out byte[] sharedSecret); // Receiver: the same secret from the ciphertextbyte[] 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)}"); using System.Security.Cryptography; // SPKI / PKCS#8 / PEM methods are marked experimental (SYSLIB5006) in .NET 10#pragma warning disable SYSLIB5006using MLKem openssl = MLKem.ImportFromPem(File.ReadAllText("openssl-pub.pem"));#pragma warning restore SYSLIB5006 // The 64-byte seed (d || z) is the whole decapsulation keybyte[] 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
MLKem.IsSupportedtells whether the platform provides ML-KEM; check it first.MLKem.GenerateKey(MLKemAlgorithm.MLKem768)creates the key;ExportEncapsulationKey()returns the 1184-byte key the sender imports withImportEncapsulationKey.Encapsulate(out ciphertext, out sharedSecret)gives 1088 and 32 bytes;Decapsulate(ciphertext)returns the same 32 bytes — compare withCryptographicOperations.FixedTimeEquals.ImportPrivateSeedtakes the 64-byte seed: the shared seed gives OpenSSL's encapsulation key (read withImportFromPem) and decapsulates OpenSSL's ciphertext todee35810…30f9.
Gotchas
- On the Ubuntu 24.04 SDK image (OpenSSL 3.0.13)
MLKem.IsSupportedisfalse; the Alpine 3.24 image ships OpenSSL 3.5 and supports it. ImportFromPemand the other SPKI / PKCS#8 methods are experimental in .NET 10 (SYSLIB5006); a file-based app does not build until you suppress the diagnostic, asinterop.csdoes.- A tampered ciphertext returns a different secret without an exception (implicit rejection); a ciphertext of the wrong length throws
ArgumentException.
Related recipes
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Java
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Python
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 with OpenSSL
- How to sign and verify with ML-DSA-65 in C# (.NET)
- How to derive a shared key with ECDH and HKDF in C#