using System.Diagnostics; using System.Collections; using System.Reflection; using System.Runtime; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using Opc.Ua; using Opc.Ua.Configuration; using Opc.Ua.Security.Certificates; internal static class ReloadControl { const string Uri = "urn:localhost:NativeRetention.Control"; const int Repetitions = 200; static readonly byte[] Challenge = Encoding.UTF8.GetBytes("native-retention-diagnostic-only"); static string Root = ""; static string Hash(Certificate cert) => Convert.ToHexString(SHA256.HashData(cert.RawData)).ToLowerInvariant(); static void Require(bool yes, string message) { if (!yes) throw new InvalidOperationException(message); } static ApplicationConfiguration Configuration(string pki, ITelemetryContext telemetry) => new(telemetry) { ApplicationName = "NativeRetention.Control", ApplicationUri = Uri, ApplicationType = ApplicationType.Client, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "own"), SubjectName = "CN=NativeRetention.Control" }, TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "trusted") }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "issuers") }, RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "rejected") }, AutoAcceptUntrustedCertificates = false, }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15000 }, }; static async Task Open(string pki, ITelemetryContext telemetry, CancellationToken ct) { var app = new ApplicationInstance(telemetry) { ApplicationType = ApplicationType.Client, ApplicationName = "NativeRetention.Control", ApplicationConfiguration = Configuration(pki, telemetry) }; try { await app.ApplicationConfiguration!.ValidateAsync(ApplicationType.Client, ct); Require(await app.CheckApplicationInstanceCertificatesAsync(true, null, ct), "Normal application certificate check failed"); return app; } catch { await app.DisposeAsync(); throw; } } static bool ManagerDisposed(ICertificateManager manager) { Require(manager.GetType().FullName == "Opc.Ua.CertificateManager" && manager.GetType().Assembly.GetName().Version?.ToString() == "2.0.1.0", "Exact SDK diagnostic layout changed"); var field = manager.GetType().GetField("m_disposed", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); Require(field?.FieldType == typeof(bool), "Exact SDK disposal bool missing"); return (bool)field!.GetValue(manager)!; } static int RuntimeTemporaryKeychainCount() { var assembly = typeof(X509Certificate2).Assembly; Require(assembly.GetCustomAttribute()?.InformationalVersion == "10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa", "Exact runtime diagnostic build changed"); var type = assembly.GetType("System.Security.Cryptography.Apple.SafeTemporaryKeychainHandle", throwOnError: true)!; var field = type.GetField("s_lookup", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); Require(field is not null && field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>) && field.FieldType.GetGenericArguments()[0] == typeof(IntPtr) && field.FieldType.GetGenericArguments()[1] == type, "Exact runtime keychain dictionary layout changed"); var lookup = (IDictionary)(field!.GetValue(null) ?? throw new InvalidOperationException("Runtime keychain dictionary absent")); Require(Monitor.TryEnter(lookup, TimeSpan.FromSeconds(1)), "Runtime keychain monitor did not become available"); try { return lookup.Count; } finally { Monitor.Exit(lookup); } } static void ValidateKey(Certificate certificate, string expected, bool sign) { Require(certificate.HasPrivateKey && Hash(certificate) == expected, "Wrong/missing private certificate"); if (!sign) return; using var privateKey = certificate.GetRSAPrivateKey(); using var publicKey = certificate.GetRSAPublicKey(); Require(privateKey is not null && publicKey is not null, "Expected normal SDK RSA application certificate"); var signature = privateKey!.SignData(Challenge, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); Require(publicKey!.VerifyData(Challenge, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), "Private key mismatch"); } static async Task Stage(string mode, string stage, int operations, bool disposed, CancellationToken ct) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); await Task.Delay(250, ct); using var process = Process.GetCurrentProcess(); process.Refresh(); Console.WriteLine(JsonSerializer.Serialize(new { kind = "checkpoint", mode, stage, operations, processId = Environment.ProcessId, utc = DateTimeOffset.UtcNow, managerDisposed = disposed, managedBytes = GC.GetTotalMemory(false), rssBytes = process.WorkingSet64, privateBytes = process.PrivateMemorySize64, allocatedBytes = GC.GetTotalAllocatedBytes(), compiledMethods = JitInfo.GetCompiledMethodCount(), gc = new[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) }, certificateCoresCreated = Certificate.InstancesCreated, certificateCoresDisposed = Certificate.InstancesDisposed, certificateCoresOutstanding = Certificate.InstancesLeaked, runtimeTrackedTemporaryKeychains = RuntimeTemporaryKeychainCount() })); Console.Out.Flush(); while (!File.Exists(Path.Combine(Root, "continue-" + stage))) await Task.Delay(25, ct); } static async Task Arm(string mode, CancellationToken ct) { var telemetry = DefaultTelemetry.Create(_ => { }); var pki = Path.Combine(Root, "pki"); var before = Directory.GetFiles(pki, "*.der", SearchOption.AllDirectories).Where(p => p.Contains(Path.DirectorySeparatorChar + "own" + Path.DirectorySeparatorChar + "certs" + Path.DirectorySeparatorChar, StringComparison.Ordinal)) .Select(p => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(p)))).Order().ToArray(); Require(before.Length == 1, "Expected one copied own DER certificate"); ApplicationInstance? app = null; try { app = await Open(pki, telemetry, ct); var config = app.ApplicationConfiguration!; var manager = config.CertificateManager; string expected; using (var entry = manager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256)) { Require(entry is not null, "Normal initialization did not populate registry"); expected = Hash(entry!.Certificate); ValidateKey(entry.Certificate, expected, true); } await Stage(mode, "ready", 0, ManagerDisposed(manager), ct); for (var i = 1; i <= Repetitions; i++) { if (mode == "store") { using var certificate = await config.SecurityConfiguration.FindApplicationCertificateAsync( SecurityPolicies.Basic256Sha256, true, telemetry, ct).WaitAsync(ct); Require(certificate is not null, "Configured-store acquisition failed"); ValidateKey(certificate!, expected, i is 1 or Repetitions); } else { using var entry = manager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256); Require(entry is not null, "Registry acquisition failed"); ValidateKey(entry!.Certificate, expected, i is 1 or Repetitions); } if (i is 1 or Repetitions) await Stage(mode, i == 1 ? "first" : "repeated", i, ManagerDisposed(manager), ct); } await app.DisposeAsync().AsTask().WaitAsync(ct); app = null; await Stage(mode, "disposed", Repetitions, ManagerDisposed(manager), ct); await Task.Delay(TimeSpan.FromSeconds(5), ct); await Stage(mode, "idle", Repetitions, ManagerDisposed(manager), ct); // Preserve managed and native post-disposal observations before any ownership assertion rejects the arm. Require(ManagerDisposed(manager), "Application manager not disposed"); Require(Certificate.InstancesLeaked == 0, "SDK certificate cores remain after owner disposal"); Require(RuntimeTemporaryKeychainCount() == 0, "Runtime tracked temporary keychain owners remain after application disposal"); var after = Directory.GetFiles(pki, "*.der", SearchOption.AllDirectories).Where(p => p.Contains(Path.DirectorySeparatorChar + "own" + Path.DirectorySeparatorChar + "certs" + Path.DirectorySeparatorChar, StringComparison.Ordinal)) .Select(p => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(p)))).Order().ToArray(); Require(after.SequenceEqual(before), "Own application certificate changed"); Console.WriteLine(JsonSerializer.Serialize(new { kind = "complete", mode, operations = Repetitions, sameLeafSha256 = expected, certificateUnchanged = true, managerDisposed = true, certificateCoresOutstanding = Certificate.InstancesLeaked, fullSoakGate = false })); } finally { if (app is not null) await app.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(10)); } } public static async Task Main(string[] args) { Root = Path.GetFullPath(args[1]); Directory.CreateDirectory(Root); using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(180)); try { Require(OperatingSystem.IsMacOS(), "This control requires macOS native inventory"); Require(Environment.Version.ToString() == "10.0.14", "Matched runtime must be .NET 10.0.14"); if (args[0] == "seed") { await using var app = await Open(Path.Combine(Root, "pki"), DefaultTelemetry.Create(_ => { }), deadline.Token); using var entry = app.ApplicationConfiguration!.CertificateManager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256); Require(entry is not null, "Seed registry not populated"); Console.WriteLine(JsonSerializer.Serialize(new { kind = "seed", leafSha256 = Hash(entry!.Certificate), runtime = Environment.Version.ToString(), identifierPublicProperties = typeof(CertificateIdentifier).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => p.Name).Order().ToArray(), identifierPublicConstructors = typeof(CertificateIdentifier).GetConstructors().Select(c => c.ToString()).ToArray(), cryptographyRuntime = new { file = typeof(X509Certificate2).Assembly.Location, informationalVersion = typeof(X509Certificate2).Assembly.GetCustomAttribute()?.InformationalVersion, sha256 = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(typeof(X509Certificate2).Assembly.Location))).ToLowerInvariant() } })); return 0; } Require(args[0] is "store" or "registry", "Unknown control mode"); await Arm(args[0], deadline.Token); return 0; } catch (Exception error) { Console.WriteLine(JsonSerializer.Serialize(new { kind = "failed", errorType = error.GetType().FullName, message = error.Message })); return 1; } } }