Erroneous caching of SCHANNEL_CRED in .NET Framework leads to TLS handshake failures for clients using a client certificate that is also used as a server certificate

Erlend Graff 1 Reputation point
2022-08-26T12:08:40.713+00:00

I have discovered a bug in .NET Framework's SSPI SSL/TLS implementation, which implicates the static SslSessionsCache that is used to cache SCHANNEL_CRED handles.

The bug can be consistently reproduced on the latest version of .NET Framework 4.8 (I have tested release build 528049 on Windows 7 6.1.7601 SP1 and release build 528449 on Windows 11 21H2 22000.856).

The issue occurs when a certificate is attempted used as a client certificate, after it has already been used as a server certificate - specifically, when an SCHANNEL_CRED server credential is requested for the certificate specifying SslProtocols.None as the enabled SSL protocols, and afterwards a client credential is requested for the same certificate, also using specifying SslProtocols.None as the enabled SSL protocols. The problem does not occur if either the server or client specifies a specific SSL protocol (or a bitmask combination thereof), but since this is officially recommended against, I do not consider this as a viable workaround (the official documented recommendation, which is also implemented in C# code quality analyzers, is to always use SslProtocols.None to let the OS pick the best/latest available TLS version).

The issue manifests as the following exception being thrown by SslStream.AuthenticateAsClientAsync() (it also happens for higher-level APIs such as HttpClient, but then the low-level exception is obscured from the end-user, and it is not clear exactly what kind of failure this is):

System.Security.Authentication.AuthenticationException: A call to SSPI failed, see inner exception. ---> System.ComponentModel.Win32Exception: The client and server cannot communicate, because they do not possess a common algorithm  
   --- End of inner exception stack trace ---  
   at System.Net.Security.SslState.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, Exception exception)  
   at System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)  
   at System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest, Boolean renegotiation)  
   at System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)  
   at System.Net.Security.SslStream.BeginAuthenticateAsClient(String targetHost, X509CertificateCollection clientCertificates, SslProtocols enabledSslProtocols, Boolean checkCertificateRevocation, AsyncCallback asyncCallback, Object asyncState)  
   at System.Net.Security.SslStream.<>c__DisplayClass32_0.<AuthenticateAsClientAsync>b__0(AsyncCallback callback, Object state)  

I do not know precisely what is going wrong with the caching of or use of the cached SCHANNEL_CRED, but I have proven that the SslSessionsCache is implicated, because the problem does not happen if the SslSessionsCache is forcibly cleared after the server credential has been cached, before trying to request a client credential. Moreover, using WireShark, I have observed that when the exception occurs, the TLS handshake is never even started (a ClientHello is never placed on the wire), so the error happens prior to that. Finally, this problem does not reproduce on .NET 6.

Below is example code that reproduces the issue exactly. The code is slightly complicated by the need to generate certificates as a prerequisite for reproducing the issue. However, all the code for reproducing the issue is confined to a single method "ReproduceIssueAsync()". The example code executes 3 scenarios. In the first scenario, a client credential is requested for the certificate without first requesting a server credential, so no credential has been cached beforehand, and the code executes successfully. In the second scenario, I demonstrate a hacky workaround, using reflection to get a reference to the internal SslSessionsCache, so that after requesting a server credential (which is cached), the cache is cleared, and the following request for a client credential also succeeds. In the third scenario, a server credential is requested (and cached), and we do not clear the cache, so the subsequent attempt at acquiring a client credential fails.

The code is a stand-alone application that may be compiled for .NET Framework 4.8 or .NET 6.0, and reproduces the issue for .NET Framework:

// ReSharper disable ArgumentsStyleLiteral  
// ReSharper disable ArgumentsStyleOther  
// ReSharper disable ArgumentsStyleNamedExpression  
namespace CertCredCachingProblem  
{  
    using System;  
    using System.Net.Sockets;  
    using System.Net;  
    using System.Security.Cryptography;  
    using System.Security.Cryptography.X509Certificates;  
    using System.Threading.Tasks;  
  
    using Org.BouncyCastle.Crypto.Tls;  
  
