diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs index c3cb2b1bdf0eb..1bfaff24c229b 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs @@ -306,7 +306,8 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth if (!Interop.Ssl.Capabilities.Tls13Supported || string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) || sslAuthenticationOptions.CertificateContext != null || - sslAuthenticationOptions.CertSelectionDelegate != null) + sslAuthenticationOptions.ClientCertificates?.Count > 0 || + sslAuthenticationOptions.CertSelectionDelegate != null) { cacheSslContext = false; } diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs index 2a69e63149f24..7cd0a1b36bd19 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs @@ -134,7 +134,7 @@ internal bool TryAddSession(IntPtr namePtr, IntPtr session) if (!string.IsNullOrEmpty(targetName)) { // We do this only for lookup in RemoveSession. - // Since this is part of chache manipulation and no function impact it is done here. + // Since this is part of cache manipulation and no function impact it is done here. // This will use strdup() so it is safe to pass in raw pointer. Interop.Ssl.SessionSetHostname(session, namePtr); diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs index a7adc8755b77c..eaaf058e9d173 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -64,6 +64,7 @@ internal enum ContextAttribute SECPKG_ATTR_ISSUER_LIST_EX = 0x59, // returns SecPkgContext_IssuerListInfoEx SECPKG_ATTR_CLIENT_CERT_POLICY = 0x60, // sets SecPkgCred_ClientCertCtlPolicy SECPKG_ATTR_CONNECTION_INFO = 0x5A, // returns SecPkgContext_ConnectionInfo + SECPKG_ATTR_SESSION_INFO = 0x5D, // sets SecPkgContext_SessionInfo SECPKG_ATTR_CIPHER_INFO = 0x64, // returns SecPkgContext_CipherInfo SECPKG_ATTR_REMOTE_CERT_CHAIN = 0x67, // returns PCCERT_CONTEXT SECPKG_ATTR_UI_INFO = 0x68, // sets SEcPkgContext_UiInfo @@ -331,6 +332,21 @@ internal unsafe struct SecPkgCred_ClientCertPolicy public char* pwszSslCtlIdentifier; } + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct SecPkgContext_SessionInfo + { + public uint dwFlags; + public uint cbSessionId; + public fixed byte rgbSessionId[32]; + + [Flags] + public enum Flags + { + Zero = 0, + SSL_SESSION_RECONNECT = 0x01, + }; + } + [LibraryImport(Interop.Libraries.SspiCli, SetLastError = true)] internal static partial int EncryptMessage( ref CredHandle contextHandle, diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs index 12603f7df6ae9..10a2a7af6ab75 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Runtime.InteropServices; using System.Security.Authentication.ExtendedProtection; +using System.Security.Cryptography.X509Certificates; using Microsoft.Win32.SafeHandles; namespace System.Net.Security @@ -310,10 +311,15 @@ public static unsafe int AcquireCredentialsHandle( internal sealed class SafeFreeCredential_SECURITY : SafeFreeCredentials { +#pragma warning disable 0649 + // This is used only by SslStream but it is included elsewhere + public X509Certificate? LocalCertificate; +#pragma warning restore 0649 public SafeFreeCredential_SECURITY() : base() { } protected override bool ReleaseHandle() { + LocalCertificate?.Dispose(); return Interop.SspiCli.FreeCredentialsHandle(ref _handle) == 0; } } diff --git a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Android.cs b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Android.cs index 2141373d41025..bc5cdba291562 100644 --- a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Android.cs +++ b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Android.cs @@ -92,7 +92,7 @@ internal static SslPolicyErrors VerifyCertificateProperties( } // Check if the local certificate has been sent to the peer during the handshake. - internal static bool IsLocalCertificateUsed(SafeDeleteContext? securityContext) + internal static bool IsLocalCertificateUsed(SafeFreeCredentials? _, SafeDeleteContext? securityContext) { SafeSslHandle? sslContext = ((SafeDeleteSslContext?)securityContext)?.SslContext; if (sslContext == null) diff --git a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs index 1f3fce1d7d863..3bd0c7142c3fc 100644 --- a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs @@ -104,7 +104,7 @@ internal static SslPolicyErrors VerifyCertificateProperties( // This is only called when we selected local client certificate. // Currently this is only when Apple crypto asked for it. - internal static bool IsLocalCertificateUsed(SafeDeleteContext? _) => true; + internal static bool IsLocalCertificateUsed(SafeFreeCredentials? _1, SafeDeleteContext? _2) => true; // // Used only by client SSL code, never returns null. diff --git a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs index 26f47491a12ba..700901e13e8dd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs @@ -97,7 +97,7 @@ internal static SslPolicyErrors VerifyCertificateProperties( // This is only called when we selected local client certificate. // Currently this is only when OpenSSL needs it because peer asked. - internal static bool IsLocalCertificateUsed(SafeDeleteContext? _) => true; + internal static bool IsLocalCertificateUsed(SafeFreeCredentials? _1, SafeDeleteContext? _2) => true; // // Used only by client SSL code, never returns null. diff --git a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Windows.cs index 3830727d40b68..f0e58802dbda5 100644 --- a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.Windows.cs @@ -8,6 +8,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; +using static Interop.SspiCli; namespace System.Net { @@ -90,8 +91,23 @@ internal static SslPolicyErrors VerifyCertificateProperties( } // Check that local certificate was used by schannel. - internal static bool IsLocalCertificateUsed(SafeDeleteContext securityContext) + internal static bool IsLocalCertificateUsed(SafeFreeCredentials? _credentialsHandle, SafeDeleteContext securityContext) { + SecPkgContext_SessionInfo info = default; + // fails on Server 2008 and older. We will fall-back to probing LOCAL_CERT_CONTEXT in that case. + if (SSPIWrapper.QueryBlittableContextAttributes( + GlobalSSPI.SSPISecureChannel, + securityContext, + Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SESSION_INFO, + ref info) && + ((SecPkgContext_SessionInfo.Flags)info.dwFlags).HasFlag(SecPkgContext_SessionInfo.Flags.SSL_SESSION_RECONNECT)) + { + // This is TLS Resumed session. Windows can fail to query the local cert bellow. + // Instead, we will determine the usage form used credentials. + SafeFreeCredential_SECURITY creds = (SafeFreeCredential_SECURITY)_credentialsHandle!; + return creds.LocalCertificate != null; + } + SafeFreeCertContext? localContext = null; try { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs index e18a73fbb3d91..843805c078789 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs @@ -512,7 +512,7 @@ private bool CompleteHandshake(ref ProtocolToken? alertToken, out SslPolicyError return true; } - if (_selectedClientCertificate != null && !CertificateValidationPal.IsLocalCertificateUsed(_securityContext!)) + if (_selectedClientCertificate != null && !CertificateValidationPal.IsLocalCertificateUsed(_credentialsHandle, _securityContext!)) { // We may select client cert but it may not be used. // This is primarily an issue on Windows with credential caching. diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index cf6e16e9e501d..839cb0f639e94 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -30,8 +30,6 @@ public partial class SslStream private int _trailerSize = 16; private int _maxDataSize = 16354; - private bool _refreshCredentialNeeded = true; - private static readonly Oid s_serverAuthOid = new Oid("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.1"); private static readonly Oid s_clientAuthOid = new Oid("1.3.6.1.5.5.7.3.2", "1.3.6.1.5.5.7.3.2"); @@ -104,11 +102,6 @@ internal bool RemoteCertRequired } } - internal void SetRefreshCredentialNeeded() - { - _refreshCredentialNeeded = true; - } - internal void CloseContext() { if (!_remoteCertificateExposed) @@ -489,6 +482,7 @@ private string[] GetRequestCertificateAuthorities() if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Selected cert = {selectedCert}"); _selectedClientCertificate = clientCertificate; + return selectedCert; } @@ -529,7 +523,7 @@ This will not restart a session but helps minimizing the number of handles we cr --*/ - private bool AcquireClientCredentials(ref byte[]? thumbPrint) + private bool AcquireClientCredentials(ref byte[]? thumbPrint, bool newCredentialsRequested = false) { // Acquire possible Client Certificate information and set it on the handle. @@ -594,7 +588,7 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint) _sslAuthenticationOptions.CertificateContext ??= SslStreamCertificateContext.Create(selectedCert!); } - _credentialsHandle = AcquireCredentialsHandle(_sslAuthenticationOptions); + _credentialsHandle = AcquireCredentialsHandle(_sslAuthenticationOptions, newCredentialsRequested); thumbPrint = guessedThumbPrint; // Delay until here in case something above threw. } } @@ -689,8 +683,11 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint) // byte[] guessedThumbPrint = selectedCert.GetCertHash(); bool sendTrustedList = _sslAuthenticationOptions.CertificateContext!.Trust?._sendTrustInHandshake ?? false; - SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential(guessedThumbPrint, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy, sendTrustedList); - + SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential(guessedThumbPrint, + _sslAuthenticationOptions.EnabledSslProtocols, + _sslAuthenticationOptions.IsServer, + _sslAuthenticationOptions.EncryptionPolicy, + sendTrustedList); if (cachedCredentialHandle != null) { _credentialsHandle = cachedCredentialHandle; @@ -705,9 +702,9 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint) return cachedCred; } - private static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions) + private static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, bool newCredentialsRequested = false) { - SafeFreeCredentials? cred = SslStreamPal.AcquireCredentialsHandle(sslAuthenticationOptions); + SafeFreeCredentials? cred = SslStreamPal.AcquireCredentialsHandle(sslAuthenticationOptions, newCredentialsRequested); if (sslAuthenticationOptions.CertificateContext != null && cred != null) { @@ -761,16 +758,6 @@ internal ProtocolToken NextMessage(ReadOnlySpan incomingBuffer) { byte[]? nextmsg = null; SecurityStatusPal status = GenerateToken(incomingBuffer, ref nextmsg); - - if (!_sslAuthenticationOptions.IsServer && status.ErrorCode == SecurityStatusPalErrorCode.CredentialsNeeded) - { - if (NetEventSource.Log.IsEnabled()) - NetEventSource.Info(this, "NextMessage() returned SecurityStatusPal.CredentialsNeeded"); - - SetRefreshCredentialNeeded(); - status = GenerateToken(incomingBuffer, ref nextmsg); - } - ProtocolToken token = new ProtocolToken(nextmsg, status); if (NetEventSource.Log.IsEnabled()) @@ -806,6 +793,10 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte bool sendTrustList = false; byte[]? thumbPrint = null; + // We need to try get credentials at the beginning. + // _credentialsHandle may be always null on some platforms but + // _securityContext will be allocated on first call. + bool refreshCredentialNeeded = _securityContext == null; // // Looping through ASC or ISC with potentially cached credential that could have been // already disposed from a different thread before ISC or ASC dir increment a cred ref count. @@ -815,7 +806,7 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte do { thumbPrint = null; - if (_refreshCredentialNeeded) + if (refreshCredentialNeeded) { cachedCreds = _sslAuthenticationOptions.IsServer ? AcquireServerCredentials(ref thumbPrint) @@ -865,15 +856,31 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte _sslAuthenticationOptions, SelectClientCertificate ); + + if (status.ErrorCode == SecurityStatusPalErrorCode.CredentialsNeeded) + { + refreshCredentialNeeded = true; + cachedCreds = AcquireClientCredentials(ref thumbPrint, newCredentialsRequested: true); + + if (NetEventSource.Log.IsEnabled()) + NetEventSource.Info(this, "InitializeSecurityContext() returned 'CredentialsNeeded'."); + + status = SslStreamPal.InitializeSecurityContext( + ref _credentialsHandle!, + ref _securityContext, + _sslAuthenticationOptions.TargetHost, + inputBuffer, + ref result, + _sslAuthenticationOptions, + SelectClientCertificate); + } } } while (cachedCreds && _credentialsHandle == null); } finally { - if (_refreshCredentialNeeded) + if (refreshCredentialNeeded) { - _refreshCredentialNeeded = false; - // // Assuming the ISC or ASC has referenced the credential, // we want to call dispose so to decrement the effective ref count. diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs index 53b8f5ce77dc3..a7a35fb98c4fe 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs @@ -66,7 +66,7 @@ public static SecurityStatusPal Renegotiate( throw new PlatformNotSupportedException(); } - public static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions) + public static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions _1, bool _2) { return null; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs index 1acfe5468e07a..efa33eaef46b9 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs @@ -117,7 +117,7 @@ public static SecurityStatusPal Renegotiate( throw new PlatformNotSupportedException(); } - public static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions) + public static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions _1, bool _2) { return null; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs index ee2fd42fb69b0..c894dad2abdf7 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs @@ -56,7 +56,7 @@ public static SecurityStatusPal InitializeSecurityContext( return HandshakeInternal(ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions, clientCertificateSelectionCallback); } - public static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions) + public static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions _1, bool _2) { return null; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index 2a1a3d3174409..f96f5f9a19f66 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -171,7 +171,7 @@ public static SecurityStatusPal Renegotiate( return status; } - public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions) + public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, bool newCredentialsRequested) { try { @@ -191,6 +191,16 @@ public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOpti AttachCertificateStore(cred, certificateContext.Trust._store!); } + // Windows can fail to get local credentials in case of TLS Resume. + // We will store associated certificate in credentials and use it in case + // of TLS resume. It will be disposed when the credentials are. + if (newCredentialsRequested && sslAuthenticationOptions.CertificateContext != null) + { + SafeFreeCredential_SECURITY handle = (SafeFreeCredential_SECURITY)cred; + // We need to create copy to avoid Disposal issue. + handle.LocalCertificate = new X509Certificate2(sslAuthenticationOptions.CertificateContext.Certificate); + } + return cred; } catch (Win32Exception e) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs index dbfd825d84c65..758c72f3c638e 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs @@ -143,6 +143,168 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + public async Task SslStream_ResumedSessionsClientCollection_IsMutuallyAuthenticatedCorrect( + SslProtocols protocol) + { + var clientOptions = new SslClientAuthenticationOptions + { + EnabledSslProtocols = protocol, + RemoteCertificateValidationCallback = delegate { return true; }, + TargetHost = Guid.NewGuid().ToString("N") + }; + + // Create options with certificate context so TLS resume is possible on Linux + var serverOptions = new SslServerAuthenticationOptions + { + ClientCertificateRequired = true, + ServerCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, null), + RemoteCertificateValidationCallback = delegate { return true; }, + EnabledSslProtocols = protocol + }; + + for (int i = 0; i < 5; i++) + { + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + bool expectMutualAuthentication = (i % 2) == 0; + + clientOptions.ClientCertificates = expectMutualAuthentication ? new X509CertificateCollection() { _clientCertificate } : null; + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + // mutual authentication should only be set if client set certificate + Assert.Equal(expectMutualAuthentication, server.IsMutuallyAuthenticated); + Assert.Equal(expectMutualAuthentication, client.IsMutuallyAuthenticated); + + if (expectMutualAuthentication) + { + Assert.NotNull(server.RemoteCertificate); + } + else + { + Assert.Null(server.RemoteCertificate); + } + }; + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + public async Task SslStream_ResumedSessionsCallbackSet_IsMutuallyAuthenticatedCorrect( + SslProtocols protocol) + { + var clientOptions = new SslClientAuthenticationOptions + { + EnabledSslProtocols = protocol, + RemoteCertificateValidationCallback = delegate { return true; }, + TargetHost = Guid.NewGuid().ToString("N") + }; + + // Create options with certificate context so TLS resume is possible on Linux + var serverOptions = new SslServerAuthenticationOptions + { + ClientCertificateRequired = true, + ServerCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, null), + RemoteCertificateValidationCallback = delegate { return true; }, + EnabledSslProtocols = protocol + }; + + for (int i = 0; i < 5; i++) + { + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + bool expectMutualAuthentication = (i % 2) == 0; + + clientOptions.LocalCertificateSelectionCallback = (s, t, l, r, a) => + { + return expectMutualAuthentication ? _clientCertificate : null; + }; + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + // mutual authentication should only be set if client set certificate + Assert.Equal(expectMutualAuthentication, server.IsMutuallyAuthenticated); + Assert.Equal(expectMutualAuthentication, client.IsMutuallyAuthenticated); + + if (expectMutualAuthentication) + { + Assert.NotNull(server.RemoteCertificate); + } + else + { + Assert.Null(server.RemoteCertificate); + } + }; + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + public async Task SslStream_ResumedSessionsCallbackMaybeSet_IsMutuallyAuthenticatedCorrect( + SslProtocols protocol) + { + var clientOptions = new SslClientAuthenticationOptions + { + EnabledSslProtocols = protocol, + RemoteCertificateValidationCallback = delegate { return true; }, + TargetHost = Guid.NewGuid().ToString("N") + }; + + // Create options with certificate context so TLS resume is possible on Linux + var serverOptions = new SslServerAuthenticationOptions + { + ClientCertificateRequired = true, + ServerCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, null), + RemoteCertificateValidationCallback = delegate { return true; }, + EnabledSslProtocols = protocol + }; + + for (int i = 0; i < 5; i++) + { + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + bool expectMutualAuthentication = (i % 2) == 0; + + if (expectMutualAuthentication) + { + clientOptions.LocalCertificateSelectionCallback = (s, t, l, r, a) => _clientCertificate; + } + else + { + clientOptions.LocalCertificateSelectionCallback = null; + } + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + // mutual authentication should only be set if client set certificate + Assert.Equal(expectMutualAuthentication, server.IsMutuallyAuthenticated); + Assert.Equal(expectMutualAuthentication, client.IsMutuallyAuthenticated); + + if (expectMutualAuthentication) + { + Assert.NotNull(server.RemoteCertificate); + } + else + { + Assert.Null(server.RemoteCertificate); + } + }; + } + } + private static bool AllowAnyCertificate( object sender, X509Certificate certificate,