diff --git a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs index adc60fdf4a89a..054985f64b8e6 100644 --- a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs @@ -8,6 +8,7 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.IO; +using System.Net.Security; using System.Net.Sockets; using System.Net.WebSockets; using System.Threading; @@ -174,6 +175,9 @@ public class GenericLoopbackOptions SslProtocols.Tls12; public int ListenBacklog { get; set; } = 1; +#if !NETSTANDARD2_0 && !NETFRAMEWORK + public SslStreamCertificateContext? CertificateContext { get; set; } +#endif } public struct HttpHeaderData diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 980c88b6587df..3506102194551 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -33,6 +33,12 @@ public sealed partial class LoopbackServer : GenericLoopbackServer, IDisposable public LoopbackServer(Options options = null) { _options = options ??= new Options(); +#if !NETSTANDARD2_0 && !NETFRAMEWORK + if (_options.UseSsl && _options.CertificateContext == null) + { + _options.CertificateContext = SslStreamCertificateContext.Create(_options.Certificate ?? Configuration.Certificates.GetServerCertificate(), null); + } +#endif } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -471,6 +477,16 @@ public static async Task CreateAsync(SocketWrapper socket, Stream st if (httpOptions.UseSsl) { var sslStream = new SslStream(stream, false, delegate { return true; }); +#if !NETFRAMEWORK + SslServerAuthenticationOptions sslOptions = new SslServerAuthenticationOptions() + { + EnabledSslProtocols = httpOptions.SslProtocols, + ServerCertificateContext = httpOptions.CertificateContext ?? SslStreamCertificateContext.Create(Configuration.Certificates.GetServerCertificate(), null), + ClientCertificateRequired = true, + }; + + await sslStream.AuthenticateAsServerAsync(sslOptions, default).ConfigureAwait(false); +#else using (X509Certificate2 cert = httpOptions.Certificate ?? Configuration.Certificates.GetServerCertificate()) { await sslStream.AuthenticateAsServerAsync( @@ -479,6 +495,7 @@ await sslStream.AuthenticateAsServerAsync( enabledSslProtocols: httpOptions.SslProtocols, checkCertificateRevocation: false).ConfigureAwait(false); } +#endif stream = sslStream; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs index 411aade42be6f..8b9919906076e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs @@ -107,6 +107,8 @@ public bool AllowAutoRedirect } } + internal ClientCertificateOption ClientCertificateOptions; + public const bool SupportsAutomaticDecompression = false; public const bool SupportsProxy = false; public const bool SupportsRedirectConfiguration = true; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs index 9e4bf892ceb1a..cd88e7930918d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs @@ -28,8 +28,6 @@ public partial class HttpClientHandler : HttpMessageHandler private HttpHandlerType Handler => _underlyingHandler; #endif - private ClientCertificateOption _clientCertificateOptions; - private volatile bool _disposed; public HttpClientHandler() @@ -207,27 +205,21 @@ public int MaxResponseHeadersLength public ClientCertificateOption ClientCertificateOptions { - get => _clientCertificateOptions; + get => _underlyingHandler.ClientCertificateOptions; set { switch (value) { case ClientCertificateOption.Manual: -#if TARGET_BROWSER - _clientCertificateOptions = value; -#else +#if !TARGET_BROWSER ThrowForModifiedManagedSslOptionsIfStarted(); - _clientCertificateOptions = value; _underlyingHandler.SslOptions.LocalCertificateSelectionCallback = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => CertificateHelper.GetEligibleClientCertificate(_underlyingHandler.SslOptions.ClientCertificates)!; #endif break; case ClientCertificateOption.Automatic: -#if TARGET_BROWSER - _clientCertificateOptions = value; -#else +#if !TARGET_BROWSER ThrowForModifiedManagedSslOptionsIfStarted(); - _clientCertificateOptions = value; _underlyingHandler.SslOptions.LocalCertificateSelectionCallback = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => CertificateHelper.GetEligibleClientCertificate()!; #endif break; @@ -235,6 +227,7 @@ public ClientCertificateOption ClientCertificateOptions default: throw new ArgumentOutOfRangeException(nameof(value)); } + _underlyingHandler.ClientCertificateOptions = value; } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index c8ea7b6148410..a460307b99c04 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -327,6 +327,15 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection SslClientAuthenticationOptions sslOptions = poolManager.Settings._sslOptions?.ShallowClone() ?? new SslClientAuthenticationOptions(); + // This is only set if we are underlying handler for HttpClientHandler + if (poolManager.Settings._clientCertificateOptions == ClientCertificateOption.Manual && sslOptions.LocalCertificateSelectionCallback != null && + (sslOptions.ClientCertificates == null || sslOptions.ClientCertificates.Count == 0)) + { + // If we have no client certificates do not set callback when internal selection is used. + // It breaks TLS resume on Linux + sslOptions.LocalCertificateSelectionCallback = null; + } + // Set TargetHost for SNI sslOptions.TargetHost = sslHostName; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs index f651dca784b35..eaedf244ce4d4 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs @@ -63,6 +63,8 @@ internal sealed class HttpConnectionSettings // Http2 flow control settings: internal int _initialHttp2StreamWindowSize = HttpHandlerDefaults.DefaultInitialHttp2StreamWindowSize; + internal ClientCertificateOption _clientCertificateOptions; + public HttpConnectionSettings() { bool allowHttp2 = GlobalHttpSettings.SocketsHttpHandler.AllowHttp2; @@ -71,6 +73,8 @@ public HttpConnectionSettings() allowHttp3 && allowHttp2 ? HttpVersion.Version30 : allowHttp2 ? HttpVersion.Version20 : HttpVersion.Version11; + + _clientCertificateOptions = ClientCertificateOption.Automatic; } /// Creates a copy of the settings but with some values normalized to suit the implementation. @@ -117,6 +121,7 @@ public HttpConnectionSettings CloneAndNormalize() _activityHeadersPropagator = _activityHeadersPropagator, _defaultCredentialsUsedForProxy = _proxy != null && (_proxy.Credentials == CredentialCache.DefaultCredentials || _defaultProxyCredentials == CredentialCache.DefaultCredentials), _defaultCredentialsUsedForServer = _credentials == CredentialCache.DefaultCredentials, + _clientCertificateOptions = _clientCertificateOptions, }; return settings; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs index 7bc6673276d31..eae4cb5245717 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs @@ -450,6 +450,15 @@ public DistributedContextPropagator? ActivityHeadersPropagator } } + internal ClientCertificateOption ClientCertificateOptions + { + get => _settings._clientCertificateOptions; + set + { + CheckDisposedOrStarted(); + _settings._clientCertificateOptions = value; + } + } protected override void Dispose(bool disposing) { if (disposing && !_disposed) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index e365c7a72006f..9ec410e7c8605 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -11,6 +11,7 @@ using System.Net.Sockets; using System.Net.Test.Common; using System.Numerics; +using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; @@ -4288,6 +4289,51 @@ public sealed class SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http11 : { public SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http11(ITestOutputHelper output) : base(output) { } protected override Version UseVersion => HttpVersion.Version11; + +#if DEBUG + [Theory] + [InlineData(true)] + [InlineData(false)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public async Task Https_MultipleRequests_TlsResumed(bool useSocketHandler) + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + HttpMessageHandler handler = useSocketHandler ? CreateSocketsHttpHandler(allowAllCertificates: true) : CreateHttpClientHandler(); + using (HttpClient client = CreateHttpClient(handler)) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get,uri); + request.Headers.Add("Host", "foo.bar"); + request.Headers.Add("Connection", "close"); + + HttpResponseMessage response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + request = new HttpRequestMessage(HttpMethod.Get,uri); + request.Headers.Add("Host", "foo.bar"); + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + }, + async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(); + await server.AcceptConnectionAsync(async connection => + { + SslStream ssl = (SslStream)connection.Stream; + object connectionInfo = typeof(SslStream).GetField( + "_connectionInfo", + BindingFlags.Instance | BindingFlags.NonPublic).GetValue(ssl); + + bool resumed = (bool)connectionInfo.GetType().GetProperty("TlsResumed").GetValue(connectionInfo); + Assert.True(resumed); + + await connection.ReadRequestHeaderAndSendResponseAsync(); + }); + }, + new LoopbackServer.Options { UseSsl = true, SslProtocols = SslProtocols.Tls12 }); + } +#endif } [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]