Skip to content

C# Wrapper: ML-KEM and ML-DSA (Dilithium) Support#10191

Open
dgarske wants to merge 3 commits intowolfSSL:masterfrom
dgarske:csharp_pqc
Open

C# Wrapper: ML-KEM and ML-DSA (Dilithium) Support#10191
dgarske wants to merge 3 commits intowolfSSL:masterfrom
dgarske:csharp_pqc

Conversation

@dgarske
Copy link
Copy Markdown
Contributor

@dgarske dgarske commented Apr 10, 2026

Description

Adds post-quantum ML-KEM and ML-DSA support to the wolfSSL C# wrapper, with
build/test instructions, peer review fixes, and a switch to FIPS 204 compliant
Dilithium APIs.

Changes

  • ML-KEM wrapper functions: MlKemMakeKey, MlKemEncode/DecodePublicKey,
    MlKemEncode/DecodePrivateKey, MlKemEncapsulate, MlKemDecapsulate,
    MlKemFreeKey (supports ML_KEM_512, ML_KEM_768, ML_KEM_1024).
  • ML-DSA (Dilithium) wrapper functions: DilithiumMakeKey,
    DilithiumExport/Import for public/private keys, DilithiumSignMsg,
    DilithiumVerifyMsg, DilithiumFreeKey (supports ML_DSA_44/65/87).
  • Sign/verify use the FIPS 204 wc_dilithium_sign_ctx_msg /
    wc_dilithium_verify_ctx_msg APIs (with ctx=NULL/ctxLen=0) so the wrapper
    works without WOLFSSL_DILITHIUM_NO_CTX.
  • user_settings.h: enables HAVE_MLKEM, WOLFSSL_WC_MLKEM, HAVE_DILITHIUM,
    WOLFSSL_WC_DILITHIUM, WOLFSSL_SHAKE128/256, plus WOLFSSL_DTLS_CH_FRAG
    (required for PQC + DTLS 1.3).
  • NamedGroup enum extended with WOLFSSL_ML_KEM_512/768/1024 and the
    hybrid groups (SECP256R1MLKEM768, X25519MLKEM768, SECP384R1MLKEM1024).
  • New mlkem_test and mldsa_test cases in wolfCrypt-Test.cs covering
    keygen, encode/decode (or export/import), and full encap/decap or
    sign/verify roundtrips for every parameter set.
  • README.md: documents a sudo-free build with
    --prefix=$(pwd)/install and running the test via
    LD_LIBRARY_PATH=./install/lib mono wrapper/CSharp/wolfcrypttest.exe so
    mono picks up the freshly-built library instead of any stale system one.
  • Peer review fixes: stricter return-code checks on wc_MlKemKey_*Size /
    wc_MlDsaKey_Get*Len callers, MEMORY_E (not EXCEPTION_E) on RNG alloc
    failure in DilithiumSignMsg, dead null re-checks removed from the Free
    helpers, indentation cleanup, consistent wolfcrypt. prefix in
    mldsa_test, and removal of the C-only WOLF_ENUM_DUMMY_LAST_ELEMENT
    from NamedGroup.

Testing

  • ./autogen.sh && cp wrapper/CSharp/user_settings.h . && ./configure --enable-usersettings --prefix=$(pwd)/install && make && make install
  • cd wrapper/CSharp && mcs wolfCrypt-Test/wolfCrypt-Test.cs wolfSSL_CSharp/{wolfCrypt,wolfSSL,X509}.cs -OUT:wolfcrypttest.exe
  • LD_LIBRARY_PATH=./install/lib mono wrapper/CSharp/wolfcrypttest.exe — all
    tests pass, including ML-KEM 512/768/1024 and ML-DSA 44/65/87
    (signature lengths 2420/3309/4627).

Checklist

  • added tests
  • updated/added doxygen
  • updated appropriate READMEs
  • Updated manual and documentation

@dgarske dgarske self-assigned this Apr 10, 2026
Copilot AI review requested due to automatic review settings April 10, 2026 19:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds post-quantum ML-KEM (KEM) and ML-DSA/Dilithium (signature) support to the wolfSSL C# wrapper, including test coverage and updated build/run instructions.

Changes:

  • Added C# P/Invoke + managed wrapper APIs for ML-KEM (keygen, encode/decode, encaps/decaps, free) and ML-DSA/Dilithium (keygen, export/import, sign/verify, free).
  • Extended the wolfSSL C# wrapper with TLS 1.3 UseKeyShare support and added a NamedGroup enum covering PQC/hybrid groups.
  • Added new C# wrapper tests for ML-KEM and ML-DSA, plus updated README/user settings for local-prefix builds and running under mono.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