    using CertificateRequest = System.Security.Cryptography.X509Certificates.CertificateRequest;  
    using Org.BouncyCastle.Security;  
    using System.Net.Security;  
    using System.Security.Authentication;  
    using System.Collections;  
    using System.Reflection;  
  
    internal class TlsClient : DefaultTlsClient  
    {  
        /// <inheritdoc />  
        public override Org.BouncyCastle.Crypto.Tls.TlsAuthentication GetAuthentication()  
        {  
            return new TlsAuthentication();  
        }  
  
        private class TlsAuthentication : Org.BouncyCastle.Crypto.Tls.TlsAuthentication  
        {  
            /// <inheritdoc />  
            public void NotifyServerCertificate(Certificate serverCertificate)  
            {  
            }  
  
            /// <inheritdoc />  
            public TlsCredentials GetClientCredentials(  
                Org.BouncyCastle.Crypto.Tls.CertificateRequest certificateRequest  
            )  
            {  
                return null;  
            }  
        }  
    }  
  
    internal class Program  
    {  
        private static readonly RSAParameters RsaParams = new RSAParameters  
        {  
            Modulus = new byte[]  
            {  
                0xd8, 0xff, 0xb0, 0xad, 0xe1, 0x4f, 0xf0, 0x1f, 0x07, 0x28, 0x6a, 0x70, 0x2a, 0x8d, 0x92, 0x91, 0xd1,  
                0x7f, 0x06, 0x1b, 0x51, 0xa3, 0x82, 0x48, 0x32, 0x67, 0xd5, 0x55, 0x2b, 0x1b, 0x53, 0x51, 0x24, 0x62,  
                0xd7, 0xac, 0xb9, 0xf4, 0x27, 0x00, 0xf4, 0x23, 0xbf, 0xa9, 0xb3, 0xc7, 0x13, 0xfb, 0x46, 0x44, 0x4e,  
                0xb4, 0x1a, 0x92, 0x4a, 0xfe, 0xc2, 0x39, 0x9b, 0xf3, 0x54, 0xee, 0x63, 0xd5, 0xa6, 0xcc, 0x85, 0xff,  
                0xd8, 0x13, 0x0e, 0x77, 0xda, 0x7b, 0x11, 0x49, 0xb9, 0xa0, 0x9b, 0x73, 0x18, 0x2b, 0xae, 0x1d, 0xe1,  
                0x8a, 0xfc, 0xfb, 0xd1, 0x6e, 0xa1, 0x8b, 0x7d, 0xb4, 0xde, 0xdc, 0xec, 0x58, 0xd8, 0x5a, 0x25, 0xfc,  
                0xd6, 0x19, 0xfb, 0xc5, 0xa4, 0x88, 0x5d, 0x74, 0x7d, 0x5c, 0xeb, 0x64, 0xeb, 0x85, 0x1f, 0xb2, 0x89,  
                0xc9, 0xb7, 0xeb, 0x68, 0xa6, 0x94, 0x18, 0xfd, 0x71, 0xce, 0x8d, 0x80, 0x9f, 0x52, 0x99, 0x4d, 0x9a,  
                0x7b, 0x22, 0xbf, 0x9e, 0xb8, 0x18, 0x49, 0xda, 0xce, 0xff, 0x24, 0x32, 0x7b, 0x61, 0x0d, 0x4b, 0xbd,  
                0x4e, 0xfe, 0xd3, 0x96, 0x49, 0xe1, 0xbf, 0xc2, 0x4f, 0x93, 0x7d, 0xcf, 0x35, 0xce, 0x80, 0xd2, 0x91,  
                0x31, 0xa2, 0xd6, 0xd7, 0xe2, 0xc2, 0x9b, 0x90, 0xae, 0xfa, 0x4c, 0x11, 0x0f, 0x9c, 0xf7, 0x59, 0xea,  
                0x81, 0x6b, 0xee, 0x4b, 0x43, 0x28, 0x88, 0x7e, 0x2c, 0xd8, 0xbd, 0x40, 0xc1, 0x2f, 0xf7, 0x78, 0x1d,  
                0x63, 0x78, 0x15, 0x88, 0x38, 0x5b, 0x37, 0xfb, 0xdf, 0x12, 0x43, 0xe6, 0x0b, 0x26, 0xd5, 0xfd, 0x68,  
                0xa6, 0x18, 0x05, 0x19, 0x26, 0x95, 0xed, 0x08, 0x37, 0xa8, 0xae, 0x6a, 0x83, 0x87, 0xc7, 0x68, 0xb6,  
                0x16, 0x44, 0x8a, 0xec, 0x21, 0x04, 0x71, 0x95, 0x96, 0x38, 0x08, 0x77, 0x51, 0xdc, 0xb4, 0xf7, 0xf3,  
                0xf9,  
            },  
            Exponent = new byte[] { 0x01, 0x00, 0x01 },  
            P = new byte[]  
            {  
                0xf2, 0x53, 0xd8, 0xc9, 0xe5, 0xd1, 0xc2, 0x58, 0xce, 0x8e, 0xdb, 0xbf, 0xfc, 0x8d, 0x5e, 0x0a, 0xc5,  
                0xfd, 0xdb, 0x4b, 0x27, 0x99, 0x23, 0x01, 0x4c, 0x22, 0x65, 0xdf, 0xfe, 0xc7, 0x29, 0x8a, 0xa7, 0xae,  
                0xe4, 0xdc, 0xc4, 0xfa, 0x6d, 0xa5, 0x7d, 0x73, 0xc3, 0xee, 0x1b, 0xa0, 0x06, 0x30, 0x50, 0xdd, 0x71,  
                0xff, 0xd6, 0x08, 0x17, 0xe5, 0x98, 0xfe, 0x1c, 0xb7, 0x0b, 0xef, 0x6f, 0xf5, 0xbe, 0xaa, 0xb8, 0x53,  
                0x2c, 0x26, 0xe8, 0xe8, 0x39, 0xaf, 0xd2, 0x8c, 0xe0, 0x87, 0x08, 0x9c, 0xf3, 0x60, 0xd8, 0xb1, 0xd9,  
                0x66, 0x93, 0xb9, 0x35, 0x11, 0xf5, 0xb6, 0xe2, 0x53, 0xb5, 0xa3, 0xdb, 0x1d, 0x75, 0x5c, 0xcb, 0x7c,  
                0x3b, 0x91, 0x2f, 0x01, 0xf1, 0x46, 0x6b, 0xe7, 0xc0, 0x08, 0xa5, 0x89, 0x8a, 0xac, 0x5b, 0xa3, 0x78,  
                0x12, 0x8e, 0x62, 0x0f, 0x3a, 0xf9, 0x88, 0x3d, 0x47,  
            },  
            Q = new byte[]  
            {  
                0xe5, 0x3d, 0xff, 0x6a, 0xcd, 0xf6, 0xb3, 0x2a, 0xd7, 0xee, 0x3a, 0x63, 0x30, 0x1c, 0x39, 0x51, 0x22,  
                0x78, 0xed, 0x28, 0xb8, 0x11, 0x94, 0x78, 0xfa, 0x70, 0xd6, 0xdc, 0xa2, 0xa5, 0x5f, 0x90, 0x4b, 0x08,  
                0x41, 0x3e, 0x73, 0x72, 0xe0, 0xde, 0x03, 0x3f, 0xb5, 0x07, 0xcc, 0x36, 0x46, 0x18, 0x6a, 0x4f, 0x8a,  
                0x30, 0xdd, 0xf9, 0x28, 0xb0, 0x3a, 0xd7, 0x9b, 0x04, 0x14, 0x7b, 0xab, 0x2b, 0xa9, 0xb9, 0x61, 0xc8,  
                0xcf, 0x59, 0x5c, 0x25, 0xdb, 0x05, 0x12, 0x97, 0x68, 0x7f, 0xad, 0x0c, 0xd2, 0x91, 0xfe, 0xa7, 0x10,  
                0x1c, 0xa0, 0xa6, 0x22, 0xc3, 0xa8, 0x2c, 0x82, 0xef, 0xd7, 0xd7, 0x47, 0xa7, 0xd8, 0x74, 0x12, 0xb0,  
                0xcd, 0x96, 0x6b, 0x9a, 0x50, 0xf0, 0xb1, 0xb6, 0xf1, 0x7c, 0x9c, 0xc5, 0x1a, 0x62, 0x24, 0x87, 0x99,  
                0x85, 0xa3, 0xad, 0x39, 0x36, 0xc3, 0xc9, 0xe4, 0xbf,  
            },  
            D = new byte[]  
            {  
                0xa6, 0xa4, 0xcd, 0x70, 0xea, 0xfb, 0xf1, 0xa2, 0x52, 0x63, 0xe6, 0x41, 0x97, 0x5c, 0x3b, 0x78, 0x02,  
                0x13, 0x73, 0x84, 0x1d, 0x50, 0xdd, 0x27, 0x46, 0x96, 0x58, 0xcd, 0x5c, 0x1a, 0x53, 0x04, 0x98, 0x55,  
                0xd3, 0xdd, 0x50, 0xbc, 0xc0, 0x0b, 0x4a, 0x71, 0xfd, 0xa9, 0x7c, 0x67, 0x60, 0xdf, 0xf2, 0x19, 0x58,  
                0xfb, 0x95, 0x00, 0x4d, 0xd9, 0x91, 0x1c, 0x9e, 0xb7, 0xe2, 0xbc, 0x64, 0x2c, 0xda, 0x38, 0x6c, 0x9b,  
                0x8a, 0xbb, 0x2f, 0xbc, 0x39, 0x2b, 0x93, 0x9e, 0x33, 0x90, 0xb4, 0x70, 0x51, 0xda, 0x91, 0x8f, 0x5e,  
                0xfa, 0xd6, 0xc7, 0x28, 0x11, 0xb6, 0xbb, 0xa1, 0xe0, 0xf9, 0xd9, 0x5d, 0x23, 0xe9, 0x9a, 0x69, 0x5b,  
                0xde, 0xab, 0xfb, 0x9e, 0xcf, 0x78, 0xed, 0x94, 0x1d, 0x05, 0xf3, 0xbb, 0xff, 0xe6, 0xae, 0xed, 0xf4,  
                0x44, 0xd6, 0x1a, 0x51, 0xb6, 0xc3, 0x3a, 0xe1, 0xbe, 0x4f, 0x44, 0xfa, 0x14, 0x4f, 0x3c, 0x81, 0x06,  
                0x1f, 0x6d, 0xcd, 0x57, 0x14, 0x40, 0x01, 0x91, 0xd4, 0xc6, 0x58, 0xf6, 0x6b, 0x2c, 0x3e, 0x81, 0x6a,  
                0x96, 0x4c, 0x3a, 0x46, 0xf7, 0x89, 0x30, 0xa0, 0x40, 0x25, 0x98, 0xb5, 0xc4, 0xea, 0x0d, 0x87, 0x06,  
                0x27, 0xe1, 0x9e, 0x76, 0x70, 0xb1, 0xcd, 0xf1, 0xa2, 0x86, 0x90, 0x61, 0x6b, 0x92, 0xc6, 0xe2, 0xa9,  
                0xff, 0x80, 0x54, 0x11, 0xed, 0x89, 0x5a, 0x29, 0x02, 0x8e, 0x74, 0x5b, 0xb3, 0x33, 0x37, 0x10, 0x19,  
                0x7f, 0x06, 0x1c, 0x22, 0x7f, 0x67, 0xca, 0xf6, 0xba, 0x6f, 0x8f, 0xf3, 0xd8, 0xd7, 0x81, 0xa6, 0xf0,  
                0x7c, 0x87, 0x79, 0xe5, 0x9c, 0xd5, 0xbc, 0xd3, 0x48, 0x6f, 0x34, 0x1f, 0x8b, 0x32, 0xef, 0xd9, 0xca,  
                0xf0, 0x52, 0xb9, 0x98, 0x3f, 0x6f, 0x23, 0x63, 0xb2, 0xcf, 0xf0, 0xde, 0xda, 0x84, 0xac, 0x04, 0x4a,  
                0xd5,  
            },  
            DP = new byte[]  
            {  
                0xbb, 0x39, 0xf3, 0x16, 0x52, 0xdd, 0x55, 0x06, 0x1e, 0x59, 0x9c, 0x09, 0x62, 0x8c, 0xaa, 0xeb, 0x31,  
                0xfc, 0x28, 0x11, 0x91, 0xff, 0xac, 0x5f, 0x15, 0x3e, 0xc2, 0x6d, 0x65, 0x40, 0xe5, 0xa4, 0xbe, 0x57,  
                0xcf, 0x75, 0x8f, 0x2f, 0x59, 0xc5, 0xf1, 0xfe, 0x9e, 0x93, 0xfa, 0x7e, 0x12, 0x2a, 0x04, 0x60, 0x83,  
                0xf2, 0xc1, 0xa0, 0x31, 0x2e, 0x70, 0x9d, 0x6c, 0xfc, 0x34, 0x59, 0x83, 0xac, 0x5f, 0xeb, 0x31, 0x4c,  
                0xf9, 0xa0, 0xfa, 0x74, 0x6a, 0x15, 0xa1, 0x5c, 0xbd, 0x21, 0x37, 0x93, 0x64, 0x2b, 0x20, 0x61, 0x90,  
                0xf1, 0xc3, 0x12, 0xe6, 0xa1, 0x00, 0xb2, 0x93, 0x7d, 0x4f, 0xaa, 0xd0, 0xe1, 0x9a, 0xca, 0xde, 0x61,  
                0x16, 0xf8, 0xde, 0x53, 0xe6, 0xe1, 0x9c, 0xff, 0x4a, 0x8c, 0xa3, 0xb1, 0x78, 0x16, 0x21, 0x1b, 0x54,  
                0xeb, 0x29, 0x5d, 0x34, 0x1d, 0x41, 0xac, 0x74, 0x83,  
            },  
            DQ = new byte[]  
            {  
                0xa8, 0x89, 0x3a, 0x1d, 0x15, 0xab, 0x87, 0xf1, 0xb9, 0xaa, 0xc5, 0x76, 0x62, 0xca, 0x7d, 0x41, 0x2f,  
                0x2c, 0xe4, 0x7f, 0x09, 0x44, 0xb3, 0x79, 0x75, 0xf6, 0x3b, 0xa1, 0x1e, 0x5a, 0xa2, 0xb5, 0x7c, 0xd4,  
                0x66, 0xd3, 0x39, 0x21, 0x7e, 0x3c, 0xfa, 0xfa, 0x7d, 0x67, 0x6c, 0x35, 0x82, 0xb7, 0x34, 0x81, 0xa1,  
                0xc1, 0x67, 0x90, 0x64, 0xdf, 0x9b, 0x83, 0x23, 0xce, 0x8e, 0x18, 0x95, 0xb1, 0x96, 0x28, 0x5a, 0xc1,  
                0xbd, 0xdf, 0x9e, 0xa5, 0x9e, 0x2e, 0x4e, 0x8a, 0xce, 0x22, 0xff, 0xe0, 0xeb, 0x76, 0xb6, 0x57, 0xb0,  
                0xba, 0xbb, 0x49, 0x29, 0x49, 0xdb, 0x7c, 0x4e, 0x0f, 0x73, 0x0a, 0x2c, 0xfe, 0x33, 0x5e, 0xb2, 0xd7,  
                0x15, 0x6e, 0xbf, 0x51, 0x46, 0xac, 0x8e, 0x9b, 0x47, 0x53, 0x2c, 0x16, 0xa4, 0xdc, 0xfe, 0xaa, 0x4a,  
                0xae, 0x3b, 0xb5, 0x80, 0xd8, 0xc8, 0x7c, 0xc8, 0x15,  
            },  
            InverseQ = new byte[]  
            {  
                0x1b, 0x05, 0x2a, 0x58, 0xa1, 0xe2, 0x3b, 0x15, 0x4b, 0xa8, 0x52, 0x6b, 0x1e, 0xf4, 0x56, 0x90, 0x03,  
                0xef, 0xa7, 0x32, 0x64, 0x7c, 0xf1, 0xb8, 0xbe, 0x99, 0xfd, 0xc0, 0x6d, 0x9b, 0xc8, 0x0d, 0xe1, 0xe6,  
                0xd7, 0x61, 0xf5, 0x66, 0x61, 0xd9, 0x6b, 0x54, 0xf4, 0x59, 0x6e, 0x11, 0xf0, 0x8b, 0xf9, 0x6a, 0x02,  
                0x20, 0xbe, 0x49, 0xe2, 0x38, 0x71, 0xf7, 0xf2, 0x8d, 0x24, 0x3b, 0x8c, 0x6a, 0x2a, 0x03, 0x3f, 0xbb,  
                0x38, 0x67, 0x18, 0xe5, 0x48, 0x5c, 0x48, 0x29, 0x91, 0x12, 0x45, 0xb2, 0xb1, 0xe9, 0xfc, 0x7d, 0xc2,  
                0x0e, 0xb9, 0x7a, 0xe3, 0x61, 0xf7, 0x51, 0x42, 0xcd, 0x87, 0xe0, 0x96, 0x0e, 0xcc, 0x24, 0x89, 0xd8,  
                0xba, 0xb9, 0x7f, 0x24, 0x10, 0xf9, 0x50, 0x23, 0x79, 0x9c, 0x75, 0x62, 0x79, 0x22, 0xe0, 0x9d, 0x19,  
                0x49, 0x2f, 0x61, 0x0c, 0x91, 0x83, 0x3c, 0x55, 0x51,  
            },  
        };  
  
