/* ======================================================================== * 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.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Threading; using Microsoft.Extensions.Logging; namespace Opc.Ua.Server { /// /// A generic session manager object for a server. /// public class Session : ISession { /// /// Initializes a new instance of the class. /// /// The context. /// The Server object. /// The server certificate. /// The unique private identifier assigned to the Session. /// The client nonce. /// The server nonce. /// The name assigned to the Session. /// Application description for the client application. /// The endpoint URL. /// The client certificate. /// The client certifiate chain /// The session timeout. /// The maximum number of browse continuation points. /// The maximum number of history continuation points. public Session( OperationContext context, IServerInternal server, X509Certificate2 serverCertificate, NodeId authenticationToken, byte[] clientNonce, Nonce serverNonce, string sessionName, ApplicationDescription clientDescription, string endpointUrl, X509Certificate2 clientCertificate, X509Certificate2Collection clientCertificateChain, double sessionTimeout, int maxBrowseContinuationPoints, int maxHistoryContinuationPoints) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // verify that a secure channel was specified. if (context.ChannelContext == null) { throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid); } m_server = server ?? throw new ArgumentNullException(nameof(server)); m_logger = server.Telemetry.CreateLogger(); ClientNonce = clientNonce; m_serverNonce = serverNonce; m_sessionName = sessionName; m_serverCertificate = serverCertificate; ClientCertificate = clientCertificate; m_clientIssuerCertificates = clientCertificateChain; SecureChannelId = context.ChannelContext.SecureChannelId; MaxBrowseContinuationPoints = maxBrowseContinuationPoints; m_maxHistoryContinuationPoints = maxHistoryContinuationPoints; EndpointDescription = context.ChannelContext.EndpointDescription; // use anonymous the default identity. Identity = new UserIdentity(); // initialize diagnostics. DateTime now = DateTime.UtcNow; SessionDiagnostics = new SessionDiagnosticsDataType { SessionId = default, SessionName = sessionName, ClientDescription = clientDescription, ServerUri = null, EndpointUrl = endpointUrl, ActualSessionTimeout = sessionTimeout, ClientConnectionTime = now, ClientLastContactTime = now }; // initialize security diagnostics. m_securityDiagnostics = new SessionSecurityDiagnosticsDataType { SessionId = Id, ClientUserIdOfSession = Identity.DisplayName, AuthenticationMechanism = Identity.TokenType.ToString(), Encoding = context.ChannelContext.MessageEncoding.ToString() }; m_securityDiagnostics.ClientUserIdHistory.Add(Identity.DisplayName); EndpointDescription description = context.ChannelContext.EndpointDescription; if (description != null) { m_securityDiagnostics.TransportProtocol = new Uri(description.EndpointUrl).Scheme; m_securityDiagnostics.SecurityMode = EndpointDescription.SecurityMode; m_securityDiagnostics.SecurityPolicyUri = EndpointDescription.SecurityPolicyUri; } if (clientCertificate != null) { m_securityDiagnostics.ClientCertificate = clientCertificate.RawData; } ServerSystemContext systemContext = m_server.DefaultSystemContext.Copy(context); // create diagnostics object. Id = server.DiagnosticsNodeManager.CreateSessionDiagnostics( systemContext, SessionDiagnostics, OnUpdateDiagnostics, m_securityDiagnostics, OnUpdateSecurityDiagnostics); TraceState("CREATED"); } /// /// Frees any unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { List browseCPs = null; lock (m_lock) { browseCPs = m_browseContinuationPoints; m_browseContinuationPoints = null; } if (browseCPs != null) { for (int ii = 0; ii < browseCPs.Count; ii++) { Utils.SilentDispose(browseCPs[ii]); } } List historyCPs = null; lock (m_lock) { historyCPs = m_historyContinuationPoints; m_historyContinuationPoints = null; } if (historyCPs != null) { for (int ii = 0; ii < historyCPs.Count; ii++) { Utils.SilentDispose(historyCPs[ii].Value); } } } } /// /// Gets the identifier assigned to the session when it was created. /// public NodeId Id { get; } /// /// The user identity provided by the client. /// public IUserIdentity Identity { get; private set; } /// /// The application defined mapping for user identity provided by the client. /// public IUserIdentity EffectiveIdentity { get; private set; } /// /// The user identity token provided by the client. /// public UserIdentityToken IdentityToken { get; private set; } /// /// A lock which must be acquired before accessing the diagnostics. /// public object DiagnosticsLock => SessionDiagnostics; /// /// The diagnostics associated with the session. /// public SessionDiagnosticsDataType SessionDiagnostics { get; } /// /// The client Nonce associated with the session. /// public byte[] ClientNonce { get; } /// /// The application instance certificate associated with the client. /// public X509Certificate2 ClientCertificate { get; } /// /// The locales requested when the session was created. /// public string[] PreferredLocales { get; private set; } /// /// Whether the session timeout has elapsed since the last communication from the client. /// public bool HasExpired { get { lock (DiagnosticsLock) { return SessionDiagnostics.ClientLastContactTime.AddMilliseconds( SessionDiagnostics.ActualSessionTimeout ) < DateTime.UtcNow; } } } /// /// The last time the session was contacted by the client. /// public DateTime ClientLastContactTime { get { lock (DiagnosticsLock) { return SessionDiagnostics.ClientLastContactTime; } } } /// /// Whether the session has been activated. /// public bool Activated { get; private set; } /// /// Set the ECC security policy URI /// public virtual void SetEccUserTokenSecurityPolicy(string securityPolicyUri) { lock (m_lock) { m_eccUserTokenSecurityPolicyUri = securityPolicyUri; m_eccUserTokenNonce = null; } } /// /// Create new ECC ephemeral key /// /// A new ephemeral key public virtual EphemeralKeyType GetNewEccKey() { lock (m_lock) { if (m_eccUserTokenSecurityPolicyUri == null) { return null; } m_eccUserTokenNonce = Nonce.CreateNonce(m_eccUserTokenSecurityPolicyUri); var key = new EphemeralKeyType { PublicKey = m_eccUserTokenNonce.Data }; key.Signature = EccUtils.Sign( new ArraySegment(key.PublicKey), m_serverCertificate, m_eccUserTokenSecurityPolicyUri); return key; } } /// /// Returns the session's endpoint /// public EndpointDescription EndpointDescription { get; } /// /// Returns the session's SecureChannelId /// public string SecureChannelId { get; private set; } /// /// allow derived classes access /// protected int MaxBrowseContinuationPoints { get; set; } /// /// Validates the request. /// /// is null. /// public virtual void ValidateRequest(RequestHeader requestHeader, SecureChannelContext secureChannelContext, RequestType requestType) { if (requestHeader == null) { throw new ArgumentNullException(nameof(requestHeader)); } lock (m_lock) { if (secureChannelContext == null || !IsSecureChannelValid(secureChannelContext.SecureChannelId)) { UpdateDiagnosticCounters(requestType, true, true); throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid); } // verify that session has been activated. if (!Activated && requestType != RequestType.CloseSession) { UpdateDiagnosticCounters(requestType, true, true); throw new ServiceResultException(StatusCodes.BadSessionNotActivated); } // request accepted. UpdateDiagnosticCounters(requestType, false, false); } } /// /// Validate the diagnostic info. /// public virtual void ValidateDiagnosticInfo(RequestHeader requestHeader) { const uint additionalInfoDiagnosticsMask = (uint)( DiagnosticsMasks.ServiceAdditionalInfo | DiagnosticsMasks.OperationAdditionalInfo); if ((requestHeader.ReturnDiagnostics & additionalInfoDiagnosticsMask) != 0) { NodeIdCollection currentRoleIds = EffectiveIdentity?.GrantedRoleIds; if ((currentRoleIds?.Contains(ObjectIds.WellKnownRole_SecurityAdmin)) == true || (currentRoleIds?.Contains(ObjectIds.WellKnownRole_ConfigureAdmin)) == true) { requestHeader.ReturnDiagnostics |= (uint)DiagnosticsMasks.UserPermissionAdditionalInfo; } } } /// /// Checks if the secure channel is currently valid. /// public virtual bool IsSecureChannelValid(string secureChannelId) { lock (m_lock) { return SecureChannelId == secureChannelId; } } /// /// Updates the requested locale ids. /// /// true if the new locale ids are different from the old locale ids. /// is null. public bool UpdateLocaleIds(StringCollection localeIds) { if (localeIds == null) { throw new ArgumentNullException(nameof(localeIds)); } lock (m_lock) { string[] ids = [.. localeIds]; if (!Utils.IsEqual(ids, PreferredLocales)) { PreferredLocales = ids; // update diagnostics. lock (DiagnosticsLock) { SessionDiagnostics.LocaleIds = [.. localeIds]; } return true; } return false; } } /// /// Activates the session and binds it to the current secure channel. /// /// public void ValidateBeforeActivate( OperationContext context, SignatureData clientSignature, ExtensionObject userIdentityToken, SignatureData userTokenSignature, out UserIdentityToken identityToken, out UserTokenPolicy userTokenPolicy) { lock (m_lock) { // verify that a secure channel was specified. if (context.ChannelContext == null) { throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid); } // verify that the same security policy has been used. EndpointDescription endpoint = context.ChannelContext.EndpointDescription; if (endpoint.SecurityPolicyUri != EndpointDescription.SecurityPolicyUri || endpoint.SecurityMode != EndpointDescription.SecurityMode) { throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); } // verify the client signature. if (ClientCertificate != null) { if (EndpointDescription.SecurityPolicyUri != SecurityPolicies.None && clientSignature != null && clientSignature.Signature == null) { throw new ServiceResultException( StatusCodes.BadApplicationSignatureInvalid); } byte[] dataToSign = Utils.Append( m_serverCertificate.RawData, m_serverNonce.Data); if (!SecurityPolicies.Verify( ClientCertificate, EndpointDescription.SecurityPolicyUri, dataToSign, clientSignature)) { // verify for certificate chain in endpoint. // validate the signature with complete chain if the check with leaf certificate failed. X509Certificate2Collection serverCertificateChain = Utils.ParseCertificateChainBlob( EndpointDescription.ServerCertificate, m_server.Telemetry); if (serverCertificateChain.Count > 1) { var serverCertificateChainList = new List(); for (int i = 0; i < serverCertificateChain.Count; i++) { serverCertificateChainList.AddRange( serverCertificateChain[i].RawData); } byte[] serverCertificateChainData = [.. serverCertificateChainList]; dataToSign = Utils.Append( serverCertificateChainData, m_serverNonce.Data); if (!SecurityPolicies.Verify( ClientCertificate, EndpointDescription.SecurityPolicyUri, dataToSign, clientSignature)) { throw new ServiceResultException( StatusCodes.BadApplicationSignatureInvalid); } } else { throw new ServiceResultException( StatusCodes.BadApplicationSignatureInvalid); } } } if (!Activated) { // must active the session on the channel that was used to create it. if (SecureChannelId != context.ChannelContext.SecureChannelId) { throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid); } } // validate the user identity token. identityToken = ValidateUserIdentityToken( userIdentityToken, userTokenSignature, out userTokenPolicy); TraceState("VALIDATED"); } } /// /// Activates the session and binds it to the current secure channel. /// public bool Activate( OperationContext context, UserIdentityToken identityToken, IUserIdentity identity, IUserIdentity effectiveIdentity, StringCollection localeIds, Nonce serverNonce) { lock (m_lock) { // update user identity. bool changed = false; if (identityToken != null && UpdateUserIdentity(identityToken, identity, effectiveIdentity)) { changed = true; } // update local ids. if (UpdateLocaleIds(localeIds)) { changed = true; } if (!Activated) { // toggle the activated flag. Activated = true; TraceState("FIRST ACTIVATION"); } else { // bind to the new secure channel. SecureChannelId = context.ChannelContext.SecureChannelId; TraceState("RE-ACTIVATION"); } // update server nonce. m_serverNonce = serverNonce; // update the contact time. lock (DiagnosticsLock) { SessionDiagnostics.ClientLastContactTime = DateTime.UtcNow; } // indicate whether the user context has changed. return changed; } } /// /// Closes a session and removes itself from the address space. /// public void Close() { TraceState("CLOSED"); m_server.DiagnosticsNodeManager .DeleteSessionDiagnostics(m_server.DefaultSystemContext, Id); } /// /// Saves a continuation point for a session. /// /// /// If the session has too many continuation points the oldest one is dropped. /// /// is null. public void SaveContinuationPoint(ContinuationPoint continuationPoint) { if (continuationPoint == null) { throw new ArgumentNullException(nameof(continuationPoint)); } lock (m_lock) { m_browseContinuationPoints ??= []; // remove the first continuation point if too many points. while (m_browseContinuationPoints.Count >= MaxBrowseContinuationPoints) { ContinuationPoint cp = m_browseContinuationPoints[0]; m_browseContinuationPoints.RemoveAt(0); Utils.SilentDispose(cp); } // add to end of list. m_browseContinuationPoints.Add(continuationPoint); } } /// /// Restores a continuation point for a session. /// /// /// The caller is responsible for disposing the continuation point returned. /// public ContinuationPoint RestoreContinuationPoint(byte[] continuationPoint) { lock (m_lock) { if (m_browseContinuationPoints == null) { return null; } if (continuationPoint == null || continuationPoint.Length != 16) { return null; } var id = new Guid(continuationPoint); for (int ii = 0; ii < m_browseContinuationPoints.Count; ii++) { if (m_browseContinuationPoints[ii].Id == id) { ContinuationPoint cp = m_browseContinuationPoints[ii]; m_browseContinuationPoints.RemoveAt(ii); return cp; } } return null; } } /// /// Saves a continuation point used for historical reads. /// /// The identifier for the continuation point. /// The continuation point. /// /// If the continuationPoint implements IDisposable it will be disposed when /// the Session is closed or discarded. /// /// is null. public void SaveHistoryContinuationPoint(Guid id, object continuationPoint) { if (continuationPoint == null) { throw new ArgumentNullException(nameof(continuationPoint)); } lock (m_lock) { m_historyContinuationPoints ??= []; // remove existing continuation point if space needed. while (m_historyContinuationPoints.Count >= m_maxHistoryContinuationPoints) { HistoryContinuationPoint oldCP = m_historyContinuationPoints[0]; m_historyContinuationPoints.RemoveAt(0); Utils.SilentDispose(oldCP.Value); } // create the cp. var cp = new HistoryContinuationPoint { Id = id, Value = continuationPoint, Timestamp = DateTime.UtcNow }; m_historyContinuationPoints.Add(cp); } } /// /// Saves a continuation point used for historical reads unless the request /// that creates it already holds all continuation points of the session. /// /// /// OPC 10000-4 7.9: "A Server shall process the operations until it uses the /// maximum number of continuation points in this response. Once that happens /// the Server shall return a Bad_NoContinuationPoints error for any remaining /// operations." and "A Server shall automatically free ContinuationPoints from /// prior requests from a Session if they are needed to process a new request /// from this Session." /// When the session holds MaxHistoryContinuationPoints points, the oldest point /// of a prior request is freed. If all stored points belong to the request /// identified by , nothing is freed or saved and /// the method returns false; the caller then reports /// Bad_NoContinuationPoints for the operation. A of /// 0 is not associated with a request and behaves like /// . /// If the continuationPoint implements IDisposable it will be disposed when /// it is freed or when the Session is closed or discarded. /// /// The identifier for the continuation point. /// The continuation point. /// The of the request. /// true if the continuation point was saved. /// is null. public bool TrySaveHistoryContinuationPoint(Guid id, object continuationPoint, uint requestId) { if (continuationPoint == null) { throw new ArgumentNullException(nameof(continuationPoint)); } lock (m_lock) { m_historyContinuationPoints ??= []; while (m_maxHistoryContinuationPoints > 0 && m_historyContinuationPoints.Count >= m_maxHistoryContinuationPoints) { int index = m_historyContinuationPoints.FindIndex( cp => requestId == 0 || cp.RequestId != requestId); if (index < 0) { return false; } HistoryContinuationPoint oldCP = m_historyContinuationPoints[index]; m_historyContinuationPoints.RemoveAt(index); Utils.SilentDispose(oldCP.Value); } m_historyContinuationPoints.Add(new HistoryContinuationPoint { Id = id, Value = continuationPoint, Timestamp = DateTime.UtcNow, RequestId = requestId }); return true; } } /// /// Restores a previously saves history continuation point. /// /// The identifier for the continuation point. /// The save continuation point. null if not found. public object RestoreHistoryContinuationPoint(byte[] continuationPoint) { lock (m_lock) { if (m_historyContinuationPoints == null) { return null; } if (continuationPoint == null || continuationPoint.Length != 16) { return null; } var id = new Guid(continuationPoint); for (int ii = 0; ii < m_historyContinuationPoints.Count; ii++) { HistoryContinuationPoint cp = m_historyContinuationPoints[ii]; if (cp.Id == id) { m_historyContinuationPoints.RemoveAt(ii); return cp.Value; } } return null; } } /// /// Stores a continuation point used for historial reads. /// private class HistoryContinuationPoint { public Guid Id; public object Value; public DateTime Timestamp; public uint RequestId; } /// /// Dumps the current state of the session queue. /// internal void TraceState(string context) { // Legacy event source logging ServerUtils.EventLog.SessionState( context, Id.ToString(), m_sessionName, SecureChannelId, Identity?.DisplayName ?? "(none)"); m_logger.LogInformation( "Session {Context}, Id={SessionId}, Name={Name}, ChannelId={ChannelId}, User={User}", context, Id.ToString(), m_sessionName, SecureChannelId, Identity?.DisplayName ?? "(none)"); } /// /// Returns a copy of the current diagnostics. /// private ServiceResult OnUpdateDiagnostics( ISystemContext context, NodeState node, ref object value) { lock (DiagnosticsLock) { value = Utils.Clone(SessionDiagnostics); } return ServiceResult.Good; } /// /// Returns a copy of the current security diagnostics. /// private ServiceResult OnUpdateSecurityDiagnostics( ISystemContext context, NodeState node, ref object value) { lock (DiagnosticsLock) { value = Utils.Clone(m_securityDiagnostics); } return ServiceResult.Good; } /// /// Validates the identity token supplied by the client. /// /// private UserIdentityToken ValidateUserIdentityToken( ExtensionObject identityToken, SignatureData userTokenSignature, out UserTokenPolicy policy) { policy = null; // check for empty token. if (identityToken == null || identityToken.Body == null || identityToken.Body.GetType() == typeof(AnonymousIdentityToken)) { // check if an anonymous login is permitted. if (EndpointDescription.UserIdentityTokens != null && EndpointDescription.UserIdentityTokens.Count > 0) { bool found = false; for (int ii = 0; ii < EndpointDescription.UserIdentityTokens.Count; ii++) { if (EndpointDescription.UserIdentityTokens[ii] .TokenType == UserTokenType.Anonymous) { found = true; policy = EndpointDescription.UserIdentityTokens[ii]; break; } } if (!found) { throw ServiceResultException.Create( StatusCodes.BadIdentityTokenRejected, "Anonymous user token policy not supported."); } } // create an anonymous token to use for subsequent validation. return new AnonymousIdentityToken { PolicyId = policy.PolicyId }; } UserIdentityToken token; // check for unrecognized token. if (!typeof(UserIdentityToken).IsInstanceOfType(identityToken.Body)) { //handle the use case when the UserIdentityToken is binary encoded over xml message encoding if (identityToken.Encoding == ExtensionObjectEncoding.Binary && typeof(byte[]).IsInstanceOfType(identityToken.Body)) { if (BaseVariableState.DecodeExtensionObject( null, typeof(UserIdentityToken), identityToken, false) is not UserIdentityToken newToken) { throw ServiceResultException.Create( StatusCodes.BadUserAccessDenied, "Invalid user identity token provided."); } policy = EndpointDescription.FindUserTokenPolicy( newToken.PolicyId, EndpointDescription.SecurityPolicyUri); if (policy == null) { throw ServiceResultException.Create( StatusCodes.BadUserAccessDenied, "User token policy not supported.", "Opc.Ua.Server.Session.ValidateUserIdentityToken"); } switch (policy.TokenType) { case UserTokenType.Anonymous: token = BaseVariableState.DecodeExtensionObject( null, typeof(AnonymousIdentityToken), identityToken, true ) as AnonymousIdentityToken; break; case UserTokenType.UserName: token = BaseVariableState.DecodeExtensionObject( null, typeof(UserNameIdentityToken), identityToken, true ) as UserNameIdentityToken; break; case UserTokenType.Certificate: token = BaseVariableState.DecodeExtensionObject( null, typeof(X509IdentityToken), identityToken, true ) as X509IdentityToken; break; case UserTokenType.IssuedToken: token = BaseVariableState.DecodeExtensionObject( null, typeof(IssuedIdentityToken), identityToken, true ) as IssuedIdentityToken; break; default: throw ServiceResultException.Create( StatusCodes.BadUserAccessDenied, "Invalid user identity token provided."); } } else { throw ServiceResultException.Create( StatusCodes.BadUserAccessDenied, "Invalid user identity token provided."); } } else { // get the token. token = (UserIdentityToken)identityToken.Body; } // find the user token policy. policy = EndpointDescription.FindUserTokenPolicy( token.PolicyId, EndpointDescription.SecurityPolicyUri); if (policy == null) { throw ServiceResultException.Create( StatusCodes.BadIdentityTokenInvalid, "User token policy not supported."); } if (token is IssuedIdentityToken issuedToken && policy.IssuedTokenType == Profiles.JwtUserToken) { issuedToken.IssuedTokenType = IssuedTokenType.JWT; } // determine the security policy uri. string securityPolicyUri = policy.SecurityPolicyUri; if (string.IsNullOrEmpty(securityPolicyUri)) { securityPolicyUri = EndpointDescription.SecurityPolicyUri; } if (ServerBase.RequireEncryption(EndpointDescription)) { // decrypt the token. if (m_serverCertificate == null) { m_serverCertificate = X509CertificateLoader.LoadCertificate( EndpointDescription.ServerCertificate); // check for valid certificate. if (m_serverCertificate == null) { throw ServiceResultException.Create( StatusCodes.BadConfigurationError, "ApplicationCertificate cannot be found."); } } try { token.Decrypt( m_serverCertificate, m_serverNonce, securityPolicyUri, m_server.MessageContext, m_eccUserTokenNonce, ClientCertificate, m_clientIssuerCertificates); } catch (Exception e) { // Every decryption, padding, length or nonce error must look the // same to the client (OPC 10000-4 7.40.2.1). The cause is logged // here only: returning it as the status or the inner result would // let a client tell the failures apart and use the server key as // a decryption oracle. m_logger.LogWarning( e, "Could not decrypt the user identity token of session {SessionId} (SecurityPolicyUri={SecurityPolicyUri}).", Id, securityPolicyUri); throw ServiceResultException.Create( StatusCodes.BadIdentityTokenInvalid, "Could not decrypt identity token."); } // verify the signature. if (securityPolicyUri != SecurityPolicies.None) { byte[] dataToSign = Utils.Append( m_serverCertificate.RawData, m_serverNonce.Data); if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri, m_server.Telemetry)) { // verify for certificate chain in endpoint. // validate the signature with complete chain if the check with leaf certificate failed. X509Certificate2Collection serverCertificateChain = Utils.ParseCertificateChainBlob( EndpointDescription.ServerCertificate, m_server.Telemetry); if (serverCertificateChain.Count > 1) { var serverCertificateChainList = new List(); for (int i = 0; i < serverCertificateChain.Count; i++) { serverCertificateChainList.AddRange( serverCertificateChain[i].RawData); } byte[] serverCertificateChainData = [.. serverCertificateChainList]; dataToSign = Utils.Append( serverCertificateChainData, m_serverNonce.Data); if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri, m_server.Telemetry)) { throw new ServiceResultException( StatusCodes.BadIdentityTokenRejected, "Invalid user signature!"); } } else { throw new ServiceResultException( StatusCodes.BadIdentityTokenRejected, "Invalid user signature!"); } } } } // validate user identity token. return token; } /// /// Updates the user identity. /// /// true if the new identity is different from the old identity. /// is null. private bool UpdateUserIdentity( UserIdentityToken identityToken, IUserIdentity identity, IUserIdentity effectiveIdentity) { if (identityToken == null) { throw new ArgumentNullException(nameof(identityToken)); } lock (m_lock) { bool changed = EffectiveIdentity == null && effectiveIdentity != null; if (EffectiveIdentity != null) { changed = !EffectiveIdentity.Equals(effectiveIdentity); } // always save the new identity since it may have additional information that does not affect equality. IdentityToken = identityToken; Identity = identity; EffectiveIdentity = effectiveIdentity; // update diagnostics. lock (DiagnosticsLock) { m_securityDiagnostics.ClientUserIdOfSession = identity.DisplayName; m_securityDiagnostics.AuthenticationMechanism = identity.TokenType.ToString(); m_securityDiagnostics.ClientUserIdHistory.Add(identity.DisplayName); } return changed; } } /// /// Updates the diagnostic counters associated with the request. /// /// private void UpdateDiagnosticCounters( RequestType requestType, bool error, bool authorizationError) { ServiceCounterDataType counter = null; lock (DiagnosticsLock) { if (!error) { SessionDiagnostics.ClientLastContactTime = DateTime.UtcNow; } SessionDiagnostics.TotalRequestCount.TotalCount++; if (error) { SessionDiagnostics.TotalRequestCount.ErrorCount++; if (authorizationError) { SessionDiagnostics.UnauthorizedRequestCount++; } } switch (requestType) { case RequestType.Read: counter = SessionDiagnostics.ReadCount; break; case RequestType.HistoryRead: counter = SessionDiagnostics.HistoryReadCount; break; case RequestType.Write: counter = SessionDiagnostics.WriteCount; break; case RequestType.HistoryUpdate: counter = SessionDiagnostics.HistoryUpdateCount; break; case RequestType.Call: counter = SessionDiagnostics.CallCount; break; case RequestType.CreateMonitoredItems: counter = SessionDiagnostics.CreateMonitoredItemsCount; break; case RequestType.ModifyMonitoredItems: counter = SessionDiagnostics.ModifyMonitoredItemsCount; break; case RequestType.SetMonitoringMode: counter = SessionDiagnostics.SetMonitoringModeCount; break; case RequestType.SetTriggering: counter = SessionDiagnostics.SetTriggeringCount; break; case RequestType.DeleteMonitoredItems: counter = SessionDiagnostics.DeleteMonitoredItemsCount; break; case RequestType.CreateSubscription: counter = SessionDiagnostics.CreateSubscriptionCount; break; case RequestType.ModifySubscription: counter = SessionDiagnostics.ModifySubscriptionCount; break; case RequestType.SetPublishingMode: counter = SessionDiagnostics.SetPublishingModeCount; break; case RequestType.Publish: counter = SessionDiagnostics.PublishCount; break; case RequestType.Republish: counter = SessionDiagnostics.RepublishCount; break; case RequestType.TransferSubscriptions: counter = SessionDiagnostics.TransferSubscriptionsCount; break; case RequestType.DeleteSubscriptions: counter = SessionDiagnostics.DeleteSubscriptionsCount; break; case RequestType.AddNodes: counter = SessionDiagnostics.AddNodesCount; break; case RequestType.AddReferences: counter = SessionDiagnostics.AddReferencesCount; break; case RequestType.DeleteNodes: counter = SessionDiagnostics.DeleteNodesCount; break; case RequestType.DeleteReferences: counter = SessionDiagnostics.DeleteReferencesCount; break; case RequestType.Browse: counter = SessionDiagnostics.BrowseCount; break; case RequestType.BrowseNext: counter = SessionDiagnostics.BrowseNextCount; break; case RequestType.TranslateBrowsePathsToNodeIds: counter = SessionDiagnostics.TranslateBrowsePathsToNodeIdsCount; break; case RequestType.QueryFirst: counter = SessionDiagnostics.QueryFirstCount; break; case RequestType.QueryNext: counter = SessionDiagnostics.QueryNextCount; break; case RequestType.RegisterNodes: counter = SessionDiagnostics.RegisterNodesCount; break; case RequestType.UnregisterNodes: counter = SessionDiagnostics.UnregisterNodesCount; break; case RequestType.Unknown: case RequestType.FindServers: case RequestType.GetEndpoints: case RequestType.CreateSession: case RequestType.ActivateSession: case RequestType.CloseSession: case RequestType.Cancel: break; default: throw ServiceResultException.Unexpected( $"Unexpected RequestType {requestType}"); } if (counter != null) { counter.TotalCount++; if (error) { counter.ErrorCount++; } } } if (counter != null) { m_server.SessionManager.RaiseSessionDiagnosticsChangedEvent(this); } } private readonly Lock m_lock = new(); private readonly ILogger m_logger; private readonly IServerInternal m_server; private readonly string m_sessionName; private X509Certificate2 m_serverCertificate; private Nonce m_serverNonce; private string m_eccUserTokenSecurityPolicyUri; private Nonce m_eccUserTokenNonce; private readonly X509Certificate2Collection m_clientIssuerCertificates; private readonly int m_maxHistoryContinuationPoints; private readonly SessionSecurityDiagnosticsDataType m_securityDiagnostics; private List m_browseContinuationPoints; private List m_historyContinuationPoints; } }