Files
UA-.NETStandard/tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs
Cristi Pogacean 5fa272d89a Fix instance-scoped certificate store resolution and consumers (#4546)
## Summary
Route certificate operations through the providers registered on the
configured manager, so custom stores work consistently during
validation, key loading, provisioning and GDS operations.

- Add optional `ICertificateStoreResolver`, implemented by
`CertificateManager`, without adding members to `ICertificateManager` or
removing existing public signatures.
- Use scoped resolution for configuration validation, startup and
cold-cache private-key lookup, application certificate
provisioning/reuse/deletion, existing-key GDS signing requests, and
hosted TrustList access.
- Preserve built-in lookup when no resolver is available, private-key
lookup fallbacks, access checks and transaction behavior. Resolver
failures do not silently fall back to Directory. Returned stores remain
caller-owned; TrustList retains cached-store disposal.
- Add regression coverage for instance isolation, mixed provider types,
real private-key reuse, rotation lookup, cancellation/failure disposal,
unavailable-key rejection, CSR output and TrustList lifetime. Document
the optional capability.

Fixes #4527
Fixes #4528
Fixes #4529
Fixes #4530
Fixes #4531

## Validation
Change-scoped Release tests on Windows, on base `5d26f6f43`:

| Fixture selection | net10.0 | net48 |
| --- | ---: | ---: |
| CertificateManagerFactoryTests + CertificateManagerTests | 71 passed |
71 passed |
| ApplicationInstanceTests | 56 passed | 56 passed |
| TrustListTests + ConfigurationNodeManagerPushTests | 147 passed | 147
passed |

All six selections exited 0 with zero failures or skips. Changed-file
whitespace verification and `git diff --check` passed.

Fresh no-fix comparisons retained the new resolver API/manager
implementation, but reversed the validation, provisioning and CSR
consumer fixes: validation had 2 custom-store failures / 1 built-in
control pass; provisioning had 4 custom-store failures / 1 built-in
control pass; CSR had 1 custom-store failure / 1 built-in control pass.
Failures reported unknown custom store types or invalid custom trust
stores. All consumer source changes were restored, rebuilt, and the full
focused net10.0 selections passed again.

Earlier TrustList regression evidence is recorded in #4531: its before
comparison retained the constructor injection boundary while exercising
the old store-opening path. It was not a test against an entirely
unextended API.

The complete platform/framework matrix is left to PR CI; no full local
matrix or completed CI qualification is claimed. Local restore used
command-line-only NuGet v2/feed-audit overrides; no repository feed,
test or contribution guidance was changed.
2026-09-25 12:13:31 +00:00

1893 lines
89 KiB
C#

/* ========================================================================
* 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 && !NET_STANDARD_TESTS
using System.Runtime.InteropServices;
#endif
namespace Opc.Ua.Configuration.Tests
{
/// <summary>
/// Tests for the ApplicationInstance configuration, certificate provisioning,
/// and application-configuration loading.
/// </summary>
[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";
/// <summary>
/// Test setup.
/// </summary>
[SetUp]
public void SetUp()
{
// pki directory root for test runs.
m_pkiRoot = Path.GetTempPath() + Path.GetRandomFileName() + Path.DirectorySeparatorChar;
}
/// <summary>
/// Test teardown.
/// </summary>
[TearDown]
public void TearDown()
{
try
{
// pki directory root for test runs.
Directory.Delete(m_pkiRoot, true);
}
catch
{
}
}
/// <summary>
/// Load a file configuration.
/// </summary>
[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<CertificateIdentifier> 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<ICertificateManager>(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<TrustListIdentifier>()),
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<CertificateIdentifier> 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<CertificateIdentifier> 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<ServiceResultException>(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<ArgumentException>(async () =>
await applicationInstance
.Build(ApplicationUri, ProductUri)
.AsClient()
.AddSecurityConfiguration(applicationCerts, m_pkiRoot)
.CreateAsync()
.ConfigureAwait(false));
Assert.ThrowsAsync<ArgumentException>(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<ArgumentException>(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<ArgumentException>(async () =>
await applicationInstance
.Build(ApplicationUri, ProductUri)
.AsServer([EndpointUrl])
.AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.None)
.AddSecurityConfiguration(applicationCerts)
.CreateAsync()
.ConfigureAwait(false));
// invalid policy
Assert.ThrowsAsync<ArgumentException>(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<ArgumentNullException>(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<CertificateIdentifier> 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<CertificateIdentifier> 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<CertificateIdentifier> 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);
}
}
/// <summary>
/// Test case when app cert already exists or when new
/// cert is created in X509Store.
/// </summary>
[Test]
[Repeat(2)]
public async Task TestNoFileConfigAsServerX509StoreAsync()
{
#if NETCOREAPP2_1_OR_GREATER && !NET_STANDARD_TESTS
// 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<CertificateIdentifier> 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<CertificateIdentifier> 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
}
/// <summary>
/// Test to verify that an existing cert with suppressible issues
/// is not recreated/replaced.
/// </summary>
[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<CertificateIdentifier> 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<ServiceResultException>(async () =>
await applicationInstance.CheckApplicationInstanceCertificatesAsync(
true)
.ConfigureAwait(false));
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError));
}
}
}
}
/// <summary>
/// Test to verify that an existing cert with suppressible issues
/// is not recreated/replaced.
/// </summary>
[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<CertificateIdentifier> 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<ServiceResultException>(async () =>
await applicationInstance.CheckApplicationInstanceCertificatesAsync(
true)
.ConfigureAwait(false));
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError));
}
}
}
}
/// <summary>
/// Tests that a supplied certificate is stored in the Trusted store of the Server after calling method AddOwnCertificateToTrustedStoreAsync
/// </summary>
[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));
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
[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<ServiceResultException>(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));
}
}
}
/// <summary>
/// Test to verify that a new cert is not recreated/replaced if DisableCertificateAutoCreation is set.
/// </summary>
[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<CertificateIdentifier> 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<ServiceResultException>(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);
}
}
}
/// <summary>
/// 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.
/// </summary>
[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<ServiceResultException>(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"));
}
}
/// <summary>
/// Test that multiple certificates with the same ApplicationUri succeed.
/// </summary>
[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);
}
}
/// <summary>
/// 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).
/// </summary>
[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<string> 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());
}
}
/// <summary>
/// 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).
/// </summary>
[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<ServiceResultException>(async () =>
await applicationInstance.CheckApplicationInstanceCertificatesAsync(true)
.ConfigureAwait(false));
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError));
}
}
/// <summary>
/// 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).
/// </summary>
[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);
}
}
/// <summary>
/// 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).
/// </summary>
[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<ServiceResultException>(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];
}
/// <summary>
/// Provides a collection of certificate types for parameterized tests.
/// </summary>
/// <returns>An enumerable collection of NodeIds representing different certificate types.</returns>
/// <remarks>
/// This method is used as a TestCaseSource to provide different certificate types (RSA, ECC) for testing.
/// Individual tests check platform support using Utils.IsSupportedCertificateType().
/// </remarks>
private static IEnumerable<NodeId> CertificateTypes()
{
yield return ObjectTypeIds.RsaSha256ApplicationCertificateType;
yield return ObjectTypeIds.EccNistP256ApplicationCertificateType;
yield return ObjectTypeIds.EccNistP384ApplicationCertificateType;
yield return ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType;
yield return ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType;
}
/// <summary>
/// Helper method to create a certificate with multiple URI entries in the SAN.
/// </summary>
/// <param name="applicationUris">The list of application URIs to include in the SAN</param>
/// <param name="subjectName">The subject name for the certificate</param>
/// <param name="domainNames">The domain names for the certificate</param>
/// <returns>A certificate with multiple URIs in the SAN extension</returns>
private static Certificate CreateCertificateWithMultipleUris(
IList<string> applicationUris,
string subjectName,
IList<string> 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"];
}
}