        public static readonly X500DistinguishedName ReusedCertDn =  
            new X500DistinguishedName("CN=some.server.invalid");  
  
        public static readonly X500DistinguishedName OtherCertDn =  
            new X500DistinguishedName("CN=other.server.invalid");  
  
        private static DateTimeOffset GetUtcNowWithSecondResolution()  
        {  
            DateTime now = DateTime.UtcNow;  
            return new DateTime(  
                now.Year,  
                now.Month,  
                now.Day,  
                now.Hour,  
                now.Minute,  
                now.Second,  
                DateTimeKind.Utc  
            ).ToUniversalTime();  
        }  
  
        private static CertificateRequest CreateCertificateRequest(  
            RSA key,  
            X500DistinguishedName subjectName  
        )  
        {  
            var req = new CertificateRequest(  
                subjectName,  
                key,  
                HashAlgorithmName.SHA256,  
                RSASignaturePadding.Pkcs1  
            );  
  
            req.CertificateExtensions.Add(  
                new X509SubjectKeyIdentifierExtension(  
                    key: req.PublicKey,  
                    critical: false  
                )  
            );  
  
            req.CertificateExtensions.Add(  
                new X509KeyUsageExtension(  
                    keyUsages: X509KeyUsageFlags.DigitalSignature,  
                    critical: true  
                )  
            );  
  
            req.CertificateExtensions.Add(  
                new X509BasicConstraintsExtension(  
                    certificateAuthority: false,  
                    hasPathLengthConstraint: false,  
                    pathLengthConstraint: 0,  
                    critical: true  
                )  
            );  
  
            var extendedKeyUsages = new OidCollection  
            {  
                new Oid("1.3.6.1.5.5.7.3.1", "id-kp-serverAuth"),  
                new Oid("1.3.6.1.5.5.7.3.2", "id-kp-clientAuth"),  
            };  
  
            req.CertificateExtensions.Add(  
                new X509EnhancedKeyUsageExtension(  
                    enhancedKeyUsages: extendedKeyUsages,  
                    critical: true  
                )  
            );  
  
            return req;  
        }  
  
