mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-07-05 12:00:51 +02:00
Merge pull request #10171 from dgarske/hpke_csharp
Add HPKE (RFC 9180) C# wrapper
This commit is contained in:
@@ -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`):
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
#define WOLFSSL_SHA512
|
||||
|
||||
#define HAVE_HKDF
|
||||
#define HAVE_HPKE
|
||||
|
||||
#undef NO_DH
|
||||
#define HAVE_PUBLIC_FFDHE
|
||||
|
||||
@@ -880,6 +880,202 @@ public class wolfCrypt_Test_CSharp
|
||||
}
|
||||
} /* 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 class wolfCrypt_Test_CSharp
|
||||
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");
|
||||
|
||||
@@ -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 @@ namespace wolfSSL.CSharp
|
||||
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 @@ namespace wolfSSL.CSharp
|
||||
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 @@ namespace wolfSSL.CSharp
|
||||
#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 @@ namespace wolfSSL.CSharp
|
||||
/* 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 reuses 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<IntPtr, HpkeContextState> hpkeContexts =
|
||||
new Dictionary<IntPtr, HpkeContextState>();
|
||||
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<IntPtr, HpkeContextState> hpkeContexts =
|
||||
new ConcurrentDictionary<IntPtr, HpkeContextState>();
|
||||
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Get the enc (encapsulated key) length for a given KEM
|
||||
/// </summary>
|
||||
/// <param name="kem">KEM identifier</param>
|
||||
/// <returns>Length in bytes</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocate and initialize an HPKE context
|
||||
/// </summary>
|
||||
/// <param name="kem">KEM algorithm identifier</param>
|
||||
/// <param name="kdf">KDF algorithm identifier</param>
|
||||
/// <param name="aead">AEAD algorithm identifier</param>
|
||||
/// <returns>Pointer to allocated Hpke context or IntPtr.Zero on failure</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new HPKE keypair
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <returns>Pointer to keypair or IntPtr.Zero on failure</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the public key to bytes
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <param name="keypair">Keypair from HpkeGenerateKeyPair()</param>
|
||||
/// <returns>Serialized public key bytes or null on failure</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a public key from bytes
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <param name="pubKeyBytes">Serialized public key bytes</param>
|
||||
/// <returns>Pointer to keypair or IntPtr.Zero on failure</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Free a keypair created by HpkeGenerateKeyPair or HpkeDeserializePublicKey
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <param name="keypair">Keypair to free</param>
|
||||
/// <param name="kem">KEM used when the keypair was created</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Free an HPKE context allocated by HpkeInit
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context to free</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <param name="ephemeralKey">Ephemeral keypair for sender</param>
|
||||
/// <param name="receiverKey">Receiver public key</param>
|
||||
/// <param name="info">Info context bytes (can be null)</param>
|
||||
/// <param name="aad">Additional authenticated data (can be null)</param>
|
||||
/// <param name="plaintext">Plaintext to encrypt</param>
|
||||
/// <returns>enc||ciphertext byte array or null on failure</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <param name="receiverKey">Receiver public key</param>
|
||||
/// <param name="info">Info context bytes (can be null)</param>
|
||||
/// <param name="aad">Additional authenticated data (can be null)</param>
|
||||
/// <param name="plaintext">Plaintext to encrypt</param>
|
||||
/// <param name="kem">KEM used (needed to free the ephemeral key)</param>
|
||||
/// <returns>enc||ciphertext byte array or null on failure</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SingleShot open (decrypt) using HPKE Base mode.
|
||||
/// Takes the full enc||ciphertext blob returned by HpkeSealBase.
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <param name="receiverKey">Receiver private keypair</param>
|
||||
/// <param name="encCiphertext">enc||ciphertext blob from HpkeSealBase()</param>
|
||||
/// <param name="info">Info context bytes (can be null)</param>
|
||||
/// <param name="aad">Additional authenticated data (can be null)</param>
|
||||
/// <param name="ptLen">Expected plaintext length</param>
|
||||
/// <returns>Decrypted plaintext byte array or null on failure</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="hpke">HPKE context from HpkeInit()</param>
|
||||
/// <param name="receiverKey">Receiver private keypair</param>
|
||||
/// <param name="encCiphertext">enc||ciphertext blob from HpkeSealBase()</param>
|
||||
/// <param name="info">Info context bytes (can be null)</param>
|
||||
/// <param name="aad">Additional authenticated data (can be null)</param>
|
||||
/// <param name="kem">KEM used (to derive enc length)</param>
|
||||
/// <returns>Decrypted plaintext byte array or null on failure</returns>
|
||||
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
|
||||
**********************************************************************/
|
||||
|
||||
@@ -317,6 +317,7 @@
|
||||
<ClCompile Include="..\..\wolfcrypt\src\ge_low_mem.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\ge_operations.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\hash.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\hpke.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\hmac.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\integer.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\kdf.c" />
|
||||
@@ -332,6 +333,7 @@
|
||||
<ClCompile Include="..\..\wolfcrypt\src\poly1305.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\pwdbased.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\random.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\rng_bank.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\rc2.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\ripemd.c" />
|
||||
<ClCompile Include="..\..\wolfcrypt\src\rsa.c" />
|
||||
|
||||
Reference in New Issue
Block a user