using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text.Json; using Org.BouncyCastle.Crypto.Prng; using Org.BouncyCastle.Security; using Entropy = XFESystem.NativeEntropy.NativeEntropy; if (args.Length != 2 || args[0] is not ("production" or "failure" or "missing-export")) throw new ArgumentException("AdapterChecks "); string mode = args[0], libraryPath = Path.GetFullPath(args[1]); nint library = NativeLibrary.Load(libraryPath); NativeLibrary.SetDllImportResolver(typeof(Entropy).Assembly, (name, _, _) => name == "xfe_pal" ? library : 0); var checks = new List(); bool passed = false; string? failure = null; void Check(string name, bool condition) { checks.Add(new { Name = name, Passed = condition }); if (!condition) throw new InvalidOperationException(name); } void Throws(string name, Action action) where T : Exception { bool thrown = false; try { action(); } catch (T) { thrown = true; } Check(name, thrown); } bool HasCryptoFailure(Exception error) { for (Exception? current = error; current is not null; current = current.InnerException) if (current is CryptographicException && current.Message.Contains("native hardware entropy source failed", StringComparison.Ordinal)) return true; return false; } void BouncyCastleFailsClosed() { bool rejected = false; try { new SecureRandom().NextBytes(new byte[32]); } catch (Exception error) { rejected = HasCryptoFailure(error); } Check("PatchedBouncyCastleDefaultSeedingRejectsUnavailableHardware", rejected); } try { Check("AdapterHasNoBouncyCastleDependency", !typeof(Entropy).Assembly.GetReferencedAssemblies().Any(name => name.Name!.Contains("BouncyCastle", StringComparison.Ordinal))); Check("PrivateBouncyCastleReferencesNativeAdapter", typeof(SecureRandom).Assembly.GetReferencedAssemblies().Any(name => name.Name == "XFESystem.NativeEntropy")); Entropy.Fill(Span.Empty); Check("EmptySpanDoesNotRequireHardware", true); byte[] tooLarge = Enumerable.Repeat((byte)0x55, Entropy.MaximumRequestBytes + 1).ToArray(); Throws("OneMiBBoundRejectedBeforeNativeAccess", () => Entropy.Fill(tooLarge)); Check("OversizedBufferRemainsUntouched", tooLarge.All(value => value == 0x55)); using var generator = Entropy.Create(); Check("CreateUsesOnlyTheExplicitHardwareSubclass", generator.GetType().Name.Contains("HardwareRandomNumberGenerator", StringComparison.Ordinal)); Throws("NullGeneratorRejected", () => Entropy.GetBytes(null!, new byte[4])); Throws("NullArrayRejected", () => Entropy.GetBytes(generator, (byte[])null!)); Throws("InvalidSliceRejected", () => Entropy.GetBytes(generator, new byte[4], 3, 2)); using var foreign = new ForeignRandom(); Throws("ExplicitForeignProviderRejected", () => Entropy.GetBytes(foreign, new byte[32])); Check("ForeignProviderNeverRan", foreign.Calls == 0); Entropy.Dispose(generator); Entropy.Dispose(generator); Throws("DisposedStaticArrayRejected", () => Entropy.GetBytes(generator, new byte[4])); Throws("DisposedStaticSliceRejected", () => Entropy.GetBytes(generator, new byte[8], 2, 4)); Throws("DisposedStaticSpanRejected", () => Entropy.GetBytes(generator, new byte[8].AsSpan(2, 4))); Throws("DisposedInstanceRejected", () => generator.GetBytes(new byte[4])); if (mode == "production") { byte[] first = new byte[32], second = new byte[32]; Entropy.Fill(first); Entropy.Fill(second); Check("ActualProductionCpuBytesAreNonconstant", first.Distinct().Count() > 1 && second.Distinct().Count() > 1); Check("SeparateActualHardwareCallsDiffer", !first.SequenceEqual(second)); using var actual = Entropy.Create(); byte[] data = Enumerable.Repeat((byte)0x55, 80).ToArray(); Entropy.GetBytes(actual, data, 16, 32); Check("OffsetPrefixAndSuffixUntouched", data.Take(16).All(value => value == 0x55) && data.Skip(48).All(value => value == 0x55)); Check("OffsetSliceUsesActualHardware", data.Skip(16).Take(32).Distinct().Count() > 1); Entropy.GetBytes(actual, data.AsSpan(8, 24)); Check("StaticSpanUsesActualHardware", data.Skip(8).Take(24).Distinct().Count() > 1); actual.GetBytes(data); Check("InstanceArrayUsesActualHardware", data.Distinct().Count() > 1); actual.GetBytes(data.AsSpan(8, 24)); Check("InstanceSpanUsesActualHardware", data.Skip(8).Take(24).Distinct().Count() > 1); actual.GetBytes(data, 8, 24); Check("InstanceSliceUsesActualHardware", data.Skip(8).Take(24).Distinct().Count() > 1); var random = new SecureRandom(); random.NextBytes(first); Check("PatchedBouncyCastleDefaultSecureRandomSeeding", first.Distinct().Count() > 1); var api = new CryptoApiRandomGenerator(); api.NextBytes(first); api.Dispose(); Check("PatchedBouncyCastleCryptoApiRandomGenerator", first.Distinct().Count() > 1); byte[] seed = new CryptoApiEntropySourceProvider().Get(256).GetEntropy(); Check("PatchedBouncyCastleCryptoApiEntropySourceProvider", seed.Length == 32 && seed.Distinct().Count() > 1); SecureRandom.GetInstance("SHA256PRNG", true).NextBytes(second); Check("PatchedBouncyCastleAutoSeededSha256Prng", second.Distinct().Count() > 1); } else { byte[] data = Enumerable.Repeat((byte)0x55, 64).ToArray(); if (mode == "missing-export") Throws("MissingNativeEntryIsRefused", () => Entropy.Fill(data.AsSpan(8, 32))); else Throws("EveryNonSuccessNativeResultIsRefused", () => Entropy.Fill(data.AsSpan(8, 32))); Check("FailureClearsWholeDestinationSpan", data.Skip(8).Take(32).All(value => value == 0)); Check("FailureDoesNotTouchOutsideDestinationSpan", data.Take(8).Concat(data.Skip(40)).All(value => value == 0x55)); if (mode == "failure") BouncyCastleFailsClosed(); } passed = true; } catch (Exception error) { failure = error.ToString(); } Console.WriteLine(JsonSerializer.Serialize(new { Kind = "NativeEntropyAdapterHostChecks", Passed = passed, Mode = mode, HardwareEntropyProof = mode == "production", FailureOnlyTestProvider = mode == "failure", GuestExecutionVerified = false, GuestTlsVerified = false, EntropyQualityCertified = false, NativeLibrary = libraryPath, Checks = checks, Failure = failure })); return passed ? 0 : 1; sealed class ForeignRandom : RandomNumberGenerator { public int Calls { get; private set; } public override void GetBytes(byte[] data) { Calls++; throw new InvalidOperationException("A foreign source must never run."); } }