        private static X509Certificate2 CreateCert(  
            RSA key,  
            X500DistinguishedName subjectName,  
            byte[] serialNumber,  
            RSA issuerKey,  
            X500DistinguishedName issuerName,  
            DateTimeOffset notBefore,  
            DateTimeOffset notAfter  
        )  
        {  
            var signatureGenerator = X509SignatureGenerator.CreateForRSA(issuerKey, RSASignaturePadding.Pkcs1);  
  
            CertificateRequest req = CreateCertificateRequest(key, subjectName);  
  
            using (X509Certificate2 certWithoutKey = req.Create(  
                issuerName: issuerName,  
                generator: signatureGenerator,  
                notBefore: notBefore,  
                notAfter: notAfter,  
                serialNumber: serialNumber  
            ))  
            {  
                using (var certWithKey = certWithoutKey.CopyWithPrivateKey(key))  
                {  
                    // https://github.com/dotnet/runtime/issues/23749#issuecomment-388231655  
                    return new X509Certificate2(  
                        certWithKey.Export(X509ContentType.Pkcs12),  
                        (string)null,  
                        X509KeyStorageFlags.Exportable  
                    );  
                }  
            }  
        }  
  
        private static void GenerateCerts(  
            out X509Certificate2 reusedCert,  
            out X509Certificate2 otherCert  
        )  
        {  
            DateTimeOffset now = GetUtcNowWithSecondResolution();  
  
            RSA rsaKey = RSA.Create(RsaParams);  
  
            reusedCert = CreateCert(  
                key: rsaKey,  
                subjectName: ReusedCertDn,  
                serialNumber: new byte[] { 0x01 },  
                issuerKey: rsaKey,  
                issuerName: ReusedCertDn,  
                notBefore: now.AddYears(-10),  
                notAfter: now.AddMinutes(2)  
            );  
  
            otherCert = CreateCert(  
                key: rsaKey,  
                subjectName: OtherCertDn,  
                serialNumber: new byte[] { 0x02 },  
                issuerKey: rsaKey,  
                issuerName: OtherCertDn,  
                notBefore: now.AddYears(-10),  
                notAfter: now.AddYears(2)  
            );  
        }  
  
