diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs
index 41d95a783..dca79c278 100644
--- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs
+++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs
@@ -694,7 +694,7 @@ public void Initialize(StreamingContext context)
/// This certificate must contain the application uri.
/// For servers, URLs for each supported protocol must also be present.
///
- [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 0)]
+ [IgnoreDataMember]
public CertificateIdentifier ApplicationCertificate
{
get
@@ -730,10 +730,19 @@ public CertificateIdentifier ApplicationCertificate
}
}
+ // This private property exists solely to control serialization of the legacy single
+ // certificate element. It is emitted only when the configuration was marked deprecated.
+ [DataMember(Name = "ApplicationCertificate", IsRequired = false, EmitDefaultValue = false, Order = 0)]
+ private CertificateIdentifier ApplicationCertificateLegacy
+ {
+ get => IsDeprecatedConfiguration ? ApplicationCertificate : null;
+ set => ApplicationCertificate = value;
+ }
+
///
/// The application instance certificates in use for the application.
///
- [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 1)]
+ [IgnoreDataMember]
public CertificateIdentifierCollection ApplicationCertificates
{
get => m_applicationCertificates;
@@ -745,6 +754,11 @@ public CertificateIdentifierCollection ApplicationCertificates
return;
}
+ // If both legacy () and modern () elements
+ // are present during deserialization (as a consequence of previous serialization that included both unintentionally),
+ // prefer the modern representation and clear the
+ // deprecated flag when we process the collection below.
+
var newCertificates = new CertificateIdentifierCollection(value);
// Remove unsupported certificate types
@@ -791,10 +805,22 @@ public CertificateIdentifierCollection ApplicationCertificates
m_applicationCertificates = newCertificates;
+ // Presence of the modern collection takes precedence over legacy; clear the flag so
+ // hybrid configurations are treated as modern.
+ IsDeprecatedConfiguration = false;
SupportedSecurityPolicies = BuildSupportedSecurityPolicies();
}
}
+ // This private property exists solely to control the serialization of the modern certificates collection.
+ // Emit only when the configuration is not marked deprecated.
+ [DataMember(Name = "ApplicationCertificates", IsRequired = false, EmitDefaultValue = false, Order = 1)]
+ private CertificateIdentifierCollection ApplicationCertificatesDataContract
+ {
+ get => IsDeprecatedConfiguration ? null : ApplicationCertificates;
+ set => ApplicationCertificates = value;
+ }
+
///
/// The store containing any additional issuer certificates.
///
diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj
index 6f0f56f7a..de9137b8c 100644
--- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj
+++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj
@@ -35,5 +35,11 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
diff --git a/Tests/Opc.Ua.Configuration.Tests/SecurityConfigurationTests.cs b/Tests/Opc.Ua.Configuration.Tests/SecurityConfigurationTests.cs
index a214e6be6..ccd8a0cbd 100644
--- a/Tests/Opc.Ua.Configuration.Tests/SecurityConfigurationTests.cs
+++ b/Tests/Opc.Ua.Configuration.Tests/SecurityConfigurationTests.cs
@@ -27,7 +27,15 @@
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
+using System;
using System.Collections.Generic;
+using System.Configuration;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
using NUnit.Framework;
using Opc.Ua.Tests;
@@ -63,6 +71,278 @@ public void InvalidConfigurationThrows(SecurityConfiguration configuration)
Assert.Throws(() => configuration.Validate(telemetry));
}
+ [Test]
+ public async Task LoadingConfigurationWithApplicationCertificateShouldMarkItDeprecated()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ var file = Path.Combine(TestContext.CurrentContext.WorkDirectory, "testlegacyconfig.xml");
+
+ var serializer = new DataContractSerializer(typeof(ApplicationConfiguration));
+ using var stream = new FileStream(file, FileMode.Open);
+ var reloadedConfiguration =
+ (ApplicationConfiguration)serializer.ReadObject(stream);
+
+ Assert.That(
+ reloadedConfiguration.SecurityConfiguration.IsDeprecatedConfiguration,
+ Is.True);
+ }
+
+ [Test]
+ public async Task LoadingConfigurationWithApplicationCertificateAndApplicationCertificatesShouldNotMarkItDeprecated()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ var file = Path.Combine(TestContext.CurrentContext.WorkDirectory, "testhybridconfig.xml");
+
+ var serializer = new DataContractSerializer(typeof(ApplicationConfiguration));
+ using var stream = new FileStream(file, FileMode.Open);
+ var reloadedConfiguration =
+ (ApplicationConfiguration)serializer.ReadObject(stream);
+
+ Assert.That(
+ reloadedConfiguration.SecurityConfiguration.IsDeprecatedConfiguration,
+ Is.False);
+ }
+
+ [Test]
+ public void SavingConfigurationShouldNotMarkItDeprecated()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+
+ var securityConfiguration = new SecurityConfiguration
+ {
+ ApplicationCertificates = new CertificateIdentifierCollection
+ {
+ new CertificateIdentifier
+ {
+ StoreType = CertificateStoreType.Directory,
+ StorePath = "pki/own",
+ CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType
+ }
+ },
+ TrustedPeerCertificates = new CertificateTrustList { StorePath = "Test" },
+ TrustedIssuerCertificates = new CertificateTrustList { StorePath = "Test" }
+ };
+
+ var configuration = new ApplicationConfiguration(telemetry)
+ {
+ ApplicationName = "DeprecatedConfigurationTest",
+ ApplicationUri = "urn:localhost:DeprecatedConfigurationTest",
+ ApplicationType = ApplicationType.Server,
+ SecurityConfiguration = securityConfiguration
+ };
+
+ var serializer = new DataContractSerializer(typeof(ApplicationConfiguration));
+ using var stream = new MemoryStream();
+ serializer.WriteObject(stream, configuration);
+ stream.Position = 0;
+
+ var reloadedConfiguration =
+ (ApplicationConfiguration)serializer.ReadObject(stream);
+
+ Assert.That(
+ reloadedConfiguration.SecurityConfiguration.IsDeprecatedConfiguration,
+ Is.False,
+ "Deserializing a configuration that uses ApplicationCertificates should not mark it deprecated via the legacy ApplicationCertificate setter.");
+ }
+
+ [Test]
+ public void DeprecatedConfigurationRoundTripsWithLegacyElement()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ var serializer = new DataContractSerializer(typeof(ApplicationConfiguration));
+
+ var configuration = new ApplicationConfiguration(telemetry)
+ {
+ ApplicationName = "DeprecatedConfigurationTest",
+ ApplicationUri = "urn:localhost:DeprecatedConfigurationTest",
+ ApplicationType = ApplicationType.Server,
+ SecurityConfiguration = new SecurityConfiguration
+ {
+ TrustedPeerCertificates = new CertificateTrustList { StorePath = "Test" },
+ TrustedIssuerCertificates = new CertificateTrustList { StorePath = "Test" }
+ }
+ };
+
+ configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier
+ {
+ StoreType = CertificateStoreType.Directory,
+ StorePath = "pki/own",
+ CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType
+ };
+
+ string xml;
+ using (var stream = new MemoryStream())
+ {
+ serializer.WriteObject(stream, configuration);
+ xml = Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ var document = XDocument.Parse(xml);
+ var roundTripped = (ApplicationConfiguration)serializer.ReadObject(
+ new MemoryStream(Encoding.UTF8.GetBytes(xml)));
+
+ Assert.That(configuration.SecurityConfiguration.IsDeprecatedConfiguration, Is.True);
+ Assert.That(
+ document.Descendants(XName.Get("ApplicationCertificate", Namespaces.OpcUaConfig)).Any(),
+ Is.True,
+ "Legacy ApplicationCertificate element should be present for deprecated configurations.");
+ }
+
+ [Test]
+ public void ModernConfigurationOmitsLegacyElement()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ var serializer = new DataContractSerializer(typeof(ApplicationConfiguration));
+
+ var configuration = new ApplicationConfiguration(telemetry)
+ {
+ ApplicationName = "ModernConfigurationTest",
+ ApplicationUri = "urn:localhost:ModernConfigurationTest",
+ ApplicationType = ApplicationType.Server,
+ SecurityConfiguration = new SecurityConfiguration
+ {
+ ApplicationCertificates = new CertificateIdentifierCollection
+ {
+ new CertificateIdentifier
+ {
+ StoreType = CertificateStoreType.Directory,
+ StorePath = "pki/own",
+ CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType
+ }
+ },
+ TrustedPeerCertificates = new CertificateTrustList { StorePath = "Test" },
+ TrustedIssuerCertificates = new CertificateTrustList { StorePath = "Test" }
+ }
+ };
+
+ string xml;
+ using (var stream = new MemoryStream())
+ {
+ serializer.WriteObject(stream, configuration);
+ xml = Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ var document = XDocument.Parse(xml);
+ var roundTripped = (ApplicationConfiguration)serializer.ReadObject(
+ new MemoryStream(Encoding.UTF8.GetBytes(xml)));
+
+ Assert.That(configuration.SecurityConfiguration.IsDeprecatedConfiguration, Is.False);
+ Assert.That(
+ document.Descendants(XName.Get("ApplicationCertificate", Namespaces.OpcUaConfig)).Any(),
+ Is.False,
+ "Modern configurations should not emit the legacy ApplicationCertificate element.");
+ Assert.That(
+ document.Descendants(XName.Get("ApplicationCertificates", Namespaces.OpcUaConfig)).Any(),
+ Is.True,
+ "Modern configurations should emit the ApplicationCertificates element.");
+ }
+
+ [Test]
+ public void DeprecatedConfigurationOmitsApplicationCertificatesElement()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ var serializer = new DataContractSerializer(typeof(ApplicationConfiguration));
+
+ var configuration = new ApplicationConfiguration(telemetry)
+ {
+ ApplicationName = "DeprecatedNoListConfig",
+ ApplicationUri = "urn:localhost:DeprecatedNoListConfig",
+ ApplicationType = ApplicationType.Server,
+ SecurityConfiguration = new SecurityConfiguration
+ {
+ TrustedPeerCertificates = new CertificateTrustList { StorePath = "Test" },
+ TrustedIssuerCertificates = new CertificateTrustList { StorePath = "Test" }
+ }
+ };
+
+ configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier
+ {
+ StoreType = CertificateStoreType.Directory,
+ StorePath = "pki/own",
+ CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType
+ };
+
+ string xml;
+ using (var stream = new MemoryStream())
+ {
+ serializer.WriteObject(stream, configuration);
+ xml = Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ var document = XDocument.Parse(xml);
+
+ Assert.That(configuration.SecurityConfiguration.IsDeprecatedConfiguration, Is.True);
+ Assert.That(
+ document.Descendants(XName.Get("ApplicationCertificate", Namespaces.OpcUaConfig)).Any(),
+ Is.True,
+ "Legacy ApplicationCertificate element should be present for deprecated configurations.");
+ Assert.That(
+ document.Descendants(XName.Get("ApplicationCertificates", Namespaces.OpcUaConfig)).Any(),
+ Is.False,
+ "Deprecated configurations should not emit the ApplicationCertificates element.");
+ }
+
+ [Test]
+ public void HybridConfigurationPrefersModernElementOnSave()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ var serializer = new DataContractSerializer(typeof(ApplicationConfiguration));
+
+ var legacyCert = new CertificateIdentifier
+ {
+ StoreType = CertificateStoreType.Directory,
+ StorePath = "pki/own",
+ CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType
+ };
+
+ var modernCert = new CertificateIdentifier
+ {
+ StoreType = CertificateStoreType.Directory,
+ StorePath = "pki/own-modern",
+ CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType
+ };
+
+ var configuration = new ApplicationConfiguration(telemetry)
+ {
+ ApplicationName = "HybridConfiguration",
+ ApplicationUri = "urn:localhost:HybridConfiguration",
+ ApplicationType = ApplicationType.Server,
+ SecurityConfiguration = new SecurityConfiguration
+ {
+ TrustedPeerCertificates = new CertificateTrustList { StorePath = "Test" },
+ TrustedIssuerCertificates = new CertificateTrustList { StorePath = "Test" }
+ }
+ };
+
+ // First set legacy to mark deprecated, then set the modern collection.
+ configuration.SecurityConfiguration.ApplicationCertificate = legacyCert;
+ configuration.SecurityConfiguration.ApplicationCertificates =
+ new CertificateIdentifierCollection { modernCert };
+
+ string xml;
+ using (var stream = new MemoryStream())
+ {
+ serializer.WriteObject(stream, configuration);
+ xml = Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ var document = XDocument.Parse(xml);
+ var roundTripped = (ApplicationConfiguration)serializer.ReadObject(
+ new MemoryStream(Encoding.UTF8.GetBytes(xml)));
+
+ Assert.That(configuration.SecurityConfiguration.IsDeprecatedConfiguration, Is.False);
+ Assert.That(roundTripped.SecurityConfiguration.IsDeprecatedConfiguration, Is.False);
+ Assert.That(
+ document.Descendants(XName.Get("ApplicationCertificate", Namespaces.OpcUaConfig)).Any(),
+ Is.False,
+ "Hybrid configurations should serialize as modern and omit the legacy element.");
+ Assert.That(
+ document.Descendants(XName.Get("ApplicationCertificates", Namespaces.OpcUaConfig)).Any(),
+ Is.True,
+ "Hybrid configurations should serialize the modern ApplicationCertificates element.");
+ Assert.That(roundTripped.SecurityConfiguration.ApplicationCertificates.Count, Is.EqualTo(1));
+ }
+
private static IEnumerable GetInvalidConfigurations()
{
yield return new TestCaseData(
diff --git a/Tests/Opc.Ua.Configuration.Tests/testhybridconfig.xml b/Tests/Opc.Ua.Configuration.Tests/testhybridconfig.xml
new file mode 100644
index 000000000..801578ef1
--- /dev/null
+++ b/Tests/Opc.Ua.Configuration.Tests/testhybridconfig.xml
@@ -0,0 +1,45 @@
+
+ HybridConfigurationTest
+ urn:localhost:DeprecatedConfigurationTest
+
+ Server_0
+
+
+ Directory
+ pki/own
+
+ i=12560
+
+ RsaSha256
+
+
+
+ Directory
+ pki/own
+
+ i=12560
+
+ RsaSha256
+
+
+
+ Directory
+ Test
+
+
+
+ Directory
+ Test
+
+
+ 32
+ 5
+
+ true
+ 2048
+ true
+ true
+
+
+
+
\ No newline at end of file
diff --git a/Tests/Opc.Ua.Configuration.Tests/testlegacyconfig.xml b/Tests/Opc.Ua.Configuration.Tests/testlegacyconfig.xml
new file mode 100644
index 000000000..58fe41562
--- /dev/null
+++ b/Tests/Opc.Ua.Configuration.Tests/testlegacyconfig.xml
@@ -0,0 +1,32 @@
+
+ HybridConfigurationTest
+ urn:localhost:DeprecatedConfigurationTest
+
+ Server_0
+
+
+ Directory
+ pki/own
+ RsaSha256
+
+
+ Directory
+ Test
+
+
+
+ Directory
+ Test
+
+
+ 32
+ 5
+
+ true
+ 2048
+ true
+ true
+
+
+
+
\ No newline at end of file
diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs
index 76f0c9060..28833b2d1 100644
--- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs
+++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs
@@ -131,7 +131,7 @@ .. m_server
.SelectMany(cg => cg.CertificateTypes)
.Select(s => typeof(Ua.ObjectTypeIds).GetField(s).GetValue(null) as NodeId)
.Where(n => n != null && Utils.IsSupportedCertificateType(n))
-#if NETFRAMEWORK
+#if NETFRAMEWORK || SKIP_ECC_CERTIFICATE_REQUEST_SIGNING
// Only rsa gds issuance supported in net framework
.Where(n =>
n == Ua.ObjectTypeIds.RsaSha256ApplicationCertificateType ||
diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs
index 00db3c87e..a4e3ee280 100644
--- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs
+++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs
@@ -664,7 +664,7 @@ public async Task UpdateCertificateCASignedAsync()
public async Task UpdateCertificateCASignedAsync(bool regeneratePrivateKey)
{
-#if NETFRAMEWORK
+#if NETFRAMEWORK || SKIP_ECC_CERTIFICATE_REQUEST_SIGNING
if (m_certificateType != OpcUa.ObjectTypeIds.RsaMinApplicationCertificateType &&
m_certificateType != OpcUa.ObjectTypeIds.RsaSha256ApplicationCertificateType)
{
diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs
index 18568358a..8109e9663 100644
--- a/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs
+++ b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs
@@ -148,7 +148,7 @@ public void CreateAndParseEcdsaCsrP256()
Assert.Greater(csr.SubjectPublicKeyInfo.Length, 0);
// Verify signature
-#if NET6_0_OR_GREATER
+#if NET6_0_OR_GREATER && !SKIP_ECC_CERTIFICATE_REQUEST_SIGNING
bool isValid = csr.Verify();
Assert.True(isValid, "ECDSA CSR signature should be valid");
#else
diff --git a/targets.props b/targets.props
index eb0cfb537..c91544d03 100644
--- a/targets.props
+++ b/targets.props
@@ -59,6 +59,7 @@
netstandard2.1
netstandard2.1
netstandard2.1
+ true