Cryptography is changing as researchers develop algorithms designed to withstand attacks from future quantum computers. Many applications still depend on public-key cryptography to establish secure connections, exchange encryption keys, and protect sensitive information. Although today's widely used algorithms remain important, developers need to understand how post-quantum cryptography works and how to test its implementation.
.NET provides cryptographic APIs for the Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM), which is standardized in FIPS 203. These APIs allow applications to establish a shared secret using a public-key-based process designed to resist known quantum attacks against conventional public-key algorithms.
Testing these APIs requires more than checking whether a method returns successfully. Developers must verify that both parties derive the same shared secret, public keys can be transferred and imported correctly, malformed inputs are rejected appropriately, and the application behaves consistently across supported environments.
.NET 11 also introduces experimental APIs for composite ML-KEM and Hybrid Public Key Encryption (HPKE). These features require additional care because experimental APIs can change, and support for a particular algorithm or platform cannot be assumed.
This article explains how to test ML-KEM in C#, write automated tests for key encapsulation and decapsulation, check invalid inputs, and prepare cryptographic code for cross-platform testing.
Understand ML-KEM Before Writing Tests
ML-KEM is a key-encapsulation mechanism, or KEM. It allows two parties to establish a shared secret without sending that secret directly across the network.
Although developers sometimes describe this process as a key exchange, ML-KEM is technically different from a traditional Diffie-Hellman key exchange. Understanding the distinction helps when designing tests and interpreting API behavior.
A typical ML-KEM operation involves two parties:
The receiving party generates an ML-KEM key pair and shares its public encapsulation key.
The sending party imports that public key and uses it to generate a ciphertext and a shared secret.
The sending party transmits the ciphertext to the receiver.
The receiving party uses its private decapsulation key and the ciphertext to derive the corresponding shared secret.
Both parties use the resulting secret as input to a key-derivation process before using it for symmetric encryption.
The ciphertext does not contain the shared secret in a form that an observer can simply read. The receiver uses its private key to perform decapsulation and obtain the corresponding secret.
The central correctness test is straightforward: the sender's secret and the receiver's secret must match for a valid encapsulation and decapsulation operation.
However, ML-KEM does not encrypt an entire application message by itself. Applications normally derive an appropriate encryption key from the shared secret and use an authenticated encryption algorithm, such as AES-GCM, to protect the actual data.
Understand the Cryptographic APIs in .NET 11
The System.Security.Cryptography namespace provides the MLKem abstraction and related types for working with ML-KEM keys.
The main APIs used in a typical test are:
API | Purpose |
|---|
MLKem.GenerateKey | Generates an ML-KEM key pair |
MLKemAlgorithm.MLKem768 | Selects the ML-KEM-768 parameter set |
ExportEncapsulationKey | Exports the public key that another party can use |
ImportEncapsulationKey | Imports the recipient's public key |
Encapsulate | Produces a ciphertext and the sender's shared secret |
Decapsulate | Uses the private key and ciphertext to derive the receiver's shared secret |
CryptographicOperations.FixedTimeEquals | Compares secret values using a fixed-time comparison method |
ML-KEM supports parameter sets such as ML-KEM-512, ML-KEM-768, and ML-KEM-1024. These parameter sets have different security and performance characteristics. Both parties must use compatible algorithms and key formats.
The examples in this article use ML-KEM-768. This is an illustrative choice for testing, not a recommendation that every application must use this parameter set.
The .NET cryptographic implementation also depends on the platform and its underlying cryptographic support. Before running tests, verify that the selected algorithm is available in the actual runtime environment.
Set Up a .NET Test Project
Start by creating a test project targeting .NET 11. You can use xUnit, NUnit, or MSTest; the examples below use xUnit.
For a project created with the .NET SDK, the following command creates a basic xUnit test project:
dotnet new xunit -n PostQuantumCrypto.Tests
Update the project's target framework to the appropriate .NET 11 target framework, and use a compatible .NET 11 SDK and runtime.
If the cryptographic APIs are supplied through a prerelease or package-specific distribution in your environment, install the compatible package version documented for that build. Avoid mixing unrelated package versions or assuming that an API available in one preview is identical in another.
Add the necessary namespace to the test file:
using System.Security.Cryptography;
using Xunit;
The first test should verify whether the environment can perform the selected algorithm. Do not treat a missing platform implementation as a cryptographic failure.
Instead, report the test as unsupported or skip it through the test framework's supported mechanism. This distinction is important in CI environments where the same test suite may run on Windows and Linux agents with different cryptographic capabilities.
Test Key Generation and Shared Secret Agreement
The first test verifies the most important property of a KEM implementation: a valid encapsulation and decapsulation operation should produce matching shared secrets.
The following example uses the ML-KEM APIs to generate a recipient key, export its public key, import that key for the sender, and compare the resulting secrets.
using System.Security.Cryptography;
using Xunit;
public class MlKemTests
{
[Fact]
public void EncapsulationAndDecapsulation_ProduceSameSecret()
{
MLKemAlgorithm algorithm = MLKemAlgorithm.MLKem768;
if (!MLKem.IsSupported)
{
return;
}
using MLKem recipient = MLKem.GenerateKey(algorithm);
byte[] publicKey = recipient.ExportEncapsulationKey();
using MLKem sender =
MLKem.ImportEncapsulationKey(algorithm, publicKey);
byte[] ciphertext =
new byte[algorithm.CiphertextSizeInBytes];
byte[] senderSecret =
new byte[algorithm.SharedSecretSizeInBytes];
byte[] recipientSecret =
new byte[algorithm.SharedSecretSizeInBytes];
sender.Encapsulate(ciphertext, senderSecret);
recipient.Decapsulate(ciphertext, recipientSecret);
Assert.True(
CryptographicOperations.FixedTimeEquals(
senderSecret,
recipientSecret));
}
}
The example illustrates the core agreement test. The exact support-check mechanism and test-framework handling should be adjusted to the target .NET 11 build. In particular, unsupported environments should be reported as skipped rather than silently passing; the early return shown here is suitable only as a simplified demonstration.
What happens in this test?
The recipient first generates a key pair. The public encapsulation key is exported and imported by the sender, which simulates the public-key exchange that would happen between two applications.
The sender then calls Encapsulate. This operation writes the ciphertext and the sender's shared secret into the supplied buffers.
The recipient passes the ciphertext to Decapsulate. The recipient's private key is used to derive the corresponding secret.
Finally, the test compares the two secret values. A normal array comparison is not ideal for secret values in security-sensitive code because it may not provide fixed-time comparison behavior. CryptographicOperations.FixedTimeEquals is designed for this type of comparison.
If the test fails, investigate the selected algorithm, key import, ciphertext handling, and the implementation used by the environment before drawing conclusions about the cryptographic primitive.
Test Public-Key Export and Import
In real applications, the sender and receiver usually operate in separate processes or on different machines. The receiver's public key must therefore be serialized, transmitted, and imported by the sender.
A test that generates and imports a key in memory is a useful starting point, but it does not cover the complete key-transfer path.
Add tests that export the public key, preserve the bytes, and import it into a separate ML-KEM object. The imported key should be able to perform encapsulation successfully.
For example:
[Fact]
public void ExportedPublicKey_CanBeImported()
{
MLKemAlgorithm algorithm = MLKemAlgorithm.MLKem768;
using MLKem recipient = MLKem.GenerateKey(algorithm);
byte[] exportedKey = recipient.ExportEncapsulationKey();
using MLKem importedKey =
MLKem.ImportEncapsulationKey(algorithm, exportedKey);
byte[] ciphertext =
new byte[algorithm.CiphertextSizeInBytes];
byte[] sharedSecret =
new byte[algorithm.SharedSecretSizeInBytes];
importedKey.Encapsulate(ciphertext, sharedSecret);
Assert.Equal(
algorithm.CiphertextSizeInBytes,
ciphertext.Length);
Assert.Equal(
algorithm.SharedSecretSizeInBytes,
sharedSecret.Length);
}
This test checks that the exported key can be imported and used for encapsulation. It also verifies that the application has allocated buffers of the expected sizes.
For production systems, test the actual serialization format used by your application. If the key is wrapped in a certificate structure, protocol message, or another container, test that complete path rather than testing only the raw key bytes.
Never export or log private keys simply to make public-key interoperability tests easier. The public encapsulation key can be shared, but the private decapsulation key must remain protected.
Test Invalid and Modified Ciphertext
Applications must handle malformed cryptographic input safely. Network messages can be truncated, corrupted, or deliberately modified by an attacker.
A useful test is to pass a ciphertext with an invalid length and verify that the API rejects it appropriately.
[Fact]
public void Decapsulation_RejectsInvalidCiphertextLength()
{
MLKemAlgorithm algorithm = MLKemAlgorithm.MLKem768;
using MLKem recipient = MLKem.GenerateKey(algorithm);
byte[] invalidCiphertext = new byte[1];
Assert.Throws<CryptographicException>(() =>
recipient.Decapsulate(invalidCiphertext));
}
This test checks a clearly invalid ciphertext length. It should be run against the specific implementation and runtime being supported.
A different case occurs when the ciphertext has the correct length but some bytes have been modified. ML-KEM implementations can use implicit rejection behavior for malformed ciphertexts, so decapsulation may produce a different secret instead of throwing an exception.
For that reason, do not write a test that assumes every corrupted ciphertext must cause a CryptographicException. Instead, test the behavior specified by the implementation and standard, and verify that the resulting secret does not match the original secret in your controlled test case.
Also test application-level protocol behavior. If an incoming message fails validation or cannot establish the expected session key, the application should not continue as if a valid secure session had been created.
Test Platform Support and Interoperability
Cryptographic APIs may behave differently across supported operating systems because they depend on platform-specific implementations and libraries.
A test that passes on a developer's Windows machine may fail on a Linux build agent if the installed OpenSSL version does not support the required algorithm.
Create a test matrix that includes the operating systems and runtime environments your application intends to support.
Test environment | What to verify |
|---|
Windows | Runtime support, algorithm availability, and key import/export |
Linux | Required cryptographic library version and algorithm support |
macOS | Availability of the selected implementation and correct key handling |
CI build agent | Runtime version, native dependencies, and consistent test reporting |
Container image | Installed native libraries and the actual runtime used by the application |
Do not assume that all platforms support every ML-KEM parameter set. Use the documented support information for the specific runtime and operating system.
Interoperability testing should also involve the other implementation used by your application. If one service uses .NET and another uses a different cryptographic library, verify that both agree on the algorithm, parameter set, public-key encoding, ciphertext format, and shared-secret handling.
A successful test between two instances of the same implementation does not prove that two different implementations will interoperate correctly.
Understand the Experimental APIs in .NET 11
.NET 11 introduces experimental APIs for composite ML-KEM and HPKE. These features need to be treated separately from the basic ML-KEM tests shown earlier.
Composite ML-KEM combines post-quantum and classical key-encapsulation mechanisms so that both contribute to the resulting shared secret. This approach can be useful when an application is evaluating a hybrid transition strategy.
The composite API is experimental in .NET 11, so its surface and behavior may change. Applications that test it should explicitly account for the experimental diagnostics and verify support for the selected algorithm combination.
HPKE provides a framework for hybrid public-key encryption, combining a key-encapsulation mechanism with key derivation and authenticated encryption. However, the experimental .NET 11 HPKE implementation has limitations, including the lack of support for ML-KEM and hybrid ML-KEM suites in the current implementation.
Do not assume that the presence of an HPKE API means that every post-quantum suite is supported. Test the exact combination required by your protocol and consult the documentation for the precise runtime build.
Common Mistakes When Testing Post-Quantum Cryptography
Testing only successful key agreement
A successful shared-secret comparison proves an important property, but it does not cover malformed inputs, serialization errors, unsupported platforms, or protocol-level mistakes. Include negative tests and interoperability tests.
Comparing secret values with ordinary equality
Secret material should be compared with an appropriate cryptographic comparison method. Use CryptographicOperations.FixedTimeEquals rather than relying on a regular byte-array comparison in security-sensitive code.
Assuming every platform supports ML-KEM
Support depends on the runtime, operating system, and underlying cryptographic implementation. Check support before running the test and report unsupported configurations accurately.
Treating ML-KEM as a complete encryption system
ML-KEM establishes a shared secret. Applications still need to derive appropriate keys and use a suitable authenticated encryption mechanism to protect messages.
Ignoring experimental API warnings
Experimental APIs may change between releases. Keep their use isolated, document the dependency, and retest when upgrading the runtime or package.
Best Practices for Production Testing
Keep cryptographic tests deterministic where possible without weakening the production algorithm. For key agreement tests, generate real test keys and verify the resulting secret agreement. Avoid hard-coding production secrets or writing tests that depend on a particular generated ciphertext.
Test invalid lengths, corrupted input, public-key serialization, and unsupported environments. Run the suite on every operating system that the application officially supports. When integrating with another cryptographic implementation, test the complete wire format and key lifecycle.
Keep private keys and shared secrets out of logs, test reports, and failure messages. Use disposable key objects and avoid retaining secret buffers longer than necessary. For real applications, rely on established cryptographic protocols and reviewed libraries rather than designing a new key-exchange protocol around low-level primitives.
Summary
Testing post-quantum cryptographic APIs in .NET 11 begins with a clear understanding of ML-KEM. The receiver generates a key pair, the sender uses the public encapsulation key to create a ciphertext and shared secret, and the receiver decapsulates the ciphertext to derive the matching secret.
Automated tests should verify successful secret agreement, public-key import and export, invalid ciphertext handling, platform support, and interoperability with other implementations. Applications should also distinguish the core ML-KEM primitive from the symmetric encryption and protocol logic that use its output.
.NET 11's experimental composite ML-KEM and HPKE APIs require additional compatibility checks. Their presence does not guarantee that every hybrid algorithm suite is supported.
The goal of cryptographic testing is not merely to confirm that an API call succeeds. It is to verify that the implementation, application protocol, and deployment environment work together correctly while protecting sensitive key material.
Join the conversation! Your thoughts help the community grow.