diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs index 0fe2051f7beff9..4342e47a194894 100644 --- a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs @@ -87,6 +87,10 @@ internal static void SSLStreamSetTargetHost( throw new SslException(); } + [DllImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamIsLocalCertificateUsed")] + [return: MarshalAs(UnmanagedType.U1)] + internal static extern bool SSLStreamIsLocalCertificateUsed(SafeSslHandle sslHandle); + [DllImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamRequestClientAuthentication")] internal static extern void SSLStreamRequestClientAuthentication(SafeSslHandle sslHandle); 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 1694ca539a86a9..098057aca7cd5b 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -67,8 +67,10 @@ 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_UI_INFO = 0x68, // sets SEcPkgContext_UiInfo + SECPKG_ATTR_REMOTE_CERT_CHAIN = 0x67, // returns PCCERT_CONTEXT + SECPKG_ATTR_UI_INFO = 0x68, // sets SEcPkgContext_UiInfo } // These values are defined within sspi.h as ISC_REQ_*, ISC_RET_*, ASC_REQ_* and ASC_RET_*. @@ -330,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, + }; + } + [DllImport(Interop.Libraries.SspiCli, ExactSpelling = true, SetLastError = true)] internal static extern int EncryptMessage( ref CredHandle contextHandle, diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs index 95763518c96ae7..f36a072d981f95 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs @@ -426,27 +426,41 @@ public static bool QueryBlittableContextAttributes(ISSPIInterface secModule, } } - public static SafeFreeCertContext? QueryContextAttributes_SECPKG_ATTR_REMOTE_CERT_CONTEXT(ISSPIInterface secModule, SafeDeleteContext securityContext) + private static bool QueryCertContextAttribute(ISSPIInterface secModule, SafeDeleteContext securityContext, Interop.SspiCli.ContextAttribute attribute, out SafeFreeCertContext? certContext) { Span buffer = stackalloc IntPtr[1]; int errorCode = secModule.QueryContextAttributes( securityContext, - Interop.SspiCli.ContextAttribute.SECPKG_ATTR_REMOTE_CERT_CONTEXT, + attribute, MemoryMarshal.AsBytes(buffer), typeof(SafeFreeCertContext), out SafeHandle? sspiHandle); - if (errorCode != 0) + // certificate is not always present (e.g. on server when querying client certificate) + // but we still want to consider such case as a success. + bool success = errorCode == 0 || errorCode == (int)Interop.SECURITY_STATUS.NoCredentials; + + if (!success) { sspiHandle?.Dispose(); + sspiHandle = null; if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, $"ERROR = {ErrorDescription(errorCode)}"); - return null; } - var result = (SafeFreeCertContext)sspiHandle!; - return result; + certContext = sspiHandle as SafeFreeCertContext; + return success; } + public static bool QueryContextAttributes_SECPKG_ATTR_REMOTE_CERT_CONTEXT(ISSPIInterface secModule, SafeDeleteContext securityContext, out SafeFreeCertContext? certContext) + => QueryCertContextAttribute(secModule, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_REMOTE_CERT_CONTEXT, out certContext); + + public static bool QueryContextAttributes_SECPKG_ATTR_LOCAL_CERT_CONTEXT(ISSPIInterface secModule, SafeDeleteContext securityContext, out SafeFreeCertContext? certContext) + => QueryCertContextAttribute(secModule, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_LOCAL_CERT_CONTEXT, out certContext); + + public static bool QueryContextAttributes_SECPKG_ATTR_REMOTE_CERT_CHAIN(ISSPIInterface secModule, SafeDeleteContext securityContext, out SafeFreeCertContext? certContext) + => QueryCertContextAttribute(secModule, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_REMOTE_CERT_CHAIN, out certContext); + + public static bool QueryContextAttributes_SECPKG_ATTR_ISSUER_LIST_EX(ISSPIInterface secModule, SafeDeleteContext securityContext, ref Interop.SspiCli.SecPkgContext_IssuerListInfoEx ctx, out SafeHandle? sspiHandle) { Span buffer = diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs index 43b718524d1ca9..7fe89376e831ba 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 @@ -320,10 +321,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 778ecefdb26331..143310a8d3a0cd 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 @@ -105,6 +105,16 @@ internal static SslPolicyErrors VerifyCertificateProperties( return cert; } + // Check if the local certificate has been sent to the peer during the handshake. + internal static bool IsLocalCertificateUsed(SafeFreeCredentials? _, SafeDeleteContext? securityContext) + { + SafeSslHandle? sslContext = ((SafeDeleteSslContext?)securityContext)?.SslContext; + if (sslContext == null) + return false; + + return Interop.AndroidCrypto.SSLStreamIsLocalCertificateUsed(sslContext); + } + // // Used only by client SSL code, never returns 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 f1fef45e081264..f59bcd364c54ef 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 @@ -114,6 +114,11 @@ internal static SslPolicyErrors VerifyCertificateProperties( return result; } + // This is only called when we selected local client certificate. + // Currently this is only when Apple crypto asked for it. + 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 040111c8042b05..00a7db7b6b0c46 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 @@ -110,6 +110,11 @@ internal static SslPolicyErrors VerifyCertificateProperties( return result; } + + // 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(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 05ec3e1cce0975..76a43827455061 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 { @@ -48,7 +49,21 @@ internal static SslPolicyErrors VerifyCertificateProperties( SafeFreeCertContext? remoteContext = null; try { - remoteContext = SSPIWrapper.QueryContextAttributes_SECPKG_ATTR_REMOTE_CERT_CONTEXT(GlobalSSPI.SSPISecureChannel, securityContext); + // SECPKG_ATTR_REMOTE_CERT_CONTEXT will not succeed before TLS handshake completes. Inside the handshake, + // we need to use (more expensive) SECPKG_ATTR_REMOTE_CERT_CHAIN. That one may be unsupported on older + // versions of windows. In that case, we have no option than to return null. + // + // We can use retrieveCollection to distinguish between in-handshake and after-handshake calls, because + // the collection is retrieved for cert validation purposes after the handshake completes. + if (retrieveCollection) // handshake completed + { + SSPIWrapper.QueryContextAttributes_SECPKG_ATTR_REMOTE_CERT_CONTEXT(GlobalSSPI.SSPISecureChannel, securityContext, out remoteContext); + } + else // in handshake + { + SSPIWrapper.QueryContextAttributes_SECPKG_ATTR_REMOTE_CERT_CHAIN(GlobalSSPI.SSPISecureChannel, securityContext, out remoteContext); + } + if (remoteContext != null && !remoteContext.IsInvalid) { result = new X509Certificate2(remoteContext.DangerousGetHandle()); @@ -71,6 +86,43 @@ internal static SslPolicyErrors VerifyCertificateProperties( return result; } + // Check that local certificate was used by schannel. + 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 + { + if (SSPIWrapper.QueryContextAttributes_SECPKG_ATTR_LOCAL_CERT_CONTEXT(GlobalSSPI.SSPISecureChannel, securityContext, out localContext) && + localContext != null) + { + return !localContext.IsInvalid; + } + } + finally + { + localContext?.Dispose(); + } + + // Some older Windows do not support that. This is only called when client certificate was provided + // so assume it was for a reason. + return true; + } + // // Used only by client SSL code, never returns null. // diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs index 05806fedd9cbdc..288443bd90941a 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs @@ -76,7 +76,12 @@ internal X509Certificate? LocalClientCertificate { get { - return _selectedClientCertificate; + if (_selectedClientCertificate != null && CertificateValidationPal.IsLocalCertificateUsed(_credentialsHandle, _securityContext!)) + { + return _selectedClientCertificate; + } + + return null; } } @@ -332,7 +337,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. X509Certificate? clientCertificate = null; // This is a candidate that can come from the user callback or be guessed when targeting a session restart. @@ -604,7 +609,7 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint) } _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext, - _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); + _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer, newCredentialsRequested); thumbPrint = guessedThumbPrint; // Delay until here in case something above threw. _selectedClientCertificate = clientCertificate; @@ -711,7 +716,7 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint) else { _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext, _sslAuthenticationOptions.EnabledSslProtocols, - _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); + _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer, newCredentialsRequested: false); thumbPrint = guessedThumbPrint; } @@ -804,6 +809,23 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte inputBuffer, ref result, _sslAuthenticationOptions); + + 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); + } } } while (cachedCreds && _credentialsHandle == null); } 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 27c854c99168e7..d5155b2cfa3964 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 @@ -54,7 +54,8 @@ public static SafeFreeCredentials AcquireCredentialsHandle( SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, - bool isServer) + bool isServer, + bool newCredentialsRequested = false) { return new SafeFreeSslCredentials(certificateContext, protocols, policy); } 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 0414082781c1e8..455ee4b5bb3985 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 @@ -61,7 +61,8 @@ public static SafeFreeCredentials AcquireCredentialsHandle( SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, - bool isServer) + bool isServer, + bool newCredentialsRequested = false) { return new SafeFreeSslCredentials(certificateContext, protocols, policy); } 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 c3b2e7e291e89f..acf1f11fa3040b 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 @@ -37,7 +37,7 @@ public static SecurityStatusPal InitializeSecurityContext(ref SafeFreeCredential } public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, - SslProtocols protocols, EncryptionPolicy policy, bool isServer) + SslProtocols protocols, EncryptionPolicy policy, bool isServer, bool newCredentialsRequested = false) { return new SafeFreeSslCredentials(certificateContext?.Certificate, protocols, policy); } 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 85d176aaa88a68..8f6b6462da1df3 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 @@ -120,7 +120,7 @@ public static SecurityStatusPal Renegotiate(ref SafeFreeCredentials? credentials return status; } - public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer) + public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer, bool newCredentialsRequested) { try { @@ -133,6 +133,16 @@ public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateC 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 && certificateContext != null) + { + SafeFreeCredential_SECURITY handle = (SafeFreeCredential_SECURITY)cred; + // We need to create copy to avoid Disposal issue. + handle.LocalCertificate = new X509Certificate2(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 new file mode 100644 index 00000000000000..bb753a9c8bbe86 --- /dev/null +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs @@ -0,0 +1,400 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using System.Net.Test.Common; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +using Xunit; +using System.Runtime.InteropServices; + +namespace System.Net.Security.Tests +{ + using Configuration = System.Net.Test.Common.Configuration; + + [PlatformSpecific(TestPlatforms.Windows)] + public class SslStreamMutualAuthenticationTest : IDisposable + { + private readonly X509Certificate2 _clientCertificate; + private readonly X509Certificate2 _serverCertificate; + private readonly X509Certificate2 _selfSignedCertificate; + + public SslStreamMutualAuthenticationTest() + { + _serverCertificate = Configuration.Certificates.GetServerCertificate(); + _clientCertificate = Configuration.Certificates.GetClientCertificate(); + _selfSignedCertificate = Configuration.Certificates.GetSelfSignedServerCertificate(); + } + + public void Dispose() + { + _serverCertificate.Dispose(); + _clientCertificate.Dispose(); + } + + public enum ClientCertSource + { + ClientCertificate, + SelectionCallback, + } + + public static TheoryData CertSourceData() + { + TheoryData data = new(); + + foreach (var source in Enum.GetValues()) + { + data.Add(source); + } + + return data; + } + + + public static TheoryData BoolAndCertSourceData() + { + TheoryData data = new(); + + foreach (var source in Enum.GetValues()) + { + data.Add(true, source); + data.Add(false, source); + } + + return data; + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))] + [MemberData(nameof(BoolAndCertSourceData))] + public async Task SslStream_RequireClientCert_IsMutuallyAuthenticated_ReturnsTrue(bool clientCertificateRequired, ClientCertSource certSource) + { + (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); + using (var client = new SslStream(stream1, false, AllowAnyCertificate)) + using (var server = new SslStream(stream2, false, AllowAnyCertificate)) + { + var clientOptions = new SslClientAuthenticationOptions + { + TargetHost = Guid.NewGuid().ToString("N") + }; + + switch (certSource) + { + case ClientCertSource.ClientCertificate: + clientOptions.ClientCertificates = new X509CertificateCollection() { _clientCertificate }; + break; + case ClientCertSource.SelectionCallback: + clientOptions.LocalCertificateSelectionCallback = ClientCertSelectionCallback; + break; + } + + Task t2 = client.AuthenticateAsClientAsync(clientOptions); + Task t1 = server.AuthenticateAsServerAsync(new SslServerAuthenticationOptions + { + ServerCertificate = _serverCertificate, + ClientCertificateRequired = clientCertificateRequired + }); + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); + + if (clientCertificateRequired) + { + Assert.True(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.True(server.IsMutuallyAuthenticated, "server.IsMutuallyAuthenticated"); + } + else + { + // Even though the certificate was provided, it was not requested by the server and thus the client + // was not authenticated. + Assert.False(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.False(server.IsMutuallyAuthenticated, "server.IsMutuallyAuthenticated"); + } + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + public async Task SslStream_CachedCredentials_IsMutuallyAuthenticatedCorrect( + SslProtocols protocol) + { + var clientOptions = new SslClientAuthenticationOptions + { + ClientCertificates = new X509CertificateCollection() { _clientCertificate }, + EnabledSslProtocols = protocol, + RemoteCertificateValidationCallback = delegate { return true; }, + TargetHost = Guid.NewGuid().ToString("N") + }; + + for (int i = 0; i < 5; i++) + { + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + bool expectMutualAuthentication = (i % 2) == 0; + + var serverOptions = new SslServerAuthenticationOptions + { + ClientCertificateRequired = expectMutualAuthentication, + ServerCertificate = expectMutualAuthentication ? _serverCertificate : _selfSignedCertificate, + RemoteCertificateValidationCallback = delegate { return true; }, + EnabledSslProtocols = protocol + }; + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + // mutual authentication should only be set if server required client cert + Assert.Equal(expectMutualAuthentication, server.IsMutuallyAuthenticated); + Assert.Equal(expectMutualAuthentication, client.IsMutuallyAuthenticated); + }; + } + } + + [ConditionalTheory(typeof(TestConfiguration), nameof(TestConfiguration.SupportsRenegotiation))] + [MemberData(nameof(CertSourceData))] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public async Task SslStream_NegotiateClientCertificate_IsMutuallyAuthenticatedCorrect(ClientCertSource certSource) + { + SslStreamCertificateContext context = SslStreamCertificateContext.Create(_serverCertificate, null); + var clientOptions = new SslClientAuthenticationOptions + { + TargetHost = Guid.NewGuid().ToString("N") + }; + + for (int round = 0; round < 3; round++) + { + (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); + using (var client = new SslStream(stream1, false, AllowAnyCertificate)) + using (var server = new SslStream(stream2, false, AllowAnyCertificate)) + { + + switch (certSource) + { + case ClientCertSource.ClientCertificate: + clientOptions.ClientCertificates = new X509CertificateCollection() { _clientCertificate }; + break; + case ClientCertSource.SelectionCallback: + clientOptions.LocalCertificateSelectionCallback = ClientCertSelectionCallback; + break; + } + + Task t2 = client.AuthenticateAsClientAsync(clientOptions); + Task t1 = server.AuthenticateAsServerAsync(new SslServerAuthenticationOptions + { + ServerCertificateContext = context, + ClientCertificateRequired = false, + EnabledSslProtocols = SslProtocols.Tls12, + + }); + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); + + if (round >= 0 && server.RemoteCertificate != null) + { + // TLS resumed + Assert.True(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.True(server.IsMutuallyAuthenticated, "server.IsMutuallyAuthenticated"); + continue; + } + + Assert.False(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.False(server.IsMutuallyAuthenticated, "server.IsMutuallyAuthenticated"); + + var t = client.ReadAsync(new byte[1]); + await server.NegotiateClientCertificateAsync(); + Assert.NotNull(server.RemoteCertificate); + await server.WriteAsync(new byte[1]); + await t; + + Assert.NotNull(server.RemoteCertificate); + Assert.True(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.True(server.IsMutuallyAuthenticated, "server.IsMutuallyAuthenticated"); + } + } + } + + [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, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + return true; + } + + private X509Certificate ClientCertSelectionCallback( + object sender, + string targetHost, + X509CertificateCollection localCertificates, + X509Certificate remoteCertificate, + string[] acceptableIssuers) + { + return _clientCertificate; + } + } +} diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs index b9917bee216dc4..f2b6bb575bd6c7 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs @@ -72,12 +72,6 @@ public static IEnumerable SslStream_StreamToStream_Authentication_Succ [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "X509 certificate store is not supported on iOS or tvOS.")] public async Task SslStream_StreamToStream_Authentication_Success(X509Certificate serverCert = null, X509Certificate clientCert = null) { - if (PlatformDetection.IsWindows10Version20348OrGreater) - { - // [ActiveIssue("https://github.com/dotnet/runtime/issues/58927")] - throw new SkipTestException("Unstable on Windows 11"); - } - (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); using (var client = new SslStream(stream1, false, AllowAnyServerCertificate)) using (var server = new SslStream(stream2, false, delegate { return true; })) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSystemDefaultsTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSystemDefaultsTest.cs index 9175833c8e4702..b7f70fcecd57c5 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSystemDefaultsTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSystemDefaultsTest.cs @@ -76,12 +76,6 @@ public static IEnumerable OneOrBothUseDefaulData() [MemberData(nameof(OneOrBothUseDefaulData))] public async Task ClientAndServer_OneOrBothUseDefault_Ok(SslProtocols? clientProtocols, SslProtocols? serverProtocols) { - if (PlatformDetection.IsWindows10Version20348OrGreater) - { - // [ActiveIssue("https://github.com/dotnet/runtime/issues/58927")] - throw new SkipTestException("Unstable on Windows 11"); - } - using (X509Certificate2 serverCertificate = Configuration.Certificates.GetServerCertificate()) using (X509Certificate2 clientCertificate = Configuration.Certificates.GetClientCertificate()) { diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index 3dc537ad91deb9..f92663083b5271 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -27,6 +27,7 @@ +