File: Tcp\SctRenewalRegressionTests.cs
Web Access
Project: ..\..\..\src\System.Private.ServiceModel\tests\Scenarios\Security\TransportSecurity\Security.TransportSecurity.IntegrationTests.csproj (Security.TransportSecurity.IntegrationTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.Threading;
using Infrastructure.Common;
using Xunit;
 
// Regression tests for dotnet/wcf#5883.
//
// On a NetTcp + TransportWithMessageCredential + Certificate session the client establishes
// a Secure Conversation Token (SCT). When the server's SessionKeyRenewalInterval elapses the
// next outgoing message triggers an RST/SCT/Renew exchange that is signed with the SCT-derived
// symmetric key (KeyedHashAlgorithm).
//
// Pre-fix, WSSecurityOneDotZeroSendSecurityHeader.CompletePrimarySignatureCore added a To-header
// reference for any signing key (symmetric or asymmetric). That had two visible effects on the
// renew exchange:
//   1. _toHeaderStream was consumed by the primary signature and then re-read for the endorsing
//      signature, which threw XmlException "Root element is missing." from inside
//      SignedXml.ComputeSignature and surfaced as a CommunicationObjectFaultedException.
//   2. The extra <Reference> on the symmetric primary signature is a shape that .NET Framework
//      4.8 servers reject with "The security protocol cannot verify the incoming message."
//
// The fix limits the To-header reference to the asymmetric primary signature path. This test
// hosts the service with a short SessionKeyRenewalInterval so the renew exchange is forced a
// few seconds in, then sends messages across the renewal boundary. Without the fix, the call
// that triggers the renew faults the channel.
public class SctRenewalRegressionTests : ConditionalWcfTest
{
    [WcfFact]
    [Issue(2870, OS = OSID.OSX)]
    [Condition(nameof(Root_Certificate_Installed),
               nameof(Client_Certificate_Installed),
               nameof(SSL_Available),
               nameof(Skip_CoreWCFService_FailedTest))]
    [OuterLoop]
    public static void NetTcp_TransWithMessCred_CertClientCredential_SctRenewal_Succeeds()
    {
        string clientCertThumb = null;
        EndpointAddress endpointAddress = null;
        ChannelFactory<IWcfService> factory = null;
        IWcfService serviceProxy = null;
 
        try
        {
            // *** SETUP *** \\
            // The server's SessionKeyRenewalInterval is configured to ~10 seconds. Run the
            // echo loop for ~25 seconds so we cross at least one renewal boundary.
            const int RenewalCrossingTotalSeconds = 25;
            const int CallIntervalSeconds = 2;
 
            NetTcpBinding binding = new NetTcpBinding(SecurityMode.TransportWithMessageCredential);
            binding.Security.Message.ClientCredentialType = MessageCredentialType.Certificate;
 
            endpointAddress = new EndpointAddress(new Uri(Endpoints.Tcp_SecModeTransWithMessCred_ClientCredTypeCert_Renew));
            clientCertThumb = ServiceUtilHelper.ClientCertificate.Thumbprint;
 
            factory = new ChannelFactory<IWcfService>(binding, endpointAddress);
            factory.Credentials.ClientCertificate.SetCertificate(
                StoreLocation.CurrentUser,
                StoreName.My,
                X509FindType.FindByThumbprint,
                clientCertThumb);
 
            serviceProxy = factory.CreateChannel();
            ((ICommunicationObject)serviceProxy).Open();
 
            // *** EXECUTE *** \\
            int callsSent = 0;
            int elapsed = 0;
            while (elapsed < RenewalCrossingTotalSeconds)
            {
                callsSent++;
                string testString = string.Format("Hello #{0}", callsSent);
                string result = serviceProxy.Echo(testString);
 
                Assert.Equal(testString, result);
 
                Thread.Sleep(TimeSpan.FromSeconds(CallIntervalSeconds));
                elapsed += CallIntervalSeconds;
            }
 
            // *** VALIDATE *** \\
            // The pre-fix bug manifested as the channel faulting at the renew boundary, so the
            // surviving signal is "all calls round-tripped and the channel is still Opened".
            Assert.Equal(CommunicationState.Opened, ((ICommunicationObject)serviceProxy).State);
 
            // *** CLEANUP *** \\
            ((ICommunicationObject)serviceProxy).Close();
            factory.Close();
        }
        finally
        {
            // *** ENSURE CLEANUP *** \\
            ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory);
        }
    }
}