/* ======================================================================== * 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.Linq; using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Security.Certificates; using Opc.Ua.Tests; namespace Opc.Ua.Server.Tests { [TestFixture] [Category("Session")] [Category("Security")] [NonParallelizable] public sealed class SessionSecurityTests { private const string AnonymousPolicyId = "anonymous"; private const string UserNamePolicyId = "username"; private static readonly ICertificateFactory s_certificateFactory = DefaultCertificateFactory.Instance; private ITelemetryContext m_telemetry = null!; private Mock m_server = null!; private Certificate m_serverCertificate = null!; private Certificate m_clientCertificate = null!; private Certificate m_otherClientCertificate = null!; [SetUp] public void SetUp() { m_telemetry = NUnitTelemetryContext.Create(); m_serverCertificate = CreateCertificate("CN=SessionSecurityServer"); m_clientCertificate = CreateCertificate("CN=SessionSecurityClient"); m_otherClientCertificate = CreateCertificate("CN=OtherSessionSecurityClient"); m_server = new Mock(); m_server.Setup(s => s.Telemetry).Returns(m_telemetry); m_server.Setup(s => s.NamespaceUris).Returns(new NamespaceTable()); m_server.Setup(s => s.SubscriptionStore).Returns((ISubscriptionStore)null!); m_server.Setup(s => s.MessageContext).Returns( ServiceMessageContext.CreateEmpty(m_telemetry)); var serverDiagnostics = new ServerDiagnosticsSummaryDataType(); m_server .Setup(s => s.UpdateServerDiagnostics( It.IsAny>())) .Callback>( update => update(serverDiagnostics)); var diagnostics = new Mock(); diagnostics .Setup(d => d.CreateSessionDiagnosticsAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(new NodeId(1001, 1))); diagnostics .Setup(d => d.DeleteSessionDiagnosticsAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(default(ValueTask)); m_server.Setup(s => s.DiagnosticsNodeManager).Returns(diagnostics.Object); m_server.Setup(s => s.DefaultSystemContext).Returns( new ServerSystemContext(m_server.Object)); } [TearDown] public void TearDown() { m_otherClientCertificate.Dispose(); m_clientCertificate.Dispose(); m_serverCertificate.Dispose(); } [Test] public async Task NewChannelWithSameUsernameIgnoresDisplayAliasChangesAsync() { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.SignAndEncrypt, includeUserName: true); int authenticationCount = 0; using SecuritySessionManager manager = CreateManager( _ => Interlocked.Increment(ref authenticationCount) == 1 ? "First display alias" : "Second display alias"); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, CreateUserNameToken("alice")).ConfigureAwait(false); OperationContext newContext = CreateContext( endpoint, "channel-2", m_clientCertificate); SignatureData signature = CreateClientSignature( newContext, created.ClientNonce, created.ServerNonce, m_clientCertificate); (bool _, ByteString newNonce, _) = await manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, signature, CreateUserNameToken("alice"), null, [], default).ConfigureAwait(false); Assert.That(newNonce.Length, Is.InRange(32, 128)); Assert.That(created.Result.Session.SecureChannelId, Is.EqualTo("channel-2")); AssertRequestRejectedOnOldChannel(created, newContext); } [Test] public async Task NewChannelWithDifferentCertificateIsRejectedAndOldChannelRemainsValidAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); OperationContext newContext = CreateContext( endpoint, "channel-2", m_otherClientCertificate); SignatureData signature = CreateClientSignature( newContext, created.ClientNonce, created.ServerNonce, m_clientCertificate); ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadSecurityChecksFailed)); AssertRequestAcceptedOnOriginalChannel(created); } [TestCase(false)] [TestCase(true)] public async Task SecuredReadOnlyLookupRejectsMissingOrDifferentChannelCertificateAsync(bool missingCertificate) { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); Assert.That(manager.TryGetSessionContext( created.Result.AuthenticationToken, created.Context.ChannelContext!, out var original), Is.True); var invalidChannel = new SecureChannelContext( "channel-1", endpoint, RequestEncoding.Binary, missingCertificate ? null : m_otherClientCertificate.RawData); Assert.That(manager.TryGetSessionContext( created.Result.AuthenticationToken, invalidChannel, out var rejected), Is.False); Assert.That(rejected, Is.Null); Assert.That(manager.TryGetSessionContext( created.Result.AuthenticationToken, created.Context.ChannelContext!, out var current), Is.True); Assert.That(current, Is.SameAs(original)); Assert.That(manager.HasSession("channel-1"), Is.True); } [Test] public async Task NewChannelWithDifferentUsernameIsRejectedWhenDisplayAliasMatchesAsync() { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.SignAndEncrypt, includeUserName: true); using SecuritySessionManager manager = CreateManager( _ => "Shared display alias"); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, CreateUserNameToken("alice")).ConfigureAwait(false); OperationContext newContext = CreateContext( endpoint, "channel-2", m_clientCertificate); SignatureData signature = CreateClientSignature( newContext, created.ClientNonce, created.ServerNonce, m_clientCertificate); ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, signature, CreateUserNameToken("alice-alias"), null, [], default).ConfigureAwait(false))!; Assert.That( exception.StatusCode, Is.EqualTo(StatusCodes.BadIdentityChangeNotSupported)); AssertRequestAcceptedOnOriginalChannel(created); } [Test] public void ClientUserIdUsesExactTokenSemantics() { var anonymousToken = new AnonymousIdentityTokenHandler(); Assert.That( ClientUserIdResolver.Resolve(anonymousToken, new UserIdentity()), Is.Null); var userNameToken = new UserNameIdentityTokenHandler("ExactUser", [1, 2, 3]); var aliasedUserName = new UserIdentity(userNameToken) { DisplayName = "Display Alias" }; Assert.That( ClientUserIdResolver.Resolve(userNameToken, aliasedUserName), Is.EqualTo("ExactUser")); var x509Token = new X509IdentityTokenHandler(new X509IdentityToken { CertificateData = m_clientCertificate.RawData.ToByteString() }); var aliasedCertificate = new UserIdentity(x509Token) { DisplayName = "Certificate Alias" }; Assert.That( ClientUserIdResolver.Resolve(x509Token, aliasedCertificate), Is.EqualTo(m_clientCertificate.Subject)); var issuedToken = new IssuedIdentityTokenHandler(new IssuedIdentityToken { PolicyId = Profiles.JwtUserToken }); var jwtIdentity = new JwtUserIdentity( issuedToken, new Dictionary(), [], [], "https://issuer.example/", "subject-42"); var roleBasedJwtIdentity = new RoleBasedIdentity( jwtIdentity, [Role.AuthenticatedUser], new NamespaceTable()); Assert.That( ClientUserIdResolver.Resolve(issuedToken, roleBasedJwtIdentity), Is.EqualTo("https://issuer.example/subject-42")); var jwtWithoutIssuer = new JwtUserIdentity( issuedToken, new Dictionary(), [], [], null, "subject-only"); Assert.That( ClientUserIdResolver.Resolve(issuedToken, jwtWithoutIssuer), Is.EqualTo("subject-only")); var jwtToken = new IssuedIdentityTokenHandler( Profiles.JwtUserToken, CreateJwt("https://issuer.example/", "parsed-subject")); var authenticatedJwt = new UserIdentity(jwtToken) { DisplayName = "Display Alias" }; Assert.That( ClientUserIdResolver.Resolve(jwtToken, authenticatedJwt), Is.EqualTo("https://issuer.example/parsed-subject")); } [Test] public void ClientUserIdRejectsIssuedIdentityWithoutStableOwner() { var issuedToken = new IssuedIdentityTokenHandler( Profiles.JwtUserToken, "opaque-token"u8); var aliasedIdentity = new UserIdentity(issuedToken) { DisplayName = "Shared display alias" }; ServiceResultException exception = Assert.Throws( () => ClientUserIdResolver.Resolve(issuedToken, aliasedIdentity))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid)); } [Test] public async Task ActivateSessionRejectsAnonymousWhenEndpointOmitsAnonymousPolicyAsync() { var endpoint = new EndpointDescription { EndpointUrl = "opc.tcp://localhost:4840/SessionSecurity", SecurityMode = MessageSecurityMode.SignAndEncrypt, SecurityPolicyUri = SecurityPolicies.Basic256Sha256, UserIdentityTokens = new[] { new UserTokenPolicy { PolicyId = UserNamePolicyId, TokenType = UserTokenType.UserName, SecurityPolicyUri = SecurityPolicies.None } }.ToArrayOf() }; using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenRejected)); } [Test] public async Task ActivationValidationRejectsMissingSecureChannelContextAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); var contextWithoutChannel = new OperationContext( new RequestHeader(), null, RequestType.ActivateSession, RequestLifetime.None); ServiceResultException exception = Assert.ThrowsAsync( async () => await created.Result.Session.ValidateBeforeActivateAsync( contextWithoutChannel, new SignatureData(), default, null!, default).ConfigureAwait(false))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid)); } [Test] public async Task ActivationValidationDecodesBinaryEncodedUserIdentityTokenAsync() { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.None, includeUserName: true, securityPolicyUri: SecurityPolicies.None); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); // Clients may send the UserIdentityToken as a raw binary body; the // Server has to decode it against the matching UserTokenPolicy. ExtensionObject binaryToken = EncodeAsBinaryBody(new UserNameIdentityToken { PolicyId = UserNamePolicyId, UserName = "alice", Password = ByteString.From([1, 2, 3]) }); (IUserIdentityTokenHandler identityToken, UserTokenPolicy? policy) = await created.Result.Session.ValidateBeforeActivateAsync( created.Context, signature, binaryToken, null!, default).ConfigureAwait(false); Assert.Multiple(() => { Assert.That(identityToken, Is.TypeOf()); Assert.That( ((UserNameIdentityToken)identityToken.Token).UserName, Is.EqualTo("alice")); Assert.That(policy!.PolicyId, Is.EqualTo(UserNamePolicyId)); }); } /// /// Verifies that decoded and binary identity tokens cannot select a policy for a different token type. /// [TestCase(UserTokenType.UserName, false)] [TestCase(UserTokenType.UserName, true)] [TestCase(UserTokenType.Certificate, false)] [TestCase(UserTokenType.Certificate, true)] public async Task ActivationRejectsIdentityTokenWithAnotherTokenTypesPolicyAsync( UserTokenType tokenType, bool binaryEncoded) { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.None, securityPolicyUri: SecurityPolicies.None); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); UserIdentityToken token = tokenType == UserTokenType.Certificate ? new X509IdentityToken { PolicyId = AnonymousPolicyId, CertificateData = m_clientCertificate.RawData.ToByteString() } : new UserNameIdentityToken { PolicyId = AnonymousPolicyId, UserName = "policy-mismatch", Password = ByteString.From([1, 2, 3]) }; ExtensionObject identity = binaryEncoded ? EncodeAsBinaryBody(token) : new ExtensionObject(token); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); ServiceResultException error = Assert.ThrowsAsync(async () => await created.Result.Session.ValidateBeforeActivateAsync( created.Context, signature, identity, new SignatureData(), CancellationToken.None).ConfigureAwait(false))!; Assert.That(error.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid)); } /// /// Verifies that certificate user authentication requires a valid signature even on an unsecured channel. /// [TestCase("null")] [TestCase("missing")] [TestCase("empty")] [TestCase("wrong")] [TestCase("valid")] public async Task CertificateUserPolicyVerifiesProofOnNoneChannelAsync(string proof) { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.None, securityPolicyUri: SecurityPolicies.None); endpoint.ServerCertificate = m_serverCertificate.RawData.ToByteString(); endpoint.UserIdentityTokens = endpoint.UserIdentityTokens.AddItem(new UserTokenPolicy { PolicyId = "certificate", TokenType = UserTokenType.Certificate, SecurityPolicyUri = SecurityPolicies.Basic256Sha256 }); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); var identity = new ExtensionObject(new X509IdentityToken { PolicyId = "certificate", CertificateData = m_clientCertificate.RawData.ToByteString() }); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); SecurityPolicyInfo policy = SecurityPolicies.Default.GetInfo(SecurityPolicies.Basic256Sha256)!; SecureChannelContext channel = created.Context.ChannelContext!; byte[] dataToSign = policy.GetUserTokenSignatureData( channel.ChannelThumbprint.ToArrayOrNull(), created.ServerNonce.ToArray(), m_serverCertificate.RawData, channel.ServerChannelCertificate.ToArrayOrNull(), m_clientCertificate.RawData, channel.ClientChannelCertificate.ToArrayOrNull(), created.ClientNonce.ToArray()); SignatureData userSignature = proof switch { "null" => null!, "missing" => new SignatureData(), "empty" => new SignatureData { Algorithm = SecurityAlgorithms.RsaSha256, Signature = ByteString.Empty }, _ => await SecurityPolicies.Default.CreateSignatureDataAsync( policy, proof == "valid" ? m_clientCertificate : m_otherClientCertificate, dataToSign, CancellationToken.None).ConfigureAwait(false) }; if (proof == "valid") { (IUserIdentityTokenHandler token, UserTokenPolicy? selected) = await created.Result.Session.ValidateBeforeActivateAsync( created.Context, signature, identity, userSignature, CancellationToken.None) .ConfigureAwait(false); Assert.That(token.TokenType, Is.EqualTo(UserTokenType.Certificate)); Assert.That(selected, Is.Not.Null); Assert.That(selected!.PolicyId, Is.EqualTo("certificate")); } else { ServiceResultException error = Assert.ThrowsAsync(async () => await created.Result.Session.ValidateBeforeActivateAsync( created.Context, signature, identity, userSignature, CancellationToken.None) .ConfigureAwait(false))!; Assert.That(error.StatusCode, Is.EqualTo(StatusCodes.BadUserSignatureInvalid)); } } /// /// Verifies that with an enhanced user token policy on a None channel the server /// expects the Part 4 6.1.8 None variant ServerNonce | HASH(ServerCertificate) | ClientNonce. /// [TestCase(true)] [TestCase(false)] public async Task EnhancedCertificateUserPolicyUsesNoneVariantOnNoneChannelAsync(bool noneVariant) { SecurityPolicyInfo? policy = SecurityPolicies.Default.GetInfo(SecurityPolicies.RSA_DH_AesGcm); if (policy == null) { Assert.Ignore("The RSA_DH_AesGcm security policy is not supported on this platform."); } EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.None, securityPolicyUri: SecurityPolicies.None); endpoint.ServerCertificate = m_serverCertificate.RawData.ToByteString(); endpoint.UserIdentityTokens = endpoint.UserIdentityTokens.AddItem(new UserTokenPolicy { PolicyId = "certificate", TokenType = UserTokenType.Certificate, SecurityPolicyUri = SecurityPolicies.RSA_DH_AesGcm }); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); var identity = new ExtensionObject(new X509IdentityToken { PolicyId = "certificate", CertificateData = m_clientCertificate.RawData.ToByteString() }); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); SecureChannelContext channel = created.Context.ChannelContext!; byte[] dataToSign = noneVariant ? policy!.GetUserTokenSignatureData( channel.ChannelThumbprint.ToArrayOrNull(), created.ServerNonce.ToArray(), m_serverCertificate.RawData, channel.ServerChannelCertificate.ToArrayOrNull(), m_clientCertificate.RawData, channel.ClientChannelCertificate.ToArrayOrNull(), created.ClientNonce.ToArray(), MessageSecurityMode.None) : policy!.GetUserTokenSignatureData( channel.ChannelThumbprint.ToArrayOrNull(), created.ServerNonce.ToArray(), m_serverCertificate.RawData, channel.ServerChannelCertificate.ToArrayOrNull(), m_clientCertificate.RawData, channel.ClientChannelCertificate.ToArrayOrNull(), created.ClientNonce.ToArray()); SignatureData userSignature = await SecurityPolicies.Default.CreateSignatureDataAsync( policy, m_clientCertificate, dataToSign, CancellationToken.None).ConfigureAwait(false); if (noneVariant) { (IUserIdentityTokenHandler token, UserTokenPolicy? selected) = await created.Result.Session.ValidateBeforeActivateAsync( created.Context, signature, identity, userSignature, CancellationToken.None) .ConfigureAwait(false); Assert.That(token.TokenType, Is.EqualTo(UserTokenType.Certificate)); Assert.That(selected!.PolicyId, Is.EqualTo("certificate")); } else { ServiceResultException error = Assert.ThrowsAsync(async () => await created.Result.Session.ValidateBeforeActivateAsync( created.Context, signature, identity, userSignature, CancellationToken.None) .ConfigureAwait(false))!; Assert.That(error.StatusCode, Is.EqualTo(StatusCodes.BadUserSignatureInvalid)); } } [Test] public async Task ActivationValidationRejectsBinaryTokenWithUnknownPolicyAsync() { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.None, includeUserName: true, securityPolicyUri: SecurityPolicies.None); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); ExtensionObject binaryToken = EncodeAsBinaryBody(new UserNameIdentityToken { PolicyId = "policy-that-does-not-exist", UserName = "alice", Password = ByteString.From([1, 2, 3]) }); ServiceResultException exception = Assert.ThrowsAsync( async () => await created.Result.Session.ValidateBeforeActivateAsync( created.Context, signature, binaryToken, null!, default).ConfigureAwait(false))!; // An unknown policy is Bad_IdentityTokenInvalid (Part 4 5.7.3.3), as on the // regular decoding path. Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid)); } [Test] public async Task ActivateSessionWrapsUnexpectedAuthenticationFailuresAsync() { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.SignAndEncrypt, includeUserName: true); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); manager.FailNextAuthentication( new InvalidOperationException("identity store unavailable")); // A non-ServiceResultException from an identity provider must surface // as Bad_IdentityTokenInvalid rather than escaping as an internal fault. ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, CreateUserNameToken("alice"), null, [], default).ConfigureAwait(false))!; Assert.Multiple(() => { Assert.That( exception.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid)); Assert.That( exception.InnerException, Is.TypeOf()); }); } [Test] public async Task ActivationSequenceIncreasesWithEveryActivationAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); long firstSequence = manager.ObservedActivationSequence; SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false); // Listeners persist activation state after the gate is released, so // they need a monotonic stamp to discard superseded writes. Assert.Multiple(() => { Assert.That(firstSequence, Is.GreaterThan(0)); Assert.That( manager.ObservedActivationSequence, Is.GreaterThan(firstSequence)); }); } [Test] public async Task RestoredTransferSecurityStateRejectsInconsistentArgumentsAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); ISession session = created.Result.Session; ByteString channelCertificate = ByteString.From(m_clientCertificate.RawData); Assert.Multiple(() => { Assert.That( () => manager.ApplyRestoredTransferSecurityState( null!, channelCertificate, SecurityPolicies.Basic256Sha256, MessageSecurityMode.SignAndEncrypt, UserTokenType.Anonymous, null), Throws.TypeOf()); Assert.That( () => manager.ApplyRestoredTransferSecurityState( session, channelCertificate, string.Empty, MessageSecurityMode.SignAndEncrypt, UserTokenType.Anonymous, null), Throws.TypeOf()); Assert.That( () => manager.ApplyRestoredTransferSecurityState( session, channelCertificate, SecurityPolicies.Basic256Sha256, MessageSecurityMode.SignAndEncrypt, (UserTokenType)42, "user1"), Throws.TypeOf()); Assert.That( () => manager.ApplyRestoredTransferSecurityState( session, channelCertificate, SecurityPolicies.Basic256Sha256, MessageSecurityMode.SignAndEncrypt, UserTokenType.Anonymous, "user1"), Throws.TypeOf()); Assert.That( () => manager.ApplyRestoredTransferSecurityState( session, channelCertificate, SecurityPolicies.Basic256Sha256, MessageSecurityMode.SignAndEncrypt, UserTokenType.UserName, null), Throws.TypeOf()); }); } [Test] public async Task NewChannelWithChangedSecurityModeIsRejectedAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); EndpointDescription changedEndpoint = CreateEndpoint(MessageSecurityMode.Sign); OperationContext newContext = CreateContext( changedEndpoint, "channel-2", m_clientCertificate); ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, new SignatureData(), default, null, [], default).ConfigureAwait(false))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected)); AssertRequestAcceptedOnOriginalChannel(created); } [Test] public async Task NewChannelWithChangedSecurityPolicyIsRejectedAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); EndpointDescription changedEndpoint = CreateEndpoint( MessageSecurityMode.SignAndEncrypt, securityPolicyUri: SecurityPolicies.Aes256_Sha256_RsaPss); OperationContext newContext = CreateContext( changedEndpoint, "channel-2", m_clientCertificate); ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, new SignatureData(), default, null, [], default).ConfigureAwait(false))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected)); AssertRequestAcceptedOnOriginalChannel(created); } [Test] public async Task AnonymousSessionCannotMoveToSignOnlyChannelAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.Sign); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); OperationContext newContext = CreateContext( endpoint, "channel-2", m_clientCertificate); SignatureData signature = CreateClientSignature( newContext, created.ClientNonce, created.ServerNonce, m_clientCertificate); ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false))!; Assert.That( exception.StatusCode, Is.EqualTo(StatusCodes.BadIdentityChangeNotSupported)); AssertRequestAcceptedOnOriginalChannel(created); } [Test] public async Task SecuredActivationRequiresCreateSessionClientCertificateAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); OperationContext context = CreateContext( endpoint, "channel-1", m_clientCertificate); ByteString clientNonce = ByteString.From(CreateBytes(32, 0x21)); CreateSessionResult result = await manager.CreateSessionAsync( context, m_serverCertificate, "MissingClientCertificate", clientNonce, new ApplicationDescription { ApplicationUri = "urn:test:missing-client-certificate", ApplicationName = new LocalizedText("Missing Client Certificate"), ApplicationType = ApplicationType.Client }, endpoint.EndpointUrl, null, [], 60_000, 64 * 1024, default).ConfigureAwait(false); ServiceResultException exception = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( context, result.AuthenticationToken, new SignatureData(), default, null, [], default).ConfigureAwait(false))!; Assert.That( exception.StatusCode, Is.EqualTo(StatusCodes.BadApplicationSignatureInvalid)); } [Test] public async Task SecuredIdentityCanUsePublicAsyncSessionValidationAsync() { EndpointDescription endpoint = CreateEndpoint( MessageSecurityMode.SignAndEncrypt, includeUserName: true); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); ISession session = created.Result.Session; (IUserIdentityTokenHandler identityToken, UserTokenPolicy? policy) = await session.ValidateBeforeActivateAsync( created.Context, signature, CreateUserNameToken("alice"), null!, default).ConfigureAwait(false); Assert.That(identityToken, Is.TypeOf()); Assert.That(policy!.PolicyId, Is.EqualTo(UserNamePolicyId)); } [Test] public async Task FailedFirstActivationDoesNotPoisonCreateSessionSecurityStateAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); OperationContext wrongContext = CreateContext( endpoint, "channel-2", m_otherClientCertificate); SignatureData wrongSignature = CreateClientSignature( wrongContext, created.ClientNonce, created.ServerNonce, m_clientCertificate); ServiceResultException rejected = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( wrongContext, created.Result.AuthenticationToken, wrongSignature, default, null, [], default).ConfigureAwait(false))!; Assert.That(rejected.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid)); SignatureData originalSignature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); (_, ByteString serverNonce, _) = await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, originalSignature, default, null, [], default).ConfigureAwait(false); Assert.That(serverNonce.Length, Is.InRange(32, 128)); } [Test] public async Task AuthenticationCancellationIsPropagatedAndDoesNotLockOutClientAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager( maxFailedAuthenticationAttempts: 1); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); var cancellation = new OperationCanceledException("authentication cancelled"); manager.CancelNextAuthentication(cancellation); OperationCanceledException propagated = Assert.ThrowsAsync( async () => await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false))!; Assert.That(propagated, Is.SameAs(cancellation)); (_, ByteString serverNonce, _) = await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false); Assert.That(serverNonce.Length, Is.InRange(32, 128)); } [Test] public async Task CloseSessionCancellationDuringActivationLeavesSessionAvailableAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); OperationContext newContext = CreateContext( endpoint, "channel-2", m_clientCertificate); SignatureData signature = CreateClientSignature( newContext, created.ClientNonce, created.ServerNonce, m_clientCertificate); var authenticationEntered = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var releaseAuthentication = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); manager.PauseNextAuthentication(authenticationEntered, releaseAuthentication); Task activation = manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, signature, default, null, [], default).AsTask(); await authenticationEntered.Task.ConfigureAwait(false); using var cancellation = new CancellationTokenSource(); Task closeOperation = manager.CloseSessionAsync( created.Result.SessionId, cancellation.Token).AsTask(); try { Assert.That(closeOperation.IsCompleted, Is.False); cancellation.Cancel(); Task completed = await Task.WhenAny( closeOperation, Task.Delay(TimeSpan.FromSeconds(5))).ConfigureAwait(false); Assert.That(completed, Is.SameAs(closeOperation)); Assert.CatchAsync( async () => await closeOperation.ConfigureAwait(false)); Assert.That( manager.GetSession(created.Result.AuthenticationToken), Is.SameAs(created.Result.Session)); } finally { releaseAuthentication.TrySetResult(true); } await activation.ConfigureAwait(false); await manager.CloseSessionAsync(created.Result.SessionId).ConfigureAwait(false); Assert.That(manager.GetSession(created.Result.AuthenticationToken), Is.Null); } [Test] public async Task SessionActivatedCallbackRunsAfterActivationGateIsReleasedAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); ValueTask closeOperation = default; bool completedInsideCallback = false; manager.SessionActivated += (session, _) => { closeOperation = manager.CloseSessionAsync(session.Id); completedInsideCallback = closeOperation.IsCompleted; }; await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false); await closeOperation.ConfigureAwait(false); Assert.That(completedInsideCallback, Is.True); Assert.That(manager.GetSession(created.Result.AuthenticationToken), Is.Null); } [Test] public async Task ActivationCommittedHookRunsAfterActivationGateIsReleasedAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateSessionAsync( manager, endpoint, "channel-1", m_clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); manager.ProbeGateOnNextActivation(); await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, default, null, [], default).ConfigureAwait(false); await manager.PendingCallbackOperation.ConfigureAwait(false); Assert.That(manager.CallbackObservedReleasedGate, Is.True); Assert.That(manager.GetSession(created.Result.AuthenticationToken), Is.Null); } [Test] public async Task ConcurrentActivationConsumesServerNonceOnceAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(); CreatedSession created = await CreateAndActivateAsync( manager, endpoint, "channel-1", m_clientCertificate, default).ConfigureAwait(false); OperationContext newContext = CreateContext( endpoint, "channel-2", m_clientCertificate); SignatureData signature = CreateClientSignature( newContext, created.ClientNonce, created.ServerNonce, m_clientCertificate); var authenticationEntered = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var releaseAuthentication = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); manager.PauseNextAuthentication(authenticationEntered, releaseAuthentication); Task first = CaptureAsync(manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, signature, default, null, [], default)); await authenticationEntered.Task.ConfigureAwait(false); Task second = CaptureAsync(manager.ActivateSessionAsync( newContext, created.Result.AuthenticationToken, signature, default, null, [], default)); releaseAuthentication.SetResult(true); Exception?[] exceptions = await Task.WhenAll(first, second).ConfigureAwait(false); Assert.That(exceptions.Count(e => e == null), Is.EqualTo(1)); ServiceResultException rejected = exceptions .OfType() .Single(); Assert.That( rejected.StatusCode, Is.EqualTo(StatusCodes.BadApplicationSignatureInvalid)); } /// /// Verifies that activation attempts from another channel cannot exhaust the victim application's lockout /// budget. /// [TestCase(false)] [TestCase(true)] public async Task WrongChannelActivationNeverChargesVictimLockoutAsync(bool differentPolicy) { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(maxFailedAuthenticationAttempts: 5); CreatedSession victim = await CreateSessionAsync( manager, endpoint, "victim-channel", m_clientCertificate).ConfigureAwait(false); EndpointDescription attackerEndpoint = differentPolicy ? CreateEndpoint(MessageSecurityMode.None, securityPolicyUri: SecurityPolicies.None) : endpoint; OperationContext attacker = CreateContext(attackerEndpoint, "attacker-channel", m_otherClientCertificate); var rejections = new List(); for (int i = 0; i < 5; i++) { ServiceResultException rejected = Assert.ThrowsAsync(async () => await manager.ActivateSessionAsync( attacker, victim.Result.AuthenticationToken, new SignatureData(), default, null!, [], default) .ConfigureAwait(false))!; rejections.Add(rejected.StatusCode); } SignatureData valid = CreateClientSignature( victim.Context, victim.ClientNonce, victim.ServerNonce, m_clientCertificate); (_, ByteString nonce, ServiceResult result) = await manager.ActivateSessionAsync( victim.Context, victim.Result.AuthenticationToken, valid, default, null!, [], default) .ConfigureAwait(false); Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good)); Assert.That(nonce.IsEmpty, Is.False); Assert.That(victim.Result.Session.Activated, Is.True); AssertRequestAcceptedOnOriginalChannel(victim); Assert.That(rejections, Is.All.EqualTo( differentPolicy ? StatusCodes.BadSecurityPolicyRejected : StatusCodes.BadSecureChannelIdInvalid)); } /// /// Verifies that invalid signatures lock out their originating application without blocking another /// application. /// [Test] public async Task InvalidSignaturesStillLockOutTheOriginatingApplicationAsync() { EndpointDescription endpoint = CreateEndpoint(MessageSecurityMode.SignAndEncrypt); using SecuritySessionManager manager = CreateManager(maxFailedAuthenticationAttempts: 5); CreatedSession created = await CreateSessionAsync( manager, endpoint, "client-channel", m_clientCertificate).ConfigureAwait(false); SignatureData invalid = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_otherClientCertificate); for (int i = 0; i < 5; i++) { ServiceResultException rejected = Assert.ThrowsAsync(async () => await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, invalid, default, null!, [], default) .ConfigureAwait(false))!; Assert.That(rejected.StatusCode, Is.EqualTo(StatusCodes.BadApplicationSignatureInvalid)); } SignatureData valid = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, m_clientCertificate); ServiceResultException lockedOut = Assert.ThrowsAsync(async () => await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, valid, default, null!, [], default) .ConfigureAwait(false))!; Assert.That(lockedOut.StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied)); OperationContext otherContext = CreateContext(endpoint, "other-channel", m_otherClientCertificate); ByteString otherNonce = ByteString.From(CreateBytes(32, 0x42)); CreateSessionResult other = await manager.CreateSessionAsync( otherContext, m_serverCertificate, "OtherApplication", otherNonce, new ApplicationDescription { ApplicationUri = "urn:test:other-application", ApplicationType = ApplicationType.Client }, endpoint.EndpointUrl, m_otherClientCertificate.AddRef(), [], 60_000, 64 * 1024, default) .ConfigureAwait(false); SignatureData otherSignature = CreateClientSignature( otherContext, otherNonce, other.ServerNonce, m_otherClientCertificate); await manager.ActivateSessionAsync( otherContext, other.AuthenticationToken, otherSignature, default, null!, [], default) .ConfigureAwait(false); Assert.That(other.Session.Activated, Is.True); } private SecuritySessionManager CreateManager( Func? canonicalize = null, int maxFailedAuthenticationAttempts = 0) { var configuration = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration { MinSessionTimeout = 1000, MaxSessionTimeout = 60_000, MaxSessionCount = 20, MaxFailedAuthenticationAttempts = maxFailedAuthenticationAttempts, MaxRequestAge = 60_000, MaxBrowseContinuationPoints = 10, MaxHistoryContinuationPoints = 10 } }; var manager = new SecuritySessionManager( m_server.Object, configuration, canonicalize); m_server.Setup(s => s.SessionManager).Returns(manager); return manager; } private async Task CreateAndActivateAsync( SecuritySessionManager manager, EndpointDescription endpoint, string channelId, Certificate clientCertificate, ExtensionObject userIdentityToken) { CreatedSession created = await CreateSessionAsync( manager, endpoint, channelId, clientCertificate).ConfigureAwait(false); SignatureData signature = CreateClientSignature( created.Context, created.ClientNonce, created.ServerNonce, clientCertificate); (_, ByteString serverNonce, _) = await manager.ActivateSessionAsync( created.Context, created.Result.AuthenticationToken, signature, userIdentityToken, null, [], default).ConfigureAwait(false); return created with { ServerNonce = serverNonce }; } private async Task CreateSessionAsync( SecuritySessionManager manager, EndpointDescription endpoint, string channelId, Certificate clientCertificate) { OperationContext context = CreateContext(endpoint, channelId, clientCertificate); ByteString clientNonce = ByteString.From(CreateBytes(32, 0x11)); CreateSessionResult result = await manager.CreateSessionAsync( context, m_serverCertificate, "SecuritySession", clientNonce, new ApplicationDescription { ApplicationUri = "urn:test:session-security-client", ApplicationName = new LocalizedText("Session Security Client"), ApplicationType = ApplicationType.Client }, endpoint.EndpointUrl, clientCertificate.AddRef(), [], 60_000, 64 * 1024, default).ConfigureAwait(false); return new CreatedSession(result, context, clientNonce, result.ServerNonce); } private SignatureData CreateClientSignature( OperationContext context, ByteString clientNonce, ByteString serverNonce, Certificate signingCertificate) { SecurityPolicyInfo securityPolicy = SecurityPolicies.Default.GetInfo( context.ChannelContext!.EndpointDescription!.SecurityPolicyUri!)!; byte[] dataToSign = securityPolicy.GetClientSignatureData( context.ChannelContext.ChannelThumbprint.ToArrayOrNull(), serverNonce.ToArray(), m_serverCertificate.RawData, context.ChannelContext.ServerChannelCertificate.ToArrayOrNull(), context.ChannelContext.ClientChannelCertificate.ToArrayOrNull(), clientNonce.ToArray()); return SecurityPolicies.Default.CreateSignatureData( securityPolicy, signingCertificate, dataToSign); } private OperationContext CreateContext( EndpointDescription endpoint, string channelId, Certificate clientCertificate) { var channelContext = new SecureChannelContext( channelId, endpoint, RequestEncoding.Binary, clientCertificate.RawData, m_serverCertificate.RawData, CreateBytes(32, 0x71)); return new OperationContext( new RequestHeader(), channelContext, RequestType.ActivateSession, RequestLifetime.None); } private static EndpointDescription CreateEndpoint( MessageSecurityMode securityMode, bool includeUserName = false, string securityPolicyUri = SecurityPolicies.Basic256Sha256) { var policies = new[] { new UserTokenPolicy { PolicyId = AnonymousPolicyId, TokenType = UserTokenType.Anonymous, SecurityPolicyUri = SecurityPolicies.None } }.ToList(); if (includeUserName) { policies.Add(new UserTokenPolicy { PolicyId = UserNamePolicyId, TokenType = UserTokenType.UserName, SecurityPolicyUri = SecurityPolicies.None }); } return new EndpointDescription { EndpointUrl = "opc.tcp://localhost:4840/SessionSecurity", SecurityMode = securityMode, SecurityPolicyUri = securityPolicyUri, UserIdentityTokens = policies.ToArrayOf() }; } private static ExtensionObject CreateUserNameToken(string userName) { return new ExtensionObject(new UserNameIdentityToken { PolicyId = UserNamePolicyId, UserName = userName, Password = ByteString.From([1, 2, 3]) }); } /// /// Wraps an identity token as an that carries /// the binary encoded body instead of the decoded instance. /// private ExtensionObject EncodeAsBinaryBody(UserIdentityToken token) { using var encoder = new BinaryEncoder(m_server.Object.MessageContext); token.Encode(encoder); byte[]? body = encoder.CloseAndReturnBuffer(); return new ExtensionObject( token.BinaryEncodingId, body is null ? ByteString.Empty : ByteString.From(body)); } private static Certificate CreateCertificate(string subject) { return s_certificateFactory .CreateCertificate(subject) .SetRSAKeySize(CertificateFactory.DefaultKeySize) .CreateForRSA(); } private static byte[] CreateBytes(int length, byte seed) { byte[] bytes = new byte[length]; for (int i = 0; i < bytes.Length; i++) { bytes[i] = (byte)(seed + i); } return bytes; } private static byte[] CreateJwt(string issuer, string subject) { static string Base64UrlEncode(string value) { return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(value)) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } string payload = $"{{\"iss\":\"{issuer}\",\"sub\":\"{subject}\"}}"; return System.Text.Encoding.UTF8.GetBytes( $"{Base64UrlEncode("{}")}.{Base64UrlEncode(payload)}.signature"); } private static void AssertRequestAcceptedOnOriginalChannel(CreatedSession created) { Assert.That( () => created.Result.Session.ValidateRequest( new RequestHeader(), created.Context.ChannelContext!, RequestType.Read), Throws.Nothing); } private static void AssertRequestRejectedOnOldChannel( CreatedSession created, OperationContext newContext) { ServiceResultException exception = Assert.Throws( () => created.Result.Session.ValidateRequest( new RequestHeader(), created.Context.ChannelContext!, RequestType.Read))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid)); Assert.That( () => created.Result.Session.ValidateRequest( new RequestHeader(), newContext.ChannelContext!, RequestType.Read), Throws.Nothing); } private static async Task CaptureAsync( ValueTask<( bool IdentityContextChanged, ByteString ServerNonce, ServiceResult ActivationStatus)> activation) { try { await activation.ConfigureAwait(false); return null; } catch (Exception exception) { return exception; } } private sealed record CreatedSession( CreateSessionResult Result, OperationContext Context, ByteString ClientNonce, ByteString ServerNonce); private sealed class SecuritySessionManager : SessionManager { public SecuritySessionManager( IServerInternal server, ApplicationConfiguration configuration, Func? canonicalize) : base(server, configuration) { m_canonicalize = canonicalize ?? (token => token.DisplayName); } public void PauseNextAuthentication( TaskCompletionSource entered, TaskCompletionSource release) { m_authenticationEntered = entered; m_releaseAuthentication = release; Volatile.Write(ref m_pauseNextAuthentication, 1); } public void CancelNextAuthentication(OperationCanceledException exception) { m_cancellationException = exception ?? throw new ArgumentNullException(nameof(exception)); } public void FailNextAuthentication(Exception exception) { m_authenticationFailure = exception ?? throw new ArgumentNullException(nameof(exception)); } public bool CallbackObservedReleasedGate { get; private set; } public long ObservedActivationSequence { get; private set; } public Task PendingCallbackOperation { get; private set; } = Task.CompletedTask; public void ProbeGateOnNextActivation() { Volatile.Write(ref m_probeGateOnNextActivation, 1); } /// /// Exposes the protected restore hook so its argument validation can /// be exercised without a full distributed Session restore. /// public void ApplyRestoredTransferSecurityState( ISession session, ByteString originalClientChannelCertificate, string securityPolicyUri, MessageSecurityMode securityMode, UserTokenType clientUserTokenType, string? clientUserId) { SetRestoredSessionTransferSecurityState( session, originalClientChannelCertificate, securityPolicyUri, securityMode, clientUserTokenType, clientUserId); } protected override async ValueTask<( IUserIdentity? Identity, IUserIdentity? EffectiveIdentity, ServiceResult? Error)> AuthenticateUserIdentityAsync( ISession session, IUserIdentityTokenHandler newIdentity, UserTokenPolicy? userTokenPolicy, EndpointDescription endpointDescription, CancellationToken cancellationToken) { OperationCanceledException? cancellation = Interlocked.Exchange( ref m_cancellationException, null); if (cancellation != null) { throw cancellation; } Exception? failure = Interlocked.Exchange( ref m_authenticationFailure, null); if (failure != null) { throw failure; } if (Interlocked.Exchange(ref m_pauseNextAuthentication, 0) == 1) { m_authenticationEntered!.SetResult(true); await m_releaseAuthentication!.Task .WaitAsync(cancellationToken) .ConfigureAwait(false); } var identity = new UserIdentity(newIdentity) { DisplayName = m_canonicalize(newIdentity) }; return (identity, identity, null); } protected override ValueTask OnSessionActivatedAsync( NodeId authenticationToken, ISession session, ByteString serverNonce, UserTokenType clientUserTokenType, string? clientUserId, long activationSequence, CancellationToken cancellationToken) { ObservedActivationSequence = activationSequence; if (Interlocked.Exchange(ref m_probeGateOnNextActivation, 0) == 1) { ValueTask closeOperation = CloseSessionAsync( session.Id, CancellationToken.None); CallbackObservedReleasedGate = closeOperation.IsCompleted; PendingCallbackOperation = closeOperation.AsTask(); } return default; } private readonly Func m_canonicalize; private TaskCompletionSource? m_authenticationEntered; private TaskCompletionSource? m_releaseAuthentication; private OperationCanceledException? m_cancellationException; private Exception? m_authenticationFailure; private int m_pauseNextAuthentication; private int m_probeGateOnNextActivation; } } }