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