using Opc.Ua; using Opc.Ua.Configuration; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters; using ZB.MOM.WW.OtOpcUa.Client.Shared.Models; namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Adapters; /// Uses the real configuration factory so fake sessions cannot conceal SDK initialization failures. public sealed class DefaultApplicationConfigurationFactoryTests { [Theory] [InlineData(false)] [InlineData(true)] public async Task New_certificates_use_the_default_lifetime_and_existing_private_keys_are_reused(bool seedLongerCertificate) { var ct = TestContext.Current.CancellationToken; var root = Path.Combine(Path.GetTempPath(), $"otopcua-certificate-lifetime-{Guid.NewGuid():N}"); var settings = new ConnectionSettings { EndpointUrl = "opc.tcp://localhost:4840", CertificateStorePath = root, SecurityMode = SecurityMode.None, AutoAcceptCertificates = false, }; var factory = new DefaultApplicationConfigurationFactory(); ApplicationConfiguration? current = null; try { if (seedLongerCertificate) { var seed = await factory.CreateAsync(settings, ct); await using var application = new ApplicationInstance(DefaultTelemetry.Create(_ => { })) { ApplicationConfiguration = seed }; (await application.CheckApplicationInstanceCertificatesAsync(true, lifeTimeInMonths: 60, ct)).ShouldBeTrue(); } settings.SecurityMode = SecurityMode.SignAndEncrypt; current = await factory.CreateAsync(settings, ct); var certPath = Directory.GetFiles(Path.Combine(root, "own", "certs"), "*.der").ShouldHaveSingleItem(); var privatePath = Directory.GetFiles(Path.Combine(root, "own", "private"), "*.pfx").ShouldHaveSingleItem(); using var certificate = X509CertificateLoader.LoadCertificateFromFile(certPath); using var privateCertificate = X509CertificateLoader.LoadPkcs12FromFile(privatePath, null, OperatingSystem.IsMacOS() ? X509KeyStorageFlags.DefaultKeySet : X509KeyStorageFlags.EphemeralKeySet); privateCertificate.HasPrivateKey.ShouldBeTrue(); certificate.Thumbprint.ShouldBe(privateCertificate.Thumbprint); using var rsa = certificate.GetRSAPublicKey(); rsa.ShouldNotBeNull().KeySize.ShouldBeGreaterThanOrEqualTo(2048); if (seedLongerCertificate) certificate.NotAfter.ToUniversalTime().ShouldBeGreaterThan(DateTime.UtcNow.AddYears(4)); else { certificate.NotAfter.ToUniversalTime().ShouldBeGreaterThan(DateTime.UtcNow.AddMonths(CertificateFactory.DefaultLifeTime - 1)); certificate.NotAfter.ToUniversalTime().ShouldBeLessThan(DateTime.UtcNow.AddMonths(CertificateFactory.DefaultLifeTime).AddDays(2)); } var originalKeyHash = SHA256.HashData(await File.ReadAllBytesAsync(privatePath, ct)); (current.CertificateManager as IDisposable)?.Dispose(); current = null; current = await factory.CreateAsync(settings, ct); using var reused = X509CertificateLoader.LoadCertificateFromFile(certPath); reused.Thumbprint.ShouldBe(certificate.Thumbprint); SHA256.HashData(await File.ReadAllBytesAsync(privatePath, ct)).ShouldBe(originalKeyHash, "reinitializing with a shorter creation default must not rotate an existing certificate or private key"); Directory.GetFiles(Path.Combine(root, "own", "certs"), "*.der").Length.ShouldBe(1); } finally { (current?.CertificateManager as IDisposable)?.Dispose(); if (Directory.Exists(root)) Directory.Delete(root, true); } } [Theory] [InlineData(SecurityMode.None)] [InlineData(SecurityMode.SignAndEncrypt)] public async Task Real_configuration_initializes_its_certificate_manager(SecurityMode mode) { var root = Path.Combine(Path.GetTempPath(), $"otopcua-real-config-{Guid.NewGuid():N}"); ApplicationConfiguration? config = null; try { config = await new DefaultApplicationConfigurationFactory().CreateAsync(new ConnectionSettings { EndpointUrl = "opc.tcp://localhost:4840", CertificateStorePath = root, SecurityMode = mode, AutoAcceptCertificates = false, }, TestContext.Current.CancellationToken); config.CertificateManager.ShouldNotBeNull(); config.SecurityConfiguration.AutoAcceptUntrustedCertificates.ShouldBeFalse(); if (mode != SecurityMode.None) Directory.EnumerateFiles(Path.Combine(root, "own"), "*", SearchOption.AllDirectories).ShouldNotBeEmpty(); } finally { (config?.CertificateManager as IDisposable)?.Dispose(); if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } }