From ef3dbfb59c1d1262e4a88a1ce52dd9e043d681b1 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 8 Apr 2026 14:24:21 -0700 Subject: [PATCH 1/5] Add HPKE (RFC 9180) C# wrapper and test --- wrapper/CSharp/user_settings.h | 1 + .../CSharp/wolfCrypt-Test/wolfCrypt-Test.cs | 115 +++++ wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs | 414 ++++++++++++++++++ 3 files changed, 530 insertions(+) diff --git a/wrapper/CSharp/user_settings.h b/wrapper/CSharp/user_settings.h index c5f7c693d1e..65e2ea7e820 100644 --- a/wrapper/CSharp/user_settings.h +++ b/wrapper/CSharp/user_settings.h @@ -70,6 +70,7 @@ #define WOLFSSL_SHA512 #define HAVE_HKDF +#define HAVE_HPKE #undef NO_DH #define HAVE_PUBLIC_FFDHE diff --git a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs index c1d7ccbea46..ce0df870139 100644 --- a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs +++ b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs @@ -880,6 +880,117 @@ private static void hash_test(uint hashType) } } /* END hash_test */ + private static void hpke_test() + { + IntPtr hpke = IntPtr.Zero; + IntPtr receiverKey = IntPtr.Zero; + IntPtr deserializedKey = IntPtr.Zero; + IntPtr ephemeralKey = IntPtr.Zero; + + wolfcrypt.HpkeKem kem = wolfcrypt.HpkeKem.DHKEM_P256_HKDF_SHA256; + wolfcrypt.HpkeKdf kdf = wolfcrypt.HpkeKdf.HKDF_SHA256; + wolfcrypt.HpkeAead aead = wolfcrypt.HpkeAead.AES_128_GCM; + + try + { + Console.WriteLine("\nStarting HPKE Base mode test..."); + + /* Initialize HPKE context */ + Console.WriteLine("Initializing HPKE context..."); + hpke = wolfcrypt.HpkeInit(kem, kdf, aead); + if (hpke == IntPtr.Zero) + { + throw new Exception("HpkeInit failed"); + } + Console.WriteLine("HPKE context initialization passed."); + + /* Generate receiver keypair */ + Console.WriteLine("Generating receiver keypair..."); + receiverKey = wolfcrypt.HpkeGenerateKeyPair(hpke); + if (receiverKey == IntPtr.Zero) + { + throw new Exception("HpkeGenerateKeyPair (receiver) failed"); + } + Console.WriteLine("Receiver keypair generation passed."); + + /* Serialize and deserialize public key (round-trip) */ + Console.WriteLine("Testing public key serialize/deserialize round-trip..."); + byte[] pubKeyBytes = wolfcrypt.HpkeSerializePublicKey(hpke, receiverKey); + if (pubKeyBytes == null) + { + throw new Exception("HpkeSerializePublicKey failed"); + } + Console.WriteLine($"Serialized public key length: {pubKeyBytes.Length}"); + + deserializedKey = wolfcrypt.HpkeDeserializePublicKey(hpke, pubKeyBytes); + if (deserializedKey == IntPtr.Zero) + { + throw new Exception("HpkeDeserializePublicKey failed"); + } + + /* Verify round-trip by re-serializing */ + byte[] pubKeyBytes2 = wolfcrypt.HpkeSerializePublicKey(hpke, deserializedKey); + if (pubKeyBytes2 == null || !wolfcrypt.ByteArrayVerify(pubKeyBytes, pubKeyBytes2)) + { + throw new Exception("Public key round-trip verification failed"); + } + Console.WriteLine("Public key round-trip test passed."); + + /* Generate ephemeral keypair for sender */ + Console.WriteLine("Generating ephemeral keypair..."); + ephemeralKey = wolfcrypt.HpkeGenerateKeyPair(hpke); + if (ephemeralKey == IntPtr.Zero) + { + throw new Exception("HpkeGenerateKeyPair (ephemeral) failed"); + } + Console.WriteLine("Ephemeral keypair generation passed."); + + /* Define test data */ + byte[] info = Encoding.UTF8.GetBytes("HPKE .NET Test"); + byte[] aad = Encoding.UTF8.GetBytes("additional data"); + byte[] plaintext = Encoding.UTF8.GetBytes("Hello HPKE from wolfCrypt .NET!"); + + /* Seal (encrypt) */ + Console.WriteLine("Testing HpkeSealBase..."); + byte[] encCiphertext = wolfcrypt.HpkeSealBase(hpke, ephemeralKey, + receiverKey, info, aad, plaintext); + if (encCiphertext == null) + { + throw new Exception("HpkeSealBase failed"); + } + Console.WriteLine($"HpkeSealBase passed. Output length: {encCiphertext.Length}"); + + /* Open (decrypt) */ + Console.WriteLine("Testing HpkeOpenBase..."); + byte[] decrypted = wolfcrypt.HpkeOpenBase(hpke, receiverKey, + encCiphertext, info, aad, plaintext.Length); + if (decrypted == null) + { + throw new Exception("HpkeOpenBase failed"); + } + Console.WriteLine("HpkeOpenBase passed."); + + /* Compare plaintext and decrypted */ + if (!wolfcrypt.ByteArrayVerify(plaintext, decrypted)) + { + throw new Exception("Decrypted text does not match original plaintext"); + } + Console.WriteLine("HPKE Base mode test PASSED."); + } + finally + { + /* Cleanup */ + if (ephemeralKey != IntPtr.Zero) + wolfcrypt.HpkeFreeKey(hpke, ephemeralKey, kem); + if (deserializedKey != IntPtr.Zero) + wolfcrypt.HpkeFreeKey(hpke, deserializedKey, kem); + if (receiverKey != IntPtr.Zero) + wolfcrypt.HpkeFreeKey(hpke, receiverKey, kem); + if (hpke != IntPtr.Zero) + wolfcrypt.HpkeFree(hpke); + } + } /* END hpke_test */ + public static void standard_log(int lvl, StringBuilder msg) { Console.WriteLine(msg); @@ -941,6 +1052,10 @@ public static void Main(string[] args) hash_test((uint)wolfcrypt.hashType.WC_HASH_TYPE_SHA512); /* SHA-512 HASH test */ hash_test((uint)wolfcrypt.hashType.WC_HASH_TYPE_SHA3_256); /* SHA3_256 HASH test */ + Console.WriteLine("\nStarting HPKE tests"); + + hpke_test(); /* HPKE Base mode test */ + wolfcrypt.Cleanup(); Console.WriteLine("\nAll tests completed successfully"); diff --git a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs index ae44f29fde3..c1c85a45396 100644 --- a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs +++ b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs @@ -469,6 +469,43 @@ public class wolfcrypt #endif + /******************************** + * HPKE + * Requires: HAVE_HPKE, HAVE_ECC (or HAVE_CURVE25519), HAVE_AESGCM + */ +#if WindowsCE + [DllImport(wolfssl_dll)] + private extern static int wc_HpkeInit(IntPtr hpke, int kem, int kdf, int aead, IntPtr heap); + [DllImport(wolfssl_dll)] + private extern static int wc_HpkeGenerateKeyPair(IntPtr hpke, ref IntPtr keypair, IntPtr rng); + [DllImport(wolfssl_dll)] + private extern static int wc_HpkeSerializePublicKey(IntPtr hpke, IntPtr key, byte[] outBuf, ref ushort outSz); + [DllImport(wolfssl_dll)] + private extern static int wc_HpkeDeserializePublicKey(IntPtr hpke, ref IntPtr key, byte[] inBuf, ushort inSz); + [DllImport(wolfssl_dll)] + private extern static void wc_HpkeFreeKey(IntPtr hpke, ushort kem, IntPtr keypair, IntPtr heap); + [DllImport(wolfssl_dll)] + private extern static int wc_HpkeSealBase(IntPtr hpke, IntPtr ephemeralKey, IntPtr receiverKey, byte[] info, uint infoSz, byte[] aad, uint aadSz, byte[] plaintext, uint ptSz, byte[] ciphertext); + [DllImport(wolfssl_dll)] + private extern static int wc_HpkeOpenBase(IntPtr hpke, IntPtr receiverKey, byte[] pubKey, ushort pubKeySz, byte[] info, uint infoSz, byte[] aad, uint aadSz, byte[] ciphertext, uint ctSz, byte[] plaintext); +#else + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static int wc_HpkeInit(IntPtr hpke, int kem, int kdf, int aead, IntPtr heap); + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static int wc_HpkeGenerateKeyPair(IntPtr hpke, ref IntPtr keypair, IntPtr rng); + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static int wc_HpkeSerializePublicKey(IntPtr hpke, IntPtr key, byte[] outBuf, ref ushort outSz); + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static int wc_HpkeDeserializePublicKey(IntPtr hpke, ref IntPtr key, byte[] inBuf, ushort inSz); + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static void wc_HpkeFreeKey(IntPtr hpke, ushort kem, IntPtr keypair, IntPtr heap); + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static int wc_HpkeSealBase(IntPtr hpke, IntPtr ephemeralKey, IntPtr receiverKey, byte[] info, uint infoSz, byte[] aad, uint aadSz, byte[] plaintext, uint ptSz, byte[] ciphertext); + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static int wc_HpkeOpenBase(IntPtr hpke, IntPtr receiverKey, byte[] pubKey, ushort pubKeySz, byte[] info, uint infoSz, byte[] aad, uint aadSz, byte[] ciphertext, uint ctSz, byte[] plaintext); +#endif + + /******************************** * HASH */ @@ -3192,6 +3229,383 @@ public enum hashType /* END HASH */ + /*********************************************************************** + * HPKE (RFC 9180) - Base mode SingleShot + * Requires: HAVE_HPKE, HAVE_ECC (or HAVE_CURVE25519), HAVE_AESGCM + **********************************************************************/ + + /* BEGIN HPKE */ + + /* HPKE KEM IDs */ + public enum HpkeKem : ushort { + DHKEM_P256_HKDF_SHA256 = 0x0010, + DHKEM_P384_HKDF_SHA384 = 0x0011, + DHKEM_P521_HKDF_SHA512 = 0x0012, + DHKEM_X25519_HKDF_SHA256 = 0x0020, + DHKEM_X448_HKDF_SHA512 = 0x0021, + } + /* HPKE KDF IDs */ + public enum HpkeKdf : ushort { + HKDF_SHA256 = 0x0001, + HKDF_SHA384 = 0x0002, + HKDF_SHA512 = 0x0003, + } + /* HPKE AEAD IDs */ + public enum HpkeAead : ushort { + AES_128_GCM = 0x0001, + AES_256_GCM = 0x0002, + } + + /* HPKE Nt (GCM tag length) */ + private static readonly int HPKE_Nt = 16; + + /* Hpke struct is ~80 bytes on 64-bit (see hpke.h). Over-allocate to + * accommodate future growth and platform alignment differences. */ + private static readonly int HPKE_STRUCT_SZ = 512; + + /// + /// Get the enc (encapsulated key) length for a given KEM + /// + /// KEM identifier + /// Length in bytes + private static ushort HpkeEncLen(HpkeKem kem) + { + switch (kem) + { + case HpkeKem.DHKEM_P256_HKDF_SHA256: return 65; + case HpkeKem.DHKEM_P384_HKDF_SHA384: return 97; + case HpkeKem.DHKEM_P521_HKDF_SHA512: return 133; + case HpkeKem.DHKEM_X25519_HKDF_SHA256: return 32; + case HpkeKem.DHKEM_X448_HKDF_SHA512: return 56; + default: return 0; + } + } + + /// + /// Allocate and initialize an HPKE context + /// + /// KEM algorithm identifier + /// KDF algorithm identifier + /// AEAD algorithm identifier + /// Pointer to allocated Hpke context or IntPtr.Zero on failure + public static IntPtr HpkeInit(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) + { + IntPtr hpke = IntPtr.Zero; + + try + { + hpke = Marshal.AllocHGlobal(HPKE_STRUCT_SZ); + if (hpke == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE alloc failed"); + return IntPtr.Zero; + } + + /* Zero the memory */ + for (int i = 0; i < HPKE_STRUCT_SZ; i++) + { + Marshal.WriteByte(hpke, i, 0); + } + + int ret = wc_HpkeInit(hpke, (int)kem, (int)kdf, (int)aead, IntPtr.Zero); + if (ret != 0) + { + log(ERROR_LOG, "HPKE init failed " + ret + ": " + GetError(ret)); + Marshal.FreeHGlobal(hpke); + return IntPtr.Zero; + } + } + catch (Exception e) + { + log(ERROR_LOG, "HPKE init exception " + e.ToString()); + if (hpke != IntPtr.Zero) + { + Marshal.FreeHGlobal(hpke); + } + return IntPtr.Zero; + } + + return hpke; + } + + /// + /// Generate a new HPKE keypair + /// + /// HPKE context from HpkeInit() + /// Pointer to keypair or IntPtr.Zero on failure + public static IntPtr HpkeGenerateKeyPair(IntPtr hpke) + { + IntPtr keypair = IntPtr.Zero; + IntPtr rng = IntPtr.Zero; + + try + { + if (hpke == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE generate keypair: invalid context"); + return IntPtr.Zero; + } + + rng = RandomNew(); + if (rng == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE generate keypair: RNG init failed"); + return IntPtr.Zero; + } + + int ret = wc_HpkeGenerateKeyPair(hpke, ref keypair, rng); + if (ret != 0) + { + log(ERROR_LOG, "HPKE generate keypair failed " + ret + ": " + GetError(ret)); + keypair = IntPtr.Zero; + } + } + catch (Exception e) + { + log(ERROR_LOG, "HPKE generate keypair exception " + e.ToString()); + keypair = IntPtr.Zero; + } + finally + { + if (rng != IntPtr.Zero) RandomFree(rng); + } + + return keypair; + } + + /// + /// Serialize the public key to bytes + /// + /// HPKE context from HpkeInit() + /// Keypair from HpkeGenerateKeyPair() + /// Serialized public key bytes or null on failure + public static byte[] HpkeSerializePublicKey(IntPtr hpke, IntPtr keypair) + { + try + { + if (hpke == IntPtr.Zero || keypair == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE serialize public key: invalid parameter"); + return null; + } + + ushort outSz = 133; /* HPKE_Npk_MAX */ + byte[] outBuf = new byte[outSz]; + + int ret = wc_HpkeSerializePublicKey(hpke, keypair, outBuf, ref outSz); + if (ret != 0) + { + log(ERROR_LOG, "HPKE serialize public key failed " + ret + ": " + GetError(ret)); + return null; + } + + /* Trim to actual size */ + byte[] result = new byte[outSz]; + Array.Copy(outBuf, 0, result, 0, outSz); + return result; + } + catch (Exception e) + { + log(ERROR_LOG, "HPKE serialize public key exception " + e.ToString()); + return null; + } + } + + /// + /// Deserialize a public key from bytes + /// + /// HPKE context from HpkeInit() + /// Serialized public key bytes + /// Pointer to keypair or IntPtr.Zero on failure + public static IntPtr HpkeDeserializePublicKey(IntPtr hpke, byte[] pubKeyBytes) + { + IntPtr key = IntPtr.Zero; + + try + { + if (hpke == IntPtr.Zero || pubKeyBytes == null || pubKeyBytes.Length == 0) + { + log(ERROR_LOG, "HPKE deserialize public key: invalid parameter"); + return IntPtr.Zero; + } + + int ret = wc_HpkeDeserializePublicKey(hpke, ref key, pubKeyBytes, (ushort)pubKeyBytes.Length); + if (ret != 0) + { + log(ERROR_LOG, "HPKE deserialize public key failed " + ret + ": " + GetError(ret)); + return IntPtr.Zero; + } + } + catch (Exception e) + { + log(ERROR_LOG, "HPKE deserialize public key exception " + e.ToString()); + return IntPtr.Zero; + } + + return key; + } + + /// + /// Free a keypair created by HpkeGenerateKeyPair or HpkeDeserializePublicKey + /// + /// HPKE context from HpkeInit() + /// Keypair to free + /// KEM used when the keypair was created + public static void HpkeFreeKey(IntPtr hpke, IntPtr keypair, HpkeKem kem) + { + if (hpke != IntPtr.Zero && keypair != IntPtr.Zero) + { + wc_HpkeFreeKey(hpke, (ushort)kem, keypair, IntPtr.Zero); + } + } + + /// + /// Free an HPKE context allocated by HpkeInit + /// + /// HPKE context to free + public static void HpkeFree(IntPtr hpke) + { + if (hpke != IntPtr.Zero) + { + Marshal.FreeHGlobal(hpke); + } + } + + /// + /// SingleShot seal (encrypt) using HPKE Base mode. + /// Returns enc||ciphertext as a single byte array. + /// The enc length is determined by the KEM (e.g. 65 bytes for P-256). + /// Ciphertext length = plaintext length + Nt (16-byte GCM tag). + /// + /// HPKE context from HpkeInit() + /// Ephemeral keypair for sender + /// Receiver public key + /// Info context bytes (can be null) + /// Additional authenticated data (can be null) + /// Plaintext to encrypt + /// enc||ciphertext byte array or null on failure + public static byte[] HpkeSealBase(IntPtr hpke, IntPtr ephemeralKey, IntPtr receiverKey, + byte[] info, byte[] aad, byte[] plaintext) + { + try + { + if (hpke == IntPtr.Zero || ephemeralKey == IntPtr.Zero || receiverKey == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE seal base: invalid parameter"); + return null; + } + if (plaintext == null || plaintext.Length == 0) + { + log(ERROR_LOG, "HPKE seal base: invalid plaintext"); + return null; + } + + /* Serialize the ephemeral public key (enc) */ + byte[] enc = HpkeSerializePublicKey(hpke, ephemeralKey); + if (enc == null) + { + log(ERROR_LOG, "HPKE seal base: failed to serialize ephemeral key"); + return null; + } + + uint infoSz = (info != null) ? (uint)info.Length : 0; + uint aadSz = (aad != null) ? (uint)aad.Length : 0; + uint ptSz = (uint)plaintext.Length; + + /* wc_HpkeSealBase outputs ptSz + Nt (GCM tag) bytes */ + int sealLen = (int)ptSz + HPKE_Nt; + byte[] sealOut = new byte[sealLen]; + + int ret = wc_HpkeSealBase(hpke, ephemeralKey, receiverKey, + info, infoSz, aad, aadSz, plaintext, ptSz, sealOut); + if (ret != 0) + { + log(ERROR_LOG, "HPKE seal base failed " + ret + ": " + GetError(ret)); + return null; + } + + /* Return enc || sealOut */ + byte[] result = new byte[enc.Length + sealLen]; + Array.Copy(enc, 0, result, 0, enc.Length); + Array.Copy(sealOut, 0, result, enc.Length, sealLen); + return result; + } + catch (Exception e) + { + log(ERROR_LOG, "HPKE seal base exception " + e.ToString()); + return null; + } + } + + /// + /// SingleShot open (decrypt) using HPKE Base mode. + /// Takes the full enc||ciphertext blob returned by HpkeSealBase. + /// + /// HPKE context from HpkeInit() + /// Receiver private keypair + /// enc||ciphertext blob from HpkeSealBase() + /// Info context bytes (can be null) + /// Additional authenticated data (can be null) + /// Expected plaintext length + /// Decrypted plaintext byte array or null on failure + public static byte[] HpkeOpenBase(IntPtr hpke, IntPtr receiverKey, + byte[] encCiphertext, byte[] info, byte[] aad, int ptLen) + { + try + { + if (hpke == IntPtr.Zero || receiverKey == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE open base: invalid parameter"); + return null; + } + if (encCiphertext == null || encCiphertext.Length == 0) + { + log(ERROR_LOG, "HPKE open base: invalid ciphertext"); + return null; + } + + /* encCiphertext = enc || ciphertext || GCM tag + * where ciphertext is ptLen bytes, tag is Nt bytes */ + int sealLen = ptLen + HPKE_Nt; + if (ptLen < 0 || encCiphertext.Length < sealLen) + { + log(ERROR_LOG, "HPKE open base: encCiphertext too short for given ptLen"); + return null; + } + ushort pubKeySz = (ushort)(encCiphertext.Length - sealLen); + + /* Split enc and sealed data (ciphertext || tag) */ + byte[] pubKey = new byte[pubKeySz]; + byte[] ct = new byte[sealLen]; + Array.Copy(encCiphertext, 0, pubKey, 0, pubKeySz); + Array.Copy(encCiphertext, pubKeySz, ct, 0, sealLen); + + uint infoSz = (info != null) ? (uint)info.Length : 0; + uint aadSz = (aad != null) ? (uint)aad.Length : 0; + + byte[] plaintext = new byte[ptLen]; + + /* ctSz is just the ciphertext length (without tag); + * wc_HpkeContextOpenBase reads the tag from ct + ctSz */ + int ret = wc_HpkeOpenBase(hpke, receiverKey, pubKey, pubKeySz, + info, infoSz, aad, aadSz, ct, (uint)ptLen, plaintext); + if (ret != 0) + { + log(ERROR_LOG, "HPKE open base failed " + ret + ": " + GetError(ret)); + return null; + } + + return plaintext; + } + catch (Exception e) + { + log(ERROR_LOG, "HPKE open base exception " + e.ToString()); + return null; + } + } + /* END HPKE */ + + /*********************************************************************** * Logging / Other **********************************************************************/ From 0228bac576003886264c655488c2162b8e69e7bf Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 8 Apr 2026 16:05:24 -0700 Subject: [PATCH 2/5] Add missing hpke.c to project. Add overload to support internal ephemeral key generation --- .../CSharp/wolfCrypt-Test/wolfCrypt-Test.cs | 22 +++++++++ wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs | 47 +++++++++++++++++-- wrapper/CSharp/wolfssl.vcxproj | 2 + 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs index ce0df870139..48bdff2829c 100644 --- a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs +++ b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs @@ -976,6 +976,28 @@ private static void hpke_test() throw new Exception("Decrypted text does not match original plaintext"); } Console.WriteLine("HPKE Base mode test PASSED."); + + /* Test convenience overload (no ephemeral key) */ + Console.WriteLine("Testing HpkeSealBase convenience overload..."); + byte[] encCiphertext2 = wolfcrypt.HpkeSealBase(hpke, receiverKey, + info, aad, plaintext, kem); + if (encCiphertext2 == null) + { + throw new Exception("HpkeSealBase (convenience) failed"); + } + Console.WriteLine($"HpkeSealBase convenience passed. Output length: {encCiphertext2.Length}"); + + byte[] decrypted2 = wolfcrypt.HpkeOpenBase(hpke, receiverKey, + encCiphertext2, info, aad, plaintext.Length); + if (decrypted2 == null) + { + throw new Exception("HpkeOpenBase (convenience) failed"); + } + if (!wolfcrypt.ByteArrayVerify(plaintext, decrypted2)) + { + throw new Exception("Convenience seal/open: decrypted text does not match"); + } + Console.WriteLine("HPKE convenience overload test PASSED."); } finally { diff --git a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs index c1c85a45396..56b17995f0b 100644 --- a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs +++ b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs @@ -3302,10 +3302,7 @@ public static IntPtr HpkeInit(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) } /* Zero the memory */ - for (int i = 0; i < HPKE_STRUCT_SZ; i++) - { - Marshal.WriteByte(hpke, i, 0); - } + Marshal.Copy(new byte[HPKE_STRUCT_SZ], 0, hpke, HPKE_STRUCT_SZ); int ret = wc_HpkeInit(hpke, (int)kem, (int)kdf, (int)aead, IntPtr.Zero); if (ret != 0) @@ -3537,6 +3534,48 @@ public static byte[] HpkeSealBase(IntPtr hpke, IntPtr ephemeralKey, IntPtr recei } } + /// + /// Convenience SingleShot seal (encrypt) using HPKE Base mode. + /// Generates an ephemeral keypair internally so the caller does not + /// need to manage one. + /// Returns enc||ciphertext as a single byte array. + /// + /// HPKE context from HpkeInit() + /// Receiver public key + /// Info context bytes (can be null) + /// Additional authenticated data (can be null) + /// Plaintext to encrypt + /// KEM used (needed to free the ephemeral key) + /// enc||ciphertext byte array or null on failure + public static byte[] HpkeSealBase(IntPtr hpke, IntPtr receiverKey, + byte[] info, byte[] aad, byte[] plaintext, HpkeKem kem) + { + IntPtr ephemeralKey = IntPtr.Zero; + + try + { + ephemeralKey = HpkeGenerateKeyPair(hpke); + if (ephemeralKey == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE seal base: ephemeral keygen failed"); + return null; + } + + return HpkeSealBase(hpke, ephemeralKey, receiverKey, + info, aad, plaintext); + } + catch (Exception e) + { + log(ERROR_LOG, "HPKE seal base exception " + e.ToString()); + return null; + } + finally + { + if (ephemeralKey != IntPtr.Zero) + HpkeFreeKey(hpke, ephemeralKey, kem); + } + } + /// /// SingleShot open (decrypt) using HPKE Base mode. /// Takes the full enc||ciphertext blob returned by HpkeSealBase. diff --git a/wrapper/CSharp/wolfssl.vcxproj b/wrapper/CSharp/wolfssl.vcxproj index 8ac0fdb7220..a01d55d5a49 100644 --- a/wrapper/CSharp/wolfssl.vcxproj +++ b/wrapper/CSharp/wolfssl.vcxproj @@ -317,6 +317,7 @@ + @@ -332,6 +333,7 @@ + From c495f1c9d7f35dc3ea0dff66c6610260bc9a3ff2 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 9 Apr 2026 08:58:39 -0700 Subject: [PATCH 3/5] Peer review fixes --- wrapper/CSharp/README.md | 23 ++++++++ .../CSharp/wolfCrypt-Test/wolfCrypt-Test.cs | 49 ++++++++++++++--- wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs | 55 +++++++++++++++++-- 3 files changed, 115 insertions(+), 12 deletions(-) diff --git a/wrapper/CSharp/README.md b/wrapper/CSharp/README.md index ac15e0c338c..e1091b502da 100644 --- a/wrapper/CSharp/README.md +++ b/wrapper/CSharp/README.md @@ -40,6 +40,8 @@ apt-get install mono-complete ### Build wolfSSL and install +#### System-wide install + ``` ./autogen.sh cp wrapper/CSharp/user_settings.h . @@ -49,6 +51,16 @@ make check sudo make install ``` +#### Local-only install (no sudo required) + +``` +./autogen.sh +cp wrapper/CSharp/user_settings.h . +./configure --enable-usersettings --prefix=$(pwd)/install +make +make install +``` + ### Build and run the wolfCrypt test wrapper From the `wrapper/CSharp` directory (`cd wrapper/CSharp`): @@ -57,9 +69,20 @@ Compile wolfCrypt test: ``` mcs wolfCrypt-Test/wolfCrypt-Test.cs wolfSSL_CSharp/wolfCrypt.cs wolfSSL_CSharp/wolfSSL.cs wolfSSL_CSharp/X509.cs -OUT:wolfcrypttest.exe +``` + +Run with system-wide install: + +``` mono wolfcrypttest.exe ``` +Run with local-only install (from the wolfSSL root directory): + +``` +LD_LIBRARY_PATH=./install/lib mono wrapper/CSharp/wolfcrypttest.exe +``` + ### Build and run the wolfSSL client/server test From the `wrapper/CSharp` directory (`cd wrapper/CSharp`): diff --git a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs index 48bdff2829c..3fec10853e1 100644 --- a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs +++ b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs @@ -880,17 +880,14 @@ private static void hash_test(uint hashType) } } /* END hash_test */ - private static void hpke_test() + private static void hpke_test(wolfcrypt.HpkeKem kem, + wolfcrypt.HpkeKdf kdf, wolfcrypt.HpkeAead aead) { IntPtr hpke = IntPtr.Zero; IntPtr receiverKey = IntPtr.Zero; IntPtr deserializedKey = IntPtr.Zero; IntPtr ephemeralKey = IntPtr.Zero; - wolfcrypt.HpkeKem kem = wolfcrypt.HpkeKem.DHKEM_P256_HKDF_SHA256; - wolfcrypt.HpkeKdf kdf = wolfcrypt.HpkeKdf.HKDF_SHA256; - wolfcrypt.HpkeAead aead = wolfcrypt.HpkeAead.AES_128_GCM; - try { Console.WriteLine("\nStarting HPKE Base mode test..."); @@ -988,7 +985,7 @@ private static void hpke_test() Console.WriteLine($"HpkeSealBase convenience passed. Output length: {encCiphertext2.Length}"); byte[] decrypted2 = wolfcrypt.HpkeOpenBase(hpke, receiverKey, - encCiphertext2, info, aad, plaintext.Length); + encCiphertext2, info, aad, kem); if (decrypted2 == null) { throw new Exception("HpkeOpenBase (convenience) failed"); @@ -998,6 +995,39 @@ private static void hpke_test() throw new Exception("Convenience seal/open: decrypted text does not match"); } Console.WriteLine("HPKE convenience overload test PASSED."); + + /* Negative test: tampered ciphertext should fail */ + Console.WriteLine("Testing HpkeOpenBase with tampered ciphertext..."); + byte[] tampered = (byte[])encCiphertext.Clone(); + tampered[tampered.Length - 1] ^= 0xFF; /* flip last byte (in tag) */ + byte[] badResult = wolfcrypt.HpkeOpenBase(hpke, receiverKey, + tampered, info, aad, plaintext.Length); + if (badResult != null) + { + throw new Exception("HpkeOpenBase should fail with tampered ciphertext"); + } + Console.WriteLine("Tampered ciphertext test PASSED (correctly rejected)."); + + /* Negative test: mismatched AAD should fail */ + Console.WriteLine("Testing HpkeOpenBase with mismatched AAD..."); + byte[] wrongAad = Encoding.UTF8.GetBytes("wrong aad"); + badResult = wolfcrypt.HpkeOpenBase(hpke, receiverKey, + encCiphertext, info, wrongAad, plaintext.Length); + if (badResult != null) + { + throw new Exception("HpkeOpenBase should fail with mismatched AAD"); + } + Console.WriteLine("Mismatched AAD test PASSED (correctly rejected)."); + + /* Negative test: invalid ptLen should fail */ + Console.WriteLine("Testing HpkeOpenBase with invalid ptLen..."); + badResult = wolfcrypt.HpkeOpenBase(hpke, receiverKey, + encCiphertext, info, aad, plaintext.Length + 1); + if (badResult != null) + { + throw new Exception("HpkeOpenBase should fail with incorrect ptLen"); + } + Console.WriteLine("Invalid ptLen test PASSED (correctly rejected)."); } finally { @@ -1076,7 +1106,12 @@ public static void Main(string[] args) Console.WriteLine("\nStarting HPKE tests"); - hpke_test(); /* HPKE Base mode test */ + hpke_test(wolfcrypt.HpkeKem.DHKEM_P256_HKDF_SHA256, + wolfcrypt.HpkeKdf.HKDF_SHA256, + wolfcrypt.HpkeAead.AES_128_GCM); + hpke_test(wolfcrypt.HpkeKem.DHKEM_X25519_HKDF_SHA256, + wolfcrypt.HpkeKdf.HKDF_SHA256, + wolfcrypt.HpkeAead.AES_128_GCM); wolfcrypt.Cleanup(); diff --git a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs index 56b17995f0b..59a3c433ca4 100644 --- a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs +++ b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs @@ -3259,8 +3259,10 @@ public enum HpkeAead : ushort { /* HPKE Nt (GCM tag length) */ private static readonly int HPKE_Nt = 16; - /* Hpke struct is ~80 bytes on 64-bit (see hpke.h). Over-allocate to - * accommodate future growth and platform alignment differences. */ + /* Hpke struct is ~80 bytes on 64-bit (see hpke.h). Allocate 512 bytes + * (6x headroom) to accommodate platform alignment and future growth. + * If the native struct ever exceeds this, wc_HpkeInit will write OOB — + * keep in sync with hpke.h if the struct grows significantly. */ private static readonly int HPKE_STRUCT_SZ = 512; /// @@ -3605,13 +3607,26 @@ public static byte[] HpkeOpenBase(IntPtr hpke, IntPtr receiverKey, /* encCiphertext = enc || ciphertext || GCM tag * where ciphertext is ptLen bytes, tag is Nt bytes */ + if (ptLen < 0 || ptLen > int.MaxValue - HPKE_Nt) + { + log(ERROR_LOG, "HPKE open base: invalid ptLen"); + return null; + } + int sealLen = ptLen + HPKE_Nt; - if (ptLen < 0 || encCiphertext.Length < sealLen) + if (encCiphertext.Length < sealLen) { log(ERROR_LOG, "HPKE open base: encCiphertext too short for given ptLen"); return null; } - ushort pubKeySz = (ushort)(encCiphertext.Length - sealLen); + + int pubKeySzInt = encCiphertext.Length - sealLen; + if (pubKeySzInt < 0 || pubKeySzInt > ushort.MaxValue) + { + log(ERROR_LOG, "HPKE open base: invalid encapsulated public key size"); + return null; + } + ushort pubKeySz = (ushort)pubKeySzInt; /* Split enc and sealed data (ciphertext || tag) */ byte[] pubKey = new byte[pubKeySz]; @@ -3625,7 +3640,7 @@ public static byte[] HpkeOpenBase(IntPtr hpke, IntPtr receiverKey, byte[] plaintext = new byte[ptLen]; /* ctSz is just the ciphertext length (without tag); - * wc_HpkeContextOpenBase reads the tag from ct + ctSz */ + * wc_HpkeOpenBase reads the tag from ct + ctSz */ int ret = wc_HpkeOpenBase(hpke, receiverKey, pubKey, pubKeySz, info, infoSz, aad, aadSz, ct, (uint)ptLen, plaintext); if (ret != 0) @@ -3642,6 +3657,36 @@ public static byte[] HpkeOpenBase(IntPtr hpke, IntPtr receiverKey, return null; } } + + /// + /// Convenience SingleShot open (decrypt) using HPKE Base mode. + /// Derives the plaintext length from the KEM enc length, so the caller + /// does not need to know ptLen. + /// + /// HPKE context from HpkeInit() + /// Receiver private keypair + /// enc||ciphertext blob from HpkeSealBase() + /// Info context bytes (can be null) + /// Additional authenticated data (can be null) + /// KEM used (to derive enc length) + /// Decrypted plaintext byte array or null on failure + public static byte[] HpkeOpenBase(IntPtr hpke, IntPtr receiverKey, + byte[] encCiphertext, byte[] info, byte[] aad, HpkeKem kem) + { + ushort encLen = HpkeEncLen(kem); + if (encLen == 0) + { + log(ERROR_LOG, "HPKE open base: unsupported KEM"); + return null; + } + if (encCiphertext == null || encCiphertext.Length < encLen + HPKE_Nt) + { + log(ERROR_LOG, "HPKE open base: encCiphertext too short"); + return null; + } + int ptLen = encCiphertext.Length - encLen - HPKE_Nt; + return HpkeOpenBase(hpke, receiverKey, encCiphertext, info, aad, ptLen); + } /* END HPKE */ From 8617f0007cf40dd12e0bb13c25727684951074de Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 10 Apr 2026 12:21:18 -0700 Subject: [PATCH 4/5] More peer review fixes --- wrapper/CSharp/README.md | 4 +- .../CSharp/wolfCrypt-Test/wolfCrypt-Test.cs | 16 +++ wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs | 101 ++++++++++++++---- 3 files changed, 102 insertions(+), 19 deletions(-) diff --git a/wrapper/CSharp/README.md b/wrapper/CSharp/README.md index e1091b502da..4323045b4ba 100644 --- a/wrapper/CSharp/README.md +++ b/wrapper/CSharp/README.md @@ -77,7 +77,9 @@ Run with system-wide install: mono wolfcrypttest.exe ``` -Run with local-only install (from the wolfSSL root directory): +Run with local-only install. The compile step above produced +`wolfcrypttest.exe` inside `wrapper/CSharp/`; this run command is invoked +from the wolfSSL project root so the relative paths line up: ``` LD_LIBRARY_PATH=./install/lib mono wrapper/CSharp/wolfcrypttest.exe diff --git a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs index 3fec10853e1..9cb58b87e31 100644 --- a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs +++ b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs @@ -1019,6 +1019,22 @@ private static void hpke_test(wolfcrypt.HpkeKem kem, } Console.WriteLine("Mismatched AAD test PASSED (correctly rejected)."); + /* Null info/aad round-trip - exercises the null-marshaling path through P/Invoke */ + Console.WriteLine("Testing HpkeSealBase/OpenBase with null info and aad..."); + byte[] encNull = wolfcrypt.HpkeSealBase(hpke, receiverKey, + null, null, plaintext, kem); + if (encNull == null) + { + throw new Exception("HpkeSealBase with null info/aad failed"); + } + byte[] decNull = wolfcrypt.HpkeOpenBase(hpke, receiverKey, + encNull, null, null, kem); + if (decNull == null || !wolfcrypt.ByteArrayVerify(plaintext, decNull)) + { + throw new Exception("HpkeOpenBase with null info/aad: round-trip failed"); + } + Console.WriteLine("Null info/aad round-trip test PASSED."); + /* Negative test: invalid ptLen should fail */ Console.WriteLine("Testing HpkeOpenBase with invalid ptLen..."); badResult = wolfcrypt.HpkeOpenBase(hpke, receiverKey, diff --git a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs index 59a3c433ca4..77d03535816 100644 --- a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs +++ b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs @@ -20,6 +20,7 @@ */ using System; +using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -361,6 +362,10 @@ public class wolfcrypt private extern static int wc_curve25519_make_key(IntPtr rng, int keysize, IntPtr key); [DllImport(wolfssl_dll)] private extern static int wc_curve25519_shared_secret(IntPtr privateKey, IntPtr publicKey, byte[] outSharedSecret, ref int outlen); + /* Only available when wolfSSL is built with WOLFSSL_CURVE25519_BLINDING. + * Calls are wrapped in try/catch to tolerate builds without it. */ + [DllImport(wolfssl_dll)] + private extern static int wc_curve25519_set_rng(IntPtr key, IntPtr rng); /* ASN.1 DER format */ [DllImport(wolfssl_dll)] @@ -400,6 +405,10 @@ public class wolfcrypt private extern static int wc_curve25519_make_key(IntPtr rng, int keysize, IntPtr key); [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] private extern static int wc_curve25519_shared_secret(IntPtr privateKey, IntPtr publicKey, byte[] outSharedSecret, ref int outlen); + /* Only available when wolfSSL is built with WOLFSSL_CURVE25519_BLINDING. + * Calls are wrapped in try/catch to tolerate builds without it. */ + [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] + private extern static int wc_curve25519_set_rng(IntPtr key, IntPtr rng); /* ASN.1 DER format */ [DllImport(wolfssl_dll, CallingConvention = CallingConvention.Cdecl)] @@ -3257,13 +3266,31 @@ public enum HpkeAead : ushort { } /* HPKE Nt (GCM tag length) */ - private static readonly int HPKE_Nt = 16; + private const int HPKE_Nt = 16; + + /* HPKE max encoded public-key length (matches HPKE_Npk_MAX in hpke.h) */ + private const int HPKE_Npk_MAX = 133; /* Hpke struct is ~80 bytes on 64-bit (see hpke.h). Allocate 512 bytes * (6x headroom) to accommodate platform alignment and future growth. * If the native struct ever exceeds this, wc_HpkeInit will write OOB — * keep in sync with hpke.h if the struct grows significantly. */ - private static readonly int HPKE_STRUCT_SZ = 512; + private const int HPKE_STRUCT_SZ = 512; + + /* Per-Hpke-context state owned by the C# wrapper. + * The RNG must outlive any keypair created with this context: when + * wolfSSL is built with WOLFSSL_CURVE25519_BLINDING, wc_curve25519_make_key + * stores the rng pointer inside the keypair (via wc_curve25519_set_rng) + * and re-uses it for blinding during shared-secret operations. If the + * wrapper freed the rng after key generation, that pointer would dangle + * and the next seal/open would fail with RNG_FAILURE_E (-199). */ + private struct HpkeContextState + { + public IntPtr rng; + public HpkeKem kem; + } + private static readonly ConcurrentDictionary hpkeContexts = + new ConcurrentDictionary(); /// /// Get the enc (encapsulated key) length for a given KEM @@ -3272,13 +3299,15 @@ public enum HpkeAead : ushort { /// Length in bytes private static ushort HpkeEncLen(HpkeKem kem) { + /* Values must match DHKEM_*_ENC_LEN macros in wolfssl/wolfcrypt/hpke.h. + * Not P/Invoked because wc_HpkeKemGetEncLen is currently WOLFSSL_LOCAL. */ switch (kem) { - case HpkeKem.DHKEM_P256_HKDF_SHA256: return 65; - case HpkeKem.DHKEM_P384_HKDF_SHA384: return 97; - case HpkeKem.DHKEM_P521_HKDF_SHA512: return 133; - case HpkeKem.DHKEM_X25519_HKDF_SHA256: return 32; - case HpkeKem.DHKEM_X448_HKDF_SHA512: return 56; + case HpkeKem.DHKEM_P256_HKDF_SHA256: return 65; /* DHKEM_P256_ENC_LEN */ + case HpkeKem.DHKEM_P384_HKDF_SHA384: return 97; /* DHKEM_P384_ENC_LEN */ + case HpkeKem.DHKEM_P521_HKDF_SHA512: return 133; /* DHKEM_P521_ENC_LEN */ + case HpkeKem.DHKEM_X25519_HKDF_SHA256: return 32; /* DHKEM_X25519_ENC_LEN */ + case HpkeKem.DHKEM_X448_HKDF_SHA512: return 56; /* DHKEM_X448_ENC_LEN */ default: return 0; } } @@ -3293,6 +3322,7 @@ private static ushort HpkeEncLen(HpkeKem kem) public static IntPtr HpkeInit(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) { IntPtr hpke = IntPtr.Zero; + IntPtr rng = IntPtr.Zero; try { @@ -3313,10 +3343,27 @@ public static IntPtr HpkeInit(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) Marshal.FreeHGlobal(hpke); return IntPtr.Zero; } + + /* Allocate a persistent RNG that lives as long as this context. + * Required so curve25519 keypairs (with blinding) retain a valid + * rng pointer for shared-secret operations. */ + rng = RandomNew(); + if (rng == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE init: RNG allocation failed"); + Marshal.FreeHGlobal(hpke); + return IntPtr.Zero; + } + + hpkeContexts[hpke] = new HpkeContextState { rng = rng, kem = kem }; } catch (Exception e) { log(ERROR_LOG, "HPKE init exception " + e.ToString()); + if (rng != IntPtr.Zero) + { + RandomFree(rng); + } if (hpke != IntPtr.Zero) { Marshal.FreeHGlobal(hpke); @@ -3335,7 +3382,6 @@ public static IntPtr HpkeInit(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) public static IntPtr HpkeGenerateKeyPair(IntPtr hpke) { IntPtr keypair = IntPtr.Zero; - IntPtr rng = IntPtr.Zero; try { @@ -3345,18 +3391,36 @@ public static IntPtr HpkeGenerateKeyPair(IntPtr hpke) return IntPtr.Zero; } - rng = RandomNew(); - if (rng == IntPtr.Zero) + HpkeContextState state; + if (!hpkeContexts.TryGetValue(hpke, out state) || state.rng == IntPtr.Zero) { - log(ERROR_LOG, "HPKE generate keypair: RNG init failed"); + log(ERROR_LOG, "HPKE generate keypair: no RNG associated with context"); return IntPtr.Zero; } - int ret = wc_HpkeGenerateKeyPair(hpke, ref keypair, rng); + int ret = wc_HpkeGenerateKeyPair(hpke, ref keypair, state.rng); if (ret != 0) { log(ERROR_LOG, "HPKE generate keypair failed " + ret + ": " + GetError(ret)); - keypair = IntPtr.Zero; + return IntPtr.Zero; + } + + /* For X25519, explicitly bind the persistent rng to the keypair. + * wc_curve25519_make_key already does this internally when wolfSSL + * is built with WOLFSSL_CURVE25519_BLINDING, but the explicit call + * here documents the lifetime requirement and is defensive against + * future changes. The function only exists when blinding is built + * in, so swallow EntryPointNotFoundException for builds without it. */ + if (state.kem == HpkeKem.DHKEM_X25519_HKDF_SHA256 && keypair != IntPtr.Zero) + { + try + { + wc_curve25519_set_rng(keypair, state.rng); + } + catch (EntryPointNotFoundException) + { + /* wolfSSL built without WOLFSSL_CURVE25519_BLINDING; nothing to do */ + } } } catch (Exception e) @@ -3364,10 +3428,6 @@ public static IntPtr HpkeGenerateKeyPair(IntPtr hpke) log(ERROR_LOG, "HPKE generate keypair exception " + e.ToString()); keypair = IntPtr.Zero; } - finally - { - if (rng != IntPtr.Zero) RandomFree(rng); - } return keypair; } @@ -3388,7 +3448,7 @@ public static byte[] HpkeSerializePublicKey(IntPtr hpke, IntPtr keypair) return null; } - ushort outSz = 133; /* HPKE_Npk_MAX */ + ushort outSz = (ushort)HPKE_Npk_MAX; byte[] outBuf = new byte[outSz]; int ret = wc_HpkeSerializePublicKey(hpke, keypair, outBuf, ref outSz); @@ -3466,6 +3526,11 @@ public static void HpkeFree(IntPtr hpke) { if (hpke != IntPtr.Zero) { + HpkeContextState state; + if (hpkeContexts.TryRemove(hpke, out state) && state.rng != IntPtr.Zero) + { + RandomFree(state.rng); + } Marshal.FreeHGlobal(hpke); } } From f6533c4d6e20a7895c2fab220f5b304407b51f1a Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 10 Apr 2026 14:49:43 -0700 Subject: [PATCH 5/5] Further peer review fixes --- .../CSharp/wolfCrypt-Test/wolfCrypt-Test.cs | 17 ++++++ wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs | 57 +++++++++++++++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs index 9cb58b87e31..96c09682269 100644 --- a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs +++ b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs @@ -996,6 +996,23 @@ private static void hpke_test(wolfcrypt.HpkeKem kem, } Console.WriteLine("HPKE convenience overload test PASSED."); + /* Empty plaintext round-trip - native API accepts ptSz == 0 */ + Console.WriteLine("Testing HpkeSealBase/OpenBase with empty plaintext..."); + byte[] emptyPt = new byte[0]; + byte[] encEmpty = wolfcrypt.HpkeSealBase(hpke, receiverKey, + info, aad, emptyPt, kem); + if (encEmpty == null) + { + throw new Exception("HpkeSealBase with empty plaintext failed"); + } + byte[] decEmpty = wolfcrypt.HpkeOpenBase(hpke, receiverKey, + encEmpty, info, aad, kem); + if (decEmpty == null || decEmpty.Length != 0) + { + throw new Exception("HpkeOpenBase with empty plaintext: round-trip failed"); + } + Console.WriteLine("Empty plaintext round-trip test PASSED."); + /* Negative test: tampered ciphertext should fail */ Console.WriteLine("Testing HpkeOpenBase with tampered ciphertext..."); byte[] tampered = (byte[])encCiphertext.Clone(); diff --git a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs index 77d03535816..bd66a965492 100644 --- a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs +++ b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs @@ -20,7 +20,10 @@ */ using System; +using System.Collections.Generic; +#if !WindowsCE using System.Collections.Concurrent; +#endif using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -3289,9 +3292,53 @@ private struct HpkeContextState public IntPtr rng; public HpkeKem kem; } + +#if WindowsCE + /* .NET Compact Framework / Windows CE does not provide + * System.Collections.Concurrent, so fall back to a plain Dictionary + * guarded by an explicit lock. */ + private static readonly Dictionary hpkeContexts = + new Dictionary(); + private static readonly object hpkeContextsLock = new object(); + + private static void HpkeContextStore(IntPtr hpke, HpkeContextState state) + { + lock (hpkeContextsLock) { hpkeContexts[hpke] = state; } + } + private static bool HpkeContextTryGet(IntPtr hpke, out HpkeContextState state) + { + lock (hpkeContextsLock) { return hpkeContexts.TryGetValue(hpke, out state); } + } + private static bool HpkeContextTryRemove(IntPtr hpke, out HpkeContextState state) + { + lock (hpkeContextsLock) + { + if (hpkeContexts.TryGetValue(hpke, out state)) + { + hpkeContexts.Remove(hpke); + return true; + } + return false; + } + } +#else private static readonly ConcurrentDictionary hpkeContexts = new ConcurrentDictionary(); + private static void HpkeContextStore(IntPtr hpke, HpkeContextState state) + { + hpkeContexts[hpke] = state; + } + private static bool HpkeContextTryGet(IntPtr hpke, out HpkeContextState state) + { + return hpkeContexts.TryGetValue(hpke, out state); + } + private static bool HpkeContextTryRemove(IntPtr hpke, out HpkeContextState state) + { + return hpkeContexts.TryRemove(hpke, out state); + } +#endif + /// /// Get the enc (encapsulated key) length for a given KEM /// @@ -3355,7 +3402,7 @@ public static IntPtr HpkeInit(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) return IntPtr.Zero; } - hpkeContexts[hpke] = new HpkeContextState { rng = rng, kem = kem }; + HpkeContextStore(hpke, new HpkeContextState { rng = rng, kem = kem }); } catch (Exception e) { @@ -3392,7 +3439,7 @@ public static IntPtr HpkeGenerateKeyPair(IntPtr hpke) } HpkeContextState state; - if (!hpkeContexts.TryGetValue(hpke, out state) || state.rng == IntPtr.Zero) + if (!HpkeContextTryGet(hpke, out state) || state.rng == IntPtr.Zero) { log(ERROR_LOG, "HPKE generate keypair: no RNG associated with context"); return IntPtr.Zero; @@ -3527,7 +3574,7 @@ public static void HpkeFree(IntPtr hpke) if (hpke != IntPtr.Zero) { HpkeContextState state; - if (hpkeContexts.TryRemove(hpke, out state) && state.rng != IntPtr.Zero) + if (HpkeContextTryRemove(hpke, out state) && state.rng != IntPtr.Zero) { RandomFree(state.rng); } @@ -3558,7 +3605,9 @@ public static byte[] HpkeSealBase(IntPtr hpke, IntPtr ephemeralKey, IntPtr recei log(ERROR_LOG, "HPKE seal base: invalid parameter"); return null; } - if (plaintext == null || plaintext.Length == 0) + /* Native wc_HpkeSealBase only requires plaintext to be non-NULL; + * ptSz == 0 is valid (output is just the AEAD tag). */ + if (plaintext == null) { log(ERROR_LOG, "HPKE seal base: invalid plaintext"); return null;