mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-10-24 04:09:07 +03:00
343 lines
15 KiB
C#
343 lines
15 KiB
C#
/* ModuleTests.cs
|
|
*
|
|
* Copyright (C) 2006-2026 wolfSSL Inc.
|
|
*
|
|
* This file is part of wolfSSL.
|
|
*
|
|
* wolfSSL is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* wolfSSL is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
|
*/
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using System.Threading;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace wolfSSL.CSharp.Fips.Test
|
|
{
|
|
internal static class ModuleTests
|
|
{
|
|
public static void Run()
|
|
{
|
|
T.Section("Binding audit");
|
|
|
|
T.Run("every libwolfssl binding in the assembly targets a _fips entry point", () =>
|
|
{
|
|
var all = typeof(FipsModule).Assembly.GetTypes()
|
|
.SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public |
|
|
BindingFlags.DeclaredOnly))
|
|
.Select(m => (m, a: m.GetCustomAttribute<DllImportAttribute>()))
|
|
.Where(x => x.a != null).ToList();
|
|
var bad = all.Where(x => x.a!.Value != "wolfssl_csharp_fips" &&
|
|
!(x.a.EntryPoint ?? x.m.Name).EndsWith("_fips"))
|
|
.Select(x => x.m.DeclaringType!.Name + "." + x.m.Name).ToList();
|
|
T.True(all.Count > 100, "found " + all.Count + " P/Invoke methods");
|
|
T.True(all.All(x => x.m.DeclaringType!.Name == "Native"), "P/Invoke outside Native");
|
|
T.True(bad.Count == 0, "non-_fips bindings: " + string.Join(", ", bad));
|
|
});
|
|
|
|
T.Section("Module status");
|
|
|
|
T.Run("Initialize succeeds (POST and in-core integrity passed)", () =>
|
|
{
|
|
FipsModule.Initialize();
|
|
});
|
|
|
|
T.Run("status is 0 and mode is Normal", () =>
|
|
{
|
|
T.Equal(0, FipsModule.Status, "status");
|
|
T.Equal(FipsMode.Normal, FipsModule.Mode, "mode");
|
|
T.True(FipsModule.IsOperational, "IsOperational");
|
|
});
|
|
|
|
T.Run("module version is v5.2.x", () =>
|
|
{
|
|
string? v = FipsModule.Version;
|
|
Console.WriteLine(" version: " + v);
|
|
T.True(v != null && v.Contains("5.2"), "version string: " + v);
|
|
});
|
|
|
|
T.Run("computed core hash is cleared on an operational module", () =>
|
|
{
|
|
T.Equal("", FipsModule.CoreHash ?? "", "CoreHash");
|
|
});
|
|
|
|
/* the native call always returns 0; IntegrityTest returns the
|
|
* status afterwards, so this can fail */
|
|
T.Run("on-demand integrity test passes", () =>
|
|
{
|
|
T.Equal(0, FipsModule.IntegrityTest(), "IntegrityTest (module status)");
|
|
T.Equal(FipsMode.Normal, FipsModule.Mode, "mode");
|
|
T.True(FipsModule.IsOperational, "operational");
|
|
});
|
|
|
|
T.Section("Conditional algorithm self-tests (CASTs)");
|
|
|
|
T.Run("RunAllCasts returns 0", () =>
|
|
{
|
|
T.Equal(0, FipsModule.RunAllCasts(), "RunAllCasts");
|
|
});
|
|
|
|
/* Module behavior: RunCast(EccCdh) returns 0 but leaves the
|
|
* CAST state at Processing (identical from C). The CAST result
|
|
* is the return code, so assert that and require the state to
|
|
* not be Failure. */
|
|
foreach (FipsCast c in Enum.GetValues<FipsCast>())
|
|
{
|
|
FipsCast cast = c;
|
|
T.Run("CAST " + cast + " passes", () =>
|
|
{
|
|
T.Equal(0, FipsModule.RunCast(cast), "RunCast");
|
|
FipsCastState st = FipsModule.GetCastState(cast);
|
|
T.True(st != FipsCastState.Failure, "state " + st);
|
|
});
|
|
}
|
|
|
|
/* wc_RunCast_fips writes its state array before checking the
|
|
* id, so these must never reach the module */
|
|
T.Run("RunCast and GetCastState refuse out-of-range CAST ids", () =>
|
|
{
|
|
foreach (int id in new[] { -1, FipsModule.CastCount, int.MinValue, int.MaxValue })
|
|
{
|
|
bool threw = false, threwState = false;
|
|
try { FipsModule.RunCast((FipsCast)id); } catch (ArgumentOutOfRangeException) { threw = true; }
|
|
try { FipsModule.GetCastState((FipsCast)id); } catch (ArgumentOutOfRangeException) { threwState = true; }
|
|
T.True(threw, "RunCast: CAST id " + id + " accepted");
|
|
T.True(threwState, "GetCastState: CAST id " + id + " accepted");
|
|
}
|
|
T.True(FipsModule.IsOperational, "operational");
|
|
});
|
|
|
|
/* Asks the module: the last CAST id is valid and the next is out
|
|
* of range (wc_GetCastStatus_fips returns -1). */
|
|
T.Run("CAST enum matches the module's CAST count", () =>
|
|
{
|
|
T.True(Native.wc_GetCastStatus_fips(FipsModule.CastCount - 1) >= 0, "last CAST id rejected");
|
|
T.Equal(-1, Native.wc_GetCastStatus_fips(FipsModule.CastCount), "id past the end");
|
|
T.Equal(FipsModule.CastCount, Enum.GetValues<FipsCast>().Length, "enum size");
|
|
});
|
|
|
|
T.Section("Callbacks and key export gate");
|
|
|
|
T.Run("failure callback registers", () =>
|
|
{
|
|
FipsModule.SetFailureCallback((ok, err, hash) =>
|
|
Console.WriteLine(" FIPS callback: ok=" + ok + " err=" + err + " hash=" + hash));
|
|
});
|
|
|
|
T.Run("OS seed source registers", () =>
|
|
{
|
|
FipsModule.UseOsSeed();
|
|
});
|
|
|
|
T.Run("Initialize keeps a custom seed source registered earlier", () =>
|
|
{
|
|
int calls = 0;
|
|
FipsModule.SetSeedCallback((os, seed, sz) =>
|
|
{
|
|
unsafe { RandomNumberGenerator.Fill(new Span<byte>((void*)seed, (int)sz)); }
|
|
Interlocked.Increment(ref calls);
|
|
return 0;
|
|
});
|
|
FipsModule.Initialize(); /* e.g. a second component starting up */
|
|
using (var r = new FipsRng())
|
|
{
|
|
r.Generate(16);
|
|
}
|
|
|
|
T.True(calls > 0, "Initialize replaced the custom seed source");
|
|
FipsModule.UseOsSeed();
|
|
int before = calls;
|
|
FipsModule.Initialize();
|
|
using (var r = new FipsRng())
|
|
{
|
|
r.Generate(16);
|
|
}
|
|
|
|
T.Equal(before, calls, "custom callback still registered after UseOsSeed");
|
|
});
|
|
|
|
T.Run("custom seed callback is used, then the OS source restored", () =>
|
|
{
|
|
int calls = 0;
|
|
FipsModule.SetSeedCallback((os, seed, sz) =>
|
|
{
|
|
unsafe { RandomNumberGenerator.Fill(new Span<byte>((void*)seed, (int)sz)); }
|
|
Interlocked.Increment(ref calls);
|
|
return 0;
|
|
});
|
|
using (var r = new FipsRng())
|
|
{
|
|
r.Generate(16);
|
|
}
|
|
|
|
T.True(calls > 0, "seed callback not called");
|
|
FipsModule.UseOsSeed();
|
|
int before = calls;
|
|
using (var r = new FipsRng())
|
|
{
|
|
r.Generate(16);
|
|
}
|
|
|
|
T.Equal(before, calls, "custom callback still registered");
|
|
});
|
|
|
|
/* Objects used only for their final call must stay alive while
|
|
* the module runs (SafeHandle); a GC with finalization runs in
|
|
* parallel to provoke early collection. */
|
|
T.Run("native state survives GC during calls on last-use objects", () =>
|
|
{
|
|
using var stop = new CancellationTokenSource();
|
|
var gc = Task.Run(() =>
|
|
{
|
|
while (!stop.IsCancellationRequested)
|
|
{
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
}
|
|
});
|
|
try
|
|
{
|
|
byte[] key = new byte[16], iv = new byte[16], msg = new byte[4096];
|
|
byte[] expected = FipsAes.CreateCbc(key, iv, true).Transform(msg);
|
|
for (int i = 0; i < 2000; i++)
|
|
{
|
|
T.Equal(32, new FipsRng().Generate(32).Length, "rng");
|
|
T.Bytes(expected, FipsAes.CreateCbc(key, iv, true).Transform(msg), "cbc");
|
|
T.Equal(32, FipsHash.Compute(FipsHashType.Sha256, msg).Length, "sha");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
stop.Cancel();
|
|
gc.Wait();
|
|
}
|
|
});
|
|
|
|
T.Run("seed callback: null refused, replacement and a throwing callback are safe", () =>
|
|
{
|
|
bool threw = false;
|
|
try { FipsModule.SetSeedCallback(null!); } catch (ArgumentNullException) { threw = true; }
|
|
T.True(threw, "null accepted");
|
|
int a = 0, b = 0;
|
|
FipsModule.SetSeedCallback((os, seed, sz) => { Interlocked.Increment(ref a); unsafe { RandomNumberGenerator.Fill(new Span<byte>((void*)seed, (int)sz)); } return 0; });
|
|
FipsModule.SetSeedCallback((os, seed, sz) => { Interlocked.Increment(ref b); unsafe { RandomNumberGenerator.Fill(new Span<byte>((void*)seed, (int)sz)); } return 0; });
|
|
GC.Collect(); GC.WaitForPendingFinalizers();
|
|
using (var r = new FipsRng())
|
|
{
|
|
r.Generate(8);
|
|
}
|
|
|
|
T.True(b > 0 && a == 0, "replacement callback not used");
|
|
FipsModule.SetSeedCallback((os, seed, sz) => throw new InvalidOperationException("boom"));
|
|
bool failed = false;
|
|
try { new FipsRng().Dispose(); } catch (WolfCryptFipsException) { failed = true; }
|
|
T.True(failed, "DRBG instantiated from a throwing seed callback");
|
|
FipsModule.UseOsSeed();
|
|
using (var r = new FipsRng())
|
|
{
|
|
r.Generate(8);
|
|
}
|
|
});
|
|
|
|
T.Run("private key read gate: one disable closes a nested gate", () =>
|
|
{
|
|
/* raise the module's per-thread counter directly */
|
|
T.Equal(0, Native.wolfCrypt_SetPrivateKeyReadEnable_fips(1, 0), "native enable");
|
|
T.Equal(0, Native.wolfCrypt_SetPrivateKeyReadEnable_fips(1, 0), "native enable");
|
|
T.Equal(0, Native.wolfCrypt_SetPrivateKeyReadEnable_fips(1, 0), "native enable");
|
|
T.True(Native.wolfCrypt_GetPrivateKeyReadEnable_fips(0) > 1, "counter did not nest");
|
|
FipsModule.SetPrivateKeyReadEnable(false);
|
|
T.True(!FipsModule.PrivateKeyReadEnabled, "gate still open");
|
|
});
|
|
|
|
T.Run("WOLFSSL_FIPS_LIB_DIR pointing at a directory without the libraries fails loudly", () =>
|
|
{
|
|
string empty = Path.Combine(Path.GetTempPath(), "wolfcrypt-fips-empty-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(empty);
|
|
try
|
|
{
|
|
var psi = new System.Diagnostics.ProcessStartInfo(Environment.ProcessPath!)
|
|
{
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false
|
|
};
|
|
if (Path.GetFileNameWithoutExtension(Environment.ProcessPath!) == "dotnet")
|
|
{
|
|
psi.ArgumentList.Add(typeof(ModuleTests).Assembly.Location);
|
|
}
|
|
|
|
psi.ArgumentList.Add("--probe-load");
|
|
psi.Environment["WOLFSSL_FIPS_LIB_DIR"] = empty;
|
|
using var p = System.Diagnostics.Process.Start(psi)!;
|
|
var o = p.StandardOutput.ReadToEndAsync(); var e = p.StandardError.ReadToEndAsync();
|
|
if (!p.WaitForExit(120_000))
|
|
{
|
|
p.Kill(entireProcessTree: true);
|
|
p.WaitForExit();
|
|
throw new Exception("probe-load child timed out");
|
|
}
|
|
string all = o.Result + e.Result;
|
|
T.True(p.ExitCode != 0 && all.Contains("could not be loaded from"), "fell back or loaded: " + all.Trim());
|
|
}
|
|
finally { Directory.Delete(empty); }
|
|
});
|
|
|
|
T.Run("private key read gate toggles", () =>
|
|
{
|
|
FipsModule.SetPrivateKeyReadEnable(true);
|
|
T.True(FipsModule.PrivateKeyReadEnabled, "enabled");
|
|
FipsModule.SetPrivateKeyReadEnable(false);
|
|
T.True(!FipsModule.PrivateKeyReadEnabled, "disabled");
|
|
});
|
|
|
|
T.Run("size helper is bound to the loaded libwolfssl binary", () =>
|
|
{
|
|
FipsModule.EnsureHelperMatchesModule();
|
|
Console.WriteLine(" libwolfssl: " + NativeLoader.WolfsslPath);
|
|
T.True(Native.SizeOf((int)FipsStructType.LibCrc) != 0, "no CRC fingerprint");
|
|
});
|
|
|
|
T.Run("POSIX cksum matches the cksum utility", () =>
|
|
{
|
|
T.Equal(930766865u, FipsModule.PosixCksum(System.Text.Encoding.ASCII.GetBytes("123456789")), "123456789");
|
|
T.Equal(4294967295u, FipsModule.PosixCksum(Array.Empty<byte>()), "empty");
|
|
T.Equal(2610763910u, FipsModule.PosixCksum(new byte[1000]), "1000 zeros");
|
|
});
|
|
|
|
T.Section("Native size helper");
|
|
|
|
foreach (string s in new[] { "Rng", "Aes", "Rsa", "Ecc", "Dh", "Sha", "Sha224",
|
|
"Sha256", "Sha384", "Sha512", "Sha3", "Hmac", "Cmac" })
|
|
{
|
|
string name = s;
|
|
T.Run("sizeof(" + name + ") available", () =>
|
|
{
|
|
Type st = typeof(FipsModule).Assembly.GetType("wolfSSL.CSharp.Fips.FipsStructType")!;
|
|
Type native = typeof(FipsModule).Assembly.GetType("wolfSSL.CSharp.Fips.Native")!;
|
|
int v = (int)Enum.Parse(st, name);
|
|
int sz = (int)native.GetMethod("SizeOf", BindingFlags.Static | BindingFlags.NonPublic)!
|
|
.Invoke(null, new object[] { v })!;
|
|
T.True(sz > 0, name + " size " + sz);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|