/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ // CA2007: tests run without a SynchronizationContext; ConfigureAwait(false) // adds noise without a behavioural benefit. Disabled file-level for the suite. #pragma warning disable CA2007 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Security.Certificates; using Opc.Ua.Tests; #if NETCOREAPP2_1_OR_GREATER using System.Runtime.InteropServices; #endif namespace Opc.Ua.Configuration.Tests { /// /// Tests for the ApplicationInstance configuration, certificate provisioning, /// and application-configuration loading. /// [TestFixture] [Category("ApplicationInstance")] [SetCulture("en-us")] [SetUICulture("en-us")] [Parallelizable] public class ApplicationInstanceTests { public const string ApplicationName = "UA Configuration Test"; public const string ApplicationUri = "urn:localhost:opcfoundation.org:ConfigurationTest"; public const string ProductUri = "http://opcfoundation.org/UA/ConfigurationTest"; public const string SubjectName = "CN=UA Configuration Test, O=OPC Foundation, C=US, S=Arizona"; public const string EndpointUrl = "opc.tcp://localhost:51000"; /// /// Test setup. /// [SetUp] public void SetUp() { // pki directory root for test runs. m_pkiRoot = Path.GetTempPath() + Path.GetRandomFileName() + Path.DirectorySeparatorChar; } /// /// Test teardown. /// [TearDown] public void TearDown() { try { // pki directory root for test runs. Directory.Delete(m_pkiRoot, true); } catch { } } /// /// Load a file configuration. /// [Test] public async Task TestFileConfigAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); string configPath = Utils.GetAbsoluteFilePath( "Opc.Ua.Configuration.Tests.Config.xml", checkCurrentDirectory: true, createAlways: false); Assert.That(configPath, Is.Not.Null); ApplicationConfiguration applicationConfiguration = await applicationInstance .LoadApplicationConfigurationAsync(configPath, true) .ConfigureAwait(false); Assert.That(applicationConfiguration, Is.Not.Null); bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } [Test] public async Task TestNoFileConfigAsClientAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } [TestCase(false, false)] [TestCase(true, false)] [TestCase(true, true)] public async Task ScopedStoreProvisioningCreatesAndReusesIdentityAsync(bool customProvider, bool customTrust) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var provider = new ProvisioningStoreProvider(); string? thumbprint = null; for (int iteration = 0; iteration < 2; iteration++) { var application = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (application.ConfigureAwait(false)) { var builder = application.Build(ApplicationUri, ProductUri).AsClient() .AddSecurityConfiguration(ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot), m_pkiRoot); SecurityConfiguration security = application.ApplicationConfiguration!.SecurityConfiguration; if (customProvider) { foreach (CertificateIdentifier identifier in security.ApplicationCertificates) { identifier.StoreType = provider.StoreTypeName; } } if (customTrust) { security.TrustedPeerCertificates.StoreType = provider.StoreTypeName; security.TrustedIssuerCertificates.StoreType = provider.StoreTypeName; } security.AddAppCertToTrustedStore = true; application.ApplicationConfiguration.CertificateManager = CertificateManagerFactory.Create(security, telemetry, options => { if (customProvider) { options.AddStoreProvider(provider); } }); ApplicationConfiguration configuration = await builder.CreateAsync().ConfigureAwait(false); bool valid = await application.CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false); Assert.That(valid, Is.True); using CertificateEntry entry = configuration.CertificateManager.AcquireApplicationCertificateByType( ObjectTypeIds.RsaSha256ApplicationCertificateType)!; Assert.That(entry, Is.Not.Null); Assert.That(entry.Certificate.HasPrivateKey, Is.True); if (iteration == 0) { thumbprint = entry.Certificate.Thumbprint; } Assert.That(entry.Certificate.Thumbprint, Is.EqualTo(thumbprint)); using ICertificateStore trusted = configuration.CertificateManager.OpenTrustedStore(TrustListIdentifier.Peers); using CertificateCollection peers = await trusted.FindByThumbprintAsync(thumbprint!).ConfigureAwait(false); Assert.That(peers, Has.Count.EqualTo(1)); if (iteration == 1) { CertificateIdentifier identifier = security.ApplicationCertificates[0]; await application.DeleteApplicationInstanceCertificateAsync().ConfigureAwait(false); using ICertificateStore own = ((ICertificateStoreResolver)configuration.CertificateManager) .OpenCertificateStore(identifier.StorePath!, identifier.StoreType, false); using CertificateCollection remaining = await own.EnumerateAsync().ConfigureAwait(false); using CertificateCollection trustedAfterDelete = await trusted.FindByThumbprintAsync(thumbprint!) .ConfigureAwait(false); Assert.That(remaining, Is.Empty); Assert.That(trustedAfterDelete, Is.Empty); } } } Assert.That(provider.OpenCount, customProvider ? Is.GreaterThan(0) : Is.Zero); } [TestCase(null)] [TestCase("")] public async Task DeleteCertificateSkipsMissingTrustedStorePathAsync(string? trustedPath) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); using Certificate certificate = CertificateBuilder.Create(SubjectName).SetRSAKeySize(2048).CreateForRSA(); var identifier = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(m_pkiRoot, "own"), Thumbprint = certificate.Thumbprint }; await certificate.AddToStoreAsync(identifier.StoreType, identifier.StorePath, null, telemetry) .ConfigureAwait(false); using var manager = new CertificateManager(telemetry); var application = new ApplicationInstance(telemetry) { ApplicationConfiguration = new ApplicationConfiguration(telemetry) { CertificateManager = manager, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificates = [identifier], TrustedPeerCertificates = new CertificateTrustList { StorePath = trustedPath } } } }; await using (application.ConfigureAwait(false)) { await application.DeleteApplicationInstanceCertificateAsync().ConfigureAwait(false); using ICertificateStore own = CertificateIdentifierResolver.OpenStore(identifier, telemetry)!; using CertificateCollection remaining = await own!.EnumerateAsync().ConfigureAwait(false); Assert.That(remaining, Is.Empty); } } [Test] public async Task DeleteCertificateUsesConfiguredStoresForManagerWithoutResolverAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var application = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (application.ConfigureAwait(false)) { ApplicationConfiguration configuration = await application.Build(ApplicationUri, ProductUri).AsClient() .AddSecurityConfiguration(ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot), m_pkiRoot) .CreateAsync().ConfigureAwait(false); configuration.SecurityConfiguration.AddAppCertToTrustedStore = true; Assert.That(await application.CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false), Is.True); ICertificateManager originalManager = configuration.CertificateManager; using CertificateEntry entry = originalManager.AcquireApplicationCertificateByType( ObjectTypeIds.RsaSha256ApplicationCertificateType)!; var legacyManager = new Mock(MockBehavior.Strict); legacyManager.Setup(manager => manager.SnapshotApplicationCertificates()) .Returns(originalManager.SnapshotApplicationCertificates); configuration.CertificateManager = legacyManager.Object; try { await application.DeleteApplicationInstanceCertificateAsync().ConfigureAwait(false); CertificateIdentifier identifier = configuration.SecurityConfiguration.ApplicationCertificates[0]; using ICertificateStore own = CertificateIdentifierResolver.OpenStore(identifier, telemetry)!; using CertificateCollection remaining = await own!.EnumerateAsync().ConfigureAwait(false); using ICertificateStore trusted = configuration.SecurityConfiguration.TrustedPeerCertificates .OpenStore(telemetry); using CertificateCollection peers = await trusted.FindByThumbprintAsync(entry!.Certificate.Thumbprint) .ConfigureAwait(false); Assert.That(remaining, Is.Empty); Assert.That(peers, Is.Empty); legacyManager.Verify(manager => manager.OpenTrustedStore(It.IsAny()), Times.Never); } finally { configuration.CertificateManager = originalManager; } } } [TestCase(false)] [TestCase(true)] public async Task ScopedStoreProvisioningRejectsUnavailablePrivateKeyAsync(bool publicOnly) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var provider = new ProvisioningStoreProvider(); var application = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName, DisableCertificateAutoCreation = !publicOnly }; await using (application.ConfigureAwait(false)) { var builder = application.Build(ApplicationUri, ProductUri).AsClient() .AddSecurityConfiguration(ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot), m_pkiRoot); SecurityConfiguration security = application.ApplicationConfiguration!.SecurityConfiguration; CertificateIdentifier identifier = security.ApplicationCertificates[0]; identifier.StoreType = provider.StoreTypeName; var manager = CertificateManagerFactory.Create(security, telemetry, options => options.AddStoreProvider(provider)); application.ApplicationConfiguration.CertificateManager = manager; string? originalThumbprint = null; using (ICertificateStore store = manager.OpenCertificateStore(identifier.StorePath!, identifier.StoreType, false)) { if (publicOnly) { using Certificate keyPair = CertificateBuilder.Create(SubjectName).SetRSAKeySize(2048).CreateForRSA(); using var certificate = new Certificate(keyPair.RawData); originalThumbprint = certificate.Thumbprint; await store.AddAsync(certificate).ConfigureAwait(false); } } await builder.CreateAsync().ConfigureAwait(false); ServiceResultException? failure = null; try { await application.CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false); } catch (ServiceResultException exception) { failure = exception; } Assert.That(failure, Is.Not.Null); Assert.That(failure.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); using ICertificateStore own = manager.OpenCertificateStore(identifier.StorePath!, identifier.StoreType, false); using CertificateCollection remaining = await own.EnumerateAsync().ConfigureAwait(false); Assert.That(remaining, Has.Count.EqualTo(publicOnly ? 1 : 0)); if (publicOnly) { Assert.That(remaining[0].Thumbprint, Is.EqualTo(originalThumbprint)); Assert.That(remaining[0].HasPrivateKey, Is.False); } } } private sealed class ProvisioningStoreProvider : ICertificateStoreProvider { public string StoreTypeName => "ScopedProvisioningDirectory"; public int OpenCount { get; private set; } public bool SupportsStorePath(string storePath) => false; public ICertificateStore CreateStore(ITelemetryContext telemetry) { OpenCount++; return new DirectoryCertificateStore(false, telemetry); } } [Test] public async Task TestNoFileConfigRespectsMinimumKeySizeOnCreationAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); const ushort minimumKeySize = 4096; ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .SetMinimumCertificateKeySize(minimumKeySize) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); CertificateIdentifier certId = config.SecurityConfiguration.ApplicationCertificates[0]; using Certificate certificate = (await CertificateIdentifierResolver .LoadPrivateKeyAsync( certId, passwordProvider: config.SecurityConfiguration.CertificatePasswordProvider, config.ApplicationUri, telemetry) .ConfigureAwait(false))!; Assert.That(certificate, Is.Not.Null); Assert.That(X509Utils.GetPublicKeySize(certificate), Is.EqualTo(minimumKeySize)); } } [Test] public async Task TestBadApplicationInstanceAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); // no app name await using (var applicationInstance = new ApplicationInstance(telemetry)) { Assert.That(applicationInstance, Is.Not.Null); Assert.ThrowsAsync(async () => await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false)); } // discoveryserver can not be combined with client/server await using (var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName, ApplicationType = ApplicationType.DiscoveryServer }) { Assert.ThrowsAsync(async () => await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false)); Assert.ThrowsAsync(async () => await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false)); } // server overrides client settings await using (var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName, ApplicationType = ApplicationType.Client }) { ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false); Assert.That(applicationInstance.ApplicationType, Is.EqualTo(ApplicationType.Server)); } // client overrides server setting await using (var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName, ApplicationType = ApplicationType.Server }) { await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false); Assert.That(applicationInstance.ApplicationType, Is.EqualTo(ApplicationType.Client)); } // invalid sec policy testing await using (var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }) { // invalid use, use AddUnsecurePolicyNone instead Assert.ThrowsAsync(async () => await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddPolicy(MessageSecurityMode.None, SecurityPolicies.None) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false)); // invalid mix sign / none Assert.ThrowsAsync(async () => await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.None) .AddSecurityConfiguration(applicationCerts) .CreateAsync() .ConfigureAwait(false)); // invalid policy Assert.ThrowsAsync(async () => await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddPolicy(MessageSecurityMode.Sign, "123") .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false)); // invalid user token policy Assert.ThrowsAsync(async () => await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddUserTokenPolicy(null!) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false)); } } [Test] public async Task TestNoFileConfigAsServerMinimalAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .SetOperationTimeout(10000) .AsServer([EndpointUrl]) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } [Test] public async Task TestNoFileConfigAsServerMaximalAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .SetTransportQuotas(new TransportQuotas { OperationTimeout = 10000 }) .AsServer([EndpointUrl]) .AddSignPolicies() .AddSignAndEncryptPolicies() .AddUnsecurePolicyNone() .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic256) .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15) .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256) .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15) .AddUserTokenPolicy(UserTokenType.Anonymous) .AddUserTokenPolicy(UserTokenType.UserName) .AddUserTokenPolicy( new UserTokenPolicy(UserTokenType.Certificate) { SecurityPolicyUri = SecurityPolicies.Basic256Sha256 }) .SetDiagnosticsEnabled(true) .SetPublishingResolution(100) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .SetAddAppCertToTrustedStore(true) .SetAutoAcceptUntrustedCertificates(true) .SetMinimumCertificateKeySize(1024) .SetRejectSHA1SignedCertificates(false) .SetSendCertificateChain(true) .SetSuppressNonceValidationErrors(true) .SetRejectUnknownRevocationStatus(true) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } [Test] public async Task TestNoFileConfigAsClientAndServerAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .SetMaxBufferSize(32768) .AsServer([EndpointUrl]) .AddUnsecurePolicyNone() .AddSignPolicies() .AddSignAndEncryptPolicies() .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic256) .SetDiagnosticsEnabled(true) .AsClient() .AddSecurityConfiguration( applicationCerts, CertificateStoreType.Directory, CertificateStoreType.X509Store) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } /// /// Test case when app cert already exists or when new /// cert is created in X509Store. /// [Test] [Repeat(2)] public async Task TestNoFileConfigAsServerX509StoreAsync() { #if NETCOREAPP2_1_OR_GREATER // this test fails on macOS, ignore if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { Assert.Ignore("X509Store trust lists not supported on mac OS."); } #endif ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl]) .AddUnsecurePolicyNone() .AddSignAndEncryptPolicies() .AddUserTokenPolicy(UserTokenType.UserName) .AsClient() .SetDefaultSessionTimeout(10000) .AddSecurityConfiguration(applicationCerts, CertificateStoreType.X509Store) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); CertificateIdentifier applicationCertificate = applicationInstance .ApplicationConfiguration! .SecurityConfiguration .ApplicationCertificate!; bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); // Resolve the cert via the resolver since the identifier no // longer caches it. using Certificate appCert = (await CertificateIdentifierResolver .ResolveAsync( applicationCertificate, registry: null, needPrivateKey: false, applicationInstance.ApplicationConfiguration.ApplicationUri, telemetry) .ConfigureAwait(false))!; bool deleteAfterUse = appCert != null; Assert.That(certOK, Is.True); using ( ICertificateStore store = applicationInstance.ApplicationConfiguration.SecurityConfiguration .TrustedPeerCertificates .OpenStore(telemetry)) { // store public key in trusted store byte[] rawData = appCert!.RawData; using var publicKey = Certificate.FromRawData(rawData); await store.AddAsync(publicKey) .ConfigureAwait(false); } if (deleteAfterUse) { string thumbprint = appCert.Thumbprint; using (ICertificateStore store = CertificateIdentifierResolver .OpenStore(applicationCertificate, telemetry)!) { bool success = await store!.DeleteAsync(thumbprint).ConfigureAwait(false); Assert.That(success, Is.True); } using ( ICertificateStore store = applicationInstance.ApplicationConfiguration.SecurityConfiguration .TrustedPeerCertificates .OpenStore(telemetry)) { bool success = await store.DeleteAsync(thumbprint).ConfigureAwait(false); Assert.That(success, Is.True); } } } } [Test] public async Task TestNoFileConfigAsServerCustomAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer([EndpointUrl, "opc.https://localhost:51001"], s_alternateBaseAddresses) .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .SetAddAppCertToTrustedStore(true) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } public enum InvalidCertType { NoIssues, NoIssuer, Expired, IssuerExpired, NotYetValid, IssuerNotYetValid, KeySize1024, HostName } /// /// Test to verify that an existing cert with suppressible issues /// is not recreated/replaced. /// [Test] [TestCase(InvalidCertType.NoIssues, true, true)] [TestCase(InvalidCertType.NotYetValid, true, true)] [TestCase(InvalidCertType.Expired, true, true)] [TestCase(InvalidCertType.HostName, true, false)] [TestCase(InvalidCertType.HostName, false, true)] [TestCase(InvalidCertType.KeySize1024, true, false)] public async Task TestInvalidAppCertDoNotRecreateAsync( InvalidCertType certType, bool server, bool suppress) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); // pki directory root for test runs. string pkiRoot = Path.GetTempPath() + Path.GetRandomFileName() + Path.DirectorySeparatorChar; var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, pkiRoot); ApplicationConfiguration config; if (server) { config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer(s_baseAddresses) .AddSecurityConfiguration(applicationCerts, pkiRoot) .CreateAsync() .ConfigureAwait(false); } else { config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(applicationCerts, pkiRoot) .CreateAsync() .ConfigureAwait(false); } Assert.That(config, Is.Not.Null); CertificateIdentifier applicationCertificate = applicationInstance .ApplicationConfiguration! .SecurityConfiguration .ApplicationCertificate!; Assert.That(applicationCertificate!.Thumbprint, Is.Null.Or.Empty); Certificate? publicKey = null; using (Certificate testCert = CreateInvalidCert(certType)) { Assert.That(testCert, Is.Not.Null); Assert.That(testCert.HasPrivateKey, Is.True); await testCert.AddToStoreAsync( applicationCertificate.StoreType!, applicationCertificate.StorePath!, password: null, telemetry).ConfigureAwait(false); publicKey = Certificate.FromRawData(testCert.RawData); } using (publicKey) { if (suppress) { bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); Assert.That(applicationCertificate.Thumbprint, Is.EqualTo(publicKey.Thumbprint)); } else { ServiceResultException sre = Assert .ThrowsAsync(async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync( true) .ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); } } } } /// /// Test to verify that an existing cert with suppressible issues /// is not recreated/replaced. /// [Test] [TestCase(InvalidCertType.NoIssues, true, true)] [TestCase(InvalidCertType.NoIssuer, true, false)] [TestCase(InvalidCertType.NotYetValid, true, true)] [TestCase(InvalidCertType.Expired, true, true)] [TestCase(InvalidCertType.IssuerNotYetValid, true, true)] [TestCase(InvalidCertType.IssuerExpired, true, true)] [TestCase(InvalidCertType.HostName, true, false)] [TestCase(InvalidCertType.HostName, false, true)] //TODO [TestCase(InvalidCertType.KeySize1024, true, false)] public async Task TestInvalidAppCertChainDoNotRecreateAsync( InvalidCertType certType, bool server, bool suppress) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); // pki directory root for test runs. string pkiRoot = Path.GetTempPath() + Path.GetRandomFileName() + Path.DirectorySeparatorChar; var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, pkiRoot); ApplicationConfiguration config; if (server) { config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer(s_baseAddresses) .AddSecurityConfiguration(applicationCerts, pkiRoot) .CreateAsync() .ConfigureAwait(false); } else { config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(applicationCerts, pkiRoot) .CreateAsync() .ConfigureAwait(false); } Assert.That(config, Is.Not.Null); CertificateIdentifier applicationCertificate = applicationInstance .ApplicationConfiguration! .SecurityConfiguration .ApplicationCertificate!; Assert.That(applicationCertificate!.Thumbprint, Is.Null.Or.Empty); using CertificateCollection testCerts = CreateInvalidCertChain(certType); if (certType != InvalidCertType.NoIssuer) { using Certificate issuerCert = testCerts[1]; Assert.That(issuerCert, Is.Not.Null); Assert.That(issuerCert.HasPrivateKey, Is.False); await issuerCert.AddToStoreAsync( applicationInstance .ApplicationConfiguration .SecurityConfiguration .TrustedIssuerCertificates .StoreType!, applicationInstance .ApplicationConfiguration .SecurityConfiguration .TrustedIssuerCertificates .StorePath!, password: null, telemetry).ConfigureAwait(false); } Certificate? publicKey = null; using (Certificate testCert = testCerts[0]) { Assert.That(testCert, Is.Not.Null); Assert.That(testCert.HasPrivateKey, Is.True); await testCert.AddToStoreAsync( applicationCertificate.StoreType!, applicationCertificate.StorePath!, password: null, telemetry).ConfigureAwait(false); publicKey = Certificate.FromRawData(testCert.RawData); } using (publicKey) { if (suppress) { bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); Assert.That(applicationCertificate.Thumbprint, Is.EqualTo(publicKey.Thumbprint)); } else { ServiceResultException sre = Assert .ThrowsAsync(async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync( true) .ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); } } } } /// /// Tests that a supplied certificate is stored in the Trusted store of the Server after calling method AddOwnCertificateToTrustedStoreAsync /// [Test] public async Task TestAddOwnCertificateToTrustedStoreAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); //Arrange Application Instance var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { ApplicationConfiguration configuration = await applicationInstance .Build(ApplicationUri, ProductUri) .SetOperationTimeout(10000) .AsServer([EndpointUrl]) .AddSecurityConfiguration(SubjectName, m_pkiRoot) .CreateAsync() .ConfigureAwait(false); //Arrange cert DateTime notBefore = DateTime.Today.AddDays(-30); DateTime notAfter = DateTime.Today.AddDays(30); using Certificate cert = DefaultCertificateFactory.Instance.CreateCertificate(SubjectName) .SetNotBefore(notBefore) .SetNotAfter(notAfter) .SetCAConstraint(-1) .CreateForRSA(); //Act await applicationInstance .AddOwnCertificateToTrustedStoreAsync(cert, new CancellationToken()) .ConfigureAwait(false); using ICertificateStore store = configuration.SecurityConfiguration.TrustedPeerCertificates .OpenStore(telemetry); using CertificateCollection storedCertificates = await store .FindByThumbprintAsync(cert.Thumbprint) .ConfigureAwait(false); //Assert Assert.That(storedCertificates, Does.Contain(cert)); } } /// /// This tests that instantiating three application instances the second with a SubjectName /// being a substring of the first one's CN, but without containing the "CN=", failes due to the legacy fuzzy search /// matching the first certificate, which is invalid for the second application instance (different ApplicationUri /// The test shows 3 cases of creating application instances: /// 1 succeeds since there is no certificate identification collision (store is empty). /// 2 fails because in the process of finding the Application certificate the /// 1'st - AppCertificate (Wrong one) is selected due to the Fuzzy search, /// and further on the validation fails (different ApplicationUri, etc). /// 3 succeeds because in the process of finding the Application certificate the correct application certificate /// with correct subjectName "CN=UA, O=...." and ApplicationUri is searched for, no fuzzy search performed, /// none is found and a new one gets created, selected, further validations succeed. /// /// [Test] public async Task TestAddTwoAppCertificatesToTrustedStoreAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); string subjectName = SubjectName; //Arrange Application Instance var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { ApplicationConfiguration configuration = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfigurationStores(subjectName, $"{m_pkiRoot}/pki/own", $"{m_pkiRoot}/pki/trusted", $"{m_pkiRoot}/pki/issuer", $"{m_pkiRoot}/pki/rejected") .CreateAsync() .ConfigureAwait(false); Assert.DoesNotThrowAsync( async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false)); subjectName = "UA";// UA is a substring of the previous certificate SubjectName CN var applicationInstance2 = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance2.ConfigureAwait(false)) { ApplicationConfiguration configuration2 = await applicationInstance2 .Build(ApplicationUri + "2", ProductUri + "2") .AsClient() .AddSecurityConfigurationStores(subjectName, $"{m_pkiRoot}/pki/own", $"{m_pkiRoot}/pki/trusted", $"{m_pkiRoot}/pki/issuer", $"{m_pkiRoot}/pki/rejected") .CreateAsync() .ConfigureAwait(false); // Since the SubjectName is a substring of the first one's CN, // the matching algorithm will find the first certificate because a fuzzy match is done on the SubjectName when SubjectName does not contain CN=. // However, since the ApplicationUri is different, the certificate will be considered invalid ServiceResultException exception = Assert .ThrowsAsync(async () => await applicationInstance2.CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false)); Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); } subjectName = "CN=UA";// UA is a substring of the previous certificate SubjectName CN var applicationInstance3 = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance3.ConfigureAwait(false)) { ApplicationConfiguration configuration3 = await applicationInstance3 .Build(ApplicationUri + "3", ProductUri + "3") .AsClient() .AddSecurityConfigurationStores(subjectName, $"{m_pkiRoot}/pki/own", $"{m_pkiRoot}/pki/trusted", $"{m_pkiRoot}/pki/issuer", $"{m_pkiRoot}/pki/rejected") .CreateAsync() .ConfigureAwait(false); // Since the SubjectName contains CN=UA, the matching algorithm will not do a fuzzy match and will not find the first certificate. Assert.DoesNotThrowAsync( async () => await applicationInstance3.CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false)); } } } /// /// Test to verify that a new cert is not recreated/replaced if DisableCertificateAutoCreation is set. /// [Theory] public async Task TestDisableCertificateAutoCreationAsync( bool server, bool disableCertificateAutoCreation) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); // pki directory root for test runs. string pkiRoot = Path.GetTempPath() + Path.GetRandomFileName() + Path.DirectorySeparatorChar; var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName, DisableCertificateAutoCreation = disableCertificateAutoCreation }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); ApplicationConfiguration config; ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( SubjectName, CertificateStoreType.Directory, m_pkiRoot); if (server) { config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsServer(s_baseAddressesArray) .AddSecurityConfiguration(applicationCerts, pkiRoot) .CreateAsync() .ConfigureAwait(false); } else { config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(applicationCerts, pkiRoot) .CreateAsync() .ConfigureAwait(false); } Assert.That(config, Is.Not.Null); CertificateIdentifier applicationCertificate = applicationInstance .ApplicationConfiguration! .SecurityConfiguration .ApplicationCertificate!; Assert.That(applicationCertificate!.Thumbprint, Is.Null.Or.Empty); if (disableCertificateAutoCreation) { ServiceResultException sre = Assert .ThrowsAsync(async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); } else { bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } } /// /// Test that multiple certificates with different ApplicationUris throw an exception. /// Even though FindAsync might load certificates by SubjectName without checking ApplicationUri, /// the explicit validation against the configured ApplicationUri ensures all loaded certificates have the same URI. /// [Test] public async Task TestMultipleCertificatesDifferentUrisThrowsExceptionAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); // Create two certificates with different ApplicationUris const string uri1 = "urn:localhost:opcfoundation.org:App1"; const string uri2 = "urn:localhost:opcfoundation.org:App2"; using Certificate cert1 = DefaultCertificateFactory.Instance .CreateApplicationCertificate(uri1, ApplicationName, SubjectName, [Utils.GetHostName()]) .SetNotBefore(DateTime.Today.AddDays(-1)) .SetNotAfter(DateTime.Today.AddYears(1)) .CreateForRSA(); const string subjectName2 = "CN=UA Configuration Test 2, O=OPC Foundation, C=US, S=Arizona"; using Certificate cert2 = DefaultCertificateFactory.Instance .CreateApplicationCertificate(uri2, ApplicationName, subjectName2, [Utils.GetHostName()]) .SetNotBefore(DateTime.Today.AddDays(-1)) .SetNotAfter(DateTime.Today.AddYears(1)) .SetRSAKeySize(CertificateFactory.DefaultKeySize) .CreateForRSA(); // Save certificates to stores string certStorePath = m_pkiRoot + "own"; var certStoreIdentifier = new CertificateStoreIdentifier(certStorePath, CertificateStoreType.Directory, false); using (ICertificateStore certStore = certStoreIdentifier.OpenStore(telemetry)) { await certStore.AddAsync(cert1).ConfigureAwait(false); await certStore.AddAsync(cert2).ConfigureAwait(false); } var certId1 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = SubjectName, CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType }; var certId2 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = subjectName2, CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType }; ApplicationConfiguration config = await applicationInstance .Build(uri1, ProductUri) .AsClient() .AddSecurityConfiguration(SubjectName, m_pkiRoot) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); // Set multiple certificates config.SecurityConfiguration.ApplicationCertificates = [ certId1, certId2 ]; // This should throw because all certificates must have the same ApplicationUri ServiceResultException sre = Assert .ThrowsAsync(async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); Assert.That(sre.Message, Does.Contain("certificate") & Does.Contain("invalid")); } } /// /// Test that multiple certificates with the same ApplicationUri succeed. /// [Test] public async Task TestMultipleCertificatesSameUriSucceedsAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); // Create two certificates with the same ApplicationUri using Certificate cert1 = DefaultCertificateFactory.Instance .CreateApplicationCertificate(ApplicationUri, ApplicationName, SubjectName, [Utils.GetHostName()]) .SetNotBefore(DateTime.Today.AddDays(-1)) .SetNotAfter(DateTime.Today.AddYears(1)) .CreateForRSA(); const string subjectName2 = "CN=UA Configuration Test RSA, O=OPC Foundation, C=US, S=Arizona"; using Certificate cert2 = DefaultCertificateFactory.Instance .CreateApplicationCertificate(ApplicationUri, ApplicationName, subjectName2, [Utils.GetHostName()]) .SetNotBefore(DateTime.Today.AddDays(-1)) .SetNotAfter(DateTime.Today.AddYears(1)) .SetRSAKeySize(CertificateFactory.DefaultKeySize) .CreateForRSA(); // Save certificates to stores string certStorePath = m_pkiRoot + "own"; var certStoreIdentifier = new CertificateStoreIdentifier(certStorePath, CertificateStoreType.Directory, false); using (ICertificateStore certStore = certStoreIdentifier.OpenStore(telemetry)) { await certStore.AddAsync(cert1).ConfigureAwait(false); await certStore.AddAsync(cert2).ConfigureAwait(false); } var certId1 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = SubjectName, CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType }; var certId2 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = subjectName2, CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType }; ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(SubjectName, m_pkiRoot) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); // Set multiple certificates with same URI config.SecurityConfiguration.ApplicationCertificates = [ certId1, certId2 ]; // This should succeed because all certificates have the same ApplicationUri bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } /// /// Test that a certificate with multiple SAN URI entries where one matches the ApplicationUri succeeds. /// Tests with different certificate types (RSA, ECC NIST P-256, ECC NIST P-384, ECC Brainpool). /// [Test] [TestCaseSource(nameof(CertificateTypes))] public async Task TestCertificateWithMultipleSanUrisMatchingSucceedsAsync(NodeId certificateType) { // Skip test if certificate type is not supported on this platform if (!Utils.IsSupportedCertificateType(certificateType)) { Assert.Ignore($"Certificate type {certificateType} is not supported on this platform."); } ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); // Create a certificate with multiple URIs in SAN, including the ApplicationUri const string uri1 = "urn:localhost:opcfoundation.org:App1"; const string uri2 = ApplicationUri; // This matches const string uri3 = "https://localhost:8080/OpcUaApp"; using Certificate cert = CreateCertificateWithMultipleUris( [uri1, uri2, uri3], SubjectName, [Utils.GetHostName()], certificateType); // Save certificate to store string certStorePath = m_pkiRoot + "own"; var certStoreIdentifier = new CertificateStoreIdentifier(certStorePath, CertificateStoreType.Directory, false); using (ICertificateStore certStore = certStoreIdentifier.OpenStore(telemetry)) { await certStore.AddAsync(cert).ConfigureAwait(false); } var certId = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = SubjectName, CertificateType = certificateType }; ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(SubjectName, m_pkiRoot) .SetMinimumCertificateKeySize(256) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); config.SecurityConfiguration.ApplicationCertificates = [certId]; // This should succeed because one of the URIs matches bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); // Verify the certificate has multiple URIs // Load the certificate to check its URIs using Certificate loadedCert = (await CertificateIdentifierResolver .ResolveAsync(certId, registry: null, needPrivateKey: false, applicationUri: null, telemetry) .ConfigureAwait(false))!; IReadOnlyList uris = X509Utils.GetApplicationUrisFromCertificate(loadedCert!); Assert.That(uris, Has.Count.EqualTo(3)); Assert.Contains(uri1, uris.ToList()); Assert.Contains(uri2, uris.ToList()); Assert.Contains(uri3, uris.ToList()); } } /// /// Test that a certificate with multiple SAN URI entries where none matches the ApplicationUri fails. /// Tests with different certificate types (RSA, ECC NIST P-256, ECC NIST P-384, ECC Brainpool). /// [Test] [TestCaseSource(nameof(CertificateTypes))] public async Task TestCertificateWithMultipleSanUrisNotMatchingThrowsAsync(NodeId certificateType) { // Skip test if certificate type is not supported on this platform if (!Utils.IsSupportedCertificateType(certificateType)) { Assert.Ignore($"Certificate type {certificateType} is not supported on this platform."); } ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); // Create a certificate with multiple URIs in SAN, but none matching ApplicationUri const string uri1 = "urn:localhost:opcfoundation.org:App1"; const string uri2 = "urn:localhost:opcfoundation.org:App2"; const string uri3 = "https://localhost:8080/OpcUaApp"; using Certificate cert = CreateCertificateWithMultipleUris( [uri1, uri2, uri3], SubjectName, [Utils.GetHostName()], certificateType); // Save certificate to store string certStorePath = m_pkiRoot + "own"; var certStoreIdentifier = new CertificateStoreIdentifier(certStorePath, CertificateStoreType.Directory, false); using (ICertificateStore certStore = certStoreIdentifier.OpenStore(telemetry)) { await certStore.AddAsync(cert).ConfigureAwait(false); } var certId = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = SubjectName, CertificateType = certificateType }; ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(SubjectName, m_pkiRoot) .SetMinimumCertificateKeySize(256) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); config.SecurityConfiguration.ApplicationCertificates = [certId]; // This should fail because none of the URIs match ServiceResultException sre = Assert .ThrowsAsync(async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); } } /// /// Test with multiple certificates, each with multiple SAN URIs, where all match the ApplicationUri. /// Tests with different certificate types (RSA, ECC NIST P-256, ECC NIST P-384, ECC Brainpool). /// [Test] [TestCaseSource(nameof(CertificateTypes))] public async Task TestMultipleCertificatesWithMultipleSanUrisAllMatchingSucceedsAsync(NodeId certificateType) { // Skip test if certificate type is not supported on this platform if (!Utils.IsSupportedCertificateType(certificateType)) { Assert.Ignore($"Certificate type {certificateType} is not supported on this platform."); } ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); // Create first certificate with multiple URIs including ApplicationUri using Certificate cert1 = CreateCertificateWithMultipleUris( [ApplicationUri, "https://localhost:8080/Test1", "opc.tcp://localhost:4840/Test1"], SubjectName, [Utils.GetHostName()], certificateType); const string subjectName2 = "CN=UA Configuration Test 2, O=OPC Foundation, C=US, S=Arizona"; // Create second certificate with multiple URIs including ApplicationUri using Certificate cert2 = CreateCertificateWithMultipleUris( ["urn:localhost:opcfoundation.org:OtherApp", ApplicationUri, "https://localhost:9443/Test2"], subjectName2, [Utils.GetHostName()], certificateType); // Save certificates to store string certStorePath = m_pkiRoot + "own"; var certStoreIdentifier = new CertificateStoreIdentifier(certStorePath, CertificateStoreType.Directory, false); using (ICertificateStore certStore = certStoreIdentifier.OpenStore(telemetry)) { await certStore.AddAsync(cert1).ConfigureAwait(false); await certStore.AddAsync(cert2).ConfigureAwait(false); } var certId1 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = SubjectName, CertificateType = certificateType }; var certId2 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = subjectName2, CertificateType = certificateType }; ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(SubjectName, m_pkiRoot) .SetMinimumCertificateKeySize(256) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); config.SecurityConfiguration.ApplicationCertificates = [ certId1, certId2 ]; // This should succeed because both certificates contain ApplicationUri in their SAN bool certOK = await applicationInstance .CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false); Assert.That(certOK, Is.True); } } /// /// Test with multiple certificates where only one has the matching ApplicationUri in its SAN URIs. /// Tests with different certificate types (RSA, ECC NIST P-256, ECC NIST P-384, ECC Brainpool). /// [Test] [TestCaseSource(nameof(CertificateTypes))] public async Task TestMultipleCertificatesWithOnlyOneMatchingSanUriThrowsAsync(NodeId certificateType) { // Skip test if certificate type is not supported on this platform if (!Utils.IsSupportedCertificateType(certificateType)) { Assert.Ignore($"Certificate type {certificateType} is not supported on this platform."); } ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName }; await using (applicationInstance.ConfigureAwait(false)) { Assert.That(applicationInstance, Is.Not.Null); // Create first certificate with ApplicationUri using Certificate cert1 = CreateCertificateWithMultipleUris( [ApplicationUri, "https://localhost:8080/Test1"], SubjectName, [Utils.GetHostName()], certificateType); const string subjectName2 = "CN=UA Configuration Test 2, O=OPC Foundation, C=US, S=Arizona"; // Create second certificate WITHOUT ApplicationUri using Certificate cert2 = CreateCertificateWithMultipleUris( ["urn:localhost:opcfoundation.org:OtherApp", "https://localhost:9443/Test2"], subjectName2, [Utils.GetHostName()], certificateType); // Save certificates to store string certStorePath = m_pkiRoot + "own"; var certStoreIdentifier = new CertificateStoreIdentifier(certStorePath, CertificateStoreType.Directory, false); using (ICertificateStore certStore = certStoreIdentifier.OpenStore(telemetry)) { await certStore.AddAsync(cert1).ConfigureAwait(false); await certStore.AddAsync(cert2).ConfigureAwait(false); } var certId1 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = SubjectName, CertificateType = certificateType }; var certId2 = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = certStorePath, SubjectName = subjectName2, CertificateType = certificateType }; ApplicationConfiguration config = await applicationInstance .Build(ApplicationUri, ProductUri) .AsClient() .AddSecurityConfiguration(SubjectName, m_pkiRoot) .SetMinimumCertificateKeySize(256) .CreateAsync() .ConfigureAwait(false); Assert.That(config, Is.Not.Null); config.SecurityConfiguration.ApplicationCertificates = [ certId1, certId2 ]; // This should fail because cert2 doesn't contain ApplicationUri ServiceResultException sre = Assert .ThrowsAsync(async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync(true) .ConfigureAwait(false)); Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); } } private static Certificate CreateInvalidCert(InvalidCertType certType) { // reasonable defaults DateTime notBefore = DateTime.Today.AddDays(-30); DateTime notAfter = DateTime.Today.AddDays(30); ushort keySize = CertificateFactory.DefaultKeySize; string[] domainNames = [Utils.GetHostName()]; switch (certType) { case InvalidCertType.Expired: notBefore = DateTime.Today.AddMonths(-12); notAfter = DateTime.Today.AddDays(-7); break; case InvalidCertType.NotYetValid: notBefore = DateTime.Today.AddDays(7); notAfter = notBefore.AddMonths(12); break; case InvalidCertType.KeySize1024: keySize = 1024; break; case InvalidCertType.HostName: domainNames = ["myhost", "1.2.3.4"]; break; case InvalidCertType.NoIssues: case InvalidCertType.NoIssuer: case InvalidCertType.IssuerExpired: case InvalidCertType.IssuerNotYetValid: break; default: throw ServiceResultException.Unexpected( $"Unexpected InvalidCertType {certType}"); } return DefaultCertificateFactory.Instance.CreateApplicationCertificate(ApplicationUri, ApplicationName, SubjectName, domainNames) .SetNotBefore(notBefore) .SetNotAfter(notAfter) .SetRSAKeySize(keySize) .CreateForRSA(); } private static CertificateCollection CreateInvalidCertChain(InvalidCertType certType) { // reasonable defaults DateTime notBefore = DateTime.Today.AddYears(-1); DateTime notAfter = DateTime.Today.AddYears(1); DateTime issuerNotBefore = notBefore; DateTime issuerNotAfter = notAfter; ushort keySize = CertificateFactory.DefaultKeySize; string[] domainNames = [Utils.GetHostName()]; switch (certType) { case InvalidCertType.Expired: notAfter = DateTime.Today.AddDays(-7); break; case InvalidCertType.IssuerExpired: issuerNotAfter = DateTime.Today.AddDays(-7); break; case InvalidCertType.NotYetValid: notBefore = DateTime.Today.AddDays(7); break; case InvalidCertType.IssuerNotYetValid: issuerNotBefore = DateTime.Today.AddDays(7); break; case InvalidCertType.KeySize1024: keySize = 1024; break; case InvalidCertType.HostName: domainNames = ["myhost", "1.2.3.4"]; break; case InvalidCertType.NoIssues: case InvalidCertType.NoIssuer: break; default: throw ServiceResultException.Unexpected( $"Unexpected InvalidCertType {certType}"); } const string rootCASubjectName = "CN=Root CA Test, O=OPC Foundation, C=US, S=Arizona"; using Certificate rootCA = DefaultCertificateFactory.Instance.CreateCertificate(rootCASubjectName) .SetNotBefore(issuerNotBefore) .SetNotAfter(issuerNotAfter) .SetCAConstraint(-1) .CreateForRSA(); using Certificate appCert = DefaultCertificateFactory.Instance.CreateApplicationCertificate( ApplicationUri, ApplicationName, SubjectName, domainNames) .SetNotBefore(notBefore) .SetNotAfter(notAfter) .SetIssuer(rootCA) .SetRSAKeySize(keySize) .CreateForRSA(); using var rootCAPublic = Certificate.FromRawData(rootCA.RawData); // Collection AddRefs each cert; the using directives dispose our // local references so the caller's collection holds the only refs. return [appCert, rootCAPublic]; } /// /// Provides a collection of certificate types for parameterized tests. /// /// An enumerable collection of NodeIds representing different certificate types. /// /// This method is used as a TestCaseSource to provide different certificate types (RSA, ECC) for testing. /// Individual tests check platform support using Utils.IsSupportedCertificateType(). /// private static IEnumerable CertificateTypes() { yield return ObjectTypeIds.RsaSha256ApplicationCertificateType; yield return ObjectTypeIds.EccNistP256ApplicationCertificateType; yield return ObjectTypeIds.EccNistP384ApplicationCertificateType; yield return ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType; yield return ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType; } /// /// Helper method to create a certificate with multiple URI entries in the SAN. /// /// The list of application URIs to include in the SAN /// The subject name for the certificate /// The domain names for the certificate /// A certificate with multiple URIs in the SAN extension private static Certificate CreateCertificateWithMultipleUris( IList applicationUris, string subjectName, IList domainNames, NodeId certificateType = default) { DateTime notBefore = DateTime.Today.AddDays(-1); DateTime notAfter = DateTime.Today.AddYears(1); // Default to RSA if not specified if (certificateType.IsNull) { certificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType; } // Create the SAN extension with multiple URIs var subjectAltName = new X509SubjectAltNameExtension(applicationUris, domainNames); // Build the certificate with the custom SAN extension ICertificateBuilder builder = CertificateBuilder .Create(subjectName) .SetNotBefore(notBefore) .SetNotAfter(notAfter) .AddExtension(subjectAltName); // Create certificate based on type if (certificateType == ObjectTypeIds.RsaSha256ApplicationCertificateType || certificateType == ObjectTypeIds.RsaMinApplicationCertificateType) { return builder.SetRSAKeySize(CertificateFactory.DefaultKeySize).CreateForRSA(); } else if (certificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) { return builder.SetECCurve(ECCurve.NamedCurves.nistP256).CreateForECDsa(); } else if (certificateType == ObjectTypeIds.EccNistP384ApplicationCertificateType) { return builder.SetECCurve(ECCurve.NamedCurves.nistP384).CreateForECDsa(); } else if (certificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) { return builder.SetECCurve(ECCurve.NamedCurves.brainpoolP256r1).CreateForECDsa(); } else if (certificateType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType) { return builder.SetECCurve(ECCurve.NamedCurves.brainpoolP384r1).CreateForECDsa(); } else { // Default to RSA for unknown types return builder.SetRSAKeySize(CertificateFactory.DefaultKeySize).CreateForRSA(); } } private string m_pkiRoot; private static readonly string[] s_alternateBaseAddresses = ["opc.tcp://192.168.1.100:51000"]; private static readonly string[] s_baseAddresses = ["opc.tcp://localhost:12345/Configuration"]; private static readonly string[] s_baseAddressesArray = ["opc.tcp://localhost:12345/Configuration"]; } }