        private static async Task ReproduceIssueAsync(  
            X509Certificate2 reusedCert,  
            X509Certificate2 otherCert,  
            bool reproduceProblem,  
            bool clearCacheToFixProblem = false  
        )  
        {  
            var serverListenSocket = new TcpListener(IPAddress.Loopback, 0);  
            serverListenSocket.Start();  
  
            int serverPort = ((IPEndPoint)serverListenSocket.LocalEndpoint).Port;  
            Console.WriteLine("Server listening port: {0}", serverPort);  
  
            try  
            {  
                if (reproduceProblem)  
                {  
                    Task<TcpClient> serverSocketTask = serverListenSocket.AcceptTcpClientAsync();  
  
                    using (var clientSocket = new TcpClient())  
                    {  
                        await clientSocket.ConnectAsync(IPAddress.Loopback, serverPort).ConfigureAwait(false);  
                        Console.WriteLine("Client port: {0}", ((IPEndPoint)clientSocket.Client.LocalEndPoint).Port);  
  
                        using (TcpClient serverSocket = await serverSocketTask.ConfigureAwait(false))  
                        using (var serverSslStream = new SslStream(serverSocket.GetStream()))  
                        {  
                            Console.WriteLine("Connected TCP");  
  
                            Task serverHandshakeTask = Task.Run(  
                                () => serverSslStream.AuthenticateAsServerAsync(  
                                    reusedCert,  
                                    false,  
                                    SslProtocols.None,  
                                    false  
                                )  
                            );  
  
                            // Use BouncyCastle client here, to not implicate SCHANNEL for the client in this scenario  
                            var tlsClientProto = new TlsClientProtocol(  
                                clientSocket.GetStream(),  
                                new SecureRandom()  
                            );  
  
                            tlsClientProto.Connect(new TlsClient());  
                            await serverHandshakeTask.ConfigureAwait(false);  
  
                            Console.WriteLine("Handshake was successful!");  
                        }  
                    }  
  
#if NETFRAMEWORK  
                    if (clearCacheToFixProblem)  
                    {  
                        Type sslSessionsCacheType = typeof(SslStream).Assembly.GetType(  
                            "System.Net.Security.SslSessionsCache",  
                            true  
                        );  
                        if (sslSessionsCacheType == null)  
                        {  
                            throw new InvalidOperationException("Unable to clear cache");  
                        }  
  
                        FieldInfo fi = sslSessionsCacheType.GetField(  
                            "s_CachedCreds",  
                            BindingFlags.Static | BindingFlags.NonPublic  
                        );  
                        if (fi == null)  
                        {  
                            throw new InvalidOperationException("Unable to clear cache");  
                        }  
  
                        object val = fi.GetValue(null);  
                        if (val == null)  
                        {  
                            throw new InvalidOperationException("Unable to clear cache");  
                        }  
  
                        var hashTable = (Hashtable)val;  
                        hashTable.Clear();  
  
                        Console.WriteLine("Cache cleared!");  
                    }  
#endif  
                }  
  
                Task<TcpClient> otherServerSocketTask = serverListenSocket.AcceptTcpClientAsync();  
  
                using (var clientSocket = new TcpClient())  
                {  
                    await clientSocket.ConnectAsync(IPAddress.Loopback, serverPort).ConfigureAwait(false);  
                    Console.WriteLine("Client port: {0}", ((IPEndPoint)clientSocket.Client.LocalEndPoint).Port);  
  
                    using (TcpClient serverSocket = await otherServerSocketTask.ConfigureAwait(false))  
                    using (var serverSslStream = new SslStream(serverSocket.GetStream()))  
                    {  
                        Console.WriteLine("Connected TCP");  
  
                        Task serverHandshakeTask = Task.Run(  
                            () => serverSslStream.AuthenticateAsServerAsync(  
                                otherCert,  
                                false,  
                                SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12,  
                                false  
                            )  
                        );  
  
                        var clientCertificates = new X509Certificate2Collection();  
                        clientCertificates.Add(reusedCert);  
  
                        using (var clientSslStream = new SslStream(  
                            clientSocket.GetStream(),  
                            false,  
                            (sender, certificate, chain, errors) => true  
                        ))  
                        {  
                            Task clientHandshakeTask = Task.Run(  
                                () => clientSslStream.AuthenticateAsClientAsync(  
                                    "other.server.invalid",  
                                    clientCertificates,  
                                    SslProtocols.None,  
                                    false  
                                )  
                            );  
  
                            // In case we have an exception from server task, but client task has not completed  
                            await await Task.WhenAny(serverHandshakeTask, clientHandshakeTask).ConfigureAwait(false);  
  
                            await clientHandshakeTask.ConfigureAwait(false);  
                        }  
  
                        await serverHandshakeTask.ConfigureAwait(false);  
  
                        Console.WriteLine("Handshake was successful!");  
                    }  
                }  
            }  
            finally  
            {  
                serverListenSocket.Stop();  
            }  
        }  
  