wrapper/CSharp/wolfSSL_CSharp/wolfSSL.cs Adds NamedGroup and UseKeyShare() wrapper for TLS 1.3 key shares (incl. PQC/hybrid IDs).
wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs Adds ML-KEM and ML-DSA/Dilithium wolfCrypt wrappers and related constants/enums.
wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs Adds ML-KEM and ML-DSA roundtrip tests (keygen + encode/decode or export/import + encaps/decaps or sign/verify).
wrapper/CSharp/user_settings.h Enables ML-KEM/ML-DSA and required primitives/options in the wrapper build.
wrapper/CSharp/README.md Updates build/install steps to use a local prefix and run tests with LD_LIBRARY_PATH.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2867 to +2875
/// <summary>
/// Create a new ML-KEM key pair and initialize it with random values
/// </summary>
/// <param name="type">ML-KEM parameter set type</param>
/// <param name="heap">Heap pointer for memory allocation</param>
/// <param name="devId">Device ID (if applicable)</param>
/// <returns>Pointer to the MlKem key structure, or IntPtr.Zero on failure</returns>
public static IntPtr MlKemMakeKey(MlKemTypes type, IntPtr heap, int devId)
{
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ML-KEM decode APIs require a pre-initialized key with the correct type set (wc_MlKemKey_*Size uses key->type). In the C# wrapper, the only public constructor is MlKemMakeKey(), which also performs keygen (extra RNG + compute) even when the caller only wants to decode/import an existing key. Consider adding a dedicated allocator/initializer (e.g., MlKemNew/MlKemInitKey that wraps wc_MlKemKey_New or wc_MlKemKey_Init without calling wc_MlKemKey_MakeKey) and updating callers/tests to use it for decode paths.

Copilot uses AI. Check for mistakes.
Comment on lines +3291 to +3318
/// <summary>
/// Create a new Dilithium key pair and initialize it with random values
/// </summary>
/// <param name="heap">Heap pointer for memory allocation</param>
/// <param name="devId">Device ID (if applicable)</param>
/// <param name="level">Dilithium security level</param>
/// <returns>Pointer to the Dilithium key structure, or IntPtr.Zero on failure</returns>
public static IntPtr DilithiumMakeKey(IntPtr heap, int devId, MlDsaLevels level)
{
IntPtr key = IntPtr.Zero;
IntPtr rng = IntPtr.Zero;
int ret;
bool success = false;

try
{
key = wc_dilithium_new(heap, devId);
if (key == IntPtr.Zero)
{
log(ERROR_LOG, "Failed to allocate and initialize Dilithium key.");
return IntPtr.Zero;
}

ret = wc_dilithium_set_level(key, (byte)level);
if (ret != 0)
{
log(ERROR_LOG, "Failed to set Dilithium level. Error code: " + ret);
return IntPtr.Zero;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dilithium import helpers (DilithiumImportPublicKey/DilithiumImportPrivateKey) require an initialized key with level set (C API returns BAD_FUNC_ARG if key->level is not one of WC_ML_DSA_44/65/87). Currently the wrapper only exposes DilithiumMakeKey(), which always generates a new keypair before an import can occur. Consider adding a lightweight constructor (e.g., DilithiumNew + DilithiumSetLevel) so callers can import keys without unnecessary keygen.

Copilot uses AI. Check for mistakes.
Comment on lines +448 to +463
private static extern int wc_MlKemKey_Init(IntPtr key, int type, IntPtr heap, int devId);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_Free(IntPtr key);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_MakeKey(IntPtr key, IntPtr rng);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_MakeKeyWithRandom(IntPtr key, byte[] rand, int len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_EncodePublicKey(IntPtr key, byte[] output, uint len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_DecodePublicKey(IntPtr key, byte[] input, uint len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_Encapsulate(IntPtr key, byte[] ct, byte[] ss, IntPtr rng);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_EncapsulateWithRandom(IntPtr key, byte[] ct, byte[] ss, byte[] rand, int len);
[DllImport(wolfssl_dll)]
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ML-KEM P/Invoke declarations are currently unused in this wrapper (wc_MlKemKey_Init, wc_MlKemKey_Free, wc_MlKemKey_MakeKeyWithRandom, wc_MlKemKey_EncapsulateWithRandom). If they aren’t needed for the public surface area, consider removing them to reduce maintenance and keep the interop surface minimal; otherwise, add the corresponding managed wrappers that use them.

Suggested change
private static extern int wc_MlKemKey_Init(IntPtr key, int type, IntPtr heap, int devId);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_Free(IntPtr key);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_MakeKey(IntPtr key, IntPtr rng);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_MakeKeyWithRandom(IntPtr key, byte[] rand, int len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_EncodePublicKey(IntPtr key, byte[] output, uint len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_DecodePublicKey(IntPtr key, byte[] input, uint len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_Encapsulate(IntPtr key, byte[] ct, byte[] ss, IntPtr rng);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_EncapsulateWithRandom(IntPtr key, byte[] ct, byte[] ss, byte[] rand, int len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_MakeKey(IntPtr key, IntPtr rng);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_EncodePublicKey(IntPtr key, byte[] output, uint len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_DecodePublicKey(IntPtr key, byte[] input, uint len);
[DllImport(wolfssl_dll)]
private static extern int wc_MlKemKey_Encapsulate(IntPtr key, byte[] ct, byte[] ss, IntPtr rng);
[DllImport(wolfssl_dll)]

Copilot uses AI. Check for mistakes.
Comment on lines +515 to +520
private static extern int wc_dilithium_init_ex(IntPtr key, IntPtr heap, int devId);
[DllImport(wolfssl_dll)]
private static extern int wc_dilithium_set_level(IntPtr key, byte level);
[DllImport(wolfssl_dll)]
private static extern void wc_dilithium_free(IntPtr key);
[DllImport(wolfssl_dll)]
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Dilithium P/Invoke declarations (wc_dilithium_init_ex, wc_dilithium_free) are not referenced anywhere in the managed wrapper. Consider removing unused interop entries to reduce surface area, or expose managed helpers that use them (e.g., a constructor/init path that doesn’t perform keygen).

Suggested change
private static extern int wc_dilithium_init_ex(IntPtr key, IntPtr heap, int devId);
[DllImport(wolfssl_dll)]
private static extern int wc_dilithium_set_level(IntPtr key, byte level);
[DllImport(wolfssl_dll)]
private static extern void wc_dilithium_free(IntPtr key);
[DllImport(wolfssl_dll)]
private static extern int wc_dilithium_set_level(IntPtr key, byte level);
[DllImport(wolfssl_dll)]

Copilot uses AI. Check for mistakes.
ret = wolfcrypt.MlKemDecodePublicKey(keyB, pubA);
if (ret != 0)
{
Console.Error.WriteLine($"Failed to decode public key of B. Error code: {ret}");
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message here says "Failed to decode public key of B" but the code is decoding pubA into keyB. This is confusing when diagnosing failures; update the message to reflect that pubA (A’s public key) is being decoded.

Suggested change
Console.Error.WriteLine($"Failed to decode public key of B. Error code: {ret}");
Console.Error.WriteLine($"Failed to decode public key of A. Error code: {ret}");

Copilot uses AI. Check for mistakes.
Comment on lines +706 to +711
Console.WriteLine("Generate Key Pair B...");
keyB = wolfcrypt.MlKemMakeKey(type, heap, devId);
if (keyB == IntPtr.Zero)
{
ret = -1;
Console.Error.WriteLine("Failed to generate key pair B.");
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, keyB is generated with MlKemMakeKey() but then immediately overwritten by decoding keyA’s private/public key into it. This makes the intent of “Generate Key Pair B” misleading and adds extra RNG/keygen work. If you add a managed initializer for ML-KEM (e.g., MlKemNew/MlKemInitKey), use that for keyB in the decode/decap path instead of generating a fresh keypair first.

Suggested change
Console.WriteLine("Generate Key Pair B...");
keyB = wolfcrypt.MlKemMakeKey(type, heap, devId);
if (keyB == IntPtr.Zero)
{
ret = -1;
Console.Error.WriteLine("Failed to generate key pair B.");
Console.WriteLine("Initialize Key B for decode...");
keyB = wolfcrypt.MlKemInitKey(type, heap, devId);
if (keyB == IntPtr.Zero)
{
ret = -1;
Console.Error.WriteLine("Failed to initialize key B for decode.");

Copilot uses AI. Check for mistakes.
Comment on lines +770 to +772
/* Taken from draft-connolly-tls-mlkem-key-agreement, see:
* https://github.com/dconnolly/draft-connolly-tls-mlkem-key-agreement/
*/
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reference comment for the ML-KEM group IDs points to draft-connolly-tls-mlkem-key-agreement, but wolfSSL’s ssl.h currently documents these IDs as coming from draft-ietf-tls-mlkem. Updating the comment would avoid confusion when cross-referencing with upstream.

Suggested change
/* Taken from draft-connolly-tls-mlkem-key-agreement, see:
* https://github.com/dconnolly/draft-connolly-tls-mlkem-key-agreement/
*/
/* Taken from draft-ietf-tls-mlkem. */

Copilot uses AI. Check for mistakes.
Comment on lines +777 to +783
/* Taken from draft-kwiatkowski-tls-ecdhe-mlkem. see:
* https://github.com/post-quantum-cryptography/
* draft-kwiatkowski-tls-ecdhe-mlkem/
*/
WOLFSSL_SECP256R1MLKEM768 = 4587,
WOLFSSL_X25519MLKEM768 = 4588,
WOLFSSL_SECP384R1MLKEM1024 = 4589,
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reference comment for the hybrid ECDHE+ML-KEM group IDs points to draft-kwiatkowski-tls-ecdhe-mlkem, but wolfSSL’s ssl.h documents these IDs as from draft-ietf-tls-ecdhe-mlkem. Consider aligning the comment with the upstream reference to reduce ambiguity.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants