diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs index 240b143ed..23ffebf91 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs @@ -1427,7 +1427,37 @@ private static EndpointDescriptionCollection MatchEndpoints( // no matches (security parameters may have changed). if (matches.Count == 0) { - matches = collection; + // if specific security parameters were requested, throw appropriate error + bool hasSpecificPolicy = !string.IsNullOrEmpty(securityPolicyUri); + bool hasSpecificMode = securityMode != MessageSecurityMode.Invalid; + + if (hasSpecificPolicy && hasSpecificMode) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Server does not support the requested security policy '{0}' and security mode '{1}'.", + securityPolicyUri, + securityMode); + } + else if (hasSpecificPolicy) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Server does not support the requested security policy '{0}'.", + securityPolicyUri); + } + else if (hasSpecificMode) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityModeRejected, + "Server does not support the requested security mode '{0}'.", + securityMode); + } + else + { + // no specific security parameters were requested, fall back to any endpoint + matches = collection; + } } // check if list has to be narrowed down further. diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs new file mode 100644 index 000000000..4c7bda9f0 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs @@ -0,0 +1,271 @@ +/* ======================================================================== + * 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/ + * ======================================================================*/ + +using System; +using System.Reflection; +using NUnit.Framework; +using Opc.Ua.Tests; +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Core.Tests.Stack.Client +{ + /// + /// Tests for ConfiguredEndpoint matching. + /// + [TestFixture] + [Category("Client")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + [Parallelizable] + public class ConfiguredEndpointTests + { + /// + /// Test that when a requested security policy is not supported by the server, + /// BadSecurityPolicyRejected is thrown instead of BadUserAccessDenied. + /// + [Test] + public void MatchEndpoints_ThrowsBadSecurityPolicyRejected_WhenPolicyNotSupported() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + + // Create server endpoints with only None and Basic256Sha256 + var serverEndpoints = new EndpointDescriptionCollection + { + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + }, + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.SignAndEncrypt, + SecurityPolicyUri = SecurityPolicies.Basic256Sha256 + } + }; + + // Try to match with a security policy that doesn't exist (Aes256_Sha256_RsaPss) + var ex = Assert.Throws(() => + { + InvokeMatchEndpoints( + serverEndpoints, + new Uri("opc.tcp://localhost:4840"), + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.Aes256_Sha256_RsaPss + ); + }); + + Assert.IsInstanceOf(ex.InnerException); + var serviceException = (ServiceResultException)ex.InnerException; + Assert.AreEqual(StatusCodes.BadSecurityPolicyRejected, serviceException.StatusCode); + Assert.That(serviceException.Message, Does.Contain(SecurityPolicies.Aes256_Sha256_RsaPss)); + } + + /// + /// Test that when a requested security mode is not supported by the server, + /// BadSecurityModeRejected is thrown. + /// + [Test] + public void MatchEndpoints_ThrowsBadSecurityModeRejected_WhenModeNotSupported() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + + // Create server endpoints with only None security mode + var serverEndpoints = new EndpointDescriptionCollection + { + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + } + }; + + // Try to match with SignAndEncrypt mode that doesn't exist + var ex = Assert.Throws(() => + { + InvokeMatchEndpoints( + serverEndpoints, + new Uri("opc.tcp://localhost:4840"), + MessageSecurityMode.SignAndEncrypt, + null // no specific policy requested, only mode + ); + }); + + Assert.IsInstanceOf(ex.InnerException); + var serviceException = (ServiceResultException)ex.InnerException; + Assert.AreEqual(StatusCodes.BadSecurityModeRejected, serviceException.StatusCode); + Assert.That(serviceException.Message, Does.Contain("SignAndEncrypt")); + } + + /// + /// Test that when both security policy and mode are not supported, + /// BadSecurityPolicyRejected is thrown with information about both. + /// + [Test] + public void MatchEndpoints_ThrowsBadSecurityPolicyRejected_WhenBothPolicyAndModeNotSupported() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + + // Create server endpoints with only None + var serverEndpoints = new EndpointDescriptionCollection + { + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + } + }; + + // Try to match with both policy and mode that don't exist + var ex = Assert.Throws(() => + { + InvokeMatchEndpoints( + serverEndpoints, + new Uri("opc.tcp://localhost:4840"), + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.Basic256Sha256 + ); + }); + + Assert.IsInstanceOf(ex.InnerException); + var serviceException = (ServiceResultException)ex.InnerException; + Assert.AreEqual(StatusCodes.BadSecurityPolicyRejected, serviceException.StatusCode); + Assert.That(serviceException.Message, Does.Contain(SecurityPolicies.Basic256Sha256)); + Assert.That(serviceException.Message, Does.Contain("SignAndEncrypt")); + } + + /// + /// Test that when no specific security parameters are requested, + /// the method returns available endpoints without throwing. + /// + [Test] + public void MatchEndpoints_ReturnsEndpoints_WhenNoSecurityParametersSpecified() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + + // Create server endpoints + var serverEndpoints = new EndpointDescriptionCollection + { + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + }, + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.SignAndEncrypt, + SecurityPolicyUri = SecurityPolicies.Basic256Sha256 + } + }; + + // Match without specifying security parameters + var matches = InvokeMatchEndpoints( + serverEndpoints, + new Uri("opc.tcp://localhost:4840"), + MessageSecurityMode.Invalid, + null + ); + + // Should return available endpoints + Assert.IsNotNull(matches); + Assert.Greater(matches.Count, 0); + } + + /// + /// Test that matching works correctly when the requested security parameters exist. + /// + [Test] + public void MatchEndpoints_ReturnsMatchingEndpoint_WhenSecurityParametersMatch() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + + // Create server endpoints + var serverEndpoints = new EndpointDescriptionCollection + { + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + }, + new EndpointDescription + { + EndpointUrl = "opc.tcp://localhost:4840", + SecurityMode = MessageSecurityMode.SignAndEncrypt, + SecurityPolicyUri = SecurityPolicies.Basic256Sha256 + } + }; + + // Match with existing security parameters + var matches = InvokeMatchEndpoints( + serverEndpoints, + new Uri("opc.tcp://localhost:4840"), + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.Basic256Sha256 + ); + + // Should return the matching endpoint + Assert.IsNotNull(matches); + Assert.AreEqual(1, matches.Count); + Assert.AreEqual(SecurityPolicies.Basic256Sha256, matches[0].SecurityPolicyUri); + Assert.AreEqual(MessageSecurityMode.SignAndEncrypt, matches[0].SecurityMode); + } + + /// + /// Helper method to invoke the private MatchEndpoints method via reflection. + /// + private EndpointDescriptionCollection InvokeMatchEndpoints( + EndpointDescriptionCollection collection, + Uri endpointUrl, + MessageSecurityMode securityMode, + string securityPolicyUri) + { + var configuredEndpointType = typeof(ConfiguredEndpoint); + var matchEndpointsMethod = configuredEndpointType.GetMethod( + "MatchEndpoints", + BindingFlags.NonPublic | BindingFlags.Static, + null, + new[] { typeof(EndpointDescriptionCollection), typeof(Uri), typeof(MessageSecurityMode), typeof(string) }, + null + ); + + Assert.IsNotNull(matchEndpointsMethod, "MatchEndpoints method not found"); + + return (EndpointDescriptionCollection)matchEndpointsMethod.Invoke( + null, + new object[] { collection, endpointUrl, securityMode, securityPolicyUri } + ); + } + } +}