        private static async Task Main()  
        {  
            GenerateCerts(  
                out X509Certificate2 reusedCert,  
                out X509Certificate2 otherCert  
            );  
  
            Console.WriteLine("Scenario 1 -- use client cert without cached credential (works)");  
  
            await ReproduceIssueAsync(reusedCert, otherCert, reproduceProblem: false).ConfigureAwait(false);  
  
            Console.WriteLine();  
            Console.WriteLine();  
            Console.WriteLine("Scenario 2 -- use client cert after caching invalid credential, but forcibly clear cache (works)");  
  
            await ReproduceIssueAsync(reusedCert, otherCert, reproduceProblem: true, clearCacheToFixProblem: true)  
                .ConfigureAwait(false);  
  
            Console.WriteLine();  
            Console.WriteLine();  
            Console.WriteLine("Scenario 3 -- use client cert after caching invalid credential (fails)");  
  
            await ReproduceIssueAsync(reusedCert, otherCert, reproduceProblem: true, clearCacheToFixProblem: false)  
                .ConfigureAwait(false);  
        }  
    }  
}  

I hope my description (and example code) is clear enough, and also hope the issue will be fixed in a security update to .NET Framework.

Kind regards,
Erlend Graff

Windows Server
Windows Server
A family of Microsoft server operating systems that support enterprise-level management, data storage, applications, and communications.
12,053 questions
.NET Runtime
.NET Runtime
.NET: Microsoft Technologies based on the .NET software framework.Runtime: An environment required to run apps that aren't compiled to machine language.
1,117 questions
Windows 11
Windows 11
A Microsoft operating system designed for productivity, creativity, and ease of use.
8,071 questions
{count} votes