diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 373b40a7a..ff30df1b0 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1292,7 +1292,7 @@ await m_configuration identityToken.Encrypt( serverCertificate, serverNonce, - m_userTokenSecurityPolicyUri, + m_endpoint.Description.SecurityPolicyUri, MessageContext, m_eccServerEphemeralKey, m_instanceCertificate, @@ -1444,7 +1444,8 @@ public async Task UpdateSessionAsync( m_endpoint.Description.FindUserTokenPolicy( identity.TokenType, identity.IssuedTokenType, - securityPolicyUri) + securityPolicyUri, + [.. m_configuration.SecurityConfiguration.SupportedSecurityPolicies ?? []]) ?? throw ServiceResultException.Create( StatusCodes.BadIdentityTokenRejected, "Endpoint does not support the user identity type provided."); @@ -2324,7 +2325,8 @@ public async Task ReconnectAsync( UserTokenPolicy identityPolicy = endpoint.FindUserTokenPolicy( m_identity.TokenType, m_identity.IssuedTokenType, - endpoint.SecurityPolicyUri); + endpoint.SecurityPolicyUri, + [.. m_configuration.SecurityConfiguration.SupportedSecurityPolicies ?? []]); if (identityPolicy == null) { @@ -2363,7 +2365,7 @@ public async Task ReconnectAsync( identityToken.Encrypt( m_serverCertificate, m_serverNonce, - m_userTokenSecurityPolicyUri, + m_endpoint.Description.SecurityPolicyUri, MessageContext, m_eccServerEphemeralKey, m_instanceCertificate, @@ -3783,8 +3785,7 @@ private void OpenValidateIdentity( // check that the user identity is supported by the endpoint. identityPolicy = m_endpoint.Description.FindUserTokenPolicy( - identity.TokenHandler.Token.PolicyId, - securityPolicyUri); + identity.TokenHandler.Token.PolicyId); if (identityPolicy == null) { @@ -3792,7 +3793,8 @@ private void OpenValidateIdentity( identityPolicy = m_endpoint.Description.FindUserTokenPolicy( identity.TokenType, identity.IssuedTokenType, - securityPolicyUri); + securityPolicyUri, + [.. m_configuration.SecurityConfiguration.SupportedSecurityPolicies ?? []]); if (identityPolicy == null) { @@ -4772,10 +4774,17 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( string? endpointSecurityPolicyUri) { var requestHeader = new RequestHeader(); + + if (!EccUtils.IsEccPolicy(endpointSecurityPolicyUri)) + { + // No need to add additional parameters if not using an ECC policy + return requestHeader; + } + string? userTokenSecurityPolicyUri = identityTokenSecurityPolicyUri; if (string.IsNullOrEmpty(userTokenSecurityPolicyUri)) { - userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + userTokenSecurityPolicyUri = endpointSecurityPolicyUri; } m_userTokenSecurityPolicyUri = userTokenSecurityPolicyUri; diff --git a/Libraries/Opc.Ua.Server/Server/IServerInternal.cs b/Libraries/Opc.Ua.Server/Server/IServerInternal.cs index bbc42ec23..b3468aad9 100644 --- a/Libraries/Opc.Ua.Server/Server/IServerInternal.cs +++ b/Libraries/Opc.Ua.Server/Server/IServerInternal.cs @@ -31,6 +31,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Opc.Ua.Security.Certificates; namespace Opc.Ua.Server { @@ -172,6 +173,11 @@ public interface IServerInternal : IAuditEventServer, IDisposable /// ITelemetryContext Telemetry { get; } + /// + /// Provides access to the certificate types supported by the server. + /// + CertificateTypesProvider InstanceCertificateProvider { get; } + /// /// Whether the server is currently running. /// diff --git a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs index 1f7ed1199..a403484dd 100644 --- a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs @@ -78,7 +78,7 @@ public ServerInternalData( m_serverDescription = serverDescription; m_configuration = configuration; MessageContext = messageContext; - + InstanceCertificateProvider = instanceCertificateProvider; m_endpointAddresses = []; foreach (string baseAddresses in m_configuration.ServerConfiguration.BaseAddresses) @@ -246,6 +246,11 @@ public void SetModellingRulesManager(ModellingRulesManager modellingRulesManager /// The message context. public IServiceMessageContext MessageContext { get; } + /// + /// Provides access to the certificate types supported by the server. + /// + public CertificateTypesProvider InstanceCertificateProvider { get; } + /// /// The default system context for the server. /// diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index 0855b8464..f2909cd74 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -32,6 +32,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using Microsoft.Extensions.Logging; +using Opc.Ua.Security.Certificates; namespace Opc.Ua.Server { @@ -90,6 +91,7 @@ public Session( m_serverNonce = serverNonce; m_sessionName = sessionName; m_serverCertificate = serverCertificate; + m_certificateTypesProvider = server.InstanceCertificateProvider; ClientCertificate = clientCertificate; m_clientIssuerCertificates = clientCertificateChain; @@ -317,7 +319,7 @@ public virtual EphemeralKeyType GetNewEccKey() key.Signature = EccUtils.Sign( new ArraySegment(key.PublicKey), - m_serverCertificate, + m_certificateTypesProvider.GetInstanceCertificate(m_eccUserTokenSecurityPolicyUri), m_eccUserTokenSecurityPolicyUri); return key; @@ -894,8 +896,7 @@ private IUserIdentityTokenHandler ValidateUserIdentityToken( } policy = EndpointDescription.FindUserTokenPolicy( - newToken.PolicyId, - EndpointDescription.SecurityPolicyUri); + newToken.PolicyId); if (policy == null) { throw ServiceResultException.Create( @@ -954,8 +955,7 @@ private IUserIdentityTokenHandler ValidateUserIdentityToken( // find the user token policy. policy = EndpointDescription.FindUserTokenPolicy( - token.Token.PolicyId, - EndpointDescription.SecurityPolicyUri); + token.Token.PolicyId); if (policy == null) { @@ -995,7 +995,7 @@ private IUserIdentityTokenHandler ValidateUserIdentityToken( token.Decrypt( m_serverCertificate, m_serverNonce, - securityPolicyUri, + EndpointDescription.SecurityPolicyUri, m_server.MessageContext, m_eccUserTokenNonce, ClientCertificate, @@ -1255,6 +1255,7 @@ private void UpdateDiagnosticCounters( private readonly IServerInternal m_server; private readonly string m_sessionName; private X509Certificate2 m_serverCertificate; + private CertificateTypesProvider m_certificateTypesProvider; private Nonce m_serverNonce; private string m_eccUserTokenSecurityPolicyUri; private Nonce m_eccUserTokenNonce; diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index 7af466800..d60320bae 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -64,44 +64,65 @@ public BinaryEncodingSupport EncodingSupport /// /// Finds the user token policy with the specified id and securtyPolicyUri /// - public UserTokenPolicy FindUserTokenPolicy(string policyId, string tokenSecurityPolicyUri) + public UserTokenPolicy FindUserTokenPolicy(string policyId) { - UserTokenPolicy sameEncryptionAlgorithm = null; - UserTokenPolicy unspecifiedSecPolicy = null; // The specified security policies take precedence foreach (UserTokenPolicy policy in endpointDescription.UserIdentityTokens) { if (policy.PolicyId == policyId) { - if (policy.SecurityPolicyUri == tokenSecurityPolicyUri) - { - return policy; - } - else if (( - policy.SecurityPolicyUri != null && - tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) - ) || - ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) - { - sameEncryptionAlgorithm ??= policy; - } - else if (policy.SecurityPolicyUri == null) - { - unspecifiedSecPolicy = policy; - } + return policy; } } - // The first token with the same encryption algorithm (RSA/ECC) follows - if (sameEncryptionAlgorithm != null) + return null; + } + + /// + /// Finds a token policy that matches the user identity specified. + /// + public UserTokenPolicy FindUserTokenPolicy( + UserTokenType tokenType, + XmlQualifiedName issuedTokenType, + string preferredSecurityPolicyUri, + string[] fallbackSecurityPolicyUris) + { + // Iterate twice: first for exact matches, then for relaxed matches. + foreach (bool exactMatch in new[] { true, false }) { - return sameEncryptionAlgorithm; + // Check preferred policy. + UserTokenPolicy match = FindUserTokenPolicy( + endpointDescription, + tokenType, + issuedTokenType, + preferredSecurityPolicyUri, + exactMatch); + + if (match != null) + { + return match; + } + + // Check fallback policies. + if (fallbackSecurityPolicyUris != null) + { + foreach (string policy in fallbackSecurityPolicyUris) + { + match = FindUserTokenPolicy( + endpointDescription, + tokenType, + issuedTokenType, + policy, + exactMatch); + + if (match != null) + { + return match; + } + } + } } - // The first token with unspecified security policy follows / no policy - return unspecifiedSecPolicy; + + return null; } /// @@ -110,20 +131,23 @@ public UserTokenPolicy FindUserTokenPolicy(string policyId, string tokenSecurity public UserTokenPolicy FindUserTokenPolicy( UserTokenType tokenType, XmlQualifiedName issuedTokenType, - string tokenSecurityPolicyUri) + string tokenSecurityPolicyUri, + bool matchSecurityPolicyUriExactly = false) { if (issuedTokenType == null) { return endpointDescription.FindUserTokenPolicy( tokenType, (string)null, - tokenSecurityPolicyUri); + tokenSecurityPolicyUri, + matchSecurityPolicyUriExactly); } return endpointDescription.FindUserTokenPolicy( tokenType, issuedTokenType.Namespace, - tokenSecurityPolicyUri); + tokenSecurityPolicyUri, + matchSecurityPolicyUriExactly); } /// @@ -132,7 +156,8 @@ public UserTokenPolicy FindUserTokenPolicy( public UserTokenPolicy FindUserTokenPolicy( UserTokenType tokenType, string issuedTokenType, - string tokenSecurityPolicyUri) + string tokenSecurityPolicyUri, + bool matchSecurityPolicyUriExactly = false) { // construct issuer type. string issuedTokenTypeText = issuedTokenType; @@ -151,6 +176,7 @@ public UserTokenPolicy FindUserTokenPolicy( return policy; } else if (( + !matchSecurityPolicyUriExactly && policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && @@ -162,12 +188,11 @@ public UserTokenPolicy FindUserTokenPolicy( { sameEncryptionAlgorithm ??= policy; } - else if (policy.SecurityPolicyUri == null) + else if ( + (!matchSecurityPolicyUriExactly || tokenSecurityPolicyUri == null)&& + policy.SecurityPolicyUri == null) { - if (sameEncryptionAlgorithm == null) - { - unspecifiedSecPolicy = policy; - } + unspecifiedSecPolicy = policy; } } } diff --git a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs index dfcdae124..436a33a4d 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs @@ -895,6 +895,16 @@ public virtual UserTokenPolicyCollection GetUserTokenPolicies( foreach (UserTokenPolicy policy in configuration.ServerConfiguration.UserTokenPolicies) { + if (policy.SecurityPolicyUri != null && + !configuration.SecurityConfiguration.SupportedSecurityPolicies.Contains(policy.SecurityPolicyUri)) + { + m_logger.LogWarning( + "The UserTokenPolicy {Policy} specifies a security policy {SecurityPolicyUri} that is not supported by the server. " + + "Please check if all necessary application certificates are configured", + policy.ToString(), + policy.SecurityPolicyUri); + } + var clone = (UserTokenPolicy)policy.Clone(); if (string.IsNullOrEmpty(policy.SecurityPolicyUri) && @@ -908,7 +918,18 @@ public virtual UserTokenPolicyCollection GetUserTokenPolicies( else { // ensure a security policy is specified for user tokens. - clone.SecurityPolicyUri = SecurityPolicies.Basic256Sha256; + if (configuration.SecurityConfiguration.SupportedSecurityPolicies.Contains(SecurityPolicies.Basic256Sha256)) + { + clone.SecurityPolicyUri = SecurityPolicies.Basic256Sha256; + } + else if (configuration.SecurityConfiguration.SupportedSecurityPolicies.Contains(SecurityPolicies.ECC_nistP256)) + { + clone.SecurityPolicyUri = SecurityPolicies.ECC_nistP256; + } + else + { + clone.SecurityPolicyUri = SecurityPolicies.Basic256Sha256; + } } } diff --git a/Tests/Opc.Ua.Client.Tests/MixedSecurityPolicyTests.cs b/Tests/Opc.Ua.Client.Tests/MixedSecurityPolicyTests.cs new file mode 100644 index 000000000..e77dc7d38 --- /dev/null +++ b/Tests/Opc.Ua.Client.Tests/MixedSecurityPolicyTests.cs @@ -0,0 +1,120 @@ +/* ======================================================================== + * 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.Linq; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Opc.Ua.Client.Tests +{ + [TestFixture] + [Category("Client")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + public class MixedSecurityPolicyTests : ClientTestFramework + { + public MixedSecurityPolicyTests() + : base(Utils.UriSchemeOpcTcp) + { + } + + [OneTimeSetUp] + public override Task OneTimeSetUpAsync() + { + return base.OneTimeSetUpAsync(); + } + + [OneTimeTearDown] + public override Task OneTimeTearDownAsync() + { + return base.OneTimeTearDownAsync(); + } + + [Test] + public Task Connect_ECCEndpoint_RSAUserTokenAsync() + { + return Connect_Endpoint_UserECCRSATokenAsync(true); + } + + [Test] + public Task Connect_RsaEndpoint_EccUserTokenAsync() + { + return Connect_Endpoint_UserECCRSATokenAsync(false); + } + + private async Task Connect_Endpoint_UserECCRSATokenAsync(bool useEccEndpoint) + { + // 1. Get endpoints to find the PolicyId + var endpointConfiguration = EndpointConfiguration.Create(ClientFixture.Config); + using DiscoveryClient discoveryClient = await DiscoveryClient.CreateAsync( + ServerUrl, + endpointConfiguration, + Telemetry).ConfigureAwait(false); + + EndpointDescriptionCollection endpoints = await discoveryClient.GetEndpointsAsync(null).ConfigureAwait(false); + + // 2. Find RSA endpoint + EndpointDescription rsaEndpoint = endpoints.FirstOrDefault(e => + (useEccEndpoint ^ !EccUtils.IsEccPolicy(e.SecurityPolicyUri)) && + e.SecurityMode == MessageSecurityMode.SignAndEncrypt); + + Assert.That(rsaEndpoint, Is.Not.Null, "RSA Endpoint with RSA Policy and SignAndEncrypt not found"); + + var configuredEndpoint = new ConfiguredEndpoint(null, rsaEndpoint, endpointConfiguration); + + // 3. Find ECC UserTokenPolicy + UserTokenPolicy userTokenPolicy = rsaEndpoint.UserIdentityTokens.FirstOrDefault(p => + p.TokenType == UserTokenType.UserName && + (useEccEndpoint ^ EccUtils.IsEccPolicy(p.SecurityPolicyUri))); + + Assert.That(userTokenPolicy, Is.Not.Null, $"UserTokenPolicy with ECC SecurityPolicy not found on RSA Endpoint."); + TestContext.WriteLine($"Using UserTokenPolicy: {userTokenPolicy.PolicyId} ({userTokenPolicy.SecurityPolicyUri})"); + + // 4. Create UserIdentity and enforce PolicyId + var identity = new UserIdentity("user1", System.Text.Encoding.UTF8.GetBytes("password")); + identity.PolicyId = userTokenPolicy.PolicyId; + + // 5. Connect + ISession session = await ClientFixture.ConnectAsync( + configuredEndpoint, + identity).ConfigureAwait(false); + + try + { + Assert.That(session, Is.Not.Null); + Assert.That(session.Connected, Is.True); + } + finally + { + await session.CloseAsync().ConfigureAwait(false); + } + } + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Server/ServerBaseTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Server/ServerBaseTests.cs index 79c8839cc..fba6913bc 100644 --- a/Tests/Opc.Ua.Core.Tests/Stack/Server/ServerBaseTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Stack/Server/ServerBaseTests.cs @@ -136,6 +136,26 @@ protected void OneTimeSetUp() ServerConfiguration = new ServerConfiguration() }; + configuration.SecurityConfiguration.ApplicationCertificates = new CertificateIdentifierCollection + { + new CertificateIdentifier + { + CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType + }, + new CertificateIdentifier + { + CertificateType = ObjectTypeIds.EccNistP256ApplicationCertificateType + }, + new CertificateIdentifier + { + CertificateType = ObjectTypeIds.EccNistP384ApplicationCertificateType + }, + new CertificateIdentifier + { + CertificateType = ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType + } + }; + // base addresses, uses localhost. specify multiple endpoints per protocol as per config. configuration.ServerConfiguration.BaseAddresses.Add( Utils.ReplaceLocalhost("opc.https://localhost:62540/UA/SampleServer"));