diff --git a/wrapper/CSharp/README.md b/wrapper/CSharp/README.md index ac15e0c338c..4323045b4ba 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,22 @@ 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. 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 +``` + ### Build and run the wolfSSL client/server test From the `wrapper/CSharp` directory (`cd wrapper/CSharp`): 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..96c09682269 100644 --- a/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs +++ b/wrapper/CSharp/wolfCrypt-Test/wolfCrypt-Test.cs @@ -880,6 +880,202 @@ private static void hash_test(uint hashType) } } /* END hash_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; + + 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."); + + /* 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, kem); + 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."); + + /* 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(); + 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)."); + + /* 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, + 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 + { + /* 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 +1137,15 @@ 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(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(); Console.WriteLine("\nAll tests completed successfully"); diff --git a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs index ae44f29fde3..bd66a965492 100644 --- a/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs +++ b/wrapper/CSharp/wolfSSL_CSharp/wolfCrypt.cs @@ -20,6 +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; @@ -361,6 +365,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 +408,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)] @@ -469,6 +481,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 +3241,569 @@ 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 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 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; + } + +#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 + /// + /// KEM identifier + /// 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; /* 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; + } + } + + /// + /// 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; + IntPtr rng = 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 */ + 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) + { + log(ERROR_LOG, "HPKE init failed " + ret + ": " + GetError(ret)); + 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; + } + + HpkeContextStore(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); + } + 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; + + try + { + if (hpke == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE generate keypair: invalid context"); + return IntPtr.Zero; + } + + HpkeContextState state; + if (!HpkeContextTryGet(hpke, out state) || state.rng == IntPtr.Zero) + { + log(ERROR_LOG, "HPKE generate keypair: no RNG associated with context"); + return IntPtr.Zero; + } + + int ret = wc_HpkeGenerateKeyPair(hpke, ref keypair, state.rng); + if (ret != 0) + { + log(ERROR_LOG, "HPKE generate keypair failed " + ret + ": " + GetError(ret)); + 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) + { + log(ERROR_LOG, "HPKE generate keypair exception " + e.ToString()); + keypair = IntPtr.Zero; + } + + 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 = (ushort)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) + { + HpkeContextState state; + if (HpkeContextTryRemove(hpke, out state) && state.rng != IntPtr.Zero) + { + RandomFree(state.rng); + } + 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; + } + /* 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; + } + + /* 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; + } + } + + /// + /// 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. + /// + /// 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 */ + if (ptLen < 0 || ptLen > int.MaxValue - HPKE_Nt) + { + log(ERROR_LOG, "HPKE open base: invalid ptLen"); + return null; + } + + int sealLen = ptLen + HPKE_Nt; + if (encCiphertext.Length < sealLen) + { + log(ERROR_LOG, "HPKE open base: encCiphertext too short for given ptLen"); + return null; + } + + 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]; + 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_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) + { + 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; + } + } + + /// + /// 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 */ + + /*********************************************************************** * Logging / Other **********************************************************************/ 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 @@ +