mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
## 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.
1893 lines
89 KiB
C#
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"];
|
|
}
|
|
}
|