C# Wrapper: ML-KEM and ML-DSA (Dilithium) Support#10191
C# Wrapper: ML-KEM and ML-DSA (Dilithium) Support#10191dgarske wants to merge 3 commits intowolfSSL:masterfrom
Conversation
There was a problem hiding this comment.
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
UseKeySharesupport and added aNamedGroupenum 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.
| /// <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) | ||
| { |
There was a problem hiding this comment.
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.
| /// <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; |
There was a problem hiding this comment.
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.
| 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)] |
There was a problem hiding this comment.
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.
| 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)] |
| 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)] |
There was a problem hiding this comment.
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).
| 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)] |
| ret = wolfcrypt.MlKemDecodePublicKey(keyB, pubA); | ||
| if (ret != 0) | ||
| { | ||
| Console.Error.WriteLine($"Failed to decode public key of B. Error code: {ret}"); |
There was a problem hiding this comment.
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.
| 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}"); |
| 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."); |
There was a problem hiding this comment.
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.
| 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."); |
| /* Taken from draft-connolly-tls-mlkem-key-agreement, see: | ||
| * https://github.com/dconnolly/draft-connolly-tls-mlkem-key-agreement/ | ||
| */ |
There was a problem hiding this comment.
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.
| /* 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. */ |
| /* 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, |
There was a problem hiding this comment.
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.
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
MlKemMakeKey,MlKemEncode/DecodePublicKey,MlKemEncode/DecodePrivateKey,MlKemEncapsulate,MlKemDecapsulate,MlKemFreeKey(supportsML_KEM_512,ML_KEM_768,ML_KEM_1024).DilithiumMakeKey,DilithiumExport/Importfor public/private keys,DilithiumSignMsg,DilithiumVerifyMsg,DilithiumFreeKey(supportsML_DSA_44/65/87).wc_dilithium_sign_ctx_msg/wc_dilithium_verify_ctx_msgAPIs (withctx=NULL/ctxLen=0) so the wrapperworks without
WOLFSSL_DILITHIUM_NO_CTX.user_settings.h: enablesHAVE_MLKEM,WOLFSSL_WC_MLKEM,HAVE_DILITHIUM,WOLFSSL_WC_DILITHIUM,WOLFSSL_SHAKE128/256, plusWOLFSSL_DTLS_CH_FRAG(required for PQC + DTLS 1.3).
NamedGroupenum extended withWOLFSSL_ML_KEM_512/768/1024and thehybrid groups (
SECP256R1MLKEM768,X25519MLKEM768,SECP384R1MLKEM1024).mlkem_testandmldsa_testcases inwolfCrypt-Test.cscoveringkeygen, 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)/installand running the test viaLD_LIBRARY_PATH=./install/lib mono wrapper/CSharp/wolfcrypttest.exesomono picks up the freshly-built library instead of any stale system one.
wc_MlKemKey_*Size/wc_MlDsaKey_Get*Lencallers,MEMORY_E(notEXCEPTION_E) on RNG allocfailure in
DilithiumSignMsg, dead null re-checks removed from the Freehelpers, indentation cleanup, consistent
wolfcrypt.prefix inmldsa_test, and removal of the C-onlyWOLF_ENUM_DUMMY_LAST_ELEMENTfrom
NamedGroup.Testing
./autogen.sh && cp wrapper/CSharp/user_settings.h . && ./configure --enable-usersettings --prefix=$(pwd)/install && make && make installcd wrapper/CSharp && mcs wolfCrypt-Test/wolfCrypt-Test.cs wolfSSL_CSharp/{wolfCrypt,wolfSSL,X509}.cs -OUT:wolfcrypttest.exeLD_LIBRARY_PATH=./install/lib mono wrapper/CSharp/wolfcrypttest.exe— alltests pass, including ML-KEM 512/768/1024 and ML-DSA 44/65/87
(signature lengths 2420/3309/4627).
Checklist