Over recent years I have been involved in the post-quantum cryptography community, especially from the .NET angle - trying to streamline integration of PQC into .NET space and raise the awareness of developers via various projects, samples and articles.
In this spirit, I would like to announce today a library called (Maybe) LibOQS.NET, which is a thin wrapper around liboqs, a C library providing implementations for all post-quantum cryptography algorithms. This includes both the standardized ones, like ML-KEM, ML-DSA and SLH-DSA, as well as the ones currently undergoing standarization and under active research and development.
What’s there for .NET developers π
If you are a .NET developer trying to use post-quantum cryptography, you had two options so far. The first one, which has been out there for a long time, was to use BouncyCastle.Cryptography, which supplies managed implementation of all post-quantum cryptography algoirthms. I blogged about this in one of my earlier posts.
The other possibility was to use the new System.Security.Cryptography APIs introduced in .NET 10 Preview 6, which provide native support for ML-KEM and ML-DSA algorithms on Windows, as I described in my recent post. This approach, however, only worked on a Canary Channel Windows build 27852 or higher, so should be considered experimental and not suitable for production use. The new .NET PQC APIs also work on Linux now, but only if your system OpenSSL is of version 3.5 or higher. That OpenSSL was released in April 2025 and provides implementions of all three standardized algorithms (ML-KEM, ML-DSA and SLH-DSA). That version of OpenSSL is, however, not yet widely adopted.
(Maybe) LibOQS.NET π
The Open Quantum Safe (OQS) project is an open-source project that aims to support the transition to quantum-resistant cryptography. OQS is part of the Linux Foundationβs Post-Quantum Cryptography Alliance. The project delivers two main artifacts:
- liboqs, a C library with PQC algorithm implementations, which can be used directly or via wrappers in other languages and just about any platform
- oqs-provider, an OpenSSL provider that integrates liboqs algorithms into OpenSSL, making them available to applications that use OpenSSL for cryptographic operations. This leverages the OpenSSL 3.0 provider architecture, and can be used to add PQC to OpenSSL 3.0 (without needing to move to OpenSSL 3.5). It can also be used to replace the default OpenSSL 3.5 implementations of PQC with those from liboqs, if desired.
The project also used to provide its own .NET wrapper called liboqs-dotnet, but it has been archived and is no longer maintained. The new (Maybe) LibOQS.NET library is a fresh take on this idea, providing a thin, modern and performant wrapper around liboqs version 0.13.0. The unusual (silly?) naming stems from the fact that I would like to emphasize that this is a project independently developed and maintained, and is not an official OQS project. The library is available as a NuGet package and the source code is available on GitHub.
It is still just a thin wrapper around liboqs, and the philosophy is to provide as little abstraction as possible, so that the users can easily access all the features of liboqs, while having the strongly typed, safe and productive .NET experience - it handles all the native interop and memory management. The library is fully cross-platform and works on Windows x64, Windows arm64, Linux x64, Linux arm64 and macOS arm64 (the native liboqs is bundled for each of those system variants). The code is very performant and all typical flows can be done without any allocations. It supports .NET 9.0 and higher.
Sample usage π
In the previous posts about BouncyCastle and System.Security.Cryptography PQC APIs, I used a demo console application performing both the ML-KEM key exchange and ML-DSA signing and verification. The same demo application has been extended to use (Maybe) LibOQS.NET.
Assuming we have the following helper (used throughout the older posts too) for Spectre.Console to print the output nicely:
static void PrintPanel(string header, string[] data)
{
var content = string.Join(Environment.NewLine, data);
var panel = new Panel(content)
{
Header = new PanelHeader(header)
};
AnsiConsole.Write(panel);
}
This is the ML-KEM flow:
Console.WriteLine("***************** ML-KEM *******************");
using var kem = new KemInstance(KemAlgorithm.MlKem768);
// generate key pair for Alice
var (alicePublic, alicePrivate) = kem.GenerateKeypair();
PrintPanel("Alice's keys", [$":unlocked: Public: {alicePublic.PrettyPrint()}", $":locked: Private: {alicePrivate.PrettyPrint()}"]);
// Bob encapsulates a new shared secret using Alice's public key
var (cipherText, bobSecret) = kem.Encapsulate(alicePublic);
// Alice decapsulates a new shared secret using Alice's private key
byte[] aliceSecret = kem.Decapsulate(alicePrivate, cipherText);
PrintPanel("Key encapsulation", [$":man: Bob's secret: {bobSecret.PrettyPrint()}", $":locked_with_key: Cipher text (Bob -> Alice): {cipherTextPrettyPrint()}", $":woman: Alice's secret: {aliceSecret.PrettyPrint()}"]);
// Compare secrets
var equal = bobSecret.SequenceEqual(aliceSecret);
PrintPanel("Verification", [$"{(equal ? ":check_mark_button:" : ":cross_mark:")} Secrets equal!"]);
And then the ML-DSA flow:
Console.WriteLine("***************** ML-DSA *******************");
var raw = "Hello, ML-DSA!";
Console.WriteLine($"Raw Message: {raw}");
var data = Encoding.ASCII.GetBytes(raw);
PrintPanel("Message", [$"Raw: {raw}", $"Encoded: {data.PrettyPrint()}"]);
using var sig = new SigInstance(SigAlgorithm.MlDsa65);
// Generate key pair
var (publicKey, secretKey) = sig.GenerateKeypair();
PrintPanel("Keys", [$":unlocked: Public: {publicKey.PrettyPrint()}", $":locked: Private: {secretKey.PrettyPrint()}"]);
// Sign
var signature = sig.Sign(data, secretKey);
PrintPanel("Signature", [$":pen: {signature.PrettyPrint()}"]);
// Verify signature
var verified = sig.Verify(data, signature, publicKey);
PrintPanel("Verification", [$"{(verified ? ":check_mark_button:" : ":cross_mark:")} Verified!"]);
// Demonstrate key re-use - sign again with same key
var signature2 = sig.Sign(data, secretKey);
PrintPanel("Signature (from recovered key)", [$":pen: {signature2.PrettyPrint()}"]);
// Demonstrate verification with public-key-only scenario
// In real world, verifier would only have the public key, not the full key pair
using var verifierSig = new SigInstance(SigAlgorithm.MlDsa65);
var verifiedWithPublicKeyOnly = verifierSig.Verify(data, signature2, publicKey);
PrintPanel("Reverification", [$"{(verifiedWithPublicKeyOnly ? ":check_mark_button:" : ":cross_mark:")} Verified!"]);
And that’s it! It is as simple as it gets, and performance is great. You can find the full source code for this post on GitHub. In that repo, you can also find the sources for the previous posts about BouncyCastle and System.Security.Cryptography PQC APIs, so you can compare the three approaches side-by-side.
We can close off with the simple comparison of all the options that are currently out there for .NET developers wanting to use post-quantum cryptography today.
System.Security.Cryptography
- Type: .NET System API
- Windows: Win Canary 27852+
- Linux: OpenSSL 3.5+
- Mac: β
- WASM & others: β
- Platform: .NET 10+ (partly experimental)
- Key Advantage: Official
BouncyCastle
- Type: Fully Managed
- Windows: β
- Linux: β
- Mac: β
- WASM & others: β
- Platform: .NET Standard 2.0
- Key Advantage: Maximum Portability
(Maybe) LibOQS.NET
- Type: Native Wrapper
- Windows: β
- Linux: β
- Mac: β
- WASM & others: β
- Platform: .NET 9
- Key Advantage: Uses liboqs