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"));